diff --git a/README.md b/README.md index f554af5..cbf1bcc 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,33 @@ canvas.show() ![](README_files/figure-commonmark/cell-3-output-1.png) +Use `canvas.plot(x, y)` to add data directly to a canvas. When you want +to explicitly render an already-built canvas, use `canvas.render(...)`. +The older `canvas.plot(backend=...)` spelling is still supported for +compatibility, but emits a `FutureWarning`. + +Add several lines at once with shared styling: + +``` python +canvas.plot_many( + [(x, np.sin(x)), (x, np.cos(x))], + labels=["sin(x)", "cos(x)"], + linewidth=2, +) +``` + +Common figure and axis settings can be grouped with `configure()`: + +``` python +canvas.configure( + title="Trigonometry", + xlabel="Angle", + ylabel="Value", + grid=True, + facecolor="whitesmoke", +) +``` + For Matplotlib-specific customization, pass method calls declaratively. Figure methods run once and axes methods run for every subplot, providing access to any Matplotlib API without requiring a maxplotlib @@ -149,37 +176,38 @@ parts of a mixed canvas, explicitly opt into skipping unsupported primitives: ``` python -plotly_canvas.plot(backend="plotly", allow_unsupported=True) +plotly_canvas.render(backend="plotly", allow_unsupported=True) ``` Render the same line graph directly in the terminal with the `plotext` backend: ``` python -terminal_fig = canvas.plot(backend="plotext") +terminal_fig = canvas.render(backend="plotext") print(terminal_fig.build(keep_colors=False)) ``` + Trigonometry Runtime - ┌─────────────────────────────────────────────────────────────────────────┐ - 1.00┤ ▗▄▞▀▀▀▀▀▙▄▖ │ - │ ▗▄▀▘ ▝▀▄ │ - │ ▗▞▘ ▀▄ │ - 0.67┤ ▟▀ ▀▄ │ - │ ▄▛ ▚▖ │ - 0.33┤ ▗▞ ▝▄ │ - │ ▄▀ ▚▖ │ - │▗▞▘ ▀▄ │ - 0.00┤▀ ▝▚▖ ▞│ - │ ▀▄ ▗▞▘│ - │ ▝▚ ▄▀ │ - -0.33┤ ▀▖ ▞▘ │ - │ ▝▚ ▟▀ │ - -0.67┤ ▀▄ ▄▛ │ - │ ▀▄ ▗▞▘ │ - │ ▀▄▖ ▗▄▀▘ │ - -1.00┤ ▝▀▜▄▄▄▄▄▞▀▘ │ - └┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┘ + ┌┬─────────────────┬─────────────────┬─────────────────┬─────────────────┬┐ + 1.00┼ ▞▞ sin(x) ──▗▄▞▀▀▀▀▀▙▄▖────────────┼─────────────────┼─────────────▄▄▀▀▀┤ + │ ▞▞ cos(x) ▄▀▘ │ ▝▀▄ │ │ ▄▞▀ ││ + ││ ▜▄▘ │ ▀▄ │ │ ▄▛ ││ + 0.67┼┼──────▟▀─▀▄──────┼─────────▀▄──────┼─────────────────┼──────▄▀─────────┼┤ + ││ ▄▛ ▝▚▖ │ ▚▖ │ │ ▗▞▘ ││ + 0.33┼┼──▗▞────────▀▖───┼────────────▝▄───┼─────────────────┼───▗▀────────────┼┤ + ││ ▄▀ ▝▚ │ ▚▖ │ │ ▞▘ ││ + │▗▞▘ ▀▖│ ▀▄│ │▗▀ ││ + 0.00┼▞────────────────▝▙────────────────▝▚▖────────────────▟▘────────────────▄┤ + ││ │▜▖ │▀▄ ▗▛│ ▗▞▘│ + ││ │ ▝▙ │ ▝▚ ▟▘ │ ▄▀ ││ + -0.33┼┼─────────────────┼───▚▖────────────┼───▀▖───────▗▞───┼─────────────▞▘──┼┤ + ││ │ ▝▄▖ │ ▝▚ ▗▄▘ │ ▟▀ ││ + -0.67┼┼─────────────────┼──────▀▖─────────┼──────▀▄─▗▀──────┼─────────▄▛──────┼┤ + ││ │ ▝▜▄ │ ▄▛▘ │ ▗▞▘ ││ + ││ │ ▀▄▖ │ ▗▄▀ ▀▄▖ │ ▗▄▀▘ ││ + -1.00┼┼─────────────────┼────────────▝▀▚▄▄▄▄▄▞▀▘───────▝▀▜▄▄▄▄▄▞▀▘────────────┼┤ + └┼─────────────────┼─────────────────┼─────────────────┼─────────────────┼┘ 0.0 1.6 3.1 4.7 6.3 Duration Time @@ -189,7 +217,7 @@ Or plot with the TikZ backend: canvas.show(backend="tikzfigure") ``` -![](README_files/figure-commonmark/cell-12-output-1.png) +![](README_files/figure-commonmark/cell-14-output-1.png) ### Horizontal Subplots with TikZ Backend @@ -270,7 +298,7 @@ canvas.show(backend="plotext") 1.0 1.8 3.2 5.6 10.0 y x - + ### Layers @@ -306,7 +334,7 @@ Show layer 0 only, then layers 0 and 1, then everything: canvas.show(layers=[0]) ``` -![](README_files/figure-commonmark/cell-16-output-1.png) +![](README_files/figure-commonmark/cell-18-output-1.png) (
, array([[]], dtype=object)) @@ -317,7 +345,7 @@ Show all layers: canvas.show() ``` -![](README_files/figure-commonmark/cell-17-output-1.png) +![](README_files/figure-commonmark/cell-19-output-1.png) (
, array([[]], dtype=object)) diff --git a/README.qmd b/README.qmd index 45cbd13..a27e50c 100644 --- a/README.qmd +++ b/README.qmd @@ -42,6 +42,35 @@ Plot the figure with the default (matplotlib) backend: canvas.show() ``` +Use `canvas.plot(x, y)` to add data directly to a canvas. When you want to +explicitly render an already-built canvas, use `canvas.render(...)`. The older +`canvas.plot(backend=...)` spelling is still supported for compatibility, but +emits a `FutureWarning`. + +Add several lines at once with shared styling: + +```{python} +#| output: false +canvas.plot_many( + [(x, np.sin(x)), (x, np.cos(x))], + labels=["sin(x)", "cos(x)"], + linewidth=2, +) +``` + +Common figure and axis settings can be grouped with `configure()`: + +```{python} +#| output: false +canvas.configure( + title="Trigonometry", + xlabel="Angle", + ylabel="Value", + grid=True, + facecolor="whitesmoke", +) +``` + For Matplotlib-specific customization, pass method calls declaratively. Figure methods run once and axes methods run for every subplot, providing access to any Matplotlib API without requiring a maxplotlib wrapper: @@ -149,13 +178,13 @@ of a mixed canvas, explicitly opt into skipping unsupported primitives: ```{python} #| output: false -plotly_canvas.plot(backend="plotly", allow_unsupported=True) +plotly_canvas.render(backend="plotly", allow_unsupported=True) ``` Render the same line graph directly in the terminal with the `plotext` backend: ```{python} -terminal_fig = canvas.plot(backend="plotext") +terminal_fig = canvas.render(backend="plotext") print(terminal_fig.build(keep_colors=False)) ``` diff --git a/README_files/figure-commonmark/cell-14-output-1.png b/README_files/figure-commonmark/cell-14-output-1.png index ed52dae..fee6ca2 100644 Binary files a/README_files/figure-commonmark/cell-14-output-1.png and b/README_files/figure-commonmark/cell-14-output-1.png differ diff --git a/docs/source/conf.py b/docs/source/conf.py index acbc723..b976a6d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -38,11 +38,10 @@ def setup(app): "sphinx.ext.autodoc", ] +# Execute every tutorial notebook when building the documentation. +nbsphinx_execute = "always" + templates_path = ["_templates"] -exclude_patterns = [ - # tutorial_07_tikz.ipynb requires pdflatex to render — skip during docs build - "tutorials/tutorial_07_tikz.ipynb", -] # -- Options for HTML output ------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 2924abb..e1bd3ab 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,6 +24,10 @@ documentation for details. tutorials/tutorial_07_tikz tutorials/tutorial_08_plotly tutorials/tutorial_09_plotext + tutorials/tutorial_10_matplotlib_nxm_spacing + tutorials/tutorial_11_gantt_charts + tutorials/tutorial_12_flame_charts tutorials/tutorial_13_advanced_matplotlib tutorials/tutorial_14_axis_and_layout_controls - tutorials/tutorial_tikzfigure_subplots + tutorials/tutorial_15_tikzfigure_subplots + tutorials/tutorial_16_plotext_advanced diff --git a/pyproject.toml b/pyproject.toml index e8d4b05..7250c97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "maxplotlibx" -version = "0.1.7" +version = "0.1.8" description = "A reproducible plotting module with various backends and export options." readme = "README.md" requires-python = ">=3.8" @@ -18,7 +18,7 @@ dependencies = [ "matplotlib", "pint", "plotly", - "plotext", + "plotext >= 6.0, < 7", "tikzfigure[vis]>=0.3.0", ] [project.optional-dependencies] diff --git a/src/maxplotlib/backends/plotext/figure.py b/src/maxplotlib/backends/plotext/figure.py index be07159..49c00c5 100644 --- a/src/maxplotlib/backends/plotext/figure.py +++ b/src/maxplotlib/backends/plotext/figure.py @@ -2,8 +2,9 @@ import re from pathlib import Path +from typing import Any -from plotext._figure import _figure_class +from plotext import figure as _plotext_figure _ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]") @@ -12,20 +13,115 @@ def strip_ansi(text: str) -> str: return _ANSI_ESCAPE_RE.sub("", text) -def create_plotext_figure(nrows: int = 1, ncols: int = 1) -> _figure_class: - figure = _figure_class() +class _Plotext6Axes: + """Small compatibility surface for maxplotlib's plotext renderer.""" + + def __init__(self, figure): + self._figure = figure + + def _draw(self, signal, label=None): + if label is not None: + signal.label(label) + self._figure.draw(signal) + + def plot(self, x, y, **kwargs): + signal = self._figure.signal(x, y, marker=kwargs.get("marker")) + self._draw(signal, kwargs.get("label")) + + scatter = plot + + def bar(self, *args, **kwargs): + label = kwargs.pop("label", None) + kwargs.pop("color", None) + kwargs.pop("fill", None) + signal = self._figure.bar(*args, **kwargs) + self._draw(signal, label) + + def error(self, x, y, *, xerr=None, yerr=None, color=None, label=None): + signal = self._figure.error(x, y, yerr, xerr, pixel=color) + self._draw(signal, label) + + def matrix_plot(self, data, **kwargs): + signal = self._figure.heatmap(data, symbol=kwargs.get("marker")) + self._draw(signal) + + def text(self, label, x, y, **kwargs): + # Plotext 6 keeps text colour in its marker object; the renderer only + # needs the portable text/alignment arguments here. + kwargs.pop("color", None) + signal = self._figure.text(x, y, label, **kwargs) + self._figure.draw(signal) + + def title(self, label): + self._figure.title(label) + + def xlabel(self, label): + self._figure.label(label, axis=0) + + def ylabel(self, label): + self._figure.label(label, axis=1) + + def grid(self, active=True, *_args): + self._figure.ruler("x").grid(active) + self._figure.ruler("y").grid(active) + + def xlim(self, lower=None, upper=None): + self._figure.ruler("x").lim(lower, upper) + + def ylim(self, lower=None, upper=None): + self._figure.ruler("y").lim(lower, upper) + + def xscale(self, scale): + self._figure.ruler("x").scale(scale) + + def yscale(self, scale): + self._figure.ruler("y").scale(scale) + + def xticks(self, positions=None, labels=None): + self._figure.ruler("x").ticks(positions, labels) + + def yticks(self, positions=None, labels=None): + self._figure.ruler("y").ticks(positions, labels) + + def plotsize(self, width=None, height=None): + self._figure.plot_size(width, height) + + def horizontal_line(self, position, **kwargs): + kwargs.pop("color", None) + self._figure.line(position, orientation=0, **kwargs) + + def vertical_line(self, position, **kwargs): + kwargs.pop("color", None) + self._figure.line(position, orientation=1, **kwargs) + + def subplots(self, rows=None, cols=None): + self._figure.subplots(rows, cols) + return self + + def subplot(self, row=None, col=None): + return _Plotext6Axes(self._figure.subplot(row, col)) + + def __getattr__(self, name): + return getattr(self._figure, name) + + +def create_plotext_figure(nrows: int = 1, ncols: int = 1): + # Plotext 6 exposes the figure through its public API rather than the + # removed private ``plotext._figure._figure_class``. It is a singleton, + # so reset it before handing it to maxplotlib as a fresh canvas. + figure = _Plotext6Axes(_plotext_figure.clear()) if nrows > 1 or ncols > 1: figure.subplots(nrows, ncols) return figure class PlotextFigure: - def __init__(self, figure: _figure_class, suptitle: str | None = None): + def __init__(self, figure: Any, suptitle: str | None = None): self.figure = figure self.suptitle = suptitle def build(self, keep_colors: bool = True) -> str: - output = self.figure.build() + output = str(self.figure.build()) if self.suptitle: output = f"{self.suptitle}\n{output}" return output if keep_colors else strip_ansi(output) diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 248f4b5..28a2e8f 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -1,5 +1,6 @@ import os import re +import warnings from dataclasses import dataclass from typing import Mapping @@ -17,7 +18,13 @@ from maxplotlib.backends.plotext import PlotextFigure, create_plotext_figure from maxplotlib.colors.colors import Color from maxplotlib.linestyle.linestyle import Linestyle -from maxplotlib.subfigure.line_plot import LinePlot +from maxplotlib.subfigure.line_plot import ( + _TIKZ_SUPPORTED_PLOT_TYPES, + LinePlot, + _tikz_error_bounds, + _tikz_step_coordinates, + _tikz_style_kwargs, +) from maxplotlib.utils.options import Backends @@ -313,12 +320,20 @@ def __init__( self._supylabel_kwargs: dict = {} self._subplots_adjust_kwargs: dict = {} self._tight_layout_kwargs: dict | None = None + self._set_tight_layout = None + self._align_labels = False + self._align_titles = False + self._align_xlabels = False + self._align_ylabels = False + self._autofmt_xdate_kwargs: dict | None = None # Dictionary to store lines for each subplot # Key: (row, col), Value: list of lines with their data and kwargs self._subplots = {} self._twinx_subplots = {} + self._twiny_subplots = {} self._matplotlib_twin_axes = {} + self._matplotlib_twiny_axes = {} self._num_subplots = 0 self._subplot_matrix = [[None] * self.ncols for _ in range(self.nrows)] @@ -398,6 +413,9 @@ def layers(self): twin_subplot = self._twinx_subplots.get((row, col)) if twin_subplot is not None: layers.extend(twin_subplot.layers) + twin_subplot = self._twiny_subplots.get((row, col)) + if twin_subplot is not None: + layers.extend(twin_subplot.layers) return list(set(layers)) def generate_new_rowcol(self, row, col): @@ -447,6 +465,47 @@ def add_line( **kwargs, ) + def plot_many( + self, + series, + labels=None, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add several lines to the canvas and return the canvas. + + ``series`` is an iterable of ``(x, y)`` pairs. Shared line keyword + arguments are passed through ``kwargs``; individual labels can be + supplied with ``labels``. + """ + if labels is not None: + labels = list(labels) + count = 0 + for index, values in enumerate(series): + count += 1 + try: + x, y = values + except (TypeError, ValueError) as exc: + raise ValueError("each series entry must be an (x, y) pair") from exc + line_kwargs = dict(kwargs) + if labels is not None: + if index >= len(labels): + raise ValueError("labels must contain one label per series") + line_kwargs["label"] = labels[index] + self.add_line( + x, + y, + layer=layer, + row=row, + col=col, + **line_kwargs, + ) + if labels is not None and len(labels) != count: + raise ValueError("labels must contain one label per series") + return self + def _get_or_create_subplot(self, row, col): """Return the subplot at (row, col), creating it if needed.""" if row is not None and col is not None: @@ -609,6 +668,10 @@ def set_box_aspect(self, aspect, row=None, col=None): """Set the physical height-to-width ratio of a subplot.""" self._get_or_create_subplot(row, col).set_box_aspect(aspect) + def set_aspect(self, aspect, row=None, col=None): + """Set the data aspect ratio of a subplot.""" + self._get_or_create_subplot(row, col).set_aspect(aspect) + def secondary_xaxis( self, location="top", functions=None, row=None, col=None, **kwargs ): @@ -877,6 +940,16 @@ def table( cellText=cellText, layer=layer, **kwargs ) + def add_table(self, cellText=None, layer=0, row=None, col=None, **kwargs): + """Matplotlib-style alias for ``table``.""" + self._get_or_create_subplot(row, col).add_table( + cellText=cellText, layer=layer, **kwargs + ) + + def add_caption(self, caption): + """Set the figure caption.""" + self._caption = caption + def gantt( self, tasks, @@ -959,6 +1032,60 @@ def set_title( """Set the title and text properties for a subplot.""" self._get_or_create_subplot(row, col).set_title(title, **kwargs) + def configure( + self, + *, + title=None, + xlabel=None, + ylabel=None, + grid=None, + facecolor=None, + axisbelow=None, + margins=None, + xscale=None, + yscale=None, + xlim=None, + ylim=None, + tight_layout=False, + row: int | None = None, + col: int | None = None, + ): + """Apply common figure and axis settings, returning the canvas. + + ``title``, ``xlabel``, and ``ylabel`` are figure-level settings. Axis + settings are applied to the selected subplot, or the default subplot + when ``row`` and ``col`` are omitted. Use ``tight_layout=True`` for a + final layout pass before rendering. + """ + if title is not None: + self.suptitle(title) + if xlabel is not None: + self.supxlabel(xlabel) + if ylabel is not None: + self.supylabel(ylabel) + if grid is not None: + self.set_grid(grid, row=row, col=col) + if facecolor is not None: + self.set_facecolor(facecolor, row=row, col=col) + if axisbelow is not None: + self.set_axisbelow(axisbelow, row=row, col=col) + if margins is not None: + if isinstance(margins, dict): + self.margins(row=row, col=col, **margins) + else: + self.margins(margins, row=row, col=col) + if xscale is not None: + self.set_xscale(xscale, row=row, col=col) + if yscale is not None: + self.set_yscale(yscale, row=row, col=col) + if xlim is not None: + self.set_xlim(*xlim, row=row, col=col) + if ylim is not None: + self.set_ylim(*ylim, row=row, col=col) + if tight_layout: + self.tight_layout() + return self + def set_xlim( self, left=None, right=None, row: int | None = None, col: int | None = None ): @@ -983,6 +1110,9 @@ def set_legend( """Show or hide the legend for a subplot (default top-left).""" self._get_or_create_subplot(row, col).set_legend(visible) + def legend(self, row=None, col=None, **kwargs): + self._get_or_create_subplot(row, col).set_legend(**kwargs) + def tick_params(self, row: int | None = None, col: int | None = None, **kwargs): """Configure major/minor tick appearance for a subplot.""" self._get_or_create_subplot(row, col).tick_params(**kwargs) @@ -1011,6 +1141,108 @@ def set_facecolor(self, color, row: int | None = None, col: int | None = None): """Set a subplot's background color.""" self._get_or_create_subplot(row, col).set_facecolor(color) + def set_fc(self, color, row=None, col=None): + self._get_or_create_subplot(row, col).set_fc(color) + + def set_adjustable(self, adjustable, row=None, col=None): + self._get_or_create_subplot(row, col).set_adjustable(adjustable) + + def set_anchor(self, anchor, row=None, col=None): + self._get_or_create_subplot(row, col).set_anchor(anchor) + + def set(self, row=None, col=None, **kwargs): + return self._get_or_create_subplot(row, col).set(**kwargs) + + def update(self, kwargs, row=None, col=None): + return self._get_or_create_subplot(row, col).update(kwargs) + + def xaxis_inverted(self, row=None, col=None): + return self._get_or_create_subplot(row, col).xaxis_inverted() + + def yaxis_inverted(self, row=None, col=None): + return self._get_or_create_subplot(row, col).yaxis_inverted() + + def set_frame_on(self, state=True, row=None, col=None): + self._get_or_create_subplot(row, col).set_frame_on(state) + + def set_visible(self, state=True, row=None, col=None): + self._get_or_create_subplot(row, col).set_visible(state) + + def set_alpha(self, alpha, row=None, col=None): + self._get_or_create_subplot(row, col).set_alpha(alpha) + + def set_zorder(self, zorder, row=None, col=None): + self._get_or_create_subplot(row, col).set_zorder(zorder) + + def set_rasterized(self, rasterized=True, row=None, col=None): + self._get_or_create_subplot(row, col).set_rasterized(rasterized) + + def set_autoscale_on(self, enable=True, row=None, col=None): + self._get_or_create_subplot(row, col).set_autoscale_on(enable) + + def set_autoscalex_on(self, enable=True, row=None, col=None): + self._get_or_create_subplot(row, col).set_autoscalex_on(enable) + + def set_autoscaley_on(self, enable=True, row=None, col=None): + self._get_or_create_subplot(row, col).set_autoscaley_on(enable) + + def set_xbound(self, lower=None, upper=None, row=None, col=None): + self._get_or_create_subplot(row, col).set_xbound(lower, upper) + + def set_ybound(self, lower=None, upper=None, row=None, col=None): + self._get_or_create_subplot(row, col).set_ybound(lower, upper) + + def set_xmargin(self, margin, row=None, col=None): + self._get_or_create_subplot(row, col).set_xmargin(margin) + + def set_ymargin(self, margin, row=None, col=None): + self._get_or_create_subplot(row, col).set_ymargin(margin) + + def get_adjustable(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_adjustable() + + def get_anchor(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_anchor() + + def get_alpha(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_alpha() + + def get_box_aspect(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_box_aspect() + + def get_facecolor(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_facecolor() + + def get_frame_on(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_frame_on() + + def get_legend(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_legend() + + def get_rasterization_zorder(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_rasterization_zorder() + + def get_rasterized(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_rasterized() + + def get_visible(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_visible() + + def get_zorder(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_zorder() + + def get_xbound(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_xbound() + + def get_ybound(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_ybound() + + def get_xmargin(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_xmargin() + + def get_ymargin(self, row=None, col=None): + return self._get_or_create_subplot(row, col).get_ymargin() + def margins(self, *args, row: int | None = None, col: int | None = None, **kwargs): """Set x/y data margins for a subplot.""" self._get_or_create_subplot(row, col).margins(*args, **kwargs) @@ -1244,6 +1476,12 @@ def imshow( """Add an image/matrix plot to a subplot.""" self._get_or_create_subplot(row, col).add_imshow(data, layer=layer, **kwargs) + def add_image( + self, data, layer=0, row: int | None = None, col: int | None = None, **kwargs + ): + """Matplotlib-style alias for ``imshow``.""" + self._get_or_create_subplot(row, col).add_image(data, layer=layer, **kwargs) + def add_patch( self, patch, @@ -1268,6 +1506,10 @@ def colorbar( label=label, layer=layer, **kwargs ) + def add_colorbar(self, label: str = "", layer=0, row=None, col=None, **kwargs): + """Alias for ``colorbar``.""" + self.colorbar(label=label, layer=layer, row=row, col=col, **kwargs) + # ------------------------------------------------------------------ # Multi-subplot helpers # ------------------------------------------------------------------ @@ -1301,11 +1543,26 @@ def twinx(self, row: int | None = None, col: int | None = None) -> LinePlot: self._twinx_subplots[key] = LinePlot() return self._twinx_subplots[key] + def twiny(self, row: int | None = None, col: int | None = None) -> LinePlot: + """Create or return a secondary x-axis sharing a subplot's y-axis.""" + self._get_or_create_subplot(row, col) + if row is None: + row, col = 0, 0 + key = (row, col) + if key not in self._twiny_subplots: + self._twiny_subplots[key] = LinePlot() + return self._twiny_subplots[key] + @property def twinx_axes(self): """Return materialized Matplotlib secondary axes by ``(row, col)``.""" return dict(self._matplotlib_twin_axes) + @property + def twiny_axes(self): + """Return materialized Matplotlib secondary x-axes by ``(row, col)``.""" + return dict(self._matplotlib_twiny_axes) + def iter_subplots(self): """Yield (row, col, subplot) for every initialized subplot, row-major.""" for r in range(self.nrows): @@ -1343,6 +1600,91 @@ def tight_layout(self, **kwargs): """Apply Matplotlib's automatic tight layout after plotting.""" self._tight_layout_kwargs = dict(kwargs) + def set_tight_layout(self, tight=True, **kwargs): + self._set_tight_layout = (tight, dict(kwargs)) + if tight: + self._tight_layout_kwargs = dict(kwargs) + + def align_labels(self, **kwargs): + self._align_labels = True + + def align_titles(self, **kwargs): + self._align_titles = True + + def align_xlabels(self, **kwargs): + self._align_xlabels = True + + def align_ylabels(self, **kwargs): + self._align_ylabels = True + + def autofmt_xdate(self, **kwargs): + self._autofmt_xdate_kwargs = dict(kwargs) + + def get_axes(self): + """Return rendered Matplotlib axes, or the current subplot models.""" + if self._matplotlib_fig is not None: + return self._matplotlib_fig.get_axes() + return list(self._subplot_dict.values()) + + def get_suptitle(self): + return self._suptitle + + def get_supxlabel(self): + return self._supxlabel + + def get_supylabel(self): + return self._supylabel + + def set_size_inches(self, w, h=None, forward=True): + """Set the figure size in inches, matching Matplotlib.""" + if h is None: + try: + w, h = w + except (TypeError, ValueError) as exc: + raise ValueError("set_size_inches expects (width, height)") from exc + self._figsize = (float(w), float(h)) + self._width = None + if forward and self._matplotlib_fig is not None: + self._matplotlib_fig.set_size_inches(self._figsize, forward=True) + + def get_size_inches(self): + """Return the figure size as ``(width, height)`` in inches.""" + if self._matplotlib_fig is not None: + return self._matplotlib_fig.get_size_inches() + if self._figsize is not None: + return np.asarray(self._figsize, dtype=float) + return np.asarray((6.4, 4.8), dtype=float) + + def set_figwidth(self, w): + """Set the figure width in inches.""" + _, height = self.get_size_inches() + self.set_size_inches(w, height) + + def set_figheight(self, h): + """Set the figure height in inches.""" + width, _ = self.get_size_inches() + self.set_size_inches(width, h) + + def get_figwidth(self): + """Return the figure width in inches.""" + return float(self.get_size_inches()[0]) + + def get_figheight(self): + """Return the figure height in inches.""" + return float(self.get_size_inches()[1]) + + def set_dpi(self, dpi): + """Set the figure DPI used for rendering and export.""" + self._dpi = dpi + if self._matplotlib_fig is not None: + self._matplotlib_fig.set_dpi(dpi) + + def get_dpi(self): + """Return the configured or rendered figure DPI.""" + if self._matplotlib_fig is not None: + return self._matplotlib_fig.get_dpi() + return self._dpi + def add_tikzfigure( self, col=None, @@ -1446,7 +1788,7 @@ def savefig( layers = [] for layer in self.layers: layers.append(layer) - fig, axs = self.plot( + fig, axs = self._render( show=False, backend="matplotlib", savefig=True, @@ -1467,7 +1809,7 @@ def savefig( savefig_kwargs = {"dpi": self.dpi} if self.dpi is not None else {} self._matplotlib_fig.savefig(full_filepath, **savefig_kwargs) else: - fig, axs = self.plot( + fig, axs = self._render( backend="matplotlib", savefig=True, layers=layers, @@ -1481,7 +1823,7 @@ def savefig( layers = [] for layer in self.layers: layers.append(layer) - figure = self.plot( + figure = self._render( backend="plotext", savefig=False, layers=layers, @@ -1495,7 +1837,7 @@ def savefig( full_filepath = filename else: full_filepath = f"{filename_no_extension}_{layers}.{extension}" - figure = self.plot( + figure = self._render( backend="plotext", savefig=False, layers=layers, @@ -1509,7 +1851,7 @@ def savefig( for layer in self.layers: layers.append(layer) full_filepath = f"{filename_no_extension}_{layers}{extension}" - fig = self.plot( + fig = self._render( backend="plotly", savefig=False, layers=layers, @@ -1523,7 +1865,7 @@ def savefig( full_filepath = filename else: full_filepath = f"{filename_no_extension}_{layers}{extension}" - fig = self.plot( + fig = self._render( backend="plotly", savefig=False, layers=layers, @@ -1536,12 +1878,12 @@ def savefig( raise NotImplementedError( "Layer-by-layer rendering is not supported for tikzfigure backend" ) - fig = self.plot(backend="tikzfigure", savefig=False) + fig = self._render(backend="tikzfigure", savefig=False) fig.savefig(filename) if verbose: print(f"Saved {filename}") - def plot( + def _render( self, backend: Backends = "matplotlib", savefig: bool = False, @@ -1604,6 +1946,52 @@ def plot( else: raise ValueError(f"Invalid backend: {backend}") + def plot(self, *args, backend=None, **kwargs): + """Add a line, or render when called with backend options. + + ``canvas.plot(x, y, **style)`` is the convenient direct plotting form. + Rendering is named explicitly by ``canvas.render(...)``; the legacy + ``canvas.plot(backend=...)`` form remains supported. + """ + explicit_render = backend is not None or (args and isinstance(args[0], str)) + if args and not isinstance(args[0], str): + if len(args) < 2: + raise TypeError("plot(x, y) requires both x and y data") + if len(args) > 2: + raise TypeError("plot() accepts only x and y positional data") + layer = kwargs.pop("layer", 0) + row = kwargs.pop("row", None) + col = kwargs.pop("col", None) + self.add_line(args[0], args[1], layer=layer, row=row, col=col, **kwargs) + return self + if args: + if len(args) > 1: + raise TypeError( + "plot() accepts at most one backend positional argument" + ) + if backend is not None: + raise TypeError("backend was provided both positionally and by keyword") + backend = args[0] + if backend is None: + backend = "matplotlib" + if explicit_render: + warnings.warn( + "canvas.plot(backend=...) is deprecated; use " + "canvas.render(backend=...) instead", + FutureWarning, + stacklevel=2, + ) + return self._render(backend=backend, **kwargs) + + def render(self, *args, **kwargs): + """Render the canvas using the selected backend. + + This is the explicit name for the operation historically exposed as + ``Canvas.plot(backend=...)``. The latter remains available for + backwards compatibility. + """ + return self._render(*args, **kwargs) + def show( self, backend: Backends = "matplotlib", @@ -1643,7 +2031,7 @@ def show( if backend == "matplotlib": if verbose: print("Generating Matplotlib figure for display...") - fig, axes = self.plot( + fig, axes = self._render( backend="matplotlib", savefig=False, layers=layers, @@ -1671,7 +2059,9 @@ def show( allow_unsupported=allow_unsupported, ) fig.show() - return fig + # Plotly has already displayed the figure. Returning it from a + # notebook cell would trigger a second implicit rich display. + return None if _running_in_jupyter() else fig elif backend == "plotext": figure = self.plot_plotext( savefig=False, @@ -1782,6 +2172,16 @@ def plot_matplotlib( fig.subplots_adjust(**self._subplots_adjust_kwargs) if self._tight_layout_kwargs is not None: fig.tight_layout(**self._tight_layout_kwargs) + if self._align_labels: + fig.align_labels() + if self._align_titles: + fig.align_titles() + if self._align_xlabels: + fig.align_xlabels() + if self._align_ylabels: + fig.align_ylabels() + if self._autofmt_xdate_kwargs is not None: + fig.autofmt_xdate(**self._autofmt_xdate_kwargs) if verbose: print("Set suptitle.") @@ -1795,6 +2195,11 @@ def plot_matplotlib( twin_axis = axes[row][col].twinx() twin_subplot.plot_matplotlib(twin_axis, layers=layers) self._matplotlib_twin_axes[(row, col)] = twin_axis + self._matplotlib_twiny_axes = {} + for (row, col), twin_subplot in self._twiny_subplots.items(): + twin_axis = axes[row][col].twiny() + twin_subplot.plot_matplotlib(twin_axis, layers=layers) + self._matplotlib_twiny_axes[(row, col)] = twin_axis if matplotlib_customizations is not None: _apply_matplotlib_customizations(fig, axes, matplotlib_customizations) if matplotlib_postprocess is not None: @@ -1872,7 +2277,12 @@ def plot_tikzfigure( # Add each plot line to the subfigure for line_data in line_plot.line_data: - if line_data.get("plot_type") == "plot": + plot_type = line_data.get("plot_type") + if plot_type not in _TIKZ_SUPPORTED_PLOT_TYPES: + raise NotImplementedError( + f"{plot_type} is not supported by the tikzfigure backend" + ) + if plot_type == "plot": # Extract and transform x, y data x = (line_data["x"] + line_plot._xshift) * line_plot._xscale y = (line_data["y"] + line_plot._yshift) * line_plot._yscale @@ -1883,11 +2293,182 @@ def plot_tikzfigure( ax.add_plot( x=x, y=y, - # label=kwargs.get("label", ""), - color=kwargs.get("color", "black"), - line_width=kwargs.get("linewidth", 1.0), + **_tikz_style_kwargs(kwargs), ) - elif line_data.get("plot_type") == "gantt": + elif plot_type == "scatter": + x = (line_data["x"] + line_plot._xshift) * line_plot._xscale + y = (line_data["y"] + line_plot._yshift) * line_plot._yscale + kwargs = _tikz_style_kwargs(line_data.get("kwargs", {})) + kwargs.setdefault("mark", "*") + kwargs["line_width"] = 0 + ax.add_plot(x=x, y=y, **kwargs) + elif plot_type in {"bar", "barh"}: + source_kwargs = line_data.get("kwargs", {}) + kwargs = _tikz_style_kwargs(source_kwargs) + kwargs["fill"] = source_kwargs.get("color", "blue") + kwargs["fill_opacity"] = source_kwargs.get("alpha", 1.0) + kwargs["line_width"] = source_kwargs.get("linewidth", 0) + if plot_type == "bar": + width = source_kwargs.get("width", 0.8) + for x, height in zip(line_data["x"], line_data["height"]): + ax.add_plot( + x=[ + x - width / 2, + x + width / 2, + x + width / 2, + x - width / 2, + ], + y=[0, 0, height, height], + cycle=True, + **kwargs, + ) + else: + height = source_kwargs.get("height", 0.8) + for y, width in zip(line_data["y"], line_data["width"]): + ax.add_plot( + x=[0, width, width, 0], + y=[ + y - height / 2, + y - height / 2, + y + height / 2, + y + height / 2, + ], + cycle=True, + **kwargs, + ) + elif plot_type == "fill_between": + x = line_data["x"] + y1 = np.asarray(line_data["y1"]) + y2 = np.broadcast_to(line_data["y2"], y1.shape) + source_kwargs = line_data.get("kwargs", {}) + kwargs = _tikz_style_kwargs(source_kwargs) + kwargs["fill"] = source_kwargs.get("color", "blue") + kwargs["fill_opacity"] = source_kwargs.get("alpha", 0.25) + ax.add_plot( + x=list(x) + list(x[::-1]), + y=list(y1) + list(y2[::-1]), + cycle=True, + **kwargs, + ) + elif plot_type == "errorbar": + x = line_data["x"] + y = line_data["y"] + kwargs = _tikz_style_kwargs(line_data.get("kwargs", {})) + ax.add_plot(x=x, y=y, **kwargs) + y_bounds = _tikz_error_bounds(line_data["yerr"], y) + if y_bounds is not None: + lower, upper = y_bounds + for xi, low, high in zip(x, y - lower, y + upper): + ax.add_plot(x=[xi, xi], y=[low, high], **kwargs) + x_bounds = _tikz_error_bounds(line_data["xerr"], x) + if x_bounds is not None: + lower, upper = x_bounds + for yi, low, high in zip(y, x - lower, x + upper): + ax.add_plot(x=[low, high], y=[yi, yi], **kwargs) + elif plot_type in {"step", "stairs"}: + source_kwargs = line_data.get("kwargs", {}) + if plot_type == "step": + x = line_data["x"] + y = line_data["y"] + where = source_kwargs.get("where", "pre") + else: + values = line_data["values"] + edges = line_data["edges"] + if edges is None: + edges = np.arange(len(values) + 1) + x = edges + y = np.r_[values, values[-1]] + where = "post" + x, y = _tikz_step_coordinates(x, y, where=where) + ax.add_plot( + x=x, + y=y, + **_tikz_style_kwargs(source_kwargs), + ) + elif plot_type == "stem": + x = line_data["x"] + y = line_data["y"] + source_kwargs = line_data.get("kwargs", {}) + style = _tikz_style_kwargs(source_kwargs) + marker_style = dict(style) + marker_style.update( + mark=source_kwargs.get("marker", "*"), line_width=0 + ) + ax.add_plot(x=x, y=y, **marker_style) + for xi, yi in zip(x, y): + ax.add_plot(x=[xi, xi], y=[0, yi], **style) + elif plot_type in {"hlines", "vlines"}: + style = _tikz_style_kwargs(line_data.get("kwargs", {})) + if plot_type == "hlines": + for yi, left, right in zip( + np.atleast_1d(line_data["y"]), + np.atleast_1d(line_data["xmin"]), + np.atleast_1d(line_data["xmax"]), + ): + ax.add_plot(x=[left, right], y=[yi, yi], **style) + else: + for xi, bottom, top in zip( + np.atleast_1d(line_data["x"]), + np.atleast_1d(line_data["ymin"]), + np.atleast_1d(line_data["ymax"]), + ): + ax.add_plot(x=[xi, xi], y=[bottom, top], **style) + elif plot_type in {"axvspan", "axhspan"}: + source_kwargs = line_data.get("kwargs", {}) + style = _tikz_style_kwargs(source_kwargs) + style["fill"] = source_kwargs.get("color", "blue") + style["fill_opacity"] = source_kwargs.get("alpha", 0.2) + if plot_type == "axvspan": + xmin, xmax = line_data["xmin"], line_data["xmax"] + ymin, ymax = line_plot._ymin or 0, line_plot._ymax or 1 + x = [xmin, xmax, xmax, xmin] + y = [ymin, ymin, ymax, ymax] + else: + ymin, ymax = line_data["ymin"], line_data["ymax"] + xmin, xmax = line_plot._xmin or 0, line_plot._xmax or 1 + x = [xmin, xmax, xmax, xmin] + y = [ymin, ymin, ymax, ymax] + ax.add_plot(x=x, y=y, cycle=True, **style) + elif plot_type == "fill": + if len(line_data["args"]) < 2: + raise ValueError("tikzfigure fill requires x and y coordinates") + x, y = line_data["args"][:2] + source_kwargs = line_data.get("kwargs", {}) + style = _tikz_style_kwargs(source_kwargs) + style["fill"] = source_kwargs.get("color", "blue") + style["fill_opacity"] = source_kwargs.get("alpha", 0.25) + ax.add_plot(x=x, y=y, cycle=True, **style) + elif plot_type == "flame_chart": + labels = line_data["labels"] + parents = line_data["parents"] + values = line_data["values"] * line_plot._xscale + start_times = line_data["start_times"] + depths = np.zeros(len(labels), dtype=int) + if start_times is None: + start_times = np.zeros(len(labels)) + else: + start_times = ( + start_times + line_plot._xshift + ) * line_plot._xscale + for index, parent in enumerate(parents): + if parent is not None: + parent_index = ( + parent + if isinstance(parent, int) + else labels.index(parent) + ) + depths[index] = depths[parent_index] + 1 + colors = ["red", "blue", "green", "orange", "purple", "cyan"] + for index, (start, value) in enumerate(zip(start_times, values)): + y = depths[index] + ax.add_plot( + x=[start, start + value, start + value, start], + y=[y - 0.4, y - 0.4, y + 0.4, y + 0.4], + cycle=True, + fill=colors[y % len(colors)], + line_width=0, + ) + elif plot_type == "gantt": tasks = line_data["tasks"] start_times = ( line_data["start_times"] + line_plot._xshift @@ -2021,6 +2602,10 @@ def plot_plotly( "secondary_xaxis and secondary_yaxis are currently supported " "only by the matplotlib backend" ) + if self._twiny_subplots: + raise NotImplementedError( + "twiny is currently supported only by the matplotlib backend" + ) setup_tex_fonts( fontsize=self.fontsize, @@ -2362,5 +2947,5 @@ def __str__(self): c = Canvas(ncols=2, nrows=2) sp = c.add_subplot() sp.plot([0, 1, 2, 3], [0, 1, 4, 9], label="Line 1") - c.plot(backend="matplotlib") + c.render(backend="matplotlib") print("done") diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index 6e7c78a..55eacc6 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -4,6 +4,83 @@ from mpl_toolkits.axes_grid1 import make_axes_locatable from tikzfigure import TikzFigure +_TIKZ_SUPPORTED_PLOT_TYPES = { + "plot", + "scatter", + "bar", + "barh", + "fill_between", + "errorbar", + "step", + "stairs", + "stem", + "hlines", + "vlines", + "axvspan", + "axhspan", + "fill", + "gantt", + "flame_chart", +} + + +def _tikz_style_kwargs(kwargs, *, default_color="black"): + """Translate common Matplotlib-style options to pgfplots/TikZ options.""" + kwargs = dict(kwargs) + style = {} + if kwargs.get("color") is not None: + style["color"] = kwargs["color"] + else: + style["color"] = default_color + if kwargs.get("linewidth") is not None: + style["line_width"] = kwargs["linewidth"] + if kwargs.get("alpha") is not None: + style["opacity"] = kwargs["alpha"] + if kwargs.get("linestyle") in {"--", "dashed"}: + style["dash_pattern"] = "on 4pt off 2pt" + elif kwargs.get("linestyle") in {":", "dotted"}: + style["dash_pattern"] = "on 1pt off 2pt" + elif kwargs.get("linestyle") == "-.": + style["dash_pattern"] = "on 4pt off 2pt on 1pt off 2pt" + if kwargs.get("marker") is not None: + style["mark"] = kwargs["marker"] + if kwargs.get("markersize") is not None: + style["mark_size"] = f"{kwargs['markersize']}pt" + return style + + +def _tikz_error_bounds(error, values): + """Return lower and upper error arrays in Matplotlib's common formats.""" + if error is None: + return None + error = np.asarray(error, dtype=float) + values = np.asarray(values, dtype=float) + if error.ndim == 0: + error = np.full(values.shape, error.item()) + if error.ndim == 2 and error.shape[0] == 2: + return error[0], error[1] + return error, error + + +def _tikz_step_coordinates(x, y, where="pre"): + """Expand line data into explicit coordinates for a stepped path.""" + x = np.asarray(x) + y = np.asarray(y) + if len(x) < 2: + return x, y + if where == "post": + step_x = np.repeat(x, 2)[1:] + step_y = np.repeat(y, 2)[:-1] + elif where == "mid": + mids = (x[:-1] + x[1:]) / 2 + step_x = np.ravel(np.column_stack((x[:-1], mids, mids, x[1:]))) + step_y = np.ravel(np.column_stack((y[:-1], y[:-1], y[1:], y[1:]))) + return step_x, step_y + else: + step_x = np.repeat(x, 2)[:-1] + step_y = np.repeat(y, 2)[1:] + return step_x, step_y + class Node: def __init__(self, x, y, label="", content="", layer=0, **kwargs): @@ -70,6 +147,7 @@ def __init__( self._caption = None self._grid = grid self._legend = legend + self._legend_kwargs: dict = {} self._xmin = xmin self._xmax = xmax self._ymin = ymin @@ -107,6 +185,18 @@ def __init__( self._box_aspect = None self._secondary_xaxis_settings: dict | None = None self._secondary_yaxis_settings: dict | None = None + self._frame_on = None + self._visible = None + self._alpha = None + self._zorder = None + self._rasterized = None + self._autoscale_on = None + self._autoscalex_on = None + self._autoscaley_on = None + self._xmargin = None + self._ymargin = None + self._adjustable = None + self._anchor = None # Custom tick positions and labels self._xticks: list | None = None @@ -557,6 +647,10 @@ def table(self, cellText=None, layer=0, **kwargs): layer, ) + def add_table(self, cellText=None, layer=0, **kwargs): + """Matplotlib-style alias for ``table``.""" + self.table(cellText=cellText, layer=layer, **kwargs) + def gantt(self, tasks, start_times, durations, layer=0, **kwargs): """ Add a Gantt chart to the subplot. @@ -672,9 +766,10 @@ def tick_params(self, **kwargs): """Configure tick appearance using Matplotlib-style keyword arguments.""" self._tick_params = dict(kwargs) - def set_legend(self, visible: bool = True): + def set_legend(self, visible: bool = True, **kwargs): """Show or hide the legend.""" self._legend = visible + self._legend_kwargs = dict(kwargs) def set_xscale(self, scale: str): """Set the x-axis scale type: 'linear', 'log', or 'symlog'.""" @@ -700,6 +795,87 @@ def set_facecolor(self, color): """Set the subplot background color.""" self._facecolor = color + def set_frame_on(self, state): + self._frame_on = state + + def set_visible(self, state): + self._visible = state + + def set_alpha(self, alpha): + self._alpha = alpha + + def set_zorder(self, zorder): + self._zorder = zorder + + def set_rasterized(self, rasterized): + self._rasterized = rasterized + + def set_autoscale_on(self, enable): + self._autoscale_on = enable + + def set_autoscalex_on(self, enable): + self._autoscalex_on = enable + + def set_autoscaley_on(self, enable): + self._autoscaley_on = enable + + def set_xbound(self, lower=None, upper=None): + self.set_xlim(lower, upper) + + def set_ybound(self, lower=None, upper=None): + self.set_ylim(lower, upper) + + def set_xmargin(self, margin): + self._xmargin = margin + + def set_ymargin(self, margin): + self._ymargin = margin + + def get_adjustable(self): + return self._adjustable + + def get_anchor(self): + return self._anchor + + def get_alpha(self): + return self._alpha + + def get_box_aspect(self): + return self._box_aspect + + def get_facecolor(self): + return self._facecolor + + def get_frame_on(self): + return self._frame_on + + def get_legend(self): + return self._legend + + def get_rasterization_zorder(self): + return self._rasterization_zorder + + def get_rasterized(self): + return self._rasterized + + def get_visible(self): + return self._visible + + def get_zorder(self): + return self._zorder + + def get_xbound(self): + return self._xmin, self._xmax + + def get_ybound(self): + return self._ymin, self._ymax + + def get_xmargin(self): + return self._xmargin + + def get_ymargin(self): + return self._ymargin + def margins(self, *args, **kwargs): """Set x/y data margins using Matplotlib-style arguments.""" self._margins = {"args": args, **kwargs} @@ -749,6 +925,49 @@ def set_aspect(self, aspect): """Set the axes aspect ratio: 'equal', 'auto', or a float.""" self._aspect = aspect + def set_adjustable(self, adjustable): + self._adjustable = adjustable + + def set_anchor(self, anchor): + self._anchor = anchor + + def set_fc(self, color): + self.set_facecolor(color) + + def set(self, **kwargs): + """Set common axes properties using Matplotlib-style names.""" + handlers = { + "title": self.set_title, + "xlabel": self.set_xlabel, + "ylabel": self.set_ylabel, + "xlim": lambda value: self.set_xlim(*value), + "ylim": lambda value: self.set_ylim(*value), + "xscale": self.set_xscale, + "yscale": self.set_yscale, + "facecolor": self.set_facecolor, + "fc": self.set_fc, + "aspect": self.set_aspect, + "adjustable": self.set_adjustable, + "anchor": self.set_anchor, + "visible": self.set_visible, + "alpha": self.set_alpha, + "zorder": self.set_zorder, + } + for name, value in kwargs.items(): + if name not in handlers: + raise AttributeError(f"Unknown LinePlot property: {name}") + handlers[name](value) + return kwargs + + def update(self, kwargs): + return self.set(**dict(kwargs)) + + def xaxis_inverted(self): + return self._invert_xaxis + + def yaxis_inverted(self): + return self._invert_yaxis + def axis(self, *args, **kwargs): """Set Matplotlib-style axis limits or modes.""" self._axis_settings = {"args": args, **kwargs} @@ -1025,6 +1244,10 @@ def add_imshow(self, data, layer=0, **kwargs): } self._add(ld, layer) + def add_image(self, data, layer=0, **kwargs): + """Matplotlib-style alias for ``imshow``.""" + self.add_imshow(data, layer=layer, **kwargs) + def add_patch(self, patch, layer=0, **kwargs): ld = { "patch": patch, @@ -1343,7 +1566,7 @@ def plot_matplotlib( if self._ylabel: ax.set_ylabel(self._ylabel, **self._ylabel_kwargs) if self._legend and len(self.line_data) > 0: - ax.legend() + ax.legend(**self._legend_kwargs) if self._grid: ax.grid() if self._axis_settings: @@ -1382,12 +1605,36 @@ def plot_matplotlib( ax.tick_params(**self._tick_params) if self._aspect is not None: ax.set_aspect(self._aspect) + if self._adjustable is not None: + ax.set_adjustable(self._adjustable) + if self._anchor is not None: + ax.set_anchor(self._anchor) if self._box_aspect is not None: ax.set_box_aspect(self._box_aspect) if self._axisbelow is not None: ax.set_axisbelow(self._axisbelow) if self._facecolor is not None: ax.set_facecolor(self._facecolor) + if self._frame_on is not None: + ax.set_frame_on(self._frame_on) + if self._visible is not None: + ax.set_visible(self._visible) + if self._alpha is not None: + ax.set_alpha(self._alpha) + if self._zorder is not None: + ax.set_zorder(self._zorder) + if self._rasterized is not None: + ax.set_rasterized(self._rasterized) + if self._autoscale_on is not None: + ax.set_autoscale_on(self._autoscale_on) + if self._autoscalex_on is not None: + ax.set_autoscalex_on(self._autoscalex_on) + if self._autoscaley_on is not None: + ax.set_autoscaley_on(self._autoscaley_on) + if self._xmargin is not None: + ax.set_xmargin(self._xmargin) + if self._ymargin is not None: + ax.set_ymargin(self._ymargin) if self._margins: margin_settings = dict(self._margins) margin_args = margin_settings.pop("args", ()) @@ -1450,12 +1697,176 @@ def plot_tikzfigure(self, layers=None, verbose: bool = False) -> TikzFigure: if layers and layer_name not in layers: continue for line in layer_lines: - if line["plot_type"] == "plot": + plot_type = line["plot_type"] + if plot_type not in _TIKZ_SUPPORTED_PLOT_TYPES: + raise NotImplementedError( + f"{plot_type} is not supported by the tikzfigure backend" + ) + if plot_type == "plot": x = (line["x"] + self._xshift) * self._xscale y = (line["y"] + self._yshift) * self._yscale nodes = [[xi, yi] for xi, yi in zip(x, y)] - tikz_figure.draw(nodes=nodes, **line["kwargs"]) + tikz_figure.draw( + nodes=nodes, + **_tikz_style_kwargs(line["kwargs"]), + ) + elif plot_type == "scatter": + x = (line["x"] + self._xshift) * self._xscale + y = (line["y"] + self._yshift) * self._yscale + style = _tikz_style_kwargs(line["kwargs"]) + style.setdefault("mark", "*") + style["line_width"] = 0 + tikz_figure.draw( + nodes=[[xi, yi] for xi, yi in zip(x, y)], + **style, + ) + elif plot_type in {"bar", "barh"}: + kwargs = line["kwargs"] + style = _tikz_style_kwargs(kwargs) + style["fill"] = kwargs.get("color", "blue") + style["fill_opacity"] = kwargs.get("alpha", 1.0) + style["line_width"] = kwargs.get("linewidth", 0) + if plot_type == "bar": + width = kwargs.get("width", 0.8) + for x, height in zip(line["x"], line["height"]): + x = (x + self._xshift) * self._xscale + height = height * self._yscale + tikz_figure.draw( + nodes=[ + [x - width / 2, 0], + [x + width / 2, 0], + [x + width / 2, height], + [x - width / 2, height], + ], + cycle=True, + **style, + ) + else: + height = kwargs.get("height", 0.8) + for y, width in zip(line["y"], line["width"]): + y = (y + self._yshift) * self._yscale + width = width * self._xscale + tikz_figure.draw( + nodes=[ + [0, y - height / 2], + [width, y - height / 2], + [width, y + height / 2], + [0, y + height / 2], + ], + cycle=True, + **style, + ) + elif plot_type == "fill_between": + x = (line["x"] + self._xshift) * self._xscale + y1 = np.asarray(line["y1"]) + y2 = np.broadcast_to(line["y2"], y1.shape) + nodes = [[xi, yi] for xi, yi in zip(x, y1)] + nodes.extend([[xi, yi] for xi, yi in zip(x[::-1], y2[::-1])]) + kwargs = line["kwargs"] + style = _tikz_style_kwargs(kwargs) + style["fill"] = kwargs.get("color", "blue") + style["fill_opacity"] = kwargs.get("alpha", 0.25) + tikz_figure.draw(nodes=nodes, cycle=True, **style) + elif plot_type == "errorbar": + x = (line["x"] + self._xshift) * self._xscale + y = (line["y"] + self._yshift) * self._yscale + style = _tikz_style_kwargs(line["kwargs"]) + tikz_figure.draw(nodes=[[xi, yi] for xi, yi in zip(x, y)], **style) + y_bounds = _tikz_error_bounds(line["yerr"], y) + if y_bounds is not None: + lower, upper = y_bounds + for xi, low, high in zip(x, y - lower, y + upper): + tikz_figure.draw(nodes=[[xi, low], [xi, high]], **style) + x_bounds = _tikz_error_bounds(line["xerr"], x) + if x_bounds is not None: + lower, upper = x_bounds + for yi, low, high in zip(y, x - lower, x + upper): + tikz_figure.draw(nodes=[[low, yi], [high, yi]], **style) + elif plot_type in {"step", "stairs"}: + kwargs = line["kwargs"] + if plot_type == "step": + x = line["x"] + y = line["y"] + where = kwargs.get("where", "pre") + else: + values = line["values"] + edges = line["edges"] + if edges is None: + edges = np.arange(len(values) + 1) + x = edges + y = np.r_[values, values[-1]] + where = "post" + x, y = _tikz_step_coordinates(x, y, where=where) + x = (x + self._xshift) * self._xscale + y = (y + self._yshift) * self._yscale + tikz_figure.draw( + nodes=[[xi, yi] for xi, yi in zip(x, y)], + **_tikz_style_kwargs(kwargs), + ) + elif plot_type == "stem": + x = (line["x"] + self._xshift) * self._xscale + y = (line["y"] + self._yshift) * self._yscale + kwargs = line["kwargs"] + style = _tikz_style_kwargs(kwargs) + marker_style = dict(style) + marker_style.update(mark=kwargs.get("marker", "*"), line_width=0) + tikz_figure.draw( + nodes=[[xi, yi] for xi, yi in zip(x, y)], **marker_style + ) + for xi, yi in zip(x, y): + tikz_figure.draw(nodes=[[xi, 0], [xi, yi]], **style) + elif plot_type in {"hlines", "vlines"}: + kwargs = _tikz_style_kwargs(line["kwargs"]) + if plot_type == "hlines": + for yi, left, right in zip( + np.atleast_1d(line["y"]), + np.atleast_1d(line["xmin"]), + np.atleast_1d(line["xmax"]), + ): + tikz_figure.draw(nodes=[[left, yi], [right, yi]], **kwargs) + else: + for xi, bottom, top in zip( + np.atleast_1d(line["x"]), + np.atleast_1d(line["ymin"]), + np.atleast_1d(line["ymax"]), + ): + tikz_figure.draw(nodes=[[xi, bottom], [xi, top]], **kwargs) + elif plot_type in {"axvspan", "axhspan"}: + kwargs = line["kwargs"] + style = _tikz_style_kwargs(kwargs) + style["fill"] = kwargs.get("color", "blue") + style["fill_opacity"] = kwargs.get("alpha", 0.2) + if plot_type == "axvspan": + ymin, ymax = self._ymin or 0, self._ymax or 1 + nodes = [ + [line["xmin"], ymin], + [line["xmax"], ymin], + [line["xmax"], ymax], + [line["xmin"], ymax], + ] + else: + xmin, xmax = self._xmin or 0, self._xmax or 1 + nodes = [ + [xmin, line["ymin"]], + [xmax, line["ymin"]], + [xmax, line["ymax"]], + [xmin, line["ymax"]], + ] + tikz_figure.draw(nodes=nodes, cycle=True, **style) + elif plot_type == "fill": + if len(line["args"]) < 2: + raise ValueError("tikzfigure fill requires x and y coordinates") + x, y = line["args"][:2] + kwargs = line["kwargs"] + style = _tikz_style_kwargs(kwargs) + style["fill"] = kwargs.get("color", "blue") + style["fill_opacity"] = kwargs.get("alpha", 0.25) + tikz_figure.draw( + nodes=[[xi, yi] for xi, yi in zip(x, y)], + cycle=True, + **style, + ) elif line["plot_type"] == "gantt": tasks = line["tasks"] start_times = (line["start_times"] + self._xshift) * self._xscale @@ -2960,8 +3371,10 @@ def extend_y(values): y = self._transform_y(line["y"]) extend_x(x) extend_y(y) - xerr = self._coerce_numeric_array(line.get("xerr")) - yerr = self._coerce_numeric_array(line.get("yerr")) + xerr = self._plotext_error_values(line.get("xerr"), len(x)) + yerr = self._plotext_error_values(line.get("yerr"), len(y)) + xerr = self._coerce_numeric_array(xerr) + yerr = self._coerce_numeric_array(yerr) if xerr is not None: extend_x(x - xerr) extend_x(x + xerr) @@ -3021,9 +3434,19 @@ def extend_y(values): def _plotext_error_values(self, error, count): if error is None: return None - if np.isscalar(error): - return [float(error)] * count - return np.asarray(error).tolist() + values = np.asarray(error, dtype=float) + if values.ndim == 0: + # Plotext's error() interprets each value as the full bar width, + # while Matplotlib interprets it as the distance from the point + # to one end of a symmetric error bar. + return [float(values) * 2] * count + if values.ndim == 2 and values.shape[0] == 2: + # Matplotlib's asymmetric form is [lower, upper]. Plotext only + # accepts symmetric widths, so preserve the total extent. + values = values[0] + values[1] + else: + values = values * 2 + return values.tolist() def _plotext_ranges(self, layers=None): xs, ys = self._plotext_bounds(layers=layers) diff --git a/src/maxplotlib/tests/test_canvas.py b/src/maxplotlib/tests/test_canvas.py index 32d3f7d..b8ef8d5 100644 --- a/src/maxplotlib/tests/test_canvas.py +++ b/src/maxplotlib/tests/test_canvas.py @@ -94,6 +94,62 @@ def test_canvas_plot_tikzfigure_vertical_not_supported(): assert "nrows > 1" in str(exc_info.value) +def test_tikzfigure_supports_scatter_bars_fills_and_errorbars(): + import numpy as np + + from maxplotlib import Canvas + + x = np.arange(3) + canvas = Canvas() + canvas.scatter(x, [1, 2, 1], color="red") + canvas.bar(x, [1, 2, 1], color="blue") + canvas.fill_between(x, [1, 2, 1], 0, color="green", alpha=0.2) + canvas.errorbar(x, [1, 2, 1], yerr=0.1, color="black") + + tikz = canvas.render(backend="tikzfigure").generate_tikz() + + assert "mark=*" in tikz + assert "fill=blue" in tikz + assert "fill=green" in tikz + assert tikz.count("coordinates") >= 4 + + +def test_tikzfigure_rejects_unsupported_plot_types_explicitly(): + import numpy as np + import pytest + + from maxplotlib import Canvas + + canvas = Canvas() + canvas.imshow(np.ones((2, 2))) + + with pytest.raises(NotImplementedError, match="imshow"): + canvas.render(backend="tikzfigure") + + +def test_tikzfigure_supports_step_stem_reference_lines_spans_and_fill(): + import numpy as np + + from maxplotlib import Canvas + + x = np.arange(4) + canvas = Canvas() + canvas.step(x, [1, 2, 1, 3], color="black") + canvas.stem(x, [1, 2, 1, 3], color="purple") + canvas.hlines([1, 2], 0, 3, color="gray") + canvas.vlines([1, 2], 0, 3, color="gray") + canvas.axvspan(1, 2, color="orange", alpha=0.2) + canvas.axhspan(1, 2, color="green", alpha=0.2) + canvas.fill(x, [0, 1, 0, 1], color="cyan", alpha=0.2) + + tikz = canvas.render(backend="tikzfigure").generate_tikz() + + assert "mark=*" in tikz + assert "fill=orange" in tikz + assert "fill=cyan" in tikz + assert tikz.count("coordinates") >= 10 + + def test_canvas_matplotlib_gridspec_kw_affects_row_spacing(): """Test that hspace changes the vertical spacing between rows.""" import matplotlib.pyplot as plt @@ -160,7 +216,7 @@ def test_canvas_matplotlib_gridspec_kw_affects_2x2_line_spacing(): for ax in row_axes: ax.plot(x, (idx + 1) * x) idx += 1 - tight_fig, tight_matplotlib_axes = tight_canvas.plot(backend="matplotlib") + tight_fig, tight_matplotlib_axes = tight_canvas.render(backend="matplotlib") tight_hgap = ( tight_matplotlib_axes[0, 1].get_position().x0 - tight_matplotlib_axes[0, 0].get_position().x1 @@ -183,7 +239,7 @@ def test_canvas_matplotlib_gridspec_kw_affects_2x2_line_spacing(): for ax in row_axes: ax.plot(x, (idx + 1) * x) idx += 1 - loose_fig, loose_matplotlib_axes = loose_canvas.plot(backend="matplotlib") + loose_fig, loose_matplotlib_axes = loose_canvas.render(backend="matplotlib") loose_hgap = ( loose_matplotlib_axes[0, 1].get_position().x0 - loose_matplotlib_axes[0, 0].get_position().x1 @@ -222,7 +278,7 @@ def test_canvas_matplotlib_gridspec_kw_affects_2x2_imshow_spacing(): ax.add_imshow(data + idx, cmap="viridis") ax.set_title(f"Heatmap {idx + 1}") idx += 1 - tight_fig, tight_matplotlib_axes = tight_canvas.plot(backend="matplotlib") + tight_fig, tight_matplotlib_axes = tight_canvas.render(backend="matplotlib") tight_hgap = ( tight_matplotlib_axes[0, 1].get_position().x0 - tight_matplotlib_axes[0, 0].get_position().x1 @@ -246,7 +302,7 @@ def test_canvas_matplotlib_gridspec_kw_affects_2x2_imshow_spacing(): ax.add_imshow(data + idx, cmap="viridis") ax.set_title(f"Heatmap {idx + 1}") idx += 1 - loose_fig, loose_matplotlib_axes = loose_canvas.plot(backend="matplotlib") + loose_fig, loose_matplotlib_axes = loose_canvas.render(backend="matplotlib") loose_hgap = ( loose_matplotlib_axes[0, 1].get_position().x0 - loose_matplotlib_axes[0, 0].get_position().x1 @@ -355,6 +411,27 @@ def test_canvas_show_uses_matplotlib_show(monkeypatch): assert axes is not None +def test_canvas_show_plotly_does_not_return_displayed_figure_in_jupyter(monkeypatch): + import maxplotlib.canvas.canvas as canvas_module + from maxplotlib import Canvas + + class FakePlotlyFigure: + def __init__(self): + self.show_calls = 0 + + def show(self): + self.show_calls += 1 + + figure = FakePlotlyFigure() + monkeypatch.setattr(Canvas, "plot_plotly", lambda *args, **kwargs: figure) + monkeypatch.setattr(canvas_module, "_running_in_jupyter", lambda: True) + + result = Canvas().show(backend="plotly") + + assert result is None + assert figure.show_calls == 1 + + def test_canvas_show_uses_ipython_display_in_jupyter(monkeypatch): import sys import types @@ -376,7 +453,7 @@ def test_canvas_show_uses_ipython_display_in_jupyter(monkeypatch): monkeypatch.setitem(sys.modules, "IPython", ipython) monkeypatch.setitem(sys.modules, "IPython.display", ipython_display) monkeypatch.setattr(plt, "close", lambda value: closed.append(value)) - monkeypatch.setattr(Canvas, "plot", lambda *args, **kwargs: (fig, object())) + monkeypatch.setattr(Canvas, "_render", lambda *args, **kwargs: (fig, object())) monkeypatch.setattr(plt, "show", lambda: pytest.fail("pyplot.show was called")) canvas = Canvas() @@ -636,7 +713,6 @@ def test_vector_and_triangulated_plot_primitives_are_supported(): def test_stream_matrix_and_table_primitives_are_supported(): - import matplotlib.pyplot as plt import numpy as np from maxplotlib import Canvas @@ -673,7 +749,7 @@ def test_contour_labels_and_rasterization_zorder_are_supported(): axis.clabel(inline=True, fontsize=8) axis.set_rasterization_zorder(2) - fig, axes = canvas.plot(backend="matplotlib") + fig, axes = canvas.render(backend="matplotlib") assert len(axes[0, 0].texts) > 0 assert axes[0, 0].get_rasterization_zorder() == 2 @@ -682,7 +758,6 @@ def test_contour_labels_and_rasterization_zorder_are_supported(): def test_axis_layout_and_log_shortcuts_are_supported(): import matplotlib.pyplot as plt - import numpy as np from maxplotlib import Canvas @@ -696,7 +771,7 @@ def test_axis_layout_and_log_shortcuts_are_supported(): axis.set_xticklabels(["one", "two", "four"], rotation=30) axis.set_yticklabels(["low", "high"], color="navy") - fig, axes = canvas.plot(backend="matplotlib") + fig, axes = canvas.render(backend="matplotlib") matplotlib_axis = axes[0, 0] assert matplotlib_axis.get_xscale() == "log" @@ -720,12 +795,244 @@ def test_secondary_axes_are_supported_by_matplotlib(): "right", functions=(lambda y: y + 1, lambda y: y - 1), label="offset" ) - fig, axes = canvas.plot(backend="matplotlib") + fig, axes = canvas.render(backend="matplotlib") assert len(axes[0, 0].child_axes) == 2 plt.close(fig) +def test_twiny_is_supported_by_matplotlib(): + import matplotlib.pyplot as plt + + from maxplotlib import Canvas + + canvas, axis = Canvas.subplots() + secondary = canvas.twiny() + axis.plot([0, 1], [0, 1]) + secondary.plot([10, 20], [0, 1], color="red") + + fig, axes = canvas.render(backend="matplotlib") + + assert canvas.twiny_axes[(0, 0)] is not axes[0, 0] + plt.close(fig) + + +def test_axis_state_setter_aliases_are_supported(): + import matplotlib.pyplot as plt + + from maxplotlib import Canvas + + canvas, axis = Canvas.subplots() + axis.plot([0, 1], [0, 1]) + axis.set_frame_on(False) + axis.set_visible(True) + axis.set_alpha(0.8) + axis.set_zorder(3) + axis.set_rasterized(True) + axis.set_autoscale_on(True) + axis.set_autoscalex_on(False) + axis.set_autoscaley_on(True) + axis.set_autoscale_on(False) + axis.set_xbound(-1, 2) + axis.set_ybound(-2, 3) + + fig, axes = canvas.render(backend="matplotlib") + matplotlib_axis = axes[0, 0] + + assert matplotlib_axis.get_frame_on() is False + assert matplotlib_axis.get_visible() is True + assert matplotlib_axis.get_alpha() == 0.8 + assert matplotlib_axis.get_zorder() == 3 + assert matplotlib_axis.get_rasterized() is True + assert matplotlib_axis.get_xlim() == (-1, 2) + assert matplotlib_axis.get_ylim() == (-2, 3) + plt.close(fig) + + +def test_figure_size_and_dpi_setters_are_supported(): + import pytest + + from maxplotlib import Canvas + + canvas, _ = Canvas.subplots(figsize=(4, 3), dpi=120) + assert canvas.get_size_inches() == pytest.approx([4, 3]) + assert canvas.get_figwidth() == 4 + assert canvas.get_figheight() == 3 + assert canvas.get_dpi() == 120 + + canvas.set_figwidth(5) + canvas.set_figheight(2) + canvas.set_dpi(150) + + assert canvas.get_size_inches() == pytest.approx([5, 2]) + assert canvas.get_dpi() == 150 + + +def test_generic_axis_setters_and_metadata_getters_are_supported(): + import matplotlib.pyplot as plt + + from maxplotlib import Canvas + + canvas, axis = Canvas.subplots() + canvas.suptitle("Figure title") + canvas.supxlabel("Shared x") + canvas.supylabel("Shared y") + axis.plot([0, 1], [0, 1]) + axis.set(fc="lavender", adjustable="box", anchor="C") + axis.update({"aspect": "equal"}) + axis.invert_xaxis() + + fig, axes = canvas.render(backend="matplotlib") + matplotlib_axis = axes[0, 0] + + assert canvas.get_suptitle() == "Figure title" + assert canvas.get_supxlabel() == "Shared x" + assert canvas.get_supylabel() == "Shared y" + assert len(canvas.get_axes()) == 1 + assert canvas.xaxis_inverted() is True + assert matplotlib_axis.get_adjustable() == "box" + assert matplotlib_axis.get_anchor() == "C" + plt.close(fig) + + +def test_figure_layout_helpers_and_aliases_are_supported(): + import matplotlib.pyplot as plt + + from maxplotlib import Canvas + + canvas, axis = Canvas.subplots() + axis.plot([0, 1], [0, 1], label="line") + axis.set_legend(loc="upper left") + axis.add_table(cellText=[["A"]]) + axis.add_image([[1, 2], [3, 4]]) + canvas.set_tight_layout(True) + canvas.align_labels() + canvas.align_titles() + canvas.align_xlabels() + canvas.align_ylabels() + canvas.autofmt_xdate(rotation=20) + + fig, axes = canvas.render(backend="matplotlib") + + assert axes[0, 0].get_legend() is not None + assert len(axes[0, 0].tables) == 1 + assert len(axes[0, 0].images) == 1 + plt.close(fig) + + +def test_axis_getters_reflect_configured_state(): + from maxplotlib import Canvas + + canvas, axis = Canvas.subplots() + axis.set_adjustable("datalim") + axis.set_anchor("SW") + axis.set_alpha(0.5) + axis.set_box_aspect(1.2) + axis.set_facecolor("pink") + axis.set_frame_on(False) + axis.set_legend(True) + axis.set_rasterization_zorder(4) + axis.set_rasterized(True) + axis.set_visible(False) + axis.set_zorder(7) + axis.set_xbound(-1, 2) + axis.set_ybound(-2, 3) + axis.set_xmargin(0.1) + axis.set_ymargin(0.2) + + assert canvas.get_adjustable() == "datalim" + assert canvas.get_anchor() == "SW" + assert canvas.get_alpha() == 0.5 + assert canvas.get_box_aspect() == 1.2 + assert canvas.get_facecolor() == "pink" + assert canvas.get_frame_on() is False + assert canvas.get_legend() is True + assert canvas.get_rasterization_zorder() == 4 + assert canvas.get_rasterized() is True + assert canvas.get_visible() is False + assert canvas.get_zorder() == 7 + assert canvas.get_xbound() == (-1, 2) + assert canvas.get_ybound() == (-2, 3) + assert canvas.get_xmargin() == 0.1 + assert canvas.get_ymargin() == 0.2 + + +def test_render_is_the_explicit_rendering_alias(): + from maxplotlib import Canvas + + canvas = Canvas() + canvas.add_line([0, 1], [0, 1]) + + rendered = canvas.render(backend="plotly") + + assert rendered is not None + + +def test_plot_adds_line_data_when_given_x_and_y(): + from maxplotlib import Canvas + + canvas = Canvas() + result = canvas.plot([0, 1], [1, 2], color="purple", label="line") + + assert result is canvas + assert canvas.render(backend="plotly").data[0].name == "line" + + +def test_legacy_plot_backend_form_warns(): + import pytest + + from maxplotlib import Canvas + + canvas = Canvas() + canvas.add_line([0, 1], [0, 1]) + + with pytest.warns(FutureWarning, match=r"canvas\.render"): + canvas.plot(backend="plotly") + + +def test_plot_many_adds_labeled_lines_and_returns_canvas(): + from maxplotlib import Canvas + + canvas = Canvas() + result = canvas.plot_many( + [([0, 1], [0, 1]), ([0, 1], [1, 0])], + labels=["up", "down"], + ) + + assert result is canvas + assert [ + line["kwargs"]["label"] + for line in canvas._subplot_matrix[0][0].layered_line_data[0] + ] == [ + "up", + "down", + ] + + +def test_configure_applies_common_canvas_settings(): + from maxplotlib import Canvas + + canvas = Canvas() + canvas.plot([0, 1], [0, 1]).configure( + title="Example", + xlabel="Time", + ylabel="Value", + grid=True, + facecolor="whitesmoke", + xlim=(0, 2), + ylim=(-1, 2), + ) + + subplot = canvas._subplot_matrix[0][0] + assert canvas._suptitle == "Example" + assert canvas._supxlabel == "Time" + assert canvas._supylabel == "Value" + assert subplot._grid is True + assert subplot._facecolor == "whitesmoke" + assert (subplot._xmin, subplot._xmax) == (0, 2) + assert (subplot._ymin, subplot._ymax) == (-1, 2) + + def test_matplotlib_postprocess_can_customize_figure_and_axes(): import matplotlib.pyplot as plt from matplotlib.colors import to_rgba @@ -808,7 +1115,7 @@ def test_matplotlib_postprocess_rejects_non_matplotlib_backends(): from maxplotlib import Canvas with pytest.raises(ValueError, match="only supported with the matplotlib backend"): - Canvas().plot(backend="plotly", matplotlib_postprocess=lambda fig, axes: None) + Canvas().render(backend="plotly", matplotlib_postprocess=lambda fig, axes: None) def test_show_canvas_script_invokes_canvas_show(monkeypatch): diff --git a/src/maxplotlib/tests/test_plot.py b/src/maxplotlib/tests/test_plot.py index 7667ed3..3e65d65 100644 --- a/src/maxplotlib/tests/test_plot.py +++ b/src/maxplotlib/tests/test_plot.py @@ -19,7 +19,7 @@ def test_python_example_nxm_line_subplots_spacing_changes(): for i, row in enumerate(tight_axes): for j, ax in enumerate(row): ax.plot(x, np.sin((i + 1) * (j + 1) * x)) - tight_fig, tight_m_axes = tight_canvas.plot(backend="matplotlib") + tight_fig, tight_m_axes = tight_canvas.render(backend="matplotlib") tight_hgap = ( tight_m_axes[0, 1].get_position().x0 - tight_m_axes[0, 0].get_position().x1 ) @@ -38,7 +38,7 @@ def test_python_example_nxm_line_subplots_spacing_changes(): for i, row in enumerate(loose_axes): for j, ax in enumerate(row): ax.plot(x, np.sin((i + 1) * (j + 1) * x)) - loose_fig, loose_m_axes = loose_canvas.plot(backend="matplotlib") + loose_fig, loose_m_axes = loose_canvas.render(backend="matplotlib") loose_hgap = ( loose_m_axes[0, 1].get_position().x0 - loose_m_axes[0, 0].get_position().x1 ) @@ -69,7 +69,7 @@ def test_python_example_nxm_color_subplots_spacing_changes(): for ax in row: ax.add_imshow(base + idx, cmap="viridis") idx += 1 - tight_fig, tight_m_axes = tight_canvas.plot(backend="matplotlib") + tight_fig, tight_m_axes = tight_canvas.render(backend="matplotlib") tight_hgap = ( tight_m_axes[0, 1].get_position().x0 - tight_m_axes[0, 0].get_position().x1 ) @@ -90,7 +90,7 @@ def test_python_example_nxm_color_subplots_spacing_changes(): for ax in row: ax.add_imshow(base + idx, cmap="viridis") idx += 1 - loose_fig, loose_m_axes = loose_canvas.plot(backend="matplotlib") + loose_fig, loose_m_axes = loose_canvas.render(backend="matplotlib") loose_hgap = ( loose_m_axes[0, 1].get_position().x0 - loose_m_axes[0, 0].get_position().x1 ) diff --git a/src/maxplotlib/tests/test_plotext.py b/src/maxplotlib/tests/test_plotext.py index ee34a63..9522890 100644 --- a/src/maxplotlib/tests/test_plotext.py +++ b/src/maxplotlib/tests/test_plotext.py @@ -2,6 +2,7 @@ import matplotlib.patches as mpatches import numpy as np +import pytest from maxplotlib import Canvas from maxplotlib.backends.plotext import PlotextFigure @@ -27,7 +28,7 @@ def test_canvas_plot_plotext_builds_terminal_output(): ax.set_legend(True) canvas.suptitle("Plotext demo") - figure = canvas.plot(backend="plotext") + figure = canvas.render(backend="plotext") output = strip_ansi(figure.build()) assert isinstance(figure, PlotextFigure) @@ -57,7 +58,7 @@ def test_canvas_plot_plotext_supports_scalar_errorbars(): ax.set_xscale("log") ax.set_title("Log errors") - output = strip_ansi(canvas.plot(backend="plotext").build()) + output = strip_ansi(canvas.render(backend="plotext").build()) assert "Log errors" in output @@ -72,7 +73,7 @@ def test_canvas_plot_plotext_supports_fill_between_curves_and_annotations(): ax.set_title("Filled band") ax.set_legend(True) - output = strip_ansi(canvas.plot(backend="plotext").build()) + output = strip_ansi(canvas.render(backend="plotext").build()) assert "Filled band" in output assert "band" in output @@ -88,7 +89,7 @@ def test_canvas_plot_plotext_supports_matrix_plots_and_patches(): ax.add_patch(mpatches.Circle((1.8, 1.8), 0.4, fill=False, edgecolor="cyan")) ax.set_title("Matrix plot") - output = strip_ansi(canvas.plot(backend="plotext").build()) + output = strip_ansi(canvas.render(backend="plotext").build()) assert "Matrix plot" in output @@ -98,7 +99,7 @@ def test_canvas_plot_plotext_supports_colorbar_notes_symlog_aspect_and_generic_p ax.add_imshow(np.eye(3)) ax.add_colorbar(label="scale") ax.set_title("Heatmap") - output = strip_ansi(canvas.plot(backend="plotext").build()) + output = strip_ansi(canvas.render(backend="plotext").build()) assert "Heatmap" in output assert "scale:" in output @@ -111,7 +112,7 @@ def test_canvas_plot_plotext_supports_colorbar_notes_symlog_aspect_and_generic_p ax.set_aspect("equal") ax.add_caption("caption text") ax.set_title("Symlog view") - output = strip_ansi(canvas.plot(backend="plotext").build()) + output = strip_ansi(canvas.render(backend="plotext").build()) assert "Symlog view" in output assert "caption text" in output @@ -129,7 +130,160 @@ def test_canvas_plot_plotext_supports_colorbar_notes_symlog_aspect_and_generic_p ) ax.set_title("Generic patch") ax.set_legend(True) - output = strip_ansi(canvas.plot(backend="plotext").build()) + output = strip_ansi(canvas.render(backend="plotext").build()) assert "Generic patch" in output assert "ellipse" in output + + +def test_plotext_supports_axis_controls_and_line_primitives(): + canvas, ax = Canvas.subplots() + ax.plot([0, 1], [0, 1], label="line") + ax.hlines([0.25, 0.75], 0, 1, color="yellow") + ax.vlines([0.25, 0.75], 0, 1, color="cyan") + ax.axhline(0.5, color="red") + ax.axvline(0.5, color="blue") + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.set_xticks([0, 0.5, 1], labels=["left", "middle", "right"]) + ax.set_yticks([0, 0.5, 1], labels=["low", "mid", "high"]) + ax.set_title("Axis controls") + ax.set_xlabel("horizontal") + ax.set_ylabel("vertical") + ax.set_grid(True) + + output = strip_ansi(canvas.render(backend="plotext").build()) + + for text in ( + "Axis controls", + "horizontal", + "vertical", + "left", + "middle", + "right", + "low", + "mid", + "high", + ): + assert text in output + + +def test_plotext_supports_multiple_subplots(): + canvas, axes = Canvas.subplots(nrows=1, ncols=2) + axes[0].plot([0, 1], [0, 1]) + axes[0].set_title("left subplot") + axes[1].bar([0, 1], [1, 2]) + axes[1].set_title("right subplot") + + output = strip_ansi(canvas.render(backend="plotext").build()) + + assert "left subplot" in output + assert "right subplot" in output + + +def test_plotext_layer_filtering_changes_rendered_content(): + canvas, ax = Canvas.subplots() + ax.plot([0, 1], [0, 1], label="first layer", layer=0) + ax.plot([0, 1], [1, 0], label="second layer", layer=1) + ax.set_legend(True) + + first = strip_ansi(canvas.render(backend="plotext", layers=[0]).build()) + second = strip_ansi(canvas.render(backend="plotext", layers=[1]).build()) + + assert "first layer" in first + assert "second layer" not in first + assert "second layer" in second + assert "first layer" not in second + + +def test_plotext_savefig_writes_plain_text_and_supports_append(tmp_path): + canvas, ax = Canvas.subplots() + ax.plot([0, 1], [0, 1]) + ax.set_title("Saved output") + output_file = tmp_path / "plot.txt" + + figure = canvas.render(backend="plotext") + figure.savefig(output_file, keep_colors=False) + first = output_file.read_text(encoding="utf-8") + figure.savefig(output_file, append=True, keep_colors=False) + combined = output_file.read_text(encoding="utf-8") + + assert "Saved output" in first + assert "\x1b[" not in first + assert combined == first + first + + +def test_plotext_rejects_unsupported_twinx(): + canvas, ax = Canvas.subplots() + ax.plot([0, 1], [0, 1]) + canvas.twinx() + + with pytest.raises(NotImplementedError, match="twinx"): + canvas.render(backend="plotext") + + +def test_plotext_supports_asymmetric_errors_baseline_fill_and_annotations(): + canvas, ax = Canvas.subplots() + x = np.arange(4) + ax.fill_between(x, [1, 2, 3, 2], 0, label="baseline") + ax.errorbar( + x, + [1, 2, 1, 3], + xerr=[[0.1, 0.2, 0.1, 0.2], [0.2, 0.1, 0.2, 0.1]], + yerr=[[0.2, 0.1, 0.2, 0.1], [0.1, 0.2, 0.1, 0.2]], + label="measurements", + ) + ax.annotate("peak", xy=(3, 3), xytext=(2, 2.5), arrowprops={"color": "red"}) + ax.set_title("Uncertainty and annotations") + ax.set_legend(True) + + output = strip_ansi(canvas.render(backend="plotext").build()) + + assert "Uncertainty and annotations" in output + assert "baseline" in output + assert "measurements" in output + assert "peak" in output + + +def test_plotext_supports_gantt_and_flame_chart_labels(): + canvas, ax = Canvas.subplots() + ax.gantt( + ["plan", "build", "test"], + [0, 1, 3], + [1, 2, 1], + ) + ax.set_title("Gantt") + gantt_output = strip_ansi(canvas.render(backend="plotext").build()) + + canvas, ax = Canvas.subplots() + ax.flame_chart( + labels=["root", "worker", "io"], + parents=[None, 0, 1], + values=[5, 3, 1], + start_times=[0, 0, 2], + ) + ax.set_title("Flame") + flame_output = strip_ansi(canvas.render(backend="plotext").build()) + + assert "Gantt" in gantt_output + assert all(label in gantt_output for label in ("plan", "build", "test")) + assert "Flame" in flame_output + + +def test_plotext_rejects_unsupported_plot_types_and_imshow_options(): + canvas, ax = Canvas.subplots() + ax.hist([1, 2, 2, 3]) + with pytest.raises(NotImplementedError, match="plot type: hist"): + canvas.render(backend="plotext") + + canvas, ax = Canvas.subplots() + ax.add_imshow(np.eye(3), cmap="viridis") + with pytest.raises(NotImplementedError, match="imshow kwargs: cmap"): + canvas.render(backend="plotext") + + +def test_plotext_empty_canvas_still_builds(): + figure = Canvas().render(backend="plotext") + + assert isinstance(figure, PlotextFigure) + assert isinstance(figure.build(keep_colors=False), str) diff --git a/src/maxplotlib/tests/test_plotly_backend.py b/src/maxplotlib/tests/test_plotly_backend.py index 9fde583..8b0f3a9 100644 --- a/src/maxplotlib/tests/test_plotly_backend.py +++ b/src/maxplotlib/tests/test_plotly_backend.py @@ -18,7 +18,7 @@ def test_plotly_backend_supports_common_primitives(): ax.set_grid(True) ax.set_legend(True) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert fig is not None assert len(fig.data) >= 4 # line, scatter, errorbar, fill_between @@ -35,7 +35,7 @@ def test_plotly_backend_supports_tick_label_rotation(): axis.plot([0, 1], [0, 1]) axis.set_xticks([0, 1], labels=["zero", "one"], rotation=45) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert fig.layout.xaxis.tickangle == 45 @@ -49,7 +49,7 @@ def test_plotly_backend_supports_twinx(): secondary.plot([0, 1], [10, 20], color="red") secondary.set_ylabel("Secondary") - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert len(fig.data) == 2 assert fig.layout.yaxis2.title.text == "Secondary" @@ -66,7 +66,7 @@ def test_plotly_backend_supports_common_added_primitives(): axis.axhspan(0.25, 0.75, alpha=0.1) axis.arrow(0, 0, 1, 1) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert len(fig.data) >= 3 assert len(fig.layout.shapes) >= 2 @@ -82,7 +82,7 @@ def test_plotly_backend_supports_step_stairs_broken_barh_and_pie(): axis.broken_barh([(0, 1), (2, 0.5)], (0, 0.5)) axis.pie([2, 3, 4], labels=["A", "B", "C"]) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert any(trace.type == "pie" for trace in fig.data) assert len(fig.data) >= 5 @@ -98,7 +98,7 @@ def test_plotly_backend_supports_statistical_and_event_plots(): axis.violinplot([[1, 2, 3], [2, 4, 5]]) axis.eventplot([[0.2, 0.5], [1.0, 1.5]]) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert any(trace.type == "box" for trace in fig.data) assert any(trace.type == "violin" for trace in fig.data) @@ -122,7 +122,7 @@ def test_plotly_backend_supports_scientific_field_plots(): axis.hexbin(xx.ravel(), yy.ravel(), gridsize=5) axis.matshow(z) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert any(trace.type == "contour" for trace in fig.data) assert any(trace.type == "heatmap" for trace in fig.data) @@ -138,7 +138,7 @@ def test_plotly_backend_supports_contour_labels(): axis.contour(x, x, xx**2 + yy**2) axis.clabel(fontsize=10, color="black") - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert fig.data[0].contours.showlabels is True assert fig.data[0].contours.labelfont.size == 10 @@ -152,7 +152,7 @@ def test_plotly_backend_supports_fill_log_scales_and_ticklabels(): axis.loglog([1, 2, 4], [1, 4, 16]) axis.set_xticklabels(["one", "two", "four"], color="navy") - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert any(trace.fill == "toself" for trace in fig.data) assert fig.layout.xaxis.type == "log" @@ -168,8 +168,8 @@ def test_plotly_backend_respects_layers(): ax.plot(x, x, color="black", label="L0", layer=0) ax.plot(x, x**2, color="red", label="L1", layer=1) - fig0 = canvas.plot(backend="plotly", layers=[0]) - fig1 = canvas.plot(backend="plotly", layers=[1]) + fig0 = canvas.render(backend="plotly", layers=[0]) + fig1 = canvas.render(backend="plotly", layers=[1]) assert len(fig0.data) == 1 assert len(fig1.data) == 1 @@ -203,7 +203,7 @@ def test_plotly_backend_supports_common_patches_and_symlog(): ax.set_title("patches") ax.set_legend(True) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert fig is not None assert len(getattr(fig.layout, "shapes", []) or []) >= 4 # patch labels become dummy legend traces @@ -214,7 +214,7 @@ def test_plotly_backend_supports_common_patches_and_symlog(): ax2.plot(x, x**3, color="cyan", label="x^3") ax2.set_xscale("symlog") ax2.set_yscale("symlog") - fig2 = canvas2.plot(backend="plotly") + fig2 = canvas2.render(backend="plotly") assert fig2 is not None @@ -230,7 +230,7 @@ def test_plotly_backend_renders_mixed_vector_primitives(): np.ones((3, 3)), ) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert len(fig.data) > 1 @@ -241,7 +241,7 @@ def test_plotly_backend_supports_bar_labels(): axis.bar([0, 1], [2, 3]) axis.bar_label(fmt="%d") - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") labels = [annotation.text for annotation in fig.layout.annotations] assert labels[-2:] == ["2", "3"] @@ -264,7 +264,7 @@ def test_plotly_backend_supports_pseudocolor_spy_table_and_triplot(): axis.table(cellText=[["A", "B"], ["1", "2"]]) axis.triplot(points_x, points_y, triangles=triangles) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert sum(trace.type == "heatmap" for trace in fig.data) >= 3 assert any(trace.type == "table" for trace in fig.data) @@ -277,7 +277,7 @@ def test_plotly_backend_supports_quiver(): canvas, axis = Canvas.subplots() axis.quiver([0, 1], [0, 1], [1, -1], [1, 1], color="purple", alpha=0.5) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") arrows = [ annotation for annotation in fig.layout.annotations if annotation.showarrow @@ -297,7 +297,7 @@ def test_plotly_backend_supports_tripcolor(): triangles=[[0, 1, 2], [1, 3, 2]], ) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert sum(trace.fill == "toself" for trace in fig.data) == 2 @@ -313,7 +313,7 @@ def test_plotly_backend_supports_triangulated_contours(): axis.tricontour(x, y, values, triangles=triangles, levels=3) axis.tricontourf(x, y, values, triangles=triangles, levels=3) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert any(trace.fill is None for trace in fig.data) assert any(trace.fill == "toself" for trace in fig.data) @@ -332,7 +332,7 @@ def test_plotly_backend_supports_streamplot(): color="darkgreen", ) - fig = canvas.plot(backend="plotly") + fig = canvas.render(backend="plotly") assert len(fig.data) > 0 assert all(trace.type == "scatter" for trace in fig.data) diff --git a/tutorials/tutorial_01.ipynb b/tutorials/tutorial_01.ipynb index 964b647..21430b5 100644 --- a/tutorials/tutorial_01.ipynb +++ b/tutorials/tutorial_01.ipynb @@ -79,8 +79,8 @@ "x = np.linspace(0, 2 * np.pi, 200)\n", "y = np.sin(x)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, y)\n", + "canvas = Canvas()\n", + "canvas.plot(x, y)\n", "canvas.show(backend=BACKEND)" ] }, @@ -99,11 +99,15 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", + "canvas = Canvas()\n", "\n", - "ax.plot(x, np.sin(x), label=\"sin(x)\", color=\"royalblue\", linestyle=\"solid\", linewidth=2)\n", - "ax.plot(x, np.cos(x), label=\"cos(x)\", color=\"tomato\", linestyle=\"dashed\", linewidth=2)\n", - "ax.plot(\n", + "canvas.plot(\n", + " x, np.sin(x), label=\"sin(x)\", color=\"royalblue\", linestyle=\"solid\", linewidth=2\n", + ")\n", + "canvas.plot(\n", + " x, np.cos(x), label=\"cos(x)\", color=\"tomato\", linestyle=\"dashed\", linewidth=2\n", + ")\n", + "canvas.plot(\n", " x,\n", " np.sin(2 * x),\n", " label=\"sin(2x)\",\n", @@ -112,10 +116,10 @@ " linewidth=1.5,\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Sine and Cosine\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Sine and Cosine\")\n", + "canvas.set_legend(True)\n", "\n", "canvas.show(backend=BACKEND)" ] @@ -140,8 +144,8 @@ "source": [ "canvas = Canvas(ratio=0.5, fontsize=12)\n", "\n", - "canvas.add_line(x, np.sin(x), label=\"sin(x)\", color=\"steelblue\")\n", - "canvas.add_line(x, np.cos(x), label=\"cos(x)\", color=\"darkorange\", linestyle=\"dashed\")\n", + "canvas.plot(x, np.sin(x), label=\"sin(x)\", color=\"steelblue\")\n", + "canvas.plot(x, np.cos(x), label=\"cos(x)\", color=\"darkorange\", linestyle=\"dashed\")\n", "\n", "canvas.set_xlabel(\"angle (rad)\")\n", "canvas.set_ylabel(\"amplitude\")\n", @@ -179,8 +183,8 @@ " legend=True,\n", ")\n", "\n", - "ax.plot(x, np.sin(x), label=\"sin\", color=\"royalblue\")\n", - "ax.plot(x, x / (2 * np.pi), label=\"x/2π\", color=\"coral\", linestyle=\"dashed\")\n", + "canvas.plot(x, np.sin(x), label=\"sin\", color=\"royalblue\")\n", + "canvas.plot(x, x / (2 * np.pi), label=\"x/2π\", color=\"coral\", linestyle=\"dashed\")\n", "\n", "canvas.show(backend=BACKEND)" ] @@ -205,7 +209,7 @@ "source": [ "canvas = Canvas(ratio=0.5)\n", "ax = canvas.add_subplot(xlabel=\"x\", ylabel=\"sin(x)\", grid=True)\n", - "ax.plot(x, np.sin(x), color=\"steelblue\")\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\")\n", "\n", "canvas.savefig(\"tutorial_01_output.png\")\n", "print(\"Figure saved to tutorial_01_output.png\")" @@ -220,11 +224,11 @@ "\n", "| Task | Code |\n", "|---|---|\n", - "| Create canvas + subplot | `canvas, ax = Canvas.subplots()` |\n", - "| Add a line | `ax.plot(x, y, label=..., color=..., linestyle=...)` |\n", - "| Canvas shortcut | `canvas.add_line(x, y, ...)` |\n", - "| Labels / title | `ax.set_xlabel()`, `ax.set_ylabel()`, `ax.set_title()` |\n", - "| Legend / grid | `ax.set_legend(True)`, `ax.set_grid(True)` |\n", + "| Create a canvas | `canvas = Canvas()` |\n", + "| Add a line | `canvas.plot(x, y, label=..., color=..., linestyle=...)` |\n", + "| Optional Matplotlib-style axes | `canvas, ax = Canvas.subplots()` |\n", + "| Labels / title | `canvas.set_xlabel()`, `canvas.set_ylabel()`, `canvas.set_title()` |\n", + "| Legend / grid | `canvas.set_legend(True)`, `canvas.set_grid(True)` |\n", "| Display | `canvas.show()` |\n", "| Save | `canvas.savefig('out.png')` |\n", "\n", diff --git a/tutorials/tutorial_02.ipynb b/tutorials/tutorial_02.ipynb index 06f79ae..e809ee6 100644 --- a/tutorials/tutorial_02.ipynb +++ b/tutorials/tutorial_02.ipynb @@ -321,8 +321,8 @@ "canvas.add_subplot(row=0, col=0, title=\"Left\", xlabel=\"x\", ylabel=\"sin\")\n", "canvas.add_subplot(row=0, col=1, title=\"Right\", xlabel=\"x\", ylabel=\"cos\")\n", "\n", - "canvas.add_line(x, np.sin(x), row=0, col=0, color=\"royalblue\", label=\"sin\")\n", - "canvas.add_line(x, np.cos(x), row=0, col=1, color=\"tomato\", label=\"cos\")\n", + "canvas.plot(x, np.sin(x), row=0, col=0, color=\"royalblue\", label=\"sin\")\n", + "canvas.plot(x, np.cos(x), row=0, col=1, color=\"tomato\", label=\"cos\")\n", "\n", "canvas.set_legend(True, row=0, col=0)\n", "canvas.set_legend(True, row=0, col=1)\n", @@ -346,7 +346,7 @@ "| Get subplot | `canvas.subplot(r, c)` or `canvas[r, c]` |\n", "| Loop panels | `for row, col, sp in canvas.iter_subplots()` |\n", "| Figure title | `canvas.suptitle('...')` |\n", - "| Route plot | `canvas.add_line(x, y, row=r, col=c)` |\n", + "| Route plot | `canvas.plot(x, y, row=r, col=c)` |\n", "\n", "Next: **Tutorial 03** covers all the available plot types." ] diff --git a/tutorials/tutorial_03.ipynb b/tutorials/tutorial_03.ipynb index c3b8e12..f6b4c18 100644 --- a/tutorials/tutorial_03.ipynb +++ b/tutorials/tutorial_03.ipynb @@ -64,7 +64,7 @@ "id": "4", "metadata": {}, "source": [ - "## 1 Line plot — `ax.plot()`" + "## 1 Line plot — `canvas.plot()`" ] }, { @@ -74,16 +74,20 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", + "canvas = Canvas()\n", "\n", - "ax.plot(x, np.sin(x), label=\"sin(x)\", color=\"royalblue\", linestyle=\"solid\", linewidth=2)\n", - "ax.plot(x, np.cos(x), label=\"cos(x)\", color=\"tomato\", linestyle=\"dashed\", linewidth=2)\n", + "canvas.plot(\n", + " x, np.sin(x), label=\"sin(x)\", color=\"royalblue\", linestyle=\"solid\", linewidth=2\n", + ")\n", + "canvas.plot(\n", + " x, np.cos(x), label=\"cos(x)\", color=\"tomato\", linestyle=\"dashed\", linewidth=2\n", + ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Line Plot\")\n", - "ax.set_legend(True)\n", - "ax.set_grid(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Line Plot\")\n", + "canvas.set_legend(True)\n", + "canvas.set_grid(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -109,12 +113,12 @@ "sy = rng.standard_normal(n)\n", "values = np.sqrt(sx**2 + sy**2) # colour by distance from origin\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.scatter(sx, sy, c=values, s=30, label=\"data points\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Scatter Plot — coloured by distance\")\n", - "ax.set_aspect(\"equal\")\n", + "canvas = Canvas()\n", + "canvas.scatter(sx, sy, c=values, s=30, label=\"data points\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Scatter Plot — coloured by distance\")\n", + "canvas.set_aspect(\"equal\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -136,12 +140,12 @@ "categories = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"]\n", "values_bar = [12, 19, 15, 22, 30, 27]\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.bar(categories, values_bar, color=\"steelblue\", width=0.6, label=\"monthly sales\")\n", - "ax.set_xlabel(\"Month\")\n", - "ax.set_ylabel(\"Sales (units)\")\n", - "ax.set_title(\"Bar Chart\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.bar(categories, values_bar, color=\"steelblue\", width=0.6, label=\"monthly sales\")\n", + "canvas.set_xlabel(\"Month\")\n", + "canvas.set_ylabel(\"Sales (units)\")\n", + "canvas.set_title(\"Bar Chart\")\n", + "canvas.set_legend(True)\n", "# canvas.show(backend=BACKEND) # TODO: Fix this error" ] }, @@ -167,13 +171,13 @@ "upper = mean + 0.3 * (1 - t / (4 * np.pi))\n", "lower = mean - 0.3 * (1 - t / (4 * np.pi))\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(t, mean, color=\"royalblue\", label=\"mean\", linewidth=2)\n", - "ax.fill_between(t, lower, upper, alpha=0.25, color=\"royalblue\", label=\"±1 std\")\n", - "ax.set_xlabel(\"t\")\n", - "ax.set_ylabel(\"value\")\n", - "ax.set_title(\"Fill Between — Confidence Band\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.plot(t, mean, color=\"royalblue\", label=\"mean\", linewidth=2)\n", + "canvas.fill_between(t, lower, upper, alpha=0.25, color=\"royalblue\", label=\"±1 std\")\n", + "canvas.set_xlabel(\"t\")\n", + "canvas.set_ylabel(\"value\")\n", + "canvas.set_title(\"Fill Between — Confidence Band\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -196,14 +200,16 @@ "ym = np.sin(xm) + rng.normal(0, 0.1, len(xm))\n", "yerr = 0.1 + 0.05 * rng.random(len(xm))\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.errorbar(xm, ym, yerr=yerr, fmt=\"o\", capsize=4, color=\"tomato\", label=\"measurements\")\n", - "ax.plot(x, np.sin(x), color=\"gray\", linestyle=\"dashed\", label=\"true sin(x)\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Error Bars\")\n", - "ax.set_legend(True)\n", - "ax.set_grid(True)\n", + "canvas = Canvas()\n", + "canvas.errorbar(\n", + " xm, ym, yerr=yerr, fmt=\"o\", capsize=4, color=\"tomato\", label=\"measurements\"\n", + ")\n", + "canvas.plot(x, np.sin(x), color=\"gray\", linestyle=\"dashed\", label=\"true sin(x)\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Error Bars\")\n", + "canvas.set_legend(True)\n", + "canvas.set_grid(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -224,17 +230,19 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"royalblue\", label=\"sin(x)\")\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"royalblue\", label=\"sin(x)\")\n", "\n", - "ax.axhline(y=0, color=\"black\", linestyle=\"solid\", linewidth=0.8)\n", - "ax.axhline(y=0.5, color=\"green\", linestyle=\"dashed\", linewidth=1.2, label=\"y = 0.5\")\n", - "ax.axhline(y=-0.5, color=\"green\", linestyle=\"dashed\", linewidth=1.2, label=\"y = -0.5\")\n", - "ax.axvline(x=np.pi, color=\"red\", linestyle=\"dotted\", linewidth=1.5, label=\"x = π\")\n", + "canvas.axhline(y=0, color=\"black\", linestyle=\"solid\", linewidth=0.8)\n", + "canvas.axhline(y=0.5, color=\"green\", linestyle=\"dashed\", linewidth=1.2, label=\"y = 0.5\")\n", + "canvas.axhline(\n", + " y=-0.5, color=\"green\", linestyle=\"dashed\", linewidth=1.2, label=\"y = -0.5\"\n", + ")\n", + "canvas.axvline(x=np.pi, color=\"red\", linestyle=\"dotted\", linewidth=1.5, label=\"x = π\")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_title(\"axhline / axvline\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_title(\"axhline / axvline\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -255,11 +263,11 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"lightgray\", linewidth=1)\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"lightgray\", linewidth=1)\n", "\n", "# Horizontal segments spanning half the x-range\n", - "ax.hlines(\n", + "canvas.hlines(\n", " y=[0.5, -0.5],\n", " xmin=0,\n", " xmax=np.pi,\n", @@ -269,7 +277,7 @@ ")\n", "\n", "# Vertical segments at specific x positions\n", - "ax.vlines(\n", + "canvas.vlines(\n", " x=[np.pi / 2, 3 * np.pi / 2],\n", " ymin=-1,\n", " ymax=1,\n", @@ -278,9 +286,9 @@ " label=\"vlines at π/2, 3π/2\",\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_title(\"hlines / vlines\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_title(\"hlines / vlines\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -311,20 +319,20 @@ "tidx = np.arange(0, len(t), 20)\n", "tx, ty = t[tidx], (signal + noise)[tidx]\n", "\n", - "canvas, ax = Canvas.subplots()\n", + "canvas = Canvas()\n", "\n", - "ax.fill_between(\n", + "canvas.fill_between(\n", " t, signal - band, signal + band, alpha=0.2, color=\"royalblue\", label=\"uncertainty\"\n", ")\n", - "ax.plot(t, signal, color=\"royalblue\", linewidth=2, label=\"model\")\n", - "ax.scatter(tx, ty, color=\"tomato\", s=25, marker=\"o\", label=\"measurements\")\n", - "ax.axhline(y=0, color=\"gray\", linestyle=\"dashed\", linewidth=0.8)\n", - "\n", - "ax.set_xlabel(\"time\")\n", - "ax.set_ylabel(\"amplitude\")\n", - "ax.set_title(\"Combined: line + fill_between + scatter\")\n", - "ax.set_legend(True)\n", - "ax.set_grid(True)\n", + "canvas.plot(t, signal, color=\"royalblue\", linewidth=2, label=\"model\")\n", + "canvas.scatter(tx, ty, color=\"tomato\", s=25, marker=\"o\", label=\"measurements\")\n", + "canvas.axhline(y=0, color=\"gray\", linestyle=\"dashed\", linewidth=0.8)\n", + "\n", + "canvas.set_xlabel(\"time\")\n", + "canvas.set_ylabel(\"amplitude\")\n", + "canvas.set_title(\"Combined: line + fill_between + scatter\")\n", + "canvas.set_legend(True)\n", + "canvas.set_grid(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -343,11 +351,11 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"royalblue\")\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"royalblue\")\n", "\n", "# Arrow annotation pointing to the peak\n", - "ax.annotate(\n", + "canvas.annotate(\n", " \"peak\",\n", " xy=(np.pi / 2, 1.0),\n", " xytext=(np.pi / 2 + 0.8, 0.7),\n", @@ -355,11 +363,11 @@ ")\n", "\n", "# Free-floating text label\n", - "ax.text(3 * np.pi / 2, 0.15, \"zero\\ncrossing\", ha=\"center\", fontsize=9)\n", + "canvas.text(3 * np.pi / 2, 0.15, \"zero\\ncrossing\", ha=\"center\", fontsize=9)\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"sin(x)\")\n", - "ax.set_title(\"Annotate and Text\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"sin(x)\")\n", + "canvas.set_title(\"Annotate and Text\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -372,7 +380,7 @@ "\n", "| Plot type | Method | Key kwargs |\n", "|---|---|---|\n", - "| Line | `ax.plot(x, y)` | `color`, `linestyle`, `linewidth`, `label` |\n", + "| Line | `canvas.plot(x, y)` | `color`, `linestyle`, `linewidth`, `label` |\n", "| Scatter | `ax.scatter(x, y)` | `c`, `s`, `marker`, `label` |\n", "| Bar | `ax.bar(x, height)` | `color`, `width`, `label` |\n", "| Filled band | `ax.fill_between(x, y1, y2)` | `alpha`, `color`, `label` |\n", diff --git a/tutorials/tutorial_04.ipynb b/tutorials/tutorial_04.ipynb index e7f3808..81209c1 100644 --- a/tutorials/tutorial_04.ipynb +++ b/tutorials/tutorial_04.ipynb @@ -67,16 +67,16 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", "\n", - "ax.plot(x, np.sin(x), color=\"steelblue\", label=\"named: steelblue\")\n", - "ax.plot(x, np.sin(x - 0.5), color=\"#e74c3c\", label=\"hex: #e74c3c\")\n", - "ax.plot(x, np.sin(x - 1.0), color=(0.2, 0.7, 0.3), label=\"RGB tuple\")\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=\"named: steelblue\")\n", + "canvas.plot(x, np.sin(x - 0.5), color=\"#e74c3c\", label=\"hex: #e74c3c\")\n", + "canvas.plot(x, np.sin(x - 1.0), color=(0.2, 0.7, 0.3), label=\"RGB tuple\")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Color options\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Color options\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -100,10 +100,10 @@ "x = np.linspace(0, 4 * np.pi, 300)\n", "styles = [(\"solid\", \"-\"), (\"dashed\", \"--\"), (\"dotted\", \":\"), (\"dashdot\", \"-.\")]\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", "\n", "for i, (name, ls) in enumerate(styles):\n", - " ax.plot(\n", + " canvas.plot(\n", " x,\n", " np.sin(x) + i * 0.4,\n", " linestyle=ls,\n", @@ -112,9 +112,9 @@ " label=f\"{name!r} / {ls!r}\",\n", " )\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_title(\"Linestyle comparison\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_title(\"Linestyle comparison\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -138,10 +138,10 @@ "x = np.linspace(0, 2 * np.pi, 10)\n", "markers = [\"o\", \"s\", \"^\", \"D\", \"*\", \"x\", \"+\"]\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.65)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.65)\n", "\n", "for i, m in enumerate(markers):\n", - " ax.plot(\n", + " canvas.plot(\n", " x,\n", " np.sin(x) + i * 0.5,\n", " marker=m,\n", @@ -150,9 +150,9 @@ " label=f\"marker={m!r}\",\n", " )\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_title(\"Marker comparison\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_title(\"Marker comparison\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -175,17 +175,17 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 40)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", "\n", - "ax.plot(x, np.sin(x), linewidth=0.8, marker=\"o\", markersize=3, label=\"thin / small\")\n", - "ax.plot(x, np.sin(x) - 0.6, linewidth=2.5, marker=\"o\", markersize=7, label=\"medium\")\n", - "ax.plot(\n", + "canvas.plot(x, np.sin(x), linewidth=0.8, marker=\"o\", markersize=3, label=\"thin / small\")\n", + "canvas.plot(x, np.sin(x) - 0.6, linewidth=2.5, marker=\"o\", markersize=7, label=\"medium\")\n", + "canvas.plot(\n", " x, np.sin(x) - 1.2, linewidth=4.5, marker=\"o\", markersize=12, label=\"thick / large\"\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_title(\"Linewidth and markersize\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_title(\"Linewidth and markersize\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -208,15 +208,15 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", "\n", - "ax.fill_between(x, np.sin(x), 0, alpha=0.7, color=\"steelblue\", label=\"alpha=0.7\")\n", - "ax.fill_between(x, np.sin(2 * x), 0, alpha=0.4, color=\"tomato\", label=\"alpha=0.4\")\n", - "ax.fill_between(x, np.sin(3 * x), 0, alpha=0.2, color=\"seagreen\", label=\"alpha=0.2\")\n", + "canvas.fill_between(x, np.sin(x), 0, alpha=0.7, color=\"steelblue\", label=\"alpha=0.7\")\n", + "canvas.fill_between(x, np.sin(2 * x), 0, alpha=0.4, color=\"tomato\", label=\"alpha=0.4\")\n", + "canvas.fill_between(x, np.sin(3 * x), 0, alpha=0.2, color=\"seagreen\", label=\"alpha=0.2\")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_title(\"Alpha transparency\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_title(\"Alpha transparency\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, @@ -240,18 +240,20 @@ "x = np.linspace(0, 2 * np.pi, 80)\n", "noise = np.random.default_rng(0).normal(0, 0.05, len(x))\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", "\n", "# Shaded uncertainty band\n", - "ax.fill_between(x, np.sin(x) - 0.15, np.sin(x) + 0.15, alpha=0.15, color=\"steelblue\")\n", + "canvas.fill_between(\n", + " x, np.sin(x) - 0.15, np.sin(x) + 0.15, alpha=0.15, color=\"steelblue\"\n", + ")\n", "\n", "# Noisy data\n", - "ax.scatter(\n", + "canvas.scatter(\n", " x, np.sin(x) + noise, color=\"steelblue\", marker=\"o\", s=18, alpha=0.6, label=\"data\"\n", ")\n", "\n", "# Clean model\n", - "ax.plot(\n", + "canvas.plot(\n", " x,\n", " np.sin(x),\n", " color=\"#e74c3c\",\n", @@ -261,10 +263,10 @@ " label=\"model\",\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Publication-style plot\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Publication-style plot\")\n", + "canvas.set_legend(True)\n", "canvas.show(backend=BACKEND)" ] }, diff --git a/tutorials/tutorial_05.ipynb b/tutorials/tutorial_05.ipynb index 6c16f4e..b7c53bc 100644 --- a/tutorials/tutorial_05.ipynb +++ b/tutorials/tutorial_05.ipynb @@ -67,12 +67,12 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", - "ax.plot(x, np.sin(x), color=\"steelblue\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\")\n", "\n", - "ax.set_xlabel(r\"$x$ (radians)\")\n", - "ax.set_ylabel(r\"$\\sin(x)$\")\n", - "ax.set_title(r\"The sine function $f(x) = \\sin(x)$\")\n", + "canvas.set_xlabel(r\"$x$ (radians)\")\n", + "canvas.set_ylabel(r\"$\\sin(x)$\")\n", + "canvas.set_title(r\"The sine function $f(x) = \\sin(x)$\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -93,16 +93,16 @@ "source": [ "x = np.linspace(0, 4 * np.pi, 300)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.5)\n", - "ax.plot(x, np.sin(x), color=\"tomato\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.5)\n", + "canvas.plot(x, np.sin(x), color=\"tomato\")\n", "\n", "# Show only the first full period\n", - "ax.set_xlim(0, 2 * np.pi)\n", - "ax.set_ylim(-1.2, 1.2)\n", + "canvas.set_xlim(0, 2 * np.pi)\n", + "canvas.set_ylim(-1.2, 1.2)\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(r\"$\\sin(x)$\")\n", - "ax.set_title(\"Axis limits: first period only\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(r\"$\\sin(x)$\")\n", + "canvas.set_title(\"Axis limits: first period only\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -139,12 +139,12 @@ "]\n", "temps = [3, 4, 7, 12, 17, 21, 23, 22, 18, 13, 7, 4]\n", "\n", - "canvas, ax = Canvas.subplots(width=\"12cm\", ratio=0.45)\n", - "ax.plot(range(12), temps, marker=\"o\", color=\"steelblue\", linewidth=2)\n", + "canvas = Canvas(width=\"12cm\", ratio=0.45)\n", + "canvas.plot(range(12), temps, marker=\"o\", color=\"steelblue\", linewidth=2)\n", "\n", - "ax.set_xticks(list(range(12)), labels=months)\n", - "ax.set_ylabel(r\"Temperature ($^\\circ$C)\")\n", - "ax.set_title(\"Monthly average temperature\")\n", + "canvas.set_xticks(list(range(12)), labels=months)\n", + "canvas.set_ylabel(r\"Temperature ($^\\circ$C)\")\n", + "canvas.set_title(\"Monthly average temperature\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -234,14 +234,14 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", - "ax.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\")\n", - "ax.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\")\n", - "ax.plot(x, np.sin(2 * x), color=\"seagreen\", label=r\"$\\sin(2x)$\", linestyle=\"dashed\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\")\n", + "canvas.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\")\n", + "canvas.plot(x, np.sin(2 * x), color=\"seagreen\", label=r\"$\\sin(2x)$\", linestyle=\"dashed\")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_legend(True)\n", - "ax.set_title(\"Legend demo\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_legend(True)\n", + "canvas.set_title(\"Legend demo\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -265,11 +265,11 @@ "x = np.linspace(0, 2 * np.pi, 200)\n", "y = np.sin(x)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", - "ax.plot(x, y, color=\"steelblue\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", + "canvas.plot(x, y, color=\"steelblue\")\n", "\n", "# Annotate the maximum\n", - "ax.annotate(\n", + "canvas.annotate(\n", " r\"maximum $\\approx 1$\",\n", " xy=(np.pi / 2, 1.0),\n", " xytext=(np.pi / 2 + 1.0, 0.7),\n", @@ -278,9 +278,9 @@ " color=\"darkred\",\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(r\"$\\sin(x)$\")\n", - "ax.set_title(\"annotate demo\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(r\"$\\sin(x)$\")\n", + "canvas.set_title(\"annotate demo\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -303,16 +303,16 @@ "source": [ "x = np.linspace(-2, 2, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", - "ax.plot(x, x**2, color=\"darkorange\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", + "canvas.plot(x, x**2, color=\"darkorange\")\n", "\n", - "ax.text(\n", + "canvas.text(\n", " 0, 3.2, r\"$f(x) = x^2$\", ha=\"center\", va=\"bottom\", fontsize=12, color=\"darkorange\"\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(r\"$f(x)$\")\n", - "ax.set_title(\"text demo\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(r\"$f(x)$\")\n", + "canvas.set_title(\"text demo\")\n", "canvas.show(backend=BACKEND)" ] }, @@ -335,12 +335,12 @@ "source": [ "theta = np.linspace(0, 2 * np.pi, 300)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"7cm\", ratio=1.0)\n", - "ax.plot(np.cos(theta), np.sin(theta), color=\"steelblue\", linewidth=2)\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_title(\"Circle with equal aspect ratio\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", + "canvas = Canvas(width=\"7cm\", ratio=1.0)\n", + "canvas.plot(np.cos(theta), np.sin(theta), color=\"steelblue\", linewidth=2)\n", + "canvas.set_aspect(\"equal\")\n", + "canvas.set_title(\"Circle with equal aspect ratio\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", "canvas.show(backend=BACKEND)" ] }, diff --git a/tutorials/tutorial_06.ipynb b/tutorials/tutorial_06.ipynb index 6902623..51dc24f 100644 --- a/tutorials/tutorial_06.ipynb +++ b/tutorials/tutorial_06.ipynb @@ -67,11 +67,11 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", "\n", - "ax.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\", layer=0)\n", - "ax.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\", layer=1)\n", - "ax.plot(\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\", layer=0)\n", + "canvas.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\", layer=1)\n", + "canvas.plot(\n", " x,\n", " np.sin(x) * np.cos(x),\n", " color=\"seagreen\",\n", @@ -80,9 +80,9 @@ " layer=2,\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_legend(True)\n", - "ax.set_title(\"Three curves on three layers\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_legend(True)\n", + "canvas.set_title(\"Three curves on three layers\")\n", "canvas.show(backend=BACKEND) # renders all layers by default" ] }, @@ -105,11 +105,11 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", "\n", - "ax.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\", layer=0)\n", - "ax.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\", layer=1)\n", - "ax.plot(\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\", layer=0)\n", + "canvas.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\", layer=1)\n", + "canvas.plot(\n", " x,\n", " np.sin(x) * np.cos(x),\n", " color=\"seagreen\",\n", @@ -118,11 +118,11 @@ " layer=2,\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_legend(True)\n", "\n", "print(\"--- Layer 0 only ---\")\n", - "ax.set_title(\"Layer 0 only\")\n", + "canvas.set_title(\"Layer 0 only\")\n", "\n", "canvas.show(backend=BACKEND, layers=[0])" ] @@ -135,7 +135,7 @@ "outputs": [], "source": [ "# Same canvas — now show layers 0 and 1 together\n", - "ax.set_title(\"Layers 0 and 1\")\n", + "canvas.set_title(\"Layers 0 and 1\")\n", "\n", "canvas.show(backend=BACKEND, layers=[0, 1])" ] @@ -148,7 +148,7 @@ "outputs": [], "source": [ "# All layers\n", - "ax.set_title(\"All layers\")\n", + "canvas.set_title(\"All layers\")\n", "\n", "canvas.show(backend=BACKEND, layers=[0, 1, 2])" ] @@ -202,18 +202,20 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 300)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"11cm\", ratio=0.6)\n", + "canvas = Canvas(width=\"11cm\", ratio=0.6)\n", "\n", "# Layer 0: raw data (noisy sine)\n", "rng = np.random.default_rng(42)\n", "y_data = np.sin(x) + rng.normal(0, 0.15, len(x))\n", - "ax.scatter(x, y_data, color=\"gray\", s=8, alpha=0.5, label=\"measured data\", layer=0)\n", + "canvas.scatter(x, y_data, color=\"gray\", s=8, alpha=0.5, label=\"measured data\", layer=0)\n", "\n", "# Layer 1: true function\n", - "ax.plot(x, np.sin(x), color=\"steelblue\", linewidth=2, label=r\"true: $\\sin(x)$\", layer=1)\n", + "canvas.plot(\n", + " x, np.sin(x), color=\"steelblue\", linewidth=2, label=r\"true: $\\sin(x)$\", layer=1\n", + ")\n", "\n", "# Layer 2: envelope\n", - "ax.fill_between(\n", + "canvas.fill_between(\n", " x,\n", " np.sin(x) - 0.15,\n", " np.sin(x) + 0.15,\n", @@ -223,12 +225,12 @@ " layer=2,\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_legend(True)\n", "\n", "# Step 1: only the data cloud\n", - "ax.set_title(\"Step 1 – raw data\")\n", + "canvas.set_title(\"Step 1 – raw data\")\n", "canvas.show(backend=BACKEND, layers=[0])" ] }, @@ -240,7 +242,7 @@ "outputs": [], "source": [ "# Step 2: add the true curve\n", - "ax.set_title(\"Step 2 – add true function\")\n", + "canvas.set_title(\"Step 2 – add true function\")\n", "canvas.show(backend=BACKEND, layers=[0, 1])" ] }, @@ -252,7 +254,7 @@ "outputs": [], "source": [ "# Step 3: add the uncertainty envelope\n", - "ax.set_title(\"Step 3 – add uncertainty envelope\")\n", + "canvas.set_title(\"Step 3 – add uncertainty envelope\")\n", "canvas.show(backend=BACKEND, layers=[0, 1, 2])" ] }, @@ -265,7 +267,7 @@ "\n", "| Concept | How |\n", "|---|---|\n", - "| Assign to layer | `ax.plot(..., layer=1)` |\n", + "| Assign to layer | `canvas.plot(..., layer=1)` |\n", "| Render subset | `canvas.show(layers=[0, 1])` |\n", "| Save all layers | `canvas.savefig('fig.pdf', layer_by_layer=True)` |\n", "| Default layer | `0` (omit `layer=` and it goes to layer 0) |\n", diff --git a/tutorials/tutorial_07_tikz.ipynb b/tutorials/tutorial_07_tikz.ipynb index 078e321..6fdf89b 100644 --- a/tutorials/tutorial_07_tikz.ipynb +++ b/tutorials/tutorial_07_tikz.ipynb @@ -67,15 +67,15 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 60)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", - "ax.plot(x, np.sin(x), label=\"sin\", color=\"steelblue\", line_width=1.5)\n", - "ax.plot(x, np.cos(x), label=\"cos\", color=\"tomato\", line_width=1.2)\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Trigonometric functions\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", + "canvas.plot(x, np.sin(x), label=\"sin\", color=\"steelblue\", line_width=1.5)\n", + "canvas.plot(x, np.cos(x), label=\"cos\", color=\"tomato\", line_width=1.2)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Trigonometric functions\")\n", "\n", "# backend='tikzfigure' returns a TikzFigure object\n", - "tikz = canvas.plot(backend=\"tikzfigure\")\n", + "tikz = canvas.render(backend=\"tikzfigure\")\n", "print(type(tikz))" ] }, @@ -101,30 +101,39 @@ ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "7", "metadata": {}, + "outputs": [], + "source": [ + "canvas.show(backend=\"tikzfigure\")" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, "source": [ "### 1.2 Inspecting the generated LaTeX\n", "\n", - "`tikz.generate_tikz()` returns the raw LaTeX source string. \n", + "`str(tikz)` returns the raw LaTeX source string (and `generate_tikz()` remains available explicitly). \n", "Each data line becomes a `\\draw` command connecting coordinate pairs." ] }, { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "9", "metadata": {}, "outputs": [], "source": [ - "code = tikz.generate_tikz()\n", - "print(code)" + "print(tikz)" ] }, { "cell_type": "markdown", - "id": "9", + "id": "10", "metadata": {}, "source": [ "### 1.2.1 Checking explicit width and height\n", @@ -136,7 +145,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -144,7 +153,7 @@ "ax_ratio2.plot(x, np.exp(-x / np.pi), color=\"purple\", line_width=1.5)\n", "ax_ratio2.set_title(\"ratio = 2 export\")\n", "\n", - "tikz_ratio2 = canvas_ratio2.plot(backend=\"tikzfigure\")\n", + "tikz_ratio2 = canvas_ratio2.render(backend=\"tikzfigure\")\n", "ratio2_code = tikz_ratio2.generate_tikz()\n", "\n", "for line in ratio2_code.splitlines():\n", @@ -157,7 +166,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "12", "metadata": {}, "source": [ "### 1.3 TikZ-specific kwargs\n", @@ -169,7 +178,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -180,13 +189,13 @@ "ax2.set_xlabel(\"x\")\n", "ax2.set_title(\"Line width comparison\")\n", "\n", - "tikz2 = canvas2.plot(backend=\"tikzfigure\")\n", + "tikz2 = canvas2.render(backend=\"tikzfigure\")\n", "print(tikz2.generate_tikz())" ] }, { "cell_type": "markdown", - "id": "13", + "id": "14", "metadata": {}, "source": [ "### 1.4 Layer-aware TikZ output\n", @@ -198,7 +207,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -213,19 +222,19 @@ "print(\"Available layers:\", canvas3.layers)\n", "\n", "# Render only layer 0 — one \\draw command\n", - "tikz_l0 = canvas3.plot(backend=\"tikzfigure\", layers=[0])\n", + "tikz_l0 = canvas3.render(backend=\"tikzfigure\", layers=[0])\n", "print(\"\\n--- Layer 0 only ---\")\n", "print(f\"\\\\draw count: {tikz_l0.generate_tikz().count(chr(92) + 'draw')}\")\n", "\n", "# Render layers 0 and 1\n", - "tikz_l01 = canvas3.plot(backend=\"tikzfigure\", layers=[0, 1])\n", + "tikz_l01 = canvas3.render(backend=\"tikzfigure\", layers=[0, 1])\n", "print(\"\\n--- Layers 0 & 1 ---\")\n", "print(f\"\\\\draw count: {tikz_l01.generate_tikz().count(chr(92) + 'draw')}\")" ] }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "### 1.5 Saving TikZ code to a file\n", @@ -236,11 +245,11 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ - "tikz_all = canvas3.plot(backend=\"tikzfigure\")\n", + "tikz_all = canvas3.render(backend=\"tikzfigure\")\n", "\n", "with open(\"figure.tex\", \"w\") as f:\n", " f.write(tikz_all.generate_tikz())\n", @@ -259,7 +268,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "### 1.6 Rendering the figure (requires `pdflatex`)\n", @@ -270,7 +279,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -280,27 +289,27 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "### 1.7 Canvas → TikZ limitations\n", "\n", "| Feature | Supported? |\n", "|---|---|\n", - "| Line plots (`ax.plot`) | ✅ |\n", + "| Line plots (`canvas.plot`) | ✅ |\n", "| Layer filtering | ✅ |\n", "| `line_width=` kwarg | ✅ |\n", - "| Multiple subplots | ❌ (raises `NotImplementedError`) |\n", - "| `ax.scatter`, `ax.bar` | ❌ (silently ignored) |\n", - "| `ax.fill_between` | ❌ |\n", - "| Axis labels / titles | ❌ (TikZ has no axis frame by default) |\n", + "| Horizontal subplots (1×n) | ✅ |\n", + "| `canvas.scatter`, `canvas.bar`, `canvas.barh` | ✅ |\n", + "| `canvas.fill_between`, `canvas.errorbar` | ✅ |\n", + "| Axis labels / titles | ✅ |\n", "\n", - "For anything beyond line plots, use the `tikzfigure` API directly (Part 2 below)." + "For unsupported primitives, the Canvas API raises `NotImplementedError`; use the direct `tikzfigure` API for advanced TikZ shapes (Part 2 below)." ] }, { "cell_type": "markdown", - "id": "20", + "id": "21", "metadata": {}, "source": [ "---\n", @@ -318,7 +327,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "### 2.1 Drawing paths with `draw()`\n", @@ -329,7 +338,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -347,7 +356,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "24", "metadata": {}, "source": [ "### 2.2 Straight line segments with `line()`\n", @@ -359,7 +368,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -380,7 +389,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "### 2.3 Rectangles, circles, and arcs" @@ -389,7 +398,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -422,7 +431,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "28", "metadata": {}, "source": [ "### 2.4 Nodes — text labels and markers\n", @@ -433,7 +442,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -464,7 +473,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "30", "metadata": {}, "source": [ "### 2.5 Custom colours with `colorlet()`\n", @@ -475,7 +484,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -499,7 +508,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "32", "metadata": {}, "source": [ "### 2.6 Filled paths and patterns\n", @@ -510,7 +519,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -539,7 +548,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "34", "metadata": {}, "source": [ "### 2.7 Layers in `TikzFigure`\n", @@ -551,7 +560,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -573,7 +582,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "36", "metadata": {}, "source": [ "### 2.8 Escaping to raw TikZ code\n", @@ -584,7 +593,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -604,7 +613,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "38", "metadata": {}, "source": [ "### 2.9 Putting it all together — a complete figure\n", @@ -615,7 +624,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -667,7 +676,7 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -677,7 +686,7 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "41", "metadata": {}, "source": [ "### 2.10 Embedding in a LaTeX document\n", @@ -708,7 +717,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "42", "metadata": {}, "source": [ "---\n", @@ -716,9 +725,9 @@ "\n", "### Canvas → TikZ workflow\n", "```python\n", - "canvas, ax = Canvas.subplots(width='10cm', ratio=0.6)\n", - "ax.plot(x, y, color='steelblue', line_width=1.5)\n", - "tikz = canvas.plot(backend='tikzfigure')\n", + "canvas = Canvas(width='10cm', ratio=0.6)\n", + "canvas.plot(x, y, color='steelblue', line_width=1.5)\n", + "tikz = canvas.render(backend='tikzfigure')\n", "print(tikz.generate_tikz()) # inspect LaTeX\n", "tikz.show() # render (needs pdflatex)\n", "```\n", @@ -747,6 +756,106 @@ "| `'blue!50!red'` | 50% blend |\n", "| `'gray!20'` | 20% gray (80% white) |" ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "## Part 1.8 — Canvas primitives supported by TikZ\n", + "\n", + "The Canvas TikZ backend supports line, scatter, bar, horizontal-bar, filled-region,\n", + "and error-bar plots. The following example renders the canvas as TikZ source." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from maxplotlib import Canvas\n", + "\n", + "x = np.arange(5)\n", + "y = np.array([1.0, 2.2, 1.4, 3.0, 2.5])\n", + "\n", + "primitive_canvas = Canvas()\n", + "primitive_canvas.plot(x, y, color=\"black\", linewidth=1.5)\n", + "primitive_canvas.scatter(x, y + 0.35, color=\"crimson\")\n", + "primitive_canvas.bar(x, y * 0.35, color=\"steelblue\", alpha=0.7)\n", + "primitive_canvas.fill_between(x, y, 0, color=\"gold\", alpha=0.2)\n", + "primitive_canvas.errorbar(x, y, yerr=0.15, color=\"darkgreen\")\n", + "primitive_canvas.configure(\n", + " title=\"TikZ-supported Canvas primitives\",\n", + " xlabel=\"Sample\",\n", + " ylabel=\"Value\",\n", + " grid=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "tikz_primitive_figure = primitive_canvas.render(backend=\"tikzfigure\")\n", + "print(str(tikz_primitive_figure)[:2000])" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "The next cell shows the TikZ generated from the canvas. Unsupported primitives now raise\n", + "`NotImplementedError` instead of being silently omitted." + ] + }, + { + "cell_type": "markdown", + "id": "47", + "metadata": {}, + "source": [ + "## Part 1.9 — More TikZ-supported primitives\n", + "\n", + "Step and stairs plots, stems, reference lines, spans, and polygon fills are also\n", + "translated to TikZ. The generated TikZ is printed below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": {}, + "outputs": [], + "source": [ + "more_canvas = Canvas()\n", + "more_canvas.step(x, y, color=\"black\", where=\"post\")\n", + "more_canvas.stem(x, y, linefmt=\"m-\", markerfmt=\"mo\")\n", + "more_canvas.hlines([1.5, 2.5], 0, 4, color=\"gray\", linestyle=\"--\")\n", + "more_canvas.vlines([1, 3], 0, 3.5, color=\"gray\")\n", + "more_canvas.axvspan(1, 2, color=\"orange\", alpha=0.2)\n", + "more_canvas.axhspan(1, 2, color=\"green\", alpha=0.2)\n", + "more_canvas.fill(x, [0.2, 0.8, 0.4, 1.0, 0.2], color=\"cyan\", alpha=0.2)\n", + "more_canvas.configure(\n", + " title=\"Additional TikZ primitives\", xlabel=\"Sample\", ylabel=\"Value\", grid=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": {}, + "outputs": [], + "source": [ + "more_tikz = more_canvas.render(backend=\"tikzfigure\")\n", + "print(str(more_tikz)[:2000])" + ] } ], "metadata": { diff --git a/tutorials/tutorial_08_plotly.ipynb b/tutorials/tutorial_08_plotly.ipynb index 2ecf2ef..7feb299 100644 --- a/tutorials/tutorial_08_plotly.ipynb +++ b/tutorials/tutorial_08_plotly.ipynb @@ -48,7 +48,7 @@ "In notebooks, you can either:\n", "\n", "- call `canvas.show(backend=\"plotly\")` (displays and returns a Plotly figure), or\n", - "- call `fig = canvas.plot(backend=\"plotly\")` and put `fig` as the last line of a cell.\n", + "- call `fig = canvas.render(backend=\"plotly\")` and put `fig` as the last line of a cell.\n", "\n" ] }, @@ -62,7 +62,7 @@ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", "canvas = Canvas(width=\"10cm\", ratio=0.45)\n", - "canvas.add_line(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\")\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\")\n", "canvas.set_title(\"Displayed inline\")\n", "canvas.set_legend(True)\n", "\n", @@ -90,7 +90,7 @@ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", - "canvas.add_line(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\")\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\")\n", "canvas.set_xlabel(\"x\")\n", "canvas.set_ylabel(\"y\")\n", "canvas.set_title(\"Basic line plot\")\n", @@ -106,7 +106,7 @@ "source": [ "## 2 · Multiple lines\n", "\n", - "Each `ax.plot()` call becomes a separate Plotly trace. Enable the legend with `ax.set_legend(True)` so trace labels appear." + "Each `canvas.plot()` call becomes a separate Plotly trace. Enable the legend with `canvas.set_legend(True)` so trace labels appear." ] }, { @@ -119,9 +119,9 @@ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", - "canvas.add_line(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\", linewidth=2)\n", - "canvas.add_line(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2)\n", - "canvas.add_line(\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\", linewidth=2)\n", + "canvas.plot(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2)\n", + "canvas.plot(\n", " x,\n", " np.sin(2 * x),\n", " color=\"seagreen\",\n", @@ -146,7 +146,7 @@ "source": [ "## 3 · Scatter plot\n", "\n", - "`ax.scatter()` maps to a Plotly scatter trace with markers only." + "`canvas.scatter()` maps to a Plotly scatter trace with markers only." ] }, { @@ -165,7 +165,7 @@ "canvas.scatter(\n", " x_data, y_data, color=\"steelblue\", marker=\"o\", s=20, label=\"observations\"\n", ")\n", - "canvas.add_line(\n", + "canvas.plot(\n", " [0, 10],\n", " [0, 5],\n", " color=\"tomato\",\n", @@ -190,7 +190,7 @@ "source": [ "## 4 · Bar chart\n", "\n", - "`ax.bar()` maps to a Plotly bar trace." + "`canvas.bar()` maps to a Plotly bar trace." ] }, { @@ -242,7 +242,7 @@ "canvas.bar(\n", " months, rainfall, color=\"steelblue\", alpha=0.7, label=\"monthly rainfall (mm)\"\n", ")\n", - "canvas.add_line(\n", + "canvas.plot(\n", " months,\n", " cumulative / 10,\n", " color=\"tomato\",\n", @@ -283,8 +283,8 @@ "canvas = Canvas(ncols=1) # , width=\"14cm\", ratio=0.35)\n", "\n", "# Left panel — line plot\n", - "canvas.add_line(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\", linewidth=2, col=0)\n", - "canvas.add_line(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2, col=0)\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\", linewidth=2, col=0)\n", + "canvas.plot(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2, col=0)\n", "canvas.set_xlabel(\"x\", col=0)\n", "canvas.set_ylabel(\"y\", col=0)\n", "canvas.set_title(\"Trigonometric functions\", col=0)\n", @@ -294,7 +294,7 @@ "x_s = rng.uniform(0, 6, 80)\n", "y_s = np.sin(x_s) + rng.normal(0, 0.15, 80)\n", "canvas.scatter(x_s, y_s, color=\"seagreen\", marker=\"o\", s=18, label=\"noisy sin\", col=1)\n", - "canvas.add_line(\n", + "canvas.plot(\n", " x,\n", " np.sin(x),\n", " color=\"black\",\n", @@ -320,7 +320,7 @@ "source": [ "## 7 · Log scale\n", "\n", - "`ax.set_yscale('log')` is passed through to Plotly's axis type." + "`canvas.set_yscale('log')` is passed through to Plotly's axis type." ] }, { @@ -333,9 +333,9 @@ "x = np.linspace(0.1, 5, 200)\n", "\n", "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", - "canvas.add_line(x, np.exp(x), color=\"steelblue\", label=\"exp(x)\", linewidth=2)\n", - "canvas.add_line(x, np.exp(1.5 * x), color=\"tomato\", label=\"exp(1.5x)\", linewidth=2)\n", - "canvas.add_line(x, x**2, color=\"seagreen\", label=\"x²\", linewidth=2)\n", + "canvas.plot(x, np.exp(x), color=\"steelblue\", label=\"exp(x)\", linewidth=2)\n", + "canvas.plot(x, np.exp(1.5 * x), color=\"tomato\", label=\"exp(1.5x)\", linewidth=2)\n", + "canvas.plot(x, x**2, color=\"seagreen\", label=\"x²\", linewidth=2)\n", "\n", "canvas.set_xlabel(\"x\")\n", "canvas.set_ylabel(\"y (log scale)\")\n", @@ -366,8 +366,8 @@ "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", "canvas = Canvas(width=\"10cm\", ratio=0.55)\n", - "canvas.add_line(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\", linewidth=2)\n", - "canvas.add_line(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2)\n", + "canvas.plot(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\", linewidth=2)\n", + "canvas.plot(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2)\n", "canvas.set_xlabel(\"x\")\n", "canvas.set_ylabel(\"y\")\n", "canvas.set_title(\"Saved interactive figure\")\n", @@ -389,16 +389,16 @@ "\n", "| Feature | Supported | Notes |\n", "|---|---|---|\n", - "| `ax.plot()` — line trace | ✅ | `color`, `linestyle`, `linewidth`, `marker` all passed through |\n", - "| `ax.scatter()` — markers | ✅ | `color`, `marker`, `s`, `alpha` |\n", - "| `ax.bar()` — bar chart | ✅ | `color`, `alpha` |\n", - "| `ax.fill_between()` | ❌ | Not supported by this backend |\n", - "| `ax.errorbar()` | ❌ | Not supported by this backend |\n", - "| `ax.axhline/axvline` | ❌ | Not supported by this backend |\n", + "| `canvas.plot()` — line trace | ✅ | `color`, `linestyle`, `linewidth`, `marker` all passed through |\n", + "| `canvas.scatter()` — markers | ✅ | `color`, `marker`, `s`, `alpha` |\n", + "| `canvas.bar()` — bar chart | ✅ | `color`, `alpha` |\n", + "| `canvas.fill_between()` | ❌ | Not supported by this backend |\n", + "| `canvas.errorbar()` | ❌ | Not supported by this backend |\n", + "| `canvas.axhline/axvline` | ❌ | Not supported by this backend |\n", "| Multi-subplot canvas | ✅ | `Canvas.subplots(ncols=...)` etc. |\n", "| `canvas.suptitle()` | ✅ | Maps to figure title |\n", - "| `ax.set_yscale('log')` | ✅ | |\n", - "| `ax.set_legend(True)` | ✅ | |\n", + "| `canvas.set_yscale('log')` | ✅ | |\n", + "| `canvas.set_legend(True)` | ✅ | |\n", "| `fig.show()` | ✅ | Interactive in Jupyter |\n", "| `fig.write_html(path)` | ✅ | Standalone interactive HTML |\n", "\n", @@ -408,11 +408,11 @@ "from maxplotlib import Canvas\n", "import numpy as np\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, y, label='data')\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.plot(x, y, label='data')\n", + "canvas.set_legend(True)\n", "\n", - "fig = canvas.plot(backend='plotly') # → plotly.graph_objects.Figure\n", + "fig = canvas.render(backend='plotly') # → plotly.graph_objects.Figure\n", "fig.show() # interactive in Jupyter\n", "fig.write_html('report.html') # share with anyone\n", "```" diff --git a/tutorials/tutorial_09_plotext.ipynb b/tutorials/tutorial_09_plotext.ipynb index e395ded..060a562 100644 --- a/tutorials/tutorial_09_plotext.ipynb +++ b/tutorials/tutorial_09_plotext.ipynb @@ -55,7 +55,7 @@ "source": [ "## 1 · Figure lifecycle: `plot()`, `build()`, `show()`, and `savefig()`\n", "\n", - "Use `canvas.plot(backend=\"plotext\")` when you want a reusable terminal figure object. The returned object supports:\n", + "Use `canvas.render(backend=\"plotext\")` when you want a reusable terminal figure object. The returned object supports:\n", "\n", "- `.build()` to get the rendered terminal plot as a string\n", "- `.show()` to print it immediately\n", @@ -73,13 +73,13 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 100)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", - "ax.set_title(\"Demo\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", + "canvas.set_title(\"Demo\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", "\n", - "terminal_fig = canvas.plot(backend=\"plotext\")\n", + "terminal_fig = canvas.render(backend=\"plotext\")\n", "preview = terminal_fig.build(keep_colors=False)\n", "print(preview)\n", "\n", @@ -106,17 +106,17 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 120)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", - "ax.plot(x, np.cos(x), color=\"yellow\", label=\"cos(x)\", marker=\"dot\")\n", - "ax.plot(x, np.sin(2 * x), color=\"green\", label=\"sin(2x)\")\n", - "ax.set_title(\"Multiple terminal lines\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"value\")\n", - "ax.set_grid(True)\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", + "canvas.plot(x, np.cos(x), color=\"yellow\", label=\"cos(x)\", marker=\"dot\")\n", + "canvas.plot(x, np.sin(2 * x), color=\"green\", label=\"sin(2x)\")\n", + "canvas.set_title(\"Multiple terminal lines\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"value\")\n", + "canvas.set_grid(True)\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -141,15 +141,15 @@ "samples_x = np.linspace(0, 8, 15)\n", "samples_y = np.sin(samples_x) + rng.normal(0, 0.15, len(samples_x))\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"white\", label=\"sin(x)\")\n", - "ax.scatter(samples_x, samples_y, color=\"red\", marker=\"x\", label=\"samples\")\n", - "ax.set_title(\"Scatter + line\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"white\", label=\"sin(x)\")\n", + "canvas.scatter(samples_x, samples_y, color=\"red\", marker=\"x\", label=\"samples\")\n", + "canvas.set_title(\"Scatter + line\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -172,15 +172,15 @@ "bins = np.arange(5)\n", "values = np.array([4.0, 6.5, 3.2, 7.4, 5.8])\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.bar(bins, values, color=\"green\", label=\"count\")\n", - "ax.scatter(bins, values, color=\"yellow\", label=\"sample mean\")\n", - "ax.set_title(\"Bar + scatter overlay\")\n", - "ax.set_xlabel(\"bin\")\n", - "ax.set_ylabel(\"value\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.bar(bins, values, color=\"green\", label=\"count\")\n", + "canvas.scatter(bins, values, color=\"yellow\", label=\"sample mean\")\n", + "canvas.set_title(\"Bar + scatter overlay\")\n", + "canvas.set_xlabel(\"bin\")\n", + "canvas.set_ylabel(\"value\")\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -205,20 +205,20 @@ "source": [ "x = np.linspace(0, 5, 100)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.fill_between(\n", + "canvas = Canvas()\n", + "canvas.fill_between(\n", " x,\n", " np.exp(-0.5 * x) * np.sin(3 * x) + 1.0,\n", " 0.0,\n", " color=\"cyan\",\n", " label=\"signal envelope\",\n", ")\n", - "ax.set_title(\"fill_between() to a scalar baseline\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"amplitude\")\n", - "ax.set_legend(True)\n", + "canvas.set_title(\"fill_between() to a scalar baseline\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"amplitude\")\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -232,14 +232,14 @@ "upper = np.sin(x) + 1.8\n", "lower = np.cos(x) + 0.8\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.fill_between(x, upper, lower, color=\"blue\", label=\"between curves\")\n", - "ax.plot(x, upper, color=\"white\", label=\"upper\")\n", - "ax.plot(x, lower, color=\"yellow\", label=\"lower\")\n", - "ax.set_title(\"fill_between() between two curves\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.fill_between(x, upper, lower, color=\"blue\", label=\"between curves\")\n", + "canvas.plot(x, upper, color=\"white\", label=\"upper\")\n", + "canvas.plot(x, lower, color=\"yellow\", label=\"lower\")\n", + "canvas.set_title(\"fill_between() between two curves\")\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -262,18 +262,18 @@ "x = np.linspace(1, 10, 9)\n", "y = np.sqrt(x)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.errorbar(x, y, yerr=0.15, color=\"cyan\", label=\"sqrt(x)\")\n", - "ax.axhline(2.0, color=\"white\")\n", - "ax.axvline(4.0, color=\"yellow\")\n", - "ax.hlines([1.2, 2.7], xmin=[1, 5], xmax=[3, 9], color=\"green\")\n", - "ax.vlines([2.0, 8.0], ymin=[1.0, 2.0], ymax=[1.8, 3.0], color=\"red\")\n", - "ax.set_title(\"Error bars + reference lines\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.errorbar(x, y, yerr=0.15, color=\"cyan\", label=\"sqrt(x)\")\n", + "canvas.axhline(2.0, color=\"white\")\n", + "canvas.axvline(4.0, color=\"yellow\")\n", + "canvas.hlines([1.2, 2.7], xmin=[1, 5], xmax=[3, 9], color=\"green\")\n", + "canvas.vlines([2.0, 8.0], ymin=[1.0, 2.0], ymax=[1.8, 3.0], color=\"red\")\n", + "canvas.set_title(\"Error bars + reference lines\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -298,19 +298,19 @@ "peak_x = x[np.argmax(y)]\n", "peak_y = y.max()\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, y, color=\"cyan\")\n", - "ax.text(0.8, -0.8, \"terminal note\", color=\"yellow\")\n", - "ax.annotate(\n", + "canvas = Canvas()\n", + "canvas.plot(x, y, color=\"cyan\")\n", + "canvas.text(0.8, -0.8, \"terminal note\", color=\"yellow\")\n", + "canvas.annotate(\n", " \"peak\",\n", " xy=(peak_x, peak_y),\n", " xytext=(4.4, 0.4),\n", " color=\"white\",\n", " arrowprops={\"color\": \"green\"},\n", ")\n", - "ax.set_title(\"Text and annotations\")\n", + "canvas.set_title(\"Text and annotations\")\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -332,21 +332,21 @@ "source": [ "x = np.linspace(1, 20, 120)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, x**0.5, color=\"cyan\", label=\"sqrt(x)\")\n", - "ax.plot(x, np.log(x + 1), color=\"yellow\", label=\"log(x + 1)\")\n", - "ax.set_title(\"Axis controls\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"value\")\n", - "ax.set_xlim(1, 20)\n", - "ax.set_ylim(0, 5)\n", - "ax.set_xticks([1, 2, 5, 10, 20], [\"1\", \"2\", \"5\", \"10\", \"20\"])\n", - "ax.set_yticks([0, 1, 2, 3, 4, 5])\n", - "ax.set_xscale(\"log\")\n", - "ax.set_grid(True)\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.plot(x, x**0.5, color=\"cyan\", label=\"sqrt(x)\")\n", + "canvas.plot(x, np.log(x + 1), color=\"yellow\", label=\"log(x + 1)\")\n", + "canvas.set_title(\"Axis controls\")\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"value\")\n", + "canvas.set_xlim(1, 20)\n", + "canvas.set_ylim(0, 5)\n", + "canvas.set_xticks([1, 2, 5, 10, 20], [\"1\", \"2\", \"5\", \"10\", \"20\"])\n", + "canvas.set_yticks([0, 1, 2, 3, 4, 5])\n", + "canvas.set_xscale(\"log\")\n", + "canvas.set_grid(True)\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -368,12 +368,12 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 100)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"cyan\", label=\"layer 0\", layer=0)\n", - "ax.plot(x, np.cos(x), color=\"yellow\", label=\"layer 1\", layer=1)\n", - "ax.fill_between(x, np.sin(x) + 1.5, 0.0, color=\"green\", label=\"layer 2\", layer=2)\n", - "ax.set_title(\"Layers 0 and 1 only\")\n", - "ax.set_legend(True)" + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"cyan\", label=\"layer 0\", layer=0)\n", + "canvas.plot(x, np.cos(x), color=\"yellow\", label=\"layer 1\", layer=1)\n", + "canvas.fill_between(x, np.sin(x) + 1.5, 0.0, color=\"green\", label=\"layer 2\", layer=2)\n", + "canvas.set_title(\"Layers 0 and 1 only\")\n", + "canvas.set_legend(True)" ] }, { @@ -383,7 +383,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(canvas.plot(backend=\"plotext\", layers=[0]).build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\", layers=[0]).build(keep_colors=False))" ] }, { @@ -393,7 +393,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(canvas.plot(backend=\"plotext\", layers=[1]).build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\", layers=[1]).build(keep_colors=False))" ] }, { @@ -403,7 +403,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(canvas.plot(backend=\"plotext\", layers=[0, 1]).build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\", layers=[0, 1]).build(keep_colors=False))" ] }, { @@ -445,7 +445,7 @@ "ax2.set_legend(True)\n", "\n", "canvas.suptitle(\"Terminal dashboard\")\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -467,13 +467,13 @@ "source": [ "data = np.arange(1, 26).reshape(5, 5)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.add_imshow(data)\n", - "ax.set_title(\"Matrix-style imshow\")\n", - "ax.set_xlabel(\"column\")\n", - "ax.set_ylabel(\"row\")\n", + "canvas = Canvas()\n", + "canvas.imshow(data)\n", + "canvas.set_title(\"Matrix-style imshow\")\n", + "canvas.set_xlabel(\"column\")\n", + "canvas.set_ylabel(\"row\")\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -501,16 +501,16 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.add_patch(\n", + "canvas = Canvas()\n", + "canvas.add_patch(\n", " mpatches.Rectangle(\n", " (0.2, 0.2), 1.3, 0.7, fill=False, edgecolor=\"yellow\", label=\"window\"\n", " )\n", ")\n", - "ax.add_patch(\n", + "canvas.add_patch(\n", " mpatches.Circle((2.2, 1.6), 0.45, fill=False, edgecolor=\"cyan\", label=\"sensor\")\n", ")\n", - "ax.add_patch(\n", + "canvas.add_patch(\n", " mpatches.Polygon(\n", " [[3.0, 0.5], [3.8, 1.2], [3.4, 2.0]],\n", " fill=True,\n", @@ -518,17 +518,17 @@ " label=\"region\",\n", " )\n", ")\n", - "ax.add_patch(\n", + "canvas.add_patch(\n", " mpatches.Ellipse(\n", " (2.8, 1.0), 0.8, 0.5, fill=False, edgecolor=\"white\", label=\"ellipse\"\n", " )\n", ")\n", - "ax.set_xlim(0, 4.5)\n", - "ax.set_ylim(0, 2.5)\n", - "ax.set_title(\"Supported patch types\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlim(0, 4.5)\n", + "canvas.set_ylim(0, 2.5)\n", + "canvas.set_title(\"Supported patch types\")\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -578,16 +578,16 @@ "source": [ "x = np.linspace(-20, 20, 161)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, x**3, color=\"cyan\", label=\"x^3\")\n", - "ax.set_title(\"Symlog example\")\n", - "ax.add_caption(\"caption text\")\n", - "ax.set_xscale(\"symlog\")\n", - "ax.set_yscale(\"symlog\")\n", - "ax.set_aspect(\"equal\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.plot(x, x**3, color=\"cyan\", label=\"x^3\")\n", + "canvas.set_title(\"Symlog example\")\n", + "canvas.add_caption(\"caption text\")\n", + "canvas.set_xscale(\"symlog\")\n", + "canvas.set_yscale(\"symlog\")\n", + "canvas.set_aspect(\"equal\")\n", + "canvas.set_legend(True)\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -599,12 +599,12 @@ "source": [ "heat = np.arange(1, 26).reshape(5, 5)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.add_imshow(heat)\n", - "ax.add_colorbar(label=\"intensity\")\n", - "ax.set_title(\"Matrix + colorbar note\")\n", + "canvas = Canvas()\n", + "canvas.imshow(heat)\n", + "canvas.add_colorbar(label=\"intensity\")\n", + "canvas.set_title(\"Matrix + colorbar note\")\n", "\n", - "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" ] }, { @@ -648,10 +648,10 @@ "source": [ "x = np.linspace(0, 2 * np.pi, 60)\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", - "ax.set_title(\"Saved terminal figure\")\n", - "terminal_fig = canvas.plot(backend=\"plotext\")\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", + "canvas.set_title(\"Saved terminal figure\")\n", + "terminal_fig = canvas.render(backend=\"plotext\")\n", "\n", "output_path = Path(\"plotext_output.txt\")\n", "terminal_fig.savefig(output_path)\n", @@ -684,15 +684,15 @@ "x = np.linspace(0, 2 * np.pi, 120)\n", "\n", "for phase in np.linspace(0, 2 * np.pi, 24):\n", - " canvas, ax = Canvas.subplots()\n", - " ax.plot(x, np.sin(x + phase), color=\"cyan\", label=\"sin(x + phase)\")\n", - " ax.plot(x, np.cos(x + phase), color=\"yellow\", label=\"cos(x + phase)\")\n", - " ax.set_ylim(-1.2, 1.2)\n", - " ax.set_title(f\"Animated phase = {phase:.2f}\")\n", - " ax.set_legend(True)\n", + " canvas = Canvas()\n", + " canvas.plot(x, np.sin(x + phase), color=\"cyan\", label=\"sin(x + phase)\")\n", + " canvas.plot(x, np.cos(x + phase), color=\"yellow\", label=\"cos(x + phase)\")\n", + " canvas.set_ylim(-1.2, 1.2)\n", + " canvas.set_title(f\"Animated phase = {phase:.2f}\")\n", + " canvas.set_legend(True)\n", "\n", " clear_output(wait=True)\n", - " print(canvas.plot(backend=\"plotext\").build(keep_colors=False))\n", + " print(canvas.render(backend=\"plotext\").build(keep_colors=False))\n", " time.sleep(0.08)" ] }, diff --git a/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb b/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb index b5bebab..7044b62 100644 --- a/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb +++ b/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb @@ -70,10 +70,12 @@ ")\n", "for i, row in enumerate(tight_axes):\n", " for j, ax in enumerate(row):\n", - " ax.plot(x, np.sin((i + 1) * (j + 1) * x), label=f\"sin({(i + 1) * (j + 1)}x)\")\n", - " ax.set_title(f\"line {i},{j}\")\n", + " tight_canvas.plot(\n", + " x, np.sin((i + 1) * (j + 1) * x), label=f\"sin({(i + 1) * (j + 1)}x)\"\n", + " )\n", + " tight_canvas.set_title(f\"line {i},{j}\")\n", "\n", - "tight_fig, tight_m_axes = tight_canvas.plot(backend=\"matplotlib\")\n", + "tight_fig, tight_m_axes = tight_canvas.render(backend=\"matplotlib\")\n", "tight_fig.suptitle(\"Line plots - tight spacing\")\n", "tight_h, tight_v = measure_gaps(tight_m_axes)\n", "print(f\"tight line gaps: h={tight_h:.4f}, v={tight_v:.4f}\")\n", @@ -88,10 +90,12 @@ ")\n", "for i, row in enumerate(loose_axes):\n", " for j, ax in enumerate(row):\n", - " ax.plot(x, np.sin((i + 1) * (j + 1) * x), label=f\"sin({(i + 1) * (j + 1)}x)\")\n", - " ax.set_title(f\"line {i},{j}\")\n", + " loose_canvas.plot(\n", + " x, np.sin((i + 1) * (j + 1) * x), label=f\"sin({(i + 1) * (j + 1)}x)\"\n", + " )\n", + " loose_canvas.set_title(f\"line {i},{j}\")\n", "\n", - "loose_fig, loose_m_axes = loose_canvas.plot(backend=\"matplotlib\")\n", + "loose_fig, loose_m_axes = loose_canvas.render(backend=\"matplotlib\")\n", "loose_fig.suptitle(\"Line plots - loose spacing\")\n", "loose_h, loose_v = measure_gaps(loose_m_axes)\n", "print(f\"loose line gaps: h={loose_h:.4f}, v={loose_v:.4f}\")\n", @@ -128,11 +132,11 @@ "idx = 0\n", "for row in tight_axes:\n", " for ax in row:\n", - " ax.add_imshow(base + idx, cmap=\"viridis\")\n", - " ax.set_title(f\"heatmap {idx}\")\n", + " tight_canvas.imshow(base + idx, cmap=\"viridis\")\n", + " tight_canvas.set_title(f\"heatmap {idx}\")\n", " idx += 1\n", "\n", - "tight_fig, tight_m_axes = tight_canvas.plot(backend=\"matplotlib\")\n", + "tight_fig, tight_m_axes = tight_canvas.render(backend=\"matplotlib\")\n", "tight_fig.suptitle(\"Color plots - tight spacing\")\n", "tight_h, tight_v = measure_gaps(tight_m_axes)\n", "print(f\"tight color gaps: h={tight_h:.4f}, v={tight_v:.4f}\")\n", @@ -148,11 +152,11 @@ "idx = 0\n", "for row in loose_axes:\n", " for ax in row:\n", - " ax.add_imshow(base + idx, cmap=\"viridis\")\n", - " ax.set_title(f\"heatmap {idx}\")\n", + " loose_canvas.imshow(base + idx, cmap=\"viridis\")\n", + " loose_canvas.set_title(f\"heatmap {idx}\")\n", " idx += 1\n", "\n", - "loose_fig, loose_m_axes = loose_canvas.plot(backend=\"matplotlib\")\n", + "loose_fig, loose_m_axes = loose_canvas.render(backend=\"matplotlib\")\n", "loose_fig.suptitle(\"Color plots - loose spacing\")\n", "loose_h, loose_v = measure_gaps(loose_m_axes)\n", "print(f\"loose color gaps: h={loose_h:.4f}, v={loose_v:.4f}\")\n", diff --git a/tutorials/tutorial_11_gantt_charts.ipynb b/tutorials/tutorial_11_gantt_charts.ipynb index cc90229..4372f19 100644 --- a/tutorials/tutorial_11_gantt_charts.ipynb +++ b/tutorials/tutorial_11_gantt_charts.ipynb @@ -17,8 +17,7 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", - "import numpy as np" + "from maxplotlib import Canvas" ] }, { @@ -54,7 +53,7 @@ "\n", "canvas.set_xlabel(\"Time (days)\")\n", "canvas.set_title(\"Project Timeline\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -118,7 +117,7 @@ "canvas.set_xlabel(\"Time (days)\")\n", "canvas.set_title(\"Multi-Phase Project Timeline\")\n", "canvas.set_legend(True)\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -153,7 +152,7 @@ "\n", "canvas.set_xlabel(\"Week\")\n", "canvas.set_title(\"Team Resource Allocation\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -186,7 +185,7 @@ "\n", "canvas.set_xlabel(\"Days\")\n", "canvas.set_title(\"Interactive Project Timeline\")\n", - "fig = canvas.plot(backend=\"plotly\")\n", + "fig = canvas.render(backend=\"plotly\")\n", "fig.show()" ] }, @@ -221,7 +220,7 @@ "\n", "canvas.set_xlabel(\"Days\")\n", "canvas.set_title(\"Project with Milestones\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -247,13 +246,13 @@ "canvas_mpl = Canvas(nrows=1, ncols=1, figsize=(10, 5))\n", "canvas_mpl.gantt(tasks, start_times, durations, color=\"skyblue\")\n", "canvas_mpl.set_title(\"Matplotlib Backend\")\n", - "canvas_mpl.plot(backend=\"matplotlib\")\n", + "canvas_mpl.render(backend=\"matplotlib\")\n", "\n", "# Plotly\n", "canvas_plotly = Canvas(nrows=1, ncols=1, figsize=(10, 5))\n", "canvas_plotly.gantt(tasks, start_times, durations, color=\"skyblue\")\n", "canvas_plotly.set_title(\"Plotly Backend\")\n", - "fig = canvas_plotly.plot(backend=\"plotly\")\n", + "fig = canvas_plotly.render(backend=\"plotly\")\n", "fig.show()" ] }, diff --git a/tutorials/tutorial_12_flame_charts.ipynb b/tutorials/tutorial_12_flame_charts.ipynb index 9c35492..74d15a3 100644 --- a/tutorials/tutorial_12_flame_charts.ipynb +++ b/tutorials/tutorial_12_flame_charts.ipynb @@ -17,8 +17,7 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas\n", - "import numpy as np" + "from maxplotlib import Canvas" ] }, { @@ -68,7 +67,7 @@ "canvas.set_xlabel(\"Time (ms)\")\n", "canvas.set_ylabel(\"Stack Depth\")\n", "canvas.set_title(\"Function Call Hierarchy\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -133,7 +132,7 @@ "canvas.set_xlabel(\"Time (ms)\")\n", "canvas.set_ylabel(\"Stack Depth\")\n", "canvas.set_title(\"Complex Application Profiling\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -182,7 +181,7 @@ "canvas.set_xlabel(\"Time (ms)\")\n", "canvas.set_ylabel(\"Call Stack Depth\")\n", "canvas.set_title(\"CPU Profiling: Parallel Workers\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -229,7 +228,7 @@ "canvas.set_xlabel(\"Time (ms)\")\n", "canvas.set_ylabel(\"Stack Depth\")\n", "canvas.set_title(\"Interactive Flame Chart - Hover for Details\")\n", - "fig = canvas.plot(backend=\"plotly\")\n", + "fig = canvas.render(backend=\"plotly\")\n", "fig.show()" ] }, @@ -265,7 +264,7 @@ "canvas.set_xlabel(\"Relative Time\")\n", "canvas.set_ylabel(\"Depth\")\n", "canvas.set_title(\"Flame Chart with Auto-computed Start Times\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { @@ -301,7 +300,7 @@ " edgecolor=\"black\",\n", " )\n", " canvas.set_title(f\"Colormap: {cmap}\")\n", - " canvas.plot(backend=\"matplotlib\")" + " canvas.render(backend=\"matplotlib\")" ] }, { @@ -351,7 +350,7 @@ "canvas.set_xlabel(\"Time (ms)\")\n", "canvas.set_ylabel(\"Call Stack\")\n", "canvas.set_title(\"Web Request Processing Profile\")\n", - "canvas.plot(backend=\"matplotlib\")" + "canvas.render(backend=\"matplotlib\")" ] }, { diff --git a/tutorials/tutorial_13_advanced_matplotlib.ipynb b/tutorials/tutorial_13_advanced_matplotlib.ipynb index 580a319..47e2954 100644 --- a/tutorials/tutorial_13_advanced_matplotlib.ipynb +++ b/tutorials/tutorial_13_advanced_matplotlib.ipynb @@ -42,11 +42,11 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots(width=\"12cm\", ratio=0.65)\n", - "ax.barh([0, 1, 2], [2, 4, 3], color=\"steelblue\", alpha=0.8)\n", - "ax.set_yticks([0, 1, 2], labels=[\"A\", \"B\", \"C\"])\n", - "ax.set_xlabel(\"Amount\")\n", - "ax.set_title(\"Horizontal bars\")\n", + "canvas = Canvas(width=\"12cm\", ratio=0.65)\n", + "canvas.barh([0, 1, 2], [2, 4, 3], color=\"steelblue\", alpha=0.8)\n", + "canvas.set_yticks([0, 1, 2], labels=[\"A\", \"B\", \"C\"])\n", + "canvas.set_xlabel(\"Amount\")\n", + "canvas.set_title(\"Horizontal bars\")\n", "canvas.show()" ] }, @@ -58,11 +58,11 @@ "outputs": [], "source": [ "samples = np.random.default_rng(4).normal(size=1000)\n", - "canvas, ax = Canvas.subplots()\n", - "ax.hist(samples, bins=30, color=\"slateblue\", alpha=0.75)\n", - "ax.set_xlabel(\"Value\")\n", - "ax.set_ylabel(\"Count\")\n", - "ax.set_title(\"Distribution\")\n", + "canvas = Canvas()\n", + "canvas.hist(samples, bins=30, color=\"slateblue\", alpha=0.75)\n", + "canvas.set_xlabel(\"Value\")\n", + "canvas.set_ylabel(\"Count\")\n", + "canvas.set_title(\"Distribution\")\n", "canvas.show()" ] }, @@ -81,11 +81,11 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.step([0, 1, 2, 3], [1, 3, 2, 4], where=\"mid\", label=\"step\")\n", - "ax.stairs([1, 2, 1], edges=[0, 1, 2, 3], color=\"purple\", label=\"stairs\")\n", - "ax.set_title(\"Discrete data\")\n", - "ax.set_legend(True)\n", + "canvas = Canvas()\n", + "canvas.step([0, 1, 2, 3], [1, 3, 2, 4], where=\"mid\", label=\"step\")\n", + "canvas.stairs([1, 2, 1], edges=[0, 1, 2, 3], color=\"purple\", label=\"stairs\")\n", + "canvas.set_title(\"Discrete data\")\n", + "canvas.set_legend(True)\n", "canvas.show()" ] }, @@ -96,9 +96,9 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.broken_barh([(0, 1), (1.5, 0.75), (2.75, 1.0)], (0, 0.6), color=\"orange\")\n", - "ax.set_xlabel(\"Intervals\")\n", + "canvas = Canvas()\n", + "canvas.broken_barh([(0, 1), (1.5, 0.75), (2.75, 1.0)], (0, 0.6), color=\"orange\")\n", + "canvas.set_xlabel(\"Intervals\")\n", "canvas.show()" ] }, @@ -109,9 +109,9 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.pie([30, 45, 25], labels=[\"A\", \"B\", \"C\"], autopct=\"%1.0f%%\")\n", - "ax.set_title(\"Shares\")\n", + "canvas = Canvas()\n", + "canvas.pie([30, 45, 25], labels=[\"A\", \"B\", \"C\"], autopct=\"%1.0f%%\")\n", + "canvas.set_title(\"Shares\")\n", "canvas.show()" ] }, @@ -130,13 +130,13 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, np.sin(x), color=\"black\")\n", - "ax.fill_betweenx([-1, 0, 1], 0.5, [1.0, 1.5, 2.0], alpha=0.2)\n", - "ax.axvspan(1.0, 2.0, color=\"orange\", alpha=0.2)\n", - "ax.axhspan(-0.25, 0.25, color=\"steelblue\", alpha=0.15)\n", - "ax.arrow(2.0, np.sin(2.0), 0.5, 0.3, length_includes_head=True)\n", - "ax.axline((0, 0), slope=0.2, linestyle=\"--\", color=\"crimson\")\n", + "canvas = Canvas()\n", + "canvas.plot(x, np.sin(x), color=\"black\")\n", + "canvas.fill_betweenx([-1, 0, 1], 0.5, [1.0, 1.5, 2.0], alpha=0.2)\n", + "canvas.axvspan(1.0, 2.0, color=\"orange\", alpha=0.2)\n", + "canvas.axhspan(-0.25, 0.25, color=\"steelblue\", alpha=0.15)\n", + "canvas.arrow(2.0, np.sin(2.0), 0.5, 0.3, length_includes_head=True)\n", + "canvas.axline((0, 0), slope=0.2, linestyle=\"--\", color=\"crimson\")\n", "canvas.show()" ] }, @@ -191,11 +191,11 @@ "xx, yy = np.meshgrid(x, y)\n", "z = xx**2 + yy**2\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.contour(x, y, z, colors=\"black\")\n", - "ax.contourf(x, y, z, alpha=0.5)\n", - "ax.pcolormesh(x, y, z, alpha=0.25)\n", - "ax.set_title(\"Scalar field\")\n", + "canvas = Canvas()\n", + "canvas.contour(x, y, z, colors=\"black\")\n", + "canvas.contourf(x, y, z, alpha=0.5)\n", + "canvas.pcolormesh(x, y, z, alpha=0.25)\n", + "canvas.set_title(\"Scalar field\")\n", "canvas.show()" ] }, @@ -214,9 +214,9 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.hexbin(xx.ravel(), yy.ravel(), gridsize=12)\n", - "ax.matshow(z)\n", + "canvas = Canvas()\n", + "canvas.hexbin(xx.ravel(), yy.ravel(), gridsize=12)\n", + "canvas.matshow(z)\n", "canvas.show()" ] }, @@ -240,11 +240,11 @@ "triangles = [[0, 1, 2], [1, 3, 2]]\n", "values = points_x + points_y\n", "\n", - "canvas, ax = Canvas.subplots()\n", - "ax.quiver(points_x, points_y, np.ones(4), np.ones(4))\n", - "ax.triplot(points_x, points_y, triangles=triangles)\n", - "ax.tripcolor(points_x, points_y, values, triangles=triangles, alpha=0.3)\n", - "ax.tricontour(points_x, points_y, values, triangles=triangles)\n", + "canvas = Canvas()\n", + "canvas.quiver(points_x, points_y, np.ones(4), np.ones(4))\n", + "canvas.triplot(points_x, points_y, triangles=triangles)\n", + "canvas.tripcolor(points_x, points_y, values, triangles=triangles, alpha=0.3)\n", + "canvas.tricontour(points_x, points_y, values, triangles=triangles)\n", "canvas.show()" ] }, @@ -263,10 +263,10 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.stem([0, 1, 2], [1, 3, 2])\n", - "ax.stackplot([0, 1, 2], [1, 2, 1], [2, 1, 2], alpha=0.4)\n", - "ax.set_title(\"Discrete and stacked data\")\n", + "canvas = Canvas()\n", + "canvas.stem([0, 1, 2], [1, 3, 2])\n", + "canvas.stackplot([0, 1, 2], [1, 2, 1], [2, 1, 2], alpha=0.4)\n", + "canvas.set_title(\"Discrete and stacked data\")\n", "canvas.show()" ] }, @@ -277,10 +277,10 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.boxplot([[1, 2, 3], [2, 4, 5]])\n", - "ax.violinplot([[1, 2, 3], [2, 4, 5]])\n", - "ax.eventplot([[0.2, 0.5], [1.0, 1.5]])\n", + "canvas = Canvas()\n", + "canvas.boxplot([[1, 2, 3], [2, 4, 5]])\n", + "canvas.violinplot([[1, 2, 3], [2, 4, 5]])\n", + "canvas.eventplot([[0.2, 0.5], [1.0, 1.5]])\n", "canvas.show()" ] } diff --git a/tutorials/tutorial_14_axis_and_layout_controls.ipynb b/tutorials/tutorial_14_axis_and_layout_controls.ipynb index ac43ba3..e02300f 100644 --- a/tutorials/tutorial_14_axis_and_layout_controls.ipynb +++ b/tutorials/tutorial_14_axis_and_layout_controls.ipynb @@ -40,9 +40,9 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.fill([0, 1, 2, 1], [0, 2, 0, -1], color=\"tab:orange\", alpha=0.5)\n", - "ax.set_title(\"A filled polygon\")\n", + "canvas = Canvas()\n", + "canvas.fill([0, 1, 2, 1], [0, 2, 0, -1], color=\"tab:orange\", alpha=0.5)\n", + "canvas.set_title(\"A filled polygon\")\n", "canvas.show()" ] }, @@ -53,7 +53,7 @@ "source": [ "## Logarithmic plotting shortcuts\n", "\n", - "Use `semilogx()`, `semilogy()`, or `loglog()` when the data and axis scale should be configured together." + "Use `semilogx()`, `semilogy()`, or `loglog()` when the data and axis scale should be configured together. The three-panel example below shows the optional `fig`/`axs` style for users familiar with Matplotlib; ordinary one-panel examples use `Canvas` directly." ] }, { @@ -90,11 +90,11 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.plot([0, 1, 2], [0, 4, 1])\n", - "ax.axis([0, 2, -1, 5])\n", - "ax.relim()\n", - "ax.autoscale_view(tight=True)\n", + "canvas = Canvas()\n", + "canvas.plot([0, 1, 2], [0, 4, 1])\n", + "canvas.axis([0, 2, -1, 5])\n", + "canvas.relim()\n", + "canvas.autoscale_view(tight=True)\n", "canvas.show()" ] }, @@ -115,16 +115,16 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.plot(x, x**2)\n", - "ax.set_xlabel(\"distance (m)\")\n", - "ax.set_ylabel(\"area (m²)\")\n", - "ax.secondary_xaxis(\n", + "canvas = Canvas()\n", + "canvas.plot(x, x**2)\n", + "canvas.set_xlabel(\"distance (m)\")\n", + "canvas.set_ylabel(\"area (m²)\")\n", + "canvas.secondary_xaxis(\n", " \"top\",\n", " functions=(lambda value: value / 1000, lambda value: value * 1000),\n", " label=\"distance (km)\",\n", ")\n", - "ax.secondary_yaxis(\n", + "canvas.secondary_yaxis(\n", " \"right\", functions=(np.sqrt, lambda value: value**2), label=\"length (m)\"\n", ")\n", "canvas.show()" @@ -145,13 +145,13 @@ "metadata": {}, "outputs": [], "source": [ - "canvas, ax = Canvas.subplots()\n", - "ax.plot([0, 1, 2], [0, 1, 0], marker=\"o\")\n", - "ax.set_box_aspect(1)\n", - "ax.set_xticks([0, 1, 2])\n", - "ax.set_xticklabels([\"start\", \"middle\", \"end\"], rotation=25, color=\"navy\")\n", - "ax.set_yticks([0, 1])\n", - "ax.set_yticklabels([\"low\", \"high\"], fontweight=\"bold\")\n", + "canvas = Canvas()\n", + "canvas.plot([0, 1, 2], [0, 1, 0], marker=\"o\")\n", + "canvas.set_box_aspect(1)\n", + "canvas.set_xticks([0, 1, 2])\n", + "canvas.set_xticklabels([\"start\", \"middle\", \"end\"], rotation=25, color=\"navy\")\n", + "canvas.set_yticks([0, 1])\n", + "canvas.set_yticklabels([\"low\", \"high\"], fontweight=\"bold\")\n", "canvas.show()" ] }, @@ -172,10 +172,10 @@ "metadata": {}, "outputs": [], "source": [ - "plotly_canvas, plotly_ax = Canvas.subplots()\n", - "plotly_ax.fill(x, np.sin(x) + 2, color=\"purple\", alpha=0.25)\n", - "plotly_ax.loglog(x, x**2, color=\"black\")\n", - "plotly_ax.set_xticklabels([\"small\", \"medium\", \"large\"], color=\"darkgreen\")\n", + "plotly_canvas = Canvas()\n", + "plotly_canvas.fill(x, np.sin(x) + 2, color=\"purple\", alpha=0.25)\n", + "plotly_canvas.loglog(x, x**2, color=\"black\")\n", + "plotly_canvas.set_xticklabels([\"small\", \"medium\", \"large\"], color=\"darkgreen\")\n", "plotly_canvas.show(backend=\"plotly\")" ] } diff --git a/tutorials/tutorial_tikzfigure_subplots.ipynb b/tutorials/tutorial_15_tikzfigure_subplots.ipynb similarity index 95% rename from tutorials/tutorial_tikzfigure_subplots.ipynb rename to tutorials/tutorial_15_tikzfigure_subplots.ipynb index faedb7a..16daecd 100644 --- a/tutorials/tutorial_tikzfigure_subplots.ipynb +++ b/tutorials/tutorial_15_tikzfigure_subplots.ipynb @@ -5,7 +5,7 @@ "id": "0", "metadata": {}, "source": [ - "# TikzFigure Subplots Tutorial\n", + "# Tutorial 15 - TikzFigure Subplots Tutorial\n", "\n", "This tutorial demonstrates how to create side-by-side subplots using the `tikzfigure` backend.\n", "\n", @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "tikz = canvas.plot(backend=\"tikzfigure\")\n", + "tikz = canvas.render(backend=\"tikzfigure\")\n", "subplot_code = tikz.generate_tikz()\n", "\n", "for line in subplot_code.splitlines():\n", @@ -135,7 +135,7 @@ "\n", "- Only **horizontal layouts (1×n)** are supported with tikzfigure backend\n", "- Vertical/grid layouts (nrows > 1) will raise an error\n", - "- Use matplotlib backend for complex layouts or grids\n", + "- Use the direct tikzfigure API for complex layouts or grids\n", "- Each subplot's title becomes a pgfplots `title=` entry in the generated LaTeX output" ] } diff --git a/tutorials/tutorial_16_plotext_advanced.ipynb b/tutorials/tutorial_16_plotext_advanced.ipynb new file mode 100644 index 0000000..969571a --- /dev/null +++ b/tutorials/tutorial_16_plotext_advanced.ipynb @@ -0,0 +1,199 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Tutorial 16 - Plotext Advanced Workflows\n", + "\n", + "This tutorial focuses on practical terminal workflows that are easy to miss when moving from Matplotlib to the Plotext backend: layer-by-layer output, subplot dashboards, terminal-safe files, and backend limitations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import matplotlib.patches as patches\n", + "import numpy as np\n", + "\n", + "from maxplotlib import Canvas" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## 1. Build a terminal dashboard\n", + "\n", + "`Canvas.subplots()` works with Plotext too. Keep subplot titles short enough for a terminal and render with `keep_colors=False` when the output will be logged or tested." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, axes = Canvas.subplots(nrows=1, ncols=2)\n", + "x = np.linspace(0, 2 * np.pi, 80)\n", + "\n", + "axes[0].plot(x, np.sin(x), label=\"signal\")\n", + "axes[0].set_title(\"Signal\")\n", + "axes[0].set_grid(True)\n", + "axes[0].set_legend(True)\n", + "\n", + "axes[1].bar([0, 1, 2], [4, 7, 3], label=\"count\")\n", + "axes[1].set_xticks([0, 1, 2], labels=[\"A\", \"B\", \"C\"])\n", + "axes[1].set_title(\"Counts\")\n", + "\n", + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2. Add uncertainty and annotations\n", + "\n", + "Error bars accept scalar, symmetric-array, and Matplotlib's two-row asymmetric-array forms. Reference lines, text, and annotations are rendered as terminal primitives." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.arange(5)\n", + "y = np.array([1.0, 1.8, 1.3, 2.5, 2.0])\n", + "canvas, ax = Canvas.subplots()\n", + "ax.errorbar(\n", + " x,\n", + " y,\n", + " yerr=[[0.1] * 5, [0.25] * 5],\n", + " label=\"observations\",\n", + ")\n", + "ax.axhline(y.mean(), color=\"yellow\")\n", + "ax.annotate(\"peak\", xy=(3, 2.5), xytext=(2, 2.8))\n", + "ax.set_title(\"Measurements\")\n", + "ax.set_legend(True)\n", + "\n", + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3. Render layers incrementally\n", + "\n", + "Layers are useful for progress reports and debugging. Pass a list to `render(..., layers=[...])`, or use `savefig(..., layer_by_layer=True)` to write successive text files." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, ax = Canvas.subplots()\n", + "ax.plot(x, y, label=\"raw\", layer=0)\n", + "ax.plot(x, np.maximum.accumulate(y), label=\"running max\", layer=1)\n", + "ax.set_title(\"Layered diagnostics\")\n", + "ax.set_legend(True)\n", + "\n", + "for selected_layers in ([0], [0, 1]):\n", + " text = canvas.render(backend=\"plotext\", layers=selected_layers).build(\n", + " keep_colors=False\n", + " )\n", + " print(f\"--- layers={selected_layers} ---\")\n", + " print(text)" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 4. Save terminal output\n", + "\n", + "Plotext output is text, not an image. Saving without ANSI colors makes the file portable to CI logs, issue trackers, and plain-text artifacts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "output_path = Path(\"plotext-output.txt\")\n", + "figure = canvas.render(backend=\"plotext\")\n", + "figure.savefig(output_path, keep_colors=False)\n", + "print(f\"wrote {output_path}\")" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 5. Matrix plots and patches\n", + "\n", + "Matrix data is displayed with Plotext's heatmap primitive. Common Matplotlib patches are approximated by their polygon outline, which is useful for lightweight terminal diagnostics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, ax = Canvas.subplots()\n", + "ax.add_imshow(np.arange(16).reshape(4, 4))\n", + "ax.add_patch(patches.Rectangle((0.5, 0.5), 2, 2, fill=False, label=\"window\"))\n", + "ax.add_colorbar(label=\"intensity\")\n", + "ax.set_title(\"Matrix diagnostic\")\n", + "ax.set_legend(True)\n", + "\n", + "print(canvas.render(backend=\"plotext\").build(keep_colors=False))" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Current limitations\n", + "\n", + "The backend intentionally raises `NotImplementedError` for plot types that do not have a faithful Plotext 6 equivalent, such as histograms, pie charts, stem plots, and secondary/twin axes. Use Matplotlib or Plotly for those cases." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}