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.
- anyplotlib/__init__.py +53 -0
- anyplotlib/_base_plot.py +229 -0
- anyplotlib/_electron.py +74 -0
- anyplotlib/_repr_utils.py +345 -0
- anyplotlib/_utils.py +194 -0
- anyplotlib/axes/__init__.py +6 -0
- anyplotlib/axes/_axes.py +510 -0
- anyplotlib/axes/_inset_axes.py +126 -0
- anyplotlib/callbacks.py +328 -0
- anyplotlib/embed.py +194 -0
- anyplotlib/figure/__init__.py +7 -0
- anyplotlib/figure/_figure.py +633 -0
- anyplotlib/figure/_gridspec.py +99 -0
- anyplotlib/figure/_subplots.py +100 -0
- anyplotlib/figure_esm.js +6011 -0
- anyplotlib/markers.py +704 -0
- anyplotlib/plot1d/__init__.py +6 -0
- anyplotlib/plot1d/_plot1d.py +1376 -0
- anyplotlib/plot1d/_plotbar.py +436 -0
- anyplotlib/plot2d/__init__.py +5 -0
- anyplotlib/plot2d/_plot2d.py +726 -0
- anyplotlib/plot2d/_plotmesh.py +116 -0
- anyplotlib/plot3d/__init__.py +4 -0
- anyplotlib/plot3d/_plot3d.py +524 -0
- anyplotlib/plotxy/__init__.py +4 -0
- anyplotlib/plotxy/_plotxy.py +273 -0
- anyplotlib/sphinx_anywidget/__init__.py +177 -0
- anyplotlib/sphinx_anywidget/_directive.py +245 -0
- anyplotlib/sphinx_anywidget/_repr_utils.py +298 -0
- anyplotlib/sphinx_anywidget/_scraper.py +390 -0
- anyplotlib/sphinx_anywidget/_wheel_builder.py +84 -0
- anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +1099 -0
- anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +100 -0
- anyplotlib/widgets/__init__.py +18 -0
- anyplotlib/widgets/_base.py +218 -0
- anyplotlib/widgets/_widgets1d.py +109 -0
- anyplotlib/widgets/_widgets2d.py +141 -0
- anyplotlib/widgets/_widgets3d.py +50 -0
- anyplotlib-0.1.0b1.dist-info/METADATA +160 -0
- anyplotlib-0.1.0b1.dist-info/RECORD +42 -0
- anyplotlib-0.1.0b1.dist-info/WHEEL +4 -0
- anyplotlib-0.1.0b1.dist-info/licenses/LICENSE +21 -0
anyplotlib/markers.py
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
"""
|
|
2
|
+
markers.py
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
Marker registry for Plot1D and Plot2D panels inside a Figure.
|
|
6
|
+
|
|
7
|
+
The public API mirrors matplotlib's collection kwargs:
|
|
8
|
+
|
|
9
|
+
plot.add_circles(offsets, name="group1",
|
|
10
|
+
facecolors="#ff0000", edgecolors="#ffffff",
|
|
11
|
+
radius=5, labels=[...])
|
|
12
|
+
|
|
13
|
+
plot.markers["circles"]["group1"].set(offsets=new_offsets, radius=3)
|
|
14
|
+
|
|
15
|
+
Design
|
|
16
|
+
------
|
|
17
|
+
* ``MarkerGroup`` – a single named collection of markers (one type).
|
|
18
|
+
* ``MarkerTypeDict`` – dict-like for one type; mutations propagate to the plot.
|
|
19
|
+
* ``MarkerRegistry`` – top-level two-level dict: registry[type][name].
|
|
20
|
+
|
|
21
|
+
All state is stored as plain Python dicts; no traitlets here. The ``_push``
|
|
22
|
+
callback is supplied by the parent plot and is responsible for serialising
|
|
23
|
+
the full registry into the panel's JSON trait on the Figure widget.
|
|
24
|
+
|
|
25
|
+
Wire-format translation
|
|
26
|
+
-----------------------
|
|
27
|
+
The JS renderer uses the same internal field names as the standalone viewers
|
|
28
|
+
(``color``, ``fill_color``, ``fill_alpha``, ``sizes``, etc.). ``MarkerGroup``
|
|
29
|
+
stores matplotlib-style names and ``to_wire()`` translates before JSON
|
|
30
|
+
serialisation.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
__all__ = ["MarkerGroup", "MarkerTypeDict", "MarkerRegistry"]
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Type → wire-format converter registry
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def _broadcast(val, n: int) -> list:
|
|
44
|
+
"""Broadcast scalar or sequence to a list of length *n*."""
|
|
45
|
+
arr = np.asarray(val, dtype=float)
|
|
46
|
+
if arr.ndim == 0:
|
|
47
|
+
return np.full(n, float(arr)).tolist()
|
|
48
|
+
if arr.ndim == 1 and len(arr) == n:
|
|
49
|
+
return arr.tolist()
|
|
50
|
+
raise ValueError(f"Expected scalar or length-{n} array, got shape {arr.shape}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _offsets_2d(offsets) -> list:
|
|
54
|
+
arr = np.asarray(offsets, dtype=float)
|
|
55
|
+
if arr.ndim == 1 and arr.shape[0] == 2:
|
|
56
|
+
arr = arr[np.newaxis, :]
|
|
57
|
+
if arr.ndim != 2 or arr.shape[1] != 2:
|
|
58
|
+
raise ValueError("offsets must be shape (N, 2)")
|
|
59
|
+
return arr.tolist()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_VALID_TRANSFORMS = frozenset({"data", "axes", "display"})
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _apply_fill_color(wire: dict, d: dict) -> None:
|
|
66
|
+
"""Apply facecolors/alpha fill fields to a wire dict if facecolors is set."""
|
|
67
|
+
fc = d.get("facecolors")
|
|
68
|
+
if fc is not None:
|
|
69
|
+
wire["fill_color"] = fc
|
|
70
|
+
wire["fill_alpha"] = float(d.get("alpha", 0.3))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _offsets_1d(offsets) -> list:
|
|
74
|
+
"""Accept (N,), (N,1) or (N,2) — return (N,1) or (N,2) list."""
|
|
75
|
+
arr = np.asarray(offsets, dtype=float)
|
|
76
|
+
if arr.ndim == 0:
|
|
77
|
+
return [[float(arr)]]
|
|
78
|
+
if arr.ndim == 1:
|
|
79
|
+
return arr[:, np.newaxis].tolist()
|
|
80
|
+
if arr.ndim == 2 and arr.shape[1] in (1, 2):
|
|
81
|
+
return arr.tolist()
|
|
82
|
+
raise ValueError("offsets must be 1-D or shape (N,1)/(N,2)")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# MarkerGroup
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
class MarkerGroup:
|
|
90
|
+
"""A named collection of markers of one type on one plot.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
marker_type : str
|
|
95
|
+
One of the supported marker types (``'circles'``, ``'lines'``, …).
|
|
96
|
+
name : str
|
|
97
|
+
User-facing name (key in the parent :class:`MarkerTypeDict`).
|
|
98
|
+
kwargs : dict
|
|
99
|
+
Initial matplotlib-style kwargs for this group.
|
|
100
|
+
push_fn : callable
|
|
101
|
+
Zero-arg callback that serialises the full registry and pushes it to
|
|
102
|
+
the parent figure trait.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def __init__(self, marker_type: str, name: str, kwargs: dict, push_fn,
|
|
106
|
+
parent: "MarkerTypeDict | None" = None):
|
|
107
|
+
self._type = marker_type
|
|
108
|
+
self._name = name
|
|
109
|
+
tfm = kwargs.get("transform", "data")
|
|
110
|
+
if tfm not in _VALID_TRANSFORMS:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"transform must be one of {sorted(_VALID_TRANSFORMS)}, got {tfm!r}"
|
|
113
|
+
)
|
|
114
|
+
self._data: dict = dict(kwargs)
|
|
115
|
+
self._push_fn = push_fn
|
|
116
|
+
self._parent: "MarkerTypeDict | None" = parent
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
def set(self, **kwargs) -> None:
|
|
120
|
+
"""Update one or more properties and push the change to the plot.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
**kwargs : dict
|
|
125
|
+
Properties to update (e.g., offsets, radius, facecolors).
|
|
126
|
+
Matplotlib-style names are translated to wire format.
|
|
127
|
+
"""
|
|
128
|
+
if "transform" in kwargs and kwargs["transform"] not in _VALID_TRANSFORMS:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"transform must be one of {sorted(_VALID_TRANSFORMS)}, "
|
|
131
|
+
f"got {kwargs['transform']!r}"
|
|
132
|
+
)
|
|
133
|
+
self._data.update(kwargs)
|
|
134
|
+
self._push_fn()
|
|
135
|
+
|
|
136
|
+
def remove(self) -> None:
|
|
137
|
+
"""Remove this group from its parent and trigger a re-render."""
|
|
138
|
+
if self._parent is None:
|
|
139
|
+
raise RuntimeError("MarkerGroup has no parent; cannot remove.")
|
|
140
|
+
del self._parent[self._name]
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
143
|
+
return f"MarkerGroup(type={self._type!r}, name={self._name!r}, n={self._count()})"
|
|
144
|
+
|
|
145
|
+
def _count(self) -> int:
|
|
146
|
+
"""Return the number of markers in this group."""
|
|
147
|
+
offs = self._data.get("offsets")
|
|
148
|
+
if offs is None:
|
|
149
|
+
return 0
|
|
150
|
+
try:
|
|
151
|
+
return len(np.asarray(offs))
|
|
152
|
+
except Exception:
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
# Wire-format serialisation
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
def to_wire(self, group_id: str) -> dict:
|
|
159
|
+
"""Return a dict in the JS wire format for this marker group.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
group_id : str
|
|
164
|
+
Unique identifier for this marker group (usually UUID).
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
dict
|
|
169
|
+
Wire-format dict with type-specific structure, ready for JSON
|
|
170
|
+
serialization and transmission to the JavaScript renderer.
|
|
171
|
+
"""
|
|
172
|
+
d = self._data
|
|
173
|
+
t = self._type
|
|
174
|
+
|
|
175
|
+
# Build the wire dict based on marker type
|
|
176
|
+
if t == "circles":
|
|
177
|
+
offsets = _offsets_2d(d["offsets"])
|
|
178
|
+
n = len(offsets)
|
|
179
|
+
wire: dict = {
|
|
180
|
+
"id": group_id,
|
|
181
|
+
"name": self._name,
|
|
182
|
+
"type": "circles",
|
|
183
|
+
"offsets": offsets,
|
|
184
|
+
"sizes": _broadcast(d.get("radius", 5), n),
|
|
185
|
+
"color": d.get("edgecolors", "#ff0000"),
|
|
186
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
187
|
+
}
|
|
188
|
+
_apply_fill_color(wire, d)
|
|
189
|
+
|
|
190
|
+
elif t == "arrows":
|
|
191
|
+
offsets = _offsets_2d(d["offsets"])
|
|
192
|
+
n = len(offsets)
|
|
193
|
+
wire = {
|
|
194
|
+
"id": group_id,
|
|
195
|
+
"name": self._name,
|
|
196
|
+
"type": "arrows",
|
|
197
|
+
"offsets": offsets,
|
|
198
|
+
"U": _broadcast(d.get("U", 0), n),
|
|
199
|
+
"V": _broadcast(d.get("V", 0), n),
|
|
200
|
+
"color": d.get("edgecolors", d.get("color", "#ff0000")),
|
|
201
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
elif t == "ellipses":
|
|
205
|
+
offsets = _offsets_2d(d["offsets"])
|
|
206
|
+
n = len(offsets)
|
|
207
|
+
wire = {
|
|
208
|
+
"id": group_id,
|
|
209
|
+
"name": self._name,
|
|
210
|
+
"type": "ellipses",
|
|
211
|
+
"offsets": offsets,
|
|
212
|
+
"widths": _broadcast(d.get("widths", 10), n),
|
|
213
|
+
"heights": _broadcast(d.get("heights", 10), n),
|
|
214
|
+
"angles": _broadcast(d.get("angles", 0), n),
|
|
215
|
+
"color": d.get("edgecolors", d.get("color", "#ff0000")),
|
|
216
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
217
|
+
}
|
|
218
|
+
_apply_fill_color(wire, d)
|
|
219
|
+
|
|
220
|
+
elif t == "lines":
|
|
221
|
+
segs = np.asarray(d["segments"], dtype=float)
|
|
222
|
+
if segs.ndim == 2 and segs.shape == (2, 2):
|
|
223
|
+
segs = segs[np.newaxis]
|
|
224
|
+
if segs.ndim != 3 or segs.shape[1:] != (2, 2):
|
|
225
|
+
raise ValueError("segments must be shape (N,2,2)")
|
|
226
|
+
wire = {
|
|
227
|
+
"id": group_id,
|
|
228
|
+
"name": self._name,
|
|
229
|
+
"type": "lines",
|
|
230
|
+
"segments": segs.tolist(),
|
|
231
|
+
"color": d.get("edgecolors", d.get("color", "#ff0000")),
|
|
232
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
elif t == "rectangles":
|
|
236
|
+
offsets = _offsets_2d(d["offsets"])
|
|
237
|
+
n = len(offsets)
|
|
238
|
+
wire = {
|
|
239
|
+
"id": group_id,
|
|
240
|
+
"name": self._name,
|
|
241
|
+
"type": "rectangles",
|
|
242
|
+
"offsets": offsets,
|
|
243
|
+
"widths": _broadcast(d.get("widths", 10), n),
|
|
244
|
+
"heights": _broadcast(d.get("heights", 10), n),
|
|
245
|
+
"angles": _broadcast(d.get("angles", 0), n),
|
|
246
|
+
"color": d.get("edgecolors", d.get("color", "#ff0000")),
|
|
247
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
248
|
+
}
|
|
249
|
+
_apply_fill_color(wire, d)
|
|
250
|
+
|
|
251
|
+
elif t == "squares":
|
|
252
|
+
offsets = _offsets_2d(d["offsets"])
|
|
253
|
+
n = len(offsets)
|
|
254
|
+
wire = {
|
|
255
|
+
"id": group_id,
|
|
256
|
+
"name": self._name,
|
|
257
|
+
"type": "squares",
|
|
258
|
+
"offsets": offsets,
|
|
259
|
+
"widths": _broadcast(d.get("widths", 10), n),
|
|
260
|
+
"angles": _broadcast(d.get("angles", 0), n),
|
|
261
|
+
"color": d.get("edgecolors", d.get("color", "#ff0000")),
|
|
262
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
263
|
+
}
|
|
264
|
+
_apply_fill_color(wire, d)
|
|
265
|
+
|
|
266
|
+
elif t == "polygons":
|
|
267
|
+
vlist = []
|
|
268
|
+
for poly in d.get("vertices_list", []):
|
|
269
|
+
arr = np.asarray(poly, dtype=float)
|
|
270
|
+
if arr.ndim != 2 or arr.shape[1] != 2 or len(arr) < 3:
|
|
271
|
+
raise ValueError("each polygon must be (N>=3, 2)")
|
|
272
|
+
vlist.append(arr.tolist())
|
|
273
|
+
wire = {
|
|
274
|
+
"id": group_id,
|
|
275
|
+
"name": self._name,
|
|
276
|
+
"type": "polygons",
|
|
277
|
+
"vertices_list": vlist,
|
|
278
|
+
"color": d.get("edgecolors", d.get("color", "#ff0000")),
|
|
279
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
280
|
+
}
|
|
281
|
+
cp = d.get("clip_path")
|
|
282
|
+
if cp is not None:
|
|
283
|
+
cp_arr = np.asarray(cp, dtype=float)
|
|
284
|
+
if cp_arr.ndim == 2 and cp_arr.shape[1] == 2 and len(cp_arr) >= 3:
|
|
285
|
+
wire["clip_path"] = cp_arr.tolist()
|
|
286
|
+
_apply_fill_color(wire, d)
|
|
287
|
+
|
|
288
|
+
elif t == "texts":
|
|
289
|
+
offsets = _offsets_2d(d["offsets"])
|
|
290
|
+
texts = list(d.get("texts", []))
|
|
291
|
+
wire = {
|
|
292
|
+
"id": group_id,
|
|
293
|
+
"name": self._name,
|
|
294
|
+
"type": "texts",
|
|
295
|
+
"offsets": offsets,
|
|
296
|
+
"texts": texts,
|
|
297
|
+
"color": d.get("color", d.get("edgecolors", "#ff0000")),
|
|
298
|
+
"fontsize": int(d.get("fontsize", 12)),
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# ── 1D-only types ───────────────────────────────────────────────────
|
|
302
|
+
elif t == "points":
|
|
303
|
+
offsets = _offsets_1d(d["offsets"])
|
|
304
|
+
n = len(offsets)
|
|
305
|
+
wire = {
|
|
306
|
+
"id": group_id,
|
|
307
|
+
"name": self._name,
|
|
308
|
+
"type": "points",
|
|
309
|
+
"offsets": offsets,
|
|
310
|
+
"sizes": _broadcast(d.get("sizes", 5), n),
|
|
311
|
+
"color": d.get("edgecolors", d.get("color", "#ff0000")),
|
|
312
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
313
|
+
}
|
|
314
|
+
_apply_fill_color(wire, d)
|
|
315
|
+
|
|
316
|
+
elif t == "vlines":
|
|
317
|
+
offsets = _offsets_1d(d["offsets"])
|
|
318
|
+
wire = {
|
|
319
|
+
"id": group_id,
|
|
320
|
+
"name": self._name,
|
|
321
|
+
"type": "vlines",
|
|
322
|
+
"offsets": [[row[0]] for row in offsets],
|
|
323
|
+
"color": d.get("color", d.get("edgecolors", "#ff0000")),
|
|
324
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
elif t == "hlines":
|
|
328
|
+
offsets = _offsets_1d(d["offsets"])
|
|
329
|
+
wire = {
|
|
330
|
+
"id": group_id,
|
|
331
|
+
"name": self._name,
|
|
332
|
+
"type": "hlines",
|
|
333
|
+
"offsets": [[row[0]] for row in offsets],
|
|
334
|
+
"color": d.get("color", d.get("edgecolors", "#ff0000")),
|
|
335
|
+
"linewidth": float(d.get("linewidths", 1.5)),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
elif t == "raster":
|
|
339
|
+
# A single RGBA image drawn between data-coord ``extent`` corners.
|
|
340
|
+
# ``image_b64`` is the heavy payload — Plot1D.to_state_dict hoists it
|
|
341
|
+
# into the deduped geometry channel so it is sent once, not per frame.
|
|
342
|
+
ext = [float(v) for v in d["extent"]]
|
|
343
|
+
wire = {
|
|
344
|
+
"id": group_id,
|
|
345
|
+
"name": self._name,
|
|
346
|
+
"type": "raster",
|
|
347
|
+
"image_b64": d["image_b64"],
|
|
348
|
+
"image_width": int(d["image_width"]),
|
|
349
|
+
"image_height": int(d["image_height"]),
|
|
350
|
+
"extent": ext,
|
|
351
|
+
"smooth": bool(d.get("smooth", False)),
|
|
352
|
+
}
|
|
353
|
+
cp = d.get("clip_path")
|
|
354
|
+
if cp is not None:
|
|
355
|
+
cp_arr = np.asarray(cp, dtype=float)
|
|
356
|
+
if cp_arr.ndim == 2 and cp_arr.shape[1] == 2 and len(cp_arr) >= 3:
|
|
357
|
+
wire["clip_path"] = cp_arr.tolist()
|
|
358
|
+
|
|
359
|
+
else:
|
|
360
|
+
raise ValueError(f"Unknown marker type: {t!r}")
|
|
361
|
+
|
|
362
|
+
# ── coordinate transform (always emitted; defaults to "data") ──────
|
|
363
|
+
wire["transform"] = d.get("transform", "data")
|
|
364
|
+
|
|
365
|
+
# ── common optional fields ──────────────────────────────────────────
|
|
366
|
+
label = d.get("label")
|
|
367
|
+
if label is not None:
|
|
368
|
+
wire["label"] = str(label)
|
|
369
|
+
labels = d.get("labels")
|
|
370
|
+
if labels is not None:
|
|
371
|
+
wire["labels"] = [str(lb) for lb in labels]
|
|
372
|
+
|
|
373
|
+
# ── hover colours (optional) ────────────────────────────────────────
|
|
374
|
+
# Applied to all markers in the group when any one is hovered.
|
|
375
|
+
# Names mirror the base colour kwargs: edgecolors → hover_edgecolors,
|
|
376
|
+
# facecolors → hover_facecolors. Any CSS colour string is accepted.
|
|
377
|
+
hec = d.get("hover_edgecolors")
|
|
378
|
+
if hec is not None:
|
|
379
|
+
wire["hover_color"] = str(hec)
|
|
380
|
+
hfc = d.get("hover_facecolors")
|
|
381
|
+
if hfc is not None:
|
|
382
|
+
wire["hover_facecolor"] = str(hfc)
|
|
383
|
+
|
|
384
|
+
return wire
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
# MarkerTypeDict
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
class MarkerTypeDict:
|
|
392
|
+
"""Dict-like container for all named groups of one marker type.
|
|
393
|
+
|
|
394
|
+
Any modification (``__setitem__``, ``__delitem__``) automatically triggers
|
|
395
|
+
the ``_push_fn`` callback so the plot re-renders.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
marker_type : str
|
|
400
|
+
Type of markers (e.g., 'circles', 'arrows', 'lines').
|
|
401
|
+
push_fn : callable
|
|
402
|
+
Zero-arg callback to trigger re-render on mutations.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
def __init__(self, marker_type: str, push_fn):
|
|
406
|
+
self._type = marker_type
|
|
407
|
+
self._push_fn = push_fn
|
|
408
|
+
self._groups: dict[str, MarkerGroup] = {}
|
|
409
|
+
|
|
410
|
+
# ------------------------------------------------------------------
|
|
411
|
+
# dict-like interface
|
|
412
|
+
def __getitem__(self, name: str) -> MarkerGroup:
|
|
413
|
+
"""Return a MarkerGroup by name.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
name : str
|
|
418
|
+
Name of the marker group.
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
MarkerGroup
|
|
423
|
+
The requested marker group.
|
|
424
|
+
|
|
425
|
+
Raises
|
|
426
|
+
------
|
|
427
|
+
KeyError
|
|
428
|
+
If the name is not found.
|
|
429
|
+
"""
|
|
430
|
+
return self._groups[name]
|
|
431
|
+
|
|
432
|
+
def __setitem__(self, name: str, group: MarkerGroup) -> None:
|
|
433
|
+
"""Register a MarkerGroup and trigger re-render.
|
|
434
|
+
|
|
435
|
+
Parameters
|
|
436
|
+
----------
|
|
437
|
+
name : str
|
|
438
|
+
Name to register the group under.
|
|
439
|
+
group : MarkerGroup
|
|
440
|
+
The marker group object.
|
|
441
|
+
"""
|
|
442
|
+
self._groups[name] = group
|
|
443
|
+
self._push_fn()
|
|
444
|
+
|
|
445
|
+
def __delitem__(self, name: str) -> None:
|
|
446
|
+
"""Remove a MarkerGroup by name and trigger re-render.
|
|
447
|
+
|
|
448
|
+
Parameters
|
|
449
|
+
----------
|
|
450
|
+
name : str
|
|
451
|
+
Name of the group to remove.
|
|
452
|
+
|
|
453
|
+
Raises
|
|
454
|
+
------
|
|
455
|
+
KeyError
|
|
456
|
+
If the name is not found.
|
|
457
|
+
"""
|
|
458
|
+
del self._groups[name]
|
|
459
|
+
self._push_fn()
|
|
460
|
+
|
|
461
|
+
def __contains__(self, name: object) -> bool:
|
|
462
|
+
"""Check if a named group exists.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
name : object
|
|
467
|
+
Name to check.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
bool
|
|
472
|
+
True if the group exists.
|
|
473
|
+
"""
|
|
474
|
+
return name in self._groups
|
|
475
|
+
|
|
476
|
+
def __iter__(self):
|
|
477
|
+
"""Iterate over group names."""
|
|
478
|
+
return iter(self._groups)
|
|
479
|
+
|
|
480
|
+
def __len__(self) -> int:
|
|
481
|
+
"""Return the number of groups."""
|
|
482
|
+
return len(self._groups)
|
|
483
|
+
|
|
484
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
485
|
+
return f"MarkerTypeDict(type={self._type!r}, groups={list(self._groups)})"
|
|
486
|
+
|
|
487
|
+
def keys(self):
|
|
488
|
+
"""Return group names."""
|
|
489
|
+
return self._groups.keys()
|
|
490
|
+
|
|
491
|
+
def values(self):
|
|
492
|
+
"""Return MarkerGroup objects."""
|
|
493
|
+
return self._groups.values()
|
|
494
|
+
|
|
495
|
+
def items(self):
|
|
496
|
+
"""Return (name, MarkerGroup) pairs."""
|
|
497
|
+
return self._groups.items()
|
|
498
|
+
|
|
499
|
+
def pop(self, name: str, *args):
|
|
500
|
+
"""Remove and return a MarkerGroup by name.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
name : str
|
|
505
|
+
Name of the group to remove.
|
|
506
|
+
*args : optional
|
|
507
|
+
Default value if name is not found.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
MarkerGroup
|
|
512
|
+
The removed group, or default value if provided.
|
|
513
|
+
|
|
514
|
+
Raises
|
|
515
|
+
------
|
|
516
|
+
KeyError
|
|
517
|
+
If name is not found and no default is provided.
|
|
518
|
+
"""
|
|
519
|
+
result = self._groups.pop(name, *args)
|
|
520
|
+
self._push_fn()
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
# ------------------------------------------------------------------
|
|
524
|
+
def _add(self, name: str, kwargs: dict) -> "MarkerGroup":
|
|
525
|
+
"""Internal: create and register a MarkerGroup without double-pushing."""
|
|
526
|
+
g = MarkerGroup(self._type, name, kwargs, self._push_fn, parent=self)
|
|
527
|
+
self._groups[name] = g
|
|
528
|
+
return g
|
|
529
|
+
|
|
530
|
+
def to_wire_list(self) -> list:
|
|
531
|
+
"""Serialise all groups to a list of wire-format dicts."""
|
|
532
|
+
out = []
|
|
533
|
+
for gid, g in self._groups.items():
|
|
534
|
+
out.append(g.to_wire(gid))
|
|
535
|
+
return out
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# ---------------------------------------------------------------------------
|
|
539
|
+
# MarkerRegistry
|
|
540
|
+
# ---------------------------------------------------------------------------
|
|
541
|
+
|
|
542
|
+
class MarkerRegistry:
|
|
543
|
+
"""Top-level two-level marker registry for a plot.
|
|
544
|
+
|
|
545
|
+
Usage::
|
|
546
|
+
|
|
547
|
+
plot.markers["circles"]["group1"].set(offsets=new_offsets)
|
|
548
|
+
|
|
549
|
+
``plot.markers`` is a ``MarkerRegistry``. Indexing by type returns a
|
|
550
|
+
:class:`MarkerTypeDict` (auto-created on first access).
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
# Known marker types — used for validation and auto-naming.
|
|
554
|
+
_KNOWN_2D = frozenset({
|
|
555
|
+
"circles", "arrows", "ellipses", "lines",
|
|
556
|
+
"rectangles", "squares", "polygons", "texts",
|
|
557
|
+
})
|
|
558
|
+
_KNOWN_1D = frozenset({
|
|
559
|
+
"points", "vlines", "hlines", "lines", "rectangles",
|
|
560
|
+
"ellipses", "polygons", "texts", "arrows", "squares", "raster",
|
|
561
|
+
})
|
|
562
|
+
# pcolormesh panels only support points (circles) and line segments
|
|
563
|
+
_KNOWN_MESH = frozenset({"circles", "lines"})
|
|
564
|
+
|
|
565
|
+
def __init__(self, push_fn, allowed: frozenset | None = None):
|
|
566
|
+
"""
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
push_fn :
|
|
570
|
+
Callable that re-serialises the full registry and writes to the
|
|
571
|
+
Figure trait.
|
|
572
|
+
allowed :
|
|
573
|
+
Set of allowed type names. ``None`` means all known types.
|
|
574
|
+
"""
|
|
575
|
+
self._push_fn = push_fn
|
|
576
|
+
self._allowed = allowed
|
|
577
|
+
self._types: dict[str, MarkerTypeDict] = {}
|
|
578
|
+
|
|
579
|
+
# ------------------------------------------------------------------
|
|
580
|
+
def __getitem__(self, marker_type: str) -> MarkerTypeDict:
|
|
581
|
+
"""Return the MarkerTypeDict for a type, auto-creating if needed.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
marker_type : str
|
|
586
|
+
Type name (e.g., 'circles', 'lines', 'arrows').
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
MarkerTypeDict
|
|
591
|
+
The dict of named groups for this type.
|
|
592
|
+
|
|
593
|
+
Raises
|
|
594
|
+
------
|
|
595
|
+
ValueError
|
|
596
|
+
If the type is not in the allowed set (if one was provided).
|
|
597
|
+
"""
|
|
598
|
+
if self._allowed is not None and marker_type not in self._allowed:
|
|
599
|
+
raise ValueError(
|
|
600
|
+
f"Marker type '{marker_type}' is not allowed on this panel. "
|
|
601
|
+
f"Allowed types: {sorted(self._allowed)}"
|
|
602
|
+
)
|
|
603
|
+
if marker_type not in self._types:
|
|
604
|
+
self._types[marker_type] = MarkerTypeDict(marker_type, self._push_fn)
|
|
605
|
+
return self._types[marker_type]
|
|
606
|
+
|
|
607
|
+
def __contains__(self, marker_type: object) -> bool:
|
|
608
|
+
"""Check if a marker type has any registered groups."""
|
|
609
|
+
return marker_type in self._types
|
|
610
|
+
|
|
611
|
+
def __iter__(self):
|
|
612
|
+
"""Iterate over marker types that have registered groups."""
|
|
613
|
+
return iter(self._types)
|
|
614
|
+
|
|
615
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
616
|
+
return f"MarkerRegistry(types={list(self._types)})"
|
|
617
|
+
|
|
618
|
+
# ------------------------------------------------------------------
|
|
619
|
+
def _auto_name(self, marker_type: str) -> str:
|
|
620
|
+
"""Return the next auto-generated name like ``circles_1``, ``circles_2``…
|
|
621
|
+
|
|
622
|
+
Never reuses an existing key (monotonically increasing).
|
|
623
|
+
"""
|
|
624
|
+
td = self._types.get(marker_type)
|
|
625
|
+
if td is None or len(td) == 0:
|
|
626
|
+
return f"{marker_type}_1"
|
|
627
|
+
# Find the highest existing integer suffix
|
|
628
|
+
max_n = 0
|
|
629
|
+
for key in td.keys():
|
|
630
|
+
prefix = f"{marker_type}_"
|
|
631
|
+
if key.startswith(prefix):
|
|
632
|
+
try:
|
|
633
|
+
n = int(key[len(prefix):])
|
|
634
|
+
if n > max_n:
|
|
635
|
+
max_n = n
|
|
636
|
+
except ValueError:
|
|
637
|
+
pass
|
|
638
|
+
return f"{marker_type}_{max_n + 1}"
|
|
639
|
+
|
|
640
|
+
def add(self, marker_type: str, name: str | None = None, **kwargs) -> MarkerGroup:
|
|
641
|
+
"""Add a marker group, returning the :class:`MarkerGroup`.
|
|
642
|
+
|
|
643
|
+
Parameters
|
|
644
|
+
----------
|
|
645
|
+
marker_type : str
|
|
646
|
+
Type string, e.g. ``'circles'``, ``'lines'``, ``'arrows'``.
|
|
647
|
+
name : str, optional
|
|
648
|
+
Group name. Auto-generated (``'circles_1'`` etc.) if ``None``.
|
|
649
|
+
**kwargs : dict
|
|
650
|
+
Matplotlib-style kwargs for the group (offsets, radius, colors, etc.).
|
|
651
|
+
|
|
652
|
+
Returns
|
|
653
|
+
-------
|
|
654
|
+
MarkerGroup
|
|
655
|
+
The created marker group. Call ``.set()`` to update, or access
|
|
656
|
+
properties as attributes.
|
|
657
|
+
|
|
658
|
+
Examples
|
|
659
|
+
--------
|
|
660
|
+
>>> group = registry.add("circles", name="my_circles", offsets=[[10, 20]], radius=5)
|
|
661
|
+
>>> group.set(radius=8)
|
|
662
|
+
"""
|
|
663
|
+
if name is None:
|
|
664
|
+
name = self._auto_name(marker_type)
|
|
665
|
+
td = self[marker_type] # auto-creates MarkerTypeDict
|
|
666
|
+
g = td._add(name, kwargs) # create without double push
|
|
667
|
+
self._push_fn() # single push after group is ready
|
|
668
|
+
return g
|
|
669
|
+
|
|
670
|
+
def remove(self, marker_type: str, name: str) -> None:
|
|
671
|
+
"""Remove a named marker group and trigger re-render.
|
|
672
|
+
|
|
673
|
+
Parameters
|
|
674
|
+
----------
|
|
675
|
+
marker_type : str
|
|
676
|
+
Type of the group.
|
|
677
|
+
name : str
|
|
678
|
+
Name of the group to remove.
|
|
679
|
+
|
|
680
|
+
Raises
|
|
681
|
+
------
|
|
682
|
+
KeyError
|
|
683
|
+
If the type or name is not found.
|
|
684
|
+
"""
|
|
685
|
+
del self[marker_type][name] # MarkerTypeDict.__delitem__ pushes
|
|
686
|
+
|
|
687
|
+
def clear(self) -> None:
|
|
688
|
+
"""Remove all markers of all types and trigger re-render."""
|
|
689
|
+
self._types.clear()
|
|
690
|
+
self._push_fn()
|
|
691
|
+
|
|
692
|
+
def to_wire_list(self) -> list:
|
|
693
|
+
"""Flatten the full registry to a list of wire-format dicts.
|
|
694
|
+
|
|
695
|
+
Returns
|
|
696
|
+
-------
|
|
697
|
+
list of dict
|
|
698
|
+
Wire-format dicts ready for JSON serialization and JS rendering.
|
|
699
|
+
"""
|
|
700
|
+
out = []
|
|
701
|
+
for td in self._types.values():
|
|
702
|
+
out.extend(td.to_wire_list())
|
|
703
|
+
return out
|
|
704
|
+
|