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 validate_and_transform_options(series: MultiSeries, kwargs: Dict = {}) -> Options: """ This will check the keyword arguments passed to the `uniplot.plot` function, will transform them and will return them in form of an `Options` object. """ # Set bounds to show all points by default kwargs["x_min"] = kwargs.get("x_min") or series.x_min() kwargs["x_max"] = kwargs.get("x_max") or ( series.x_max() + 1e-4 * (series.x_max() - series.x_min())) if float(kwargs["x_min"]) == float(kwargs["x_max"]): kwargs["x_min"] = kwargs["x_min"] - 1 kwargs["x_max"] = kwargs["x_max"] + 1 kwargs["y_min"] = kwargs.get("y_min") or series.y_min() kwargs["y_max"] = kwargs.get("y_max") or ( series.y_max() + 1e-4 * (series.y_max() - series.y_min())) if float(kwargs["y_min"]) == float(kwargs["y_max"]): kwargs["y_min"] = kwargs["y_min"] - 1 kwargs["y_max"] = kwargs["y_max"] + 1 # Make sure the length of the labels is not exceeding the number of series if kwargs.get("legend_labels") is not None: kwargs["legend_labels"] = list(kwargs["legend_labels"])[0:len(series)] if "color" not in kwargs: kwargs["color"] = len(series) > 1 return Options(**kwargs)
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 validate_and_transform_options(series: MultiSeries, kwargs: Dict = {}) -> Options: """ This will check the keyword arguments passed to the `uniplot.plot` function, will transform them and will return them in form of an `Options` object. The idea is to cast arguments into the right format to be used by the rest of the library, and to be as tolerant as possible for ease of use of the library. As a result the somewhat hacky code below should at least be confined to this function, and not spread throughout uniplot. """ # Set x bounds to show all points by default x_enlarge_delta = AUTO_WINDOW_ENLARGE_FACTOR * (series.x_max() - series.x_min()) kwargs["x_min"] = kwargs.get("x_min", series.x_min() - x_enlarge_delta) kwargs["x_max"] = kwargs.get("x_max", series.x_max() + x_enlarge_delta) # Fallback for only a single data point, or multiple with single x coordinate if float(kwargs["x_min"]) == float(kwargs["x_max"]): kwargs["x_min"] = kwargs["x_min"] - 1 kwargs["x_max"] = kwargs["x_max"] + 1 # Set y bounds to show all points by default y_enlarge_delta = AUTO_WINDOW_ENLARGE_FACTOR * (series.y_max() - series.y_min()) kwargs["y_min"] = kwargs.get("y_min", series.y_min() - y_enlarge_delta) kwargs["y_max"] = kwargs.get("y_max", series.y_max() + y_enlarge_delta) # Fallback for only a single data point, or multiple with single y coordinate if float(kwargs["y_min"]) == float(kwargs["y_max"]): kwargs["y_min"] = kwargs["y_min"] - 1 kwargs["y_max"] = kwargs["y_max"] + 1 # Make sure the length of the labels is not exceeding the number of series if kwargs.get("legend_labels") is not None: kwargs["legend_labels"] = list(kwargs["legend_labels"])[0:len(series)] # By default, enable color for multiple series, disable color for a single one kwargs["color"] = kwargs.get("color", len(series) > 1) # Set lines option for all series if not kwargs.get("lines"): # This will work for both unset lines option and `False` kwargs["lines"] = [False] * len(series) elif kwargs.get("lines") is True: # This is used to expand a single `True` kwargs["lines"] = [True] * len(series) elif len(kwargs.get("lines")) != len(series): # type: ignore raise ValueError("Invalid 'lines' option.") return Options(**kwargs)
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 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]