maxplotlibx 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ from typing import Dict
5
5
  import matplotlib.patches as patches
6
6
  import matplotlib.pyplot as plt
7
7
  from plotly.subplots import make_subplots
8
- from tikzpics import TikzFigure
8
+ from tikzfigure import TikzFigure
9
9
 
10
10
  from maxplotlib.backends.matplotlib.utils import (
11
11
  set_size,
@@ -18,6 +18,141 @@ from maxplotlib.subfigure.line_plot import LinePlot
18
18
  from maxplotlib.utils.options import Backends
19
19
 
20
20
 
21
+ def plot_matplotlib(tikzfigure: TikzFigure, ax, layers=None):
22
+ """
23
+ Plot all nodes and paths on the provided axis using Matplotlib.
24
+
25
+ Parameters:
26
+ - ax (matplotlib.axes.Axes): Axis on which to plot the figure.
27
+ """
28
+
29
+ # TODO: Specify which layers to retreive nodes from with layers=layers
30
+ nodes = tikzfigure.layers.get_nodes()
31
+ paths = tikzfigure.layers.get_paths()
32
+
33
+ for path in paths:
34
+ x_coords = [node.x for node in path.nodes]
35
+ y_coords = [node.y for node in path.nodes]
36
+
37
+ # Parse path color
38
+ path_color_spec = path.kwargs.get("color", "black")
39
+ try:
40
+ color = Color(path_color_spec).to_rgb()
41
+ except ValueError as e:
42
+ print(e)
43
+ color = "black"
44
+
45
+ # Parse line width
46
+ line_width_spec = path.kwargs.get("line_width", 1)
47
+ if isinstance(line_width_spec, str):
48
+ match = re.match(r"([\d.]+)(pt)?", line_width_spec)
49
+ if match:
50
+ line_width = float(match.group(1))
51
+ else:
52
+ print(
53
+ f"Invalid line width specification: '{line_width_spec}', defaulting to 1",
54
+ )
55
+ line_width = 1
56
+ else:
57
+ line_width = float(line_width_spec)
58
+
59
+ # Parse line style using Linestyle class
60
+ style_spec = path.kwargs.get("style", "solid")
61
+ linestyle = Linestyle(style_spec).to_matplotlib()
62
+
63
+ ax.plot(
64
+ x_coords,
65
+ y_coords,
66
+ color=color,
67
+ linewidth=line_width,
68
+ linestyle=linestyle,
69
+ zorder=1, # Lower z-order to place behind nodes
70
+ )
71
+
72
+ # Plot nodes after paths so they appear on top
73
+ for node in nodes:
74
+ # Determine shape and size
75
+ shape = node.kwargs.get("shape", "circle")
76
+ fill_color_spec = node.kwargs.get("fill", "white")
77
+ edge_color_spec = node.kwargs.get("draw", "black")
78
+ linewidth = float(node.kwargs.get("line_width", 1))
79
+ size = float(node.kwargs.get("size", 1))
80
+
81
+ # Parse colors using the Color class
82
+ try:
83
+ facecolor = Color(fill_color_spec).to_rgb()
84
+ except ValueError as e:
85
+ print(e)
86
+ facecolor = "white"
87
+
88
+ try:
89
+ edgecolor = Color(edge_color_spec).to_rgb()
90
+ except ValueError as e:
91
+ print(e)
92
+ edgecolor = "black"
93
+
94
+ # Plot shapes
95
+ if shape == "circle":
96
+ radius = size / 2
97
+ circle = patches.Circle(
98
+ (node.x, node.y),
99
+ radius,
100
+ facecolor=facecolor,
101
+ edgecolor=edgecolor,
102
+ linewidth=linewidth,
103
+ zorder=2, # Higher z-order to place on top of paths
104
+ )
105
+ ax.add_patch(circle)
106
+ elif shape == "rectangle":
107
+ width = height = size
108
+ rect = patches.Rectangle(
109
+ (node.x - width / 2, node.y - height / 2),
110
+ width,
111
+ height,
112
+ facecolor=facecolor,
113
+ edgecolor=edgecolor,
114
+ linewidth=linewidth,
115
+ zorder=2, # Higher z-order
116
+ )
117
+ ax.add_patch(rect)
118
+ else:
119
+ # Default to circle if shape is unknown
120
+ radius = size / 2
121
+ circle = patches.Circle(
122
+ (node.x, node.y),
123
+ radius,
124
+ facecolor=facecolor,
125
+ edgecolor=edgecolor,
126
+ linewidth=linewidth,
127
+ zorder=2,
128
+ )
129
+ ax.add_patch(circle)
130
+
131
+ # Add text inside the shape
132
+ if node.content:
133
+ ax.text(
134
+ node.x,
135
+ node.y,
136
+ node.content,
137
+ fontsize=10,
138
+ ha="center",
139
+ va="center",
140
+ wrap=True,
141
+ zorder=3, # Even higher z-order for text
142
+ )
143
+
144
+ # Remove axes, ticks, and legend
145
+ ax.axis("off")
146
+
147
+ # Adjust plot limits
148
+ all_x = [node.x for node in nodes]
149
+ all_y = [node.y for node in nodes]
150
+ padding = 1 # Adjust padding as needed
151
+ ax.set_xlim(min(all_x) - padding, max(all_x) + padding)
152
+ ax.set_ylim(min(all_y) - padding, max(all_y) + padding)
153
+ ax.set_aspect("equal", adjustable="datalim")
154
+
155
+
21
156
  class Canvas:
22
157
  def __init__(
23
158
  self,
@@ -29,7 +164,7 @@ class Canvas:
29
164
  label: str | None = None,
30
165
  fontsize: int = 14,
31
166
  dpi: int = 300,
32
- width: str = "17cm",
167
+ width: str = "5cm",
33
168
  ratio: str = "golden", # TODO Add literal
34
169
  gridspec_kw: Dict = {"wspace": 0.08, "hspace": 0.1},
35
170
  ):
@@ -62,6 +197,10 @@ class Canvas:
62
197
  self._ratio = ratio
63
198
  self._gridspec_kw = gridspec_kw
64
199
  self._plotted = False
200
+ self._matplotlib_fig = None
201
+ self._matplotlib_axes = None
202
+ self._suptitle: str | None = None
203
+ self._suptitle_kwargs: dict = {}
65
204
 
66
205
  # Dictionary to store lines for each subplot
67
206
  # Key: (row, col), Value: list of lines with their data and kwargs
@@ -70,14 +209,60 @@ class Canvas:
70
209
 
71
210
  self._subplot_matrix = [[None] * self.ncols for _ in range(self.nrows)]
72
211
 
212
+ # ------------------------------------------------------------------
213
+ # Factory
214
+ # ------------------------------------------------------------------
215
+
216
+ @classmethod
217
+ def subplots(
218
+ cls,
219
+ nrows: int = 1,
220
+ ncols: int = 1,
221
+ squeeze: bool = True,
222
+ **canvas_kwargs,
223
+ ):
224
+ """
225
+ Create a Canvas pre-filled with LinePlot subplots, mirroring
226
+ ``matplotlib.pyplot.subplots()``.
227
+
228
+ Parameters:
229
+ nrows, ncols (int): Grid dimensions.
230
+ squeeze (bool): If True, return a single subplot instead of a 1-element
231
+ list when the grid is 1×1 or when one dimension is 1.
232
+ **canvas_kwargs: Forwarded to the Canvas constructor.
233
+
234
+ Returns:
235
+ (canvas, axes): A tuple of the Canvas and either a single LinePlot,
236
+ a flat list (when one dimension is 1 and squeeze=True), or a
237
+ 2-D list of LinePlots.
238
+
239
+ Examples:
240
+ >>> canvas, ax = Canvas.subplots()
241
+ >>> canvas, (ax1, ax2) = Canvas.subplots(ncols=2)
242
+ >>> canvas, axes = Canvas.subplots(nrows=2, ncols=2) # axes[row][col]
243
+ """
244
+ canvas = cls(nrows=nrows, ncols=ncols, **canvas_kwargs)
245
+ axes = [
246
+ [canvas.add_subplot(row=r, col=c) for c in range(ncols)]
247
+ for r in range(nrows)
248
+ ]
249
+ if squeeze:
250
+ if nrows == 1 and ncols == 1:
251
+ return canvas, axes[0][0]
252
+ if nrows == 1:
253
+ return canvas, axes[0]
254
+ if ncols == 1:
255
+ return canvas, [row[0] for row in axes]
256
+ return canvas, axes
257
+
73
258
  @property
74
- def subplots(self):
259
+ def _subplot_dict(self):
75
260
  return self._subplots
76
261
 
77
262
  @property
78
263
  def layers(self):
79
264
  layers = []
80
- for (row, col), subplot in self.subplots.items():
265
+ for (row, col), subplot in self._subplot_dict.items():
81
266
  layers.extend(subplot.layers)
82
267
  return list(set(layers))
83
268
 
@@ -100,13 +285,12 @@ class Canvas:
100
285
 
101
286
  def add_line(
102
287
  self,
103
- x_data,
104
- y_data,
288
+ x,
289
+ y,
105
290
  layer=0,
106
291
  subplot: LinePlot | None = None,
107
292
  row: int | None = None,
108
293
  col: int | None = None,
109
- plot_type="plot",
110
294
  **kwargs,
111
295
  ):
112
296
  if row is not None and col is not None:
@@ -123,13 +307,258 @@ class Canvas:
123
307
  subplot = self.add_subplot(col=col, row=row)
124
308
 
125
309
  subplot.add_line(
126
- x_data=x_data,
127
- y_data=y_data,
310
+ x=x,
311
+ y=y,
128
312
  layer=layer,
129
- plot_type=plot_type,
130
313
  **kwargs,
131
314
  )
132
315
 
316
+ def _get_or_create_subplot(self, row, col):
317
+ """Return the subplot at (row, col), creating it if needed."""
318
+ if row is not None and col is not None:
319
+ try:
320
+ sp = self._subplot_matrix[row][col]
321
+ except (IndexError, KeyError):
322
+ raise ValueError("Invalid subplot position.")
323
+ else:
324
+ row, col = 0, 0
325
+ sp = self._subplot_matrix[row][col]
326
+ if sp is None:
327
+ row, col = self.generate_new_rowcol(row, col)
328
+ sp = self.add_subplot(col=col, row=row)
329
+ return sp
330
+
331
+ def scatter(
332
+ self,
333
+ x,
334
+ y,
335
+ layer=0,
336
+ row: int | None = None,
337
+ col: int | None = None,
338
+ **kwargs,
339
+ ):
340
+ """
341
+ Add a scatter plot to the canvas (matplotlib-style convenience method).
342
+
343
+ Parameters:
344
+ x (array-like): X-axis data.
345
+ y (array-like): Y-axis data.
346
+ layer (int): Layer index (default 0).
347
+ row, col (int): Subplot position (default top-left).
348
+ **kwargs: Forwarded to the backend (e.g., color, marker, s, label).
349
+ """
350
+ sp = self._get_or_create_subplot(row, col)
351
+ sp.scatter(x, y, layer=layer, **kwargs)
352
+
353
+ def bar(
354
+ self,
355
+ x,
356
+ height,
357
+ layer=0,
358
+ row: int | None = None,
359
+ col: int | None = None,
360
+ **kwargs,
361
+ ):
362
+ """
363
+ Add a bar chart to the canvas (matplotlib-style convenience method).
364
+
365
+ Parameters:
366
+ x (array-like): X positions of the bars.
367
+ height (array-like): Heights of the bars.
368
+ layer (int): Layer index (default 0).
369
+ row, col (int): Subplot position (default top-left).
370
+ **kwargs: Forwarded to the backend (e.g., color, width, label).
371
+ """
372
+ sp = self._get_or_create_subplot(row, col)
373
+ sp.bar(x, height, layer=layer, **kwargs)
374
+
375
+ def set_xlabel(self, label: str, row: int | None = None, col: int | None = None):
376
+ """Set the x-axis label for a subplot (default top-left)."""
377
+ self._get_or_create_subplot(row, col).set_xlabel(label)
378
+
379
+ def set_ylabel(self, label: str, row: int | None = None, col: int | None = None):
380
+ """Set the y-axis label for a subplot (default top-left)."""
381
+ self._get_or_create_subplot(row, col).set_ylabel(label)
382
+
383
+ def set_title(self, title: str, row: int | None = None, col: int | None = None):
384
+ """Set the title for a subplot (default top-left)."""
385
+ self._get_or_create_subplot(row, col).set_title(title)
386
+
387
+ def set_xlim(
388
+ self, left=None, right=None, row: int | None = None, col: int | None = None
389
+ ):
390
+ """Set the x-axis limits for a subplot (default top-left)."""
391
+ self._get_or_create_subplot(row, col).set_xlim(left, right)
392
+
393
+ def set_ylim(
394
+ self, bottom=None, top=None, row: int | None = None, col: int | None = None
395
+ ):
396
+ """Set the y-axis limits for a subplot (default top-left)."""
397
+ self._get_or_create_subplot(row, col).set_ylim(bottom, top)
398
+
399
+ def set_grid(
400
+ self, visible: bool = True, row: int | None = None, col: int | None = None
401
+ ):
402
+ """Show or hide the grid for a subplot (default top-left)."""
403
+ self._get_or_create_subplot(row, col).set_grid(visible)
404
+
405
+ def set_legend(
406
+ self, visible: bool = True, row: int | None = None, col: int | None = None
407
+ ):
408
+ """Show or hide the legend for a subplot (default top-left)."""
409
+ self._get_or_create_subplot(row, col).set_legend(visible)
410
+
411
+ def set_xscale(self, scale: str, row: int | None = None, col: int | None = None):
412
+ """Set x-axis scale ('linear', 'log', 'symlog') for a subplot."""
413
+ self._get_or_create_subplot(row, col).set_xscale(scale)
414
+
415
+ def set_yscale(self, scale: str, row: int | None = None, col: int | None = None):
416
+ """Set y-axis scale ('linear', 'log', 'symlog') for a subplot."""
417
+ self._get_or_create_subplot(row, col).set_yscale(scale)
418
+
419
+ def set_xticks(
420
+ self, ticks, labels=None, row: int | None = None, col: int | None = None
421
+ ):
422
+ """Set x-axis tick positions (and optional labels) for a subplot."""
423
+ self._get_or_create_subplot(row, col).set_xticks(ticks, labels)
424
+
425
+ def set_yticks(
426
+ self, ticks, labels=None, row: int | None = None, col: int | None = None
427
+ ):
428
+ """Set y-axis tick positions (and optional labels) for a subplot."""
429
+ self._get_or_create_subplot(row, col).set_yticks(ticks, labels)
430
+
431
+ def fill_between(
432
+ self,
433
+ x,
434
+ y1,
435
+ y2=0,
436
+ layer=0,
437
+ row: int | None = None,
438
+ col: int | None = None,
439
+ **kwargs,
440
+ ):
441
+ """Fill the region between two curves on a subplot."""
442
+ self._get_or_create_subplot(row, col).fill_between(
443
+ x, y1, y2, layer=layer, **kwargs
444
+ )
445
+
446
+ def errorbar(
447
+ self,
448
+ x,
449
+ y,
450
+ yerr=None,
451
+ xerr=None,
452
+ layer=0,
453
+ row: int | None = None,
454
+ col: int | None = None,
455
+ **kwargs,
456
+ ):
457
+ """Add an error-bar line to a subplot."""
458
+ self._get_or_create_subplot(row, col).errorbar(
459
+ x, y, yerr=yerr, xerr=xerr, layer=layer, **kwargs
460
+ )
461
+
462
+ def axhline(
463
+ self, y=0, layer=0, row: int | None = None, col: int | None = None, **kwargs
464
+ ):
465
+ """Add a full-width horizontal reference line to a subplot."""
466
+ self._get_or_create_subplot(row, col).axhline(y=y, layer=layer, **kwargs)
467
+
468
+ def axvline(
469
+ self, x=0, layer=0, row: int | None = None, col: int | None = None, **kwargs
470
+ ):
471
+ """Add a full-height vertical reference line to a subplot."""
472
+ self._get_or_create_subplot(row, col).axvline(x=x, layer=layer, **kwargs)
473
+
474
+ def hlines(
475
+ self,
476
+ y,
477
+ xmin,
478
+ xmax,
479
+ layer=0,
480
+ row: int | None = None,
481
+ col: int | None = None,
482
+ **kwargs,
483
+ ):
484
+ """Add horizontal lines at specified y positions to a subplot."""
485
+ self._get_or_create_subplot(row, col).hlines(
486
+ y, xmin, xmax, layer=layer, **kwargs
487
+ )
488
+
489
+ def vlines(
490
+ self,
491
+ x,
492
+ ymin,
493
+ ymax,
494
+ layer=0,
495
+ row: int | None = None,
496
+ col: int | None = None,
497
+ **kwargs,
498
+ ):
499
+ """Add vertical lines at specified x positions to a subplot."""
500
+ self._get_or_create_subplot(row, col).vlines(
501
+ x, ymin, ymax, layer=layer, **kwargs
502
+ )
503
+
504
+ def annotate(
505
+ self,
506
+ text,
507
+ xy,
508
+ xytext=None,
509
+ layer=0,
510
+ row: int | None = None,
511
+ col: int | None = None,
512
+ **kwargs,
513
+ ):
514
+ """Add an annotation (with optional arrow) to a subplot."""
515
+ self._get_or_create_subplot(row, col).annotate(
516
+ text, xy, xytext=xytext, layer=layer, **kwargs
517
+ )
518
+
519
+ def text(
520
+ self, x, y, s, layer=0, row: int | None = None, col: int | None = None, **kwargs
521
+ ):
522
+ """Add a text label at (x, y) on a subplot."""
523
+ self._get_or_create_subplot(row, col).text(x, y, s, layer=layer, **kwargs)
524
+
525
+ # ------------------------------------------------------------------
526
+ # Multi-subplot helpers
527
+ # ------------------------------------------------------------------
528
+
529
+ def subplot(self, row: int = 0, col: int = 0) -> LinePlot:
530
+ """
531
+ Return the LinePlot at position (row, col).
532
+
533
+ Raises ValueError if no subplot has been created there yet.
534
+ """
535
+ sp = self._subplot_matrix[row][col]
536
+ if sp is None:
537
+ raise ValueError(
538
+ f"No subplot at ({row}, {col}). "
539
+ "Call add_subplot() or use Canvas.subplots() first."
540
+ )
541
+ return sp
542
+
543
+ def iter_subplots(self):
544
+ """Yield (row, col, subplot) for every initialized subplot, row-major."""
545
+ for r in range(self.nrows):
546
+ for c in range(self.ncols):
547
+ sp = self._subplot_matrix[r][c]
548
+ if sp is not None:
549
+ yield r, c, sp
550
+
551
+ def suptitle(self, title: str, **kwargs):
552
+ """
553
+ Set a figure-level title (rendered above all subplots).
554
+
555
+ Parameters:
556
+ title (str): Title text.
557
+ **kwargs: Forwarded to matplotlib's fig.suptitle (e.g., fontsize, y).
558
+ """
559
+ self._suptitle = title
560
+ self._suptitle_kwargs = kwargs
561
+
133
562
  def add_tikzfigure(
134
563
  self,
135
564
  col=None,
@@ -280,14 +709,15 @@ class Canvas:
280
709
  )
281
710
  elif backend == "plotly":
282
711
  return self.plot_plotly(savefig=savefig)
283
- elif backend == "tikzpics":
284
- return self.plot_tikzpics(savefig=savefig)
712
+ elif backend == "tikzfigure":
713
+ return self.plot_tikzfigure(savefig=savefig)
285
714
  else:
286
715
  raise ValueError(f"Invalid backend: {backend}")
287
716
 
288
717
  def show(
289
718
  self,
290
719
  backend: Backends = "matplotlib",
720
+ layers: list | None = None,
291
721
  verbose: bool = False,
292
722
  ):
293
723
  if verbose:
@@ -297,14 +727,14 @@ class Canvas:
297
727
  self.plot(
298
728
  backend="matplotlib",
299
729
  savefig=False,
300
- layers=None,
730
+ layers=layers,
301
731
  verbose=verbose,
302
732
  )
303
733
  # self._matplotlib_fig.show()
304
734
  elif backend == "plotly":
305
735
  self.plot_plotly(savefig=False)
306
- elif backend == "tikzpics":
307
- fig = self.plot_tikzpics(savefig=False)
736
+ elif backend == "tikzfigure":
737
+ fig = self.plot_tikzfigure(savefig=False, verbose=verbose)
308
738
  fig.show()
309
739
  else:
310
740
  raise ValueError("Invalid backend")
@@ -357,7 +787,7 @@ class Canvas:
357
787
  dpi=self.dpi,
358
788
  )
359
789
 
360
- for (row, col), subplot in self.subplots.items():
790
+ for (row, col), subplot in self._subplot_dict.items():
361
791
  ax = axes[row][col]
362
792
  if isinstance(subplot, TikzFigure):
363
793
  plot_matplotlib(subplot, ax, layers=layers)
@@ -366,26 +796,29 @@ class Canvas:
366
796
  # ax.set_title(f"Subplot ({row}, {col})")
367
797
  ax.grid()
368
798
 
799
+ if self._suptitle:
800
+ fig.suptitle(self._suptitle, **self._suptitle_kwargs)
801
+
369
802
  # Set caption, labels, etc., if needed
370
803
  self._plotted = True
371
804
  self._matplotlib_fig = fig
372
805
  self._matplotlib_axes = axes
373
806
  return fig, axes
374
807
 
375
- def plot_tikzpics(
808
+ def plot_tikzfigure(
376
809
  self,
377
- savefig=None,
378
- verbose=False,
810
+ savefig: str | None = None,
811
+ verbose: bool = False,
379
812
  ) -> TikzFigure:
380
- if len(self.subplots) > 1:
813
+ if len(self._subplot_dict) > 1:
381
814
  raise NotImplementedError(
382
- "Only one subplot is supported for tikzpics backend."
815
+ "Only one subplot is supported for tikzfigure backend."
383
816
  )
384
- for (row, col), line_plot in self.subplots.items():
817
+ for (row, col), line_plot in self._subplot_dict.items():
385
818
  if verbose:
386
819
  print(f"Plotting subplot at row {row}, col {col}")
387
820
  print(f"{line_plot = }")
388
- tikz_subplot = line_plot.plot_tikzpics(verbose=verbose)
821
+ tikz_subplot = line_plot.plot_tikzfigure(verbose=verbose)
389
822
  return tikz_subplot
390
823
 
391
824
  def plot_plotly(self, show=True, savefig=None, usetex=False):
@@ -416,31 +849,45 @@ class Canvas:
416
849
  rows=self.nrows,
417
850
  cols=self.ncols,
418
851
  subplot_titles=[
419
- f"Subplot ({row}, {col})" for (row, col) in self.subplots.keys()
852
+ sp._title or f"({row}, {col})"
853
+ for (row, col), sp in self._subplot_dict.items()
420
854
  ],
421
855
  )
422
856
 
423
- # Plot each subplot
424
- for (row, col), line_plot in self.subplots.items():
425
- traces = line_plot.plot_plotly() # Generate Plotly traces for the line_plot
857
+ # Plot each subplot and propagate axis labels/scale
858
+ axis_index = 1
859
+ for (row, col), line_plot in self._subplot_dict.items():
860
+ traces = line_plot.plot_plotly()
426
861
  for trace in traces:
427
862
  fig.add_trace(trace, row=row + 1, col=col + 1)
428
863
 
864
+ # Axis label keys are "xaxis", "xaxis2", "xaxis3", ...
865
+ xkey = "xaxis" if axis_index == 1 else f"xaxis{axis_index}"
866
+ ykey = "yaxis" if axis_index == 1 else f"yaxis{axis_index}"
867
+ layout_patch = {}
868
+ if line_plot._xlabel:
869
+ layout_patch[xkey] = {"title": {"text": line_plot._xlabel}}
870
+ if line_plot._ylabel:
871
+ layout_patch[ykey] = {"title": {"text": line_plot._ylabel}}
872
+ if line_plot._xaxis_scale == "log":
873
+ layout_patch.setdefault(xkey, {})["type"] = "log"
874
+ if line_plot._yaxis_scale == "log":
875
+ layout_patch.setdefault(ykey, {})["type"] = "log"
876
+ if layout_patch:
877
+ fig.update_layout(**layout_patch)
878
+ axis_index += 1
879
+
429
880
  # Update layout settings
430
881
  fig.update_layout(
431
- # width=fig_width,
432
- # height=fig_height,
433
882
  font=dict(size=self.fontsize),
434
- margin=dict(l=10, r=10, t=40, b=10), # Adjust margins if needed
883
+ margin=dict(l=10, r=10, t=40, b=10),
435
884
  )
885
+ if self._suptitle:
886
+ fig.update_layout(title=dict(text=self._suptitle, x=0.5))
436
887
 
437
- # Optionally save the figure
438
888
  if savefig:
439
889
  fig.write_image(savefig)
440
890
 
441
- # Show or return the figure
442
- # if show:
443
- # fig.show()
444
891
  return fig
445
892
 
446
893
  # Property getters
@@ -507,13 +954,6 @@ class Canvas:
507
954
  def figsize(self, value):
508
955
  self._figsize = value
509
956
 
510
- # Magic methods
511
- def __str__(self):
512
- return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, figsize={self.figsize})"
513
-
514
- def __repr__(self):
515
- return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, caption={self.caption}, label={self.label})"
516
-
517
957
  def __getitem__(self, key):
518
958
  """Allows accessing subplots by tuple index."""
519
959
  row, col = key
@@ -528,145 +968,17 @@ class Canvas:
528
968
  raise IndexError("Subplot index out of range")
529
969
  self._subplot_matrix[row][col] = value
530
970
 
971
+ def __repr__(self):
972
+ return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, caption={self.caption}, label={self.label})"
531
973
 
532
- def plot_matplotlib(tikzfigure: TikzFigure, ax, layers=None):
533
- """
534
- Plot all nodes and paths on the provided axis using Matplotlib.
535
-
536
- Parameters:
537
- - ax (matplotlib.axes.Axes): Axis on which to plot the figure.
538
- """
539
-
540
- # TODO: Specify which layers to retreive nodes from with layers=layers
541
- nodes = tikzfigure.layers.get_nodes()
542
- paths = tikzfigure.layers.get_paths()
543
-
544
- for path in paths:
545
- x_coords = [node.x for node in path.nodes]
546
- y_coords = [node.y for node in path.nodes]
547
-
548
- # Parse path color
549
- path_color_spec = path.kwargs.get("color", "black")
550
- try:
551
- color = Color(path_color_spec).to_rgb()
552
- except ValueError as e:
553
- print(e)
554
- color = "black"
555
-
556
- # Parse line width
557
- line_width_spec = path.kwargs.get("line_width", 1)
558
- if isinstance(line_width_spec, str):
559
- match = re.match(r"([\d.]+)(pt)?", line_width_spec)
560
- if match:
561
- line_width = float(match.group(1))
562
- else:
563
- print(
564
- f"Invalid line width specification: '{line_width_spec}', defaulting to 1",
565
- )
566
- line_width = 1
567
- else:
568
- line_width = float(line_width_spec)
569
-
570
- # Parse line style using Linestyle class
571
- style_spec = path.kwargs.get("style", "solid")
572
- linestyle = Linestyle(style_spec).to_matplotlib()
573
-
574
- ax.plot(
575
- x_coords,
576
- y_coords,
577
- color=color,
578
- linewidth=line_width,
579
- linestyle=linestyle,
580
- zorder=1, # Lower z-order to place behind nodes
581
- )
582
-
583
- # Plot nodes after paths so they appear on top
584
- for node in nodes:
585
- # Determine shape and size
586
- shape = node.kwargs.get("shape", "circle")
587
- fill_color_spec = node.kwargs.get("fill", "white")
588
- edge_color_spec = node.kwargs.get("draw", "black")
589
- linewidth = float(node.kwargs.get("line_width", 1))
590
- size = float(node.kwargs.get("size", 1))
591
-
592
- # Parse colors using the Color class
593
- try:
594
- facecolor = Color(fill_color_spec).to_rgb()
595
- except ValueError as e:
596
- print(e)
597
- facecolor = "white"
598
-
599
- try:
600
- edgecolor = Color(edge_color_spec).to_rgb()
601
- except ValueError as e:
602
- print(e)
603
- edgecolor = "black"
604
-
605
- # Plot shapes
606
- if shape == "circle":
607
- radius = size / 2
608
- circle = patches.Circle(
609
- (node.x, node.y),
610
- radius,
611
- facecolor=facecolor,
612
- edgecolor=edgecolor,
613
- linewidth=linewidth,
614
- zorder=2, # Higher z-order to place on top of paths
615
- )
616
- ax.add_patch(circle)
617
- elif shape == "rectangle":
618
- width = height = size
619
- rect = patches.Rectangle(
620
- (node.x - width / 2, node.y - height / 2),
621
- width,
622
- height,
623
- facecolor=facecolor,
624
- edgecolor=edgecolor,
625
- linewidth=linewidth,
626
- zorder=2, # Higher z-order
627
- )
628
- ax.add_patch(rect)
629
- else:
630
- # Default to circle if shape is unknown
631
- radius = size / 2
632
- circle = patches.Circle(
633
- (node.x, node.y),
634
- radius,
635
- facecolor=facecolor,
636
- edgecolor=edgecolor,
637
- linewidth=linewidth,
638
- zorder=2,
639
- )
640
- ax.add_patch(circle)
641
-
642
- # Add text inside the shape
643
- if node.content:
644
- ax.text(
645
- node.x,
646
- node.y,
647
- node.content,
648
- fontsize=10,
649
- ha="center",
650
- va="center",
651
- wrap=True,
652
- zorder=3, # Even higher z-order for text
653
- )
654
-
655
- # Remove axes, ticks, and legend
656
- ax.axis("off")
657
-
658
- # Adjust plot limits
659
- all_x = [node.x for node in nodes]
660
- all_y = [node.y for node in nodes]
661
- padding = 1 # Adjust padding as needed
662
- ax.set_xlim(min(all_x) - padding, max(all_x) + padding)
663
- ax.set_ylim(min(all_y) - padding, max(all_y) + padding)
664
- ax.set_aspect("equal", adjustable="datalim")
974
+ # Magic methods
975
+ def __str__(self):
976
+ return f"Canvas(nrows={self.nrows}, ncols={self.ncols}, figsize={self.figsize})"
665
977
 
666
978
 
667
979
  if __name__ == "__main__":
668
980
  c = Canvas(ncols=2, nrows=2)
669
981
  sp = c.add_subplot()
670
- sp.add_line("Line 1", [0, 1, 2, 3], [0, 1, 4, 9])
671
- c.plot()
982
+ sp.plot([0, 1, 2, 3], [0, 1, 4, 9], label="Line 1")
983
+ c.plot(backend="matplotlib")
672
984
  print("done")