Skip to content

Commit 5db1b57

Browse files
authored
Implement subplots for tikzfigure backend (#40)
* Added side-by-side plots with tikzfigure * improve tutorial * Formatting * bump tikzfigure[vis]>=0.2.1
1 parent b2eff7e commit 5db1b57

6 files changed

Lines changed: 380 additions & 29 deletions

File tree

README.qmd

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,36 @@ Plot the figure with the default (matplotlib) backend:
4141
canvas.show()
4242
```
4343

44-
Alternatively, plot with the TikZ backend (not done yet):
44+
Alternatively, plot with the TikZ backend:
4545

4646
```{python}
4747
canvas.show(backend="tikzfigure")
4848
```
4949

50+
### Horizontal Subplots with TikZ Backend
51+
52+
The tikzfigure backend supports creating side-by-side subplots (1×n layouts):
53+
54+
```{python}
55+
#| label: fig-showcase-subplots
56+
#| fig-width: 9
57+
#| fig-height: 6
58+
59+
x = np.linspace(0, 2 * np.pi, 200)
60+
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="10cm", ratio=0.3)
61+
62+
ax1.plot(x, np.sin(x), color="royalblue")
63+
ax1.set_title("sin(x)")
64+
65+
ax2.plot(x, np.cos(x), color="tomato")
66+
ax2.set_title("cos(x)")
67+
68+
canvas.suptitle("Trigonometric Functions")
69+
canvas.show(backend="tikzfigure") # Generates LaTeX subfigures
70+
```
71+
72+
**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.
73+
5074
### Layers
5175

5276
```{python}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
"matplotlib",
1919
"pint",
2020
"plotly",
21-
"tikzfigure[vis]>=0.2.0",
21+
"tikzfigure[vis]>=0.2.1",
2222
]
2323
[project.optional-dependencies]
2424
test = [

src/maxplotlib/canvas/canvas.py

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,9 @@ def show(
735735
self.plot_plotly(savefig=False)
736736
elif backend == "tikzfigure":
737737
fig = self.plot_tikzfigure(savefig=False, verbose=verbose)
738-
fig.show()
738+
# TikzFigure handles all rendering (single or multi-subplot)
739+
fig.show(transparent=False)
740+
return fig
739741
else:
740742
raise ValueError("Invalid backend")
741743

@@ -807,19 +809,86 @@ def plot_matplotlib(
807809

808810
def plot_tikzfigure(
809811
self,
810-
savefig: str | None = None,
812+
savefig: bool = False,
811813
verbose: bool = False,
812814
) -> TikzFigure:
813-
if len(self._subplot_dict) > 1:
815+
"""
816+
Generate a TikZ figure from subplots.
817+
818+
For now, returns the first subplot's TikzFigure.
819+
Full multi-subplot support requires TikzFigure's subfigure_axis API.
820+
821+
Parameters:
822+
verbose (bool): If True, print debug information.
823+
824+
Returns:
825+
TikzFigure: Figure object that can be shown, saved, or compiled.
826+
"""
827+
if verbose:
828+
print(f"Plotting tikzfigure with {len(self._subplot_dict)} subplot(s)")
829+
830+
# Check for unsupported layouts
831+
if self.nrows > 1:
814832
raise NotImplementedError(
815-
"Only one subplot is supported for tikzfigure backend."
833+
"Vertical/grid layouts (nrows > 1) are not yet supported for tikzfigure backend. "
834+
"Use horizontal layouts (1×n) only."
835+
)
836+
837+
# Validate that at least one subplot exists
838+
if len(self._subplot_dict) == 0:
839+
raise ValueError(
840+
"No subplots to plot. Call add_subplot() or Canvas.subplots() first."
816841
)
842+
843+
fig = TikzFigure()
844+
845+
# Add each subplot as a subfigure axis
817846
for (row, col), line_plot in self._subplot_dict.items():
818847
if verbose:
819848
print(f"Plotting subplot at row {row}, col {col}")
820-
print(f"{line_plot = }")
821-
tikz_subplot = line_plot.plot_tikzfigure(verbose=verbose)
822-
return tikz_subplot
849+
850+
# Create subfigure axis with subplot metadata
851+
ax = fig.subfigure_axis(
852+
xlabel=line_plot._xlabel or "",
853+
ylabel=line_plot._ylabel or "",
854+
xlim=(
855+
(line_plot._xmin, line_plot._xmax)
856+
if line_plot._xmin is not None
857+
else None
858+
),
859+
ylim=(
860+
(line_plot._ymin, line_plot._ymax)
861+
if line_plot._ymin is not None
862+
else None
863+
),
864+
grid=line_plot._grid,
865+
caption=line_plot._title or f"Subplot {col+1}",
866+
width=0.45,
867+
)
868+
869+
# Add each plot line to the subfigure
870+
for line_data in line_plot.line_data:
871+
if line_data.get("plot_type") == "plot":
872+
# Extract and transform x, y data
873+
x = (line_data["x"] + line_plot._xshift) * line_plot._xscale
874+
y = (line_data["y"] + line_plot._yshift) * line_plot._yscale
875+
kwargs = line_data.get("kwargs", {})
876+
if verbose:
877+
print(f"Line {kwargs = }")
878+
# Add plot to subfigure axis
879+
ax.add_plot(
880+
x=x,
881+
y=y,
882+
# label=kwargs.get("label", ""),
883+
color=kwargs.get("color", "black"),
884+
line_width=kwargs.get("linewidth", 1.0),
885+
)
886+
887+
# Add legend if requested
888+
if line_plot._legend and len(line_plot.line_data) > 0:
889+
ax.set_legend(position="north east")
890+
891+
return fig
823892

824893
def plot_plotly(self, show=True, savefig=None, usetex=False):
825894
"""

src/maxplotlib/tests/test_canvas.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,80 @@ def test():
22
pass
33

44

5+
def test_canvas_plot_tikzfigure_horizontal_subplots():
6+
"""Test that Canvas.plot_tikzfigure() works with horizontal (1×n) layouts."""
7+
import numpy as np
8+
9+
from maxplotlib import Canvas
10+
11+
# Create a 1×2 canvas
12+
canvas, (ax1, ax2) = Canvas.subplots(ncols=2, width="10cm", ratio=0.3)
13+
14+
# Add data to both subplots
15+
x = np.linspace(0, 2 * np.pi, 50)
16+
ax1.plot(x, np.sin(x), label="sin(x)", color="royalblue")
17+
ax1.set_title("Sine")
18+
ax1.set_xlabel("x")
19+
ax1.set_ylabel("y")
20+
21+
ax2.plot(x, np.cos(x), label="cos(x)", color="tomato")
22+
ax2.set_title("Cosine")
23+
ax2.set_xlabel("x")
24+
25+
canvas.suptitle("Trig Functions")
26+
27+
# This should NOT raise NotImplementedError
28+
result = canvas.plot_tikzfigure(verbose=False)
29+
30+
# Result should be a TikzFigure or string containing LaTeX
31+
assert result is not None
32+
33+
34+
def test_canvas_plot_tikzfigure_three_subplots():
35+
"""Test 1×3 layout with tikzfigure backend."""
36+
import numpy as np
37+
38+
from maxplotlib import Canvas
39+
40+
x = np.linspace(0, 2 * np.pi, 50)
41+
canvas, axes = Canvas.subplots(ncols=3, width="12cm", ratio=0.3)
42+
43+
axes[0].plot(x, np.sin(x), color="blue")
44+
axes[0].set_title("Sin")
45+
46+
axes[1].plot(x, np.cos(x), color="red")
47+
axes[1].set_title("Cos")
48+
49+
axes[2].plot(x, np.tan(x), color="green")
50+
axes[2].set_title("Tan")
51+
52+
result = canvas.plot_tikzfigure()
53+
54+
assert result is not None
55+
if isinstance(result, str):
56+
assert "\\subfigure" in result or "subfigure" in result
57+
58+
59+
def test_canvas_plot_tikzfigure_vertical_not_supported():
60+
"""Test that vertical layouts raise NotImplementedError."""
61+
import numpy as np
62+
import pytest
63+
64+
from maxplotlib import Canvas
65+
66+
x = np.linspace(0, 2 * np.pi, 50)
67+
# Create 2×1 layout (nrows=2)
68+
canvas, axes = Canvas.subplots(nrows=2, width="10cm")
69+
70+
axes[0].plot(x, np.sin(x))
71+
axes[1].plot(x, np.cos(x))
72+
73+
# Should raise NotImplementedError
74+
with pytest.raises(NotImplementedError) as exc_info:
75+
canvas.plot_tikzfigure()
76+
77+
assert "nrows > 1" in str(exc_info.value)
78+
79+
580
if __name__ == "__main__":
681
test()

0 commit comments

Comments
 (0)