anyplotlib 0.3.0__py3-none-any.whl → 0.4.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.
- anyplotlib/_utils.py +38 -0
- anyplotlib/axes/_inset_axes.py +163 -5
- anyplotlib/callbacks.py +26 -2
- anyplotlib/figure/_figure.py +29 -0
- anyplotlib/figure_esm.js +477 -39
- anyplotlib/plot1d/_plot1d.py +46 -6
- anyplotlib/plot2d/_layer.py +18 -5
- anyplotlib/plot2d/_plot2d.py +48 -14
- anyplotlib/widgets/_widgets1d.py +21 -8
- anyplotlib/widgets/_widgets2d.py +23 -6
- {anyplotlib-0.3.0.dist-info → anyplotlib-0.4.0.dist-info}/METADATA +1 -1
- {anyplotlib-0.3.0.dist-info → anyplotlib-0.4.0.dist-info}/RECORD +14 -14
- {anyplotlib-0.3.0.dist-info → anyplotlib-0.4.0.dist-info}/WHEEL +0 -0
- {anyplotlib-0.3.0.dist-info → anyplotlib-0.4.0.dist-info}/licenses/LICENSE +0 -0
anyplotlib/_utils.py
CHANGED
|
@@ -192,6 +192,44 @@ def _build_colormap_lut(name: str) -> list:
|
|
|
192
192
|
return [[v, v, v] for v in range(256)]
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
def _parse_hex_color(color: str) -> tuple:
|
|
196
|
+
"""Parse a ``#rgb`` / ``#rrggbb`` hex string → ``(r, g, b)`` ints (0–255).
|
|
197
|
+
|
|
198
|
+
Raises ``ValueError`` for anything else — tint colours are user API input
|
|
199
|
+
and a silent fallback would just render the wrong colour.
|
|
200
|
+
"""
|
|
201
|
+
if not isinstance(color, str):
|
|
202
|
+
raise ValueError(f"expected a hex colour string, got {color!r}")
|
|
203
|
+
h = color.strip().lstrip("#")
|
|
204
|
+
try:
|
|
205
|
+
if len(h) == 3:
|
|
206
|
+
return tuple(int(c * 2, 16) for c in h)
|
|
207
|
+
if len(h) == 6:
|
|
208
|
+
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
|
209
|
+
except ValueError:
|
|
210
|
+
pass
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"invalid hex colour {color!r} — expected '#rgb' or '#rrggbb'")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@functools.lru_cache(maxsize=64)
|
|
216
|
+
def _build_tint_lut(color: str) -> list:
|
|
217
|
+
"""Return a 256-entry ``[[r, g, b, a], ...]`` clear→colour TINT LUT.
|
|
218
|
+
|
|
219
|
+
RGB is the parsed *color* at every entry; alpha ramps linearly 0 → 255, so
|
|
220
|
+
a layer using this LUT is fully transparent at low intensity and the
|
|
221
|
+
opaque tint colour at high intensity (a solid-colour intensity ramp, the
|
|
222
|
+
overlay look, vs a named colormap's opaque colour gradient).
|
|
223
|
+
|
|
224
|
+
CACHED like :func:`_build_colormap_lut` and treated as read-only by
|
|
225
|
+
callers (they JSON-serialise it); do NOT mutate the returned list. The
|
|
226
|
+
4th (alpha) channel is honoured by the JS layer compositor
|
|
227
|
+
(``_layerBitmap`` reads ``cmapData[i][3] ?? 255``).
|
|
228
|
+
"""
|
|
229
|
+
r, g, b = _parse_hex_color(color)
|
|
230
|
+
return [[r, g, b, a] for a in range(256)]
|
|
231
|
+
|
|
232
|
+
|
|
195
233
|
def _resample_mesh(data: np.ndarray, x_edges, y_edges) -> np.ndarray:
|
|
196
234
|
"""Resample a mesh to a regular pixel grid via nearest-neighbour lookup.
|
|
197
235
|
|
anyplotlib/axes/_inset_axes.py
CHANGED
|
@@ -96,9 +96,10 @@ class InsetAxes(Axes):
|
|
|
96
96
|
self.h_frac = h_frac
|
|
97
97
|
self.title = title
|
|
98
98
|
self._inset_state: str = "normal"
|
|
99
|
-
# Region indication (mark_inset-style callout) tied to this
|
|
100
|
-
# None. Set via :meth:`indicate_region
|
|
101
|
-
# :meth:`clear_indication`.
|
|
99
|
+
# Region/point indication (mark_inset-style callout) tied to this
|
|
100
|
+
# inset, or None. Set via :meth:`indicate_region` /
|
|
101
|
+
# :meth:`indicate_point`, cleared via :meth:`clear_indication`.
|
|
102
|
+
# Persisted in Figure.layout_json.
|
|
102
103
|
self._indication: dict | None = None
|
|
103
104
|
|
|
104
105
|
# ── state API ─────────────────────────────────────────────────────────
|
|
@@ -129,6 +130,76 @@ class InsetAxes(Axes):
|
|
|
129
130
|
self._inset_state = "normal"
|
|
130
131
|
self._fig._push_layout()
|
|
131
132
|
|
|
133
|
+
def set_geometry(self, *, anchor=None, w_frac: float | None = None,
|
|
134
|
+
h_frac: float | None = None) -> "InsetAxes":
|
|
135
|
+
"""Move and/or resize the inset in figure-fraction coordinates.
|
|
136
|
+
|
|
137
|
+
This is the Python-side counterpart to the interactive drag / resize
|
|
138
|
+
of an inset in the renderer's edit mode: the ``inset_geometry_change``
|
|
139
|
+
event dispatches here so the authoritative Python state converges with
|
|
140
|
+
what the user did on screen. It is also callable directly to place or
|
|
141
|
+
size an inset programmatically.
|
|
142
|
+
|
|
143
|
+
Passing *anchor* switches the inset to free (anchor) placement and
|
|
144
|
+
drops any corner snapping (``corner`` becomes ``None``), mirroring the
|
|
145
|
+
renderer's corner-to-anchor conversion on drag start.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
anchor : (x_frac, y_frac), optional
|
|
150
|
+
New position of the inset's TOP-LEFT corner as fractions of the
|
|
151
|
+
figure size (0–1), measured from the figure's top-left. Each
|
|
152
|
+
component is clamped into ``[0, 1]``. When given, ``corner`` is
|
|
153
|
+
set to ``None`` (free placement). When omitted, the current
|
|
154
|
+
placement (corner or existing anchor) is left unchanged.
|
|
155
|
+
w_frac, h_frac : float, optional
|
|
156
|
+
New width / height as fractions of the figure size. Each is
|
|
157
|
+
clamped into ``(0, 1]`` (must be positive, at most the full
|
|
158
|
+
figure). When omitted, the corresponding dimension is unchanged.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
InsetAxes
|
|
163
|
+
``self``, for chaining.
|
|
164
|
+
|
|
165
|
+
Raises
|
|
166
|
+
------
|
|
167
|
+
ValueError
|
|
168
|
+
If *anchor* is not a pair of finite numbers, or ``w_frac`` /
|
|
169
|
+
``h_frac`` is not a finite number.
|
|
170
|
+
"""
|
|
171
|
+
if anchor is not None:
|
|
172
|
+
try:
|
|
173
|
+
ax_, ay_ = (float(v) for v in anchor)
|
|
174
|
+
except (TypeError, ValueError):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"set_geometry: anchor must be 2 numbers (x_frac, y_frac), "
|
|
177
|
+
f"got {anchor!r}") from None
|
|
178
|
+
if not all(math.isfinite(v) for v in (ax_, ay_)):
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"set_geometry: anchor values must be finite, got "
|
|
181
|
+
f"({ax_}, {ay_})")
|
|
182
|
+
ax_ = min(1.0, max(0.0, ax_))
|
|
183
|
+
ay_ = min(1.0, max(0.0, ay_))
|
|
184
|
+
self.anchor = (ax_, ay_)
|
|
185
|
+
# Anchor placement supersedes corner (matches __init__ / the JS
|
|
186
|
+
# corner→anchor conversion on drag start).
|
|
187
|
+
self.corner = None
|
|
188
|
+
if w_frac is not None:
|
|
189
|
+
w = float(w_frac)
|
|
190
|
+
if not math.isfinite(w):
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"set_geometry: w_frac must be finite, got {w_frac!r}")
|
|
193
|
+
self.w_frac = min(1.0, max(1e-6, w))
|
|
194
|
+
if h_frac is not None:
|
|
195
|
+
h = float(h_frac)
|
|
196
|
+
if not math.isfinite(h):
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"set_geometry: h_frac must be finite, got {h_frac!r}")
|
|
199
|
+
self.h_frac = min(1.0, max(1e-6, h))
|
|
200
|
+
self._fig._push_layout()
|
|
201
|
+
return self
|
|
202
|
+
|
|
132
203
|
# ── region indication (mark_inset-style callout) ──────────────────────
|
|
133
204
|
|
|
134
205
|
def indicate_region(self, parent_plot, region, *,
|
|
@@ -218,8 +289,94 @@ class InsetAxes(Axes):
|
|
|
218
289
|
self._fig._push_layout()
|
|
219
290
|
return self
|
|
220
291
|
|
|
292
|
+
def indicate_point(self, parent_plot, point, *,
|
|
293
|
+
color: str = "#ff9800",
|
|
294
|
+
linestyle: str = "dashed",
|
|
295
|
+
linewidth: float = 1.5,
|
|
296
|
+
marker_size: float = 5.0) -> "InsetAxes":
|
|
297
|
+
"""Draw a callout tying this inset to a single POINT of *parent_plot*.
|
|
298
|
+
|
|
299
|
+
The point sibling of :meth:`indicate_region`: renders a small circular
|
|
300
|
+
marker (with a centre cross) at *point* — in the parent image's DATA
|
|
301
|
+
coordinates — plus ONE leader line joining the marker to the inset's
|
|
302
|
+
nearest corner. The marker tracks the parent's zoom / pan and the
|
|
303
|
+
leader follows the inset as it moves (the leader hides while the inset
|
|
304
|
+
is minimized), exactly like the region callout.
|
|
305
|
+
|
|
306
|
+
Calling ``indicate_point`` (or ``indicate_region``) again REPLACES any
|
|
307
|
+
previous indication for this inset — an inset carries at most one.
|
|
308
|
+
Remove it with :meth:`clear_indication`.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
parent_plot : Plot2D
|
|
313
|
+
The parent image plot the point lives on. Must be a 2-D image
|
|
314
|
+
panel registered on the SAME figure as this inset.
|
|
315
|
+
point : tuple of float
|
|
316
|
+
The source point in the parent image's data coordinates, as
|
|
317
|
+
``(x, y)`` — same convention as :meth:`indicate_region`'s region
|
|
318
|
+
origin. Both values must be finite; a point outside the parent's
|
|
319
|
+
data bounds is allowed (it simply clips visually).
|
|
320
|
+
color : str, optional
|
|
321
|
+
Stroke colour of the marker and the leader line. Default warm
|
|
322
|
+
orange ``"#ff9800"``.
|
|
323
|
+
linestyle : str, optional
|
|
324
|
+
Leader style — ``"dashed"`` (default), ``"solid"``, or
|
|
325
|
+
``"dotted"``. The marker itself is always drawn solid.
|
|
326
|
+
linewidth : float, optional
|
|
327
|
+
Stroke width in CSS px. Default ``1.5``.
|
|
328
|
+
marker_size : float, optional
|
|
329
|
+
Marker circle radius in CSS px. Default ``5.0``. Must be > 0.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
InsetAxes
|
|
334
|
+
``self``, for chaining.
|
|
335
|
+
|
|
336
|
+
Raises
|
|
337
|
+
------
|
|
338
|
+
ValueError
|
|
339
|
+
If ``parent_plot`` has no panel id, is not registered on this
|
|
340
|
+
inset's Figure, ``point`` is not 2 finite numbers, or
|
|
341
|
+
``marker_size`` is not > 0.
|
|
342
|
+
"""
|
|
343
|
+
pid = getattr(parent_plot, "_id", None)
|
|
344
|
+
if pid is None:
|
|
345
|
+
raise ValueError("indicate_point: parent_plot has no panel id "
|
|
346
|
+
"(attach it to the figure first)")
|
|
347
|
+
if self._fig._plots_map.get(pid) is not parent_plot:
|
|
348
|
+
raise ValueError(
|
|
349
|
+
"indicate_point: parent_plot is not registered on this "
|
|
350
|
+
"inset's Figure — pass a plot created on the same figure "
|
|
351
|
+
"as this inset (fig.add_inset / fig.subplots)")
|
|
352
|
+
try:
|
|
353
|
+
x, y = (float(v) for v in point)
|
|
354
|
+
except (TypeError, ValueError):
|
|
355
|
+
raise ValueError(
|
|
356
|
+
f"indicate_point: point must be 2 numbers (x, y), "
|
|
357
|
+
f"got {point!r}") from None
|
|
358
|
+
if not all(math.isfinite(v) for v in (x, y)):
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"indicate_point: point values must be finite, got "
|
|
361
|
+
f"(x={x}, y={y})")
|
|
362
|
+
ms = float(marker_size)
|
|
363
|
+
if not (math.isfinite(ms) and ms > 0):
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"indicate_point: marker_size must be > 0, got {marker_size!r}")
|
|
366
|
+
self._indication = {
|
|
367
|
+
"parent_id": pid,
|
|
368
|
+
"point": [x, y],
|
|
369
|
+
"color": color,
|
|
370
|
+
"linestyle": linestyle,
|
|
371
|
+
"linewidth": float(linewidth),
|
|
372
|
+
"marker_size": ms,
|
|
373
|
+
}
|
|
374
|
+
self._fig._push_layout()
|
|
375
|
+
return self
|
|
376
|
+
|
|
221
377
|
def clear_indication(self) -> None:
|
|
222
|
-
"""Remove any region indication attached to this inset
|
|
378
|
+
"""Remove any region/point indication attached to this inset
|
|
379
|
+
(idempotent)."""
|
|
223
380
|
if self._indication is None:
|
|
224
381
|
return
|
|
225
382
|
self._indication = None
|
|
@@ -227,7 +384,8 @@ class InsetAxes(Axes):
|
|
|
227
384
|
|
|
228
385
|
@property
|
|
229
386
|
def indication(self) -> "dict | None":
|
|
230
|
-
"""The current
|
|
387
|
+
"""The current indication spec (``dict`` with either a ``region`` or a
|
|
388
|
+
``point`` key) or ``None``."""
|
|
231
389
|
return self._indication
|
|
232
390
|
|
|
233
391
|
# ── internal ──────────────────────────────────────────────────────────
|
anyplotlib/callbacks.py
CHANGED
|
@@ -24,7 +24,10 @@ from typing import Any, Callable
|
|
|
24
24
|
VALID_EVENT_TYPES = frozenset({
|
|
25
25
|
"pointer_down", "pointer_up", "pointer_move", "pointer_settled",
|
|
26
26
|
"pointer_enter", "pointer_leave", "double_click", "wheel",
|
|
27
|
-
"key_down", "key_up", "close", "view_changed",
|
|
27
|
+
"key_down", "key_up", "close", "view_changed",
|
|
28
|
+
# Figure-level inset drag / resize (JS → Python), so a host can persist the
|
|
29
|
+
# inset's new geometry. Fired through the figure's CallbackRegistry.
|
|
30
|
+
"inset_geometry_change", "*",
|
|
28
31
|
})
|
|
29
32
|
|
|
30
33
|
|
|
@@ -43,6 +46,7 @@ class Event:
|
|
|
43
46
|
ray — Plot3D only: {"origin": [...], "direction": [...]}
|
|
44
47
|
line_id — Plot1D only: set when pointer is over a line
|
|
45
48
|
dwell_ms — pointer_settled only: actual dwell time
|
|
49
|
+
target — double_click only: hit chrome element, or None for plot-area (title/x_label/x_ticks/y_label/y_ticks/colorbar_label/legend)
|
|
46
50
|
|
|
47
51
|
PlotBar extra fields (pointer_down only):
|
|
48
52
|
bar_index, value, x_label, group_index
|
|
@@ -54,6 +58,14 @@ class Event:
|
|
|
54
58
|
key — key name e.g. "q", "Enter", "ArrowLeft"
|
|
55
59
|
last_widget_id — id of the last widget the user clicked, or None
|
|
56
60
|
|
|
61
|
+
Panel-swap fields (figure-level panel_swap events):
|
|
62
|
+
source_panel_id, target_panel_id — the two panel ids dragged between
|
|
63
|
+
|
|
64
|
+
Inset drag/resize fields (figure-level inset_geometry_change events):
|
|
65
|
+
inset_id — the inset panel id
|
|
66
|
+
anchor — new top-left position [fx, fy] in figure fractions
|
|
67
|
+
w_frac, h_frac — new size in figure fractions
|
|
68
|
+
|
|
57
69
|
Propagation:
|
|
58
70
|
stop_propagation — set True inside a handler to halt remaining handlers
|
|
59
71
|
"""
|
|
@@ -94,6 +106,18 @@ class Event:
|
|
|
94
106
|
# ids the user dragged between. Set only on panel_swap; None otherwise.
|
|
95
107
|
source_panel_id: str | None = None
|
|
96
108
|
target_panel_id: str | None = None
|
|
109
|
+
# Inset drag / resize (figure-level inset_geometry_change events): the
|
|
110
|
+
# inset panel id plus its new geometry in figure fractions. Set only on
|
|
111
|
+
# inset_geometry_change; None otherwise.
|
|
112
|
+
inset_id: str | None = None
|
|
113
|
+
anchor: list | None = None
|
|
114
|
+
w_frac: float | None = None
|
|
115
|
+
h_frac: float | None = None
|
|
116
|
+
# Tagged double-click hit-target (double_click events): identifies WHICH
|
|
117
|
+
# text/chrome element was double-clicked — one of 'title', 'x_label',
|
|
118
|
+
# 'x_ticks', 'y_label', 'y_ticks', 'colorbar_label', 'legend'. None for a
|
|
119
|
+
# plain plot-area double_click (back-compatible) and every other event.
|
|
120
|
+
target: str | None = None
|
|
97
121
|
# Propagation (not repr'd)
|
|
98
122
|
stop_propagation: bool = field(default=False, repr=False)
|
|
99
123
|
|
|
@@ -101,7 +125,7 @@ class Event:
|
|
|
101
125
|
src = type(self.source).__name__ if self.source is not None else "None"
|
|
102
126
|
parts = [f"event_type={self.event_type!r}", f"source={src}"]
|
|
103
127
|
for fname in ("x", "y", "xdata", "ydata", "button", "key",
|
|
104
|
-
"line_id", "bar_index", "dwell_ms"):
|
|
128
|
+
"line_id", "bar_index", "dwell_ms", "target"):
|
|
105
129
|
v = getattr(self, fname)
|
|
106
130
|
if v is not None:
|
|
107
131
|
parts.append(f"{fname}={v!r}")
|
anyplotlib/figure/_figure.py
CHANGED
|
@@ -596,6 +596,23 @@ class Figure(anywidget.AnyWidget, _EventMixin):
|
|
|
596
596
|
self._push_layout()
|
|
597
597
|
return
|
|
598
598
|
|
|
599
|
+
# Inset drag / resize: the JS ships the inset's final geometry on
|
|
600
|
+
# pointer_up (anchor top-left fraction + size fractions). Apply it to
|
|
601
|
+
# the InsetAxes via set_geometry so the authoritative Python state
|
|
602
|
+
# converges (the event fires once, on release, so re-pushing layout is
|
|
603
|
+
# an idempotent echo), then fire the figure-level callbacks with the
|
|
604
|
+
# geometry so a host can persist it. Mirrors inset_state_change above.
|
|
605
|
+
if event_type == "inset_geometry_change":
|
|
606
|
+
inset_ax = self._insets_map.get(panel_id)
|
|
607
|
+
if inset_ax is not None:
|
|
608
|
+
anchor = msg.get("anchor")
|
|
609
|
+
w_frac = msg.get("w_frac")
|
|
610
|
+
h_frac = msg.get("h_frac")
|
|
611
|
+
inset_ax.set_geometry(anchor=anchor, w_frac=w_frac,
|
|
612
|
+
h_frac=h_frac)
|
|
613
|
+
self._fire_figure_event(event_type, msg)
|
|
614
|
+
return
|
|
615
|
+
|
|
599
616
|
plot = self._plots_map.get(panel_id)
|
|
600
617
|
if plot is None:
|
|
601
618
|
if event_type == "view_changed":
|
|
@@ -656,6 +673,7 @@ class Figure(anywidget.AnyWidget, _EventMixin):
|
|
|
656
673
|
display_height=msg.get("display_height"),
|
|
657
674
|
key=msg.get("key"),
|
|
658
675
|
last_widget_id=msg.get("last_widget_id"),
|
|
676
|
+
target=msg.get("target"),
|
|
659
677
|
)
|
|
660
678
|
plot.callbacks.fire(event)
|
|
661
679
|
|
|
@@ -683,6 +701,17 @@ class Figure(anywidget.AnyWidget, _EventMixin):
|
|
|
683
701
|
last_widget_id=msg.get("marker_id"),
|
|
684
702
|
source_panel_id=msg.get("source_panel_id"),
|
|
685
703
|
target_panel_id=msg.get("target_panel_id"),
|
|
704
|
+
# Inset drag / resize geometry — populated only for the
|
|
705
|
+
# inset_geometry_change event; None for every other figure-level
|
|
706
|
+
# event (figure_background click, figure-marker drag, panel_swap).
|
|
707
|
+
inset_id=(msg.get("panel_id")
|
|
708
|
+
if event_type == "inset_geometry_change" else None),
|
|
709
|
+
anchor=(msg.get("anchor")
|
|
710
|
+
if event_type == "inset_geometry_change" else None),
|
|
711
|
+
w_frac=(msg.get("w_frac")
|
|
712
|
+
if event_type == "inset_geometry_change" else None),
|
|
713
|
+
h_frac=(msg.get("h_frac")
|
|
714
|
+
if event_type == "inset_geometry_change" else None),
|
|
686
715
|
)
|
|
687
716
|
self.callbacks.fire(event)
|
|
688
717
|
|