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,100 @@
1
+ /*
2
+ * anywidget_overlay.css
3
+ * =====================
4
+ * Styles for the per-figure interaction badges injected by AnywidgetScraper
5
+ * and the .. anywidget-figure:: RST directive.
6
+ *
7
+ * Badge structure (rendered by _scraper._iframe_html):
8
+ *
9
+ * <div class="awi-fig-wrap" data-awi-fig="FIGID">
10
+ * <iframe data-awi-fig="FIGID" ...></iframe>
11
+ * <div class="awi-badge" data-awi-badge="FIGID">
12
+ * <button class="awi-badge-icon awi-activate-btn">⚡</button> <!-- interactive only -->
13
+ * </div>
14
+ * </div>
15
+ */
16
+
17
+ /* ── figure wrapper ──────────────────────────────────────────────────────── */
18
+ .awi-fig-wrap {
19
+ position: relative; /* Badge positions relative to this */
20
+ }
21
+
22
+ /* ── badge container ─────────────────────────────────────────────────────── */
23
+ .awi-badge {
24
+ position: absolute;
25
+ top: 8px;
26
+ right: 8px;
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 8px;
30
+ z-index: 10;
31
+ pointer-events: none;
32
+ }
33
+
34
+ /* ── after activation: no change needed — stays top-right ───────────────── */
35
+
36
+ /* ── shared icon style ───────────────────────────────────────────────────── */
37
+ .awi-badge-icon {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ width: 38px;
42
+ height: 38px;
43
+ border-radius: 50%;
44
+ font-size: 18px;
45
+ line-height: 1;
46
+ background: rgba(30, 30, 46, 0.82);
47
+ backdrop-filter: blur(6px);
48
+ border: 1px solid rgba(255, 255, 255, 0.25);
49
+ color: #fff;
50
+ box-shadow: 0 2px 8px rgba(0,0,0,0.5);
51
+ transition: opacity 0.25s, background 0.2s, transform 0.15s;
52
+ opacity: 0.90;
53
+ }
54
+
55
+ /* ── ⚡ activation button ────────────────────────────────────────────────── */
56
+ .awi-activate-btn {
57
+ cursor: pointer;
58
+ pointer-events: auto;
59
+ padding: 0;
60
+ font-family: inherit;
61
+ border-color: rgba(98, 114, 164, 0.7);
62
+ background: rgba(30, 30, 46, 0.80);
63
+ }
64
+
65
+ .awi-activate-btn:hover {
66
+ background: rgba(98, 114, 164, 0.85);
67
+ opacity: 1;
68
+ transform: scale(1.12);
69
+ }
70
+
71
+ .awi-activate-btn:focus-visible {
72
+ outline: 2px solid #8be9fd;
73
+ outline-offset: 2px;
74
+ }
75
+
76
+ /* ── loading state (⏳) ──────────────────────────────────────────────────── */
77
+ .awi-activate-btn[data-state="loading"] {
78
+ cursor: default;
79
+ pointer-events: none;
80
+ background: rgba(189, 147, 249, 0.85);
81
+ }
82
+
83
+ /* ── active / live state (✓) ─────────────────────────────────────────────── */
84
+ .awi-activate-btn[data-state="active"] {
85
+ background: rgba(80, 250, 123, 0.85);
86
+ color: #1e1e2e;
87
+ cursor: default;
88
+ pointer-events: none;
89
+ font-weight: 700;
90
+ }
91
+
92
+ /* ── error state (❌) ────────────────────────────────────────────────────── */
93
+ .awi-activate-btn[data-state="error"] {
94
+ background: rgba(255, 85, 85, 0.85);
95
+ cursor: default;
96
+ pointer-events: none;
97
+ }
98
+
99
+
100
+
@@ -0,0 +1,18 @@
1
+ """anyplotlib.widgets — interactive overlay widget classes."""
2
+ from anyplotlib.widgets._base import Widget
3
+ from anyplotlib.widgets._widgets2d import (
4
+ RectangleWidget, CircleWidget, AnnularWidget,
5
+ CrosshairWidget, PolygonWidget, LabelWidget,
6
+ )
7
+ from anyplotlib.widgets._widgets1d import (
8
+ VLineWidget, HLineWidget, RangeWidget, PointWidget,
9
+ )
10
+ from anyplotlib.widgets._widgets3d import PlaneWidget
11
+
12
+ __all__ = [
13
+ "Widget",
14
+ "RectangleWidget", "CircleWidget", "AnnularWidget",
15
+ "CrosshairWidget", "PolygonWidget", "LabelWidget",
16
+ "VLineWidget", "HLineWidget", "RangeWidget", "PointWidget",
17
+ "PlaneWidget",
18
+ ]
@@ -0,0 +1,218 @@
1
+ """
2
+ widgets/_base.py
3
+ ================
4
+ Base Widget class shared by all interactive overlay widgets.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import uuid as _uuid
9
+ from typing import Any, Callable
10
+ from anyplotlib.callbacks import CallbackRegistry, Event, _EventMixin
11
+
12
+
13
+ class Widget(_EventMixin):
14
+ """Base class for all overlay widgets.
15
+
16
+ Provides attribute-based state access, callbacks for interaction events,
17
+ and automatic synchronization with the JavaScript renderer.
18
+
19
+ Parameters
20
+ ----------
21
+ wtype : str
22
+ Widget type (e.g., 'rectangle', 'circle', 'crosshair').
23
+ push_fn : Callable
24
+ Zero-arg callback to send position updates to the JavaScript renderer.
25
+ **kwargs : dict
26
+ Initial widget state (position, size, color, etc.).
27
+
28
+ Attributes
29
+ ----------
30
+ callbacks : CallbackRegistry
31
+ Event callback registry. Register handlers via
32
+ ``widget.add_event_handler(fn, "pointer_move")`` or as a decorator:
33
+ ``@widget.add_event_handler("pointer_move")``.
34
+
35
+ Common event types:
36
+
37
+ - ``"pointer_move"`` — fires on every drag frame
38
+ - ``"pointer_up"`` — fires once when drag settles
39
+ - ``"pointer_down"`` — fires on click/press event
40
+ """
41
+
42
+ def __init__(self, wtype: str, push_fn: Callable, **kwargs):
43
+ self._id: str = str(_uuid.uuid4())[:8]
44
+ self._type: str = wtype
45
+ self._data: dict = dict(kwargs)
46
+ self._data["id"] = self._id
47
+ self._data["type"] = wtype
48
+ self._push_fn: Callable = push_fn
49
+ self.callbacks: CallbackRegistry = CallbackRegistry()
50
+
51
+ # ── attribute read ────────────────────────────────────────────────
52
+
53
+ def __getattr__(self, key: str):
54
+ """Access widget properties as attributes (read-only)."""
55
+ if key.startswith("_"):
56
+ raise AttributeError(key)
57
+ try:
58
+ return self._data[key]
59
+ except KeyError:
60
+ raise AttributeError(
61
+ f"{type(self).__name__} has no attribute {key!r}. "
62
+ f"Available: {list(self._data)}"
63
+ ) from None
64
+
65
+ # ── attribute write — routes public assignments through set() ────
66
+
67
+ def __setattr__(self, key: str, value) -> None:
68
+ """Update widget properties via attribute assignment."""
69
+ # Private attrs and 'callbacks' bypass set()
70
+ if key.startswith("_") or key == "callbacks":
71
+ super().__setattr__(key, value)
72
+ return
73
+ # During __init__ _data may not exist yet
74
+ try:
75
+ object.__getattribute__(self, "_data")
76
+ except AttributeError:
77
+ super().__setattr__(key, value)
78
+ return
79
+ self.set(**{key: value})
80
+
81
+ # ── set / get ─────────────────────────────────────────────────────
82
+
83
+ def set(self, _push: bool = True, **kwargs) -> None:
84
+ """Update properties and send targeted update to JavaScript.
85
+
86
+ Parameters
87
+ ----------
88
+ _push : bool, optional
89
+ Whether to push update to renderer. Default True.
90
+ Set to False internally to avoid echo loops.
91
+ **kwargs : dict
92
+ Properties to update (e.g., x=100, y=50, radius=20).
93
+
94
+ Notes
95
+ -----
96
+ Updates are sent as targeted widget updates, not full panel re-renders.
97
+ This is more efficient for frequent updates during dragging.
98
+ """
99
+ self._data.update(kwargs)
100
+ if _push:
101
+ self._push_fn()
102
+ self.callbacks.fire(Event("pointer_move", source=self))
103
+
104
+ def get(self, key: str, default=None):
105
+ """Get a widget property by name.
106
+
107
+ Parameters
108
+ ----------
109
+ key : str
110
+ Property name.
111
+ default : optional
112
+ Default value if property not found.
113
+
114
+ Returns
115
+ -------
116
+ object
117
+ The property value.
118
+ """
119
+ return self._data.get(key, default)
120
+
121
+ def to_dict(self) -> dict:
122
+ """Return a dict copy of the widget state.
123
+
124
+ Returns
125
+ -------
126
+ dict
127
+ All widget properties including id and type.
128
+ """
129
+ return dict(self._data)
130
+
131
+ # ── visibility ────────────────────────────────────────────────────────
132
+
133
+ @property
134
+ def visible(self) -> bool:
135
+ """``True`` if the widget is rendered; ``False`` if hidden."""
136
+ return self._data.get("visible", True)
137
+
138
+ @visible.setter
139
+ def visible(self, value: bool) -> None:
140
+ self.show() if value else self.hide()
141
+
142
+ def show(self) -> None:
143
+ """Show the widget. Does not fire ``pointer_move`` callbacks."""
144
+ self._data["visible"] = True
145
+ self._push_fn()
146
+
147
+ def hide(self) -> None:
148
+ """Hide the widget without removing it or its callbacks.
149
+
150
+ Call :meth:`show` to make it visible again.
151
+ Does not fire ``pointer_move`` callbacks.
152
+ """
153
+ self._data["visible"] = False
154
+ self._push_fn()
155
+
156
+ # ── JS → Python sync ──────────────────────────────────────────────
157
+
158
+ def _update_from_js(self, msg: dict, event_type: str = "pointer_move") -> bool:
159
+ """Apply incoming JS state without pushing back (avoids echo).
160
+
161
+ Updates widget ``_data`` with widget-specific state fields from msg,
162
+ then fires widget callbacks with a flat Event.
163
+
164
+ Parameters
165
+ ----------
166
+ msg : dict
167
+ Full raw event message from JS.
168
+ event_type : str
169
+ One of the pointer event types (``pointer_move``, ``pointer_up``,
170
+ ``pointer_down``).
171
+
172
+ Returns
173
+ -------
174
+ bool
175
+ True if any widget state changed.
176
+ """
177
+ _envelope = {
178
+ "source", "panel_id", "event_type", "widget_id",
179
+ "time_stamp", "modifiers", "button", "buttons",
180
+ }
181
+ changed = False
182
+ for k, v in msg.items():
183
+ if k in ("id", "type") or k in _envelope:
184
+ continue
185
+ if self._data.get(k) != v:
186
+ self._data[k] = v
187
+ changed = True
188
+
189
+ if changed or event_type in ("pointer_up", "pointer_down"):
190
+ event = Event(
191
+ event_type=event_type,
192
+ source=self,
193
+ time_stamp=msg.get("time_stamp", 0.0),
194
+ modifiers=msg.get("modifiers", []),
195
+ x=msg.get("x"),
196
+ y=msg.get("y"),
197
+ button=msg.get("button"),
198
+ buttons=msg.get("buttons", 0),
199
+ xdata=msg.get("xdata"),
200
+ ydata=msg.get("ydata"),
201
+ )
202
+ self.callbacks.fire(event)
203
+ return changed
204
+
205
+ # ── repr ──────────────────────────────────────────────────────────
206
+
207
+ def __repr__(self) -> str:
208
+ props = ", ".join(
209
+ f"{k}={v:.4g}" if isinstance(v, float) else f"{k}={v!r}"
210
+ for k, v in self._data.items()
211
+ if k not in ("id", "type", "color")
212
+ )
213
+ return f"{type(self).__name__}({props})"
214
+
215
+ @property
216
+ def id(self) -> str:
217
+ """Return the widget's unique identifier."""
218
+ return self._id
@@ -0,0 +1,109 @@
1
+ """
2
+ widgets/_widgets1d.py
3
+ =====================
4
+ Interactive overlay widgets for 1-D line panels (Plot1D).
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from anyplotlib.widgets._base import Widget
9
+
10
+
11
+ class VLineWidget(Widget):
12
+ """Draggable vertical line overlay widget for 1-D plots.
13
+
14
+ Allows interactive selection of a single x-axis value. The line can be
15
+ dragged left/right to change the selected position.
16
+
17
+ Parameters
18
+ ----------
19
+ push_fn : Callable
20
+ Update callback.
21
+ x : float
22
+ Initial x-position in data coordinates.
23
+ color : str, optional
24
+ CSS colour for the line. Default ``"#00e5ff"``.
25
+ """
26
+ def __init__(self, push_fn, *, x, color="#00e5ff"):
27
+ super().__init__("vline", push_fn, x=float(x), color=color)
28
+
29
+
30
+ class HLineWidget(Widget):
31
+ """Draggable horizontal line overlay widget for bar charts.
32
+
33
+ Allows interactive selection of a single y-axis value. The line can be
34
+ dragged up/down to change the selected value.
35
+
36
+ Parameters
37
+ ----------
38
+ push_fn : Callable
39
+ Update callback.
40
+ y : float
41
+ Initial y-position in data coordinates.
42
+ color : str, optional
43
+ CSS colour for the line. Default ``"#00e5ff"``.
44
+ """
45
+ def __init__(self, push_fn, *, y, color="#00e5ff"):
46
+ super().__init__("hline", push_fn, y=float(y), color=color)
47
+
48
+
49
+ class RangeWidget(Widget):
50
+ """Draggable range selection widget.
51
+
52
+ Two display styles are available:
53
+
54
+ ``style='band'`` (default)
55
+ Two connected vertical lines with a translucent fill band. Either
56
+ line can be dragged independently; the whole band can be dragged by
57
+ clicking inside it.
58
+
59
+ ``style='fwhm'``
60
+ Two circular handles joined by a dashed horizontal line drawn at
61
+ height *y* (the half-maximum level). Only the x-positions of the
62
+ handles are draggable. Use this to show/edit a FWHM interval on a
63
+ peak.
64
+
65
+ Parameters
66
+ ----------
67
+ push_fn : Callable
68
+ Update callback.
69
+ x0, x1 : float
70
+ Initial left and right positions in data coordinates.
71
+ color : str, optional
72
+ CSS colour. Default ``"#00e5ff"``.
73
+ style : {'band', 'fwhm'}, optional
74
+ Visual style. Default ``"band"``.
75
+ y : float, optional
76
+ Y-position (data coordinates) for the connecting line when
77
+ ``style='fwhm'``. Ignored for ``style='band'``. Default ``0.0``.
78
+ """
79
+ def __init__(self, push_fn, *, x0, x1, color="#00e5ff",
80
+ style: str = "band", y: float = 0.0):
81
+ super().__init__("range", push_fn,
82
+ x0=float(x0), x1=float(x1), color=color,
83
+ style=str(style), y=float(y))
84
+
85
+
86
+ class PointWidget(Widget):
87
+ """Draggable point (control point) overlay widget for 1-D plots.
88
+
89
+ A free-moving handle that can be dragged to any position within the
90
+ plot area. Reports its data-space ``x`` and ``y`` coordinates back
91
+ to Python via the standard callback hooks.
92
+
93
+ Parameters
94
+ ----------
95
+ push_fn : Callable
96
+ Update callback.
97
+ x : float
98
+ Initial x position in data coordinates.
99
+ y : float
100
+ Initial y position in data coordinates (value axis).
101
+ color : str, optional
102
+ CSS colour for the handle. Default ``"#00e5ff"``.
103
+ show_crosshair : bool, optional
104
+ If ``True`` (default), draw dashed crosshair guide lines through the
105
+ handle. Set to ``False`` for a bare draggable dot with no guides.
106
+ """
107
+ def __init__(self, push_fn, *, x, y, color="#00e5ff", show_crosshair=True):
108
+ super().__init__("point", push_fn, x=float(x), y=float(y), color=color,
109
+ show_crosshair=bool(show_crosshair))
@@ -0,0 +1,141 @@
1
+ """
2
+ widgets/_widgets2d.py
3
+ =====================
4
+ Interactive overlay widgets for 2-D image panels (Plot2D / InsetAxes).
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from anyplotlib.widgets._base import Widget
9
+
10
+
11
+ class RectangleWidget(Widget):
12
+ """Draggable rectangle overlay widget for 2-D plots.
13
+
14
+ Parameters
15
+ ----------
16
+ push_fn : Callable
17
+ Update callback.
18
+ x, y : float
19
+ Top-left corner position in pixel/data coordinates.
20
+ w, h : float
21
+ Width and height in pixel/data coordinates.
22
+ color : str, optional
23
+ CSS colour for the rectangle outline. Default ``"#00e5ff"``.
24
+ """
25
+ def __init__(self, push_fn, *, x, y, w, h, color="#00e5ff"):
26
+ super().__init__("rectangle", push_fn,
27
+ x=float(x), y=float(y),
28
+ w=float(w), h=float(h), color=color)
29
+
30
+
31
+ class CircleWidget(Widget):
32
+ """Draggable circle overlay widget for 2-D plots.
33
+
34
+ Parameters
35
+ ----------
36
+ push_fn : Callable
37
+ Update callback.
38
+ cx, cy : float
39
+ Center position in pixel/data coordinates.
40
+ r : float
41
+ Radius in pixel/data coordinates.
42
+ color : str, optional
43
+ CSS colour for the circle outline. Default ``"#00e5ff"``.
44
+ """
45
+ def __init__(self, push_fn, *, cx, cy, r, color="#00e5ff"):
46
+ super().__init__("circle", push_fn,
47
+ cx=float(cx), cy=float(cy), r=float(r), color=color)
48
+
49
+
50
+ class AnnularWidget(Widget):
51
+ """Draggable annular (ring) overlay widget for 2-D plots.
52
+
53
+ Parameters
54
+ ----------
55
+ push_fn : Callable
56
+ Update callback.
57
+ cx, cy : float
58
+ Center position in pixel/data coordinates.
59
+ r_outer, r_inner : float
60
+ Outer and inner radii in pixel/data coordinates.
61
+ Inner radius must be less than outer radius.
62
+ color : str, optional
63
+ CSS colour for the ring outline. Default ``"#00e5ff"``.
64
+
65
+ Raises
66
+ ------
67
+ ValueError
68
+ If r_inner >= r_outer.
69
+ """
70
+ def __init__(self, push_fn, *, cx, cy, r_outer, r_inner, color="#00e5ff"):
71
+ if r_inner >= r_outer:
72
+ raise ValueError("r_inner must be < r_outer")
73
+ super().__init__("annular", push_fn,
74
+ cx=float(cx), cy=float(cy),
75
+ r_outer=float(r_outer), r_inner=float(r_inner),
76
+ color=color)
77
+
78
+
79
+ class CrosshairWidget(Widget):
80
+ """Draggable crosshair overlay widget for 2-D plots.
81
+
82
+ Parameters
83
+ ----------
84
+ push_fn : Callable
85
+ Update callback.
86
+ cx, cy : float
87
+ Center position in pixel/data coordinates.
88
+ color : str, optional
89
+ CSS colour for the crosshair. Default ``"#00e5ff"``.
90
+ """
91
+ def __init__(self, push_fn, *, cx, cy, color="#00e5ff"):
92
+ super().__init__("crosshair", push_fn,
93
+ cx=float(cx), cy=float(cy), color=color)
94
+
95
+
96
+ class PolygonWidget(Widget):
97
+ """Draggable polygon overlay widget for 2-D plots.
98
+
99
+ Parameters
100
+ ----------
101
+ push_fn : Callable
102
+ Update callback.
103
+ vertices : list of tuple
104
+ Polygon vertices ``[(x0, y0), (x1, y1), ...]`` in pixel/data coordinates.
105
+ Must have at least 3 vertices.
106
+ color : str, optional
107
+ CSS colour for the polygon outline. Default ``"#00e5ff"``.
108
+
109
+ Raises
110
+ ------
111
+ ValueError
112
+ If fewer than 3 vertices provided.
113
+ """
114
+ def __init__(self, push_fn, *, vertices, color="#00e5ff"):
115
+ verts = [[float(x), float(y)] for x, y in vertices]
116
+ if len(verts) < 3:
117
+ raise ValueError("polygon needs >= 3 vertices")
118
+ super().__init__("polygon", push_fn, vertices=verts, color=color)
119
+
120
+
121
+ class LabelWidget(Widget):
122
+ """Text label overlay widget for 2-D plots.
123
+
124
+ Parameters
125
+ ----------
126
+ push_fn : Callable
127
+ Update callback.
128
+ x, y : float
129
+ Label position in pixel/data coordinates.
130
+ text : str, optional
131
+ Label text. Default ``"Label"``.
132
+ fontsize : int, optional
133
+ Font size in points. Default 14.
134
+ color : str, optional
135
+ CSS colour for the text. Default ``"#00e5ff"``.
136
+ """
137
+ def __init__(self, push_fn, *, x, y, text="Label", fontsize=14,
138
+ color="#00e5ff"):
139
+ super().__init__("label", push_fn,
140
+ x=float(x), y=float(y),
141
+ text=str(text), fontsize=int(fontsize), color=color)
@@ -0,0 +1,50 @@
1
+ """
2
+ widgets/_widgets3d.py
3
+ =====================
4
+ Interactive overlay widgets for 3-D panels.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable
10
+
11
+ from anyplotlib.widgets._base import Widget
12
+
13
+
14
+ class PlaneWidget(Widget):
15
+ """A draggable axis-aligned plane in a 3-D panel.
16
+
17
+ Rendered as a translucent quad spanning the panel's bounds,
18
+ perpendicular to *axis* at *position*. Drag it in the browser to slide
19
+ it along its normal — ideal as a slice selector for voxel volumes.
20
+ Voxels lying on a plane are rendered more opaque (see
21
+ :meth:`~anyplotlib.Axes.voxels`).
22
+
23
+ Parameters
24
+ ----------
25
+ axis : ``"x"`` | ``"y"`` | ``"z"``
26
+ The plane's normal axis.
27
+ position : float
28
+ Position along *axis* in data coordinates.
29
+ color : str, optional
30
+ CSS colour of the plane fill and border.
31
+ alpha : float, optional
32
+ Fill opacity (0–1). Default 0.12.
33
+
34
+ Examples
35
+ --------
36
+ >>> pw = vol.add_widget("plane", axis="z", position=24)
37
+ >>> @pw.add_event_handler("pointer_move")
38
+ ... def on_drag(event):
39
+ ... print("slice now at", pw.position)
40
+ >>> pw.set(position=10) # move it from Python
41
+ """
42
+
43
+ def __init__(self, push_fn: Callable, axis: str = "z",
44
+ position: float = 0.0, color: str = "#00e5ff",
45
+ alpha: float = 0.12):
46
+ if axis not in ("x", "y", "z"):
47
+ raise ValueError(f"axis must be 'x', 'y', or 'z', got {axis!r}")
48
+ super().__init__("plane", push_fn,
49
+ axis=axis, position=float(position),
50
+ color=color, alpha=float(alpha))