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 +}