anyplotlib 0.1.0b1__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.
Files changed (42) hide show
  1. anyplotlib/__init__.py +53 -0
  2. anyplotlib/_base_plot.py +229 -0
  3. anyplotlib/_electron.py +74 -0
  4. anyplotlib/_repr_utils.py +345 -0
  5. anyplotlib/_utils.py +194 -0
  6. anyplotlib/axes/__init__.py +6 -0
  7. anyplotlib/axes/_axes.py +510 -0
  8. anyplotlib/axes/_inset_axes.py +126 -0
  9. anyplotlib/callbacks.py +328 -0
  10. anyplotlib/embed.py +194 -0
  11. anyplotlib/figure/__init__.py +7 -0
  12. anyplotlib/figure/_figure.py +633 -0
  13. anyplotlib/figure/_gridspec.py +99 -0
  14. anyplotlib/figure/_subplots.py +100 -0
  15. anyplotlib/figure_esm.js +6011 -0
  16. anyplotlib/markers.py +704 -0
  17. anyplotlib/plot1d/__init__.py +6 -0
  18. anyplotlib/plot1d/_plot1d.py +1376 -0
  19. anyplotlib/plot1d/_plotbar.py +436 -0
  20. anyplotlib/plot2d/__init__.py +5 -0
  21. anyplotlib/plot2d/_plot2d.py +726 -0
  22. anyplotlib/plot2d/_plotmesh.py +116 -0
  23. anyplotlib/plot3d/__init__.py +4 -0
  24. anyplotlib/plot3d/_plot3d.py +524 -0
  25. anyplotlib/plotxy/__init__.py +4 -0
  26. anyplotlib/plotxy/_plotxy.py +273 -0
  27. anyplotlib/sphinx_anywidget/__init__.py +177 -0
  28. anyplotlib/sphinx_anywidget/_directive.py +245 -0
  29. anyplotlib/sphinx_anywidget/_repr_utils.py +298 -0
  30. anyplotlib/sphinx_anywidget/_scraper.py +390 -0
  31. anyplotlib/sphinx_anywidget/_wheel_builder.py +84 -0
  32. anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +1099 -0
  33. anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +100 -0
  34. anyplotlib/widgets/__init__.py +18 -0
  35. anyplotlib/widgets/_base.py +218 -0
  36. anyplotlib/widgets/_widgets1d.py +109 -0
  37. anyplotlib/widgets/_widgets2d.py +141 -0
  38. anyplotlib/widgets/_widgets3d.py +50 -0
  39. anyplotlib-0.1.0b1.dist-info/METADATA +160 -0
  40. anyplotlib-0.1.0b1.dist-info/RECORD +42 -0
  41. anyplotlib-0.1.0b1.dist-info/WHEEL +4 -0
  42. anyplotlib-0.1.0b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,510 @@
1
+ """
2
+ axes/_axes.py
3
+ =============
4
+ Grid-cell container that owns a single plot panel.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ import numpy as np
12
+
13
+ from anyplotlib.plot1d import Plot1D, PlotBar
14
+ from anyplotlib.plot2d import Plot2D, PlotMesh
15
+ from anyplotlib.plot3d import Plot3D
16
+
17
+ if TYPE_CHECKING:
18
+ from anyplotlib.figure import Figure
19
+ from anyplotlib.figure._gridspec import SubplotSpec
20
+
21
+
22
+ class Axes:
23
+ """A single grid cell in a Figure.
24
+
25
+ Returned by Figure.add_subplot() and Figure.subplots().
26
+ Call .imshow() or .plot() to attach a data plot and get back
27
+ a Plot2D or Plot1D object.
28
+ """
29
+
30
+ def __init__(self, fig: "Figure", spec: "SubplotSpec"): # noqa: F821
31
+ self._fig = fig
32
+ self._spec = spec
33
+ self._plot: "Plot1D | Plot2D | None" = None
34
+
35
+ # ------------------------------------------------------------------
36
+ def imshow(self, data: np.ndarray,
37
+ axes: list | None = None,
38
+ units: str = "px",
39
+ cmap: str | None = None,
40
+ vmin: float | None = None,
41
+ vmax: float | None = None,
42
+ origin: str = "upper") -> "Plot2D":
43
+ """Attach a 2-D image to this axes cell.
44
+
45
+ Parameters
46
+ ----------
47
+ data : np.ndarray, shape (H, W) or (H, W, 3|4)
48
+ Image data. 2-D arrays are colormapped. ``(H, W, 3)`` /
49
+ ``(H, W, 4)`` arrays render as true-colour RGB(A): uint8 values
50
+ are used directly; floats are interpreted as 0–1 (or 0–255 when
51
+ the max exceeds 1). ``cmap``/``vmin``/``vmax`` and the colorbar
52
+ do not apply to RGB images.
53
+ axes : [x_axis, y_axis], optional
54
+ Physical coordinate arrays for each axis.
55
+ units : str, optional
56
+ Axis units label. Default ``"px"``.
57
+ cmap : str, optional
58
+ Colormap name (e.g. ``"viridis"``, ``"inferno"``).
59
+ Defaults to ``"gray"``.
60
+ vmin, vmax : float, optional
61
+ Colormap clipping limits in data units. Values outside this
62
+ range are clamped to the colormap endpoints. Defaults to the
63
+ data min / max.
64
+ origin : ``"upper"`` | ``"lower"``, optional
65
+ Where row 0 of the array is placed. ``"upper"`` (default)
66
+ puts row 0 at the top, matching the usual image convention.
67
+ ``"lower"`` puts row 0 at the bottom, matching the matplotlib
68
+ convention for matrices / scientific plots.
69
+
70
+ Returns
71
+ -------
72
+ Plot2D
73
+ """
74
+ x_axis = axes[0] if axes and len(axes) > 0 else None
75
+ y_axis = axes[1] if axes and len(axes) > 1 else None
76
+ plot = Plot2D(data, x_axis=x_axis, y_axis=y_axis, units=units,
77
+ cmap=cmap, vmin=vmin, vmax=vmax, origin=origin)
78
+ self._attach(plot)
79
+ return plot
80
+
81
+ def pcolormesh(self, data: np.ndarray,
82
+ x_edges=None, y_edges=None,
83
+ units: str = "") -> "PlotMesh":
84
+ """Attach a 2-D mesh to this axes cell using edge coordinates.
85
+
86
+ Follows the matplotlib pcolormesh convention: x_edges and y_edges
87
+ are the cell *edge* coordinates, so they have length N+1 and M+1
88
+ respectively for an (M, N) data array.
89
+
90
+ Parameters
91
+ ----------
92
+ data : np.ndarray shape (M, N)
93
+ x_edges : array-like, length N+1, optional
94
+ Column edge coordinates. Defaults to ``np.arange(N+1)``.
95
+ y_edges : array-like, length M+1, optional
96
+ Row edge coordinates. Defaults to ``np.arange(M+1)``.
97
+ units : str, optional
98
+
99
+ Returns
100
+ -------
101
+ PlotMesh
102
+ """
103
+ plot = PlotMesh(data, x_edges=x_edges, y_edges=y_edges, units=units)
104
+ self._attach(plot)
105
+ return plot
106
+
107
+ def plot_surface(self, X, Y, Z, *,
108
+ colormap: str = "viridis",
109
+ x_label: str = "x", y_label: str = "y", z_label: str = "z",
110
+ azimuth: float = -60.0, elevation: float = 30.0,
111
+ zoom: float = 1.0) -> "Plot3D":
112
+ """Attach a 3-D surface to this axes cell.
113
+
114
+ Parameters
115
+ ----------
116
+ X, Y, Z : array-like
117
+ 2-D grid arrays of the same shape (e.g. from ``np.meshgrid``),
118
+ or 1-D centre arrays for X/Y with a 2-D Z.
119
+ colormap : str, optional Matplotlib colormap name. Default ``'viridis'``.
120
+ x_label, y_label, z_label : str, optional Axis labels.
121
+ azimuth, elevation : float, optional Initial camera angles in degrees.
122
+ zoom : float, optional Initial zoom factor.
123
+
124
+ Returns
125
+ -------
126
+ Plot3D
127
+ """
128
+ plot = Plot3D("surface", X, Y, Z, colormap=colormap,
129
+ x_label=x_label, y_label=y_label, z_label=z_label,
130
+ azimuth=azimuth, elevation=elevation, zoom=zoom)
131
+ self._attach(plot)
132
+ return plot
133
+
134
+ def scatter3d(self, x, y, z, *,
135
+ color: str = "#4fc3f7",
136
+ colors=None,
137
+ point_size: float = 4.0,
138
+ x_label: str = "x", y_label: str = "y", z_label: str = "z",
139
+ azimuth: float = -60.0, elevation: float = 30.0,
140
+ zoom: float = 1.0,
141
+ bounds=None,
142
+ gpu: str | bool = "auto") -> "Plot3D":
143
+ """Attach a 3-D scatter plot to this axes cell.
144
+
145
+ Parameters
146
+ ----------
147
+ x, y, z : array-like, shape (N,) Point coordinates.
148
+ color : str, optional CSS colour for all points.
149
+ colors : list of "#rrggbb" or (N, 3) array, optional
150
+ Per-point colours (overrides *color*). Floats are 0–1.
151
+ point_size : float, optional Radius of each point in pixels.
152
+ x_label, y_label, z_label : str, optional Axis labels.
153
+ azimuth, elevation : float, optional Initial camera angles in degrees.
154
+ zoom : float, optional Initial zoom factor.
155
+ bounds : ((xmin, xmax), (ymin, ymax), (zmin, zmax)), optional
156
+ Fix the axes bounds instead of fitting them to the data — keeps
157
+ the origin and scale stable, e.g. ``((-1, 1),) * 3`` for unit
158
+ vectors on a sphere.
159
+ gpu : ``"auto"`` | bool, optional
160
+ WebGPU acceleration policy. ``"auto"`` (default) renders on the
161
+ GPU when available and the cloud exceeds ~20k points, else
162
+ Canvas2D; ``True`` always attempts GPU; ``False`` forces Canvas2D.
163
+ Falls back silently when WebGPU is unavailable — check
164
+ :attr:`Plot3D.gpu_active` for the actual path.
165
+
166
+ Returns
167
+ -------
168
+ Plot3D
169
+ """
170
+ plot = Plot3D("scatter", x, y, z, color=color, colors=colors,
171
+ point_size=point_size,
172
+ x_label=x_label, y_label=y_label, z_label=z_label,
173
+ azimuth=azimuth, elevation=elevation, zoom=zoom,
174
+ bounds=bounds, gpu=gpu)
175
+ self._attach(plot)
176
+ return plot
177
+
178
+ def voxels(self, x, y, z, *,
179
+ colors=None,
180
+ color: str = "#4fc3f7",
181
+ size: float = 1.0,
182
+ alpha: float = 0.3,
183
+ x_label: str = "x", y_label: str = "y", z_label: str = "z",
184
+ azimuth: float = -60.0, elevation: float = 30.0,
185
+ zoom: float = 1.0,
186
+ bounds=None,
187
+ gpu: str | bool = "auto") -> "Plot3D":
188
+ """Attach a 3-D voxel plot: shaded translucent cubes at the centres.
189
+
190
+ Designed for volumetric grain/label maps. Add draggable
191
+ :class:`~anyplotlib.PlaneWidget` slice selectors with
192
+ ``plot.add_widget("plane", axis=..., position=...)`` — voxels lying
193
+ on a plane render at ``voxel_slice_alpha`` (more opaque) so the
194
+ selected slice pops out of the translucent volume.
195
+
196
+ **Large volumes** With WebGPU (``gpu="auto"``, the default, active
197
+ above ~8k cubes when a GPU is present) hundreds of thousands of
198
+ voxels render interactively via instancing. On the Canvas2D
199
+ fallback the budget is ~20k cubes (~3–6 µs each); a warning is
200
+ emitted above that *only when* ``gpu=False``. For volumes too large
201
+ even for the GPU (e.g. a 512 × 512 × 300 tomogram = 78M voxels),
202
+ downsample with stride slicing (``vol[::s, ::s, ::s]``) or draw only
203
+ grain-boundary voxels, and pair the 3-D overview with linked
204
+ full-resolution 2-D slice panels — the voxel grain explorer example
205
+ demonstrates this pattern.
206
+
207
+ Parameters
208
+ ----------
209
+ x, y, z : array-like, shape (N,)
210
+ Voxel centre coordinates.
211
+ colors : list of "#rrggbb" or (N, 3) array, optional
212
+ Per-voxel colours (overrides *color*). Floats are 0–1.
213
+ color : str, optional Single CSS colour when *colors* is omitted.
214
+ size : float, optional Cube edge length in data units. Default 1.
215
+ alpha : float, optional
216
+ Base voxel opacity (0–1). Default 0.3. See also
217
+ :meth:`Plot3D.set_voxel_alpha`.
218
+ x_label, y_label, z_label : str, optional Axis labels.
219
+ azimuth, elevation : float, optional Initial camera angles in degrees.
220
+ zoom : float, optional Initial zoom factor.
221
+ bounds : ((xmin, xmax), (ymin, ymax), (zmin, zmax)), optional
222
+ Fix the axes bounds instead of fitting them to the data.
223
+ gpu : ``"auto"`` | bool, optional
224
+ WebGPU acceleration policy. ``"auto"`` (default) renders cubes
225
+ on the GPU when available and the set exceeds ~8k; ``True`` always
226
+ attempts GPU; ``False`` forces Canvas2D. Falls back silently when
227
+ WebGPU is unavailable — see :attr:`Plot3D.gpu_active`.
228
+
229
+ Returns
230
+ -------
231
+ Plot3D
232
+ """
233
+ plot = Plot3D("voxels", x, y, z, color=color, colors=colors,
234
+ voxel_size=size, alpha=alpha,
235
+ x_label=x_label, y_label=y_label, z_label=z_label,
236
+ azimuth=azimuth, elevation=elevation, zoom=zoom,
237
+ bounds=bounds, gpu=gpu)
238
+ self._attach(plot)
239
+ return plot
240
+
241
+ def plot3d(self, x, y, z, *,
242
+ color: str = "#4fc3f7",
243
+ linewidth: float = 1.5,
244
+ x_label: str = "x", y_label: str = "y", z_label: str = "z",
245
+ azimuth: float = -60.0, elevation: float = 30.0,
246
+ zoom: float = 1.0) -> "Plot3D":
247
+ """Attach a 3-D line plot to this axes cell.
248
+
249
+ Parameters
250
+ ----------
251
+ x, y, z : array-like, shape (N,) Point coordinates along the line.
252
+ color : str, optional CSS colour.
253
+ linewidth : float, optional Stroke width in pixels.
254
+ x_label, y_label, z_label : str, optional Axis labels.
255
+ azimuth, elevation : float, optional Initial camera angles in degrees.
256
+ zoom : float, optional Initial zoom factor.
257
+
258
+ Returns
259
+ -------
260
+ Plot3D
261
+ """
262
+ plot = Plot3D("line", x, y, z, color=color, linewidth=linewidth,
263
+ x_label=x_label, y_label=y_label, z_label=z_label,
264
+ azimuth=azimuth, elevation=elevation, zoom=zoom)
265
+ self._attach(plot)
266
+ return plot
267
+
268
+ def plot(self, data: np.ndarray,
269
+ axes: list | None = None,
270
+ units: str = "px",
271
+ y_units: str = "",
272
+ color: str = "#4fc3f7",
273
+ linewidth: float = 1.5,
274
+ linestyle: str = "solid",
275
+ ls: str | None = None,
276
+ alpha: float = 1.0,
277
+ marker: str = "none",
278
+ markersize: float = 4.0,
279
+ label: str = "",
280
+ yscale: str = "linear") -> "Plot1D":
281
+ """Attach a 1-D line to this axes cell.
282
+
283
+ Parameters
284
+ ----------
285
+ data : array-like, shape (N,)
286
+ Y values. Must be 1-D.
287
+ axes : list, optional
288
+ ``[x_axis]`` — a one-element list containing the x-coordinates
289
+ (shape ``(N,)``). If omitted the x-axis defaults to
290
+ ``0, 1, …, N-1``.
291
+ units : str, optional
292
+ Label for the x-axis (e.g. ``"eV"``, ``"s"``). Default
293
+ ``"px"``.
294
+ y_units : str, optional
295
+ Label for the y-axis. Default ``""`` (no label).
296
+ color : str, optional
297
+ CSS colour string for the line (hex, ``rgb()``, named colour,
298
+ etc.). Default ``"#4fc3f7"``.
299
+ linewidth : float, optional
300
+ Stroke width in pixels. Default ``1.5``.
301
+ linestyle : str, optional
302
+ Dash pattern. Accepted values: ``"solid"`` (``"-"``),
303
+ ``"dashed"`` (``"--"``), ``"dotted"`` (``":"``),
304
+ ``"dashdot"`` (``"-."``) . Default ``"solid"``.
305
+ ls : str, optional
306
+ Short alias for *linestyle*. Takes precedence if both are given.
307
+ alpha : float, optional
308
+ Line opacity in the range 0–1. Default ``1.0`` (fully opaque).
309
+ marker : str, optional
310
+ Per-point marker symbol. Supported values: ``"o"`` (circle),
311
+ ``"s"`` (square), ``"^"`` (triangle-up), ``"v"`` (triangle-down),
312
+ ``"D"`` (diamond), ``"+"`` (plus), ``"x"`` (cross),
313
+ ``"none"`` (no markers). Default ``"none"``.
314
+ markersize : float, optional
315
+ Marker radius / half-side in pixels. Default ``4.0``.
316
+ label : str, optional
317
+ Legend label. A legend is only drawn when at least one line has
318
+ a non-empty label. Default ``""`` (no legend entry).
319
+
320
+ Returns
321
+ -------
322
+ Plot1D
323
+ Live plot object. Call methods on it to update data, add
324
+ overlays, register callbacks, etc.
325
+
326
+ Examples
327
+ --------
328
+ Basic sine wave with a physical x-axis::
329
+
330
+ import numpy as np
331
+ import anyplotlib as apl
332
+
333
+ x = np.linspace(0, 4 * np.pi, 512)
334
+ fig, ax = apl.subplots(1, 1, figsize=(620, 320))
335
+ v = ax.plot(np.sin(x), axes=[x], units="rad",
336
+ color="#ff7043", linewidth=2, label="sin")
337
+ v # display in a Jupyter cell
338
+
339
+ Dashed line with semi-transparent markers::
340
+
341
+ v = ax.plot(data, linestyle="dashed", alpha=0.7,
342
+ marker="o", markersize=4)
343
+
344
+ Overlay a second curve with :meth:`Plot1D.add_line`::
345
+
346
+ v.add_line(np.cos(x), x_axis=x, color="#aed581", label="cos")
347
+ """
348
+ x_axis = axes[0] if axes and len(axes) > 0 else None
349
+ plot = Plot1D(data, x_axis=x_axis, units=units, y_units=y_units,
350
+ color=color, linewidth=linewidth,
351
+ linestyle=ls if ls is not None else linestyle,
352
+ alpha=alpha, marker=marker, markersize=markersize,
353
+ label=label, yscale=yscale)
354
+ self._attach(plot)
355
+ return plot
356
+
357
+ def semilogy(self, data: np.ndarray,
358
+ axes: list | None = None, **kwargs) -> "Plot1D":
359
+ """Attach a 1-D line with a logarithmic y-axis."""
360
+ kwargs.setdefault("yscale", "log")
361
+ return self.plot(data, axes=axes, **kwargs)
362
+
363
+ def axes2d(self, *, xlim=(0.0, 1.0), ylim=(0.0, 1.0), aspect=None,
364
+ units: str = "", y_units: str = "") -> "PlotXY": # noqa: F821
365
+ """Attach a blank **data-coordinate 2-D axis** (`PlotXY`).
366
+
367
+ Unlike :meth:`plot` (a curve over a monotonic x-axis), this is a
368
+ coordinate canvas: set ``xlim`` / ``ylim`` (+ optional ``aspect="equal"``)
369
+ and draw :meth:`~anyplotlib.plotxy.PlotXY.scatter` / ``plot`` / ``fill`` /
370
+ ``text`` as collection-style artists in data coordinates — matplotlib's
371
+ ``transData`` + ``PathCollection`` model. Suits stereographic / IPF /
372
+ pole-figure style plots.
373
+
374
+ Examples
375
+ --------
376
+ >>> import anyplotlib as apl
377
+ >>> fig, ax = apl.subplots()
378
+ >>> xy = ax.axes2d(xlim=(-1, 1), ylim=(-1, 1), aspect="equal")
379
+ >>> xy.fill([0, 1, 0.5], [0, 0, 0.9], facecolor="#eee") # triangle
380
+ >>> xy.scatter([0.3], [0.3], c="#f00", s=10)
381
+ >>> xy.text(0.5, 0.95, r"$[111]$")
382
+ """
383
+ from anyplotlib.plotxy import PlotXY
384
+ plot = PlotXY(xlim=xlim, ylim=ylim, aspect=aspect, units=units, y_units=y_units)
385
+ self._attach(plot)
386
+ return plot
387
+
388
+ def bar(self, x, height=None, width: float = 0.8, bottom: float = 0.0, *,
389
+ align: str = "center",
390
+ color: str = "#4fc3f7",
391
+ colors=None,
392
+ orient: str = "v",
393
+ log_scale: bool = False,
394
+ group_labels=None,
395
+ group_colors=None,
396
+ show_values: bool = False,
397
+ units: str = "",
398
+ y_units: str = "",
399
+ # ── legacy backward-compat kwargs ──────────────────────────────
400
+ x_labels=None,
401
+ x_centers=None,
402
+ bar_width=None,
403
+ baseline=None,
404
+ values=None) -> "PlotBar":
405
+ """Attach a bar chart to this axes cell.
406
+
407
+ Signature mirrors ``matplotlib.pyplot.bar``::
408
+
409
+ ax.bar(x, height, width=0.8, bottom=0.0, ...)
410
+
411
+ Parameters
412
+ ----------
413
+ x : array-like of str or numeric
414
+ Bar positions. Strings become category labels with auto-numeric
415
+ centres; numbers are used directly as bar centres.
416
+ height : array-like, shape ``(N,)`` or ``(N, G)``, optional
417
+ Bar heights. Pass a 2-D array to draw *G* grouped bars per
418
+ category. If omitted *x* is treated as the heights and positions
419
+ are generated automatically (backward-compatible call form).
420
+ width : float, optional
421
+ Bar width as a fraction of the category slot (0–1). Default ``0.8``.
422
+ bottom : float, optional
423
+ Value at which bars are rooted (baseline). Default ``0``.
424
+ align : ``"center"`` | ``"edge"``, optional
425
+ Alignment of the bar relative to its *x* position. Currently only
426
+ ``"center"`` is rendered; stored for future use.
427
+ color : str, optional
428
+ Single CSS colour applied to every bar. Default ``"#4fc3f7"``.
429
+ colors : list of str, optional
430
+ Per-bar colour list (ungrouped) or ignored when *group_colors* is set.
431
+ orient : ``"v"`` | ``"h"``, optional
432
+ Vertical (default) or horizontal orientation.
433
+ log_scale : bool, optional
434
+ Use a logarithmic value axis. Non-positive values are clamped to
435
+ ``1e-10`` for display. Default ``False``.
436
+ group_labels : list of str, optional
437
+ Legend labels for each group in a grouped bar chart.
438
+ group_colors : list of str, optional
439
+ CSS colours per group. Defaults to a built-in palette.
440
+ show_values : bool, optional
441
+ Draw the numeric value above / beside each bar.
442
+ units : str, optional
443
+ Label for the categorical axis.
444
+ y_units : str, optional
445
+ Label for the value axis.
446
+
447
+ Backward-compatible keyword aliases
448
+ ------------------------------------
449
+ ``values`` → ``height``
450
+ ``x_centers`` → ``x``
451
+ ``bar_width`` → ``width``
452
+ ``baseline`` → ``bottom``
453
+ ``x_labels`` → strings passed via ``x``
454
+
455
+ Returns
456
+ -------
457
+ PlotBar
458
+ """
459
+ # ── legacy backward-compat resolution ─────────────────────────────
460
+ if height is None:
461
+ if values is not None:
462
+ height = values
463
+ else:
464
+ height = x
465
+ x = None
466
+ if baseline is not None:
467
+ bottom = baseline
468
+ if bar_width is not None:
469
+ width = bar_width
470
+
471
+ plot = PlotBar(x, height, width=width, bottom=bottom,
472
+ align=align, color=color, colors=colors,
473
+ orient=orient, log_scale=log_scale,
474
+ group_labels=group_labels, group_colors=group_colors,
475
+ show_values=show_values, units=units, y_units=y_units,
476
+ x_labels=x_labels, x_centers=x_centers)
477
+ self._attach(plot)
478
+ return plot
479
+
480
+ def _panel_id_from_spec(self) -> str:
481
+ """Derive a deterministic, position-based panel ID from the SubplotSpec.
482
+
483
+ The ID is ``"p"`` followed by the first 7 hex characters of a SHA-256
484
+ hash of the row/col bounds, e.g. ``"p6a2f3b1"``. This is:
485
+
486
+ * **Deterministic** – the same SubplotSpec always produces the same ID
487
+ across Python processes and after code edits.
488
+ * **Starts with "p"** – satisfies the JS naming convention and makes it
489
+ easy to grep for panel traits (``panel_{id}_json``).
490
+ * **Short** – 8 characters total; safe to embed in CSS selectors.
491
+ """
492
+ import hashlib as _hl
493
+ key = f"{self._spec.row_start},{self._spec.row_stop},{self._spec.col_start},{self._spec.col_stop}"
494
+ return "p" + _hl.sha256(key.encode()).hexdigest()[:7]
495
+
496
+ def _attach(self, plot: "Plot1D | Plot2D | PlotMesh | Plot3D | PlotBar") -> None:
497
+ """Register a plot on this axes (replace any previous plot)."""
498
+ # Allocate a panel id if needed; reuse if replacing
499
+ if self._plot is not None:
500
+ panel_id = self._plot._id
501
+ else:
502
+ panel_id = self._panel_id_from_spec()
503
+ plot._id = panel_id
504
+ plot._fig = self._fig
505
+ self._plot = plot
506
+ self._fig._register_panel(self, plot)
507
+
508
+ def __repr__(self) -> str:
509
+ kind = type(self._plot).__name__ if self._plot else "empty"
510
+ return f"Axes(rows={self._spec.row_start}:{self._spec.row_stop}, cols={self._spec.col_start}:{self._spec.col_stop}, {kind})"
@@ -0,0 +1,126 @@
1
+ """
2
+ axes/_inset_axes.py
3
+ ===================
4
+ Floating overlay inset (not in the grid).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid as _uuid
10
+
11
+ from anyplotlib.axes._axes import Axes
12
+ from anyplotlib.plot1d import PlotBar
13
+ from anyplotlib.plot2d import Plot2D, PlotMesh
14
+ from anyplotlib.plot3d import Plot3D
15
+
16
+
17
+ def _plot_kind(plot) -> str:
18
+ """Return the JS panel-kind string for a plot object.
19
+
20
+ Used in ``Figure._push_layout()`` and ``InsetAxes.__repr__``.
21
+ """
22
+ if isinstance(plot, Plot3D):
23
+ return "3d"
24
+ if isinstance(plot, (Plot2D, PlotMesh)):
25
+ return "2d"
26
+ if isinstance(plot, PlotBar):
27
+ return "bar"
28
+ return "1d"
29
+
30
+
31
+ _VALID_CORNERS = ("top-right", "top-left", "bottom-right", "bottom-left")
32
+
33
+
34
+ class InsetAxes(Axes):
35
+ """A floating inset sub-plot that overlays the main Figure grid.
36
+
37
+ Created via :meth:`Figure.add_inset`. Supports the same plot-factory
38
+ methods as :class:`Axes` (``imshow``, ``plot``, ``pcolormesh``, etc.).
39
+
40
+ The inset is positioned at a corner of the figure and can be minimized
41
+ (title bar only), maximized (expanded to fill ~72% of the figure), or
42
+ restored to its normal size.
43
+
44
+ Parameters
45
+ ----------
46
+ fig : Figure
47
+ w_frac, h_frac : float
48
+ Width and height as fractions of the figure dimensions (0–1).
49
+ corner : str, optional
50
+ One of ``"top-right"``, ``"top-left"``, ``"bottom-right"``,
51
+ ``"bottom-left"``. Default ``"top-right"``.
52
+ title : str, optional
53
+ Text shown in the inset title bar. Default ``""``.
54
+
55
+ Examples
56
+ --------
57
+ >>> fig, ax = apl.subplots(1, 1, figsize=(640, 480))
58
+ >>> ax.imshow(data)
59
+ >>> inset = fig.add_inset(0.3, 0.25, corner="top-right", title="Zoom")
60
+ >>> inset.imshow(data[64:128, 64:128])
61
+ """
62
+
63
+ def __init__(self, fig, w_frac: float, h_frac: float, *,
64
+ corner: str = "top-right", title: str = ""):
65
+ if corner not in _VALID_CORNERS:
66
+ raise ValueError(
67
+ f"corner must be one of {_VALID_CORNERS!r}, got {corner!r}"
68
+ )
69
+ # Pass a dummy SubplotSpec so Axes.__init__ doesn't fail — InsetAxes
70
+ # never occupies a grid cell, only overlays the figure.
71
+ from anyplotlib.figure._gridspec import SubplotSpec
72
+ super().__init__(fig, SubplotSpec(None, 0, 1, 0, 1))
73
+ self.w_frac = w_frac
74
+ self.h_frac = h_frac
75
+ self.corner = corner
76
+ self.title = title
77
+ self._inset_state: str = "normal"
78
+
79
+ # ── state API ─────────────────────────────────────────────────────────
80
+
81
+ @property
82
+ def inset_state(self) -> str:
83
+ """Current state: ``"normal"``, ``"minimized"``, or ``"maximized"``."""
84
+ return self._inset_state
85
+
86
+ def minimize(self) -> None:
87
+ """Collapse the inset to its title bar only (idempotent)."""
88
+ if self._inset_state == "minimized":
89
+ return
90
+ self._inset_state = "minimized"
91
+ self._fig._push_layout()
92
+
93
+ def maximize(self) -> None:
94
+ """Expand the inset to ~72 % of the figure, centred (idempotent)."""
95
+ if self._inset_state == "maximized":
96
+ return
97
+ self._inset_state = "maximized"
98
+ self._fig._push_layout()
99
+
100
+ def restore(self) -> None:
101
+ """Return the inset to its normal corner position (idempotent)."""
102
+ if self._inset_state == "normal":
103
+ return
104
+ self._inset_state = "normal"
105
+ self._fig._push_layout()
106
+
107
+ # ── internal ──────────────────────────────────────────────────────────
108
+
109
+ def _attach(self, plot) -> None:
110
+ """Register the plot on this inset via Figure._register_inset."""
111
+ if self._plot is not None:
112
+ panel_id = self._plot._id
113
+ else:
114
+ panel_id = str(_uuid.uuid4())[:8]
115
+ plot._id = panel_id
116
+ plot._fig = self._fig
117
+ self._plot = plot
118
+ self._fig._register_inset(self, plot)
119
+
120
+ def __repr__(self) -> str:
121
+ kind = _plot_kind(self._plot) if self._plot else "empty"
122
+ return (
123
+ f"InsetAxes(corner={self.corner!r}, "
124
+ f"size=({self.w_frac:.2f}, {self.h_frac:.2f}), "
125
+ f"state={self._inset_state!r}, kind={kind!r})"
126
+ )