Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion README.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,36 @@ Plot the figure with the default (matplotlib) backend:
canvas.show()
```

Alternatively, plot with the TikZ backend (not done yet):
Alternatively, plot with the TikZ backend:

```{python}
canvas.show(backend="tikzfigure")
```

### Horizontal Subplots with TikZ Backend

The tikzfigure backend supports creating side-by-side subplots (1×n layouts):

```{python}
#| label: fig-showcase-subplots
#| fig-width: 9
#| fig-height: 6

x = np.linspace(0, 2 * np.pi, 200)
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="10cm", ratio=0.3)

ax1.plot(x, np.sin(x), color="royalblue")
ax1.set_title("sin(x)")

ax2.plot(x, np.cos(x), color="tomato")
ax2.set_title("cos(x)")

canvas.suptitle("Trigonometric Functions")
canvas.show(backend="tikzfigure") # Generates LaTeX subfigures
```

**Note:** Only horizontal layouts (1×n) are currently supported with the tikzfigure backend. Vertical/grid layouts will raise `NotImplementedError`. See the tutorials for more examples.

### Layers

```{python}
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies = [
"matplotlib",
"pint",
"plotly",
"tikzfigure[vis]>=0.2.0",
"tikzfigure[vis]>=0.2.1",
]
[project.optional-dependencies]
test = [
Expand Down
83 changes: 76 additions & 7 deletions src/maxplotlib/canvas/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,9 @@ def show(
self.plot_plotly(savefig=False)
elif backend == "tikzfigure":
fig = self.plot_tikzfigure(savefig=False, verbose=verbose)
fig.show()
# TikzFigure handles all rendering (single or multi-subplot)
fig.show(transparent=False)
return fig
else:
raise ValueError("Invalid backend")

Expand Down Expand Up @@ -807,19 +809,86 @@ def plot_matplotlib(

def plot_tikzfigure(
self,
savefig: str | None = None,
savefig: bool = False,
verbose: bool = False,
) -> TikzFigure:
if len(self._subplot_dict) > 1:
"""
Generate a TikZ figure from subplots.

For now, returns the first subplot's TikzFigure.
Full multi-subplot support requires TikzFigure's subfigure_axis API.

Parameters:
verbose (bool): If True, print debug information.

Returns:
TikzFigure: Figure object that can be shown, saved, or compiled.
"""
if verbose:
print(f"Plotting tikzfigure with {len(self._subplot_dict)} subplot(s)")

# Check for unsupported layouts
if self.nrows > 1:
raise NotImplementedError(
"Only one subplot is supported for tikzfigure backend."
"Vertical/grid layouts (nrows > 1) are not yet supported for tikzfigure backend. "
"Use horizontal layouts (1×n) only."
)

# Validate that at least one subplot exists
if len(self._subplot_dict) == 0:
raise ValueError(
"No subplots to plot. Call add_subplot() or Canvas.subplots() first."
)

fig = TikzFigure()

# Add each subplot as a subfigure axis
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 = }")
tikz_subplot = line_plot.plot_tikzfigure(verbose=verbose)
return tikz_subplot

# Create subfigure axis with subplot metadata
ax = fig.subfigure_axis(
xlabel=line_plot._xlabel or "",
ylabel=line_plot._ylabel or "",
xlim=(
(line_plot._xmin, line_plot._xmax)
if line_plot._xmin is not None
else None
),
ylim=(
(line_plot._ymin, line_plot._ymax)
if line_plot._ymin is not None
else None
),
grid=line_plot._grid,
caption=line_plot._title or f"Subplot {col+1}",
width=0.45,
)

# Add each plot line to the subfigure
for line_data in line_plot.line_data:
if line_data.get("plot_type") == "plot":
# Extract and transform x, y data
x = (line_data["x"] + line_plot._xshift) * line_plot._xscale
y = (line_data["y"] + line_plot._yshift) * line_plot._yscale
kwargs = line_data.get("kwargs", {})
if verbose:
print(f"Line {kwargs = }")
# Add plot to subfigure axis
ax.add_plot(
x=x,
y=y,
# label=kwargs.get("label", ""),
color=kwargs.get("color", "black"),
line_width=kwargs.get("linewidth", 1.0),
)

# Add legend if requested
if line_plot._legend and len(line_plot.line_data) > 0:
ax.set_legend(position="north east")

return fig

def plot_plotly(self, show=True, savefig=None, usetex=False):
"""
Expand Down
75 changes: 75 additions & 0 deletions src/maxplotlib/tests/test_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,80 @@ def test():
pass


def test_canvas_plot_tikzfigure_horizontal_subplots():
"""Test that Canvas.plot_tikzfigure() works with horizontal (1×n) layouts."""
import numpy as np

from maxplotlib import Canvas

# Create a 1×2 canvas
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="10cm", ratio=0.3)

# Add data to both subplots
x = np.linspace(0, 2 * np.pi, 50)
ax1.plot(x, np.sin(x), label="sin(x)", color="royalblue")
ax1.set_title("Sine")
ax1.set_xlabel("x")
ax1.set_ylabel("y")

ax2.plot(x, np.cos(x), label="cos(x)", color="tomato")
ax2.set_title("Cosine")
ax2.set_xlabel("x")

canvas.suptitle("Trig Functions")

# This should NOT raise NotImplementedError
result = canvas.plot_tikzfigure(verbose=False)

# Result should be a TikzFigure or string containing LaTeX
assert result is not None


def test_canvas_plot_tikzfigure_three_subplots():
"""Test 1×3 layout with tikzfigure backend."""
import numpy as np

from maxplotlib import Canvas

x = np.linspace(0, 2 * np.pi, 50)
canvas, axes = Canvas.subplots(ncols=3, width="12cm", ratio=0.3)

axes[0].plot(x, np.sin(x), color="blue")
axes[0].set_title("Sin")

axes[1].plot(x, np.cos(x), color="red")
axes[1].set_title("Cos")

axes[2].plot(x, np.tan(x), color="green")
axes[2].set_title("Tan")

result = canvas.plot_tikzfigure()

assert result is not None
if isinstance(result, str):
assert "\\subfigure" in result or "subfigure" in result


def test_canvas_plot_tikzfigure_vertical_not_supported():
"""Test that vertical layouts raise NotImplementedError."""
import numpy as np
import pytest

from maxplotlib import Canvas

x = np.linspace(0, 2 * np.pi, 50)
# Create 2×1 layout (nrows=2)
canvas, axes = Canvas.subplots(nrows=2, width="10cm")

axes[0].plot(x, np.sin(x))
axes[1].plot(x, np.cos(x))

# Should raise NotImplementedError
with pytest.raises(NotImplementedError) as exc_info:
canvas.plot_tikzfigure()

assert "nrows > 1" in str(exc_info.value)


if __name__ == "__main__":
test()
Loading
Loading