Пример #1
0
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]
Пример #2
0
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)
Пример #3
0
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)
Пример #4
0
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)
Пример #5
0
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
Пример #6
0
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
Пример #7
0
def test_passing_simple_list():
    series = MultiSeries(ys=[1, 2, 3])
    options = validate_and_transform_options(series=series)
    assert options.interactive == False
Пример #8
0
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]})
Пример #9
0
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]
Пример #10
0
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]
Пример #11
0
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
Пример #12
0
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
Пример #13
0
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]