def test_length_and_shape_when_passing_multiple_lists(): ys = [[1, 222, 3, -3.14, 0], [1, 222, 3, -3.14, 0, 4, 4, 4], [1]] xs = [[1000, 222, 3, -314, 0], [0, 2222, 3, -3.14, 0, 4, 4, 4], [1]] series = MultiSeries(xs=xs, ys=ys) assert len(series) == 3 assert series.shape() == [5, 8, 1]
def test_min_and_max(): ys = [[1, 222, 3, -3.14, 0], [1, 222, 3, -3.14, 0]] xs = [[1000, 222, 3, -314, 0], [0, 2222, 3, -3.14, 0]] series = MultiSeries(xs=xs, ys=ys) assert series.x_min() == -314 assert series.x_max() == 2222 assert series.y_min() == -3.14 assert series.y_max() == 222
def histogram( xs: Any, bins: int = 20, bins_min: Optional[float] = None, bins_max: Optional[float] = None, **kwargs, ) -> None: """ Plot a histogram to the terminal. Parameters: - `xs` are the values of the points to plot. This parameter is mandatory and can either be a list or a list of lists, or the equivalent NumPy array. - Any additional keyword arguments are passed to the `uniplot.options.Options` class. """ # HACK Use the `MultiSeries` constructor to cast values to uniform format multi_series = MultiSeries(ys=xs) # Histograms usually make sense only with lines kwargs["lines"] = kwargs.get("lines", True) bins_min = bins_min or multi_series.y_min() bins_max = bins_max or multi_series.y_max() range = bins_max - bins_min if range > 0: bins_min = bins_min - 0.1 * range bins_max = bins_max + 0.1 * range xs_histo_series = [] ys_histo_series = [] for s in multi_series.ys: hist, bin_edges = np.histogram(s, bins=bins, range=(bins_min, bins_max)) # Draw vertical and horizontal lines to connect points xs_here = np.zeros(1 + 2 * bins + 1) ys_here = np.zeros(1 + 2 * bins + 1) xs_here[0] = bin_edges[0] xs_here[1::2] = bin_edges xs_here[2::2] = bin_edges[1:] ys_here[1:-1:2] = hist ys_here[2:-1:2] = hist xs_histo_series.append(xs_here) ys_histo_series.append(ys_here) plot(xs=xs_histo_series, ys=ys_histo_series, **kwargs)
def plot_to_string(ys: Any, xs: Optional[Any] = None, **kwargs) -> List[str]: """ Same as `plot`, but the return type is a list of strings. Ignores the `interactive` option. Can be used to integrate uniplot in other applications, or if the output is desired to be not stdout. """ series: MultiSeries = MultiSeries(xs=xs, ys=ys) options: Options = validate_and_transform_options(series=series, kwargs=kwargs) header = _generate_header(options) ( x_axis_labels, y_axis_labels, pixel_character_matrix, ) = _generate_body_raw_elements(series, options) body = _generate_body(x_axis_labels, y_axis_labels, pixel_character_matrix, options) return header + body
def test_passing_simple_list(): series = MultiSeries(ys=[1, 2, 3]) options = validate_and_transform_options(series=series) assert options.interactive == False
def test_invalid_lines_option_with_multiple_lists(): series = MultiSeries(ys=[[1, 2, 3], [100, 1000, 10000]]) with pytest.raises(ValueError): validate_and_transform_options(series=series, kwargs={"lines": [False]})
def test_lines_option_with_multiple_lists(): series = MultiSeries(ys=[[1, 2, 3], [100, 1000, 10000]]) options = validate_and_transform_options( series=series, kwargs={"lines": [False, True]} ) assert options.lines == [False, True]
def test_lines_option_with_simple_list(): series = MultiSeries(ys=[1, 2, 3]) options = validate_and_transform_options(series=series, kwargs={"lines": True}) assert options.lines == [True]
def plot(ys: Any, xs: Optional[Any] = None, **kwargs) -> None: """ 2D scatter dot plot on the terminal. Parameters: - `ys` are the y coordinates of the points to plot. This parameter is mandatory and can either be a list or a list of lists, or the equivalent NumPy array. - `xs` are the x coordinates of the points to plot. This parameter is optional and can either be a `None` or of the same shape as `ys`. - Any additional keyword arguments are passed to the `uniplot.options.Options` class. """ series: MultiSeries = MultiSeries(xs=xs, ys=ys) options: Options = validate_and_transform_options(series=series, kwargs=kwargs) # Print header for line in _generate_header(options): print(line) # Main loop for interactive mode. Will only be executed once when not in interactive # mode. continue_looping: bool = True loop_iteration: int = 0 while continue_looping: # Make sure we stop after first iteration when not in interactive mode if not options.interactive: continue_looping = False ( x_axis_labels, y_axis_labels, pixel_character_matrix, ) = _generate_body_raw_elements(series, options) # Delete plot before we re-draw if loop_iteration > 0: nr_lines_to_erase = options.height + 4 if options.legend_labels is not None: nr_lines_to_erase += len(options.legend_labels) elements.erase_previous_lines(nr_lines_to_erase) for line in _generate_body(x_axis_labels, y_axis_labels, pixel_character_matrix, options): print(line) if options.interactive: print("Move h/j/k/l, zoom u/n, or r to reset. ESC/q to quit") key_pressed = getch().lower() if key_pressed == "h": options.shift_view_left() elif key_pressed == "l": options.shift_view_right() elif key_pressed == "j": options.shift_view_down() elif key_pressed == "k": options.shift_view_up() elif key_pressed == "u": options.zoom_in() elif key_pressed == "n": options.zoom_out() elif key_pressed == "r": options.reset_view() elif key_pressed in ["q", "\x1b"]: # q and Escape will end interactive mode continue_looping = False loop_iteration += 1
def plot(ys: Any, xs: Optional[Any] = None, **kwargs) -> None: """ 2D scatter dot plot on the terminal. Parameters: - `ys` are the y coordinates of the points to plot. This parameter is mandatory and can either be a list or a list of lists, or the equivalent NumPy array. - `xs` are the x coordinates of the points to plot. This parameter is optional and can either be a `None` or of the same shape as `ys`. - Any additional keyword arguments are passed to the `uniplot.options.Options` class. """ series = MultiSeries(xs=xs, ys=ys) options = validate_and_transform_options(series=series, kwargs=kwargs) # Print title if options.title is not None: print(elements.plot_title(options.title, width=options.width)) # Main loop for interactive mode. Will only be executed once when not in interactive # mode. continue_looping: bool = True loop_iteration: int = 0 while continue_looping: # Make sure we stop after first iteration when not in interactive mode if not options.interactive: continue_looping = False # Prepare y axis labels y_axis_labels = ["-error generating labels, sorry-"] * options.height y_axis_label_set = extended_talbot_labels( x_min=options.y_min, x_max=options.y_max, available_space=options.height, vertical_direction=True, ) if y_axis_label_set is not None: y_axis_labels = y_axis_label_set.render() # Prepare x axis labels x_axis_label_set = extended_talbot_labels( x_min=options.x_min, x_max=options.x_max, available_space=options.width, vertical_direction=False, ) x_axis_labels = "-error generating labels, sorry-" if x_axis_label_set is not None: x_axis_labels = x_axis_label_set.render()[0] # Prefare graph surface pixel_character_matrix = layer_assembly.assemble_scatter_plot( xs=series.xs, ys=series.ys, options=options) # Delete plot before we re-draw if loop_iteration > 0: nr_lines_to_erase = options.height + 4 if options.legend_labels is not None: nr_lines_to_erase += len(options.legend_labels) elements.erase_previous_lines(nr_lines_to_erase) # Print plot (double resolution) print(f"┌{'─'*options.width}┐") for i in range(options.height): row = pixel_character_matrix[i] print(f"│{''.join(row)}│ {y_axis_labels[i]}") print(f"└{'─'*options.width}┘") print(x_axis_labels) # Print legend if labels were specified # TODO Fix erase during interactive mode if options.legend_labels is not None: print(elements.legend(options.legend_labels, width=options.width)) if options.interactive: print("Move h/j/k/l, zoom u/n, or r to reset. ESC/q to quit") key_pressed = getch().lower() # TODO Move all of the below to the `Options` class if key_pressed == "h": # Left step = 0.1 * (options.x_max - options.x_min) options.x_min = options.x_min - step options.x_max = options.x_max - step elif key_pressed == "l": # Right step = 0.1 * (options.x_max - options.x_min) options.x_min = options.x_min + step options.x_max = options.x_max + step elif key_pressed == "j": # Up step = 0.1 * (options.y_max - options.y_min) options.y_min = options.y_min - step options.y_max = options.y_max - step elif key_pressed == "k": # Down step = 0.1 * (options.y_max - options.y_min) options.y_min = options.y_min + step options.y_max = options.y_max + step elif key_pressed == "u": # Zoom in step = 0.1 * (options.x_max - options.x_min) options.x_min = options.x_min + step options.x_max = options.x_max - step step = 0.1 * (options.y_max - options.y_min) options.y_min = options.y_min + step options.y_max = options.y_max - step elif key_pressed == "n": # Zoom out step = 0.1 * (options.x_max - options.x_min) options.x_min = options.x_min - step options.x_max = options.x_max + step step = 0.1 * (options.y_max - options.y_min) options.y_min = options.y_min - step options.y_max = options.y_max + step elif key_pressed == "r": options.reset_view() elif key_pressed in ["q", "\x1b"]: # q and Escape will end interactive mode continue_looping = False loop_iteration += 1
def test_length_and_shape_when_only_passing_single_ys_as_list(): ys = [1, 2, 3] series = MultiSeries(ys=ys) assert len(series) == 1 assert series.shape() == [3]