anyplotlib 0.1.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.
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.0.dist-info/METADATA +160 -0
  40. anyplotlib-0.1.0.dist-info/RECORD +42 -0
  41. anyplotlib-0.1.0.dist-info/WHEEL +4 -0
  42. anyplotlib-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1376 @@
1
+ """
2
+ plot1d/_plot1d.py
3
+ =================
4
+ 1-D line panel (Plot1D) and its line helper (Line1D).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid as _uuid
10
+
11
+ import numpy as np
12
+ from typing import Callable
13
+
14
+ from anyplotlib._base_plot import _BasePlot, _PanelMixin, _MarkerMixin
15
+ from anyplotlib.markers import MarkerRegistry
16
+ from anyplotlib.callbacks import CallbackRegistry
17
+ from anyplotlib.widgets import (
18
+ Widget,
19
+ VLineWidget as _VLineWidget,
20
+ HLineWidget as _HLineWidget,
21
+ RangeWidget as _RangeWidget,
22
+ PointWidget as _PointWidget,
23
+ )
24
+ from anyplotlib._utils import _norm_linestyle, _arr_to_b64, _to_rgba_u8
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Line1D — per-line handle
29
+ # ---------------------------------------------------------------------------
30
+
31
+ class Line1D:
32
+ """Handle to a single line on a :class:`Plot1D` panel.
33
+
34
+ Returned by :meth:`Plot1D.add_line`. Use it to update the line data,
35
+ register event handlers scoped to just that line, or to remove it later.
36
+
37
+ Attributes
38
+ ----------
39
+ id : str | None
40
+ ``None`` for the primary line; an 8-character UUID string for
41
+ overlay lines added with :meth:`Plot1D.add_line`.
42
+ """
43
+
44
+ def __init__(self, plot: "Plot1D", lid: str | None):
45
+ self._plot = plot
46
+ self._lid = lid
47
+
48
+ @property
49
+ def id(self) -> str | None:
50
+ return self._lid
51
+
52
+ def __str__(self) -> str:
53
+ return "" if self._lid is None else self._lid
54
+
55
+ def __repr__(self) -> str:
56
+ return f"Line1D(id={self._lid!r})"
57
+
58
+ def __eq__(self, other) -> bool:
59
+ if isinstance(other, Line1D):
60
+ return self._lid == other._lid
61
+ if isinstance(other, str):
62
+ return self._lid == other
63
+ return NotImplemented
64
+
65
+ def __hash__(self) -> int:
66
+ return hash(self._lid)
67
+
68
+ # ------------------------------------------------------------------
69
+ def add_event_handler(self, fn_or_type, *args, **kwargs):
70
+ """Register a handler scoped to this line only.
71
+
72
+ Wraps the plot-level pointer_move / pointer_down handler
73
+ with a line_id filter. Only pointer_move and pointer_down
74
+ are meaningful on a line handle.
75
+ """
76
+ target_lid = self._lid
77
+
78
+ if callable(fn_or_type):
79
+ fn = fn_or_type
80
+ types = args
81
+ return self._wrap_and_register(fn, types, target_lid, **kwargs)
82
+ else:
83
+ all_types = (fn_or_type,) + args
84
+ def _decorator(fn):
85
+ return self._wrap_and_register(fn, all_types, target_lid, **kwargs)
86
+ return _decorator
87
+
88
+ def _wrap_and_register(self, fn, types, target_lid, **kwargs):
89
+ from functools import wraps
90
+ @wraps(fn)
91
+ def _filtered(event):
92
+ if event.line_id == target_lid:
93
+ fn(event)
94
+ _filtered.__wrapped__ = fn
95
+ return self._plot.add_event_handler(_filtered, *types, **kwargs)
96
+
97
+ def remove_handler(self, cid_or_fn, *types):
98
+ """Remove a handler registered via this line handle."""
99
+ self._plot.remove_handler(cid_or_fn, *types)
100
+
101
+ def set_data(self, y: "np.ndarray", x_axis=None) -> None:
102
+ """Update the y-data (and optionally x-axis) of this overlay line.
103
+
104
+ The y-axis range is recomputed and the panel re-renders immediately.
105
+
106
+ Parameters
107
+ ----------
108
+ y : array-like, shape (N,)
109
+ New y values. Must be 1-D.
110
+ x_axis : array-like, shape (N,), optional
111
+ New x coordinates. If omitted the existing x-axis is kept.
112
+
113
+ Raises
114
+ ------
115
+ ValueError
116
+ If called on the primary line (use :meth:`Plot1D.set_data`
117
+ instead), or if *y* is not 1-D.
118
+ KeyError
119
+ If this line has already been removed.
120
+ """
121
+ if self._lid is None:
122
+ raise ValueError(
123
+ "Cannot call set_data() on the primary line; "
124
+ "use plot.set_data() instead."
125
+ )
126
+ y = np.asarray(y, dtype=float)
127
+ if y.ndim != 1:
128
+ raise ValueError("y must be 1-D")
129
+ for entry in self._plot._state["extra_lines"]:
130
+ if entry["id"] == self._lid:
131
+ entry["data"] = y
132
+ if x_axis is not None:
133
+ entry["x_axis"] = np.asarray(x_axis, dtype=float)
134
+ break
135
+ else:
136
+ raise KeyError(self._lid)
137
+ self._plot._recompute_data_range()
138
+ self._plot._push()
139
+
140
+ def remove(self) -> None:
141
+ """Remove this overlay line from its parent plot."""
142
+ if self._lid is None:
143
+ raise ValueError("Cannot remove the primary line via Line1D.remove().")
144
+ self._plot.remove_line(self._lid)
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Plot1D
149
+ # ---------------------------------------------------------------------------
150
+
151
+ class Plot1D(_BasePlot, _PanelMixin, _MarkerMixin):
152
+ """1-D line plot panel returned by :meth:`Axes.plot`.
153
+
154
+ All display state is stored in a plain ``_state`` dict. Every mutation
155
+ ends with :meth:`_push`, which serialises the state to the parent
156
+ ``Figure`` trait so the JS renderer picks up the change immediately.
157
+
158
+ Supported line properties
159
+ -------------------------
160
+ Set at construction time via :meth:`Axes.plot` or updated afterwards
161
+ with the corresponding setter:
162
+
163
+ .. list-table::
164
+ :header-rows: 1
165
+ :widths: 18 18 64
166
+
167
+ * - Parameter
168
+ - Default
169
+ - Description
170
+ * - ``color``
171
+ - ``"#4fc3f7"``
172
+ - CSS colour string for the primary line.
173
+ * - ``linewidth``
174
+ - ``1.5``
175
+ - Stroke width in pixels.
176
+ * - ``linestyle`` (``ls``)
177
+ - ``"solid"``
178
+ - Dash pattern: ``"solid"``, ``"dashed"``, ``"dotted"``,
179
+ ``"dashdot"``. Shorthands ``"-"``, ``"--"``, ``":"``,
180
+ ``"-."`` also accepted.
181
+ * - ``alpha``
182
+ - ``1.0``
183
+ - Line opacity (0 = transparent, 1 = fully opaque).
184
+ * - ``marker``
185
+ - ``"none"``
186
+ - Per-point symbol: ``"o"`` (circle), ``"s"`` (square),
187
+ ``"^"``/``"v"`` (triangles), ``"D"`` (diamond),
188
+ ``"+"``/``"x"`` (stroke-only), or ``"none"``.
189
+ * - ``markersize``
190
+ - ``4.0``
191
+ - Marker radius / half-side in pixels.
192
+ * - ``label``
193
+ - ``""``
194
+ - Legend label (empty string = no legend entry).
195
+
196
+
197
+ Public API summary
198
+ ------------------
199
+
200
+ **Data**
201
+ :meth:`update` — replace y-data (and optionally the x-axis /
202
+ units) without recreating the panel.
203
+
204
+ **Overlay lines**
205
+ :meth:`add_line` / :meth:`remove_line` / :meth:`clear_lines` —
206
+ overlay additional curves on the same axes.
207
+
208
+ **Shaded spans**
209
+ :meth:`add_span` / :meth:`remove_span` / :meth:`clear_spans` —
210
+ highlight a region along the x- or y-axis.
211
+
212
+ **View control**
213
+ :meth:`set_view` / :meth:`reset_view` — programmatic pan/zoom
214
+ (users can also pan/zoom interactively with the mouse; press **R**
215
+ to reset).
216
+
217
+ **Interactive widgets**
218
+ :meth:`add_vline_widget` / :meth:`add_hline_widget` /
219
+ :meth:`add_range_widget` — draggable overlays that report their
220
+ position back to Python via callbacks. Manage them with
221
+ :meth:`get_widget`, :meth:`remove_widget`, :meth:`list_widgets`,
222
+ and :meth:`clear_widgets`.
223
+
224
+ **Static marker collections**
225
+ :meth:`add_points` / :meth:`add_circles` / :meth:`add_vlines` /
226
+ :meth:`add_hlines` / :meth:`add_arrows` / :meth:`add_ellipses` /
227
+ :meth:`add_lines` / :meth:`add_rectangles` / :meth:`add_squares` /
228
+ :meth:`add_polygons` / :meth:`add_texts` — fixed overlays
229
+ positioned at explicit data coordinates. Access them via
230
+ ``plot.markers[type][name]`` and manage with :meth:`remove_marker`,
231
+ :meth:`clear_markers`, and :meth:`list_markers`.
232
+
233
+ **Callbacks**
234
+ :meth:`on_changed` / :meth:`on_release` / :meth:`on_click` /
235
+ :meth:`on_key` — react to pan/zoom frames, mouse clicks, and
236
+ key-presses. Remove a handler with :meth:`disconnect`.
237
+ """
238
+
239
+ def __init__(self, data: np.ndarray,
240
+ x_axis=None,
241
+ units: str = "px",
242
+ y_units: str = "",
243
+ color: str = "#4fc3f7",
244
+ linewidth: float = 1.5,
245
+ linestyle: str = "solid",
246
+ alpha: float = 1.0,
247
+ marker: str = "none",
248
+ markersize: float = 4.0,
249
+ label: str = "",
250
+ yscale: str = "linear"):
251
+ self._id: str = ""
252
+ self._fig: object = None
253
+
254
+ if yscale not in ("linear", "log"):
255
+ raise ValueError("yscale must be 'linear' or 'log'")
256
+
257
+ data = np.asarray(data, dtype=float)
258
+ if data.ndim != 1:
259
+ raise ValueError(f"data must be 1-D, got {data.shape}")
260
+ n = len(data)
261
+ if x_axis is None:
262
+ x_axis = np.arange(n, dtype=float)
263
+ x_axis = np.asarray(x_axis, dtype=float)
264
+ if len(x_axis) != n:
265
+ raise ValueError("x_axis length must match data length")
266
+
267
+ dmin = float(np.nanmin(data))
268
+ dmax = float(np.nanmax(data))
269
+ pad = (dmax - dmin) * 0.05 if dmax > dmin else 0.5
270
+ dmin -= pad; dmax += pad
271
+
272
+ self._state: dict = {
273
+ "kind": "1d",
274
+ "data": data, # numpy float64 — encoded in to_state_dict()
275
+ "x_axis": x_axis, # numpy float64 — encoded in to_state_dict()
276
+ "units": units,
277
+ "y_units": y_units,
278
+ "data_min": dmin,
279
+ "data_max": dmax,
280
+ "view_x0": 0.0,
281
+ "view_x1": 1.0,
282
+ "line_color": color,
283
+ "line_linewidth": float(linewidth),
284
+ "line_linestyle": _norm_linestyle(linestyle),
285
+ "line_alpha": float(alpha),
286
+ "line_marker": marker if marker is not None else "none",
287
+ "line_markersize": float(markersize),
288
+ "line_label": label,
289
+ "extra_lines": [],
290
+ "spans": [],
291
+ "overlay_widgets": [],
292
+ "markers": [],
293
+ "pointer_settled_ms": 0,
294
+ "pointer_settled_delta": 4,
295
+ "yscale": yscale,
296
+ # Annotation labels
297
+ "title": "",
298
+ # Explicit y-range override: [ymin, ymax] or None (auto)
299
+ "y_range": None,
300
+ # Visibility toggles
301
+ "axis_visible": True,
302
+ "x_ticks_visible": True,
303
+ "y_ticks_visible": True,
304
+ "_view_from_python": False,
305
+ }
306
+
307
+ self.markers = MarkerRegistry(self._push_markers,
308
+ allowed=MarkerRegistry._KNOWN_1D)
309
+ self.callbacks = CallbackRegistry()
310
+ self._widgets: dict[str, Widget] = {}
311
+
312
+ def to_state_dict(self) -> dict:
313
+ d = dict(self._state)
314
+ # Replace numpy arrays with b64-encoded strings for the wire format.
315
+ data_arr = d.pop("data")
316
+ x_arr = d.pop("x_axis")
317
+ d["data_b64"] = _arr_to_b64(data_arr, np.float64)
318
+ d["x_axis_b64"] = _arr_to_b64(x_arr, np.float64)
319
+ # Encode extra-line arrays too
320
+ new_extra = []
321
+ for ex in d["extra_lines"]:
322
+ ex2 = dict(ex)
323
+ ex2["data_b64"] = _arr_to_b64(ex2.pop("data"), np.float64)
324
+ ex2["x_axis_b64"] = _arr_to_b64(
325
+ np.asarray(ex2.pop("x_axis"), dtype=np.float64), np.float64)
326
+ new_extra.append(ex2)
327
+ d["extra_lines"] = new_extra
328
+ d["overlay_widgets"] = [w.to_dict() for w in self._widgets.values()]
329
+ markers = self.markers.to_wire_list()
330
+ # Hoist any raster image bytes into the deduped geometry channel so a
331
+ # view-only push never re-serialises the (potentially large) RGBA blob.
332
+ # The lightweight marker dict keeps only id + extent/clip_path/transform;
333
+ # JS re-joins them via st.raster_geom[id] (see _applyGeom / draw1d).
334
+ raster_geom: dict = {}
335
+ for ms in markers:
336
+ if ms.get("type") == "raster" and "image_b64" in ms:
337
+ raster_geom[ms["id"]] = {
338
+ "image_b64": ms.pop("image_b64"),
339
+ "image_width": ms["image_width"],
340
+ "image_height": ms["image_height"],
341
+ }
342
+ if raster_geom:
343
+ d["raster_geom"] = raster_geom
344
+ d["markers"] = markers
345
+ return d
346
+
347
+ @property
348
+ def line(self) -> "Line1D":
349
+ """Handle for the primary line, enabling per-line callbacks.
350
+
351
+ Returns a :class:`Line1D` with ``id=None`` so you can register
352
+ hover / click handlers scoped to just the primary line::
353
+
354
+ @plot.line.on_click
355
+ def on_primary_click(event):
356
+ print(f"primary line clicked at x={event.x:.3f}")
357
+ """
358
+ return Line1D(self, None)
359
+
360
+ # ------------------------------------------------------------------
361
+ # Data
362
+ # ------------------------------------------------------------------
363
+ @property
364
+ def data(self) -> np.ndarray:
365
+ """The primary line's y-data (read-only).
366
+
367
+ Returns a float64 copy with ``writeable=False``. To replace the
368
+ data call :meth:`set_data`.
369
+ """
370
+ arr = self._state["data"].copy()
371
+ arr.flags.writeable = False
372
+ return arr
373
+
374
+ def set_data(self, data: np.ndarray, x_axis=None,
375
+ units: str | None = None, y_units: str | None = None) -> None:
376
+ """Replace the primary line's y-data and optionally its x-axis / units.
377
+
378
+ The y-axis range (``data_min`` / ``data_max``) is recomputed
379
+ automatically. The viewport is **not** reset — call
380
+ :meth:`reset_view` explicitly if needed.
381
+
382
+ Parameters
383
+ ----------
384
+ data : array-like, shape (N,)
385
+ New y values. Must be 1-D.
386
+ x_axis : array-like, shape (N,), optional
387
+ New x coordinates. If omitted and the length of *data* matches
388
+ the current x-axis, the existing x-axis is reused; otherwise it
389
+ is reset to ``0, 1, …, N-1``.
390
+ units : str, optional
391
+ New x-axis label. Unchanged if not supplied.
392
+ y_units : str, optional
393
+ New y-axis label. Unchanged if not supplied.
394
+ """
395
+ data = np.asarray(data, dtype=float)
396
+ if data.ndim != 1:
397
+ raise ValueError(f"data must be 1-D, got {data.shape}")
398
+ n = len(data)
399
+ if x_axis is None:
400
+ prev = self._state["x_axis"] # already a numpy array
401
+ x_axis = prev if len(prev) == n else np.arange(n, dtype=float)
402
+ x_axis = np.asarray(x_axis, dtype=float)
403
+
404
+ dmin = float(np.nanmin(data))
405
+ dmax = float(np.nanmax(data))
406
+ pad = (dmax - dmin) * 0.05 if dmax > dmin else 0.5
407
+
408
+ self._state["data"] = data
409
+ self._state["x_axis"] = x_axis
410
+ self._state["data_min"] = dmin - pad
411
+ self._state["data_max"] = dmax + pad
412
+ if units is not None: self._state["units"] = units
413
+ if y_units is not None: self._state["y_units"] = y_units
414
+ self._push()
415
+
416
+ def _recompute_data_range(self) -> None:
417
+ """Recompute data_min/data_max across the primary line and all overlays.
418
+
419
+ Called automatically whenever the set of lines changes so that every
420
+ curve stays fully visible.
421
+ """
422
+ all_vals = [self._state["data"]] # already a numpy float64 array
423
+ for ex in self._state["extra_lines"]:
424
+ d = ex.get("data")
425
+ if d is not None and len(d):
426
+ all_vals.append(d)
427
+ combined = np.concatenate(all_vals)
428
+ dmin = float(np.nanmin(combined))
429
+ dmax = float(np.nanmax(combined))
430
+ pad = (dmax - dmin) * 0.05 if dmax > dmin else 0.5
431
+ self._state["data_min"] = dmin - pad
432
+ self._state["data_max"] = dmax + pad
433
+
434
+ # ------------------------------------------------------------------
435
+ # Extra lines
436
+ # ------------------------------------------------------------------
437
+ def add_line(self, data: np.ndarray, x_axis=None,
438
+ color: str = "#4fc3f7", linewidth: float = 1.5,
439
+ linestyle: str = "solid", ls: str | None = None,
440
+ alpha: float = 1.0,
441
+ marker: str = "none", markersize: float = 4.0,
442
+ label: str = "") -> "Line1D":
443
+ """Overlay an additional curve on this panel.
444
+
445
+ The y-axis range is automatically expanded to include the new data so
446
+ all lines remain fully visible.
447
+
448
+ Parameters
449
+ ----------
450
+ data : array-like, shape (N,)
451
+ Y values for the new line. Must be 1-D.
452
+ x_axis : array-like, shape (N,), optional
453
+ X coordinates. Defaults to the primary line's x-axis.
454
+ color : str, optional
455
+ CSS colour string. Default ``"#4fc3f7"``.
456
+ linewidth : float, optional
457
+ Stroke width in pixels. Default ``1.5``.
458
+ linestyle : str, optional
459
+ Dash pattern: ``"solid"``, ``"dashed"``, ``"dotted"``,
460
+ ``"dashdot"`` (or shorthands). Default ``"solid"``.
461
+ ls : str, optional
462
+ Short alias for *linestyle*.
463
+ alpha : float, optional
464
+ Line opacity (0–1). Default ``1.0``.
465
+ marker : str, optional
466
+ Per-point marker symbol (see :class:`Plot1D`). Default
467
+ ``"none"``.
468
+ markersize : float, optional
469
+ Marker radius / half-side in pixels. Default ``4.0``.
470
+ label : str, optional
471
+ Legend label. Default ``""`` (no legend entry).
472
+
473
+ Returns
474
+ -------
475
+ Line1D
476
+ A handle to the new overlay line. Use it to register
477
+ per-line hover/click callbacks or to remove the line later::
478
+
479
+ line = v.add_line(fit, color="#ffcc00", label="fit")
480
+ line.remove() # remove it
481
+ @line.on_click # per-line click handler
482
+ def clicked(event): ...
483
+ """
484
+ data = np.asarray(data, dtype=float)
485
+ if data.ndim != 1:
486
+ raise ValueError("data must be 1-D")
487
+ xa = (np.asarray(x_axis, dtype=float) if x_axis is not None
488
+ else self._state["x_axis"])
489
+ lid = str(_uuid.uuid4())[:8]
490
+ self._state["extra_lines"].append({
491
+ "id": lid,
492
+ "data": data,
493
+ "x_axis": xa,
494
+ "color": color,
495
+ "linewidth": float(linewidth),
496
+ "linestyle": _norm_linestyle(ls if ls is not None else linestyle),
497
+ "alpha": float(alpha),
498
+ "marker": marker if marker is not None else "none",
499
+ "markersize": float(markersize),
500
+ "label": label,
501
+ })
502
+ self._recompute_data_range()
503
+ self._push()
504
+ return Line1D(self, lid)
505
+
506
+ def remove_line(self, lid: "str | Line1D") -> None:
507
+ """Remove an overlay line by its ID or :class:`Line1D` handle.
508
+
509
+ The y-axis range is recomputed after removal.
510
+
511
+ Parameters
512
+ ----------
513
+ lid : str or Line1D
514
+ The value returned by :meth:`add_line`.
515
+
516
+ Raises
517
+ ------
518
+ KeyError
519
+ If *lid* does not match any overlay line.
520
+ """
521
+ if isinstance(lid, Line1D):
522
+ lid = lid._lid
523
+ before = len(self._state["extra_lines"])
524
+ self._state["extra_lines"] = [
525
+ e for e in self._state["extra_lines"] if e["id"] != lid]
526
+ if len(self._state["extra_lines"]) == before:
527
+ raise KeyError(lid)
528
+ self._recompute_data_range()
529
+ self._push()
530
+
531
+ def clear_lines(self) -> None:
532
+ """Remove all overlay lines, leaving the primary line intact.
533
+
534
+ The y-axis range is recomputed after clearing.
535
+ """
536
+ self._state["extra_lines"] = []
537
+ self._recompute_data_range()
538
+ self._push()
539
+
540
+ # ------------------------------------------------------------------
541
+ # Spans
542
+ # ------------------------------------------------------------------
543
+ def add_span(self, v0: float, v1: float,
544
+ axis: str = "x", color: str | None = None) -> str:
545
+ """Add a shaded span along the x- or y-axis.
546
+
547
+ Parameters
548
+ ----------
549
+ v0, v1 : float
550
+ Start and end of the span in data coordinates.
551
+ axis : ``"x"`` | ``"y"``, optional
552
+ Which axis the span runs along. Default ``"x"``.
553
+ color : str, optional
554
+ CSS colour string (supports alpha, e.g.
555
+ ``"rgba(255,200,0,0.2)"``). Defaults to a theme-appropriate
556
+ yellow tint.
557
+
558
+ Returns
559
+ -------
560
+ str
561
+ Span ID for use with :meth:`remove_span`.
562
+ """
563
+ sid = str(_uuid.uuid4())[:8]
564
+ self._state["spans"].append({
565
+ "id": sid, "v0": float(v0), "v1": float(v1),
566
+ "axis": axis, "color": color,
567
+ })
568
+ self._push()
569
+ return sid
570
+
571
+ def remove_span(self, sid: str) -> None:
572
+ """Remove a shaded span by its ID.
573
+
574
+ Parameters
575
+ ----------
576
+ sid : str
577
+ The ID returned by :meth:`add_span`.
578
+
579
+ Raises
580
+ ------
581
+ KeyError
582
+ If *sid* does not match any span.
583
+ """
584
+ before = len(self._state["spans"])
585
+ self._state["spans"] = [
586
+ s for s in self._state["spans"] if s["id"] != sid]
587
+ if len(self._state["spans"]) == before:
588
+ raise KeyError(sid)
589
+ self._push()
590
+
591
+ def clear_spans(self) -> None:
592
+ """Remove all shaded spans."""
593
+ self._state["spans"] = []
594
+ self._push()
595
+
596
+ # ------------------------------------------------------------------
597
+ # Overlay Widgets
598
+ # ------------------------------------------------------------------
599
+ def add_vline_widget(self, x: float, color: str = "#00e5ff") -> _VLineWidget:
600
+ """Add a draggable vertical-line overlay.
601
+
602
+ Parameters
603
+ ----------
604
+ x : float
605
+ Initial x position in data coordinates.
606
+ color : str, optional
607
+ CSS colour string. Default ``"#00e5ff"``.
608
+
609
+ Returns
610
+ -------
611
+ VLineWidget
612
+ Widget object. Register position callbacks with
613
+ :meth:`on_changed` / :meth:`on_release`.
614
+ """
615
+ widget = _VLineWidget(lambda: None, x=float(x), color=color)
616
+ widget._push_fn = self._make_widget_push_fn(widget)
617
+ self._widgets[widget.id] = widget
618
+ self._push()
619
+ return widget
620
+
621
+ def add_hline_widget(self, y: float, color: str = "#00e5ff") -> _HLineWidget:
622
+ """Add a draggable horizontal-line overlay.
623
+
624
+ Parameters
625
+ ----------
626
+ y : float
627
+ Initial y position in data coordinates.
628
+ color : str, optional
629
+ CSS colour string. Default ``"#00e5ff"``.
630
+
631
+ Returns
632
+ -------
633
+ HLineWidget
634
+ Widget object. Register position callbacks with
635
+ :meth:`on_changed` / :meth:`on_release`.
636
+ """
637
+ widget = _HLineWidget(lambda: None, y=float(y), color=color)
638
+ widget._push_fn = self._make_widget_push_fn(widget)
639
+ self._widgets[widget.id] = widget
640
+ self._push()
641
+ return widget
642
+
643
+ def add_range_widget(self, x0: float, x1: float,
644
+ color: str = "#00e5ff",
645
+ style: str = "band",
646
+ y: float = 0.0,
647
+ _push: bool = True) -> _RangeWidget:
648
+ """Add a draggable range overlay to this panel.
649
+
650
+ Parameters
651
+ ----------
652
+ x0, x1 : float
653
+ Initial left and right edges in data coordinates.
654
+ color : str, optional
655
+ CSS colour string. Default ``"#00e5ff"``.
656
+ style : {'band', 'fwhm'}, optional
657
+ Visual style. ``'band'`` (default) draws two vertical lines with
658
+ a translucent fill. ``'fwhm'`` draws two draggable circles
659
+ connected by a dashed horizontal line at *y* (the half-maximum
660
+ level), giving an ``o-------o`` FWHM indicator.
661
+ y : float, optional
662
+ Y-coordinate (data space) for the connecting line when
663
+ ``style='fwhm'``. Ignored when ``style='band'``. Default 0.
664
+ _push : bool, optional
665
+ Push state to JS immediately. Set to ``False`` when adding
666
+ several widgets at once; call :meth:`_push` manually afterward.
667
+
668
+ Returns
669
+ -------
670
+ RangeWidget
671
+ Widget object. Register position callbacks with
672
+ :meth:`on_changed` / :meth:`on_release`.
673
+ """
674
+ widget = _RangeWidget(lambda: None, x0=float(x0), x1=float(x1),
675
+ color=color, style=style, y=float(y))
676
+ widget._push_fn = self._make_widget_push_fn(widget)
677
+ self._widgets[widget.id] = widget
678
+ if _push:
679
+ self._push()
680
+ return widget
681
+
682
+ def add_point_widget(self, x: float, y: float,
683
+ color: str = "#00e5ff",
684
+ show_crosshair: bool = True,
685
+ _push: bool = True) -> _PointWidget:
686
+ """Add a freely-draggable control point to this panel.
687
+
688
+ Parameters
689
+ ----------
690
+ x : float
691
+ Initial x position in data coordinates.
692
+ y : float
693
+ Initial y position in data coordinates (value axis).
694
+ color : str, optional
695
+ CSS colour string. Default ``"#00e5ff"``.
696
+ show_crosshair : bool, optional
697
+ Draw dashed guide lines through the handle. Default ``True``.
698
+ Pass ``False`` for a plain dot with no guide lines.
699
+ _push : bool, optional
700
+ Push state to JS immediately. Set to ``False`` when adding
701
+ several widgets at once; call :meth:`_push` manually afterward.
702
+
703
+ Returns
704
+ -------
705
+ PointWidget
706
+ """
707
+ widget = _PointWidget(lambda: None, x=float(x), y=float(y), color=color,
708
+ show_crosshair=show_crosshair)
709
+ widget._push_fn = self._make_widget_push_fn(widget)
710
+ self._widgets[widget.id] = widget
711
+ if _push:
712
+ self._push()
713
+ return widget
714
+
715
+ # ------------------------------------------------------------------
716
+ # View control
717
+ # ------------------------------------------------------------------
718
+ def set_view(self, x0: float | None = None, x1: float | None = None) -> None:
719
+ """Programmatically set the visible x range.
720
+
721
+ Parameters
722
+ ----------
723
+ x0 : float, optional
724
+ Left edge of the view in data coordinates. ``None`` keeps the
725
+ current left edge.
726
+ x1 : float, optional
727
+ Right edge of the view in data coordinates. ``None`` keeps the
728
+ current right edge.
729
+ """
730
+ xarr = np.asarray(self._state["x_axis"])
731
+ if len(xarr) < 2:
732
+ return
733
+ xmin, xmax = float(xarr[0]), float(xarr[-1])
734
+ span = xmax - xmin or 1.0
735
+ f0 = 0.0 if x0 is None else max(0.0, min(1.0, (float(x0)-xmin)/span))
736
+ f1 = 1.0 if x1 is None else max(0.0, min(1.0, (float(x1)-xmin)/span))
737
+ with self._python_view_push():
738
+ self._state["view_x0"] = f0
739
+ self._state["view_x1"] = f1
740
+
741
+ def reset_view(self) -> None:
742
+ """Reset the view to show the full x range of the primary line."""
743
+ with self._python_view_push():
744
+ self._state["view_x0"] = 0.0
745
+ self._state["view_x1"] = 1.0
746
+
747
+ # ------------------------------------------------------------------
748
+ # Primary-line property setters
749
+ # ------------------------------------------------------------------
750
+
751
+ def set_color(self, color: str) -> None:
752
+ """Set the primary line colour.
753
+
754
+ Parameters
755
+ ----------
756
+ color : str
757
+ Any CSS colour string (hex, ``rgb()``, named colour, etc.).
758
+ """
759
+ self._state["line_color"] = color
760
+ self._push()
761
+
762
+ def set_linewidth(self, linewidth: float) -> None:
763
+ """Set the primary line stroke width.
764
+
765
+ Parameters
766
+ ----------
767
+ linewidth : float
768
+ Stroke width in pixels.
769
+ """
770
+ self._state["line_linewidth"] = float(linewidth)
771
+ self._push()
772
+
773
+ def set_linestyle(self, linestyle: str) -> None:
774
+ """Set the primary line dash pattern.
775
+
776
+ Parameters
777
+ ----------
778
+ linestyle : str
779
+ ``"solid"`` (``"-"``), ``"dashed"`` (``"--"``),
780
+ ``"dotted"`` (``":"``), or ``"dashdot"`` (``"-."``)
781
+ """
782
+ self._state["line_linestyle"] = _norm_linestyle(linestyle)
783
+ self._push()
784
+
785
+ def set_alpha(self, alpha: float) -> None:
786
+ """Set the primary line opacity.
787
+
788
+ Parameters
789
+ ----------
790
+ alpha : float
791
+ Opacity in the range 0 (transparent) to 1 (fully opaque).
792
+ """
793
+ self._state["line_alpha"] = float(alpha)
794
+ self._push()
795
+
796
+ def set_marker(self, marker: str, markersize: float | None = None) -> None:
797
+ """Set the primary line per-point marker symbol.
798
+
799
+ Parameters
800
+ ----------
801
+ marker : str
802
+ ``"o"``, ``"s"``, ``"^"``, ``"v"``, ``"D"``, ``"+"``,
803
+ ``"x"``, or ``"none"``.
804
+ markersize : float, optional
805
+ Marker radius / half-side in pixels. Unchanged if not supplied.
806
+ """
807
+ self._state["line_marker"] = marker if marker is not None else "none"
808
+ if markersize is not None:
809
+ self._state["line_markersize"] = float(markersize)
810
+ self._push()
811
+
812
+ @property
813
+ def color(self) -> str:
814
+ return self._state["line_color"]
815
+
816
+ @property
817
+ def x(self) -> np.ndarray:
818
+ return np.asarray(self._state["x_axis"])
819
+
820
+ @property
821
+ def y(self) -> np.ndarray:
822
+ return np.asarray(self._state["data"])
823
+
824
+ def set_xlabel(self, label: str, fontsize: float | None = None) -> None:
825
+ """Set the x-axis label.
826
+
827
+ Parameters
828
+ ----------
829
+ label : str
830
+ Label text. Supports the mini-TeX subset for scientific
831
+ notation, e.g. ``r"Energy ($10^{-3}$ eV)"`` or ``r"$\\Delta t$ (s)"``
832
+ — see :class:`~anyplotlib._base_plot._BasePlot` notes.
833
+ fontsize : float, optional
834
+ Font size in CSS pixels. Default 9. ``None`` keeps the
835
+ current size.
836
+ """
837
+ self._set_label("units", label, "x_label_size", fontsize)
838
+
839
+ def set_ylabel(self, label: str, fontsize: float | None = None) -> None:
840
+ """Set the y-axis label. Same semantics as :meth:`set_xlabel`."""
841
+ self._set_label("y_units", label, "y_label_size", fontsize)
842
+
843
+ def set_yscale(self, scale: str) -> None:
844
+ """Set the y-axis scale: ``'linear'`` or ``'log'``."""
845
+ if scale not in ("linear", "log"):
846
+ raise ValueError("scale must be 'linear' or 'log'")
847
+ self._state["yscale"] = scale
848
+ self._push()
849
+
850
+ def set_xlim(self, xmin: float, xmax: float) -> None:
851
+ self.set_view(x0=xmin, x1=xmax)
852
+
853
+ def set_ylim(self, ymin: float, ymax: float) -> None:
854
+ self._state["y_range"] = [float(ymin), float(ymax)]
855
+ self._push()
856
+
857
+ def get_ylim(self) -> tuple:
858
+ yr = self._state.get("y_range")
859
+ if yr is not None:
860
+ return (float(yr[0]), float(yr[1]))
861
+ return (float(self._state["data_min"]), float(self._state["data_max"]))
862
+
863
+ def get_xlim(self) -> tuple:
864
+ xarr = np.asarray(self._state["x_axis"])
865
+ if len(xarr) < 2:
866
+ return (0.0, 1.0)
867
+ xmin, xmax = float(xarr[0]), float(xarr[-1])
868
+ span = xmax - xmin or 1.0
869
+ x0 = xmin + self._state["view_x0"] * span
870
+ x1 = xmin + self._state["view_x1"] * span
871
+ return (x0, x1)
872
+
873
+ def get_xbound(self) -> tuple:
874
+ xarr = np.asarray(self._state["x_axis"])
875
+ return (float(xarr.min()), float(xarr.max()))
876
+
877
+ # ------------------------------------------------------------------
878
+ # Marker API (matplotlib-style kwargs → MarkerRegistry)
879
+ # ------------------------------------------------------------------
880
+ def add_circles(self, offsets, name=None, *, radius=5,
881
+ facecolors=None, edgecolors="#ff0000",
882
+ linewidths=1.5, alpha=0.3,
883
+ hover_edgecolors=None, hover_facecolors=None,
884
+ labels=None, label=None,
885
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
886
+ """Add circle markers at explicit (x, y) positions.
887
+
888
+ On 1-D panels circles are rendered as filled/stroked discs; *radius*
889
+ is in canvas pixels (not data units).
890
+
891
+ Parameters
892
+ ----------
893
+ offsets : array-like, shape (N, 2)
894
+ Marker positions as ``[[x0, y0], [x1, y1], …]`` in data
895
+ coordinates.
896
+ name : str, optional
897
+ Registry key. Auto-generated if omitted.
898
+ radius : float or array-like, optional
899
+ Radius in pixels. Scalar or per-marker array. Default ``5``.
900
+ facecolors : str or None, optional
901
+ Fill colour. ``None`` = no fill.
902
+ edgecolors : str, optional
903
+ Stroke colour. Default ``"#ff0000"``.
904
+ linewidths : float, optional
905
+ Stroke width in pixels. Default ``1.5``.
906
+ alpha : float, optional
907
+ Fill opacity (0–1). Default ``0.3``.
908
+ hover_edgecolors, hover_facecolors : str, optional
909
+ Colour overrides applied on mouse-hover.
910
+ labels : list of str, optional
911
+ Per-marker tooltip labels.
912
+ label : str, optional
913
+ Collection-level tooltip label.
914
+
915
+ Returns
916
+ -------
917
+ MarkerGroup
918
+ Live group object. Call ``.set(**kwargs)`` to update in place.
919
+ """
920
+ # On 1-D panels the native type is "points" (radius maps to sizes).
921
+ return self._add_marker("points", name, offsets=offsets, sizes=radius,
922
+ facecolors=facecolors, edgecolors=edgecolors,
923
+ linewidths=linewidths, alpha=alpha,
924
+ hover_edgecolors=hover_edgecolors,
925
+ hover_facecolors=hover_facecolors,
926
+ labels=labels, label=label,
927
+ transform=transform)
928
+
929
+ def add_points(self, offsets, name=None, *, sizes=5,
930
+ color="#ff0000", facecolors=None,
931
+ linewidths=1.5, alpha=0.3,
932
+ hover_edgecolors=None, hover_facecolors=None,
933
+ labels=None, label=None,
934
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
935
+ """Add point markers at (x, y) positions in data coordinates.
936
+
937
+ Parameters
938
+ ----------
939
+ offsets : array-like, shape (N, 2)
940
+ Marker positions as ``[[x0, y0], [x1, y1], …]``.
941
+ name : str, optional
942
+ Registry key. Auto-generated if omitted.
943
+ sizes : float or array-like, optional
944
+ Radius in pixels. Scalar or per-marker array. Default ``5``.
945
+ color : str, optional
946
+ Stroke colour. Default ``"#ff0000"``.
947
+ facecolors : str or None, optional
948
+ Fill colour. ``None`` = no fill.
949
+ linewidths : float, optional
950
+ Stroke width in pixels. Default ``1.5``.
951
+ alpha : float, optional
952
+ Fill opacity (0–1). Default ``0.3``.
953
+ hover_edgecolors, hover_facecolors : str, optional
954
+ Colour overrides applied on mouse-hover.
955
+ labels : list of str, optional
956
+ Per-marker tooltip labels.
957
+ label : str, optional
958
+ Collection-level tooltip label.
959
+
960
+ Returns
961
+ -------
962
+ MarkerGroup
963
+ """
964
+ return self._add_marker("points", name, offsets=offsets, sizes=sizes,
965
+ edgecolors=color, facecolors=facecolors,
966
+ linewidths=linewidths, alpha=alpha,
967
+ hover_edgecolors=hover_edgecolors,
968
+ hover_facecolors=hover_facecolors,
969
+ labels=labels, label=label,
970
+ transform=transform)
971
+
972
+ def add_hlines(self, y_values, name=None, *,
973
+ color="#ff0000", linewidths=1.5,
974
+ hover_edgecolors=None,
975
+ labels=None, label=None,
976
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
977
+ """Add static horizontal lines spanning the full x range.
978
+
979
+ Parameters
980
+ ----------
981
+ y_values : array-like, shape (N,)
982
+ Y positions of each line in data coordinates.
983
+ name : str, optional
984
+ Registry key. Auto-generated if omitted.
985
+ color : str, optional
986
+ Line colour. Default ``"#ff0000"``.
987
+ linewidths : float, optional
988
+ Stroke width in pixels. Default ``1.5``.
989
+ hover_edgecolors : str, optional
990
+ Colour override applied on mouse-hover.
991
+ labels : list of str, optional
992
+ Per-line tooltip labels.
993
+ label : str, optional
994
+ Collection-level tooltip label.
995
+
996
+ Returns
997
+ -------
998
+ MarkerGroup
999
+ """
1000
+ return self._add_marker("hlines", name, offsets=y_values,
1001
+ color=color, linewidths=linewidths,
1002
+ hover_edgecolors=hover_edgecolors,
1003
+ labels=labels, label=label,
1004
+ transform=transform)
1005
+
1006
+ def add_vlines(self, x_values, name=None, *,
1007
+ color="#ff0000", linewidths=1.5,
1008
+ hover_edgecolors=None,
1009
+ labels=None, label=None,
1010
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1011
+ """Add static vertical lines spanning the full y range.
1012
+
1013
+ Parameters
1014
+ ----------
1015
+ x_values : array-like, shape (N,)
1016
+ X positions of each line in data coordinates.
1017
+ name : str, optional
1018
+ Registry key. Auto-generated if omitted.
1019
+ color : str, optional
1020
+ Line colour. Default ``"#ff0000"``.
1021
+ linewidths : float, optional
1022
+ Stroke width in pixels. Default ``1.5``.
1023
+ hover_edgecolors : str, optional
1024
+ Colour override applied on mouse-hover.
1025
+ labels : list of str, optional
1026
+ Per-line tooltip labels.
1027
+ label : str, optional
1028
+ Collection-level tooltip label.
1029
+
1030
+ Returns
1031
+ -------
1032
+ MarkerGroup
1033
+ """
1034
+ return self._add_marker("vlines", name, offsets=x_values,
1035
+ color=color, linewidths=linewidths,
1036
+ hover_edgecolors=hover_edgecolors,
1037
+ labels=labels, label=label,
1038
+ transform=transform)
1039
+
1040
+ def add_arrows(self, offsets, U, V, name=None, *,
1041
+ edgecolors="#ff0000", linewidths=1.5,
1042
+ hover_edgecolors=None,
1043
+ labels=None, label=None,
1044
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1045
+ """Add arrow markers at explicit (x, y) positions.
1046
+
1047
+ Parameters
1048
+ ----------
1049
+ offsets : array-like, shape (N, 2)
1050
+ Arrow tail positions as ``[[x0, y0], …]`` in data coordinates.
1051
+ U, V : array-like, shape (N,)
1052
+ X and Y components of each arrow vector (in data units).
1053
+ name : str, optional
1054
+ Registry key. Auto-generated if omitted.
1055
+ edgecolors : str, optional
1056
+ Arrow colour. Default ``"#ff0000"``.
1057
+ linewidths : float, optional
1058
+ Stroke width in pixels. Default ``1.5``.
1059
+ hover_edgecolors : str, optional
1060
+ Colour override applied on mouse-hover.
1061
+ labels : list of str, optional
1062
+ Per-arrow tooltip labels.
1063
+ label : str, optional
1064
+ Collection-level tooltip label.
1065
+
1066
+ Returns
1067
+ -------
1068
+ MarkerGroup
1069
+ """
1070
+ return self._add_marker("arrows", name, offsets=offsets, U=U, V=V,
1071
+ edgecolors=edgecolors, linewidths=linewidths,
1072
+ hover_edgecolors=hover_edgecolors,
1073
+ labels=labels, label=label,
1074
+ transform=transform)
1075
+
1076
+ def add_ellipses(self, offsets, widths, heights, name=None, *,
1077
+ angles=0, facecolors=None, edgecolors="#ff0000",
1078
+ linewidths=1.5, alpha=0.3,
1079
+ hover_edgecolors=None, hover_facecolors=None,
1080
+ labels=None, label=None,
1081
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1082
+ """Add ellipse markers at explicit (x, y) positions.
1083
+
1084
+ Parameters
1085
+ ----------
1086
+ offsets : array-like, shape (N, 2)
1087
+ Centre positions in data coordinates.
1088
+ widths, heights : float or array-like
1089
+ Full width and height of each ellipse in canvas pixels.
1090
+ name : str, optional
1091
+ Registry key. Auto-generated if omitted.
1092
+ angles : float or array-like, optional
1093
+ Rotation angle(s) in degrees. Default ``0``.
1094
+ facecolors : str or None, optional
1095
+ Fill colour. ``None`` = no fill.
1096
+ edgecolors : str, optional
1097
+ Stroke colour. Default ``"#ff0000"``.
1098
+ linewidths : float, optional
1099
+ Stroke width in pixels. Default ``1.5``.
1100
+ alpha : float, optional
1101
+ Fill opacity (0–1). Default ``0.3``.
1102
+ hover_edgecolors, hover_facecolors : str, optional
1103
+ Colour overrides applied on mouse-hover.
1104
+ labels : list of str, optional
1105
+ Per-marker tooltip labels.
1106
+ label : str, optional
1107
+ Collection-level tooltip label.
1108
+
1109
+ Returns
1110
+ -------
1111
+ MarkerGroup
1112
+ """
1113
+ return self._add_marker("ellipses", name, offsets=offsets,
1114
+ widths=widths, heights=heights, angles=angles,
1115
+ facecolors=facecolors, edgecolors=edgecolors,
1116
+ linewidths=linewidths, alpha=alpha,
1117
+ hover_edgecolors=hover_edgecolors,
1118
+ hover_facecolors=hover_facecolors,
1119
+ labels=labels, label=label,
1120
+ transform=transform)
1121
+
1122
+ def add_lines(self, segments, name=None, *,
1123
+ edgecolors="#ff0000", linewidths=1.5,
1124
+ hover_edgecolors=None,
1125
+ labels=None, label=None,
1126
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1127
+ """Add line-segment markers (static, not draggable).
1128
+
1129
+ Parameters
1130
+ ----------
1131
+ segments : array-like, shape (N, 2, 2)
1132
+ Each segment is ``[[x0, y0], [x1, y1]]`` in data coordinates.
1133
+ name : str, optional
1134
+ Registry key. Auto-generated if omitted.
1135
+ edgecolors : str, optional
1136
+ Line colour. Default ``"#ff0000"``.
1137
+ linewidths : float, optional
1138
+ Stroke width in pixels. Default ``1.5``.
1139
+ hover_edgecolors : str, optional
1140
+ Colour override applied on mouse-hover.
1141
+ labels : list of str, optional
1142
+ Per-segment tooltip labels.
1143
+ label : str, optional
1144
+ Collection-level tooltip label.
1145
+
1146
+ Returns
1147
+ -------
1148
+ MarkerGroup
1149
+ """
1150
+ return self._add_marker("lines", name, segments=segments,
1151
+ edgecolors=edgecolors, linewidths=linewidths,
1152
+ hover_edgecolors=hover_edgecolors,
1153
+ labels=labels, label=label,
1154
+ transform=transform)
1155
+
1156
+ def add_rectangles(self, offsets, widths, heights, name=None, *,
1157
+ angles=0, facecolors=None, edgecolors="#ff0000",
1158
+ linewidths=1.5, alpha=0.3,
1159
+ hover_edgecolors=None, hover_facecolors=None,
1160
+ labels=None, label=None,
1161
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1162
+ """Add rectangle markers at explicit (x, y) positions.
1163
+
1164
+ Parameters
1165
+ ----------
1166
+ offsets : array-like, shape (N, 2)
1167
+ Centre positions in data coordinates.
1168
+ widths, heights : float or array-like
1169
+ Full width and height of each rectangle in canvas pixels.
1170
+ name : str, optional
1171
+ Registry key. Auto-generated if omitted.
1172
+ angles : float or array-like, optional
1173
+ Rotation angle(s) in degrees. Default ``0``.
1174
+ facecolors : str or None, optional
1175
+ Fill colour. ``None`` = no fill.
1176
+ edgecolors : str, optional
1177
+ Stroke colour. Default ``"#ff0000"``.
1178
+ linewidths : float, optional
1179
+ Stroke width in pixels. Default ``1.5``.
1180
+ alpha : float, optional
1181
+ Fill opacity (0–1). Default ``0.3``.
1182
+ hover_edgecolors, hover_facecolors : str, optional
1183
+ Colour overrides applied on mouse-hover.
1184
+ labels : list of str, optional
1185
+ Per-marker tooltip labels.
1186
+ label : str, optional
1187
+ Collection-level tooltip label.
1188
+
1189
+ Returns
1190
+ -------
1191
+ MarkerGroup
1192
+ """
1193
+ return self._add_marker("rectangles", name, offsets=offsets,
1194
+ widths=widths, heights=heights, angles=angles,
1195
+ facecolors=facecolors, edgecolors=edgecolors,
1196
+ linewidths=linewidths, alpha=alpha,
1197
+ hover_edgecolors=hover_edgecolors,
1198
+ hover_facecolors=hover_facecolors,
1199
+ labels=labels, label=label,
1200
+ transform=transform)
1201
+
1202
+ def add_squares(self, offsets, widths, name=None, *,
1203
+ angles=0, facecolors=None, edgecolors="#ff0000",
1204
+ linewidths=1.5, alpha=0.3,
1205
+ hover_edgecolors=None, hover_facecolors=None,
1206
+ labels=None, label=None,
1207
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1208
+ """Add square markers at explicit (x, y) positions.
1209
+
1210
+ Parameters
1211
+ ----------
1212
+ offsets : array-like, shape (N, 2)
1213
+ Centre positions in data coordinates.
1214
+ widths : float or array-like
1215
+ Side length of each square in canvas pixels.
1216
+ name : str, optional
1217
+ Registry key. Auto-generated if omitted.
1218
+ angles : float or array-like, optional
1219
+ Rotation angle(s) in degrees. Default ``0``.
1220
+ facecolors : str or None, optional
1221
+ Fill colour. ``None`` = no fill.
1222
+ edgecolors : str, optional
1223
+ Stroke colour. Default ``"#ff0000"``.
1224
+ linewidths : float, optional
1225
+ Stroke width in pixels. Default ``1.5``.
1226
+ alpha : float, optional
1227
+ Fill opacity (0–1). Default ``0.3``.
1228
+ hover_edgecolors, hover_facecolors : str, optional
1229
+ Colour overrides applied on mouse-hover.
1230
+ labels : list of str, optional
1231
+ Per-marker tooltip labels.
1232
+ label : str, optional
1233
+ Collection-level tooltip label.
1234
+
1235
+ Returns
1236
+ -------
1237
+ MarkerGroup
1238
+ """
1239
+ return self._add_marker("squares", name, offsets=offsets,
1240
+ widths=widths, angles=angles,
1241
+ facecolors=facecolors, edgecolors=edgecolors,
1242
+ linewidths=linewidths, alpha=alpha,
1243
+ hover_edgecolors=hover_edgecolors,
1244
+ hover_facecolors=hover_facecolors,
1245
+ labels=labels, label=label,
1246
+ transform=transform)
1247
+
1248
+ def add_polygons(self, vertices_list, name=None, *,
1249
+ facecolors=None, edgecolors="#ff0000",
1250
+ linewidths=1.5, alpha=0.3,
1251
+ hover_edgecolors=None, hover_facecolors=None,
1252
+ labels=None, label=None, clip_path=None,
1253
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1254
+ """Add polygon markers defined by explicit vertex lists.
1255
+
1256
+ Parameters
1257
+ ----------
1258
+ vertices_list : list of array-like, each shape (K, 2)
1259
+ One polygon per element; each is a list of ``[x, y]`` vertices
1260
+ in data coordinates.
1261
+ name : str, optional
1262
+ Registry key. Auto-generated if omitted.
1263
+ facecolors : str or None, optional
1264
+ Fill colour. ``None`` = no fill.
1265
+ edgecolors : str, optional
1266
+ Stroke colour. Default ``"#ff0000"``.
1267
+ linewidths : float, optional
1268
+ Stroke width in pixels. Default ``1.5``.
1269
+ alpha : float, optional
1270
+ Fill opacity (0–1). Default ``0.3``.
1271
+ hover_edgecolors, hover_facecolors : str, optional
1272
+ Colour overrides applied on mouse-hover.
1273
+ labels : list of str, optional
1274
+ Per-polygon tooltip labels.
1275
+ label : str, optional
1276
+ Collection-level tooltip label.
1277
+ clip_path : array-like, shape (K, 2), optional
1278
+ Data-coordinate polygon the group is clipped to (matplotlib
1279
+ ``set_clip_path``) — e.g. a curved sector boundary so a mesh's edge
1280
+ cells don't overflow it. ``None`` = no clip.
1281
+
1282
+ Returns
1283
+ -------
1284
+ MarkerGroup
1285
+ """
1286
+ return self._add_marker("polygons", name, vertices_list=vertices_list,
1287
+ facecolors=facecolors, edgecolors=edgecolors,
1288
+ linewidths=linewidths, alpha=alpha,
1289
+ hover_edgecolors=hover_edgecolors,
1290
+ hover_facecolors=hover_facecolors,
1291
+ labels=labels, label=label, clip_path=clip_path,
1292
+ transform=transform)
1293
+
1294
+ def add_raster(self, rgba, *, extent, name=None, clip_path=None,
1295
+ smooth: bool = False,
1296
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1297
+ """Add an RGBA image drawn between data-coordinate ``extent`` corners.
1298
+
1299
+ A single-``drawImage`` raster — the fast path for a dense regular grid
1300
+ (e.g. an orientation-density heatmap) that would otherwise be thousands
1301
+ of individual polygons. The image bytes travel on the deduped geometry
1302
+ channel, so re-aiming the view never re-transmits them.
1303
+
1304
+ Parameters
1305
+ ----------
1306
+ rgba : array-like, shape (H, W, 3|4)
1307
+ Image data. Coerced to uint8 RGBA via the usual rules (floats in
1308
+ 0–1 scaled ×255, missing alpha → 255). Alpha 0 = transparent cell.
1309
+ extent : (x0, x1, y0, y1)
1310
+ Data-coordinate bounding box the image is stretched across.
1311
+ name : str, optional
1312
+ Registry key. Auto-generated if omitted.
1313
+ clip_path : array-like, shape (K, 2), optional
1314
+ Data-coordinate polygon the image is clipped to (matplotlib
1315
+ ``set_clip_path``) — e.g. a curved fundamental-sector boundary.
1316
+ ``None`` = no clip.
1317
+ smooth : bool, optional
1318
+ If ``True``, the image is bilinearly interpolated when stretched
1319
+ (a smooth heat field). Default ``False`` keeps crisp
1320
+ nearest-neighbour cells (matplotlib ``interpolation="nearest"``).
1321
+
1322
+ Returns
1323
+ -------
1324
+ MarkerGroup
1325
+ """
1326
+ import base64
1327
+ rgba_u8 = _to_rgba_u8(np.asarray(rgba))
1328
+ h, w = rgba_u8.shape[:2]
1329
+ image_b64 = base64.b64encode(
1330
+ np.ascontiguousarray(rgba_u8).tobytes()).decode("ascii")
1331
+ return self._add_marker("raster", name, image_b64=image_b64,
1332
+ image_width=int(w), image_height=int(h),
1333
+ extent=tuple(float(v) for v in extent),
1334
+ clip_path=clip_path, smooth=bool(smooth),
1335
+ transform=transform)
1336
+
1337
+ def add_texts(self, offsets, texts, name=None, *,
1338
+ color="#ff0000", fontsize=12,
1339
+ hover_edgecolors=None,
1340
+ labels=None, label=None,
1341
+ transform: str = "data") -> "MarkerGroup": # noqa: F821
1342
+ """Add text annotations at explicit (x, y) positions.
1343
+
1344
+ Parameters
1345
+ ----------
1346
+ offsets : array-like, shape (N, 2)
1347
+ Anchor positions in data coordinates.
1348
+ texts : list of str
1349
+ One string per position.
1350
+ name : str, optional
1351
+ Registry key. Auto-generated if omitted.
1352
+ color : str, optional
1353
+ Text colour. Default ``"#ff0000"``.
1354
+ fontsize : int, optional
1355
+ Font size in pixels. Default ``12``.
1356
+ hover_edgecolors : str, optional
1357
+ Colour override applied on mouse-hover.
1358
+ labels : list of str, optional
1359
+ Per-annotation tooltip labels.
1360
+ label : str, optional
1361
+ Collection-level tooltip label.
1362
+
1363
+ Returns
1364
+ -------
1365
+ MarkerGroup
1366
+ """
1367
+ return self._add_marker("texts", name, offsets=offsets, texts=texts,
1368
+ color=color, fontsize=fontsize,
1369
+ hover_edgecolors=hover_edgecolors,
1370
+ labels=labels, label=label,
1371
+ transform=transform)
1372
+
1373
+ def __repr__(self) -> str:
1374
+ n = len(self._state.get("data", []))
1375
+ color = self._state.get("line_color", "?")
1376
+ return f"Plot1D(n={n}, color={color!r})"