From c288b533abb207241d5a7eedaa3935ad3ee38e0c Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 22 May 2026 15:13:20 +0200 Subject: [PATCH 1/3] extended the plotly coverage --- README.md | 9 + examples/plotly_backend_basic.py | 23 ++ examples/plotly_backend_parity.py | 49 ++++ pyproject.toml | 2 +- src/maxplotlib/canvas/canvas.py | 219 +++++++++++++-- src/maxplotlib/subfigure/line_plot.py | 295 ++++++++++++++++++-- src/maxplotlib/tests/test_plotly_backend.py | 42 +++ tutorials/tutorial_08_plotly.ipynb | 255 ++++++++++------- 8 files changed, 754 insertions(+), 140 deletions(-) create mode 100644 examples/plotly_backend_basic.py create mode 100644 examples/plotly_backend_parity.py create mode 100644 src/maxplotlib/tests/test_plotly_backend.py diff --git a/README.md b/README.md index 0e1215f..1eac2d2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,15 @@ ax.set_legend(True) canvas.show(backend="plotext") ``` +## Examples + +Runnable example scripts live in `examples/`: + +``` bash +python examples/plotly_backend_basic.py +python examples/plotly_backend_parity.py +``` + ### Layers ``` python diff --git a/examples/plotly_backend_basic.py b/examples/plotly_backend_basic.py new file mode 100644 index 0000000..1278b49 --- /dev/null +++ b/examples/plotly_backend_basic.py @@ -0,0 +1,23 @@ +import numpy as np + +from maxplotlib import Canvas + + +def main() -> None: + x = np.linspace(0, 2 * np.pi, 200) + + canvas = Canvas(width="12cm", ratio=0.5) + canvas.add_line(x, np.sin(x), color="royalblue", label="sin(x)") + canvas.scatter(x[::12], np.sin(x[::12]), color="tomato", label="samples") + canvas.axhline(0, color="black", linestyle="dotted") + canvas.set_title("Plotly backend (basic)") + canvas.set_xlabel("x") + canvas.set_ylabel("y") + canvas.set_grid(True) + canvas.set_legend(True) + + canvas.savefig("plotly_basic.html", backend="plotly") + + +if __name__ == "__main__": + main() diff --git a/examples/plotly_backend_parity.py b/examples/plotly_backend_parity.py new file mode 100644 index 0000000..4278754 --- /dev/null +++ b/examples/plotly_backend_parity.py @@ -0,0 +1,49 @@ +import matplotlib.patches as mpatches +import numpy as np + +from maxplotlib import Canvas + + +def main() -> None: + x = np.linspace(0.5, 10, 60) + y = np.sqrt(x) + + canvas = Canvas(width="12cm", ratio=0.55) + + canvas.add_line(x, y, color="steelblue", label="sqrt(x)") + canvas.errorbar( + x[::10], + y[::10], + yerr=0.15, + color="tomato", + marker="o", + label="samples ± err", + ) + canvas.fill_between( + x, y - 0.1, y + 0.1, color="steelblue", alpha=0.2, label="band" + ) + canvas.vlines([2, 5, 8], ymin=0, ymax=3.5, color="gray", linestyle="dashed") + canvas.text(7.2, 2.8, "note", color="purple") + canvas.annotate( + "peak-ish", xy=(9.5, np.sqrt(9.5)), xytext=(6.0, 3.1), color="purple" + ) + + canvas.add_patch( + mpatches.Rectangle((1.2, 0.0), 2.5, 1.2, fill=True), + facecolor="rgba(255,0,0,0.1)", + edgecolor="crimson", + alpha=0.3, + ) + + canvas.set_title("Plotly backend (parity features)") + canvas.set_xlabel("x") + canvas.set_ylabel("y") + canvas.set_xscale("log") + canvas.set_grid(True) + canvas.set_legend(True) + + canvas.savefig("plotly_parity.html", backend="plotly") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index ed7639c..d1c1c03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "pint", "plotly", "plotext", - "tikzfigure[vis]>=0.2.1", + "tikzfigure[vis]>=0.3.0", ] [project.optional-dependencies] test = [ diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 658185e..26b618b 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -5,6 +5,7 @@ import matplotlib.patches as patches import matplotlib.pyplot as plt +import numpy as np from plotly.subplots import make_subplots from tikzfigure import TikzFigure @@ -579,6 +580,41 @@ def text( """Add a text label at (x, y) on a subplot.""" self._get_or_create_subplot(row, col).text(x, y, s, layer=layer, **kwargs) + def imshow( + self, + data, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add an image/matrix plot to a subplot.""" + self._get_or_create_subplot(row, col).add_imshow(data, layer=layer, **kwargs) + + def add_patch( + self, + patch, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add a Matplotlib patch to a subplot.""" + self._get_or_create_subplot(row, col).add_patch(patch, layer=layer, **kwargs) + + def colorbar( + self, + label: str = "", + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add a colorbar to the most recent imshow() on a subplot (matplotlib backend).""" + self._get_or_create_subplot(row, col).add_colorbar( + label=label, layer=layer, **kwargs + ) + # ------------------------------------------------------------------ # Multi-subplot helpers # ------------------------------------------------------------------ @@ -773,6 +809,34 @@ def savefig( figure.savefig(full_filepath) if verbose: print(f"Saved {full_filepath}") + elif backend == "plotly": + if layer_by_layer: + layers = [] + for layer in self.layers: + layers.append(layer) + full_filepath = f"{filename_no_extension}_{layers}{extension}" + fig = self.plot( + backend="plotly", + savefig=False, + layers=layers, + ) + self._save_plotly(fig, full_filepath) + if verbose: + print(f"Saved {full_filepath}") + else: + if layers is None: + layers = self.layers + full_filepath = filename + else: + full_filepath = f"{filename_no_extension}_{layers}{extension}" + fig = self.plot( + backend="plotly", + savefig=False, + layers=layers, + ) + self._save_plotly(fig, full_filepath) + if verbose: + print(f"Saved {full_filepath}") def plot( self, @@ -797,6 +861,7 @@ def plot( elif backend == "plotly": return self.plot_plotly( savefig=savefig, + layers=layers, usetex=resolved_usetex, verbose=verbose, ) @@ -832,7 +897,11 @@ def show( # self._matplotlib_fig.show() elif backend == "plotly": resolved_usetex = self._usetex if usetex is None else usetex - self.plot_plotly(savefig=False, usetex=resolved_usetex) + fig = self.plot_plotly( + savefig=False, layers=layers, usetex=resolved_usetex, verbose=verbose + ) + fig.show() + return fig elif backend == "plotext": figure = self.plot_plotext( savefig=False, @@ -1034,6 +1103,7 @@ def plot_plotly( self, show=True, savefig=None, + layers: list | None = None, usetex: bool | None = None, verbose: bool = False, ): @@ -1063,38 +1133,123 @@ def plot_plotly( ratio=self._ratio, ) # print(self._width, fig_width, fig_height) - # Create subplots + # Create subplot titles in row-major order (Plotly expects rows*cols entries) + subplot_titles = [""] * (self.nrows * self.ncols) + for (row, col), sp in self._subplot_dict.items(): + index = row * self.ncols + col + subplot_titles[index] = sp._title or f"({row}, {col})" + fig = make_subplots( rows=self.nrows, cols=self.ncols, - subplot_titles=[ - sp._title or f"({row}, {col})" - for (row, col), sp in self._subplot_dict.items() - ], + subplot_titles=subplot_titles, ) # Plot each subplot and propagate axis labels/scale - axis_index = 1 for (row, col), line_plot in self._subplot_dict.items(): - traces = line_plot.plot_plotly() + traces, shapes, annotations = line_plot.plot_plotly(layers=layers) for trace in traces: fig.add_trace(trace, row=row + 1, col=col + 1) - # Axis label keys are "xaxis", "xaxis2", "xaxis3", ... - xkey = "xaxis" if axis_index == 1 else f"xaxis{axis_index}" - ykey = "yaxis" if axis_index == 1 else f"yaxis{axis_index}" - layout_patch = {} - if line_plot._xlabel: - layout_patch[xkey] = {"title": {"text": line_plot._xlabel}} - if line_plot._ylabel: - layout_patch[ykey] = {"title": {"text": line_plot._ylabel}} + # Axis indices are row-major: (row*ncols + col + 1) + axis_index = row * self.ncols + col + 1 + xref = "x" if axis_index == 1 else f"x{axis_index}" + yref = "y" if axis_index == 1 else f"y{axis_index}" + + for shape in shapes: + shape = dict(shape) + if shape.get("xref") not in {"paper"}: + shape["xref"] = xref + if shape.get("yref") not in {"paper"}: + shape["yref"] = yref + fig.add_shape(shape) + + for annotation in annotations: + annotation = dict(annotation) + annotation.setdefault("xref", xref) + annotation.setdefault("yref", yref) + fig.add_annotation(annotation) + + # Apply per-axis config in a row/col-safe way + xaxis_kwargs = dict( + title_text=line_plot._xlabel or None, + showgrid=bool(line_plot._grid), + row=row + 1, + col=col + 1, + ) if line_plot._xaxis_scale == "log": - layout_patch.setdefault(xkey, {})["type"] = "log" + xaxis_kwargs["type"] = "log" + fig.update_xaxes(**xaxis_kwargs) + + yaxis_kwargs = dict( + title_text=line_plot._ylabel or None, + showgrid=bool(line_plot._grid), + row=row + 1, + col=col + 1, + ) if line_plot._yaxis_scale == "log": - layout_patch.setdefault(ykey, {})["type"] = "log" - if layout_patch: - fig.update_layout(**layout_patch) - axis_index += 1 + yaxis_kwargs["type"] = "log" + fig.update_yaxes(**yaxis_kwargs) + + # Axis limits + if line_plot._xmin is not None or line_plot._xmax is not None: + x_range = [ + line_plot._xmin if line_plot._xmin is not None else None, + line_plot._xmax if line_plot._xmax is not None else None, + ] + if ( + line_plot._xaxis_scale == "log" + and x_range[0] is not None + and x_range[1] is not None + and x_range[0] > 0 + and x_range[1] > 0 + ): + x_range = [np.log10(x_range[0]), np.log10(x_range[1])] + fig.update_xaxes( + range=x_range, + row=row + 1, + col=col + 1, + ) + if line_plot._ymin is not None or line_plot._ymax is not None: + y_range = [ + line_plot._ymin if line_plot._ymin is not None else None, + line_plot._ymax if line_plot._ymax is not None else None, + ] + if ( + line_plot._yaxis_scale == "log" + and y_range[0] is not None + and y_range[1] is not None + and y_range[0] > 0 + and y_range[1] > 0 + ): + y_range = [np.log10(y_range[0]), np.log10(y_range[1])] + fig.update_yaxes( + range=y_range, + row=row + 1, + col=col + 1, + ) + + # Custom ticks (positions + optional labels) + if line_plot._xticks is not None: + fig.update_xaxes( + tickmode="array", + tickvals=line_plot._xticks, + ticktext=line_plot._xticklabels, + row=row + 1, + col=col + 1, + ) + if line_plot._yticks is not None: + fig.update_yaxes( + tickmode="array", + tickvals=line_plot._yticks, + ticktext=line_plot._yticklabels, + row=row + 1, + col=col + 1, + ) + + # Aspect ratio + if line_plot._aspect == "equal": + fig.update_yaxes(scaleanchor=xref, row=row + 1, col=col + 1) # Update layout settings fig.update_layout( @@ -1105,10 +1260,30 @@ def plot_plotly( fig.update_layout(title=dict(text=self._suptitle, x=0.5)) if savefig: - fig.write_image(savefig) + try: + fig.write_image(savefig) + except Exception as exc: + raise RuntimeError( + "Plotly image export failed. If you are exporting to PNG/PDF/SVG, " + "install kaleido (e.g., `pip install -U kaleido`)." + ) from exc return fig + def _save_plotly(self, fig, filename: str) -> None: + _, extension = os.path.splitext(filename) + extension = extension.lower() + if extension in {".html", ".htm"}: + fig.write_html(filename) + return + try: + fig.write_image(filename) + except Exception as exc: + raise RuntimeError( + "Plotly image export failed. For PNG/PDF/SVG export, install kaleido " + "(e.g., `pip install -U kaleido`), or export to HTML instead." + ) from exc + # Property getters @property diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index daf1383..1bfe6c6 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -565,9 +565,14 @@ def plot_tikzfigure(self, layers=None, verbose: bool = False) -> TikzFigure: print(tikz_figure.generate_tikz()) return tikz_figure - def plot_plotly(self): + def plot_plotly(self, layers=None): """ - Plot all lines using Plotly and return a list of traces for each line. + Plot all lines using Plotly. + + Returns a tuple of (traces, shapes, annotations) where: + - traces are plotly graph objects to add with fig.add_trace() + - shapes are layout shape dicts to add with fig.add_shape() + - annotations are layout annotation dicts to add with fig.add_annotation() """ linestyle_map = { "solid": "solid", @@ -576,48 +581,306 @@ def plot_plotly(self): "dashdot": "dashdot", } - traces = [] - for line in self.line_data: + marker_map = { + "o": "circle", + ".": "circle", + "s": "square", + "^": "triangle-up", + "v": "triangle-down", + "<": "triangle-left", + ">": "triangle-right", + "x": "x", + "+": "cross", + "*": "star", + "D": "diamond", + } + + traces: list[go.BaseTraceType] = [] + shapes: list[dict] = [] + annotations: list[dict] = [] + last_heatmap_idx: int | None = None + + for line in self._iter_layer_lines(layers=layers): plot_type = line["plot_type"] if plot_type == "plot": + kwargs = line["kwargs"] + marker = kwargs.get("marker") + mode = "lines+markers" if marker is not None else "lines" trace = go.Scatter( x=(line["x"] + self._xshift) * self._xscale, y=(line["y"] + self._yshift) * self._yscale, - mode="lines+markers" if "marker" in line["kwargs"] else "lines", - name=line["kwargs"].get("label", ""), + mode=mode, + name=kwargs.get("label", ""), line=dict( - color=line["kwargs"].get("color", None), + color=kwargs.get("color", None), dash=linestyle_map.get( - line["kwargs"].get("linestyle", "solid"), + kwargs.get("linestyle", "solid"), "solid", ), + width=kwargs.get("linewidth", None), + ), + marker=( + dict( + color=kwargs.get("color", None), + symbol=marker_map.get(marker, "circle"), + size=kwargs.get("markersize", None), + ) + if marker is not None + else None ), ) traces.append(trace) elif plot_type == "scatter": + kwargs = line["kwargs"] + marker = kwargs.get("marker", "circle") trace = go.Scatter( x=(line["x"] + self._xshift) * self._xscale, y=(line["y"] + self._yshift) * self._yscale, mode="markers", - name=line["kwargs"].get("label", ""), - marker=dict(color=line["kwargs"].get("color", None)), + name=kwargs.get("label", ""), + marker=dict( + color=kwargs.get("color", None), + symbol=marker_map.get(marker, marker), + size=kwargs.get("s", None), + ), ) traces.append(trace) elif plot_type == "bar": + kwargs = line["kwargs"] trace = go.Bar( x=(line["x"] + self._xshift) * self._xscale, y=line["height"] * self._yscale, - name=line["kwargs"].get("label", ""), - marker_color=line["kwargs"].get("color", None), + name=kwargs.get("label", ""), + marker_color=kwargs.get("color", None), + ) + traces.append(trace) + elif plot_type == "fill_between": + kwargs = line["kwargs"] + x = (line["x"] + self._xshift) * self._xscale + if np.isscalar(line["y1"]): + y1 = np.full_like(np.asarray(x, dtype=float), float(line["y1"])) + else: + y1 = (line["y1"] + self._yshift) * self._yscale + if np.isscalar(line["y2"]): + y2 = np.full_like(np.asarray(x, dtype=float), float(line["y2"])) + else: + y2 = (line["y2"] + self._yshift) * self._yscale + + color = kwargs.get("color", kwargs.get("facecolor", None)) + alpha = kwargs.get("alpha", 0.3) + fill_trace = go.Scatter( + x=np.concatenate([x, x[::-1]]), + y=np.concatenate([y1, y2[::-1]]), + fill="toself", + fillcolor=color, + opacity=alpha, + line=dict(color="rgba(0,0,0,0)"), + name=kwargs.get("label", ""), + showlegend=bool(kwargs.get("label")), + ) + traces.append(fill_trace) + elif plot_type == "errorbar": + kwargs = line["kwargs"] + marker = kwargs.get("marker") + mode = "lines+markers" if marker is not None else "lines" + x_vals = (line["x"] + self._xshift) * self._xscale + y_vals = (line["y"] + self._yshift) * self._yscale + yerr = line.get("yerr") + xerr = line.get("xerr") + if yerr is not None and np.isscalar(yerr): + yerr = np.full(len(x_vals), float(yerr)) + if xerr is not None and np.isscalar(xerr): + xerr = np.full(len(x_vals), float(xerr)) + trace = go.Scatter( + x=x_vals, + y=y_vals, + mode=mode, + name=kwargs.get("label", ""), + line=dict( + color=kwargs.get("color", None), + dash=linestyle_map.get(kwargs.get("linestyle", "solid"), "solid"), + width=kwargs.get("linewidth", None), + ), + marker=( + dict( + color=kwargs.get("color", None), + symbol=marker_map.get(marker, "circle"), + size=kwargs.get("markersize", None), + ) + if marker is not None + else None + ), + error_y=( + dict(type="data", array=yerr, visible=True) + if yerr is not None + else None + ), + error_x=( + dict(type="data", array=xerr, visible=True) + if xerr is not None + else None + ), ) traces.append(trace) - elif plot_type in ("axhline", "axvline"): - pass # Rendered as shape annotations; no trace needed + elif plot_type in ("axhline", "axvline", "hlines", "vlines"): + kwargs = line["kwargs"] + color = kwargs.get("color", kwargs.get("colors", "black")) + dash = linestyle_map.get(kwargs.get("linestyle", "solid"), "solid") + width = kwargs.get("linewidth", 1) + if plot_type == "axhline": + shapes.append( + dict( + type="line", + x0=0, + x1=1, + xref="paper", + y0=(line["y"] + self._yshift) * self._yscale, + y1=(line["y"] + self._yshift) * self._yscale, + line=dict(color=color, dash=dash, width=width), + ) + ) + elif plot_type == "axvline": + shapes.append( + dict( + type="line", + y0=0, + y1=1, + yref="paper", + x0=(line["x"] + self._xshift) * self._xscale, + x1=(line["x"] + self._xshift) * self._xscale, + line=dict(color=color, dash=dash, width=width), + ) + ) + elif plot_type == "hlines": + y_vals = np.atleast_1d(line["y"]) + xmins = np.atleast_1d(line["xmin"]) + xmaxs = np.atleast_1d(line["xmax"]) + for y, xmin, xmax in zip(y_vals, xmins, xmaxs): + shapes.append( + dict( + type="line", + x0=(xmin + self._xshift) * self._xscale, + x1=(xmax + self._xshift) * self._xscale, + y0=(y + self._yshift) * self._yscale, + y1=(y + self._yshift) * self._yscale, + line=dict(color=color, dash=dash, width=width), + ) + ) + elif plot_type == "vlines": + x_vals = np.atleast_1d(line["x"]) + ymins = np.atleast_1d(line["ymin"]) + ymaxs = np.atleast_1d(line["ymax"]) + for x, ymin, ymax in zip(x_vals, ymins, ymaxs): + shapes.append( + dict( + type="line", + x0=(x + self._xshift) * self._xscale, + x1=(x + self._xshift) * self._xscale, + y0=(ymin + self._yshift) * self._yscale, + y1=(ymax + self._yshift) * self._yscale, + line=dict(color=color, dash=dash, width=width), + ) + ) + elif plot_type in ("text", "annotate"): + kwargs = line["kwargs"] + if plot_type == "text": + x = (float(line["x"]) + self._xshift) * self._xscale + y = (float(line["y"]) + self._yshift) * self._yscale + text = line["s"] + annotations.append( + dict( + x=x, + y=y, + text=text, + showarrow=False, + font=dict( + color=kwargs.get("color", None), + size=kwargs.get("fontsize", None), + ), + ) + ) + else: + x = (float(line["xy"][0]) + self._xshift) * self._xscale + y = (float(line["xy"][1]) + self._yshift) * self._yscale + ann = dict( + x=x, + y=y, + text=line["text"], + showarrow=True, + arrowhead=2, + ax=0, + ay=-30, + font=dict( + color=kwargs.get("color", None), + size=kwargs.get("fontsize", None), + ), + ) + if line.get("xytext") is not None: + tx = (float(line["xytext"][0]) + self._xshift) * self._xscale + ty = (float(line["xytext"][1]) + self._yshift) * self._yscale + ann.update(axref="x", ayref="y", ax=tx, ay=ty) + annotations.append(ann) + elif plot_type == "imshow": + kwargs = line["kwargs"] + heatmap = go.Heatmap( + z=line["data"], + colorscale=kwargs.get("cmap", "Viridis"), + showscale=True, + ) + traces.append(heatmap) + last_heatmap_idx = len(traces) - 1 + elif plot_type == "colorbar": + if last_heatmap_idx is not None: + label = line.get("label", "") or line["kwargs"].get("label", "") + if label: + traces[last_heatmap_idx].update(colorbar=dict(title=dict(text=label))) + elif plot_type == "patch": + kwargs = line["kwargs"] + patch = line["patch"] + try: + import matplotlib.patches as mpl_patches + except Exception: + mpl_patches = None + + if mpl_patches is not None and isinstance(patch, mpl_patches.Rectangle): + x0 = (patch.get_x() + self._xshift) * self._xscale + y0 = (patch.get_y() + self._yshift) * self._yscale + x1 = (patch.get_x() + patch.get_width() + self._xshift) * self._xscale + y1 = (patch.get_y() + patch.get_height() + self._yshift) * self._yscale + shapes.append( + dict( + type="rect", + x0=x0, + y0=y0, + x1=x1, + y1=y1, + line=dict(color=kwargs.get("edgecolor", kwargs.get("color", "black"))), + fillcolor=kwargs.get("facecolor", None), + opacity=kwargs.get("alpha", None), + ) + ) + elif mpl_patches is not None and isinstance(patch, mpl_patches.Circle): + cx = (patch.center[0] + self._xshift) * self._xscale + cy = (patch.center[1] + self._yshift) * self._yscale + r = patch.radius + shapes.append( + dict( + type="circle", + x0=cx - r, + y0=cy - r, + x1=cx + r, + y1=cy + r, + line=dict(color=kwargs.get("edgecolor", kwargs.get("color", "black"))), + fillcolor=kwargs.get("facecolor", None), + opacity=kwargs.get("alpha", None), + ) + ) - return traces + return traces, shapes, annotations def _iter_layer_lines(self, layers=None): - for layer_name, layer_lines in self.layered_line_data.items(): + for layer_name in sorted(self.layered_line_data): + layer_lines = self.layered_line_data[layer_name] if layers and layer_name not in layers: continue for line in layer_lines: diff --git a/src/maxplotlib/tests/test_plotly_backend.py b/src/maxplotlib/tests/test_plotly_backend.py new file mode 100644 index 0000000..c463374 --- /dev/null +++ b/src/maxplotlib/tests/test_plotly_backend.py @@ -0,0 +1,42 @@ +import numpy as np + + +def test_plotly_backend_supports_common_primitives(): + from maxplotlib import Canvas + + x = np.linspace(0, 1, 10) + + canvas, ax = Canvas.subplots() + ax.plot(x, x, color="royalblue", label="line") + ax.scatter(x, x**2, color="tomato", label="points") + ax.errorbar(x[::2], (x**2)[::2], yerr=0.1, color="black", label="err") + ax.fill_between(x, x - 0.1, x + 0.1, color="gray", alpha=0.2, label="band") + ax.axhline(0.5, color="black", linestyle="dotted") + ax.axvline(0.25, color="black", linestyle="dashed") + ax.text(0.8, 0.8, "hi", color="purple") + ax.annotate("there", xy=(0.3, 0.3), xytext=(0.6, 0.5), color="purple") + ax.set_grid(True) + ax.set_legend(True) + + fig = canvas.plot(backend="plotly") + + assert fig is not None + assert len(fig.data) >= 4 # line, scatter, errorbar, fill_between + assert len(getattr(fig.layout, "shapes", []) or []) >= 2 # axhline + axvline + assert len(getattr(fig.layout, "annotations", []) or []) >= 2 # subplot title + text/annotate + + +def test_plotly_backend_respects_layers(): + from maxplotlib import Canvas + + x = np.linspace(0, 1, 10) + canvas, ax = Canvas.subplots() + 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]) + + assert len(fig0.data) == 1 + assert len(fig1.data) == 1 + diff --git a/tutorials/tutorial_08_plotly.ipynb b/tutorials/tutorial_08_plotly.ipynb index e42225c..b21fada 100644 --- a/tutorials/tutorial_08_plotly.ipynb +++ b/tutorials/tutorial_08_plotly.ipynb @@ -43,9 +43,13 @@ "id": "2", "metadata": {}, "source": [ - "## 1 · Basic line plot\n", + "## Plotly in Jupyter\n", "\n", - "Switch to the Plotly backend by passing `backend='plotly'` to `canvas.plot()`. The returned object is a genuine `plotly.graph_objects.Figure`, so every Plotly method is available on it." + "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", + "\n" ] }, { @@ -57,21 +61,50 @@ "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=\"sin(x)\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Basic line plot\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.45)\n", + "canvas.add_line(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\")\n", + "canvas.set_title(\"Displayed inline\")\n", + "canvas.set_legend(True)\n", "\n", - "fig = canvas.plot(backend=\"plotly\")\n", - "print(type(fig)) # plotly.graph_objects.Figure\n", - "fig.show()" + "fig = canvas.show(backend=\"plotly\")\n", + "fig\n", + "\n" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, + "source": [ + "## 1 · Basic line plot\n", + "\n", + "Switch to the Plotly backend by passing `backend='plotly'` to `canvas.plot()`. The returned object is a genuine `plotly.graph_objects.Figure`, so every Plotly method is available on it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "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.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Basic line plot\")\n", + "\n", + "fig = canvas.show(backend=\"plotly\")\n", + "fig\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, "source": [ "## 2 · Multiple lines\n", "\n", @@ -81,31 +114,37 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "7", "metadata": {}, "outputs": [], "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=\"sin(x)\", linewidth=2)\n", - "ax.plot(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2)\n", - "ax.plot(\n", - " x, np.sin(2 * x), color=\"seagreen\", label=\"sin(2x)\", linewidth=2, linestyle=\"dashed\"\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", + " x,\n", + " np.sin(2 * x),\n", + " color=\"seagreen\",\n", + " label=\"sin(2x)\",\n", + " linewidth=2,\n", + " linestyle=\"dashed\",\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Multiple lines\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Multiple lines\")\n", + "canvas.set_legend(True)\n", "\n", - "fig = canvas.plot(backend=\"plotly\")\n", - "fig.show()" + "fig = canvas.show(backend=\"plotly\")\n", + "fig\n", + "\n" ] }, { "cell_type": "markdown", - "id": "6", + "id": "8", "metadata": {}, "source": [ "## 3 · Scatter plot\n", @@ -116,7 +155,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -125,24 +164,30 @@ "x_data = rng.uniform(0, 10, n)\n", "y_data = 0.5 * x_data + rng.normal(0, 1, n)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", - "ax.scatter(x_data, y_data, color=\"steelblue\", marker=\"o\", s=20, label=\"observations\")\n", - "ax.plot(\n", - " [0, 10], [0, 5], color=\"tomato\", linestyle=\"dashed\", linewidth=2, label=\"y = 0.5x\"\n", + "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", + "canvas.scatter(x_data, y_data, color=\"steelblue\", marker=\"o\", s=20, label=\"observations\")\n", + "canvas.add_line(\n", + " [0, 10],\n", + " [0, 5],\n", + " color=\"tomato\",\n", + " linestyle=\"dashed\",\n", + " linewidth=2,\n", + " label=\"y = 0.5x\",\n", ")\n", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Scatter plot with trend line\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Scatter plot with trend line\")\n", + "canvas.set_legend(True)\n", "\n", - "fig = canvas.plot(backend=\"plotly\")\n", - "fig.show()" + "fig = canvas.show(backend=\"plotly\")\n", + "fig\n", + "\n" ] }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ "## 4 · Bar chart\n", @@ -153,7 +198,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -161,21 +206,23 @@ "values = [4.2, 7.1, 3.8, 5.9, 6.4]\n", "x_pos = np.arange(len(categories))\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", - "ax.bar(x_pos, values, color=\"steelblue\", label=\"metric\")\n", + "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", + "canvas.bar(x_pos, values, color=\"steelblue\", label=\"metric\")\n", + "canvas.set_xticks(x_pos, categories)\n", "\n", - "ax.set_xlabel(\"Category\")\n", - "ax.set_ylabel(\"Value\")\n", - "ax.set_title(\"Bar chart\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"Category\")\n", + "canvas.set_ylabel(\"Value\")\n", + "canvas.set_title(\"Bar chart\")\n", + "canvas.set_legend(True)\n", "\n", - "fig = canvas.plot(backend=\"plotly\")\n", - "fig.show()" + "fig = canvas.show(backend=\"plotly\")\n", + "fig\n", + "\n" ] }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": {}, "source": [ "## 5 · Mixing lines and bars\n", @@ -186,7 +233,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -194,9 +241,9 @@ "rainfall = np.array([55, 48, 62, 70, 85, 40, 30, 35, 60, 90, 75, 65])\n", "cumulative = np.cumsum(rainfall)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", - "ax.bar(months, rainfall, color=\"steelblue\", alpha=0.7, label=\"monthly rainfall (mm)\")\n", - "ax.plot(\n", + "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", + "canvas.bar(months, rainfall, color=\"steelblue\", alpha=0.7, label=\"monthly rainfall (mm)\")\n", + "canvas.add_line(\n", " months,\n", " cumulative / 10,\n", " color=\"tomato\",\n", @@ -205,18 +252,19 @@ " label=\"cumulative / 10\",\n", ")\n", "\n", - "ax.set_xlabel(\"Month\")\n", - "ax.set_ylabel(\"Rainfall (mm)\")\n", - "ax.set_title(\"Monthly rainfall + cumulative trend\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"Month\")\n", + "canvas.set_ylabel(\"Rainfall (mm)\")\n", + "canvas.set_title(\"Monthly rainfall + cumulative trend\")\n", + "canvas.set_legend(True)\n", "\n", - "fig = canvas.plot(backend=\"plotly\")\n", - "fig.show()" + "fig = canvas.show(backend=\"plotly\")\n", + "fig\n", + "\n" ] }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "## 6 · Multiple subplots\n", @@ -227,44 +275,50 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 2 * np.pi, 200)\n", "rng = np.random.default_rng(0)\n", "\n", - "canvas, (ax1, ax2) = Canvas.subplots(ncols=2)\n", + "canvas = Canvas(ncols=1) #, width=\"14cm\", ratio=0.35)\n", "\n", "# Left panel — line plot\n", - "ax1.plot(x, np.sin(x), color=\"steelblue\", label=\"sin(x)\", linewidth=2)\n", - "ax1.plot(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2)\n", - "ax1.set_xlabel(\"x\")\n", - "ax1.set_ylabel(\"y\")\n", - "ax1.set_title(\"Trigonometric functions\")\n", - "ax1.set_legend(True)\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.set_xlabel(\"x\", col=0)\n", + "canvas.set_ylabel(\"y\", col=0)\n", + "canvas.set_title(\"Trigonometric functions\", col=0)\n", + "canvas.set_legend(True, col=0)\n", "\n", "# Right panel — scatter\n", "x_s = rng.uniform(0, 6, 80)\n", "y_s = np.sin(x_s) + rng.normal(0, 0.15, 80)\n", - "ax2.scatter(x_s, y_s, color=\"seagreen\", marker=\"o\", s=18, label=\"noisy sin\")\n", - "ax2.plot(\n", - " x, np.sin(x), color=\"black\", linestyle=\"dashed\", linewidth=1.5, label=\"true sin\"\n", + "canvas.scatter(x_s, y_s, color=\"seagreen\", marker=\"o\", s=18, label=\"noisy sin\", col=1)\n", + "canvas.add_line(\n", + " x,\n", + " np.sin(x),\n", + " color=\"black\",\n", + " linestyle=\"dashed\",\n", + " linewidth=1.5,\n", + " label=\"true sin\",\n", + " col=1,\n", ")\n", - "ax2.set_xlabel(\"x\")\n", - "ax2.set_ylabel(\"y\")\n", - "ax2.set_title(\"Noisy observations\")\n", - "ax2.set_legend(True)\n", + "canvas.set_xlabel(\"x\", col=1)\n", + "canvas.set_ylabel(\"y\", col=1)\n", + "canvas.set_title(\"Noisy observations\", col=1)\n", + "canvas.set_legend(True, col=1)\n", "\n", "canvas.suptitle(\"Multi-panel Plotly figure\")\n", "\n", - "fig = canvas.plot(backend=\"plotly\")\n", - "fig.show()" + "fig = canvas.show(backend=\"plotly\")\n", + "\n" ] }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "## 7 · Log scale\n", @@ -275,30 +329,30 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "17", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0.1, 5, 200)\n", "\n", - "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", - "ax.plot(x, np.exp(x), color=\"steelblue\", label=\"exp(x)\", linewidth=2)\n", - "ax.plot(x, np.exp(1.5 * x), color=\"tomato\", label=\"exp(1.5x)\", linewidth=2)\n", - "ax.plot(x, x**2, color=\"seagreen\", label=\"x²\", linewidth=2)\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", "\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y (log scale)\")\n", - "ax.set_title(\"Log-scale y axis\")\n", - "ax.set_yscale(\"log\")\n", - "ax.set_legend(True)\n", + "canvas.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y (log scale)\")\n", + "canvas.set_title(\"Log-scale y axis\")\n", + "canvas.set_yscale(\"log\")\n", + "canvas.set_legend(True)\n", "\n", - "fig = canvas.plot(backend=\"plotly\")\n", - "fig.show()" + "fig = canvas.show(backend=\"plotly\")\n", + "\n" ] }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ "## 8 · Saving to HTML\n", @@ -309,30 +363,29 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "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=\"sin(x)\", linewidth=2)\n", - "ax.plot(x, np.cos(x), color=\"tomato\", label=\"cos(x)\", linewidth=2)\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_title(\"Saved interactive figure\")\n", - "ax.set_legend(True)\n", - "\n", - "fig = canvas.plot(backend=\"plotly\")\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.set_xlabel(\"x\")\n", + "canvas.set_ylabel(\"y\")\n", + "canvas.set_title(\"Saved interactive figure\")\n", + "canvas.set_legend(True)\n", "\n", "# Writes a standalone HTML file — open it in any browser\n", - "fig.write_html(\"output.html\")\n", - "print(\"Saved to output.html\")" + "canvas.savefig(\"output.html\", backend=\"plotly\")\n", + "print(\"Saved to output.html\")\n", + "\n" ] }, { "cell_type": "markdown", - "id": "18", + "id": "20", "metadata": {}, "source": [ "## Summary\n", @@ -373,7 +426,7 @@ ], "metadata": { "kernelspec": { - "display_name": "env_maxplotlib", + "display_name": ".venv", "language": "python", "name": "python3" }, From 120f503f7d47182987239c4461ac992fa3dbd42f Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 22 May 2026 15:32:48 +0200 Subject: [PATCH 2/3] updated tutorials --- src/maxplotlib/canvas/canvas.py | 31 ++- src/maxplotlib/subfigure/line_plot.py | 229 +++++++++++++----- src/maxplotlib/tests/test_plotly_backend.py | 38 +++ tutorials/tutorial_01.ipynb | 60 +++-- tutorials/tutorial_02.ipynb | 78 ++++-- tutorials/tutorial_03.ipynb | 91 ++++--- tutorials/tutorial_04.ipynb | 70 ++++-- tutorials/tutorial_05.ipynb | 91 ++++--- tutorials/tutorial_06.ipynb | 73 ++++-- tutorials/tutorial_07_tikz.ipynb | 86 ++++--- tutorials/tutorial_09_plotext.ipynb | 61 ++++- .../tutorial_10_matplotlib_nxm_spacing.ipynb | 23 ++ 12 files changed, 696 insertions(+), 235 deletions(-) diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 26b618b..1ad74f1 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -1193,10 +1193,11 @@ def plot_plotly( # Axis limits if line_plot._xmin is not None or line_plot._xmax is not None: - x_range = [ - line_plot._xmin if line_plot._xmin is not None else None, - line_plot._xmax if line_plot._xmax is not None else None, - ] + x_range = [line_plot._xmin, line_plot._xmax] + if x_range[0] is not None: + x_range[0] = line_plot._transform_scalar_x(x_range[0]) + if x_range[1] is not None: + x_range[1] = line_plot._transform_scalar_x(x_range[1]) if ( line_plot._xaxis_scale == "log" and x_range[0] is not None @@ -1211,10 +1212,11 @@ def plot_plotly( col=col + 1, ) if line_plot._ymin is not None or line_plot._ymax is not None: - y_range = [ - line_plot._ymin if line_plot._ymin is not None else None, - line_plot._ymax if line_plot._ymax is not None else None, - ] + y_range = [line_plot._ymin, line_plot._ymax] + if y_range[0] is not None: + y_range[0] = line_plot._transform_scalar_y(y_range[0]) + if y_range[1] is not None: + y_range[1] = line_plot._transform_scalar_y(y_range[1]) if ( line_plot._yaxis_scale == "log" and y_range[0] is not None @@ -1231,17 +1233,19 @@ def plot_plotly( # Custom ticks (positions + optional labels) if line_plot._xticks is not None: + tickvals = [line_plot._transform_scalar_x(v) for v in line_plot._xticks] fig.update_xaxes( tickmode="array", - tickvals=line_plot._xticks, + tickvals=tickvals, ticktext=line_plot._xticklabels, row=row + 1, col=col + 1, ) if line_plot._yticks is not None: + tickvals = [line_plot._transform_scalar_y(v) for v in line_plot._yticks] fig.update_yaxes( tickmode="array", - tickvals=line_plot._yticks, + tickvals=tickvals, ticktext=line_plot._yticklabels, row=row + 1, col=col + 1, @@ -1250,6 +1254,13 @@ def plot_plotly( # Aspect ratio if line_plot._aspect == "equal": fig.update_yaxes(scaleanchor=xref, row=row + 1, col=col + 1) + elif isinstance(line_plot._aspect, (int, float)): + fig.update_yaxes( + scaleanchor=xref, + scaleratio=float(line_plot._aspect), + row=row + 1, + col=col + 1, + ) # Update layout settings fig.update_layout( diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index 1bfe6c6..9579836 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -600,6 +600,36 @@ def plot_plotly(self, layers=None): annotations: list[dict] = [] last_heatmap_idx: int | None = None + def tx(values): + return self._transform_x(values) + + def ty(values): + return self._transform_y(values) + + def txs(value): + return self._transform_scalar_x(value) + + def tys(value): + return self._transform_scalar_y(value) + + def plotly_color(value): + if value is None: + return None + if isinstance(value, np.generic): + value = value.item() + if isinstance(value, (list, tuple, np.ndarray)): + arr = np.asarray(value).astype(float).reshape(-1) + if arr.size in (3, 4): + rgb = (arr[:3] * 255.0) if np.all(arr[:3] <= 1.0) else arr[:3] + r, g, b = [int(round(float(x))) for x in rgb] + if arr.size == 4: + a = float(arr[3]) + if a > 1.0: + a = a / 255.0 + return f"rgba({r},{g},{b},{a})" + return f"rgb({r},{g},{b})" + return value + for line in self._iter_layer_lines(layers=layers): plot_type = line["plot_type"] if plot_type == "plot": @@ -607,12 +637,13 @@ def plot_plotly(self, layers=None): marker = kwargs.get("marker") mode = "lines+markers" if marker is not None else "lines" trace = go.Scatter( - x=(line["x"] + self._xshift) * self._xscale, - y=(line["y"] + self._yshift) * self._yscale, + x=tx(line["x"]), + y=ty(line["y"]), mode=mode, name=kwargs.get("label", ""), + showlegend=bool(kwargs.get("label")) and bool(self._legend), line=dict( - color=kwargs.get("color", None), + color=plotly_color(kwargs.get("color", None)), dash=linestyle_map.get( kwargs.get("linestyle", "solid"), "solid", @@ -621,7 +652,7 @@ def plot_plotly(self, layers=None): ), marker=( dict( - color=kwargs.get("color", None), + color=plotly_color(kwargs.get("color", None)), symbol=marker_map.get(marker, "circle"), size=kwargs.get("markersize", None), ) @@ -634,12 +665,13 @@ def plot_plotly(self, layers=None): kwargs = line["kwargs"] marker = kwargs.get("marker", "circle") trace = go.Scatter( - x=(line["x"] + self._xshift) * self._xscale, - y=(line["y"] + self._yshift) * self._yscale, + x=tx(line["x"]), + y=ty(line["y"]), mode="markers", name=kwargs.get("label", ""), + showlegend=bool(kwargs.get("label")) and bool(self._legend), marker=dict( - color=kwargs.get("color", None), + color=plotly_color(kwargs.get("color", None)), symbol=marker_map.get(marker, marker), size=kwargs.get("s", None), ), @@ -648,25 +680,26 @@ def plot_plotly(self, layers=None): elif plot_type == "bar": kwargs = line["kwargs"] trace = go.Bar( - x=(line["x"] + self._xshift) * self._xscale, - y=line["height"] * self._yscale, + x=tx(line["x"]), + y=np.asarray(line["height"]) * self._yscale, name=kwargs.get("label", ""), - marker_color=kwargs.get("color", None), + showlegend=bool(kwargs.get("label")) and bool(self._legend), + marker_color=plotly_color(kwargs.get("color", None)), ) traces.append(trace) elif plot_type == "fill_between": kwargs = line["kwargs"] - x = (line["x"] + self._xshift) * self._xscale + x = tx(line["x"]) if np.isscalar(line["y1"]): - y1 = np.full_like(np.asarray(x, dtype=float), float(line["y1"])) + y1 = np.full_like(np.asarray(x, dtype=float), float(tys(line["y1"]))) else: - y1 = (line["y1"] + self._yshift) * self._yscale + y1 = ty(line["y1"]) if np.isscalar(line["y2"]): - y2 = np.full_like(np.asarray(x, dtype=float), float(line["y2"])) + y2 = np.full_like(np.asarray(x, dtype=float), float(tys(line["y2"]))) else: - y2 = (line["y2"] + self._yshift) * self._yscale + y2 = ty(line["y2"]) - color = kwargs.get("color", kwargs.get("facecolor", None)) + color = plotly_color(kwargs.get("color", kwargs.get("facecolor", None))) alpha = kwargs.get("alpha", 0.3) fill_trace = go.Scatter( x=np.concatenate([x, x[::-1]]), @@ -676,15 +709,15 @@ def plot_plotly(self, layers=None): opacity=alpha, line=dict(color="rgba(0,0,0,0)"), name=kwargs.get("label", ""), - showlegend=bool(kwargs.get("label")), + showlegend=bool(kwargs.get("label")) and bool(self._legend), ) traces.append(fill_trace) elif plot_type == "errorbar": kwargs = line["kwargs"] marker = kwargs.get("marker") mode = "lines+markers" if marker is not None else "lines" - x_vals = (line["x"] + self._xshift) * self._xscale - y_vals = (line["y"] + self._yshift) * self._yscale + x_vals = tx(line["x"]) + y_vals = ty(line["y"]) yerr = line.get("yerr") xerr = line.get("xerr") if yerr is not None and np.isscalar(yerr): @@ -696,14 +729,15 @@ def plot_plotly(self, layers=None): y=y_vals, mode=mode, name=kwargs.get("label", ""), + showlegend=bool(kwargs.get("label")) and bool(self._legend), line=dict( - color=kwargs.get("color", None), + color=plotly_color(kwargs.get("color", None)), dash=linestyle_map.get(kwargs.get("linestyle", "solid"), "solid"), width=kwargs.get("linewidth", None), ), marker=( dict( - color=kwargs.get("color", None), + color=plotly_color(kwargs.get("color", None)), symbol=marker_map.get(marker, "circle"), size=kwargs.get("markersize", None), ) @@ -724,7 +758,7 @@ def plot_plotly(self, layers=None): traces.append(trace) elif plot_type in ("axhline", "axvline", "hlines", "vlines"): kwargs = line["kwargs"] - color = kwargs.get("color", kwargs.get("colors", "black")) + color = plotly_color(kwargs.get("color", kwargs.get("colors", "black"))) dash = linestyle_map.get(kwargs.get("linestyle", "solid"), "solid") width = kwargs.get("linewidth", 1) if plot_type == "axhline": @@ -734,8 +768,8 @@ def plot_plotly(self, layers=None): x0=0, x1=1, xref="paper", - y0=(line["y"] + self._yshift) * self._yscale, - y1=(line["y"] + self._yshift) * self._yscale, + y0=tys(line["y"]), + y1=tys(line["y"]), line=dict(color=color, dash=dash, width=width), ) ) @@ -746,8 +780,8 @@ def plot_plotly(self, layers=None): y0=0, y1=1, yref="paper", - x0=(line["x"] + self._xshift) * self._xscale, - x1=(line["x"] + self._xshift) * self._xscale, + x0=txs(line["x"]), + x1=txs(line["x"]), line=dict(color=color, dash=dash, width=width), ) ) @@ -759,10 +793,10 @@ def plot_plotly(self, layers=None): shapes.append( dict( type="line", - x0=(xmin + self._xshift) * self._xscale, - x1=(xmax + self._xshift) * self._xscale, - y0=(y + self._yshift) * self._yscale, - y1=(y + self._yshift) * self._yscale, + x0=txs(xmin), + x1=txs(xmax), + y0=tys(y), + y1=tys(y), line=dict(color=color, dash=dash, width=width), ) ) @@ -774,18 +808,18 @@ def plot_plotly(self, layers=None): shapes.append( dict( type="line", - x0=(x + self._xshift) * self._xscale, - x1=(x + self._xshift) * self._xscale, - y0=(ymin + self._yshift) * self._yscale, - y1=(ymax + self._yshift) * self._yscale, + x0=txs(x), + x1=txs(x), + y0=tys(ymin), + y1=tys(ymax), line=dict(color=color, dash=dash, width=width), ) ) elif plot_type in ("text", "annotate"): kwargs = line["kwargs"] if plot_type == "text": - x = (float(line["x"]) + self._xshift) * self._xscale - y = (float(line["y"]) + self._yshift) * self._yscale + x = txs(float(line["x"])) + y = tys(float(line["y"])) text = line["s"] annotations.append( dict( @@ -794,14 +828,14 @@ def plot_plotly(self, layers=None): text=text, showarrow=False, font=dict( - color=kwargs.get("color", None), + color=plotly_color(kwargs.get("color", None)), size=kwargs.get("fontsize", None), ), ) ) else: - x = (float(line["xy"][0]) + self._xshift) * self._xscale - y = (float(line["xy"][1]) + self._yshift) * self._yscale + x = txs(float(line["xy"][0])) + y = tys(float(line["xy"][1])) ann = dict( x=x, y=y, @@ -811,14 +845,14 @@ def plot_plotly(self, layers=None): ax=0, ay=-30, font=dict( - color=kwargs.get("color", None), + color=plotly_color(kwargs.get("color", None)), size=kwargs.get("fontsize", None), ), ) if line.get("xytext") is not None: - tx = (float(line["xytext"][0]) + self._xshift) * self._xscale - ty = (float(line["xytext"][1]) + self._yshift) * self._yscale - ann.update(axref="x", ayref="y", ax=tx, ay=ty) + tx_val = txs(float(line["xytext"][0])) + ty_val = tys(float(line["xytext"][1])) + ann.update(axref="x", ayref="y", ax=tx_val, ay=ty_val) annotations.append(ann) elif plot_type == "imshow": kwargs = line["kwargs"] @@ -842,11 +876,38 @@ def plot_plotly(self, layers=None): except Exception: mpl_patches = None + def _patch_line_color(): + return plotly_color( + kwargs.get( + "edgecolor", + kwargs.get( + "color", + patch.get_edgecolor() + if hasattr(patch, "get_edgecolor") + else "black", + ), + ) + ) + + def _patch_fill_color(): + return plotly_color( + kwargs.get( + "facecolor", + patch.get_facecolor() if hasattr(patch, "get_facecolor") else None, + ) + ) + + patch_label = kwargs.get("label") + if patch_label is None and hasattr(patch, "get_label"): + raw = patch.get_label() + if raw and not str(raw).startswith("_"): + patch_label = str(raw) + if mpl_patches is not None and isinstance(patch, mpl_patches.Rectangle): - x0 = (patch.get_x() + self._xshift) * self._xscale - y0 = (patch.get_y() + self._yshift) * self._yscale - x1 = (patch.get_x() + patch.get_width() + self._xshift) * self._xscale - y1 = (patch.get_y() + patch.get_height() + self._yshift) * self._yscale + x0 = txs(patch.get_x()) + y0 = tys(patch.get_y()) + x1 = txs(patch.get_x() + patch.get_width()) + y1 = tys(patch.get_y() + patch.get_height()) shapes.append( dict( type="rect", @@ -854,27 +915,77 @@ def plot_plotly(self, layers=None): y0=y0, x1=x1, y1=y1, - line=dict(color=kwargs.get("edgecolor", kwargs.get("color", "black"))), - fillcolor=kwargs.get("facecolor", None), + line=dict(color=_patch_line_color()), + fillcolor=_patch_fill_color(), opacity=kwargs.get("alpha", None), ) ) elif mpl_patches is not None and isinstance(patch, mpl_patches.Circle): - cx = (patch.center[0] + self._xshift) * self._xscale - cy = (patch.center[1] + self._yshift) * self._yscale - r = patch.radius + cx = txs(patch.center[0]) + cy = tys(patch.center[1]) + rx = abs(txs(patch.center[0] + patch.radius) - cx) + ry = abs(tys(patch.center[1] + patch.radius) - cy) + path = ( + f"M {cx - rx},{cy} " + f"A {rx},{ry} 0 1,0 {cx + rx},{cy} " + f"A {rx},{ry} 0 1,0 {cx - rx},{cy} Z" + ) + shapes.append( + dict( + type="path", + path=path, + line=dict(color=_patch_line_color()), + fillcolor=_patch_fill_color(), + opacity=kwargs.get("alpha", None), + ) + ) + elif mpl_patches is not None and isinstance(patch, mpl_patches.Ellipse): + cx = txs(patch.center[0]) + cy = tys(patch.center[1]) + rx = abs(txs(patch.center[0] + patch.width / 2.0) - cx) + ry = abs(tys(patch.center[1] + patch.height / 2.0) - cy) + # Ignore rotation for now; provides useful parity for tutorials. + path = ( + f"M {cx - rx},{cy} " + f"A {rx},{ry} 0 1,0 {cx + rx},{cy} " + f"A {rx},{ry} 0 1,0 {cx - rx},{cy} Z" + ) shapes.append( dict( - type="circle", - x0=cx - r, - y0=cy - r, - x1=cx + r, - y1=cy + r, - line=dict(color=kwargs.get("edgecolor", kwargs.get("color", "black"))), - fillcolor=kwargs.get("facecolor", None), + type="path", + path=path, + line=dict(color=_patch_line_color()), + fillcolor=_patch_fill_color(), opacity=kwargs.get("alpha", None), ) ) + elif mpl_patches is not None and isinstance(patch, mpl_patches.Polygon): + pts = patch.get_xy() + if len(pts) >= 2: + pts_t = [(txs(float(x)), tys(float(y))) for x, y in pts] + path = "M " + " L ".join(f"{x},{y}" for x, y in pts_t) + " Z" + shapes.append( + dict( + type="path", + path=path, + line=dict(color=_patch_line_color()), + fillcolor=_patch_fill_color(), + opacity=kwargs.get("alpha", None), + ) + ) + + # Plotly shapes don't participate in legends; add a dummy trace. + if patch_label and bool(self._legend): + traces.append( + go.Scatter( + x=[None], + y=[None], + mode="lines", + name=patch_label, + line=dict(color=_patch_line_color()), + showlegend=True, + ) + ) return traces, shapes, annotations diff --git a/src/maxplotlib/tests/test_plotly_backend.py b/src/maxplotlib/tests/test_plotly_backend.py index c463374..43c607d 100644 --- a/src/maxplotlib/tests/test_plotly_backend.py +++ b/src/maxplotlib/tests/test_plotly_backend.py @@ -40,3 +40,41 @@ def test_plotly_backend_respects_layers(): assert len(fig0.data) == 1 assert len(fig1.data) == 1 + +def test_plotly_backend_supports_common_patches_and_symlog(): + import matplotlib.patches as mpatches + + from maxplotlib import Canvas + + canvas, ax = Canvas.subplots() + ax.add_patch( + mpatches.Rectangle((0.2, 0.2), 1.3, 0.7, fill=False, edgecolor="yellow", label="r") + ) + ax.add_patch(mpatches.Circle((2.2, 1.6), 0.45, fill=False, edgecolor="cyan", label="c")) + ax.add_patch( + mpatches.Polygon( + [[3.0, 0.5], [3.8, 1.2], [3.4, 2.0]], + fill=True, + facecolor="green", + label="p", + ) + ) + ax.add_patch( + mpatches.Ellipse((2.8, 1.0), 0.8, 0.5, fill=False, edgecolor="white", label="e") + ) + ax.set_title("patches") + ax.set_legend(True) + + fig = canvas.plot(backend="plotly") + assert fig is not None + assert len(getattr(fig.layout, "shapes", []) or []) >= 4 + # patch labels become dummy legend traces + assert any(getattr(t, "name", "") == "p" for t in fig.data) + + canvas2, ax2 = Canvas.subplots() + x = np.linspace(-20, 20, 41) + ax2.plot(x, x**3, color="cyan", label="x^3") + ax2.set_xscale("symlog") + ax2.set_yscale("symlog") + fig2 = canvas2.plot(backend="plotly") + assert fig2 is not None diff --git a/tutorials/tutorial_01.ipynb b/tutorials/tutorial_01.ipynb index ab79db8..ec216f7 100644 --- a/tutorials/tutorial_01.ipynb +++ b/tutorials/tutorial_01.ipynb @@ -35,6 +35,34 @@ "cell_type": "markdown", "id": "2", "metadata": {}, + "source": [ + "## Backend selection\n", + "\n", + "All examples in this tutorial use the same `Canvas` API; you can switch rendering backends at any time:\n", + "\n", + "- `BACKEND = \"matplotlib\"` for static Matplotlib output\n", + "- `BACKEND = \"plotly\"` for interactive Plotly output (Jupyter-friendly)\n", + "\n", + "Most cells end with `canvas.show(backend=BACKEND)` so you can re-run the whole notebook with a different backend.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Change to \"plotly\" for interactive output\n", + "BACKEND = \"matplotlib\"\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, "source": [ "## 1 Minimal example\n", "\n", @@ -45,7 +73,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "5", "metadata": {}, "outputs": [], "source": [ @@ -54,12 +82,13 @@ "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, y)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "4", + "id": "6", "metadata": {}, "source": [ "## 2 Multiple lines with labels, colors, and linestyles" @@ -68,7 +97,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -90,12 +119,13 @@ "ax.set_title(\"Sine and Cosine\")\n", "ax.set_legend(True)\n", "\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "6", + "id": "8", "metadata": {}, "source": [ "## 3 Canvas-level shortcut API\n", @@ -107,7 +137,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -122,12 +152,13 @@ "canvas.set_legend(True)\n", "canvas.set_grid(True)\n", "\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ "## 4 Configuring the subplot at creation time\n", @@ -139,7 +170,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -155,12 +186,13 @@ "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", "\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": {}, "source": [ "## 5 Saving a figure\n", @@ -172,7 +204,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -186,7 +218,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "## Summary\n", diff --git a/tutorials/tutorial_02.ipynb b/tutorials/tutorial_02.ipynb index 5a00f68..987ff05 100644 --- a/tutorials/tutorial_02.ipynb +++ b/tutorials/tutorial_02.ipynb @@ -35,10 +35,38 @@ "x = np.linspace(0, 2 * np.pi, 200)" ] }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Backend selection\n", + "\n", + "All examples in this tutorial use the same `Canvas` API; you can switch rendering backends at any time:\n", + "\n", + "- `BACKEND = \"matplotlib\"` for static Matplotlib output\n", + "- `BACKEND = \"plotly\"` for interactive Plotly output (Jupyter-friendly)\n", + "\n", + "Most cells end with `canvas.show(backend=BACKEND)` so you can re-run the whole notebook with a different backend.\n", + "\n" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "2", + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Change to \"plotly\" for interactive output\n", + "BACKEND = \"matplotlib\"\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", "metadata": {}, "outputs": [], "source": [ @@ -79,7 +107,7 @@ }, { "cell_type": "markdown", - "id": "3", + "id": "5", "metadata": {}, "source": [ "## 1 1×2 layout — side-by-side subplots" @@ -88,7 +116,7 @@ { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "6", "metadata": {}, "outputs": [], "source": [ @@ -111,7 +139,7 @@ }, { "cell_type": "markdown", - "id": "5", + "id": "7", "metadata": {}, "source": [ "## 2 2×2 layout — grid of subplots\n", @@ -123,7 +151,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -139,12 +167,13 @@ "axes[1][1].set_title(\"cos(2x)\")\n", "\n", "canvas.suptitle(\"2 × 2 Layout\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "7", + "id": "9", "metadata": {}, "source": [ "## 3 `squeeze=False` — always get a 2-D list\n", @@ -156,7 +185,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -170,12 +199,13 @@ " axes[0][col].plot(x, d, color=\"steelblue\")\n", " axes[0][col].set_title(t)\n", "\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "9", + "id": "11", "metadata": {}, "source": [ "## 4 Manual layout — `canvas.add_subplot(row, col)`\n", @@ -187,7 +217,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -207,12 +237,13 @@ "ax11.plot(x, np.cos(x), label=\"cos\", color=\"tomato\")\n", "\n", "canvas.suptitle(\"Manual Layout\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "11", + "id": "13", "metadata": {}, "source": [ "## 5 Accessing subplots after creation\n", @@ -223,7 +254,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -240,12 +271,13 @@ "canvas[1, 0].set_title(\"Method C\")\n", "canvas[1, 1].set_title(\"Method D (index)\")\n", "\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "13", + "id": "15", "metadata": {}, "source": [ "## 6 `canvas.iter_subplots()` — loop over all panels" @@ -254,7 +286,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -270,12 +302,13 @@ " sp.set_grid(True)\n", " sp.set_xlabel(\"x\")\n", "\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "15", + "id": "17", "metadata": {}, "source": [ "## 7 Canvas-level plot routing\n", @@ -286,7 +319,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -300,12 +333,13 @@ "canvas.set_legend(True, row=0, col=0)\n", "canvas.set_legend(True, row=0, col=1)\n", "canvas.suptitle(\"Canvas-level routing\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "17", + "id": "19", "metadata": {}, "source": [ "## Summary\n", diff --git a/tutorials/tutorial_03.ipynb b/tutorials/tutorial_03.ipynb index cc9de2b..fb60404 100644 --- a/tutorials/tutorial_03.ipynb +++ b/tutorials/tutorial_03.ipynb @@ -37,7 +37,15 @@ "id": "2", "metadata": {}, "source": [ - "## 1 Line plot — `ax.plot()`" + "## Backend selection\n", + "\n", + "All examples in this tutorial use the same `Canvas` API; you can switch rendering backends at any time:\n", + "\n", + "- `BACKEND = \"matplotlib\"` for static Matplotlib output\n", + "- `BACKEND = \"plotly\"` for interactive Plotly output (Jupyter-friendly)\n", + "\n", + "Most cells end with `canvas.show(backend=BACKEND)` so you can re-run the whole notebook with a different backend.\n", + "\n" ] }, { @@ -46,6 +54,26 @@ "id": "3", "metadata": {}, "outputs": [], + "source": [ + "# Change to \"plotly\" for interactive output\n", + "BACKEND = \"matplotlib\"\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 1 Line plot — `ax.plot()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], "source": [ "canvas, ax = Canvas.subplots()\n", "\n", @@ -57,12 +85,13 @@ "ax.set_title(\"Line Plot\")\n", "ax.set_legend(True)\n", "ax.set_grid(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "4", + "id": "6", "metadata": {}, "source": [ "## 2 Scatter plot — `ax.scatter()`\n", @@ -73,7 +102,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -88,12 +117,13 @@ "ax.set_ylabel(\"y\")\n", "ax.set_title(\"Scatter Plot — coloured by distance\")\n", "ax.set_aspect(\"equal\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "6", + "id": "8", "metadata": {}, "source": [ "## 3 Bar chart — `ax.bar()`" @@ -102,7 +132,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -115,12 +145,13 @@ "ax.set_ylabel(\"Sales (units)\")\n", "ax.set_title(\"Bar Chart\")\n", "ax.set_legend(True)\n", - "# canvas.show() # TODO: Fix this error" + "# canvas.show(backend=BACKEND) # TODO: Fix this error\n", + "\n" ] }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ "## 4 Fill between — `ax.fill_between()`\n", @@ -131,7 +162,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -147,12 +178,13 @@ "ax.set_ylabel(\"value\")\n", "ax.set_title(\"Fill Between — Confidence Band\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": {}, "source": [ "## 5 Error bars — `ax.errorbar()`" @@ -161,7 +193,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -177,12 +209,13 @@ "ax.set_title(\"Error Bars\")\n", "ax.set_legend(True)\n", "ax.set_grid(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "## 6 Reference lines — `axhline` and `axvline`\n", @@ -193,7 +226,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -208,12 +241,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"axhline / axvline\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "## 7 Segment lines — `hlines` and `vlines`\n", @@ -224,7 +258,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -254,12 +288,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"hlines / vlines\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ "## 8 Combining plot types\n", @@ -271,7 +306,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -298,12 +333,13 @@ "ax.set_title(\"Combined: line + fill_between + scatter\")\n", "ax.set_legend(True)\n", "ax.set_grid(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "18", + "id": "20", "metadata": {}, "source": [ "## 9 Annotations — `ax.annotate()` and `ax.text()`" @@ -312,7 +348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -333,12 +369,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"sin(x)\")\n", "ax.set_title(\"Annotate and Text\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "20", + "id": "22", "metadata": {}, "source": [ "## Summary\n", diff --git a/tutorials/tutorial_04.ipynb b/tutorials/tutorial_04.ipynb index 7406fdd..922c035 100644 --- a/tutorials/tutorial_04.ipynb +++ b/tutorials/tutorial_04.ipynb @@ -25,6 +25,34 @@ "cell_type": "markdown", "id": "2", "metadata": {}, + "source": [ + "## Backend selection\n", + "\n", + "All examples in this tutorial use the same `Canvas` API; you can switch rendering backends at any time:\n", + "\n", + "- `BACKEND = \"matplotlib\"` for static Matplotlib output\n", + "- `BACKEND = \"plotly\"` for interactive Plotly output (Jupyter-friendly)\n", + "\n", + "Most cells end with `canvas.show(backend=BACKEND)` so you can re-run the whole notebook with a different backend.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Change to \"plotly\" for interactive output\n", + "BACKEND = \"matplotlib\"\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, "source": [ "## 1 · Colors\n", "\n", @@ -34,7 +62,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "5", "metadata": {}, "outputs": [], "source": [ @@ -50,12 +78,13 @@ "ax.set_ylabel(\"y\")\n", "ax.set_title(\"Color options\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "4", + "id": "6", "metadata": {}, "source": [ "## 2 · Linestyles\n", @@ -66,7 +95,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -88,12 +117,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Linestyle comparison\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "6", + "id": "8", "metadata": {}, "source": [ "## 3 · Markers\n", @@ -104,7 +134,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -126,12 +156,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Marker comparison\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ "## 4 · Linewidth and markersize\n", @@ -142,7 +173,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -159,12 +190,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Linewidth and markersize\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": {}, "source": [ "## 5 · Alpha (transparency)\n", @@ -175,7 +207,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -190,12 +222,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Alpha transparency\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "## 6 · Combining styles – a publication-ready plot\n", @@ -206,7 +239,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -238,12 +271,13 @@ "ax.set_ylabel(\"y\")\n", "ax.set_title(\"Publication-style plot\")\n", "ax.set_legend(True)\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "## Summary\n", diff --git a/tutorials/tutorial_05.ipynb b/tutorials/tutorial_05.ipynb index 828046d..517ad9a 100644 --- a/tutorials/tutorial_05.ipynb +++ b/tutorials/tutorial_05.ipynb @@ -25,6 +25,34 @@ "cell_type": "markdown", "id": "2", "metadata": {}, + "source": [ + "## Backend selection\n", + "\n", + "All examples in this tutorial use the same `Canvas` API; you can switch rendering backends at any time:\n", + "\n", + "- `BACKEND = \"matplotlib\"` for static Matplotlib output\n", + "- `BACKEND = \"plotly\"` for interactive Plotly output (Jupyter-friendly)\n", + "\n", + "Most cells end with `canvas.show(backend=BACKEND)` so you can re-run the whole notebook with a different backend.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Change to \"plotly\" for interactive output\n", + "BACKEND = \"matplotlib\"\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, "source": [ "## 1 · Labels and title\n", "\n", @@ -34,7 +62,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "5", "metadata": {}, "outputs": [], "source": [ @@ -46,12 +74,13 @@ "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.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "4", + "id": "6", "metadata": {}, "source": [ "## 2 · Axis limits" @@ -60,7 +89,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -76,12 +105,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(r\"$\\sin(x)$\")\n", "ax.set_title(\"Axis limits: first period only\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "6", + "id": "8", "metadata": {}, "source": [ "## 3 · Custom ticks\n", @@ -92,7 +122,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -118,12 +148,13 @@ "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.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ "## 4 · Log scale\n", @@ -134,7 +165,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -154,12 +185,13 @@ "ax2.set_title(\"Log scale\")\n", "\n", "canvas.suptitle(\"Linear vs log scale\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": {}, "source": [ "## 5 · Grid" @@ -168,7 +200,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -185,12 +217,13 @@ "ax2.set_title(\"With grid\")\n", "ax2.set_xlabel(\"x\")\n", "\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "12", + "id": "14", "metadata": {}, "source": [ "## 6 · Legend\n", @@ -201,7 +234,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -215,12 +248,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_legend(True)\n", "ax.set_title(\"Legend demo\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "## 7 · annotate\n", @@ -231,7 +265,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -254,12 +288,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(r\"$\\sin(x)$\")\n", "ax.set_title(\"annotate demo\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ "## 8 · text\n", @@ -270,7 +305,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -286,12 +321,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(r\"$f(x)$\")\n", "ax.set_title(\"text demo\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "18", + "id": "20", "metadata": {}, "source": [ "## 9 · Aspect ratio\n", @@ -302,7 +338,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -314,12 +350,13 @@ "ax.set_title(\"Circle with equal aspect ratio\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"y\")\n", - "canvas.show()" + "canvas.show(backend=BACKEND)\n", + "\n" ] }, { "cell_type": "markdown", - "id": "20", + "id": "22", "metadata": {}, "source": [ "## Summary\n", diff --git a/tutorials/tutorial_06.ipynb b/tutorials/tutorial_06.ipynb index df85ad1..45510ab 100644 --- a/tutorials/tutorial_06.ipynb +++ b/tutorials/tutorial_06.ipynb @@ -25,6 +25,34 @@ "cell_type": "markdown", "id": "2", "metadata": {}, + "source": [ + "## Backend selection\n", + "\n", + "All examples in this tutorial use the same `Canvas` API; you can switch rendering backends at any time:\n", + "\n", + "- `BACKEND = \"matplotlib\"` for static Matplotlib output\n", + "- `BACKEND = \"plotly\"` for interactive Plotly output (Jupyter-friendly)\n", + "\n", + "Most cells end with `canvas.show(backend=BACKEND)` so you can re-run the whole notebook with a different backend.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Change to \"plotly\" for interactive output\n", + "BACKEND = \"matplotlib\"\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, "source": [ "## 1 · Assigning data to layers\n", "\n", @@ -34,7 +62,7 @@ { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "5", "metadata": {}, "outputs": [], "source": [ @@ -56,12 +84,13 @@ "ax.set_xlabel(\"x\")\n", "ax.set_legend(True)\n", "ax.set_title(\"Three curves on three layers\")\n", - "canvas.show() # renders all layers by default" + "canvas.show(backend=BACKEND) # renders all layers by default\n", + "\n" ] }, { "cell_type": "markdown", - "id": "4", + "id": "6", "metadata": {}, "source": [ "## 2 · Rendering specific layers\n", @@ -72,7 +101,7 @@ { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -97,38 +126,41 @@ "print(\"--- Layer 0 only ---\")\n", "ax.set_title(\"Layer 0 only\")\n", "\n", - "canvas.show(layers=[0])" + "canvas.show(backend=BACKEND, layers=[0])\n", + "\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "8", "metadata": {}, "outputs": [], "source": [ "# Same canvas — now show layers 0 and 1 together\n", "ax.set_title(\"Layers 0 and 1\")\n", "\n", - "canvas.show(layers=[0, 1])" + "canvas.show(backend=BACKEND, layers=[0, 1])\n", + "\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ "# All layers\n", "ax.set_title(\"All layers\")\n", "\n", - "canvas.show(layers=[0, 1, 2])" + "canvas.show(backend=BACKEND, layers=[0, 1, 2])\n", + "\n" ] }, { "cell_type": "markdown", - "id": "8", + "id": "10", "metadata": {}, "source": [ "## 3 · Saving layer-by-layer\n", @@ -139,7 +171,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -158,7 +190,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "12", "metadata": {}, "source": [ "## 4 · Use case: progressively revealing a derivation\n", @@ -169,7 +201,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -202,36 +234,39 @@ "\n", "# Step 1: only the data cloud\n", "ax.set_title(\"Step 1 – raw data\")\n", - "canvas.show(layers=[0])" + "canvas.show(backend=BACKEND, layers=[0])\n", + "\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "14", "metadata": {}, "outputs": [], "source": [ "# Step 2: add the true curve\n", "ax.set_title(\"Step 2 – add true function\")\n", - "canvas.show(layers=[0, 1])" + "canvas.show(backend=BACKEND, layers=[0, 1])\n", + "\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "15", "metadata": {}, "outputs": [], "source": [ "# Step 3: add the uncertainty envelope\n", "ax.set_title(\"Step 3 – add uncertainty envelope\")\n", - "canvas.show(layers=[0, 1, 2])" + "canvas.show(backend=BACKEND, layers=[0, 1, 2])\n", + "\n" ] }, { "cell_type": "markdown", - "id": "14", + "id": "16", "metadata": {}, "source": [ "## Summary\n", diff --git a/tutorials/tutorial_07_tikz.ipynb b/tutorials/tutorial_07_tikz.ipynb index 816c63c..4b2636b 100644 --- a/tutorials/tutorial_07_tikz.ipynb +++ b/tutorials/tutorial_07_tikz.ipynb @@ -83,6 +83,28 @@ "cell_type": "markdown", "id": "5", "metadata": {}, + "source": [ + "### Plotly preview\n", + "\n", + "Before exporting to TikZ, you can preview the same `Canvas` interactively in a notebook using the Plotly backend:\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "canvas.show(backend=\"plotly\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, "source": [ "### 1.2 Inspecting the generated LaTeX\n", "\n", @@ -93,7 +115,7 @@ { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -103,7 +125,7 @@ }, { "cell_type": "markdown", - "id": "7", + "id": "9", "metadata": {}, "source": [ "### 1.3 TikZ-specific kwargs\n", @@ -115,7 +137,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -132,7 +154,7 @@ }, { "cell_type": "markdown", - "id": "9", + "id": "11", "metadata": {}, "source": [ "### 1.4 Layer-aware TikZ output\n", @@ -144,7 +166,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -171,7 +193,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "13", "metadata": {}, "source": [ "### 1.5 Saving TikZ code to a file\n", @@ -182,7 +204,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -205,7 +227,7 @@ }, { "cell_type": "markdown", - "id": "13", + "id": "15", "metadata": {}, "source": [ "### 1.6 Rendering the figure (requires `pdflatex`)\n", @@ -216,7 +238,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -226,7 +248,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "17", "metadata": {}, "source": [ "### 1.7 Canvas → TikZ limitations\n", @@ -246,7 +268,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "18", "metadata": {}, "source": [ "---\n", @@ -264,7 +286,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "19", "metadata": {}, "source": [ "### 2.1 Drawing paths with `draw()`\n", @@ -275,7 +297,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -293,7 +315,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "21", "metadata": {}, "source": [ "### 2.2 Straight line segments with `line()`\n", @@ -305,7 +327,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -326,7 +348,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "23", "metadata": {}, "source": [ "### 2.3 Rectangles, circles, and arcs" @@ -335,7 +357,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -368,7 +390,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "25", "metadata": {}, "source": [ "### 2.4 Nodes — text labels and markers\n", @@ -379,7 +401,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -410,7 +432,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "27", "metadata": {}, "source": [ "### 2.5 Custom colours with `colorlet()`\n", @@ -421,7 +443,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -445,7 +467,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "29", "metadata": {}, "source": [ "### 2.6 Filled paths and patterns\n", @@ -456,7 +478,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -485,7 +507,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "31", "metadata": {}, "source": [ "### 2.7 Layers in `TikzFigure`\n", @@ -497,7 +519,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -519,7 +541,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "33", "metadata": {}, "source": [ "### 2.8 Escaping to raw TikZ code\n", @@ -530,7 +552,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -550,7 +572,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "35", "metadata": {}, "source": [ "### 2.9 Putting it all together — a complete figure\n", @@ -561,7 +583,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -613,7 +635,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -623,7 +645,7 @@ }, { "cell_type": "markdown", - "id": "36", + "id": "38", "metadata": {}, "source": [ "### 2.10 Embedding in a LaTeX document\n", @@ -654,7 +676,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "39", "metadata": {}, "source": [ "---\n", diff --git a/tutorials/tutorial_09_plotext.ipynb b/tutorials/tutorial_09_plotext.ipynb index a1a02b9..b15db7c 100644 --- a/tutorials/tutorial_09_plotext.ipynb +++ b/tutorials/tutorial_09_plotext.ipynb @@ -535,6 +535,30 @@ "cell_type": "markdown", "id": "30", "metadata": {}, + "source": [ + "### Plotly backend note (patches)\n", + "\n", + "Plotly can render common Matplotlib patches too.\n", + "Note: Plotly “shapes” do not appear in legends, so maxplotlib adds a small dummy legend entry for labeled patches.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "# Interactive rendering of the same canvas\n", + "canvas.show(backend=\"plotly\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, "source": [ "## 13 · Captions, symlog scales, aspect, and colorbar notes\n", "\n", @@ -549,7 +573,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -570,7 +594,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -586,7 +610,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "35", "metadata": {}, "source": [ "## 14 · Saving terminal output to a file\n", @@ -594,10 +618,33 @@ "Saving with the plotext backend writes the rendered terminal figure to a text file. By default the saved text is plain and easy to inspect in editors, CI logs, or generated artifacts." ] }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### Plotly backend note (symlog)\n", + "\n", + "Plotly has no native symlog axis type. For `symlog`, maxplotlib applies a symmetric log transform to the data and uses a linear Plotly axis.\n", + "This keeps the plot working across backends, but tick formatting may differ from Matplotlib/plotext.\n", + "\n" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "canvas.show(backend=\"plotly\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -615,7 +662,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "39", "metadata": {}, "source": [ "## 15 · A simple terminal animation\n", @@ -628,7 +675,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -653,7 +700,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "41", "metadata": {}, "source": [ "## 16 · Current limitations\n", diff --git a/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb b/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb index e85ba12..c92696d 100644 --- a/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb +++ b/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb @@ -162,6 +162,29 @@ "\n", "plt.show()" ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Plotly preview\n", + "\n", + "The spacing measurements above are Matplotlib-specific. You can still preview the same canvas with the Plotly backend (layout/spacing will differ):\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "# Reuse the last canvas created above\n", + "loose_canvas.show(backend=\"plotly\")\n", + "\n" + ] } ], "metadata": { From d8531dcde9ae53c6aa2cadb26d253dac585b18ec Mon Sep 17 00:00:00 2001 From: Max Lindqvist Date: Fri, 22 May 2026 15:37:50 +0200 Subject: [PATCH 3/3] formatting --- examples/plotly_backend_parity.py | 4 +- src/maxplotlib/subfigure/line_plot.py | 44 ++++++++++++------- src/maxplotlib/tests/test_plotly_backend.py | 12 +++-- tutorials/tutorial_01.ipynb | 15 +++---- tutorials/tutorial_02.ipynb | 21 +++------ tutorials/tutorial_03.ipynb | 30 +++++-------- tutorials/tutorial_04.ipynb | 21 +++------ tutorials/tutorial_05.ipynb | 30 +++++-------- tutorials/tutorial_06.ipynb | 24 ++++------ tutorials/tutorial_07_tikz.ipynb | 3 +- tutorials/tutorial_08_plotly.ipynb | 37 +++++++--------- tutorials/tutorial_09_plotext.ipynb | 6 +-- .../tutorial_10_matplotlib_nxm_spacing.ipynb | 3 +- 13 files changed, 106 insertions(+), 144 deletions(-) diff --git a/examples/plotly_backend_parity.py b/examples/plotly_backend_parity.py index 4278754..67a19a7 100644 --- a/examples/plotly_backend_parity.py +++ b/examples/plotly_backend_parity.py @@ -19,9 +19,7 @@ def main() -> None: marker="o", label="samples ± err", ) - canvas.fill_between( - x, y - 0.1, y + 0.1, color="steelblue", alpha=0.2, label="band" - ) + canvas.fill_between(x, y - 0.1, y + 0.1, color="steelblue", alpha=0.2, label="band") canvas.vlines([2, 5, 8], ymin=0, ymax=3.5, color="gray", linestyle="dashed") canvas.text(7.2, 2.8, "note", color="purple") canvas.annotate( diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index 9579836..8704ea1 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -691,11 +691,15 @@ def plotly_color(value): kwargs = line["kwargs"] x = tx(line["x"]) if np.isscalar(line["y1"]): - y1 = np.full_like(np.asarray(x, dtype=float), float(tys(line["y1"]))) + y1 = np.full_like( + np.asarray(x, dtype=float), float(tys(line["y1"])) + ) else: y1 = ty(line["y1"]) if np.isscalar(line["y2"]): - y2 = np.full_like(np.asarray(x, dtype=float), float(tys(line["y2"]))) + y2 = np.full_like( + np.asarray(x, dtype=float), float(tys(line["y2"])) + ) else: y2 = ty(line["y2"]) @@ -732,7 +736,9 @@ def plotly_color(value): showlegend=bool(kwargs.get("label")) and bool(self._legend), line=dict( color=plotly_color(kwargs.get("color", None)), - dash=linestyle_map.get(kwargs.get("linestyle", "solid"), "solid"), + dash=linestyle_map.get( + kwargs.get("linestyle", "solid"), "solid" + ), width=kwargs.get("linewidth", None), ), marker=( @@ -867,7 +873,9 @@ def plotly_color(value): if last_heatmap_idx is not None: label = line.get("label", "") or line["kwargs"].get("label", "") if label: - traces[last_heatmap_idx].update(colorbar=dict(title=dict(text=label))) + traces[last_heatmap_idx].update( + colorbar=dict(title=dict(text=label)) + ) elif plot_type == "patch": kwargs = line["kwargs"] patch = line["patch"] @@ -879,22 +887,28 @@ def plotly_color(value): def _patch_line_color(): return plotly_color( kwargs.get( - "edgecolor", - kwargs.get( - "color", - patch.get_edgecolor() - if hasattr(patch, "get_edgecolor") - else "black", - ), - ) + "edgecolor", + kwargs.get( + "color", + ( + patch.get_edgecolor() + if hasattr(patch, "get_edgecolor") + else "black" + ), + ), + ) ) def _patch_fill_color(): return plotly_color( kwargs.get( - "facecolor", - patch.get_facecolor() if hasattr(patch, "get_facecolor") else None, - ) + "facecolor", + ( + patch.get_facecolor() + if hasattr(patch, "get_facecolor") + else None + ), + ) ) patch_label = kwargs.get("label") diff --git a/src/maxplotlib/tests/test_plotly_backend.py b/src/maxplotlib/tests/test_plotly_backend.py index 43c607d..2184972 100644 --- a/src/maxplotlib/tests/test_plotly_backend.py +++ b/src/maxplotlib/tests/test_plotly_backend.py @@ -23,7 +23,9 @@ def test_plotly_backend_supports_common_primitives(): assert fig is not None assert len(fig.data) >= 4 # line, scatter, errorbar, fill_between assert len(getattr(fig.layout, "shapes", []) or []) >= 2 # axhline + axvline - assert len(getattr(fig.layout, "annotations", []) or []) >= 2 # subplot title + text/annotate + assert ( + len(getattr(fig.layout, "annotations", []) or []) >= 2 + ) # subplot title + text/annotate def test_plotly_backend_respects_layers(): @@ -48,9 +50,13 @@ def test_plotly_backend_supports_common_patches_and_symlog(): canvas, ax = Canvas.subplots() ax.add_patch( - mpatches.Rectangle((0.2, 0.2), 1.3, 0.7, fill=False, edgecolor="yellow", label="r") + mpatches.Rectangle( + (0.2, 0.2), 1.3, 0.7, fill=False, edgecolor="yellow", label="r" + ) + ) + ax.add_patch( + mpatches.Circle((2.2, 1.6), 0.45, fill=False, edgecolor="cyan", label="c") ) - ax.add_patch(mpatches.Circle((2.2, 1.6), 0.45, fill=False, edgecolor="cyan", label="c")) ax.add_patch( mpatches.Polygon( [[3.0, 0.5], [3.8, 1.2], [3.4, 2.0]], diff --git a/tutorials/tutorial_01.ipynb b/tutorials/tutorial_01.ipynb index ec216f7..964b647 100644 --- a/tutorials/tutorial_01.ipynb +++ b/tutorials/tutorial_01.ipynb @@ -55,8 +55,7 @@ "outputs": [], "source": [ "# Change to \"plotly\" for interactive output\n", - "BACKEND = \"matplotlib\"\n", - "\n" + "BACKEND = \"matplotlib\"" ] }, { @@ -82,8 +81,7 @@ "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, y)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -119,8 +117,7 @@ "ax.set_title(\"Sine and Cosine\")\n", "ax.set_legend(True)\n", "\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -152,8 +149,7 @@ "canvas.set_legend(True)\n", "canvas.set_grid(True)\n", "\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -186,8 +182,7 @@ "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", "\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { diff --git a/tutorials/tutorial_02.ipynb b/tutorials/tutorial_02.ipynb index 987ff05..06f79ae 100644 --- a/tutorials/tutorial_02.ipynb +++ b/tutorials/tutorial_02.ipynb @@ -59,8 +59,7 @@ "outputs": [], "source": [ "# Change to \"plotly\" for interactive output\n", - "BACKEND = \"matplotlib\"\n", - "\n" + "BACKEND = \"matplotlib\"" ] }, { @@ -167,8 +166,7 @@ "axes[1][1].set_title(\"cos(2x)\")\n", "\n", "canvas.suptitle(\"2 × 2 Layout\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -199,8 +197,7 @@ " axes[0][col].plot(x, d, color=\"steelblue\")\n", " axes[0][col].set_title(t)\n", "\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -237,8 +234,7 @@ "ax11.plot(x, np.cos(x), label=\"cos\", color=\"tomato\")\n", "\n", "canvas.suptitle(\"Manual Layout\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -271,8 +267,7 @@ "canvas[1, 0].set_title(\"Method C\")\n", "canvas[1, 1].set_title(\"Method D (index)\")\n", "\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -302,8 +297,7 @@ " sp.set_grid(True)\n", " sp.set_xlabel(\"x\")\n", "\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -333,8 +327,7 @@ "canvas.set_legend(True, row=0, col=0)\n", "canvas.set_legend(True, row=0, col=1)\n", "canvas.suptitle(\"Canvas-level routing\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { diff --git a/tutorials/tutorial_03.ipynb b/tutorials/tutorial_03.ipynb index fb60404..c3b8e12 100644 --- a/tutorials/tutorial_03.ipynb +++ b/tutorials/tutorial_03.ipynb @@ -56,8 +56,7 @@ "outputs": [], "source": [ "# Change to \"plotly\" for interactive output\n", - "BACKEND = \"matplotlib\"\n", - "\n" + "BACKEND = \"matplotlib\"" ] }, { @@ -85,8 +84,7 @@ "ax.set_title(\"Line Plot\")\n", "ax.set_legend(True)\n", "ax.set_grid(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -117,8 +115,7 @@ "ax.set_ylabel(\"y\")\n", "ax.set_title(\"Scatter Plot — coloured by distance\")\n", "ax.set_aspect(\"equal\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -145,8 +142,7 @@ "ax.set_ylabel(\"Sales (units)\")\n", "ax.set_title(\"Bar Chart\")\n", "ax.set_legend(True)\n", - "# canvas.show(backend=BACKEND) # TODO: Fix this error\n", - "\n" + "# canvas.show(backend=BACKEND) # TODO: Fix this error" ] }, { @@ -178,8 +174,7 @@ "ax.set_ylabel(\"value\")\n", "ax.set_title(\"Fill Between — Confidence Band\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -209,8 +204,7 @@ "ax.set_title(\"Error Bars\")\n", "ax.set_legend(True)\n", "ax.set_grid(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -241,8 +235,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"axhline / axvline\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -288,8 +281,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"hlines / vlines\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -333,8 +325,7 @@ "ax.set_title(\"Combined: line + fill_between + scatter\")\n", "ax.set_legend(True)\n", "ax.set_grid(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -369,8 +360,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"sin(x)\")\n", "ax.set_title(\"Annotate and Text\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { diff --git a/tutorials/tutorial_04.ipynb b/tutorials/tutorial_04.ipynb index 922c035..e7f3808 100644 --- a/tutorials/tutorial_04.ipynb +++ b/tutorials/tutorial_04.ipynb @@ -45,8 +45,7 @@ "outputs": [], "source": [ "# Change to \"plotly\" for interactive output\n", - "BACKEND = \"matplotlib\"\n", - "\n" + "BACKEND = \"matplotlib\"" ] }, { @@ -78,8 +77,7 @@ "ax.set_ylabel(\"y\")\n", "ax.set_title(\"Color options\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -117,8 +115,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Linestyle comparison\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -156,8 +153,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Marker comparison\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -190,8 +186,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Linewidth and markersize\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -222,8 +217,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_title(\"Alpha transparency\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -271,8 +265,7 @@ "ax.set_ylabel(\"y\")\n", "ax.set_title(\"Publication-style plot\")\n", "ax.set_legend(True)\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { diff --git a/tutorials/tutorial_05.ipynb b/tutorials/tutorial_05.ipynb index 517ad9a..6c16f4e 100644 --- a/tutorials/tutorial_05.ipynb +++ b/tutorials/tutorial_05.ipynb @@ -45,8 +45,7 @@ "outputs": [], "source": [ "# Change to \"plotly\" for interactive output\n", - "BACKEND = \"matplotlib\"\n", - "\n" + "BACKEND = \"matplotlib\"" ] }, { @@ -74,8 +73,7 @@ "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.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -105,8 +103,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(r\"$\\sin(x)$\")\n", "ax.set_title(\"Axis limits: first period only\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -148,8 +145,7 @@ "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.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -185,8 +181,7 @@ "ax2.set_title(\"Log scale\")\n", "\n", "canvas.suptitle(\"Linear vs log scale\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -217,8 +212,7 @@ "ax2.set_title(\"With grid\")\n", "ax2.set_xlabel(\"x\")\n", "\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -248,8 +242,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_legend(True)\n", "ax.set_title(\"Legend demo\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -288,8 +281,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(r\"$\\sin(x)$\")\n", "ax.set_title(\"annotate demo\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -321,8 +313,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(r\"$f(x)$\")\n", "ax.set_title(\"text demo\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { @@ -350,8 +341,7 @@ "ax.set_title(\"Circle with equal aspect ratio\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"y\")\n", - "canvas.show(backend=BACKEND)\n", - "\n" + "canvas.show(backend=BACKEND)" ] }, { diff --git a/tutorials/tutorial_06.ipynb b/tutorials/tutorial_06.ipynb index 45510ab..6902623 100644 --- a/tutorials/tutorial_06.ipynb +++ b/tutorials/tutorial_06.ipynb @@ -45,8 +45,7 @@ "outputs": [], "source": [ "# Change to \"plotly\" for interactive output\n", - "BACKEND = \"matplotlib\"\n", - "\n" + "BACKEND = \"matplotlib\"" ] }, { @@ -84,8 +83,7 @@ "ax.set_xlabel(\"x\")\n", "ax.set_legend(True)\n", "ax.set_title(\"Three curves on three layers\")\n", - "canvas.show(backend=BACKEND) # renders all layers by default\n", - "\n" + "canvas.show(backend=BACKEND) # renders all layers by default" ] }, { @@ -126,8 +124,7 @@ "print(\"--- Layer 0 only ---\")\n", "ax.set_title(\"Layer 0 only\")\n", "\n", - "canvas.show(backend=BACKEND, layers=[0])\n", - "\n" + "canvas.show(backend=BACKEND, layers=[0])" ] }, { @@ -140,8 +137,7 @@ "# Same canvas — now show layers 0 and 1 together\n", "ax.set_title(\"Layers 0 and 1\")\n", "\n", - "canvas.show(backend=BACKEND, layers=[0, 1])\n", - "\n" + "canvas.show(backend=BACKEND, layers=[0, 1])" ] }, { @@ -154,8 +150,7 @@ "# All layers\n", "ax.set_title(\"All layers\")\n", "\n", - "canvas.show(backend=BACKEND, layers=[0, 1, 2])\n", - "\n" + "canvas.show(backend=BACKEND, layers=[0, 1, 2])" ] }, { @@ -234,8 +229,7 @@ "\n", "# Step 1: only the data cloud\n", "ax.set_title(\"Step 1 – raw data\")\n", - "canvas.show(backend=BACKEND, layers=[0])\n", - "\n" + "canvas.show(backend=BACKEND, layers=[0])" ] }, { @@ -247,8 +241,7 @@ "source": [ "# Step 2: add the true curve\n", "ax.set_title(\"Step 2 – add true function\")\n", - "canvas.show(backend=BACKEND, layers=[0, 1])\n", - "\n" + "canvas.show(backend=BACKEND, layers=[0, 1])" ] }, { @@ -260,8 +253,7 @@ "source": [ "# Step 3: add the uncertainty envelope\n", "ax.set_title(\"Step 3 – add uncertainty envelope\")\n", - "canvas.show(backend=BACKEND, layers=[0, 1, 2])\n", - "\n" + "canvas.show(backend=BACKEND, layers=[0, 1, 2])" ] }, { diff --git a/tutorials/tutorial_07_tikz.ipynb b/tutorials/tutorial_07_tikz.ipynb index 4b2636b..3892904 100644 --- a/tutorials/tutorial_07_tikz.ipynb +++ b/tutorials/tutorial_07_tikz.ipynb @@ -97,8 +97,7 @@ "metadata": {}, "outputs": [], "source": [ - "canvas.show(backend=\"plotly\")\n", - "\n" + "canvas.show(backend=\"plotly\")" ] }, { diff --git a/tutorials/tutorial_08_plotly.ipynb b/tutorials/tutorial_08_plotly.ipynb index b21fada..2ecf2ef 100644 --- a/tutorials/tutorial_08_plotly.ipynb +++ b/tutorials/tutorial_08_plotly.ipynb @@ -67,8 +67,7 @@ "canvas.set_legend(True)\n", "\n", "fig = canvas.show(backend=\"plotly\")\n", - "fig\n", - "\n" + "fig" ] }, { @@ -97,8 +96,7 @@ "canvas.set_title(\"Basic line plot\")\n", "\n", "fig = canvas.show(backend=\"plotly\")\n", - "fig\n", - "\n" + "fig" ] }, { @@ -138,8 +136,7 @@ "canvas.set_legend(True)\n", "\n", "fig = canvas.show(backend=\"plotly\")\n", - "fig\n", - "\n" + "fig" ] }, { @@ -165,7 +162,9 @@ "y_data = 0.5 * x_data + rng.normal(0, 1, n)\n", "\n", "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", - "canvas.scatter(x_data, y_data, color=\"steelblue\", marker=\"o\", s=20, label=\"observations\")\n", + "canvas.scatter(\n", + " x_data, y_data, color=\"steelblue\", marker=\"o\", s=20, label=\"observations\"\n", + ")\n", "canvas.add_line(\n", " [0, 10],\n", " [0, 5],\n", @@ -181,8 +180,7 @@ "canvas.set_legend(True)\n", "\n", "fig = canvas.show(backend=\"plotly\")\n", - "fig\n", - "\n" + "fig" ] }, { @@ -216,8 +214,7 @@ "canvas.set_legend(True)\n", "\n", "fig = canvas.show(backend=\"plotly\")\n", - "fig\n", - "\n" + "fig" ] }, { @@ -242,7 +239,9 @@ "cumulative = np.cumsum(rainfall)\n", "\n", "canvas = Canvas(width=\"10cm\", ratio=0.6)\n", - "canvas.bar(months, rainfall, color=\"steelblue\", alpha=0.7, label=\"monthly rainfall (mm)\")\n", + "canvas.bar(\n", + " months, rainfall, color=\"steelblue\", alpha=0.7, label=\"monthly rainfall (mm)\"\n", + ")\n", "canvas.add_line(\n", " months,\n", " cumulative / 10,\n", @@ -258,8 +257,7 @@ "canvas.set_legend(True)\n", "\n", "fig = canvas.show(backend=\"plotly\")\n", - "fig\n", - "\n" + "fig" ] }, { @@ -282,7 +280,7 @@ "x = np.linspace(0, 2 * np.pi, 200)\n", "rng = np.random.default_rng(0)\n", "\n", - "canvas = Canvas(ncols=1) #, width=\"14cm\", ratio=0.35)\n", + "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", @@ -312,8 +310,7 @@ "\n", "canvas.suptitle(\"Multi-panel Plotly figure\")\n", "\n", - "fig = canvas.show(backend=\"plotly\")\n", - "\n" + "fig = canvas.show(backend=\"plotly\")" ] }, { @@ -346,8 +343,7 @@ "canvas.set_yscale(\"log\")\n", "canvas.set_legend(True)\n", "\n", - "fig = canvas.show(backend=\"plotly\")\n", - "\n" + "fig = canvas.show(backend=\"plotly\")" ] }, { @@ -379,8 +375,7 @@ "\n", "# Writes a standalone HTML file — open it in any browser\n", "canvas.savefig(\"output.html\", backend=\"plotly\")\n", - "print(\"Saved to output.html\")\n", - "\n" + "print(\"Saved to output.html\")" ] }, { diff --git a/tutorials/tutorial_09_plotext.ipynb b/tutorials/tutorial_09_plotext.ipynb index b15db7c..e395ded 100644 --- a/tutorials/tutorial_09_plotext.ipynb +++ b/tutorials/tutorial_09_plotext.ipynb @@ -551,8 +551,7 @@ "outputs": [], "source": [ "# Interactive rendering of the same canvas\n", - "canvas.show(backend=\"plotly\")\n", - "\n" + "canvas.show(backend=\"plotly\")" ] }, { @@ -637,8 +636,7 @@ "metadata": {}, "outputs": [], "source": [ - "canvas.show(backend=\"plotly\")\n", - "\n" + "canvas.show(backend=\"plotly\")" ] }, { diff --git a/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb b/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb index c92696d..b5bebab 100644 --- a/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb +++ b/tutorials/tutorial_10_matplotlib_nxm_spacing.ipynb @@ -182,8 +182,7 @@ "outputs": [], "source": [ "# Reuse the last canvas created above\n", - "loose_canvas.show(backend=\"plotly\")\n", - "\n" + "loose_canvas.show(backend=\"plotly\")" ] } ],