anyplotlib 0.1.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. anyplotlib/__init__.py +53 -0
  2. anyplotlib/_base_plot.py +229 -0
  3. anyplotlib/_electron.py +74 -0
  4. anyplotlib/_repr_utils.py +345 -0
  5. anyplotlib/_utils.py +194 -0
  6. anyplotlib/axes/__init__.py +6 -0
  7. anyplotlib/axes/_axes.py +510 -0
  8. anyplotlib/axes/_inset_axes.py +126 -0
  9. anyplotlib/callbacks.py +328 -0
  10. anyplotlib/embed.py +194 -0
  11. anyplotlib/figure/__init__.py +7 -0
  12. anyplotlib/figure/_figure.py +633 -0
  13. anyplotlib/figure/_gridspec.py +99 -0
  14. anyplotlib/figure/_subplots.py +100 -0
  15. anyplotlib/figure_esm.js +6011 -0
  16. anyplotlib/markers.py +704 -0
  17. anyplotlib/plot1d/__init__.py +6 -0
  18. anyplotlib/plot1d/_plot1d.py +1376 -0
  19. anyplotlib/plot1d/_plotbar.py +436 -0
  20. anyplotlib/plot2d/__init__.py +5 -0
  21. anyplotlib/plot2d/_plot2d.py +726 -0
  22. anyplotlib/plot2d/_plotmesh.py +116 -0
  23. anyplotlib/plot3d/__init__.py +4 -0
  24. anyplotlib/plot3d/_plot3d.py +524 -0
  25. anyplotlib/plotxy/__init__.py +4 -0
  26. anyplotlib/plotxy/_plotxy.py +273 -0
  27. anyplotlib/sphinx_anywidget/__init__.py +177 -0
  28. anyplotlib/sphinx_anywidget/_directive.py +245 -0
  29. anyplotlib/sphinx_anywidget/_repr_utils.py +298 -0
  30. anyplotlib/sphinx_anywidget/_scraper.py +390 -0
  31. anyplotlib/sphinx_anywidget/_wheel_builder.py +84 -0
  32. anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +1099 -0
  33. anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +100 -0
  34. anyplotlib/widgets/__init__.py +18 -0
  35. anyplotlib/widgets/_base.py +218 -0
  36. anyplotlib/widgets/_widgets1d.py +109 -0
  37. anyplotlib/widgets/_widgets2d.py +141 -0
  38. anyplotlib/widgets/_widgets3d.py +50 -0
  39. anyplotlib-0.1.0b1.dist-info/METADATA +160 -0
  40. anyplotlib-0.1.0b1.dist-info/RECORD +42 -0
  41. anyplotlib-0.1.0b1.dist-info/WHEEL +4 -0
  42. anyplotlib-0.1.0b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,116 @@
1
+ """
2
+ plot2d/_plotmesh.py
3
+ ===================
4
+ pcolormesh panel (non-uniform grid).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+
11
+ from anyplotlib.markers import MarkerRegistry
12
+ from anyplotlib.plot2d._plot2d import Plot2D
13
+ from anyplotlib._utils import _normalize_image, _build_colormap_lut, _resample_mesh
14
+
15
+
16
+ class PlotMesh(Plot2D):
17
+ """2-D mesh plot panel created by :meth:`Axes.pcolormesh`.
18
+
19
+ Accepts cell *edge* arrays (length N+1 / M+1) rather than centre arrays,
20
+ matches matplotlib's ``pcolormesh`` convention. Only ``'circles'`` and
21
+ ``'lines'`` markers are supported.
22
+ """
23
+
24
+ def __init__(self, data: np.ndarray,
25
+ x_edges=None, y_edges=None, units: str = ""):
26
+ data = np.asarray(data)
27
+ if data.ndim != 2:
28
+ raise ValueError(f"data must be 2-D (M x N), got {data.shape}")
29
+ rows, cols = data.shape
30
+
31
+ if x_edges is None:
32
+ x_edges = np.arange(cols + 1, dtype=float)
33
+ if y_edges is None:
34
+ y_edges = np.arange(rows + 1, dtype=float)
35
+ x_edges = np.asarray(x_edges, dtype=float)
36
+ y_edges = np.asarray(y_edges, dtype=float)
37
+
38
+ if len(x_edges) != cols + 1:
39
+ raise ValueError(
40
+ f"x_edges must have length {cols + 1} for {cols} columns, "
41
+ f"got {len(x_edges)}")
42
+ if len(y_edges) != rows + 1:
43
+ raise ValueError(
44
+ f"y_edges must have length {rows + 1} for {rows} rows, "
45
+ f"got {len(y_edges)}")
46
+
47
+ # Resample to a regular pixel grid for display
48
+ resampled = _resample_mesh(data, x_edges, y_edges)
49
+
50
+ # Use cell centres to initialise the parent (axes will be replaced)
51
+ x_c = (x_edges[:-1] + x_edges[1:]) / 2.0
52
+ y_c = (y_edges[:-1] + y_edges[1:]) / 2.0
53
+ super().__init__(resampled, x_axis=x_c, y_axis=y_c, units=units)
54
+
55
+ # Override mesh-specific state
56
+ self._state["is_mesh"] = True
57
+ self._state["has_axes"] = True
58
+ # Store edges (not centres) so the JS renderer can place grid lines
59
+ self._state["x_axis"] = x_edges.tolist()
60
+ self._state["y_axis"] = y_edges.tolist()
61
+ # Mesh panels have no fixed pixel scale
62
+ self._state.pop("scale_x", None)
63
+ self._state.pop("scale_y", None)
64
+
65
+ # Restrict markers to circles + lines only
66
+ self.markers = MarkerRegistry(self._push_markers,
67
+ allowed=MarkerRegistry._KNOWN_MESH)
68
+
69
+ # ------------------------------------------------------------------
70
+ # Data
71
+ # ------------------------------------------------------------------
72
+ def set_data(self, data: np.ndarray,
73
+ x_edges=None, y_edges=None, units: str | None = None) -> None:
74
+ """Replace the mesh data (and optionally the edge arrays)."""
75
+ data = np.asarray(data)
76
+ if data.ndim != 2:
77
+ raise ValueError(f"data must be 2-D, got {data.shape}")
78
+ rows, cols = data.shape
79
+
80
+ cur_xe = np.asarray(self._state["x_axis"], dtype=float)
81
+ cur_ye = np.asarray(self._state["y_axis"], dtype=float)
82
+ xe = np.asarray(x_edges, dtype=float) if x_edges is not None else cur_xe
83
+ ye = np.asarray(y_edges, dtype=float) if y_edges is not None else cur_ye
84
+
85
+ if len(xe) != cols + 1:
86
+ raise ValueError(f"x_edges must have length {cols + 1}")
87
+ if len(ye) != rows + 1:
88
+ raise ValueError(f"y_edges must have length {rows + 1}")
89
+
90
+ resampled = _resample_mesh(data, xe, ye)
91
+ img_u8, vmin, vmax = _normalize_image(resampled)
92
+ self._raw_u8, self._raw_vmin, self._raw_vmax = img_u8, vmin, vmax
93
+
94
+ self._state.update({
95
+ "image_b64": self._encode_bytes(img_u8),
96
+ "image_width": cols,
97
+ "image_height": rows,
98
+ "x_axis": xe.tolist(),
99
+ "y_axis": ye.tolist(),
100
+ "display_min": vmin,
101
+ "display_max": vmax,
102
+ "raw_min": vmin,
103
+ "raw_max": vmax,
104
+ "colormap_data": _build_colormap_lut(self._state["colormap_name"]),
105
+ })
106
+ if units is not None:
107
+ self._state["units"] = units
108
+ self._push()
109
+
110
+ def __repr__(self) -> str:
111
+ xe = self._state.get("x_axis", [])
112
+ ye = self._state.get("y_axis", [])
113
+ cols = max(0, len(xe) - 1)
114
+ rows = max(0, len(ye) - 1)
115
+ cmap = self._state.get("colormap_name", "?")
116
+ return f"PlotMesh({rows}×{cols}, cmap={cmap!r})"
@@ -0,0 +1,4 @@
1
+ """anyplotlib.plot3d — 3-D surface, scatter, and line plot panel."""
2
+ from anyplotlib.plot3d._plot3d import Plot3D
3
+
4
+ __all__ = ["Plot3D"]
@@ -0,0 +1,524 @@
1
+ """
2
+ plot3d/_plot3d.py
3
+ =================
4
+ 3-D surface / scatter / line plot panel.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Callable
10
+
11
+ import numpy as np
12
+
13
+ from anyplotlib._base_plot import _BasePlot
14
+ from anyplotlib.callbacks import CallbackRegistry
15
+ from anyplotlib._utils import _arr_to_b64, _build_colormap_lut
16
+
17
+
18
+ def _triangulate_grid(rows: int, cols: int) -> list:
19
+ """Return a flat list of [i0, i1, i2] triangle indices for an (rows×cols) grid."""
20
+ faces = []
21
+ for r in range(rows - 1):
22
+ for c in range(cols - 1):
23
+ i = r * cols + c
24
+ faces.append([i, i + 1, i + cols])
25
+ faces.append([i + 1, i + cols + 1, i + cols])
26
+ return faces
27
+
28
+
29
+ def _colors_to_u8(colors, n: int) -> np.ndarray:
30
+ """Convert per-point colours to an (N, 3) uint8 RGB array.
31
+
32
+ Accepts a sequence of CSS hex strings (``"#rrggbb"``) or an (N, 3)
33
+ numeric array (floats 0–1, or 0–255).
34
+ """
35
+ if isinstance(colors, (list, tuple)) and colors and isinstance(colors[0], str):
36
+ out = np.empty((len(colors), 3), dtype=np.uint8)
37
+ for i, c in enumerate(colors):
38
+ h = c.lstrip("#")
39
+ if len(h) != 6:
40
+ raise ValueError(f"colors[{i}]: expected '#rrggbb', got {c!r}")
41
+ out[i] = [int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)]
42
+ else:
43
+ arr = np.asarray(colors)
44
+ if arr.ndim != 2 or arr.shape[1] != 3:
45
+ raise ValueError(f"colors must be (N, 3) or a list of hex strings, "
46
+ f"got shape {arr.shape}")
47
+ if arr.dtype != np.uint8:
48
+ arr = arr.astype(np.float64)
49
+ if arr.max() <= 1.0:
50
+ arr = arr * 255.0
51
+ arr = np.clip(arr, 0, 255)
52
+ out = arr.astype(np.uint8)
53
+ if len(out) != n:
54
+ raise ValueError(f"got {len(out)} colors for {n} points")
55
+ return out
56
+
57
+
58
+ def _geometry_state(geom_type: str, x, y, z, bounds=None) -> dict:
59
+ """Validate x/y/z for *geom_type* and return the wire-format state fields.
60
+
61
+ Shared by ``Plot3D.__init__`` and ``Plot3D.set_data`` so geometry
62
+ validation and encoding live in exactly one place.
63
+
64
+ Parameters
65
+ ----------
66
+ bounds : ((xmin, xmax), (ymin, ymax), (zmin, zmax)) or None
67
+ Override the auto-computed data bounds. The JS renderer normalises
68
+ geometry into these bounds, so fixing them keeps the origin and
69
+ scale stable — essential for direction vectors on a unit sphere
70
+ (use ``((-1, 1),) * 3``) or when streaming data of varying extent.
71
+ """
72
+ x = np.asarray(x, dtype=float)
73
+ y = np.asarray(y, dtype=float)
74
+ z = np.asarray(z, dtype=float)
75
+
76
+ if geom_type == "surface":
77
+ # Accept 2-D grid arrays (meshgrid style) or 1-D flat arrays
78
+ if x.ndim == 2 and y.ndim == 2 and z.ndim == 2:
79
+ rows, cols = z.shape
80
+ xf, yf, zf = x.ravel(), y.ravel(), z.ravel()
81
+ elif x.ndim == 1 and y.ndim == 1 and z.ndim == 2:
82
+ rows, cols = z.shape
83
+ if len(x) != cols or len(y) != rows:
84
+ raise ValueError(
85
+ "For surface with 1-D x/y: x must have length ncols "
86
+ "and y must have length nrows")
87
+ XX, YY = np.meshgrid(x, y)
88
+ xf, yf, zf = XX.ravel(), YY.ravel(), z.ravel()
89
+ else:
90
+ raise ValueError(
91
+ "Surface x/y/z must be 2-D grids of the same shape, "
92
+ "or 1-D x/y centre arrays with 2-D z.")
93
+ faces_list = _triangulate_grid(rows, cols)
94
+ else:
95
+ if x.ndim != 1 or y.ndim != 1 or z.ndim != 1:
96
+ raise ValueError(f"{geom_type} x, y, z must be 1-D arrays")
97
+ if not (len(x) == len(y) == len(z)):
98
+ raise ValueError("x, y, z must have the same length")
99
+ xf, yf, zf = x, y, z
100
+ faces_list = []
101
+
102
+ # Encode geometry as b64 (float32 saves 50 % wire size vs float64)
103
+ verts_arr = np.column_stack([xf, yf, zf]).astype(np.float32) # (N, 3)
104
+ zvals_arr = zf.astype(np.float32) # (N,)
105
+ faces_arr = (np.asarray(faces_list, dtype=np.int32).reshape(-1, 3)
106
+ if faces_list else np.empty((0, 3), dtype=np.int32))
107
+
108
+ if bounds is not None:
109
+ (bx0, bx1), (by0, by1), (bz0, bz1) = bounds
110
+ data_bounds = {"xmin": float(bx0), "xmax": float(bx1),
111
+ "ymin": float(by0), "ymax": float(by1),
112
+ "zmin": float(bz0), "zmax": float(bz1)}
113
+ else:
114
+ data_bounds = {
115
+ "xmin": float(xf.min()), "xmax": float(xf.max()),
116
+ "ymin": float(yf.min()), "ymax": float(yf.max()),
117
+ "zmin": float(zf.min()), "zmax": float(zf.max()),
118
+ }
119
+
120
+ return {
121
+ "vertices_b64": _arr_to_b64(verts_arr, np.float32),
122
+ "vertices_count": len(verts_arr),
123
+ "faces_b64": _arr_to_b64(faces_arr, np.int32),
124
+ "faces_count": len(faces_arr),
125
+ "z_values_b64": _arr_to_b64(zvals_arr, np.float32),
126
+ "data_bounds": data_bounds,
127
+ }
128
+
129
+
130
+ class Plot3D(_BasePlot):
131
+ """3-D plot panel.
132
+
133
+ Supports four geometry types:
134
+
135
+ * ``'surface'`` – triangulated surface, Z-coloured via colormap.
136
+ * ``'scatter'`` – point cloud; single colour or per-point ``colors``.
137
+ * ``'line'`` – connected line through 3-D points.
138
+ * ``'voxels'`` – shaded translucent cubes at the given centres;
139
+ voxels lying on a :class:`~anyplotlib.PlaneWidget` slice render more
140
+ opaque.
141
+
142
+ A single point can be emphasised with :meth:`set_highlight` (e.g. the
143
+ "current" orientation in an IPF explorer), and ``bounds=`` fixes the
144
+ axes extents for origin-true geometry such as unit vectors on a sphere.
145
+ Draggable :class:`~anyplotlib.PlaneWidget` slice selectors are added
146
+ with :meth:`add_widget`.
147
+
148
+ Created by :meth:`Axes.plot_surface`, :meth:`Axes.scatter3d`,
149
+ and :meth:`Axes.plot3d`.
150
+
151
+ Not an anywidget. Holds state in ``_state`` dict; every mutation
152
+ calls ``_push()`` which writes to the parent Figure's panel trait.
153
+ """
154
+
155
+ #: Heavy, rarely-changing state keys routed to the separate geometry
156
+ #: channel — re-sent only when their content changes, so view updates
157
+ #: (highlight / camera / planes) never re-transmit them.
158
+ _GEOM_KEYS = frozenset({
159
+ "vertices_b64", "faces_b64", "z_values_b64", "point_colors_b64",
160
+ "colormap_data",
161
+ })
162
+
163
+ def __init__(self, geom_type: str,
164
+ x, y, z, *,
165
+ colormap: str = "viridis",
166
+ color: str = "#4fc3f7",
167
+ colors=None,
168
+ point_size: float = 4.0,
169
+ linewidth: float = 1.5,
170
+ x_label: str = "x",
171
+ y_label: str = "y",
172
+ z_label: str = "z",
173
+ azimuth: float = -60.0,
174
+ elevation: float = 30.0,
175
+ zoom: float = 1.0,
176
+ bounds=None,
177
+ voxel_size: float = 1.0,
178
+ alpha: float | None = None,
179
+ gpu: str | bool = "auto"):
180
+ self._id: str = ""
181
+ self._fig: object = None
182
+ self._gpu_active: bool = False
183
+
184
+ geom_type = geom_type.lower()
185
+ if geom_type not in ("surface", "scatter", "line", "voxels"):
186
+ raise ValueError(
187
+ "geom_type must be 'surface', 'scatter', 'line', or 'voxels'")
188
+
189
+ cmap_lut = _build_colormap_lut(colormap)
190
+ self._bounds = bounds
191
+ geom = _geometry_state(geom_type, x, y, z, bounds=bounds)
192
+
193
+ point_colors_b64 = ""
194
+ if colors is not None:
195
+ if geom_type not in ("scatter", "voxels"):
196
+ raise ValueError(
197
+ "per-point colors are only supported for scatter/voxels")
198
+ point_colors_b64 = _arr_to_b64(
199
+ _colors_to_u8(colors, geom["vertices_count"]), np.uint8)
200
+
201
+ # The canvas budget is ~20k cubes; WebGPU (gpu="auto"/True) handles
202
+ # far more, so only warn when GPU is explicitly disabled.
203
+ gpu_off = gpu is False or str(gpu) == "off"
204
+ if geom_type == "voxels" and gpu_off and geom["vertices_count"] > 20_000:
205
+ import warnings
206
+ warnings.warn(
207
+ f"Rendering {geom['vertices_count']:,} voxels with gpu=False — "
208
+ f"the canvas renderer budgets roughly 3–6 µs per cube, so "
209
+ f"interactive frame rates need ≤ ~20k. Either allow WebGPU "
210
+ f"(gpu='auto'), downsample the volume (stride slicing) or "
211
+ f"extract boundary voxels, and show full-resolution data in "
212
+ f"linked 2-D slice panels instead.",
213
+ RuntimeWarning, stacklevel=3)
214
+
215
+ self._state: dict = {
216
+ "kind": "3d",
217
+ "geom_type": geom_type,
218
+ **geom,
219
+ "colormap_name": colormap,
220
+ "colormap_data": cmap_lut,
221
+ "color": color,
222
+ "point_colors_b64": point_colors_b64,
223
+ # Highlight point: {"x","y","z","color","size"} or None
224
+ "highlight": None,
225
+ # Reference sphere: {"radius","color","alpha","wireframe"} or None
226
+ "sphere": None,
227
+ # WebGPU activation policy: 'auto' (GPU above a point threshold),
228
+ # 'always', or 'off'. Falls back to Canvas2D whenever the GPU is
229
+ # unavailable; query the result via the gpu_active property.
230
+ "gpu_mode": ("always" if gpu is True else
231
+ "off" if gpu is False else str(gpu)),
232
+ # Voxel rendering (geom_type == 'voxels')
233
+ "voxel_size": float(voxel_size),
234
+ "voxel_alpha": float(alpha) if alpha is not None else 0.3,
235
+ "voxel_slice_alpha": 0.95,
236
+ # Interactive overlay widgets (PlaneWidget slice selectors)
237
+ "overlay_widgets": [],
238
+ "point_size": float(point_size),
239
+ "linewidth": float(linewidth),
240
+ "title": "",
241
+ "x_label": x_label,
242
+ "y_label": y_label,
243
+ "z_label": z_label,
244
+ "axis_visible": True,
245
+ "azimuth": float(azimuth),
246
+ "elevation": float(elevation),
247
+ "zoom": float(zoom),
248
+ "_default_azimuth": float(azimuth),
249
+ "_default_elevation": float(elevation),
250
+ "_default_zoom": float(zoom),
251
+ "_view_from_python": False,
252
+ "pointer_settled_ms": 0,
253
+ "pointer_settled_delta": 4,
254
+ }
255
+ self.callbacks = CallbackRegistry()
256
+ self._widgets: dict = {}
257
+
258
+ # ------------------------------------------------------------------
259
+ def _push(self) -> None:
260
+ if self._fig is None:
261
+ return
262
+ self._state["overlay_widgets"] = [w.to_dict() for w in self._widgets.values()]
263
+ self._fig._push(self._id)
264
+
265
+ def _push_fields(self, **fields) -> None:
266
+ """Targeted update of small state fields without re-sending geometry.
267
+
268
+ For a heavy voxel/scatter panel, moving the highlight or camera would
269
+ otherwise re-transmit hundreds of KB of unchanged ``vertices_b64`` —
270
+ this ships only *fields* via the lightweight event channel.
271
+ """
272
+ self._state.update(fields)
273
+ if self._fig is not None:
274
+ self._fig._push_panel_fields(self._id, fields)
275
+
276
+ # ------------------------------------------------------------------
277
+ # Interactive widgets (3-D)
278
+ # ------------------------------------------------------------------
279
+ def add_widget(self, kind: str, **kwargs):
280
+ """Add an interactive overlay widget to this 3-D panel.
281
+
282
+ Currently supports ``"plane"`` — a draggable axis-aligned slice
283
+ plane (see :class:`~anyplotlib.PlaneWidget`)::
284
+
285
+ pw = vol.add_widget("plane", axis="z", position=24)
286
+
287
+ @pw.add_event_handler("pointer_move")
288
+ def on_drag(event):
289
+ resliced(int(round(pw.position)))
290
+ """
291
+ if kind.lower() != "plane":
292
+ raise ValueError("3-D panels currently support only 'plane' widgets")
293
+ from anyplotlib.widgets import PlaneWidget
294
+ widget = PlaneWidget(lambda: None, **kwargs)
295
+ widget._push_fn = self._make_widget_push_fn(widget)
296
+ self._widgets[widget.id] = widget
297
+ self._push()
298
+ return widget
299
+
300
+ def remove_widget(self, wid) -> None:
301
+ """Remove a widget by ID string or Widget instance."""
302
+ from anyplotlib.widgets import Widget
303
+ if isinstance(wid, Widget):
304
+ wid = wid.id
305
+ if wid not in self._widgets:
306
+ raise KeyError(wid)
307
+ del self._widgets[wid]
308
+ self._push()
309
+
310
+ def list_widgets(self) -> list:
311
+ """Return a list of all active widget objects on this panel."""
312
+ return list(self._widgets.values())
313
+
314
+ # ------------------------------------------------------------------
315
+ def set_voxel_alpha(self, alpha: float, slice_alpha: float | None = None) -> None:
316
+ """Set voxel transparency (geom_type ``'voxels'``).
317
+
318
+ Parameters
319
+ ----------
320
+ alpha : float
321
+ Base opacity (0–1) for voxels not on any plane widget.
322
+ slice_alpha : float, optional
323
+ Opacity for voxels lying on a :class:`~anyplotlib.PlaneWidget`
324
+ slice. ``None`` keeps the current value (default 0.95).
325
+ """
326
+ self._state["voxel_alpha"] = float(alpha)
327
+ if slice_alpha is not None:
328
+ self._state["voxel_slice_alpha"] = float(slice_alpha)
329
+ self._push()
330
+
331
+ def to_state_dict(self) -> dict:
332
+ # Always serialise the live overlay widgets, so *every* push path
333
+ # (full _push, targeted _push_fields, batched) carries the current
334
+ # plane positions. Without this, a view-only push (set_highlight /
335
+ # set_view) re-serialises a stale overlay_widgets snapshot and clobbers
336
+ # an in-progress plane drag in JS — the "snap-back" symptom.
337
+ d = dict(self._state)
338
+ if self._widgets:
339
+ d["overlay_widgets"] = [w.to_dict() for w in self._widgets.values()]
340
+ return d
341
+
342
+ # ------------------------------------------------------------------
343
+ @property
344
+ def gpu_active(self) -> bool:
345
+ """``True`` if this panel is currently rendering geometry on the GPU.
346
+
347
+ Reflects the JS renderer's decision after the first frame: WebGPU is
348
+ used only when available and when the panel's ``gpu`` policy and
349
+ point count call for it. Always ``False`` on the Canvas2D fallback
350
+ path (no ``navigator.gpu``, no adapter, device lost, or ``gpu=False``).
351
+ """
352
+ return self._gpu_active
353
+
354
+ def _set_gpu_active(self, active: bool) -> None:
355
+ """Internal: called from the Figure's gpu_status event dispatch."""
356
+ self._gpu_active = bool(active)
357
+
358
+ # ------------------------------------------------------------------
359
+ # Display settings
360
+ # ------------------------------------------------------------------
361
+ def set_colormap(self, name: str) -> None:
362
+ """Set the surface colormap (ignored for scatter/line)."""
363
+ self._state["colormap_name"] = name
364
+ self._state["colormap_data"] = _build_colormap_lut(name)
365
+ self._push()
366
+
367
+ def set_view(self, azimuth: float | None = None,
368
+ elevation: float | None = None) -> None:
369
+ """Set the camera azimuth (°) and/or elevation (°).
370
+
371
+ Uses a targeted field push so re-aiming the camera never re-transmits
372
+ the panel's geometry — important for large voxel/scatter panels.
373
+ """
374
+ fields = {"_view_from_python": True}
375
+ if azimuth is not None: fields["azimuth"] = float(azimuth)
376
+ if elevation is not None: fields["elevation"] = float(elevation)
377
+ self._push_fields(**fields)
378
+ self._state["_view_from_python"] = False
379
+
380
+ def set_zoom(self, zoom: float) -> None:
381
+ self._push_fields(zoom=float(zoom), _view_from_python=True)
382
+ self._state["_view_from_python"] = False
383
+
384
+ def reset_view(self) -> None:
385
+ """Restore the camera to the angles/zoom set at construction time."""
386
+ with self._python_view_push():
387
+ self._state["azimuth"] = self._state["_default_azimuth"]
388
+ self._state["elevation"] = self._state["_default_elevation"]
389
+ self._state["zoom"] = self._state["_default_zoom"]
390
+
391
+ def set_xlabel(self, label: str, fontsize: float | None = None) -> None:
392
+ """Set the x-axis label (mini-TeX allowed; default size 11 px)."""
393
+ self._set_label("x_label", label, "x_label_size", fontsize)
394
+
395
+ def set_ylabel(self, label: str, fontsize: float | None = None) -> None:
396
+ """Set the y-axis label (mini-TeX allowed; default size 11 px)."""
397
+ self._set_label("y_label", label, "y_label_size", fontsize)
398
+
399
+ def set_zlabel(self, label: str, fontsize: float | None = None) -> None:
400
+ """Set the z-axis label (mini-TeX allowed; default size 11 px)."""
401
+ self._set_label("z_label", label, "z_label_size", fontsize)
402
+
403
+ def get_xlim(self) -> tuple:
404
+ """Return the data x range as ``(xmin, xmax)``."""
405
+ b = self._state["data_bounds"]
406
+ return (b["xmin"], b["xmax"])
407
+
408
+ def get_ylim(self) -> tuple:
409
+ """Return the data y range as ``(ymin, ymax)``."""
410
+ b = self._state["data_bounds"]
411
+ return (b["ymin"], b["ymax"])
412
+
413
+ def get_zlim(self) -> tuple:
414
+ """Return the data z range as ``(zmin, zmax)``."""
415
+ b = self._state["data_bounds"]
416
+ return (b["zmin"], b["zmax"])
417
+
418
+ def set_data(self, x, y, z) -> None:
419
+ """Replace the geometry data (same shape rules as the constructor).
420
+
421
+ Bounds given at construction time (``bounds=``) are preserved.
422
+ """
423
+ self._state.update(_geometry_state(
424
+ self._state["geom_type"], x, y, z, bounds=self._bounds))
425
+ self._push()
426
+
427
+ def set_point_colors(self, colors) -> None:
428
+ """Set (or clear) per-point colours on a scatter or voxels panel.
429
+
430
+ Parameters
431
+ ----------
432
+ colors : list of "#rrggbb" strings, (N, 3) array, or None
433
+ One colour per point / voxel. Floats are interpreted as 0–1 (or
434
+ 0–255 when the max exceeds 1). ``None`` reverts to the single
435
+ ``color`` for all elements.
436
+ """
437
+ if self._state["geom_type"] not in ("scatter", "voxels"):
438
+ raise ValueError(
439
+ "per-point colors are only supported for scatter/voxels")
440
+ if colors is None:
441
+ self._state["point_colors_b64"] = ""
442
+ else:
443
+ n = self._state["vertices_count"]
444
+ self._state["point_colors_b64"] = _arr_to_b64(
445
+ _colors_to_u8(colors, n), np.uint8)
446
+ self._push()
447
+
448
+ def set_highlight(self, x: float, y: float, z: float, *,
449
+ color: str = "#ff1744", size: float = 7.0) -> None:
450
+ """Mark one 3-D point with an emphasised dot drawn on top.
451
+
452
+ The highlight is independent of the panel's geometry — use it to
453
+ flag the "current" item in a point cloud or on a surface (e.g. the
454
+ orientation under a crosshair in an IPF explorer). Points on the
455
+ far side of the data are drawn semi-transparent as a depth cue.
456
+
457
+ Parameters
458
+ ----------
459
+ x, y, z : float
460
+ Position in data coordinates.
461
+ color : str, optional
462
+ CSS colour of the dot and ring. Default ``"#ff1744"``.
463
+ size : float, optional
464
+ Dot radius in pixels. Default 7.
465
+
466
+ See Also
467
+ --------
468
+ clear_highlight : Remove the highlight.
469
+ """
470
+ self._push_fields(highlight={
471
+ "x": float(x), "y": float(y), "z": float(z),
472
+ "color": str(color), "size": float(size),
473
+ })
474
+
475
+ def clear_highlight(self) -> None:
476
+ """Remove the highlight point set by :meth:`set_highlight`."""
477
+ self._push_fields(highlight=None)
478
+
479
+ def set_sphere(self, radius: float = 1.0, *,
480
+ color: str = "#9e9e9e",
481
+ alpha: float = 0.15,
482
+ wireframe: bool = True) -> None:
483
+ """Draw an origin-centred reference sphere behind the data.
484
+
485
+ Rendered as a shaded silhouette disk plus latitude/longitude
486
+ wireframe arcs (far-side arcs dimmed). Scatter points on the far
487
+ side of the sphere are also dimmed, so a point cloud on the sphere
488
+ reads with correct depth — ideal for inverse-pole-figure /
489
+ orientation plots of unit vectors.
490
+
491
+ Assumes origin-centred, isotropic bounds — pass
492
+ ``bounds=((-r, r),) * 3`` to the constructor so the sphere's
493
+ screen silhouette is a true circle.
494
+
495
+ Parameters
496
+ ----------
497
+ radius : float, optional
498
+ Sphere radius in data units. Default 1 (unit sphere).
499
+ color : str, optional
500
+ Base CSS colour of the shading and wireframe. Default grey.
501
+ alpha : float, optional
502
+ Opacity of the shaded silhouette (0–1). Default 0.15.
503
+ wireframe : bool, optional
504
+ Draw latitude/longitude arcs. Default True.
505
+
506
+ See Also
507
+ --------
508
+ clear_sphere : Remove the reference sphere.
509
+ """
510
+ self._state["sphere"] = {
511
+ "radius": float(radius), "color": str(color),
512
+ "alpha": float(alpha), "wireframe": bool(wireframe),
513
+ }
514
+ self._push()
515
+
516
+ def clear_sphere(self) -> None:
517
+ """Remove the reference sphere set by :meth:`set_sphere`."""
518
+ self._state["sphere"] = None
519
+ self._push()
520
+
521
+ def __repr__(self) -> str:
522
+ geom = self._state.get("geom_type", "?")
523
+ n = self._state.get("vertices_count", 0)
524
+ return f"Plot3D(geom={geom!r}, n_vertices={n})"
@@ -0,0 +1,4 @@
1
+ """anyplotlib.plotxy — PlotXY, a blank data-coordinate 2-D axis."""
2
+ from anyplotlib.plotxy._plotxy import PlotXY
3
+
4
+ __all__ = ["PlotXY"]