From 5c47205f7c7a906bd7b8d6d9888f76cb659e79dd Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 27 Jan 2026 09:13:32 +0100 Subject: [PATCH 1/7] Improved verbose prints --- src/maxplotlib/canvas/canvas.py | 6 +++--- src/maxplotlib/subfigure/line_plot.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index a64520e..ad9f1c4 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -304,7 +304,7 @@ def show( elif backend == "plotly": self.plot_plotly(savefig=False) elif backend == "tikzpics": - fig = self.plot_tikzpics(savefig=False) + fig = self.plot_tikzpics(savefig=False, verbose=verbose) fig.show() else: raise ValueError("Invalid backend") @@ -374,8 +374,8 @@ def plot_matplotlib( def plot_tikzpics( self, - savefig=None, - verbose=False, + savefig: str | None = None, + verbose: bool = False, ) -> TikzFigure: if len(self.subplots) > 1: raise NotImplementedError( diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index 8a50cfc..a0f6c83 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -235,6 +235,9 @@ def plot_tikzpics(self, layers=None, verbose: bool = False) -> TikzFigure: nodes = [[xi, yi] for xi, yi in zip(x, y)] tikz_figure.draw(nodes=nodes, **line["kwargs"]) + if verbose: + print("Generated TikZ figure:") + print(tikz_figure.generate_tikz()) return tikz_figure def plot_plotly(self): From ce0ec07650ef7b763d22ceec294e4b3141d27982 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 2 Feb 2026 10:14:18 +0100 Subject: [PATCH 2/7] Removed unused args --- src/maxplotlib/subfigure/line_plot.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index a0f6c83..f314c3d 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -106,7 +106,6 @@ def add_line( x_data, y_data, layer=0, - plot_type="plot", **kwargs, ): """ @@ -122,34 +121,34 @@ def add_line( "x": np.array(x_data), "y": np.array(y_data), "layer": layer, - "plot_type": plot_type, + "plot_type": "plot", "kwargs": kwargs, } self._add(ld, layer) - def add_imshow(self, data, layer=0, plot_type="imshow", **kwargs): + def add_imshow(self, data, layer=0, **kwargs): ld = { "data": np.array(data), "layer": layer, - "plot_type": plot_type, + "plot_type": "imshow", "kwargs": kwargs, } self._add(ld, layer) - def add_patch(self, patch, layer=0, plot_type="patch", **kwargs): + def add_patch(self, patch, layer=0, **kwargs): ld = { "patch": patch, "layer": layer, - "plot_type": plot_type, + "plot_type": "patch", "kwargs": kwargs, } self._add(ld, layer) - def add_colorbar(self, label="", layer=0, plot_type="colorbar", **kwargs): + def add_colorbar(self, label="", layer=0, **kwargs): cb = { "label": label, "layer": layer, - "plot_type": plot_type, + "plot_type": "colorbar", "kwargs": kwargs, } self._add(cb, layer) From 3406673c74ca25d6dc0ef86de26944754d0592d6 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 2 Feb 2026 10:32:25 +0100 Subject: [PATCH 3/7] Set _matplotlib_fig and _matplotlib_axes to None at initializatioN --- src/maxplotlib/canvas/canvas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index ad9f1c4..6d0a866 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -62,6 +62,8 @@ def __init__( self._ratio = ratio self._gridspec_kw = gridspec_kw self._plotted = False + self._matplotlib_fig = None + self._matplotlib_axes = None # Dictionary to store lines for each subplot # Key: (row, col), Value: list of lines with their data and kwargs From 21f503418175f7b01c4e0a636b80c8a764df963d Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 2 Feb 2026 15:36:59 +0100 Subject: [PATCH 4/7] ssort --- src/maxplotlib/canvas/canvas.py | 280 +++++++++++++------------- src/maxplotlib/colors/colors.py | 21 +- src/maxplotlib/linestyle/linestyle.py | 21 +- 3 files changed, 162 insertions(+), 160 deletions(-) diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 6d0a866..255a47b 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -18,6 +18,141 @@ from maxplotlib.utils.options import Backends +def plot_matplotlib(tikzfigure: TikzFigure, ax, layers=None): + """ + Plot all nodes and paths on the provided axis using Matplotlib. + + Parameters: + - ax (matplotlib.axes.Axes): Axis on which to plot the figure. + """ + + # TODO: Specify which layers to retreive nodes from with layers=layers + nodes = tikzfigure.layers.get_nodes() + paths = tikzfigure.layers.get_paths() + + for path in paths: + x_coords = [node.x for node in path.nodes] + y_coords = [node.y for node in path.nodes] + + # Parse path color + path_color_spec = path.kwargs.get("color", "black") + try: + color = Color(path_color_spec).to_rgb() + except ValueError as e: + print(e) + color = "black" + + # Parse line width + line_width_spec = path.kwargs.get("line_width", 1) + if isinstance(line_width_spec, str): + match = re.match(r"([\d.]+)(pt)?", line_width_spec) + if match: + line_width = float(match.group(1)) + else: + print( + f"Invalid line width specification: '{line_width_spec}', defaulting to 1", + ) + line_width = 1 + else: + line_width = float(line_width_spec) + + # Parse line style using Linestyle class + style_spec = path.kwargs.get("style", "solid") + linestyle = Linestyle(style_spec).to_matplotlib() + + ax.plot( + x_coords, + y_coords, + color=color, + linewidth=line_width, + linestyle=linestyle, + zorder=1, # Lower z-order to place behind nodes + ) + + # Plot nodes after paths so they appear on top + for node in nodes: + # Determine shape and size + shape = node.kwargs.get("shape", "circle") + fill_color_spec = node.kwargs.get("fill", "white") + edge_color_spec = node.kwargs.get("draw", "black") + linewidth = float(node.kwargs.get("line_width", 1)) + size = float(node.kwargs.get("size", 1)) + + # Parse colors using the Color class + try: + facecolor = Color(fill_color_spec).to_rgb() + except ValueError as e: + print(e) + facecolor = "white" + + try: + edgecolor = Color(edge_color_spec).to_rgb() + except ValueError as e: + print(e) + edgecolor = "black" + + # Plot shapes + if shape == "circle": + radius = size / 2 + circle = patches.Circle( + (node.x, node.y), + radius, + facecolor=facecolor, + edgecolor=edgecolor, + linewidth=linewidth, + zorder=2, # Higher z-order to place on top of paths + ) + ax.add_patch(circle) + elif shape == "rectangle": + width = height = size + rect = patches.Rectangle( + (node.x - width / 2, node.y - height / 2), + width, + height, + facecolor=facecolor, + edgecolor=edgecolor, + linewidth=linewidth, + zorder=2, # Higher z-order + ) + ax.add_patch(rect) + else: + # Default to circle if shape is unknown + radius = size / 2 + circle = patches.Circle( + (node.x, node.y), + radius, + facecolor=facecolor, + edgecolor=edgecolor, + linewidth=linewidth, + zorder=2, + ) + ax.add_patch(circle) + + # Add text inside the shape + if node.content: + ax.text( + node.x, + node.y, + node.content, + fontsize=10, + ha="center", + va="center", + wrap=True, + zorder=3, # Even higher z-order for text + ) + + # Remove axes, ticks, and legend + ax.axis("off") + + # Adjust plot limits + all_x = [node.x for node in nodes] + all_y = [node.y for node in nodes] + padding = 1 # Adjust padding as needed + ax.set_xlim(min(all_x) - padding, max(all_x) + padding) + ax.set_ylim(min(all_y) - padding, max(all_y) + padding) + ax.set_aspect("equal", adjustable="datalim") + + class Canvas: def __init__( self, @@ -509,13 +644,6 @@ def label(self, value): def figsize(self, value): self._figsize = value - # Magic methods - def __str__(self): - return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, figsize={self.figsize})" - - def __repr__(self): - return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, caption={self.caption}, label={self.label})" - def __getitem__(self, key): """Allows accessing subplots by tuple index.""" row, col = key @@ -530,140 +658,12 @@ def __setitem__(self, key, value): raise IndexError("Subplot index out of range") self._subplot_matrix[row][col] = value + def __repr__(self): + return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, caption={self.caption}, label={self.label})" -def plot_matplotlib(tikzfigure: TikzFigure, ax, layers=None): - """ - Plot all nodes and paths on the provided axis using Matplotlib. - - Parameters: - - ax (matplotlib.axes.Axes): Axis on which to plot the figure. - """ - - # TODO: Specify which layers to retreive nodes from with layers=layers - nodes = tikzfigure.layers.get_nodes() - paths = tikzfigure.layers.get_paths() - - for path in paths: - x_coords = [node.x for node in path.nodes] - y_coords = [node.y for node in path.nodes] - - # Parse path color - path_color_spec = path.kwargs.get("color", "black") - try: - color = Color(path_color_spec).to_rgb() - except ValueError as e: - print(e) - color = "black" - - # Parse line width - line_width_spec = path.kwargs.get("line_width", 1) - if isinstance(line_width_spec, str): - match = re.match(r"([\d.]+)(pt)?", line_width_spec) - if match: - line_width = float(match.group(1)) - else: - print( - f"Invalid line width specification: '{line_width_spec}', defaulting to 1", - ) - line_width = 1 - else: - line_width = float(line_width_spec) - - # Parse line style using Linestyle class - style_spec = path.kwargs.get("style", "solid") - linestyle = Linestyle(style_spec).to_matplotlib() - - ax.plot( - x_coords, - y_coords, - color=color, - linewidth=line_width, - linestyle=linestyle, - zorder=1, # Lower z-order to place behind nodes - ) - - # Plot nodes after paths so they appear on top - for node in nodes: - # Determine shape and size - shape = node.kwargs.get("shape", "circle") - fill_color_spec = node.kwargs.get("fill", "white") - edge_color_spec = node.kwargs.get("draw", "black") - linewidth = float(node.kwargs.get("line_width", 1)) - size = float(node.kwargs.get("size", 1)) - - # Parse colors using the Color class - try: - facecolor = Color(fill_color_spec).to_rgb() - except ValueError as e: - print(e) - facecolor = "white" - - try: - edgecolor = Color(edge_color_spec).to_rgb() - except ValueError as e: - print(e) - edgecolor = "black" - - # Plot shapes - if shape == "circle": - radius = size / 2 - circle = patches.Circle( - (node.x, node.y), - radius, - facecolor=facecolor, - edgecolor=edgecolor, - linewidth=linewidth, - zorder=2, # Higher z-order to place on top of paths - ) - ax.add_patch(circle) - elif shape == "rectangle": - width = height = size - rect = patches.Rectangle( - (node.x - width / 2, node.y - height / 2), - width, - height, - facecolor=facecolor, - edgecolor=edgecolor, - linewidth=linewidth, - zorder=2, # Higher z-order - ) - ax.add_patch(rect) - else: - # Default to circle if shape is unknown - radius = size / 2 - circle = patches.Circle( - (node.x, node.y), - radius, - facecolor=facecolor, - edgecolor=edgecolor, - linewidth=linewidth, - zorder=2, - ) - ax.add_patch(circle) - - # Add text inside the shape - if node.content: - ax.text( - node.x, - node.y, - node.content, - fontsize=10, - ha="center", - va="center", - wrap=True, - zorder=3, # Even higher z-order for text - ) - - # Remove axes, ticks, and legend - ax.axis("off") - - # Adjust plot limits - all_x = [node.x for node in nodes] - all_y = [node.y for node in nodes] - padding = 1 # Adjust padding as needed - ax.set_xlim(min(all_x) - padding, max(all_x) + padding) - ax.set_ylim(min(all_y) - padding, max(all_y) + padding) - ax.set_aspect("equal", adjustable="datalim") + # Magic methods + def __str__(self): + return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, figsize={self.figsize})" if __name__ == "__main__": diff --git a/src/maxplotlib/colors/colors.py b/src/maxplotlib/colors/colors.py index fdb117e..4d04287 100644 --- a/src/maxplotlib/colors/colors.py +++ b/src/maxplotlib/colors/colors.py @@ -5,16 +5,6 @@ class Color: - def __init__(self, color_spec): - """ - Initialize the Color object by parsing the color specification. - - Parameters: - - color_spec: Can be a TikZ color string (e.g., 'blue!20'), a standard color name, - an RGB tuple, a hex code, etc. - """ - self.color_spec = color_spec - self.rgb = self._parse_color(color_spec) def _parse_color(self, color_spec): """ @@ -53,6 +43,17 @@ def _parse_color(self, color_spec): except ValueError: raise ValueError(f"Invalid color specification: '{color_spec}'") + def __init__(self, color_spec): + """ + Initialize the Color object by parsing the color specification. + + Parameters: + - color_spec: Can be a TikZ color string (e.g., 'blue!20'), a standard color name, + an RGB tuple, a hex code, etc. + """ + self.color_spec = color_spec + self.rgb = self._parse_color(color_spec) + def to_rgb(self): """ Return the color as an RGB tuple. diff --git a/src/maxplotlib/linestyle/linestyle.py b/src/maxplotlib/linestyle/linestyle.py index 0ba1f04..27e0758 100644 --- a/src/maxplotlib/linestyle/linestyle.py +++ b/src/maxplotlib/linestyle/linestyle.py @@ -2,16 +2,6 @@ class Linestyle: - def __init__(self, style_spec): - """ - Initialize the Linestyle object by parsing the style specification. - - Parameters: - - style_spec: Can be a TikZ-style line style string (e.g., 'dashed', 'dotted', 'solid', 'dashdot'), - or a custom dash pattern. - """ - self.style_spec = style_spec - self.matplotlib_style = self._parse_style(style_spec) def _parse_style(self, style_spec): """ @@ -48,6 +38,17 @@ def _parse_style(self, style_spec): print(f"Unknown line style: '{style_spec}', defaulting to 'solid'") return "solid" + def __init__(self, style_spec): + """ + Initialize the Linestyle object by parsing the style specification. + + Parameters: + - style_spec: Can be a TikZ-style line style string (e.g., 'dashed', 'dotted', 'solid', 'dashdot'), + or a custom dash pattern. + """ + self.style_spec = style_spec + self.matplotlib_style = self._parse_style(style_spec) + def to_matplotlib(self): """ Return the line style in Matplotlib format. From c77fc257b825f82204de6c97060e27ea7350b9b5 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 2 Feb 2026 16:10:31 +0100 Subject: [PATCH 5/7] Set default width to 5cm --- src/maxplotlib/canvas/canvas.py | 4 +--- tutorials/tutorial_01.ipynb | 8 ++++---- tutorials/tutorial_02.ipynb | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 255a47b..962ab21 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -164,7 +164,7 @@ def __init__( label: str | None = None, fontsize: int = 14, dpi: int = 300, - width: str = "17cm", + width: str = "5cm", ratio: str = "golden", # TODO Add literal gridspec_kw: Dict = {"wspace": 0.08, "hspace": 0.1}, ): @@ -243,7 +243,6 @@ def add_line( subplot: LinePlot | None = None, row: int | None = None, col: int | None = None, - plot_type="plot", **kwargs, ): if row is not None and col is not None: @@ -263,7 +262,6 @@ def add_line( x_data=x_data, y_data=y_data, layer=layer, - plot_type=plot_type, **kwargs, ) diff --git a/tutorials/tutorial_01.ipynb b/tutorials/tutorial_01.ipynb index 2b110e4..5a054f8 100644 --- a/tutorials/tutorial_01.ipynb +++ b/tutorials/tutorial_01.ipynb @@ -29,7 +29,7 @@ "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=\"17mm\", ratio=0.5, fontsize=12)\n", + "c = Canvas(ratio=0.5, fontsize=12)\n", "c.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\")\n", "c.add_line([0, 1, 2, 3], [0, 2, 3, 4], linestyle=\"dashed\", color=\"red\", label=\"Line 2\")\n", "c.show()" @@ -44,7 +44,7 @@ "source": [ "# You can also explicitly create a subplot and add lines to it\n", "\n", - "c = Canvas(width=\"17cm\", ratio=0.5, fontsize=12)\n", + "c = Canvas(ratio=0.5, fontsize=12)\n", "sp = c.add_subplot(\n", " grid=True, xlabel=\"(x - 10) * 0.1\", ylabel=\"10y\", yscale=10, xshift=-10, xscale=0.1\n", ")\n", @@ -63,7 +63,7 @@ "source": [ "# Example with multiple subplots\n", "\n", - "c = Canvas(width=\"17cm\", ncols=2, nrows=2, ratio=0.5)\n", + "c = Canvas(width=\"10cm\", ncols=2, nrows=2, ratio=0.5)\n", "sp = c.add_subplot(grid=True)\n", "c.add_subplot(row=1)\n", "sp2 = c.add_subplot(row=1, legend=False)\n", @@ -82,7 +82,7 @@ "outputs": [], "source": [ "# Test with plotly backend\n", - "c = Canvas(width=\"17cm\", ratio=0.5)\n", + "c = Canvas(ratio=0.5)\n", "sp = c.add_subplot(\n", " grid=True, xlabel=\"x (mm)\", ylabel=\"10y\", yscale=10, xshift=-10, xscale=0.1\n", ")\n", diff --git a/tutorials/tutorial_02.ipynb b/tutorials/tutorial_02.ipynb index f2e3528..89ec85b 100644 --- a/tutorials/tutorial_02.ipynb +++ b/tutorials/tutorial_02.ipynb @@ -61,7 +61,7 @@ "metadata": {}, "outputs": [], "source": [ - "c = Canvas(ncols=2, width=\"20cm\", ratio=0.5)\n", + "c = Canvas(width=\"10cm\", ncols=2, ratio=0.5)\n", "tikz = c.add_tikzfigure(grid=False)\n", "\n", "# Add nodes\n", From b587c152f7c43d129f5f0ee5258e1aab95da1241 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 8 Apr 2026 22:07:28 +0200 Subject: [PATCH 6/7] Add tikzfigure to dependencies (#36) --- pyproject.toml | 2 +- src/maxplotlib/canvas/canvas.py | 16 ++-- src/maxplotlib/subfigure/line_plot.py | 4 +- src/maxplotlib/utils/options.py | 2 +- tutorials/tutorial_04.ipynb | 119 +++----------------------- tutorials/tutorial_07_tikzpics.ipynb | 2 +- 6 files changed, 27 insertions(+), 118 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index aa4ecb8..1fa3e94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "matplotlib", "pint", "plotly", - "tikzpics>=0.1.1", + "tikzfigure>=0.2.0", ] [project.optional-dependencies] test = [ diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 962ab21..11d33e8 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -5,7 +5,7 @@ import matplotlib.patches as patches import matplotlib.pyplot as plt from plotly.subplots import make_subplots -from tikzpics import TikzFigure +from tikzfigure import TikzFigure from maxplotlib.backends.matplotlib.utils import ( set_size, @@ -415,8 +415,8 @@ def plot( ) elif backend == "plotly": return self.plot_plotly(savefig=savefig) - elif backend == "tikzpics": - return self.plot_tikzpics(savefig=savefig) + elif backend == "tikzfigure": + return self.plot_tikzfigure(savefig=savefig) else: raise ValueError(f"Invalid backend: {backend}") @@ -438,8 +438,8 @@ def show( # self._matplotlib_fig.show() elif backend == "plotly": self.plot_plotly(savefig=False) - elif backend == "tikzpics": - fig = self.plot_tikzpics(savefig=False, verbose=verbose) + elif backend == "tikzfigure": + fig = self.plot_tikzfigure(savefig=False, verbose=verbose) fig.show() else: raise ValueError("Invalid backend") @@ -507,20 +507,20 @@ def plot_matplotlib( self._matplotlib_axes = axes return fig, axes - def plot_tikzpics( + def plot_tikzfigure( self, savefig: str | None = None, verbose: bool = False, ) -> TikzFigure: if len(self.subplots) > 1: raise NotImplementedError( - "Only one subplot is supported for tikzpics backend." + "Only one subplot is supported for tikzfigure backend." ) for (row, col), line_plot in self.subplots.items(): if verbose: print(f"Plotting subplot at row {row}, col {col}") print(f"{line_plot = }") - tikz_subplot = line_plot.plot_tikzpics(verbose=verbose) + tikz_subplot = line_plot.plot_tikzfigure(verbose=verbose) return tikz_subplot def plot_plotly(self, show=True, savefig=None, usetex=False): diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index f314c3d..2b9872e 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -2,7 +2,7 @@ import numpy as np import plotly.graph_objects as go from mpl_toolkits.axes_grid1 import make_axes_locatable -from tikzpics import TikzFigure +from tikzfigure import TikzFigure class Node: @@ -221,7 +221,7 @@ def plot_matplotlib( if self.ymax is not None: ax.axis(ymax=self.ymax) - def plot_tikzpics(self, layers=None, verbose: bool = False) -> TikzFigure: + def plot_tikzfigure(self, layers=None, verbose: bool = False) -> TikzFigure: tikz_figure = TikzFigure() for layer_name, layer_lines in self.layered_line_data.items(): diff --git a/src/maxplotlib/utils/options.py b/src/maxplotlib/utils/options.py index 6666e4d..074514e 100644 --- a/src/maxplotlib/utils/options.py +++ b/src/maxplotlib/utils/options.py @@ -1,3 +1,3 @@ from typing import Literal -Backends = Literal["matplotlib", "plotly", "tikzpics"] +Backends = Literal["matplotlib", "plotly", "tikzfigure"] diff --git a/tutorials/tutorial_04.ipynb b/tutorials/tutorial_04.ipynb index ef2dc8b..abde34e 100644 --- a/tutorials/tutorial_04.ipynb +++ b/tutorials/tutorial_04.ipynb @@ -10,21 +10,10 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "1", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'\\nTutorial 4.\\n\\nAdd raw tikz code to the tikz subplot.\\n'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "\"\"\"\n", "Tutorial 4.\n", @@ -35,7 +24,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "2", "metadata": {}, "outputs": [], @@ -45,7 +34,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "3", "metadata": {}, "outputs": [], @@ -56,23 +45,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "4", "metadata": { "lines_to_next_cell": 2 }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Add nodes\n", "tikz.add_node(0, 0, label=\"A\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=0)\n", @@ -83,21 +61,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "5", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Add a line between nodes\n", "tikz.draw(\n", @@ -112,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "6", "metadata": {}, "outputs": [], @@ -132,32 +99,21 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "7", "metadata": {}, "outputs": [], "source": [ - "# TODO: Not implemented in tikzpics yet\n", + "# TODO: Not implemented in tikzfigure yet\n", "# tikz.add_raw(raw_tikz)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "8", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "tikz.add_node(0.5, 0.5, content=\"Cube\", layer=10)" ] @@ -167,54 +123,7 @@ "execution_count": null, "id": "9", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "\n", - "\n", - "% --------------------------------------------- %\n", - "% Tikzfigure generated by tikzpics v0.1.1 %\n", - "% https://github.com/max-models/tikzpics %\n", - "% --------------------------------------------- %\n", - "\\begin{tikzpicture}\n", - " \n", - " % Define the layers library\n", - " \\pgfdeclarelayer{0}\n", - " \\pgfdeclarelayer{1}\n", - " \\pgfdeclarelayer{10}\n", - " \\pgfdeclarelayer{2}\n", - " \\pgfsetlayers{0,1,10,2}\n", - " \n", - " % Layer 0\n", - " \\begin{pgfonlayer}{0}\n", - " \\node[shape=circle, draw=black, fill=blue] (A) at (0, 0) {};\n", - " \\node[shape=circle, draw=black, fill=blue] (B) at (10, 0) {};\n", - " \\node[shape=circle, draw=black, fill=blue] (C) at (10, 10) {};\n", - " \\end{pgfonlayer}{0}\n", - " \n", - " % Layer 2\n", - " \\begin{pgfonlayer}{2}\n", - " \\node[shape=circle, draw=black, fill=blue] (D) at (0, 10) {};\n", - " \\end{pgfonlayer}{2}\n", - " \n", - " % Layer 1\n", - " \\begin{pgfonlayer}{1}\n", - " \\draw[path actions=['draw', 'rounded corners'], fill=red, opacity=0.5] (A) to (B) to (C) to (D) -- cycle;\n", - " \\end{pgfonlayer}{1}\n", - " \n", - " % Layer 10\n", - " \\begin{pgfonlayer}{10}\n", - " \\node (node4) at (0.5, 0.5) {Cube};\n", - " \\end{pgfonlayer}{10}\n", - "\\end{tikzpicture}\n", - "\n" - ] - } - ], + "outputs": [], "source": [ "# Generate the TikZ script\n", "script = tikz.generate_tikz()\n", diff --git a/tutorials/tutorial_07_tikzpics.ipynb b/tutorials/tutorial_07_tikzpics.ipynb index d3bc587..4a9e42b 100644 --- a/tutorials/tutorial_07_tikzpics.ipynb +++ b/tutorials/tutorial_07_tikzpics.ipynb @@ -34,7 +34,7 @@ "\n", "\n", "# TODO: Uncomment if pdflatex is installed\n", - "# c.show(backend=\"tikzpics\")" + "# c.show(backend=\"tikzfigure\")" ] } ], From 206e15deaa8a7c85da4dbe95e4e5057abc88256d Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 9 Apr 2026 11:29:55 +0200 Subject: [PATCH 7/7] Extend the API (#37) * Added new methods to the canvas API * Extended the canvas API, added subplot factory like in matplotlib * Install tikzfigure[vis] * Updated the tutorials * Set version to 0.1.4 * Formatting and add black[jupyter] to deps * Skip tikz tutorial in the CI and docs --- .github/workflows/ci_pipeline.yml | 5 +- docs/source/conf.py | 5 +- pyproject.toml | 8 +- src/maxplotlib/canvas/canvas.py | 358 ++++++++++++- src/maxplotlib/subfigure/line_plot.py | 440 ++++++++++++++-- tutorials/tutorial_01.ipynb | 197 +++++-- tutorials/tutorial_02.ipynb | 304 ++++++++--- tutorials/tutorial_03.ipynb | 349 ++++++++++++- tutorials/tutorial_04.ipynb | 245 ++++++--- tutorials/tutorial_05.ipynb | 329 +++++++++++- tutorials/tutorial_06.ipynb | 234 ++++++++- tutorials/tutorial_07_tikz.ipynb | 719 ++++++++++++++++++++++++++ tutorials/tutorial_07_tikzpics.ipynb | 62 --- tutorials/tutorial_08_plotly.ipynb | 395 ++++++++++++++ 14 files changed, 3273 insertions(+), 377 deletions(-) create mode 100644 tutorials/tutorial_07_tikz.ipynb delete mode 100644 tutorials/tutorial_07_tikzpics.ipynb create mode 100644 tutorials/tutorial_08_plotly.ipynb diff --git a/.github/workflows/ci_pipeline.yml b/.github/workflows/ci_pipeline.yml index 6c222e7..eced47c 100644 --- a/.github/workflows/ci_pipeline.yml +++ b/.github/workflows/ci_pipeline.yml @@ -37,4 +37,7 @@ jobs: - name: Test tutorials run: | - jupyter nbconvert --to notebook --execute tutorials/*.ipynb --output-dir=/tmp --ExecutePreprocessor.timeout=300 + # tutorial_07_tikz.ipynb requires pdflatex β€” skip it in CI + jupyter nbconvert --to notebook --execute \ + $(ls tutorials/*.ipynb | grep -v tutorial_07_tikz) \ + --output-dir=/tmp --ExecutePreprocessor.timeout=300 diff --git a/docs/source/conf.py b/docs/source/conf.py index 03dcf32..acbc723 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -39,7 +39,10 @@ def setup(app): ] templates_path = ["_templates"] -exclude_patterns = [] +exclude_patterns = [ + # tutorial_07_tikz.ipynb requires pdflatex to render β€” skip during docs build + "tutorials/tutorial_07_tikz.ipynb", +] # -- Options for HTML output ------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 1fa3e94..7128d81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "maxplotlibx" -version = "0.1.3" +version = "0.1.4" description = "A reproducible plotting module with various backends and export options." readme = "README.md" requires-python = ">=3.8" @@ -18,7 +18,7 @@ dependencies = [ "matplotlib", "pint", "plotly", - "tikzfigure>=0.2.0", + "tikzfigure[vis]>=0.2.0", ] [project.optional-dependencies] test = [ @@ -36,7 +36,7 @@ docs = [ dev = [ "maxplotlibx[test,docs]", "ruff", - "black", + "black[jupyter]", "isort", "jupyterlab", "nbstripout", @@ -56,4 +56,4 @@ line-length = 88 line-length = 88 [tool.isort] -profile = "black" \ No newline at end of file +profile = "black" diff --git a/src/maxplotlib/canvas/canvas.py b/src/maxplotlib/canvas/canvas.py index 11d33e8..bea3e39 100644 --- a/src/maxplotlib/canvas/canvas.py +++ b/src/maxplotlib/canvas/canvas.py @@ -199,6 +199,8 @@ def __init__( self._plotted = False self._matplotlib_fig = None self._matplotlib_axes = None + self._suptitle: str | None = None + self._suptitle_kwargs: dict = {} # Dictionary to store lines for each subplot # Key: (row, col), Value: list of lines with their data and kwargs @@ -207,14 +209,60 @@ def __init__( self._subplot_matrix = [[None] * self.ncols for _ in range(self.nrows)] + # ------------------------------------------------------------------ + # Factory + # ------------------------------------------------------------------ + + @classmethod + def subplots( + cls, + nrows: int = 1, + ncols: int = 1, + squeeze: bool = True, + **canvas_kwargs, + ): + """ + Create a Canvas pre-filled with LinePlot subplots, mirroring + ``matplotlib.pyplot.subplots()``. + + Parameters: + nrows, ncols (int): Grid dimensions. + squeeze (bool): If True, return a single subplot instead of a 1-element + list when the grid is 1Γ—1 or when one dimension is 1. + **canvas_kwargs: Forwarded to the Canvas constructor. + + Returns: + (canvas, axes): A tuple of the Canvas and either a single LinePlot, + a flat list (when one dimension is 1 and squeeze=True), or a + 2-D list of LinePlots. + + Examples: + >>> canvas, ax = Canvas.subplots() + >>> canvas, (ax1, ax2) = Canvas.subplots(ncols=2) + >>> canvas, axes = Canvas.subplots(nrows=2, ncols=2) # axes[row][col] + """ + canvas = cls(nrows=nrows, ncols=ncols, **canvas_kwargs) + axes = [ + [canvas.add_subplot(row=r, col=c) for c in range(ncols)] + for r in range(nrows) + ] + if squeeze: + if nrows == 1 and ncols == 1: + return canvas, axes[0][0] + if nrows == 1: + return canvas, axes[0] + if ncols == 1: + return canvas, [row[0] for row in axes] + return canvas, axes + @property - def subplots(self): + def _subplot_dict(self): return self._subplots @property def layers(self): layers = [] - for (row, col), subplot in self.subplots.items(): + for (row, col), subplot in self._subplot_dict.items(): layers.extend(subplot.layers) return list(set(layers)) @@ -237,8 +285,8 @@ def generate_new_rowcol(self, row, col): def add_line( self, - x_data, - y_data, + x, + y, layer=0, subplot: LinePlot | None = None, row: int | None = None, @@ -259,12 +307,258 @@ def add_line( subplot = self.add_subplot(col=col, row=row) subplot.add_line( - x_data=x_data, - y_data=y_data, + x=x, + y=y, layer=layer, **kwargs, ) + def _get_or_create_subplot(self, row, col): + """Return the subplot at (row, col), creating it if needed.""" + if row is not None and col is not None: + try: + sp = self._subplot_matrix[row][col] + except (IndexError, KeyError): + raise ValueError("Invalid subplot position.") + else: + row, col = 0, 0 + sp = self._subplot_matrix[row][col] + if sp is None: + row, col = self.generate_new_rowcol(row, col) + sp = self.add_subplot(col=col, row=row) + return sp + + def scatter( + self, + x, + y, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """ + Add a scatter plot to the canvas (matplotlib-style convenience method). + + Parameters: + x (array-like): X-axis data. + y (array-like): Y-axis data. + layer (int): Layer index (default 0). + row, col (int): Subplot position (default top-left). + **kwargs: Forwarded to the backend (e.g., color, marker, s, label). + """ + sp = self._get_or_create_subplot(row, col) + sp.scatter(x, y, layer=layer, **kwargs) + + def bar( + self, + x, + height, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """ + Add a bar chart to the canvas (matplotlib-style convenience method). + + Parameters: + x (array-like): X positions of the bars. + height (array-like): Heights of the bars. + layer (int): Layer index (default 0). + row, col (int): Subplot position (default top-left). + **kwargs: Forwarded to the backend (e.g., color, width, label). + """ + sp = self._get_or_create_subplot(row, col) + sp.bar(x, height, layer=layer, **kwargs) + + def set_xlabel(self, label: str, row: int | None = None, col: int | None = None): + """Set the x-axis label for a subplot (default top-left).""" + self._get_or_create_subplot(row, col).set_xlabel(label) + + def set_ylabel(self, label: str, row: int | None = None, col: int | None = None): + """Set the y-axis label for a subplot (default top-left).""" + self._get_or_create_subplot(row, col).set_ylabel(label) + + def set_title(self, title: str, row: int | None = None, col: int | None = None): + """Set the title for a subplot (default top-left).""" + self._get_or_create_subplot(row, col).set_title(title) + + def set_xlim( + self, left=None, right=None, row: int | None = None, col: int | None = None + ): + """Set the x-axis limits for a subplot (default top-left).""" + self._get_or_create_subplot(row, col).set_xlim(left, right) + + def set_ylim( + self, bottom=None, top=None, row: int | None = None, col: int | None = None + ): + """Set the y-axis limits for a subplot (default top-left).""" + self._get_or_create_subplot(row, col).set_ylim(bottom, top) + + def set_grid( + self, visible: bool = True, row: int | None = None, col: int | None = None + ): + """Show or hide the grid for a subplot (default top-left).""" + self._get_or_create_subplot(row, col).set_grid(visible) + + def set_legend( + self, visible: bool = True, row: int | None = None, col: int | None = None + ): + """Show or hide the legend for a subplot (default top-left).""" + self._get_or_create_subplot(row, col).set_legend(visible) + + def set_xscale(self, scale: str, row: int | None = None, col: int | None = None): + """Set x-axis scale ('linear', 'log', 'symlog') for a subplot.""" + self._get_or_create_subplot(row, col).set_xscale(scale) + + def set_yscale(self, scale: str, row: int | None = None, col: int | None = None): + """Set y-axis scale ('linear', 'log', 'symlog') for a subplot.""" + self._get_or_create_subplot(row, col).set_yscale(scale) + + def set_xticks( + self, ticks, labels=None, row: int | None = None, col: int | None = None + ): + """Set x-axis tick positions (and optional labels) for a subplot.""" + self._get_or_create_subplot(row, col).set_xticks(ticks, labels) + + def set_yticks( + self, ticks, labels=None, row: int | None = None, col: int | None = None + ): + """Set y-axis tick positions (and optional labels) for a subplot.""" + self._get_or_create_subplot(row, col).set_yticks(ticks, labels) + + def fill_between( + self, + x, + y1, + y2=0, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Fill the region between two curves on a subplot.""" + self._get_or_create_subplot(row, col).fill_between( + x, y1, y2, layer=layer, **kwargs + ) + + def errorbar( + self, + x, + y, + yerr=None, + xerr=None, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add an error-bar line to a subplot.""" + self._get_or_create_subplot(row, col).errorbar( + x, y, yerr=yerr, xerr=xerr, layer=layer, **kwargs + ) + + def axhline( + self, y=0, layer=0, row: int | None = None, col: int | None = None, **kwargs + ): + """Add a full-width horizontal reference line to a subplot.""" + self._get_or_create_subplot(row, col).axhline(y=y, layer=layer, **kwargs) + + def axvline( + self, x=0, layer=0, row: int | None = None, col: int | None = None, **kwargs + ): + """Add a full-height vertical reference line to a subplot.""" + self._get_or_create_subplot(row, col).axvline(x=x, layer=layer, **kwargs) + + def hlines( + self, + y, + xmin, + xmax, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add horizontal lines at specified y positions to a subplot.""" + self._get_or_create_subplot(row, col).hlines( + y, xmin, xmax, layer=layer, **kwargs + ) + + def vlines( + self, + x, + ymin, + ymax, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add vertical lines at specified x positions to a subplot.""" + self._get_or_create_subplot(row, col).vlines( + x, ymin, ymax, layer=layer, **kwargs + ) + + def annotate( + self, + text, + xy, + xytext=None, + layer=0, + row: int | None = None, + col: int | None = None, + **kwargs, + ): + """Add an annotation (with optional arrow) to a subplot.""" + self._get_or_create_subplot(row, col).annotate( + text, xy, xytext=xytext, layer=layer, **kwargs + ) + + def text( + self, x, y, s, layer=0, row: int | None = None, col: int | None = None, **kwargs + ): + """Add a text label at (x, y) on a subplot.""" + self._get_or_create_subplot(row, col).text(x, y, s, layer=layer, **kwargs) + + # ------------------------------------------------------------------ + # Multi-subplot helpers + # ------------------------------------------------------------------ + + def subplot(self, row: int = 0, col: int = 0) -> LinePlot: + """ + Return the LinePlot at position (row, col). + + Raises ValueError if no subplot has been created there yet. + """ + sp = self._subplot_matrix[row][col] + if sp is None: + raise ValueError( + f"No subplot at ({row}, {col}). " + "Call add_subplot() or use Canvas.subplots() first." + ) + return sp + + def iter_subplots(self): + """Yield (row, col, subplot) for every initialized subplot, row-major.""" + for r in range(self.nrows): + for c in range(self.ncols): + sp = self._subplot_matrix[r][c] + if sp is not None: + yield r, c, sp + + def suptitle(self, title: str, **kwargs): + """ + Set a figure-level title (rendered above all subplots). + + Parameters: + title (str): Title text. + **kwargs: Forwarded to matplotlib's fig.suptitle (e.g., fontsize, y). + """ + self._suptitle = title + self._suptitle_kwargs = kwargs + def add_tikzfigure( self, col=None, @@ -423,6 +717,7 @@ def plot( def show( self, backend: Backends = "matplotlib", + layers: list | None = None, verbose: bool = False, ): if verbose: @@ -432,7 +727,7 @@ def show( self.plot( backend="matplotlib", savefig=False, - layers=None, + layers=layers, verbose=verbose, ) # self._matplotlib_fig.show() @@ -492,7 +787,7 @@ def plot_matplotlib( dpi=self.dpi, ) - for (row, col), subplot in self.subplots.items(): + for (row, col), subplot in self._subplot_dict.items(): ax = axes[row][col] if isinstance(subplot, TikzFigure): plot_matplotlib(subplot, ax, layers=layers) @@ -501,6 +796,9 @@ def plot_matplotlib( # ax.set_title(f"Subplot ({row}, {col})") ax.grid() + if self._suptitle: + fig.suptitle(self._suptitle, **self._suptitle_kwargs) + # Set caption, labels, etc., if needed self._plotted = True self._matplotlib_fig = fig @@ -512,11 +810,11 @@ def plot_tikzfigure( savefig: str | None = None, verbose: bool = False, ) -> TikzFigure: - if len(self.subplots) > 1: + if len(self._subplot_dict) > 1: raise NotImplementedError( "Only one subplot is supported for tikzfigure backend." ) - for (row, col), line_plot in self.subplots.items(): + for (row, col), line_plot in self._subplot_dict.items(): if verbose: print(f"Plotting subplot at row {row}, col {col}") print(f"{line_plot = }") @@ -551,31 +849,45 @@ def plot_plotly(self, show=True, savefig=None, usetex=False): rows=self.nrows, cols=self.ncols, subplot_titles=[ - f"Subplot ({row}, {col})" for (row, col) in self.subplots.keys() + sp._title or f"({row}, {col})" + for (row, col), sp in self._subplot_dict.items() ], ) - # Plot each subplot - for (row, col), line_plot in self.subplots.items(): - traces = line_plot.plot_plotly() # Generate Plotly traces for the line_plot + # 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() 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}} + if line_plot._xaxis_scale == "log": + layout_patch.setdefault(xkey, {})["type"] = "log" + if line_plot._yaxis_scale == "log": + layout_patch.setdefault(ykey, {})["type"] = "log" + if layout_patch: + fig.update_layout(**layout_patch) + axis_index += 1 + # Update layout settings fig.update_layout( - # width=fig_width, - # height=fig_height, font=dict(size=self.fontsize), - margin=dict(l=10, r=10, t=40, b=10), # Adjust margins if needed + margin=dict(l=10, r=10, t=40, b=10), ) + if self._suptitle: + fig.update_layout(title=dict(text=self._suptitle, x=0.5)) - # Optionally save the figure if savefig: fig.write_image(savefig) - # Show or return the figure - # if show: - # fig.show() return fig # Property getters @@ -667,6 +979,6 @@ def __str__(self): if __name__ == "__main__": c = Canvas(ncols=2, nrows=2) sp = c.add_subplot() - sp.add_line("Line 1", [0, 1, 2, 3], [0, 1, 4, 9]) - c.plot() + sp.plot([0, 1, 2, 3], [0, 1, 4, 9], label="Line 1") + c.plot(backend="matplotlib") print("done") diff --git a/src/maxplotlib/subfigure/line_plot.py b/src/maxplotlib/subfigure/line_plot.py index 2b9872e..923bd67 100644 --- a/src/maxplotlib/subfigure/line_plot.py +++ b/src/maxplotlib/subfigure/line_plot.py @@ -80,6 +80,19 @@ def __init__( self._xshift = xshift self._yshift = yshift + # Axis scale type ('linear', 'log', 'symlog') + self._xaxis_scale: str | None = None + self._yaxis_scale: str | None = None + + # Custom tick positions and labels + self._xticks: list | None = None + self._xticklabels: list | None = None + self._yticks: list | None = None + self._yticklabels: list | None = None + + # Aspect ratio + self._aspect = None + # List to store line data, each entry contains x and y data, label, and plot kwargs self.line_data = [] self.layered_line_data = {} @@ -103,29 +116,283 @@ def _add(self, obj, layer): def add_line( self, - x_data, - y_data, + x, + y, layer=0, **kwargs, ): """ - Add a line to the plot. + Add a line to the subplot. Parameters: - label (str): Label for the line. - x_data (list): X-axis data. - y_data (list): Y-axis data. - **kwargs: Additional keyword arguments for the plot (e.g., color, linestyle). + x (array-like): X-axis data. + y (array-like): Y-axis data. + layer (int): Layer index (default 0). + **kwargs: Additional keyword arguments forwarded to the backend + (e.g., color, linestyle, label, linewidth). """ ld = { - "x": np.array(x_data), - "y": np.array(y_data), + "x": np.array(x), + "y": np.array(y), "layer": layer, "plot_type": "plot", "kwargs": kwargs, } self._add(ld, layer) + def plot(self, x, y, layer=0, **kwargs): + """Matplotlib-style alias for :meth:`add_line`.""" + self.add_line(x, y, layer=layer, **kwargs) + + def scatter(self, x, y, layer=0, **kwargs): + """ + Add a scatter plot to the subplot. + + Parameters: + x (array-like): X-axis data. + y (array-like): Y-axis data. + layer (int): Layer index (default 0). + **kwargs: Additional keyword arguments forwarded to the backend + (e.g., color, marker, s, label). + """ + ld = { + "x": np.array(x), + "y": np.array(y), + "layer": layer, + "plot_type": "scatter", + "kwargs": kwargs, + } + self._add(ld, layer) + + def bar(self, x, height, layer=0, **kwargs): + """ + Add a bar chart to the subplot. + + Parameters: + x (array-like): X positions of the bars. + height (array-like): Heights of the bars. + layer (int): Layer index (default 0). + **kwargs: Additional keyword arguments forwarded to the backend + (e.g., color, width, label). + """ + ld = { + "x": np.array(x), + "height": np.array(height), + "layer": layer, + "plot_type": "bar", + "kwargs": kwargs, + } + self._add(ld, layer) + + def axhline(self, y=0, layer=0, **kwargs): + """ + Add a horizontal line spanning the full width of the axes. + + Parameters: + y (float): Y-coordinate of the line (default 0). + **kwargs: Additional keyword arguments (e.g., color, linestyle, label). + """ + ld = { + "y": y, + "layer": layer, + "plot_type": "axhline", + "kwargs": kwargs, + } + self._add(ld, layer) + + def axvline(self, x=0, layer=0, **kwargs): + """ + Add a vertical line spanning the full height of the axes. + + Parameters: + x (float): X-coordinate of the line (default 0). + **kwargs: Additional keyword arguments (e.g., color, linestyle, label). + """ + ld = { + "x": x, + "layer": layer, + "plot_type": "axvline", + "kwargs": kwargs, + } + self._add(ld, layer) + + def set_xlabel(self, label: str): + """Set the x-axis label.""" + self._xlabel = label + + def set_ylabel(self, label: str): + """Set the y-axis label.""" + self._ylabel = label + + def set_title(self, title: str): + """Set the subplot title.""" + self._title = title + + def set_xlim(self, left=None, right=None): + """Set the x-axis limits.""" + if left is not None: + self._xmin = left + if right is not None: + self._xmax = right + + def set_ylim(self, bottom=None, top=None): + """Set the y-axis limits.""" + if bottom is not None: + self._ymin = bottom + if top is not None: + self._ymax = top + + def set_grid(self, visible: bool = True): + """Show or hide the grid.""" + self._grid = visible + + def set_legend(self, visible: bool = True): + """Show or hide the legend.""" + self._legend = visible + + def set_xscale(self, scale: str): + """Set the x-axis scale type: 'linear', 'log', or 'symlog'.""" + self._xaxis_scale = scale + + def set_yscale(self, scale: str): + """Set the y-axis scale type: 'linear', 'log', or 'symlog'.""" + self._yaxis_scale = scale + + def set_xticks(self, ticks, labels=None): + """Set x-axis tick positions and optional labels.""" + self._xticks = list(ticks) + self._xticklabels = list(labels) if labels is not None else None + + def set_yticks(self, ticks, labels=None): + """Set y-axis tick positions and optional labels.""" + self._yticks = list(ticks) + self._yticklabels = list(labels) if labels is not None else None + + def set_aspect(self, aspect): + """Set the axes aspect ratio: 'equal', 'auto', or a float.""" + self._aspect = aspect + + def fill_between(self, x, y1, y2=0, layer=0, **kwargs): + """ + Fill the region between two curves. + + Parameters: + x (array-like): X-axis data. + y1 (array-like): Upper boundary. + y2 (array-like or scalar): Lower boundary (default 0). + layer (int): Layer index. + **kwargs: Forwarded to the backend (e.g., color, alpha, label). + """ + ld = { + "x": np.array(x), + "y1": np.array(y1) if not np.isscalar(y1) else y1, + "y2": np.array(y2) if not np.isscalar(y2) else y2, + "layer": layer, + "plot_type": "fill_between", + "kwargs": kwargs, + } + self._add(ld, layer) + + def errorbar(self, x, y, yerr=None, xerr=None, layer=0, **kwargs): + """ + Add a line plot with error bars. + + Parameters: + x (array-like): X-axis data. + y (array-like): Y-axis data. + yerr (array-like or scalar, optional): Y-axis error. + xerr (array-like or scalar, optional): X-axis error. + layer (int): Layer index. + **kwargs: Forwarded to the backend (e.g., color, fmt, capsize, label). + """ + ld = { + "x": np.array(x), + "y": np.array(y), + "yerr": yerr, + "xerr": xerr, + "layer": layer, + "plot_type": "errorbar", + "kwargs": kwargs, + } + self._add(ld, layer) + + def hlines(self, y, xmin, xmax, layer=0, **kwargs): + """ + Draw horizontal lines at each y from xmin to xmax. + + Parameters: + y (float or array-like): Y positions. + xmin, xmax (float or array-like): Start and end of each line. + **kwargs: Forwarded to the backend (e.g., colors, linestyles, label). + """ + ld = { + "y": y, + "xmin": xmin, + "xmax": xmax, + "layer": layer, + "plot_type": "hlines", + "kwargs": kwargs, + } + self._add(ld, layer) + + def vlines(self, x, ymin, ymax, layer=0, **kwargs): + """ + Draw vertical lines at each x from ymin to ymax. + + Parameters: + x (float or array-like): X positions. + ymin, ymax (float or array-like): Start and end of each line. + **kwargs: Forwarded to the backend (e.g., colors, linestyles, label). + """ + ld = { + "x": x, + "ymin": ymin, + "ymax": ymax, + "layer": layer, + "plot_type": "vlines", + "kwargs": kwargs, + } + self._add(ld, layer) + + def annotate(self, text, xy, xytext=None, layer=0, **kwargs): + """ + Add a text annotation, optionally with an arrow. + + Parameters: + text (str): Annotation text. + xy (tuple): (x, y) position to annotate. + xytext (tuple, optional): (x, y) position for the text. + **kwargs: Forwarded to ax.annotate (e.g., arrowprops, fontsize, color). + """ + ld = { + "text": text, + "xy": xy, + "xytext": xytext, + "layer": layer, + "plot_type": "annotate", + "kwargs": kwargs, + } + self._add(ld, layer) + + def text(self, x, y, s, layer=0, **kwargs): + """ + Add a text label at position (x, y). + + Parameters: + x, y (float): Position. + s (str): Text string. + **kwargs: Forwarded to ax.text (e.g., fontsize, color, ha, va). + """ + ld = { + "x": x, + "y": y, + "s": s, + "layer": layer, + "plot_type": "text", + "kwargs": kwargs, + } + self._add(ld, layer) + def add_imshow(self, data, layer=0, **kwargs): ld = { "data": np.array(data), @@ -172,6 +439,7 @@ def plot_matplotlib( Parameters: ax (matplotlib.axes.Axes): Axis on which to plot the lines. """ + im = None for layer_name, layer_lines in self.layered_line_data.items(): if layers and layer_name not in layers: continue @@ -188,6 +456,48 @@ def plot_matplotlib( (line["y"] + self._yshift) * self._yscale, **line["kwargs"], ) + elif line["plot_type"] == "bar": + ax.bar( + (line["x"] + self._xshift) * self._xscale, + line["height"] * self._yscale, + **line["kwargs"], + ) + elif line["plot_type"] == "fill_between": + ax.fill_between( + (line["x"] + self._xshift) * self._xscale, + ( + line["y1"] + if np.isscalar(line["y1"]) + else (line["y1"] + self._yshift) * self._yscale + ), + ( + line["y2"] + if np.isscalar(line["y2"]) + else (line["y2"] + self._yshift) * self._yscale + ), + **line["kwargs"], + ) + elif line["plot_type"] == "errorbar": + ax.errorbar( + (line["x"] + self._xshift) * self._xscale, + (line["y"] + self._yshift) * self._yscale, + yerr=line["yerr"], + xerr=line["xerr"], + **line["kwargs"], + ) + elif line["plot_type"] == "hlines": + ax.hlines(line["y"], line["xmin"], line["xmax"], **line["kwargs"]) + elif line["plot_type"] == "vlines": + ax.vlines(line["x"], line["ymin"], line["ymax"], **line["kwargs"]) + elif line["plot_type"] == "annotate": + ann_kwargs = dict(line["kwargs"]) + if line["xytext"] is not None: + ann_kwargs["xytext"] = line["xytext"] + ax.annotate(line["text"], xy=line["xy"], **ann_kwargs) + elif line["plot_type"] == "text": + ax.text(line["x"], line["y"], line["s"], **line["kwargs"]) + elif line["plot_type"] == "axvline": + ax.axvline(x=line["x"], **line["kwargs"]) elif line["plot_type"] == "imshow": im = ax.imshow( line["data"], @@ -202,24 +512,39 @@ def plot_matplotlib( divider = make_axes_locatable(ax) cax = divider.append_axes("right", size="5%", pad=0.05) plt.colorbar(im, cax=cax, label="Potential (V)") - if self._title: - ax.set_title(self._title) - if self._xlabel: - ax.set_xlabel(self._xlabel) - if self._ylabel: - ax.set_ylabel(self._ylabel) - if self._legend and len(self.line_data) > 0: - ax.legend() - if self._grid: - ax.grid() - if self.xmin is not None: - ax.axis(xmin=self.xmin) - if self.xmax is not None: - ax.axis(xmax=self.xmax) - if self.ymin is not None: - ax.axis(ymin=self.ymin) - if self.ymax is not None: - ax.axis(ymax=self.ymax) + + if self._title: + ax.set_title(self._title) + if self._xlabel: + ax.set_xlabel(self._xlabel) + if self._ylabel: + ax.set_ylabel(self._ylabel) + if self._legend and len(self.line_data) > 0: + ax.legend() + if self._grid: + ax.grid() + if self.xmin is not None: + ax.axis(xmin=self.xmin) + if self.xmax is not None: + ax.axis(xmax=self.xmax) + if self.ymin is not None: + ax.axis(ymin=self.ymin) + if self.ymax is not None: + ax.axis(ymax=self.ymax) + if self._xaxis_scale is not None: + ax.set_xscale(self._xaxis_scale) + if self._yaxis_scale is not None: + ax.set_yscale(self._yaxis_scale) + if self._xticks is not None: + ax.set_xticks(self._xticks) + if self._xticklabels is not None: + ax.set_xticklabels(self._xticklabels) + if self._yticks is not None: + ax.set_yticks(self._yticks) + if self._yticklabels is not None: + ax.set_yticklabels(self._yticklabels) + if self._aspect is not None: + ax.set_aspect(self._aspect) def plot_tikzfigure(self, layers=None, verbose: bool = False) -> TikzFigure: @@ -243,7 +568,6 @@ def plot_plotly(self): """ Plot all lines using Plotly and return a list of traces for each line. """ - # Mapping Matplotlib linestyles to Plotly dash styles linestyle_map = { "solid": "solid", "dashed": "dash", @@ -253,20 +577,41 @@ def plot_plotly(self): traces = [] for line in self.line_data: - 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", ""), - line=dict( - color=line["kwargs"].get("color", None), - dash=linestyle_map.get( - line["kwargs"].get("linestyle", "solid"), - "solid", + plot_type = line["plot_type"] + if plot_type == "plot": + 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", ""), + line=dict( + color=line["kwargs"].get("color", None), + dash=linestyle_map.get( + line["kwargs"].get("linestyle", "solid"), + "solid", + ), ), - ), - ) - traces.append(trace) + ) + traces.append(trace) + elif plot_type == "scatter": + 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)), + ) + traces.append(trace) + elif plot_type == "bar": + 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), + ) + traces.append(trace) + elif plot_type in ("axhline", "axvline"): + pass # Rendered as shape annotations; no trace needed return traces @@ -306,10 +651,9 @@ def legend(self, value): if __name__ == "__main__": - plotter = LinePlot() - plotter.add_line("Line 1", [0, 1, 2, 3], [0, 1, 4, 9]) - plotter.add_line("Line 2", [0, 1, 2, 3], [0, 2, 3, 6]) - latex_code = plotter.generate_latex_plot() - with open("figures/latex_code.tex", "w") as f: - f.write(latex_code) - print(latex_code) + plotter = LinePlot(xlabel="x", ylabel="y", title="Example", legend=True) + plotter.plot([0, 1, 2, 3], [0, 1, 4, 9], label="Line 1") + plotter.plot( + [0, 1, 2, 3], [0, 2, 3, 6], linestyle="dashed", color="red", label="Line 2" + ) + plotter.scatter([0, 1, 2, 3], [0, 0.5, 2, 5], label="Scatter") diff --git a/tutorials/tutorial_01.ipynb b/tutorials/tutorial_01.ipynb index 5a054f8..ab79db8 100644 --- a/tutorials/tutorial_01.ipynb +++ b/tutorials/tutorial_01.ipynb @@ -5,8 +5,17 @@ "id": "0", "metadata": {}, "source": [ + "# Tutorial 01 β€” Quick Start\n", "\n", - "# Tutorial 1\n" + "**maxplotlib** is a thin, expressive wrapper around Matplotlib that simplifies\n", + "creating publication-quality figures. Its central class, `Canvas`, replaces the\n", + "usual `fig, ax = plt.subplots()` boilerplate and exposes a clean, chainable API.\n", + "\n", + "This notebook walks you through the very basics:\n", + "- creating a canvas and a subplot\n", + "- adding lines\n", + "- using the canvas-level shortcut API\n", + "- saving figures" ] }, { @@ -17,98 +26,194 @@ "outputs": [], "source": [ "from maxplotlib import Canvas\n", + "import numpy as np\n", + "\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## 1 Minimal example\n", "\n", - "%load_ext autoreload\n", - "%autoreload 2" + "`Canvas.subplots()` mirrors `plt.subplots()`. It returns the canvas and one (or\n", + "more) subplot axes objects." ] }, { "cell_type": "code", "execution_count": null, - "id": "2", + "id": "3", "metadata": {}, "outputs": [], "source": [ - "c = Canvas(ratio=0.5, fontsize=12)\n", - "c.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\")\n", - "c.add_line([0, 1, 2, 3], [0, 2, 3, 4], linestyle=\"dashed\", color=\"red\", label=\"Line 2\")\n", - "c.show()" + "x = np.linspace(0, 2 * np.pi, 200)\n", + "y = np.sin(x)\n", + "\n", + "canvas, ax = Canvas.subplots()\n", + "ax.plot(x, y)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2 Multiple lines with labels, colors, and linestyles" ] }, { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "5", "metadata": {}, "outputs": [], "source": [ - "# You can also explicitly create a subplot and add lines to it\n", + "canvas, ax = Canvas.subplots()\n", "\n", - "c = Canvas(ratio=0.5, fontsize=12)\n", - "sp = c.add_subplot(\n", - " grid=True, xlabel=\"(x - 10) * 0.1\", ylabel=\"10y\", yscale=10, xshift=-10, xscale=0.1\n", + "ax.plot(x, np.sin(x), label=\"sin(x)\", color=\"royalblue\", linestyle=\"solid\", linewidth=2)\n", + "ax.plot(x, np.cos(x), label=\"cos(x)\", color=\"tomato\", linestyle=\"dashed\", linewidth=2)\n", + "ax.plot(\n", + " x,\n", + " np.sin(2 * x),\n", + " label=\"sin(2x)\",\n", + " color=\"forestgreen\",\n", + " linestyle=\"dotted\",\n", + " linewidth=1.5,\n", ")\n", "\n", - "sp.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\")\n", - "sp.add_line([0, 1, 2, 3], [0, 2, 3, 4], linestyle=\"dashed\", color=\"red\", label=\"Line 2\")\n", - "c.show()" + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Sine and Cosine\")\n", + "ax.set_legend(True)\n", + "\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3 Canvas-level shortcut API\n", + "\n", + "For single-subplot figures you can call plot methods directly on the `Canvas`\n", + "object β€” they are forwarded to the subplot at `row=0, col=0`." ] }, { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "7", "metadata": {}, "outputs": [], "source": [ - "# Example with multiple subplots\n", - "\n", - "c = Canvas(width=\"10cm\", ncols=2, nrows=2, ratio=0.5)\n", - "sp = c.add_subplot(grid=True)\n", - "c.add_subplot(row=1)\n", - "sp2 = c.add_subplot(row=1, legend=False)\n", - "sp.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\")\n", - "sp2.add_line(\n", - " [0, 1, 2, 3], [0, 2, 3, 4], linestyle=\"dashed\", color=\"red\", label=\"Line 2\"\n", - ")\n", - "c.show()" + "canvas = Canvas(ratio=0.5, fontsize=12)\n", + "\n", + "canvas.add_line(x, np.sin(x), label=\"sin(x)\", color=\"steelblue\")\n", + "canvas.add_line(x, np.cos(x), label=\"cos(x)\", color=\"darkorange\", linestyle=\"dashed\")\n", + "\n", + "canvas.set_xlabel(\"angle (rad)\")\n", + "canvas.set_ylabel(\"amplitude\")\n", + "canvas.set_title(\"Using canvas-level methods\")\n", + "canvas.set_legend(True)\n", + "canvas.set_grid(True)\n", + "\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 4 Configuring the subplot at creation time\n", + "\n", + "`add_subplot()` accepts convenience kwargs so you can set labels, grid, and\n", + "legend in one call." ] }, { "cell_type": "code", "execution_count": null, - "id": "5", + "id": "9", "metadata": {}, "outputs": [], "source": [ - "# Test with plotly backend\n", - "c = Canvas(ratio=0.5)\n", - "sp = c.add_subplot(\n", - " grid=True, xlabel=\"x (mm)\", ylabel=\"10y\", yscale=10, xshift=-10, xscale=0.1\n", + "canvas = Canvas(ratio=0.5)\n", + "ax = canvas.add_subplot(\n", + " title=\"Configured at creation\",\n", + " xlabel=\"x\",\n", + " ylabel=\"f(x)\",\n", + " grid=True,\n", + " legend=True,\n", ")\n", - "sp.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\", linestyle=\"-.\")\n", - "sp.add_line([0, 1, 2, 3], [0, 2, 3, 4], linestyle=\"dashed\", color=\"red\", label=\"Line 2\")\n", - "c.show()" + "\n", + "ax.plot(x, np.sin(x), label=\"sin\", color=\"royalblue\")\n", + "ax.plot(x, x / (2 * np.pi), label=\"x/2Ο€\", color=\"coral\", linestyle=\"dashed\")\n", + "\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 5 Saving a figure\n", + "\n", + "Use `canvas.savefig()` to write the figure to disk. The file extension\n", + "determines the format; pass `backend='matplotlib'` for PDF output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "canvas = Canvas(ratio=0.5)\n", + "ax = canvas.add_subplot(xlabel=\"x\", ylabel=\"sin(x)\", grid=True)\n", + "ax.plot(x, np.sin(x), color=\"steelblue\")\n", + "\n", + "canvas.savefig(\"tutorial_01_output.png\")\n", + "print(\"Figure saved to tutorial_01_output.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Task | Code |\n", + "|---|---|\n", + "| Create canvas + subplot | `canvas, ax = Canvas.subplots()` |\n", + "| Add a line | `ax.plot(x, y, label=..., color=..., linestyle=...)` |\n", + "| Canvas shortcut | `canvas.add_line(x, y, ...)` |\n", + "| Labels / title | `ax.set_xlabel()`, `ax.set_ylabel()`, `ax.set_title()` |\n", + "| Legend / grid | `ax.set_legend(True)`, `ax.set_grid(True)` |\n", + "| Display | `canvas.show()` |\n", + "| Save | `canvas.savefig('out.png')` |\n", + "\n", + "Continue to **Tutorial 02** to learn about multi-subplot layouts." ] } ], "metadata": { "kernelspec": { - "display_name": "env_maxpic", + "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.3" + "version": "3.10.0" } }, "nbformat": 4, diff --git a/tutorials/tutorial_02.ipynb b/tutorials/tutorial_02.ipynb index 89ec85b..b30e4cb 100644 --- a/tutorials/tutorial_02.ipynb +++ b/tutorials/tutorial_02.ipynb @@ -5,7 +5,16 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 2" + "# Tutorial 02 β€” Multiple Subplots\n", + "\n", + "This notebook shows all the ways to build multi-panel figures with maxplotlib:\n", + "\n", + "- `Canvas.subplots(ncols=...)` / `Canvas.subplots(nrows=..., ncols=...)`\n", + "- `squeeze=False` for a consistent 2-D axes list\n", + "- Manual layout with `canvas.add_subplot(row=..., col=...)`\n", + "- Accessing subplots: `canvas.subplot()`, `canvas[row, col]`, `canvas.iter_subplots()`\n", + "- Figure-level title with `canvas.suptitle()`\n", + "- Canvas-level plot routing to a specific subplot" ] }, { @@ -16,129 +25,262 @@ "outputs": [], "source": [ "from maxplotlib import Canvas\n", + "import numpy as np\n", "\n", - "%load_ext autoreload\n", - "%autoreload 2" + "%matplotlib inline\n", + "\n", + "x = np.linspace(0, 2 * np.pi, 200)" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## 1 1Γ—2 layout β€” side-by-side subplots" ] }, { "cell_type": "code", "execution_count": null, - "id": "2", + "id": "3", "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=800, ratio=0.5)\n", - "tikz = c.add_tikzfigure(grid=False)\n", + "canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width=1000, ratio=0.3)\n", "\n", - "# Add nodes\n", - "tikz.add_node(0, 0, label=\"A\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=0)\n", - "tikz.add_node(1, 0, label=\"B\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=0)\n", - "tikz.add_node(1, 1, label=\"C\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=0)\n", - "tikz.add_node(0, 1, label=\"D\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=2)\n", + "ax1.plot(x, np.sin(x), color=\"royalblue\")\n", + "ax1.set_title(\"sin(x)\")\n", + "ax1.set_xlabel(\"x\")\n", + "ax1.set_ylabel(\"amplitude\")\n", "\n", + "ax2.plot(x, np.cos(x), color=\"tomato\")\n", + "ax2.set_title(\"cos(x)\")\n", + "ax2.set_xlabel(\"x\")\n", "\n", - "# Add a line between nodes\n", - "tikz.draw(\n", - " [\"A\", \"B\", \"C\", \"D\"],\n", - " path_actions=[\"draw\", \"rounded corners\"],\n", - " fill=\"red\",\n", - " opacity=1.0,\n", - " cycle=True,\n", - " layer=1,\n", - ")\n", + "canvas.suptitle(\"1 Γ— 2 Layout\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2 2Γ—2 layout β€” grid of subplots\n", + "\n", + "`Canvas.subplots(nrows=2, ncols=2)` returns a 2-D list of subplot axes\n", + "indexed as `axes[row][col]`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, axes = Canvas.subplots(nrows=2, ncols=2)\n", "\n", - "tikz.add_node(0.5, 0.5, content=\"Cube\", layer=10)\n", + "axes[0][0].plot(x, np.sin(x), color=\"royalblue\")\n", + "axes[0][0].set_title(\"sin(x)\")\n", + "axes[0][1].plot(x, np.cos(x), color=\"tomato\")\n", + "axes[0][1].set_title(\"cos(x)\")\n", + "axes[1][0].plot(x, np.sin(2 * x), color=\"seagreen\")\n", + "axes[1][0].set_title(\"sin(2x)\")\n", + "axes[1][1].plot(x, np.cos(2 * x), color=\"darkorange\")\n", + "axes[1][1].set_title(\"cos(2x)\")\n", + "\n", + "canvas.suptitle(\"2 Γ— 2 Layout\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3 `squeeze=False` β€” always get a 2-D list\n", "\n", - "# tikz.compile_pdf(\"tutorial_02_01.pdf\")\n", - "c.plot(backend=\"matplotlib\")" + "By default a 1Γ—N or NΓ—1 grid returns a flat list. Pass `squeeze=False` to\n", + "always get a 2-D nested list β€” useful when your layout code must be generic." ] }, { "cell_type": "code", "execution_count": null, - "id": "3", + "id": "7", "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=\"10cm\", ncols=2, ratio=0.5)\n", - "tikz = c.add_tikzfigure(grid=False)\n", - "\n", - "# Add nodes\n", - "node_a = tikz.add_node(\n", - " -5,\n", - " 0,\n", - " label=\"A\",\n", - " content=\"Origin node\",\n", - " shape=\"circle\",\n", - " draw=\"black\",\n", - " fill=\"blue!20\",\n", - ")\n", - "tikz.add_node(\n", - " 2,\n", - " 2,\n", - " label=\"B\",\n", - " content=\"$a^2 + b^2 = c^2$\",\n", - " shape=\"rectangle\",\n", - " draw=\"red\",\n", - " fill=\"white\",\n", - " layer=1,\n", - ")\n", - "tikz.add_node(2, 5, label=\"C\", shape=\"rectangle\", draw=\"red\", fill=\"red\")\n", - "last_node = tikz.add_node(-1, 5, shape=\"rectangle\", draw=\"red\", fill=\"red\", layer=-10)\n", - "\n", - "# # Add a line between nodes\n", - "tikz.draw(\n", - " [node_a.label, \"B\", \"C\", \"A\", last_node],\n", - " color=\"green\",\n", - " style=\"solid\",\n", - " line_width=\"2\",\n", - " layer=-5,\n", - ")\n", + "canvas, axes = Canvas.subplots(nrows=1, ncols=3, squeeze=False)\n", + "\n", + "# axes is always [[ax0, ax1, ax2]] β€” index as axes[row][col]\n", + "data = [np.sin(x), np.cos(x), np.tan(np.clip(x, 0, np.pi - 0.1))]\n", + "titles = [\"sin\", \"cos\", \"tan (clipped)\"]\n", "\n", - "sp = c.add_subplot(\n", - " grid=True, xlabel=\"(x - 10) * 0.1\", ylabel=\"10y\", yscale=10, xshift=-10, xscale=0.1\n", + "for col, (d, t) in enumerate(zip(data, titles)):\n", + " axes[0][col].plot(x, d, color=\"steelblue\")\n", + " axes[0][col].set_title(t)\n", + "\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 4 Manual layout β€” `canvas.add_subplot(row, col)`\n", + "\n", + "You can build the layout yourself by calling `add_subplot` explicitly. This\n", + "lets you configure each panel's title, labels, grid, and legend in one shot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "canvas = Canvas(nrows=2, ncols=2)\n", + "\n", + "ax00 = canvas.add_subplot(\n", + " row=0, col=0, title=\"Top-left\", xlabel=\"x\", ylabel=\"y\", grid=True\n", ")\n", - "sp.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\")\n", - "sp.add_line([0, 1, 2, 3], [0, 2, 3, 4], linestyle=\"dashed\", color=\"red\", label=\"Line 2\")\n", + "ax01 = canvas.add_subplot(row=0, col=1, title=\"Top-right\", xlabel=\"x\", grid=True)\n", + "ax10 = canvas.add_subplot(row=1, col=0, title=\"Bottom-left\", xlabel=\"x\", ylabel=\"y\")\n", + "ax11 = canvas.add_subplot(row=1, col=1, title=\"Bottom-right\", xlabel=\"x\", legend=True)\n", "\n", - "# Generate the TikZ script\n", - "# print(tikz.generate_standalone())\n", + "ax00.plot(x, np.sin(x), color=\"royalblue\")\n", + "ax01.plot(x, np.cos(x), color=\"tomato\")\n", + "ax10.plot(x, np.sin(2 * x), color=\"seagreen\")\n", + "ax11.plot(x, np.sin(x), label=\"sin\", color=\"royalblue\")\n", + "ax11.plot(x, np.cos(x), label=\"cos\", color=\"tomato\")\n", "\n", - "# tikz.compile_pdf(\"tutorial_02_02.pdf\")\n", + "canvas.suptitle(\"Manual Layout\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 5 Accessing subplots after creation\n", "\n", - "c.plot(backend=\"matplotlib\")" + "Three equivalent ways to retrieve a subplot object:" ] }, { "cell_type": "code", "execution_count": null, - "id": "4", + "id": "11", "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=800, ratio=0.5)\n", - "tikz = c.add_tikzfigure(grid=False)\n", + "canvas, axes = Canvas.subplots(nrows=2, ncols=2)\n", "\n", - "# Add nodes\n", - "tikz.add_node(0, 0, label=\"A\")\n", - "tikz.add_node(10, 0, label=\"B\")\n", + "# Method A: use the object returned by subplots()\n", + "axes[0][0].set_title(\"Method A\")\n", "\n", + "# Method B: canvas.subplot(row, col)\n", + "sp_b = canvas.subplot(row=0, col=1)\n", + "sp_b.set_title(\"Method B\")\n", + "\n", + "# Method C: canvas[row, col] indexing\n", + "canvas[1, 0].set_title(\"Method C\")\n", + "canvas[1, 1].set_title(\"Method D (index)\")\n", + "\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## 6 `canvas.iter_subplots()` β€” loop over all panels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, axes = Canvas.subplots(nrows=2, ncols=2)\n", + "\n", + "# Plot something in every panel first\n", + "for row in range(2):\n", + " for col in range(2):\n", + " axes[row][col].plot(x, np.sin((row + 1) * (col + 1) * x))\n", + "\n", + "# Then enable grid on every panel uniformly\n", + "for row, col, sp in canvas.iter_subplots():\n", + " sp.set_grid(True)\n", + " sp.set_xlabel(\"x\")\n", + "\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## 7 Canvas-level plot routing\n", + "\n", + "Pass `row=` and `col=` to canvas-level methods to target a specific subplot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "canvas = Canvas(nrows=1, ncols=2)\n", + "canvas.add_subplot(row=0, col=0, title=\"Left\", xlabel=\"x\", ylabel=\"sin\")\n", + "canvas.add_subplot(row=0, col=1, title=\"Right\", xlabel=\"x\", ylabel=\"cos\")\n", + "\n", + "canvas.add_line(x, np.sin(x), row=0, col=0, color=\"royalblue\", label=\"sin\")\n", + "canvas.add_line(x, np.cos(x), row=0, col=1, color=\"tomato\", label=\"cos\")\n", + "\n", + "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()" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Summary\n", "\n", - "# Add a line between nodes\n", - "tikz.draw([\"A\", \"B\"], path_actions=[\"->\"], out=30)\n", + "| Task | Code |\n", + "|---|---|\n", + "| 1Γ—2 grid | `canvas, (ax1, ax2) = Canvas.subplots(ncols=2)` |\n", + "| 2Γ—2 grid | `canvas, axes = Canvas.subplots(nrows=2, ncols=2)` β€” index `axes[r][c]` |\n", + "| Always 2-D | `Canvas.subplots(..., squeeze=False)` |\n", + "| Manual panel | `canvas.add_subplot(row=r, col=c, ...)` |\n", + "| Get subplot | `canvas.subplot(r, c)` or `canvas[r, c]` |\n", + "| Loop panels | `for row, col, sp in canvas.iter_subplots()` |\n", + "| Figure title | `canvas.suptitle('...')` |\n", + "| Route plot | `canvas.add_line(x, y, row=r, col=c)` |\n", "\n", - "# Generate the TikZ script\n", - "# script = tikz.generate_tikz()\n", - "# print(script)\n", - "print(tikz.generate_standalone())\n", - "# tikz.compile_pdf(\"tutorial_02_03.pdf\")" + "Next: **Tutorial 03** covers all the available plot types." ] } ], "metadata": { "kernelspec": { - "display_name": "env_maxpic", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -152,7 +294,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.3" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/tutorials/tutorial_03.ipynb b/tutorials/tutorial_03.ipynb index a323007..cc9de2b 100644 --- a/tutorials/tutorial_03.ipynb +++ b/tutorials/tutorial_03.ipynb @@ -5,7 +5,15 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 3" + "# Tutorial 03 β€” Plot Types\n", + "\n", + "maxplotlib exposes all common Matplotlib plot types through a unified API.\n", + "This notebook demonstrates each type individually, then shows how to combine\n", + "multiple types in a single subplot.\n", + "\n", + "**Plot types covered:**\n", + "`plot` Β· `scatter` Β· `bar` Β· `fill_between` Β· `errorbar` Β·\n", + "`axhline` / `axvline` Β· `hlines` / `vlines` Β· combined plots Β· `annotate` / `text`" ] }, { @@ -15,36 +23,345 @@ "metadata": {}, "outputs": [], "source": [ - "from maxplotlib import Canvas" + "from maxplotlib import Canvas\n", + "import numpy as np\n", + "\n", + "%matplotlib inline\n", + "\n", + "rng = np.random.default_rng(42)\n", + "x = np.linspace(0, 2 * np.pi, 300)" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## 1 Line plot β€” `ax.plot()`" ] }, { "cell_type": "code", "execution_count": null, - "id": "2", + "id": "3", "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=\"17cm\", ratio=0.5)\n", - "sp = c.add_subplot(\n", - " grid=False, xlabel=\"(x - 10) * 0.1\", ylabel=\"10y\", yscale=10, xshift=-10, xscale=0.1\n", + "canvas, ax = Canvas.subplots()\n", + "\n", + "ax.plot(x, np.sin(x), label=\"sin(x)\", color=\"royalblue\", linestyle=\"solid\", linewidth=2)\n", + "ax.plot(x, np.cos(x), label=\"cos(x)\", color=\"tomato\", linestyle=\"dashed\", linewidth=2)\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Line Plot\")\n", + "ax.set_legend(True)\n", + "ax.set_grid(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2 Scatter plot β€” `ax.scatter()`\n", + "\n", + "Use `c=` to colour each point by a scalar value (requires a colormap-compatible array)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "n = 200\n", + "sx = rng.standard_normal(n)\n", + "sy = rng.standard_normal(n)\n", + "values = np.sqrt(sx**2 + sy**2) # colour by distance from origin\n", + "\n", + "canvas, ax = Canvas.subplots()\n", + "ax.scatter(sx, sy, c=values, s=30, label=\"data points\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Scatter Plot β€” coloured by distance\")\n", + "ax.set_aspect(\"equal\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3 Bar chart β€” `ax.bar()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "categories = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"]\n", + "values_bar = [12, 19, 15, 22, 30, 27]\n", + "\n", + "canvas, ax = Canvas.subplots()\n", + "ax.bar(categories, values_bar, color=\"steelblue\", width=0.6, label=\"monthly sales\")\n", + "ax.set_xlabel(\"Month\")\n", + "ax.set_ylabel(\"Sales (units)\")\n", + "ax.set_title(\"Bar Chart\")\n", + "ax.set_legend(True)\n", + "# canvas.show() # TODO: Fix this error" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 4 Fill between β€” `ax.fill_between()`\n", + "\n", + "Useful for shading confidence bands or uncertainty regions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "t = np.linspace(0, 4 * np.pi, 300)\n", + "mean = np.sin(t) * np.exp(-t / 10)\n", + "upper = mean + 0.3 * (1 - t / (4 * np.pi))\n", + "lower = mean - 0.3 * (1 - t / (4 * np.pi))\n", + "\n", + "canvas, ax = Canvas.subplots()\n", + "ax.plot(t, mean, color=\"royalblue\", label=\"mean\", linewidth=2)\n", + "ax.fill_between(t, lower, upper, alpha=0.25, color=\"royalblue\", label=\"Β±1 std\")\n", + "ax.set_xlabel(\"t\")\n", + "ax.set_ylabel(\"value\")\n", + "ax.set_title(\"Fill Between β€” Confidence Band\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 5 Error bars β€” `ax.errorbar()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "xm = np.linspace(0, 2 * np.pi, 12)\n", + "ym = np.sin(xm) + rng.normal(0, 0.1, len(xm))\n", + "yerr = 0.1 + 0.05 * rng.random(len(xm))\n", + "\n", + "canvas, ax = Canvas.subplots()\n", + "ax.errorbar(xm, ym, yerr=yerr, fmt=\"o\", capsize=4, color=\"tomato\", label=\"measurements\")\n", + "ax.plot(x, np.sin(x), color=\"gray\", linestyle=\"dashed\", label=\"true sin(x)\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Error Bars\")\n", + "ax.set_legend(True)\n", + "ax.set_grid(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## 6 Reference lines β€” `axhline` and `axvline`\n", + "\n", + "Span the entire axis to mark thresholds or zero-crossings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, ax = Canvas.subplots()\n", + "ax.plot(x, np.sin(x), color=\"royalblue\", label=\"sin(x)\")\n", + "\n", + "ax.axhline(y=0, color=\"black\", linestyle=\"solid\", linewidth=0.8)\n", + "ax.axhline(y=0.5, color=\"green\", linestyle=\"dashed\", linewidth=1.2, label=\"y = 0.5\")\n", + "ax.axhline(y=-0.5, color=\"green\", linestyle=\"dashed\", linewidth=1.2, label=\"y = -0.5\")\n", + "ax.axvline(x=np.pi, color=\"red\", linestyle=\"dotted\", linewidth=1.5, label=\"x = Ο€\")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_title(\"axhline / axvline\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## 7 Segment lines β€” `hlines` and `vlines`\n", + "\n", + "Draw multiple horizontal or vertical line segments with explicit start/end coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, ax = Canvas.subplots()\n", + "ax.plot(x, np.sin(x), color=\"lightgray\", linewidth=1)\n", + "\n", + "# Horizontal segments spanning half the x-range\n", + "ax.hlines(\n", + " y=[0.5, -0.5],\n", + " xmin=0,\n", + " xmax=np.pi,\n", + " colors=\"seagreen\",\n", + " linestyles=\"dashed\",\n", + " label=\"hlines at Β±0.5\",\n", + ")\n", + "\n", + "# Vertical segments at specific x positions\n", + "ax.vlines(\n", + " x=[np.pi / 2, 3 * np.pi / 2],\n", + " ymin=-1,\n", + " ymax=1,\n", + " colors=\"tomato\",\n", + " linestyles=\"dotted\",\n", + " label=\"vlines at Ο€/2, 3Ο€/2\",\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_title(\"hlines / vlines\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## 8 Combining plot types\n", + "\n", + "A single subplot can hold many plot types simultaneously. Here we overlay\n", + "a line, a shaded uncertainty band, and scatter points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "t = np.linspace(0, 2 * np.pi, 300)\n", + "signal = np.sin(t) * np.exp(-t / 8)\n", + "noise = rng.normal(0, 0.08, len(t))\n", + "band = 0.15 * np.exp(-t / 8)\n", + "\n", + "# Sparse measurement points\n", + "tidx = np.arange(0, len(t), 20)\n", + "tx, ty = t[tidx], (signal + noise)[tidx]\n", + "\n", + "canvas, ax = Canvas.subplots()\n", + "\n", + "ax.fill_between(\n", + " t, signal - band, signal + band, alpha=0.2, color=\"royalblue\", label=\"uncertainty\"\n", ")\n", - "sp.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\", layer=0)\n", - "sp.add_line(\n", - " [0, 1, 2, 3], [0, 2, 3, 4], linestyle=\"dashed\", color=\"red\", label=\"Line 2\", layer=1\n", + "ax.plot(t, signal, color=\"royalblue\", linewidth=2, label=\"model\")\n", + "ax.scatter(tx, ty, color=\"tomato\", s=25, marker=\"o\", label=\"measurements\")\n", + "ax.axhline(y=0, color=\"gray\", linestyle=\"dashed\", linewidth=0.8)\n", + "\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"amplitude\")\n", + "ax.set_title(\"Combined: line + fill_between + scatter\")\n", + "ax.set_legend(True)\n", + "ax.set_grid(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## 9 Annotations β€” `ax.annotate()` and `ax.text()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "canvas, ax = Canvas.subplots()\n", + "ax.plot(x, np.sin(x), color=\"royalblue\")\n", + "\n", + "# Arrow annotation pointing to the peak\n", + "ax.annotate(\n", + " \"peak\",\n", + " xy=(np.pi / 2, 1.0),\n", + " xytext=(np.pi / 2 + 0.8, 0.7),\n", + " arrowprops=dict(arrowstyle=\"->\"),\n", ")\n", - "c.show()" + "\n", + "# Free-floating text label\n", + "ax.text(3 * np.pi / 2, 0.15, \"zero\\ncrossing\", ha=\"center\", fontsize=9)\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"sin(x)\")\n", + "ax.set_title(\"Annotate and Text\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Plot type | Method | Key kwargs |\n", + "|---|---|---|\n", + "| Line | `ax.plot(x, y)` | `color`, `linestyle`, `linewidth`, `label` |\n", + "| Scatter | `ax.scatter(x, y)` | `c`, `s`, `marker`, `label` |\n", + "| Bar | `ax.bar(x, height)` | `color`, `width`, `label` |\n", + "| Filled band | `ax.fill_between(x, y1, y2)` | `alpha`, `color`, `label` |\n", + "| Error bars | `ax.errorbar(x, y, yerr=...)` | `fmt`, `capsize`, `label` |\n", + "| Full-span h/v line | `ax.axhline(y=...)` / `ax.axvline(x=...)` | `color`, `linestyle` |\n", + "| Segment lines | `ax.hlines(y, xmin, xmax)` / `ax.vlines(x, ymin, ymax)` | `colors`, `linestyles` |\n", + "| Arrow annotation | `ax.annotate(text, xy=..., xytext=..., arrowprops=...)` | |\n", + "| Free text | `ax.text(x, y, text)` | `ha`, `fontsize` |\n", + "\n", + "You now know the full set of plot types available in maxplotlib. πŸŽ‰" ] } ], "metadata": { - "jupytext": { - "cell_metadata_filter": "-all", - "main_language": "python", - "notebook_metadata_filter": "-all" - }, "kernelspec": { - "display_name": "env_maxplotlib", + "display_name": ".venv", "language": "python", "name": "python3" }, diff --git a/tutorials/tutorial_04.ipynb b/tutorials/tutorial_04.ipynb index abde34e..7406fdd 100644 --- a/tutorials/tutorial_04.ipynb +++ b/tutorials/tutorial_04.ipynb @@ -5,7 +5,9 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 4" + "# Tutorial 04 – Styling\n", + "\n", + "maxplotlib passes matplotlib kwargs directly to the underlying renderer, so every colour, linestyle, marker, and alpha option you know from matplotlib works here too." ] }, { @@ -15,21 +17,18 @@ "metadata": {}, "outputs": [], "source": [ - "\"\"\"\n", - "Tutorial 4.\n", - "\n", - "Add raw tikz code to the tikz subplot.\n", - "\"\"\"" + "from maxplotlib import Canvas\n", + "import numpy as np" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "2", "metadata": {}, - "outputs": [], "source": [ - "from maxplotlib import Canvas" + "## 1 Β· Colors\n", + "\n", + "Named colors, hex strings, and RGB tuples are all accepted." ] }, { @@ -39,24 +38,29 @@ "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=800, ratio=0.5)\n", - "tikz = c.add_tikzfigure(grid=False)" + "x = np.linspace(0, 2 * np.pi, 200)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", + "\n", + "ax.plot(x, np.sin(x), color=\"steelblue\", label=\"named: steelblue\")\n", + "ax.plot(x, np.sin(x - 0.5), color=\"#e74c3c\", label=\"hex: #e74c3c\")\n", + "ax.plot(x, np.sin(x - 1.0), color=(0.2, 0.7, 0.3), label=\"RGB tuple\")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Color options\")\n", + "ax.set_legend(True)\n", + "canvas.show()" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "id": "4", - "metadata": { - "lines_to_next_cell": 2 - }, - "outputs": [], + "metadata": {}, "source": [ - "# Add nodes\n", - "tikz.add_node(0, 0, label=\"A\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=0)\n", - "tikz.add_node(10, 0, label=\"B\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=0)\n", - "tikz.add_node(10, 10, label=\"C\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=0)\n", - "tikz.add_node(0, 10, label=\"D\", shape=\"circle\", draw=\"black\", fill=\"blue\", layer=2)" + "## 2 Β· Linestyles\n", + "\n", + "Use long names (`'solid'`, `'dashed'`, `'dotted'`, `'dashdot'`) or short aliases (`'-'`, `'--'`, `':'`, `'-.'`)." ] }, { @@ -66,79 +70,200 @@ "metadata": {}, "outputs": [], "source": [ - "# Add a line between nodes\n", - "tikz.draw(\n", - " [\"A\", \"B\", \"C\", \"D\"],\n", - " path_actions=[\"draw\", \"rounded corners\"],\n", - " fill=\"red\",\n", - " opacity=0.5,\n", - " cycle=True,\n", - " layer=1,\n", - ")" + "x = np.linspace(0, 4 * np.pi, 300)\n", + "styles = [(\"solid\", \"-\"), (\"dashed\", \"--\"), (\"dotted\", \":\"), (\"dashdot\", \"-.\")]\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", + "\n", + "for i, (name, ls) in enumerate(styles):\n", + " ax.plot(\n", + " x,\n", + " np.sin(x) + i * 0.4,\n", + " linestyle=ls,\n", + " linewidth=2,\n", + " color=\"steelblue\",\n", + " label=f\"{name!r} / {ls!r}\",\n", + " )\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_title(\"Linestyle comparison\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3 Β· Markers\n", + "\n", + "Pass `marker=` to `ax.plot` or `ax.scatter`." ] }, { "cell_type": "code", "execution_count": null, - "id": "6", + "id": "7", "metadata": {}, "outputs": [], "source": [ - "raw_tikz = r\"\"\"\n", - "\\foreach \\i in {0, 45, 90, 135, 180, 225, 270, 315} {\n", - " % Place a node at angle \\i\n", - " \\node[circle, draw, fill=green] at (\\i:3) (N\\i) {};\n", - "}\n", - "\n", - "% Draw lines connecting the nodes\n", - "\\foreach \\i/\\j in {0/45, 45/90, 90/135, 135/180, 180/225, 225/270, 270/315, 315/0} {\n", - " \\draw (N\\i) -- (N\\j);\n", - "}\n", - "\"\"\"" + "x = np.linspace(0, 2 * np.pi, 10)\n", + "markers = [\"o\", \"s\", \"^\", \"D\", \"*\", \"x\", \"+\"]\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.65)\n", + "\n", + "for i, m in enumerate(markers):\n", + " ax.plot(\n", + " x,\n", + " np.sin(x) + i * 0.5,\n", + " marker=m,\n", + " linestyle=\"-\",\n", + " markersize=7,\n", + " label=f\"marker={m!r}\",\n", + " )\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_title(\"Marker comparison\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 4 Β· Linewidth and markersize\n", + "\n", + "Control visual weight with `linewidth=` and `markersize=`." ] }, { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "9", "metadata": {}, "outputs": [], "source": [ - "# TODO: Not implemented in tikzfigure yet\n", - "# tikz.add_raw(raw_tikz)" + "x = np.linspace(0, 2 * np.pi, 40)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "\n", + "ax.plot(x, np.sin(x), linewidth=0.8, marker=\"o\", markersize=3, label=\"thin / small\")\n", + "ax.plot(x, np.sin(x) - 0.6, linewidth=2.5, marker=\"o\", markersize=7, label=\"medium\")\n", + "ax.plot(\n", + " x, np.sin(x) - 1.2, linewidth=4.5, marker=\"o\", markersize=12, label=\"thick / large\"\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_title(\"Linewidth and markersize\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 5 Β· Alpha (transparency)\n", + "\n", + "`alpha=` ranges from `0` (invisible) to `1` (opaque). Useful for overlapping `fill_between` regions." ] }, { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "11", "metadata": {}, "outputs": [], "source": [ - "tikz.add_node(0.5, 0.5, content=\"Cube\", layer=10)" + "x = np.linspace(0, 2 * np.pi, 200)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "\n", + "ax.fill_between(x, np.sin(x), 0, alpha=0.7, color=\"steelblue\", label=\"alpha=0.7\")\n", + "ax.fill_between(x, np.sin(2 * x), 0, alpha=0.4, color=\"tomato\", label=\"alpha=0.4\")\n", + "ax.fill_between(x, np.sin(3 * x), 0, alpha=0.2, color=\"seagreen\", label=\"alpha=0.2\")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_title(\"Alpha transparency\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## 6 Β· Combining styles – a publication-ready plot\n", + "\n", + "Combine color, linestyle, marker, linewidth, and alpha in one plot." ] }, { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "13", "metadata": {}, "outputs": [], "source": [ - "# Generate the TikZ script\n", - "script = tikz.generate_tikz()\n", - "print(script)" + "x = np.linspace(0, 2 * np.pi, 80)\n", + "noise = np.random.default_rng(0).normal(0, 0.05, len(x))\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", + "\n", + "# Shaded uncertainty band\n", + "ax.fill_between(x, np.sin(x) - 0.15, np.sin(x) + 0.15, alpha=0.15, color=\"steelblue\")\n", + "\n", + "# Noisy data\n", + "ax.scatter(\n", + " x, np.sin(x) + noise, color=\"steelblue\", marker=\"o\", s=18, alpha=0.6, label=\"data\"\n", + ")\n", + "\n", + "# Clean model\n", + "ax.plot(\n", + " x,\n", + " np.sin(x),\n", + " color=\"#e74c3c\",\n", + " linestyle=\"dashed\",\n", + " linewidth=2.0,\n", + " alpha=0.9,\n", + " label=\"model\",\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Publication-style plot\")\n", + "ax.set_legend(True)\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Option | Example |\n", + "|---|---|\n", + "| Named color | `color='steelblue'` |\n", + "| Hex color | `color='#e74c3c'` |\n", + "| RGB tuple | `color=(0.2, 0.7, 0.3)` |\n", + "| Linestyle | `linestyle='dashed'` or `'--'` |\n", + "| Marker | `marker='o'` |\n", + "| Linewidth | `linewidth=2.0` |\n", + "| Markersize | `markersize=7` |\n", + "| Transparency | `alpha=0.5` |" ] } ], "metadata": { - "jupytext": { - "cell_metadata_filter": "-all", - "main_language": "python", - "notebook_metadata_filter": "-all" - }, "kernelspec": { - "display_name": "env_maxpic", + "display_name": "env_maxplotlib", "language": "python", "name": "python3" }, @@ -152,7 +277,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.3" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/tutorials/tutorial_05.ipynb b/tutorials/tutorial_05.ipynb index 2f70552..828046d 100644 --- a/tutorials/tutorial_05.ipynb +++ b/tutorials/tutorial_05.ipynb @@ -5,7 +5,9 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 5" + "# Tutorial 05 – Axes, Ticks, Scales, and Annotations\n", + "\n", + "This tutorial covers every way to control the coordinate frame of a subplot: labels, limits, tick marks, log scales, grids, legends, and annotations." ] }, { @@ -15,42 +17,325 @@ "metadata": {}, "outputs": [], "source": [ - "import numpy as np\n", - "\n", "from maxplotlib import Canvas\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## 1 Β· Labels and title\n", "\n", - "%load_ext autoreload\n", - "%autoreload 2" + "LaTeX strings are supported in all text arguments." ] }, { "cell_type": "code", "execution_count": null, - "id": "2", + "id": "3", + "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\")\n", + "\n", + "ax.set_xlabel(r\"$x$ (radians)\")\n", + "ax.set_ylabel(r\"$\\sin(x)$\")\n", + "ax.set_title(r\"The sine function $f(x) = \\sin(x)$\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2 Β· Axis limits" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 4 * np.pi, 300)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.5)\n", + "ax.plot(x, np.sin(x), color=\"tomato\")\n", + "\n", + "# Show only the first full period\n", + "ax.set_xlim(0, 2 * np.pi)\n", + "ax.set_ylim(-1.2, 1.2)\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(r\"$\\sin(x)$\")\n", + "ax.set_title(\"Axis limits: first period only\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3 Β· Custom ticks\n", + "\n", + "Pass tick positions and optional labels to `set_xticks`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=\"17cm\", ratio=0.5)\n", - "sp = c.add_subplot(grid=True, xlabel=\"x\", ylabel=\"y\")\n", + "months = [\n", + " \"Jan\",\n", + " \"Feb\",\n", + " \"Mar\",\n", + " \"Apr\",\n", + " \"May\",\n", + " \"Jun\",\n", + " \"Jul\",\n", + " \"Aug\",\n", + " \"Sep\",\n", + " \"Oct\",\n", + " \"Nov\",\n", + " \"Dec\",\n", + "]\n", + "temps = [3, 4, 7, 12, 17, 21, 23, 22, 18, 13, 7, 4]\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"12cm\", ratio=0.45)\n", + "ax.plot(range(12), temps, marker=\"o\", color=\"steelblue\", linewidth=2)\n", + "\n", + "ax.set_xticks(list(range(12)), labels=months)\n", + "ax.set_ylabel(r\"Temperature ($^\\circ$C)\")\n", + "ax.set_title(\"Monthly average temperature\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 4 Β· Log scale\n", "\n", - "# node_a = sp.add_node(\n", - "# 0, 0, \"A\", content=\"Node A\", shape=\"circle\", draw=\"black\", fill=\"blue!20\"\n", - "# )\n", - "# node_b = sp.add_node(\n", - "# 1, 1, \"B\", content=\"Node B\", shape=\"circle\", draw=\"black\", fill=\"blue!20\"\n", - "# )\n", - "# sp.add_node(2, 2, 'B', content=\"$a^2 + b^2 = c^2$\", shape='rectangle', draw='red', fill='white', layer=1)\n", - "# sp.add_node(2, 5, 'C', shape='rectangle', draw='red', fill='red')\n", - "# last_node = sp.add_node(-1, 5, shape='rectangle', draw='red', fill='red', layer=-10)\n", + "Use `set_yscale('log')` for data spanning several orders of magnitude. Compare linear and log in a 1Γ—2 layout." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0.1, 5, 200)\n", + "y = np.exp(2 * x)\n", + "\n", + "canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width=\"14cm\", ratio=0.4)\n", + "\n", + "ax1.plot(x, y, color=\"seagreen\")\n", + "ax1.set_xlabel(\"x\")\n", + "ax1.set_ylabel(r\"$e^{2x}$\")\n", + "ax1.set_title(\"Linear scale\")\n", + "\n", + "ax2.plot(x, y, color=\"seagreen\")\n", + "ax2.set_xlabel(\"x\")\n", + "ax2.set_yscale(\"log\")\n", + "ax2.set_title(\"Log scale\")\n", + "\n", + "canvas.suptitle(\"Linear vs log scale\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 5 Β· Grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 2 * np.pi, 200)\n", "\n", - "# Add a line between nodes\n", - "# sp.draw([\"A\", \"B\"], color=\"green\", style=\"solid\", line_width=\"2\", layer=-5)\n", + "canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width=\"12cm\", ratio=0.5)\n", "\n", - "x = np.arange(0, 2 * np.pi, 0.01)\n", + "ax1.plot(x, np.cos(x), color=\"steelblue\")\n", + "ax1.set_title(\"No grid\")\n", + "ax1.set_xlabel(\"x\")\n", + "\n", + "ax2.plot(x, np.cos(x), color=\"steelblue\")\n", + "ax2.set_grid(True)\n", + "ax2.set_title(\"With grid\")\n", + "ax2.set_xlabel(\"x\")\n", + "\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## 6 Β· Legend\n", + "\n", + "Enable with `set_legend(True)`. Labels come from `label=` kwargs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "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=r\"$\\sin(x)$\")\n", + "ax.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\")\n", + "ax.plot(x, np.sin(2 * x), color=\"seagreen\", label=r\"$\\sin(2x)$\", linestyle=\"dashed\")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_legend(True)\n", + "ax.set_title(\"Legend demo\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## 7 Β· annotate\n", + "\n", + "Draw an arrow from a text label to a data point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 2 * np.pi, 200)\n", "y = np.sin(x)\n", - "sp.add_line(x, y, label=r\"$\\sin(x)$\")\n", "\n", - "c.show()" + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "ax.plot(x, y, color=\"steelblue\")\n", + "\n", + "# Annotate the maximum\n", + "ax.annotate(\n", + " r\"maximum $\\approx 1$\",\n", + " xy=(np.pi / 2, 1.0),\n", + " xytext=(np.pi / 2 + 1.0, 0.7),\n", + " arrowprops=dict(arrowstyle=\"->\", color=\"black\"),\n", + " fontsize=9,\n", + " color=\"darkred\",\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(r\"$\\sin(x)$\")\n", + "ax.set_title(\"annotate demo\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## 8 Β· text\n", + "\n", + "Place a text label at arbitrary data coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(-2, 2, 200)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "ax.plot(x, x**2, color=\"darkorange\")\n", + "\n", + "ax.text(\n", + " 0, 3.2, r\"$f(x) = x^2$\", ha=\"center\", va=\"bottom\", fontsize=12, color=\"darkorange\"\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(r\"$f(x)$\")\n", + "ax.set_title(\"text demo\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## 9 Β· Aspect ratio\n", + "\n", + "`set_aspect('equal')` ensures a circle looks circular." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "theta = np.linspace(0, 2 * np.pi, 300)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"7cm\", ratio=1.0)\n", + "ax.plot(np.cos(theta), np.sin(theta), color=\"steelblue\", linewidth=2)\n", + "ax.set_aspect(\"equal\")\n", + "ax.set_title(\"Circle with equal aspect ratio\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "canvas.show()" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Method | Purpose |\n", + "|---|---|\n", + "| `set_xlabel` / `set_ylabel` | Axis labels (LaTeX OK) |\n", + "| `set_title` | Subplot title |\n", + "| `set_xlim` / `set_ylim` | Axis range |\n", + "| `set_xticks(pos, labels=)` | Custom tick marks |\n", + "| `set_yscale('log')` | Logarithmic scale |\n", + "| `set_grid(True)` | Background grid |\n", + "| `set_legend(True)` | Auto legend |\n", + "| `annotate(...)` | Arrow annotation |\n", + "| `text(x, y, s)` | Free text label |\n", + "| `set_aspect('equal')` | Equal x/y scaling |" ] } ], diff --git a/tutorials/tutorial_06.ipynb b/tutorials/tutorial_06.ipynb index 514274a..df85ad1 100644 --- a/tutorials/tutorial_06.ipynb +++ b/tutorials/tutorial_06.ipynb @@ -5,7 +5,9 @@ "id": "0", "metadata": {}, "source": [ - "# Tutorial 6" + "# Tutorial 06 – Layers\n", + "\n", + "**Layers** let you assign each piece of data to a numbered layer. You can then render a subset of layers β€” e.g. to reveal a derivation step by step, or to produce separate PDF overlays from a single source file." ] }, { @@ -16,32 +18,238 @@ "outputs": [], "source": [ "from maxplotlib import Canvas\n", - "import numpy as np\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## 1 Β· Assigning data to layers\n", "\n", - "%load_ext autoreload\n", - "%autoreload 2" + "Pass `layer=` to any plot call. Layers are just integer tags β€” the default is `0`." ] }, { "cell_type": "code", "execution_count": null, - "id": "2", + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 2 * np.pi, 200)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "\n", + "ax.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\", layer=0)\n", + "ax.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\", layer=1)\n", + "ax.plot(\n", + " x,\n", + " np.sin(x) * np.cos(x),\n", + " color=\"seagreen\",\n", + " label=r\"$\\sin(x)\\cos(x)$\",\n", + " linestyle=\"dashed\",\n", + " layer=2,\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_legend(True)\n", + "ax.set_title(\"Three curves on three layers\")\n", + "canvas.show() # renders all layers by default" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2 Β· Rendering specific layers\n", + "\n", + "Pass `layers=[...]` to `canvas.show()` or `canvas.savefig()` to render only a subset of layers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", "metadata": {}, "outputs": [], "source": [ - "c = Canvas(width=\"17cm\", ratio=0.5)\n", - "sp = c.add_subplot(grid=False, xlabel=\"x\", ylabel=\"y\")\n", - "# sp.add_line([0, 1, 2, 3], [0, 1, 4, 9], label=\"Line 1\",layer=1)\n", - "data = np.random.random((10, 10))\n", - "sp.add_imshow(data, extent=[1, 10, 1, 20], layer=1)\n", + "x = np.linspace(0, 2 * np.pi, 200)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "\n", + "ax.plot(x, np.sin(x), color=\"steelblue\", label=r\"$\\sin(x)$\", layer=0)\n", + "ax.plot(x, np.cos(x), color=\"tomato\", label=r\"$\\cos(x)$\", layer=1)\n", + "ax.plot(\n", + " x,\n", + " np.sin(x) * np.cos(x),\n", + " color=\"seagreen\",\n", + " label=r\"$\\sin(x)\\cos(x)$\",\n", + " linestyle=\"dashed\",\n", + " layer=2,\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_legend(True)\n", + "\n", + "print(\"--- Layer 0 only ---\")\n", + "ax.set_title(\"Layer 0 only\")\n", + "\n", + "canvas.show(layers=[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "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])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# All layers\n", + "ax.set_title(\"All layers\")\n", + "\n", + "canvas.show(layers=[0, 1, 2])" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 3 Β· Saving layer-by-layer\n", + "\n", + "`savefig` with `layer_by_layer=True` writes one file per layer (e.g. `fig_layer0.pdf`, `fig_layer1.pdf`, …). This is ideal for building slide animations or LaTeX overlays." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "# Demonstration β€” not executed to avoid writing files during tutorial\n", + "#\n", + "# canvas.savefig('fig.pdf', layer_by_layer=True)\n", + "#\n", + "# Produces:\n", + "# fig_layer0.pdf (layer 0 only)\n", + "# fig_layer1.pdf (layers 0–1)\n", + "# fig_layer2.pdf (layers 0–2, i.e. all)\n", + "#\n", + "# Each file is a cumulative reveal, suitable for \\includegraphics[<1->]{fig_layer0}\n", + "# in Beamer." + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 4 Β· Use case: progressively revealing a derivation\n", + "\n", + "Build a plot where each layer adds the next step of a mathematical derivation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 2 * np.pi, 300)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"11cm\", ratio=0.6)\n", + "\n", + "# Layer 0: raw data (noisy sine)\n", + "rng = np.random.default_rng(42)\n", + "y_data = np.sin(x) + rng.normal(0, 0.15, len(x))\n", + "ax.scatter(x, y_data, color=\"gray\", s=8, alpha=0.5, label=\"measured data\", layer=0)\n", + "\n", + "# Layer 1: true function\n", + "ax.plot(x, np.sin(x), color=\"steelblue\", linewidth=2, label=r\"true: $\\sin(x)$\", layer=1)\n", + "\n", + "# Layer 2: envelope\n", + "ax.fill_between(\n", + " x,\n", + " np.sin(x) - 0.15,\n", + " np.sin(x) + 0.15,\n", + " alpha=0.2,\n", + " color=\"steelblue\",\n", + " label=r\"$\\pm 0.15$ envelope\",\n", + " layer=2,\n", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_legend(True)\n", + "\n", + "# Step 1: only the data cloud\n", + "ax.set_title(\"Step 1 – raw data\")\n", + "canvas.show(layers=[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "# Step 2: add the true curve\n", + "ax.set_title(\"Step 2 – add true function\")\n", + "canvas.show(layers=[0, 1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "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])" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "| Concept | How |\n", + "|---|---|\n", + "| Assign to layer | `ax.plot(..., layer=1)` |\n", + "| Render subset | `canvas.show(layers=[0, 1])` |\n", + "| Save all layers | `canvas.savefig('fig.pdf', layer_by_layer=True)` |\n", + "| Default layer | `0` (omit `layer=` and it goes to layer 0) |\n", "\n", - "c.show()" + "Layers make it easy to build slide-deck animations or incremental pedagogical figures from a single Python file." ] } ], "metadata": { "kernelspec": { - "display_name": "env_maxpic", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -55,7 +263,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.13.3" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/tutorials/tutorial_07_tikz.ipynb b/tutorials/tutorial_07_tikz.ipynb new file mode 100644 index 0000000..4596e17 --- /dev/null +++ b/tutorials/tutorial_07_tikz.ipynb @@ -0,0 +1,719 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Tutorial 07 β€” TikZ Backend\n", + "\n", + "**TikZ** is the de-facto standard for drawing in LaTeX documents. \n", + "maxplotlib can render your figures as native TikZ code via the `tikzfigure` backend,\n", + "which wraps the [`tikzfigure`](https://github.com/max-models/tikzfigure) Python package.\n", + "\n", + "This tutorial covers **two complementary workflows**:\n", + "\n", + "| Workflow | When to use |\n", + "|---|---|\n", + "| **Canvas β†’ TikZ** | Quick way to turn data plots into LaTeX-ready TikZ code |\n", + "| **`tikzfigure` API directly** | Full control β€” nodes, shapes, annotations, arcs, colours … |\n", + "\n", + "**Prerequisites**\n", + "\n", + "```bash\n", + "pip install tikzfigure\n", + "```\n", + "\n", + "To actually *render* the figure (not just generate code) you also need `pdflatex` installed on your system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import tikzfigure as tz\n", + "from maxplotlib import Canvas" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "---\n", + "## Part 1 β€” Canvas β†’ TikZ\n", + "\n", + "The fastest path: build a plot with the standard Canvas API, then pass `backend='tikzfigure'` to get a `TikzFigure` object back." + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "### 1.1 Basic usage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "x = np.linspace(0, 2 * np.pi, 60)\n", + "\n", + "canvas, ax = Canvas.subplots(width=\"10cm\", ratio=0.6)\n", + "ax.plot(x, np.sin(x), label=\"sin\", color=\"steelblue\", line_width=1.5)\n", + "ax.plot(x, np.cos(x), label=\"cos\", color=\"tomato\", line_width=1.2)\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Trigonometric functions\")\n", + "\n", + "# backend='tikzfigure' returns a TikzFigure object\n", + "tikz = canvas.plot(backend=\"tikzfigure\")\n", + "print(type(tikz))" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "### 1.2 Inspecting the generated LaTeX\n", + "\n", + "`tikz.generate_tikz()` returns the raw LaTeX source string. \n", + "Each data line becomes a `\\draw` command connecting coordinate pairs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "code = tikz.generate_tikz()\n", + "print(code)" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "### 1.3 TikZ-specific kwargs\n", + "\n", + "The TikZ backend passes extra keyword arguments straight to `tikzfigure.draw()`. \n", + "Use **`line_width=`** (not matplotlib's `linewidth=`) to control stroke thickness." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "canvas2, ax2 = Canvas.subplots(width=\"10cm\", ratio=0.5)\n", + "ax2.plot(x, np.sin(x), color=\"navy\", line_width=0.5, label=\"thin\")\n", + "ax2.plot(x, np.sin(x) + 0.5, color=\"steelblue\", line_width=1.5, label=\"medium\")\n", + "ax2.plot(x, np.sin(x) + 1.0, color=\"royalblue\", line_width=3.0, label=\"thick\")\n", + "ax2.set_xlabel(\"x\")\n", + "ax2.set_title(\"Line width comparison\")\n", + "\n", + "tikz2 = canvas2.plot(backend=\"tikzfigure\")\n", + "print(tikz2.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "### 1.4 Layer-aware TikZ output\n", + "\n", + "Assign data to layers with `layer=N`. \n", + "The TikZ backend respects the layer filter β€” useful for generating incremental reveal figures (e.g. in Beamer)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "canvas3, ax3 = Canvas.subplots(width=\"10cm\", ratio=0.55)\n", + "ax3.plot(x, np.sin(x), color=\"steelblue\", line_width=1.5, layer=0, label=\"sin\")\n", + "ax3.plot(x, np.cos(x), color=\"tomato\", line_width=1.5, layer=1, label=\"cos\")\n", + "ax3.plot(\n", + " x, np.sin(x) * np.cos(x), color=\"seagreen\", line_width=1.0, layer=2, label=\"sinΒ·cos\"\n", + ")\n", + "\n", + "# All layers available on the canvas\n", + "print(\"Available layers:\", canvas3.layers)\n", + "\n", + "# Render only layer 0 β€” one \\draw command\n", + "tikz_l0 = canvas3.plot(backend=\"tikzfigure\", layers=[0])\n", + "print(\"\\n--- Layer 0 only ---\")\n", + "print(f'\\\\draw count: {tikz_l0.generate_tikz().count(chr(92)+\"draw\")}')\n", + "\n", + "# Render layers 0 and 1\n", + "tikz_l01 = canvas3.plot(backend=\"tikzfigure\", layers=[0, 1])\n", + "print(\"\\n--- Layers 0 & 1 ---\")\n", + "print(f'\\\\draw count: {tikz_l01.generate_tikz().count(chr(92)+\"draw\")}')" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### 1.5 Saving TikZ code to a file\n", + "\n", + "You can embed the generated code directly in a LaTeX document:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "tikz_all = canvas3.plot(backend=\"tikzfigure\")\n", + "\n", + "with open(\"figure.tex\", \"w\") as f:\n", + " f.write(tikz_all.generate_tikz())\n", + "\n", + "print(\"Saved figure.tex\")\n", + "\n", + "# In your LaTeX document:\n", + "# \\input{figure.tex}\n", + "# or wrap it:\n", + "# \\begin{figure}[h]\n", + "# \\centering\n", + "# \\input{figure.tex}\n", + "# \\caption{My caption}\n", + "# \\end{figure}" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "### 1.6 Rendering the figure (requires `pdflatex`)\n", + "\n", + "If `pdflatex` is installed, `tikz.show()` compiles the code and opens the PDF:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "# Requires pdflatex:\n", + "tikz_all.show(transparent=False)" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "### 1.7 Canvas β†’ TikZ limitations\n", + "\n", + "| Feature | Supported? |\n", + "|---|---|\n", + "| Line plots (`ax.plot`) | βœ… |\n", + "| Layer filtering | βœ… |\n", + "| `line_width=` kwarg | βœ… |\n", + "| Multiple subplots | ❌ (raises `NotImplementedError`) |\n", + "| `ax.scatter`, `ax.bar` | ❌ (silently ignored) |\n", + "| `ax.fill_between` | ❌ |\n", + "| Axis labels / titles | ❌ (TikZ has no axis frame by default) |\n", + "\n", + "For anything beyond line plots, use the `tikzfigure` API directly (Part 2 below)." + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "---\n", + "## Part 2 β€” The `tikzfigure` API\n", + "\n", + "The `tikzfigure` package gives you a full Python interface to TikZ primitives.\n", + "You build figures by adding nodes, paths, shapes, and annotations, then\n", + "call `generate_tikz()` (or `show()`) to obtain the output.\n", + "\n", + "```python\n", + "import tikzfigure as tz\n", + "tf = tz.TikzFigure()\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "### 2.1 Drawing paths with `draw()`\n", + "\n", + "`tf.draw(nodes, ...)` produces a `\\draw` path through a list of `(x, y)` coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "tf = tz.TikzFigure()\n", + "\n", + "x = np.linspace(0, 2 * np.pi, 60)\n", + "sin_nodes = [(float(xi), float(np.sin(xi))) for xi in x]\n", + "cos_nodes = [(float(xi), float(np.cos(xi))) for xi in x]\n", + "\n", + "tf.draw(sin_nodes, color=\"steelblue\", line_width=1.5)\n", + "tf.draw(cos_nodes, color=\"tomato\", line_width=1.2)\n", + "\n", + "print(tf.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "### 2.2 Straight line segments with `line()`\n", + "\n", + "`tf.line(start, end, ...)` is a convenience wrapper for a two-point path. \n", + "The `arrows` parameter adds arrowheads." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "tf2 = tz.TikzFigure()\n", + "\n", + "# Baseline\n", + "tf2.line((0, 0), (2 * np.pi, 0), color=\"gray\", dash_pattern=\"on 3pt off 3pt\")\n", + "\n", + "# Arrow showing direction\n", + "tf2.line((0, -1.2), (0, 1.2), color=\"black\", arrows=\"->\", line_width=0.8)\n", + "tf2.line((-0.2, 0), (2 * np.pi + 0.2, 0), color=\"black\", arrows=\"->\", line_width=0.8)\n", + "\n", + "# The curve\n", + "tf2.draw(sin_nodes, color=\"steelblue\", line_width=1.5)\n", + "\n", + "print(tf2.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "### 2.3 Rectangles, circles, and arcs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "tf3 = tz.TikzFigure()\n", + "\n", + "# Bounding rectangle (coordinate space for context)\n", + "tf3.rectangle((0, -1.2), (2 * np.pi, 1.2), draw=\"gray!40\", fill=\"gray!5\")\n", + "\n", + "# Circle at the origin\n", + "tf3.circle((0, 0), radius=0.15, fill=\"red!60\", draw=\"red\")\n", + "\n", + "# Circle at peak of sine\n", + "tf3.circle((np.pi / 2, 1.0), radius=0.12, fill=\"steelblue\", draw=\"none\")\n", + "\n", + "# Arc (quarter circle)\n", + "tf3.arc(\n", + " (0.4, 0),\n", + " start_angle=0,\n", + " end_angle=90,\n", + " radius=0.4,\n", + " draw=\"green!60!black\",\n", + " line_width=1.0,\n", + ")\n", + "\n", + "# The curve on top\n", + "tf3.draw(sin_nodes, color=\"steelblue\", line_width=1.5)\n", + "\n", + "print(tf3.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "23", + "metadata": {}, + "source": [ + "### 2.4 Nodes β€” text labels and markers\n", + "\n", + "`add_node()` places a text label (optionally inside a shape) at an `(x, y)` position." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "tf4 = tz.TikzFigure()\n", + "tf4.draw(sin_nodes, color=\"steelblue\", line_width=1.5)\n", + "\n", + "# Plain text label\n", + "tf4.add_node(np.pi / 2, 1.15, content=r\"$\\max$\", color=\"steelblue\")\n", + "\n", + "# Boxed label\n", + "tf4.add_node(\n", + " 3 * np.pi / 2,\n", + " -1.15,\n", + " content=r\"$\\min$\",\n", + " shape=\"rectangle\",\n", + " fill=\"tomato!20\",\n", + " draw=\"tomato\",\n", + " inner_sep=\"2pt\",\n", + ")\n", + "\n", + "# Circle marker at zero-crossing\n", + "tf4.add_node(\n", + " np.pi, 0, shape=\"circle\", fill=\"white\", draw=\"steelblue\", minimum_size=\"0.18cm\"\n", + ")\n", + "\n", + "print(tf4.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "### 2.5 Custom colours with `colorlet()`\n", + "\n", + "TikZ colour mixing syntax (`blue!70!white`) lets you define reusable named colours." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "tf5 = tz.TikzFigure()\n", + "\n", + "# Define named colours\n", + "tf5.colorlet(\"myblue\", \"blue!70!white\")\n", + "tf5.colorlet(\"myred\", \"red!80!black\")\n", + "tf5.colorlet(\"myfill\", \"blue!10!white\")\n", + "\n", + "# Use them in draw calls\n", + "tf5.draw(sin_nodes, color=\"myblue\", line_width=1.5)\n", + "tf5.draw(cos_nodes, color=\"myred\", line_width=1.5)\n", + "\n", + "# Filled polygon using the fill colour\n", + "closed_nodes = sin_nodes + [(float(x[-1]), 0.0), (float(x[0]), 0.0)]\n", + "tf5.draw(closed_nodes, fill=\"myfill\", draw=\"none\", cycle=True)\n", + "\n", + "print(tf5.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "### 2.6 Filled paths and patterns\n", + "\n", + "Pass `fill=` and/or `pattern=` to `draw()` to create shaded regions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "tf6 = tz.TikzFigure()\n", + "\n", + "# Shaded area under sin curve (closed path)\n", + "area_nodes = sin_nodes + [(float(x[-1]), 0.0), (float(x[0]), 0.0)]\n", + "tf6.draw(area_nodes, fill=\"steelblue!20\", draw=\"none\", cycle=True)\n", + "\n", + "# Hatched region using a pattern\n", + "cos_area = cos_nodes + [(float(x[-1]), 0.0), (float(x[0]), 0.0)]\n", + "tf6.draw(\n", + " cos_area,\n", + " pattern=\"north east lines\",\n", + " pattern_color=\"tomato\",\n", + " draw=\"none\",\n", + " cycle=True,\n", + ")\n", + "\n", + "# Curves on top\n", + "tf6.draw(sin_nodes, color=\"steelblue\", line_width=1.5)\n", + "tf6.draw(cos_nodes, color=\"tomato\", line_width=1.2)\n", + "\n", + "print(tf6.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "### 2.7 Layers in `TikzFigure`\n", + "\n", + "The `layer=` parameter on every drawing call controls render order.\n", + "Lower-numbered layers are drawn first (behind), higher layers on top." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "tf7 = tz.TikzFigure()\n", + "\n", + "# layer 0: background fill (drawn first)\n", + "tf7.rectangle((0, -1.2), (2 * np.pi, 1.2), fill=\"gray!8\", draw=\"gray!30\", layer=0)\n", + "\n", + "# layer 1: shaded area\n", + "area = sin_nodes + [(float(x[-1]), 0), (float(x[0]), 0)]\n", + "tf7.draw(area, fill=\"steelblue!25\", draw=\"none\", cycle=True, layer=1)\n", + "\n", + "# layer 2: the curve (drawn last, on top)\n", + "tf7.draw(sin_nodes, color=\"steelblue\", line_width=2.0, layer=2)\n", + "tf7.add_node(np.pi / 2, 1.15, content=r\"$\\sin(x)$\", color=\"steelblue\", layer=2)\n", + "\n", + "print(tf7.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "### 2.8 Escaping to raw TikZ code\n", + "\n", + "For anything not yet covered by the API, use `add_raw()` to inject verbatim TikZ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "tf8 = tz.TikzFigure()\n", + "tf8.draw(sin_nodes, color=\"steelblue\", line_width=1.5)\n", + "\n", + "# Inject custom TikZ β€” a dashed grid line\n", + "tf8.add_raw(r\"\\draw[gray!40, dashed] (0, 0) -- (6.28, 0);\")\n", + "\n", + "# Annotation with arrow using raw TikZ\n", + "tf8.add_raw(\n", + " r\"\\draw[->, gray] (1.0, 0.6) -- (1.57, 1.0) node[right, font=\\small] {peak};\"\n", + ")\n", + "\n", + "print(tf8.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "### 2.9 Putting it all together β€” a complete figure\n", + "\n", + "Combine paths, shapes, nodes, and colours into a single publication-ready figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "tf_final = tz.TikzFigure(figsize=(12, 7))\n", + "\n", + "# --- colours ---\n", + "tf_final.colorlet(\"cblue\", \"blue!65!white\")\n", + "tf_final.colorlet(\"cred\", \"red!75!black\")\n", + "\n", + "# --- background ---\n", + "tf_final.rectangle((0, -1.3), (2 * np.pi, 1.3), fill=\"gray!5\", draw=\"gray!30\")\n", + "\n", + "# --- zero axis ---\n", + "tf_final.line((0, 0), (2 * np.pi, 0), color=\"gray!60\", dash_pattern=\"on 2pt off 2pt\")\n", + "\n", + "# --- shaded area between curves ---\n", + "# approximate: shade where sin > cos (first half)\n", + "x_half = x[x <= np.pi]\n", + "upper = np.sin(x_half)\n", + "lower = np.cos(x_half)\n", + "region = [(float(xi), float(u)) for xi, u in zip(x_half, upper)] + [\n", + " (float(xi), float(l)) for xi, l in zip(reversed(x_half), reversed(lower))\n", + "]\n", + "tf_final.draw(region, fill=\"cblue!20\", draw=\"none\", cycle=True)\n", + "\n", + "# --- curves ---\n", + "tf_final.draw(sin_nodes, color=\"cblue\", line_width=1.8)\n", + "tf_final.draw(cos_nodes, color=\"cred\", line_width=1.5)\n", + "\n", + "# --- markers at key points ---\n", + "tf_final.circle((np.pi / 2, 1.0), radius=0.08, fill=\"cblue\", draw=\"none\")\n", + "tf_final.circle((np.pi, 0.0), radius=0.08, fill=\"cblue\", draw=\"none\")\n", + "tf_final.circle((0, 1.0), radius=0.08, fill=\"cred\", draw=\"none\")\n", + "\n", + "# --- labels ---\n", + "tf_final.add_node(\n", + " np.pi / 2 + 0.3, 1.05, content=r\"$\\sin(x)$\", color=\"cblue\", anchor=\"west\"\n", + ")\n", + "tf_final.add_node(0.2, 1.1, content=r\"$\\cos(x)$\", color=\"cred\", anchor=\"west\")\n", + "\n", + "# --- save and show ---\n", + "with open(\"complete_figure.tex\", \"w\") as f:\n", + " f.write(tf_final.generate_tikz())\n", + "print(\"Saved complete_figure.tex\")\n", + "print()\n", + "print(tf_final.generate_tikz())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "# Renders to PDF (requires pdflatex):\n", + "tf_final.show()" + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "### 2.10 Embedding in a LaTeX document\n", + "\n", + "The generated code is a standalone `tikzpicture` environment. \n", + "Drop it into any LaTeX document:\n", + "\n", + "```latex\n", + "\\usepackage{tikz}\n", + "\n", + "\\begin{figure}[h]\n", + " \\centering\n", + " \\input{complete_figure.tex}\n", + " \\caption{Trigonometric functions with shaded region.}\n", + " \\label{fig:trig}\n", + "\\end{figure}\n", + "```\n", + "\n", + "Or compile a standalone PDF with `tikzfigure`'s `generate_standalone()` method:\n", + "\n", + "```python\n", + "standalone_src = tf_final.generate_standalone()\n", + "with open('standalone.tex', 'w') as f:\n", + " f.write(standalone_src)\n", + "# Then: pdflatex standalone.tex\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "37", + "metadata": {}, + "source": [ + "---\n", + "## Summary\n", + "\n", + "### Canvas β†’ TikZ workflow\n", + "```python\n", + "canvas, ax = Canvas.subplots(width='10cm', ratio=0.6)\n", + "ax.plot(x, y, color='steelblue', line_width=1.5)\n", + "tikz = canvas.plot(backend='tikzfigure')\n", + "print(tikz.generate_tikz()) # inspect LaTeX\n", + "tikz.show() # render (needs pdflatex)\n", + "```\n", + "\n", + "### Direct `tikzfigure` API β€” key methods\n", + "\n", + "| Method | Purpose |\n", + "|---|---|\n", + "| `tf.draw(nodes, color=, line_width=, fill=, ...)` | Path through coordinate list |\n", + "| `tf.line(start, end, arrows='->', ...)` | Straight line segment |\n", + "| `tf.rectangle(corner1, corner2, fill=, draw=, ...)` | Rectangle |\n", + "| `tf.circle(center, radius, fill=, ...)` | Circle |\n", + "| `tf.arc(start, start_angle, end_angle, radius, ...)` | Arc |\n", + "| `tf.add_node(x, y, content=, shape=, fill=, ...)` | Labelled node |\n", + "| `tf.colorlet(name, color_expr)` | Define named colour |\n", + "| `tf.add_raw(tikz_code)` | Inject verbatim TikZ |\n", + "| `tf.generate_tikz()` | Return LaTeX string |\n", + "| `tf.show()` | Compile + display (needs `pdflatex`) |\n", + "\n", + "### TikZ colour syntax cheatsheet\n", + "| Expression | Meaning |\n", + "|---|---|\n", + "| `'red'`, `'blue'`, `'green'` | Standard colours |\n", + "| `'blue!70!white'` | 70% blue + 30% white |\n", + "| `'red!80!black'` | 80% red + 20% black |\n", + "| `'blue!50!red'` | 50% blend |\n", + "| `'gray!20'` | 20% gray (80% white) |" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/tutorial_07_tikzpics.ipynb b/tutorials/tutorial_07_tikzpics.ipynb deleted file mode 100644 index 4a9e42b..0000000 --- a/tutorials/tutorial_07_tikzpics.ipynb +++ /dev/null @@ -1,62 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Tutorial 6" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1", - "metadata": {}, - "outputs": [], - "source": [ - "from maxplotlib import Canvas\n", - "\n", - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2", - "metadata": {}, - "outputs": [], - "source": [ - "c = Canvas(width=\"17cm\", ratio=0.5)\n", - "sp = c.add_subplot(grid=False, xlabel=\"x\", ylabel=\"y\")\n", - "sp.add_line([0, 1, 2, 3], [0, 1, 0, 2], label=\"Line 1\", layer=1, line_width=2.0)\n", - "\n", - "\n", - "# TODO: Uncomment if pdflatex is installed\n", - "# c.show(backend=\"tikzfigure\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "env_maxpic", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/tutorials/tutorial_08_plotly.ipynb b/tutorials/tutorial_08_plotly.ipynb new file mode 100644 index 0000000..e42225c --- /dev/null +++ b/tutorials/tutorial_08_plotly.ipynb @@ -0,0 +1,395 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Tutorial 08 – Plotly Backend\n", + "\n", + "The **Plotly backend** renders your maxplotlib canvas as an interactive `plotly.graph_objects.Figure`. Unlike the default matplotlib output, Plotly figures:\n", + "\n", + "- Are **interactive** in Jupyter: zoom, pan, hover for values, toggle traces.\n", + "- Can be **exported as standalone HTML** files that work in any browserβ€”no Python or server required.\n", + "- Support multi-subplot layouts.\n", + "\n", + "### When to use Plotly vs matplotlib\n", + "\n", + "| Use case | Backend |\n", + "|---|---|\n", + "| Quick static plot / publication PDF | `matplotlib` (default) |\n", + "| Interactive exploration in Jupyter | `plotly` |\n", + "| Share a self-contained interactive report | `plotly` β†’ `fig.write_html(...)` |\n", + "| LaTeX document figure | `tikzfigure` |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "%matplotlib inline\n", + "\n", + "from maxplotlib import Canvas\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "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": "3", + "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)\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Basic line plot\")\n", + "\n", + "fig = canvas.plot(backend=\"plotly\")\n", + "print(type(fig)) # plotly.graph_objects.Figure\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## 2 Β· Multiple lines\n", + "\n", + "Each `ax.plot()` call becomes a separate Plotly trace. Enable the legend with `ax.set_legend(True)` so trace labels appear." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "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", + ")\n", + "\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_title(\"Multiple lines\")\n", + "ax.set_legend(True)\n", + "\n", + "fig = canvas.plot(backend=\"plotly\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## 3 Β· Scatter plot\n", + "\n", + "`ax.scatter()` maps to a Plotly scatter trace with markers only." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "rng = np.random.default_rng(42)\n", + "n = 120\n", + "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", + ")\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", + "\n", + "fig = canvas.plot(backend=\"plotly\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## 4 Β· Bar chart\n", + "\n", + "`ax.bar()` maps to a Plotly bar trace." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "categories = [\"Alpha\", \"Beta\", \"Gamma\", \"Delta\", \"Epsilon\"]\n", + "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", + "\n", + "ax.set_xlabel(\"Category\")\n", + "ax.set_ylabel(\"Value\")\n", + "ax.set_title(\"Bar chart\")\n", + "ax.set_legend(True)\n", + "\n", + "fig = canvas.plot(backend=\"plotly\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## 5 Β· Mixing lines and bars\n", + "\n", + "You can place multiple trace types on the same axes β€” Plotly handles the overlay automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "months = np.arange(1, 13)\n", + "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", + " months,\n", + " cumulative / 10,\n", + " color=\"tomato\",\n", + " linewidth=2.5,\n", + " marker=\"o\",\n", + " 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", + "\n", + "fig = canvas.plot(backend=\"plotly\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## 6 Β· Multiple subplots\n", + "\n", + "Multi-subplot canvases are fully supported. Each panel gets its own axis labels and title; `canvas.suptitle(...)` sets the figure-level title." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "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", + "\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", + "\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", + ")\n", + "ax2.set_xlabel(\"x\")\n", + "ax2.set_ylabel(\"y\")\n", + "ax2.set_title(\"Noisy observations\")\n", + "ax2.set_legend(True)\n", + "\n", + "canvas.suptitle(\"Multi-panel Plotly figure\")\n", + "\n", + "fig = canvas.plot(backend=\"plotly\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## 7 Β· Log scale\n", + "\n", + "`ax.set_yscale('log')` is passed through to Plotly's axis type." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "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", + "\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", + "\n", + "fig = canvas.plot(backend=\"plotly\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## 8 Β· Saving to HTML\n", + "\n", + "`fig.write_html()` saves a fully self-contained HTML file. The file works in any browser without Python, Plotly, or a running server β€” ideal for sharing interactive figures with colleagues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "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", + "\n", + "# Writes a standalone HTML file β€” open it in any browser\n", + "fig.write_html(\"output.html\")\n", + "print(\"Saved to output.html\")" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "### Plotly backend feature table\n", + "\n", + "| Feature | Supported | Notes |\n", + "|---|---|---|\n", + "| `ax.plot()` β€” line trace | βœ… | `color`, `linestyle`, `linewidth`, `marker` all passed through |\n", + "| `ax.scatter()` β€” markers | βœ… | `color`, `marker`, `s`, `alpha` |\n", + "| `ax.bar()` β€” bar chart | βœ… | `color`, `alpha` |\n", + "| `ax.fill_between()` | ❌ | Not supported by this backend |\n", + "| `ax.errorbar()` | ❌ | Not supported by this backend |\n", + "| `ax.axhline/axvline` | ❌ | Not supported by this backend |\n", + "| Multi-subplot canvas | βœ… | `Canvas.subplots(ncols=...)` etc. |\n", + "| `canvas.suptitle()` | βœ… | Maps to figure title |\n", + "| `ax.set_yscale('log')` | βœ… | |\n", + "| `ax.set_legend(True)` | βœ… | |\n", + "| `fig.show()` | βœ… | Interactive in Jupyter |\n", + "| `fig.write_html(path)` | βœ… | Standalone interactive HTML |\n", + "\n", + "### Typical workflow\n", + "\n", + "```python\n", + "from maxplotlib import Canvas\n", + "import numpy as np\n", + "\n", + "canvas, ax = Canvas.subplots()\n", + "ax.plot(x, y, label='data')\n", + "ax.set_legend(True)\n", + "\n", + "fig = canvas.plot(backend='plotly') # β†’ plotly.graph_objects.Figure\n", + "fig.show() # interactive in Jupyter\n", + "fig.write_html('report.html') # share with anyone\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env_maxplotlib", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}