hilbertplot 0.0.0__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.
@@ -0,0 +1,413 @@
1
+ """The :class:`HilbertPlot` object -- data laid on a curve, rendered on demand.
2
+
3
+ A :class:`HilbertPlot` just holds a curve and a data vector; it draws nothing until
4
+ you ask. Get one from :meth:`hilbertplot.curve.Curve.plot_data` or the
5
+ :func:`hilbert_plot` shortcut, then render with :meth:`HilbertPlot.show` /
6
+ :meth:`~HilbertPlot.save` / :meth:`~HilbertPlot.draw`, choosing the colormap,
7
+ granularity and power-of-four fit at that point (not at construction).
8
+
9
+ Colormaps
10
+ ---------
11
+ ``cmap`` accepts either a matplotlib colormap **name** (``"viridis"``, ``"gray"`` ...)
12
+ or a **list of colours** to interpolate smoothly between and spread across the data's
13
+ min-max range, e.g. ``["red", "green", "tab:blue"]`` sweeps red -> green -> blue.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import numpy as np
19
+
20
+ from .._core.curve import Curve
21
+ from .._core.curve import curve as _lookup_curve
22
+ from .._data.mapping import granulate, resolve_order, scatter, square_side
23
+
24
+ __all__ = ["HilbertPlot", "hilbert_plot"]
25
+
26
+
27
+ def _resolve_cmap(cmap):
28
+ """Turn ``cmap`` (a name or a list of colours) into a matplotlib Colormap.
29
+
30
+ NaN-filled (unmapped) cells are made transparent so padding never shows.
31
+ """
32
+ from matplotlib import colormaps
33
+ from matplotlib.colors import Colormap, LinearSegmentedColormap
34
+
35
+ if isinstance(cmap, Colormap):
36
+ resolved = cmap
37
+ elif isinstance(cmap, str):
38
+ resolved = colormaps[cmap]
39
+ else: # a sequence of colour specs to interpolate through
40
+ colors = list(cmap)
41
+ if not colors:
42
+ raise ValueError("colormap colour list is empty")
43
+ if len(colors) == 1:
44
+ colors = colors * 2
45
+ resolved = LinearSegmentedColormap.from_list("hilbertplot", colors)
46
+ # Make NaN-filled (unmapped) cells transparent so padding never shows.
47
+ return resolved.with_extremes(bad=(0.0, 0.0, 0.0, 0.0))
48
+
49
+
50
+ def _overlay_difference(ax, dmap, side, threshold, color):
51
+ """Paint the strong-locality barrier of a difference map onto ``ax``.
52
+
53
+ The barrier is the set of cells whose value exceeds ``mean + threshold * std``;
54
+ all other cells stay transparent.
55
+ """
56
+ from matplotlib.colors import to_rgba
57
+
58
+ barrier = dmap > dmap.mean() + threshold * dmap.std()
59
+ overlay = np.zeros((side, side, 4))
60
+ overlay[barrier] = to_rgba(color)
61
+ ax.imshow(
62
+ overlay,
63
+ origin="lower",
64
+ interpolation="nearest",
65
+ extent=(-0.5, side - 0.5, -0.5, side - 0.5),
66
+ zorder=2,
67
+ )
68
+
69
+
70
+ class HilbertPlot:
71
+ """A 1-D data vector bound to a curve, ready to be rendered as a Hilbert plot.
72
+
73
+ Holds only the curve and the data and draws nothing on construction; the colormap,
74
+ granularity and power-of-four fit are all chosen at render time, so one object gives
75
+ you every variant. Construct via :meth:`hilbertplot.curve.Curve.plot_data` or
76
+ :func:`hilbert_plot`.
77
+
78
+ Parameters
79
+ ----------
80
+ curve : Curve
81
+ The curve whose path lays out the data.
82
+ data : array_like
83
+ The 1-D sequence to visualise.
84
+
85
+ Examples
86
+ --------
87
+ >>> import numpy as np, hilbertplot
88
+ >>> plot = hilbertplot.hilbert_plot(0, np.arange(1, 101))
89
+ >>> plot.show(["red", "green", "tab:blue"]) # doctest: +SKIP
90
+ >>> plot.image().shape # 100 values -> tight 10x10 square (Hilbert generalizes)
91
+ (10, 10)
92
+ """
93
+
94
+ __slots__ = ("_curve", "_data")
95
+
96
+ def __init__(self, curve: Curve, data) -> None:
97
+ if not isinstance(curve, Curve):
98
+ raise TypeError(f"curve must be a Curve, got {type(curve).__name__}")
99
+ self._curve = curve
100
+ # own a private, read-only copy so the plot never aliases the caller's array
101
+ self._data = np.array(data, dtype=float).ravel()
102
+ self._data.flags.writeable = False
103
+
104
+ @property
105
+ def curve(self) -> Curve:
106
+ """The curve the data is mapped through."""
107
+ return self._curve
108
+
109
+ @property
110
+ def data(self) -> np.ndarray:
111
+ """The 1-D data vector, as a read-only array (a private copy of the input)."""
112
+ return self._data
113
+
114
+ def __len__(self) -> int:
115
+ return self._data.size
116
+
117
+ # -- the mapping ---------------------------------------------------------
118
+ def image(
119
+ self,
120
+ *,
121
+ granularity: int = 1,
122
+ order: int | None = None,
123
+ fit: str = "auto",
124
+ fill: float = np.nan,
125
+ ) -> np.ndarray:
126
+ """Return the 2-D image of the data mapped through the curve.
127
+
128
+ Parameters
129
+ ----------
130
+ granularity : int, default 1
131
+ Coarsen the data with the ``l``-granularity transform first (``1`` = none).
132
+ order : int, optional
133
+ Force a ``2**order`` grid (implies a power-of-two fit).
134
+ fit : {"auto", "square", "pad", "truncate"}, default "auto"
135
+ How to size the grid. ``"auto"`` uses ``"square"`` when the curve
136
+ :attr:`~hilbertplot.Curve.generalizes` (a tight ``ceil(sqrt(N))`` square, no
137
+ power-of-two waste) and ``"pad"`` otherwise. ``"square"`` forces the tight
138
+ square (only for generalizing curves). ``"pad"`` uses the smallest enclosing
139
+ ``2**k`` grid; ``"truncate"`` the largest ``2**k`` grid that fits inside the
140
+ data.
141
+ fill : float, default numpy.nan
142
+ Value for unmapped cells (``NaN`` renders transparent).
143
+
144
+ Returns
145
+ -------
146
+ numpy.ndarray
147
+ A ``side`` by ``side`` float image, indexed ``[y, x]``.
148
+
149
+ Examples
150
+ --------
151
+ >>> import numpy as np, hilbertplot
152
+ >>> hilbertplot.hilbert_plot(0, np.arange(10000)).image().shape # tight square
153
+ (100, 100)
154
+ >>> hilbertplot.hilbert_plot(0, np.arange(64)).image(order=3).shape
155
+ (8, 8)
156
+ """
157
+ values, points, side = self._layout(granularity, order, fit)
158
+ return scatter(points, values, side, fill)
159
+
160
+ def fourier(
161
+ self,
162
+ *,
163
+ granularity: int = 1,
164
+ order: int | None = None,
165
+ fit: str = "auto",
166
+ log: bool = True,
167
+ window: bool = False,
168
+ ) -> np.ndarray:
169
+ """Return the 2-D Fourier map of this plot -- the centred power spectrum of the image.
170
+
171
+ Periodic and self-similar structure in the data appears as bright peaks and
172
+ symmetric patterns (the "Fourier map" of Estevez-Rams et al., 2015).
173
+
174
+ Parameters
175
+ ----------
176
+ granularity, order, fit
177
+ Passed to :meth:`image` to build the data image that is transformed.
178
+ log : bool, default True
179
+ Return ``log(1 + |F|)`` so faint structure is visible.
180
+ window : bool, default False
181
+ Apply a Hann window first to suppress edge artifacts.
182
+
183
+ Returns
184
+ -------
185
+ numpy.ndarray
186
+ The centred 2-D spectrum, same shape as :meth:`image`.
187
+
188
+ Examples
189
+ --------
190
+ >>> import numpy as np, hilbertplot
191
+ >>> spec = hilbertplot.hilbert_plot(0, np.tile([1, 0], 512)).fourier() # ·10· pattern
192
+ >>> spec.shape
193
+ (32, 32)
194
+ """
195
+ from .._data.mapping import fourier_spectrum
196
+
197
+ img = self.image(granularity=granularity, order=order, fit=fit, fill=np.nan)
198
+ return fourier_spectrum(img, log=log, window=window)
199
+
200
+ def _layout(self, granularity, order, fit):
201
+ """Resolve (values, curve points, grid side) for a given granularity/order/fit."""
202
+ values = self._data
203
+ if granularity != 1:
204
+ values = granulate(values, granularity)
205
+ n = values.size
206
+ effective = fit
207
+ if order is None and fit == "auto":
208
+ effective = "square" if self._curve.generalizes else "pad"
209
+ if order is None and effective == "square":
210
+ if not self._curve.generalizes:
211
+ raise ValueError(
212
+ f"fit='square' needs a generalizing curve; {self._curve.name} is not "
213
+ f"one (see hilbertplot.generalizing()). Use fit='pad' or 'truncate'."
214
+ )
215
+ side = square_side(n)
216
+ return values, self._curve.grid(side), side
217
+ pot_fit = "pad" if effective in ("auto", "square") else effective
218
+ order = resolve_order(n, order, pot_fit)
219
+ return values, self._curve.points(order), 2**order
220
+
221
+ # -- rendering -----------------------------------------------------------
222
+ def draw(
223
+ self,
224
+ cmap="viridis",
225
+ *,
226
+ granularity: int = 1,
227
+ order: int | None = None,
228
+ fit: str = "auto",
229
+ fill: float = np.nan,
230
+ ax=None,
231
+ vmin=None,
232
+ vmax=None,
233
+ title: str | None = None,
234
+ difference: bool = False,
235
+ difference_threshold: float = 1.0,
236
+ difference_color="#2b6cff",
237
+ fourier: bool = False,
238
+ window: bool = False,
239
+ **imshow_kw,
240
+ ):
241
+ """Render onto ``ax`` (created if ``None``) and return the axes -- no window.
242
+
243
+ Parameters
244
+ ----------
245
+ cmap : str or list of color, default "viridis"
246
+ A matplotlib colormap **name**, or a **list of colours** to interpolate
247
+ smoothly between (e.g. ``["red", "green", "tab:blue"]`` sweeps red to blue).
248
+ granularity, order, fit, fill
249
+ Passed to :meth:`image`.
250
+ ax : matplotlib.axes.Axes, optional
251
+ Axes to draw on; a new figure is created when ``None``.
252
+ vmin, vmax : float, optional
253
+ Colour limits; default to the data's min/max so the colours span the full
254
+ value range.
255
+ title : str, optional
256
+ Axes title; a default naming the curve and grid size is used when ``None``.
257
+ difference : bool, default False
258
+ Overlay the curve's strong-locality barrier -- the cells of the difference
259
+ map above ``mean + difference_threshold * std`` -- marking where the curve
260
+ breaks locality (see :meth:`hilbertplot.curve.Curve.difference_map`).
261
+ difference_threshold : float, default 1.0
262
+ Std multiplier for the barrier: **higher marks fewer, worse sites**; lower
263
+ marks more.
264
+ difference_color : color, default "#2b6cff"
265
+ Colour of the barrier overlay.
266
+ fourier : bool, default False
267
+ Render the **2-D Fourier map** (centred power spectrum) instead of the data
268
+ image, revealing periodic/self-similar structure as bright peaks. The
269
+ ``difference`` overlay is ignored in this mode.
270
+ window : bool, default False
271
+ (Fourier mode) apply a Hann window first to suppress edge artifacts.
272
+ **imshow_kw
273
+ Extra keyword arguments forwarded to ``matplotlib``'s ``imshow``.
274
+
275
+ Returns
276
+ -------
277
+ matplotlib.axes.Axes
278
+
279
+ Examples
280
+ --------
281
+ >>> import numpy as np, hilbertplot
282
+ >>> plot = hilbertplot.hilbert_plot(0, np.arange(4096))
283
+ >>> ax = plot.draw("gray", difference=True) # doctest: +SKIP
284
+ >>> ax = plot.draw("magma", fourier=True) # doctest: +SKIP
285
+ """
286
+ from .._data.mapping import difference_map, fourier_spectrum
287
+ from .render import pyplot
288
+
289
+ plt = pyplot()
290
+ values, points, side = self._layout(granularity, order, fit)
291
+ image = scatter(points, values, side, fill)
292
+ if fourier:
293
+ image = fourier_spectrum(image, log=True, window=window)
294
+ difference = False # not meaningful on a spectrum
295
+ if ax is None:
296
+ _, ax = plt.subplots(figsize=(5, 5))
297
+ if fourier and (vmin is None or vmax is None):
298
+ # percentile contrast so faint spectral structure is visible by default
299
+ lo, hi = np.percentile(image, [50.0, 99.8])
300
+ vmin = lo if vmin is None else vmin
301
+ vmax = (hi if hi > lo else image.max()) if vmax is None else vmax
302
+ if vmin is None:
303
+ vmin = np.nanmin(image)
304
+ if vmax is None:
305
+ vmax = np.nanmax(image)
306
+ ax.imshow(
307
+ image,
308
+ origin="lower",
309
+ interpolation="nearest",
310
+ cmap=_resolve_cmap(cmap),
311
+ vmin=vmin,
312
+ vmax=vmax,
313
+ extent=(-0.5, side - 0.5, -0.5, side - 0.5),
314
+ **imshow_kw,
315
+ )
316
+ if difference:
317
+ _overlay_difference(
318
+ ax, difference_map(points, side), side, difference_threshold, difference_color
319
+ )
320
+ ax.set_aspect("equal")
321
+ ax.set_xticks([])
322
+ ax.set_yticks([])
323
+ for spine in ax.spines.values():
324
+ spine.set_visible(False)
325
+ if title is None:
326
+ gran = "" if granularity == 1 else f", {granularity}-granularity"
327
+ kind = "Fourier map" if fourier else "Hilbert plot"
328
+ title = f"{self._curve.name} {kind} ({side}x{side}{gran})"
329
+ ax.set_title(title, fontsize=9)
330
+ return ax
331
+
332
+ def show(self, cmap="viridis", **kwargs):
333
+ """Draw the plot and open a window.
334
+
335
+ Parameters
336
+ ----------
337
+ cmap : str or list of color, default "viridis"
338
+ See :meth:`draw`.
339
+ **kwargs
340
+ Every :meth:`draw` option (``granularity``, ``fit``, ``vmin``, ...).
341
+
342
+ Returns
343
+ -------
344
+ matplotlib.axes.Axes
345
+ """
346
+ from .render import pyplot
347
+
348
+ ax = self.draw(cmap, **kwargs)
349
+ pyplot().show()
350
+ return ax
351
+
352
+ def save(self, path: str, cmap="viridis", *, dpi: int = 140, **kwargs) -> str:
353
+ """Draw the plot, save it to ``path`` and return ``path``.
354
+
355
+ Parameters
356
+ ----------
357
+ path : str
358
+ Output file path (format inferred from the extension).
359
+ cmap : str or list of color, default "viridis"
360
+ See :meth:`draw`.
361
+ dpi : int, default 140
362
+ Output resolution.
363
+ **kwargs
364
+ Every :meth:`draw` option.
365
+
366
+ Returns
367
+ -------
368
+ str
369
+ ``path``.
370
+
371
+ Examples
372
+ --------
373
+ >>> import numpy as np, hilbertplot
374
+ >>> hilbertplot.hilbert_plot(0, np.arange(1, 101)).save(
375
+ ... "out.png", ["red", "green", "tab:blue"]) # doctest: +SKIP
376
+ 'out.png'
377
+ """
378
+ from .render import pyplot
379
+
380
+ ax = self.draw(cmap, **kwargs)
381
+ ax.figure.savefig(path, dpi=dpi, bbox_inches="tight")
382
+ pyplot().close(ax.figure)
383
+ return path
384
+
385
+ def __repr__(self) -> str:
386
+ return f"<HilbertPlot: {len(self)} values on {self._curve.name}>"
387
+
388
+
389
+ def hilbert_plot(curve: Curve | int | str, data) -> HilbertPlot:
390
+ """Make a :class:`HilbertPlot` straight from a curve and data.
391
+
392
+ A shortcut for ``hilbertplot.curve(which).plot_data(data)`` that skips the intermediate
393
+ curve object.
394
+
395
+ Parameters
396
+ ----------
397
+ curve : Curve or int or str
398
+ A :class:`Curve`, or anything :func:`hilbertplot.curve` accepts (index, name or symbol).
399
+ data : array_like
400
+ The 1-D sequence to visualise.
401
+
402
+ Returns
403
+ -------
404
+ HilbertPlot
405
+
406
+ Examples
407
+ --------
408
+ >>> import hilbertplot
409
+ >>> plot = hilbertplot.hilbert_plot(0, range(1024)) # curve 0 (Hilbert)
410
+ >>> plot.show(["red", "green", "tab:blue"]) # doctest: +SKIP
411
+ """
412
+ c = curve if isinstance(curve, Curve) else _lookup_curve(curve)
413
+ return HilbertPlot(c, data)
@@ -0,0 +1,193 @@
1
+ """Matplotlib rendering, kept behind the :class:`~hilbertplot._core.curve.Curve` methods.
2
+
3
+ Nothing else in the package imports matplotlib, and it is imported lazily only when a
4
+ plot is actually drawn, so the maths runs with numpy alone. Users never call into
5
+ this module directly -- they use ``curve.show(order)`` / ``curve.save(order, path)`` /
6
+ ``curve.plot(order)`` and :func:`gallery`.
7
+
8
+ Two render modes, chosen automatically by order:
9
+
10
+ * ``"line"`` draws the actual polyline. At low orders this is colour-graded along the
11
+ curve (via a ``LineCollection``) so the traversal order is visible; that technique
12
+ is one path per stroke, so it is only used up to order 7.
13
+ * ``"fill"`` paints a ``2**order`` by ``2**order`` image whose pixels are coloured by
14
+ visit order and shows it with ``imshow``. This is O(cells) and stays fast for
15
+ millions of points -- and at high orders, where individual strokes are sub-pixel,
16
+ it is also the *only* legible view.
17
+
18
+ ``mode="auto"`` (the default) uses ``line`` below order 8 and ``fill`` at or above it.
19
+ Passing an explicit ``color=`` always draws a single fast solid polyline.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import numpy as np
25
+
26
+ # At/above this order individual strokes are sub-pixel: switch to the fill image.
27
+ _AUTO_FILL_ORDER = 8
28
+ # Per-stroke gradient colouring (LineCollection) is only practical up to this order.
29
+ _GRADIENT_MAX_ORDER = 7
30
+ _DEFAULT_COLOR = "#1f4e79"
31
+
32
+
33
+ def pyplot():
34
+ """Return ``matplotlib.pyplot`` with a friendly error if matplotlib is missing."""
35
+ try:
36
+ import matplotlib.pyplot as plt
37
+ except ModuleNotFoundError as exc: # pragma: no cover - environment dependent
38
+ raise ModuleNotFoundError(
39
+ "Plotting needs matplotlib. Install it with "
40
+ "'uv pip install hilbertplot[plot]' (or 'pip install matplotlib')."
41
+ ) from exc
42
+ return plt
43
+
44
+
45
+ def _draw_fill(ax, pts, side, cmap):
46
+ img = np.empty((side, side), dtype=np.float32)
47
+ img[pts[:, 1], pts[:, 0]] = np.linspace(0.0, 1.0, len(pts), dtype=np.float32)
48
+ ax.imshow(
49
+ img,
50
+ cmap=cmap,
51
+ origin="lower",
52
+ interpolation="nearest",
53
+ extent=(-0.5, side - 0.5, -0.5, side - 0.5),
54
+ )
55
+
56
+
57
+ def _draw_line(ax, pts, order, color, cmap, linewidth):
58
+ if linewidth is None:
59
+ linewidth = max(0.15, 3.0 - 0.35 * order)
60
+ if color is None and order <= _GRADIENT_MAX_ORDER:
61
+ from matplotlib.collections import LineCollection
62
+
63
+ segs = np.stack([pts[:-1], pts[1:]], axis=1)
64
+ lc = LineCollection(
65
+ segs, cmap=cmap, linewidth=linewidth, capstyle="round", joinstyle="round"
66
+ )
67
+ lc.set_array(np.linspace(0.0, 1.0, len(segs)))
68
+ ax.add_collection(lc)
69
+ ax.autoscale_view()
70
+ else:
71
+ ax.plot(
72
+ pts[:, 0],
73
+ pts[:, 1],
74
+ color=color or _DEFAULT_COLOR,
75
+ linewidth=linewidth,
76
+ solid_capstyle="round",
77
+ solid_joinstyle="round",
78
+ )
79
+
80
+
81
+ def draw(
82
+ curve,
83
+ order: int,
84
+ ax=None,
85
+ *,
86
+ mode: str = "auto",
87
+ color: str | None = None,
88
+ cmap: str = "viridis",
89
+ linewidth: float | None = None,
90
+ endpoints: bool = True,
91
+ title: str | None = None,
92
+ ):
93
+ """Draw one curve at ``order`` onto ``ax`` (created if ``None``); return the axes."""
94
+ plt = pyplot()
95
+ pts = curve.points(order)
96
+ side = 2**order
97
+ if ax is None:
98
+ _, ax = plt.subplots(figsize=(5, 5))
99
+ if mode == "auto":
100
+ mode = "fill" if order >= _AUTO_FILL_ORDER else "line"
101
+
102
+ if mode == "fill":
103
+ _draw_fill(ax, pts, side, cmap)
104
+ elif mode == "line":
105
+ _draw_line(ax, pts, order, color, cmap, linewidth)
106
+ if endpoints:
107
+ ms = max(2.0, 6.0 - 0.5 * order)
108
+ ax.plot(*pts[0], "o", color="black", markersize=ms, zorder=3) # entry
109
+ ax.plot(*pts[-1], "s", color="black", markersize=ms, zorder=3) # exit
110
+ else:
111
+ raise ValueError(f"mode must be 'auto', 'line' or 'fill', got {mode!r}")
112
+
113
+ ax.set_xlim(-0.75, side - 0.25)
114
+ ax.set_ylim(-0.75, side - 0.25)
115
+ ax.set_aspect("equal")
116
+ ax.set_xticks([])
117
+ ax.set_yticks([])
118
+ for spine in ax.spines.values():
119
+ spine.set_visible(False)
120
+ if title is None:
121
+ title = f"{curve.symbol} ({curve.name}), order {order}"
122
+ ax.set_title(title, fontsize=9)
123
+ return ax
124
+
125
+
126
+ def gallery(
127
+ curves,
128
+ order=3,
129
+ *,
130
+ ncols=None,
131
+ save=None,
132
+ mode="auto",
133
+ cmap="viridis",
134
+ color=None,
135
+ title=None,
136
+ dpi=140,
137
+ ):
138
+ """Draw a grid of curves; show it, or save it if ``save=`` is given.
139
+
140
+ Parameters
141
+ ----------
142
+ curves : iterable of Curve
143
+ The curves to draw, e.g. ``hilbertplot.homogeneous()`` or ``hilbertplot.by_kernels(3, 5)``.
144
+ order : int, default 3
145
+ Order at which to draw every curve.
146
+ ncols : int, optional
147
+ Number of columns; chosen automatically when ``None``.
148
+ save : str, optional
149
+ If given, save the figure to this path and close it; otherwise open a window.
150
+ mode : {"auto", "line", "fill"}, default "auto"
151
+ Render mode passed to each panel (see :meth:`hilbertplot.curve.Curve.plot`).
152
+ cmap : str, default "viridis"
153
+ Colormap for the gradient/fill rendering.
154
+ color : str, optional
155
+ Solid line colour (overrides the gradient) for every panel.
156
+ title : str, optional
157
+ Overall figure title.
158
+ dpi : int, default 140
159
+ Resolution used when ``save`` is given.
160
+
161
+ Returns
162
+ -------
163
+ matplotlib.figure.Figure
164
+
165
+ Examples
166
+ --------
167
+ >>> import hilbertplot
168
+ >>> hilbertplot.gallery(hilbertplot.improper(), order=3, save="improper.png") # doctest: +SKIP
169
+ """
170
+ plt = pyplot()
171
+ curves = list(curves)
172
+ if not curves:
173
+ raise ValueError("no curves to plot")
174
+ if ncols is None:
175
+ ncols = min(6, len(curves))
176
+ nrows = -(-len(curves) // ncols)
177
+
178
+ fig, axes = plt.subplots(nrows, ncols, figsize=(2.2 * ncols, 2.2 * nrows), squeeze=False)
179
+ flat = axes.ravel()
180
+ for ax, c in zip(flat, curves, strict=False):
181
+ draw(c, order, ax=ax, mode=mode, cmap=cmap, color=color, endpoints=False)
182
+ for ax in flat[len(curves) :]:
183
+ ax.set_visible(False)
184
+ if title:
185
+ fig.suptitle(title, fontsize=12)
186
+ fig.tight_layout()
187
+
188
+ if save is not None:
189
+ fig.savefig(save, dpi=dpi, bbox_inches="tight")
190
+ plt.close(fig)
191
+ else:
192
+ plt.show()
193
+ return fig
hilbertplot/py.typed ADDED
File without changes