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,436 @@
1
+ """
2
+ plot1d/_plotbar.py
3
+ ==================
4
+ Bar chart panel (PlotBar).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+ from typing import Callable
11
+
12
+ from anyplotlib._base_plot import _BasePlot, _PanelMixin
13
+ from anyplotlib.callbacks import CallbackRegistry
14
+ from anyplotlib.widgets import (
15
+ Widget,
16
+ VLineWidget as _VLineWidget,
17
+ HLineWidget as _HLineWidget,
18
+ RangeWidget as _RangeWidget,
19
+ PointWidget as _PointWidget,
20
+ )
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # _bar_x_axis helper
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def _bar_x_axis(x_centers: np.ndarray) -> list:
28
+ """Return a 2-element [x_left_edge, x_right_edge] list for a bar chart.
29
+
30
+ The edges are half a slot-width outside the first/last bar centre so that
31
+ a vline_widget at ``x_centers[i]`` renders at exactly the bar's centre
32
+ pixel when used with ``_xToFrac1d`` / ``_fracToPx1d`` in the JS renderer.
33
+ """
34
+ n = len(x_centers)
35
+ if n == 0:
36
+ return [0.0, 1.0]
37
+ if n == 1:
38
+ return [float(x_centers[0]) - 0.5, float(x_centers[0]) + 0.5]
39
+ slot = (float(x_centers[-1]) - float(x_centers[0])) / (n - 1)
40
+ half = slot / 2.0
41
+ return [float(x_centers[0]) - half, float(x_centers[-1]) + half]
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # PlotBar
46
+ # ---------------------------------------------------------------------------
47
+
48
+ _LOG_CLAMP = 1e-10 # smallest positive value used when log_scale=True
49
+
50
+ _DEFAULT_GROUP_PALETTE = [
51
+ "#4fc3f7", "#ff7043", "#66bb6a", "#ab47bc",
52
+ "#ffa726", "#26c6da", "#ec407a", "#8d6e63",
53
+ ]
54
+
55
+
56
+ def _bar_range(flat: np.ndarray, bottom: float, log_scale: bool):
57
+ """Return ``(dmin, dmax)`` with padding for the value axis."""
58
+ if log_scale:
59
+ pos = flat[flat > 0]
60
+ dmin = float(np.nanmin(pos)) if len(pos) else _LOG_CLAMP
61
+ dmax = max(float(np.nanmax(flat)) if len(flat) else 1.0,
62
+ bottom if bottom > 0 else _LOG_CLAMP)
63
+ if dmin <= 0:
64
+ dmin = _LOG_CLAMP
65
+ if dmax <= 0:
66
+ dmax = 1.0
67
+ dmax = 10 ** (np.log10(dmax) + 0.15)
68
+ dmin = 10 ** (np.log10(dmin) - 0.15)
69
+ else:
70
+ dmin = min(bottom, float(np.nanmin(flat)) if len(flat) else 0.0)
71
+ dmax = max(bottom, float(np.nanmax(flat)) if len(flat) else 1.0)
72
+ pad = (dmax - dmin) * 0.07 if dmax > dmin else 0.5
73
+ dmax += pad
74
+ if dmin < bottom:
75
+ dmin -= pad
76
+ return dmin, dmax
77
+
78
+
79
+ class PlotBar(_BasePlot, _PanelMixin):
80
+ """Bar-chart plot panel.
81
+
82
+ Not an anywidget. Holds state in ``_state`` dict; every mutation calls
83
+ ``_push()`` which writes to the parent Figure's panel trait.
84
+
85
+ Supports grouped bars (pass a 2-D *height* array with shape ``(N, G)``),
86
+ log-scale value axis, draggable overlay widgets, and hover/click callbacks.
87
+
88
+ Created by :meth:`Axes.bar`.
89
+ """
90
+
91
+ def __init__(self, x, height=None, width: float = 0.8, bottom: float = 0.0, *,
92
+ align: str = "center",
93
+ color: str = "#4fc3f7",
94
+ colors=None,
95
+ orient: str = "v",
96
+ log_scale: bool = False,
97
+ group_labels=None,
98
+ group_colors=None,
99
+ show_values: bool = False,
100
+ units: str = "",
101
+ y_units: str = "",
102
+ # ── legacy backward-compat kwargs ──────────────────────
103
+ x_labels=None,
104
+ x_centers=None,
105
+ bar_width=None,
106
+ baseline=None,
107
+ values=None):
108
+ self._id: str = ""
109
+ self._fig: object = None
110
+
111
+ if align not in ("center", "edge"):
112
+ raise ValueError("align must be 'center' or 'edge'")
113
+ if orient not in ("v", "h"):
114
+ raise ValueError("orient must be 'v' or 'h'")
115
+
116
+ # ── legacy resolution ──────────────────────────────────────────
117
+ if height is None:
118
+ if values is not None:
119
+ height = values
120
+ else:
121
+ height = x
122
+ x = None
123
+ if baseline is not None:
124
+ bottom = baseline
125
+ if bar_width is not None:
126
+ width = bar_width
127
+
128
+ # ── height (values) — 1-D or 2-D for grouped bars ─────────────
129
+ height_arr = np.asarray(height, dtype=float)
130
+ if height_arr.ndim == 1:
131
+ groups = 1
132
+ values_2d = height_arr.reshape(-1, 1)
133
+ elif height_arr.ndim == 2:
134
+ groups = height_arr.shape[1]
135
+ values_2d = height_arr
136
+ else:
137
+ raise ValueError(
138
+ f"height must be 1-D or 2-D, got shape {height_arr.shape}"
139
+ )
140
+ n = values_2d.shape[0]
141
+
142
+ # ── x (positions or labels) ────────────────────────────────────
143
+ _x_labels: list = []
144
+ _x_centers: np.ndarray | None = None
145
+
146
+ if x is not None:
147
+ x_list = list(x)
148
+ if x_list and isinstance(x_list[0], str):
149
+ _x_labels = x_list
150
+ else:
151
+ _x_centers = np.asarray(x, dtype=float)
152
+
153
+ # Legacy keyword overrides
154
+ if x_labels is not None:
155
+ _x_labels = list(x_labels)
156
+ if x_centers is not None:
157
+ _x_centers = np.asarray(x_centers, dtype=float)
158
+
159
+ if _x_centers is None:
160
+ _x_centers = np.arange(n, dtype=float)
161
+ if len(_x_centers) != n:
162
+ raise ValueError("x length must match height length")
163
+
164
+ # ── data range ─────────────────────────────────────────────────
165
+ flat = values_2d.ravel()
166
+ dmin, dmax = _bar_range(flat, float(bottom), bool(log_scale))
167
+
168
+ # ── group colours ──────────────────────────────────────────────
169
+ if group_colors is None:
170
+ gc_list = (
171
+ [_DEFAULT_GROUP_PALETTE[i % len(_DEFAULT_GROUP_PALETTE)]
172
+ for i in range(groups)]
173
+ if groups > 1 else []
174
+ )
175
+ else:
176
+ gc_list = list(group_colors)
177
+
178
+ x_axis = _bar_x_axis(_x_centers)
179
+
180
+ self._state: dict = {
181
+ "kind": "bar",
182
+ "values": values_2d.tolist(), # always (N, G) 2-D list
183
+ "groups": groups,
184
+ "x_centers": _x_centers.tolist(),
185
+ "x_labels": _x_labels,
186
+ "bar_color": color,
187
+ "bar_colors": list(colors) if colors is not None else [],
188
+ "group_labels": list(group_labels) if group_labels is not None else [],
189
+ "group_colors": gc_list,
190
+ "bar_width": float(width),
191
+ "align": align,
192
+ "orient": orient,
193
+ "baseline": float(bottom),
194
+ "log_scale": bool(log_scale),
195
+ "show_values": bool(show_values),
196
+ "data_min": dmin,
197
+ "data_max": dmax,
198
+ "y_range": None,
199
+ "units": units,
200
+ "y_units": y_units,
201
+ "title": "",
202
+ "x_label": "",
203
+ "y_label": "",
204
+ "axis_visible": True,
205
+ "x_ticks_visible": True,
206
+ "y_ticks_visible": True,
207
+ # overlay-widget coordinate system (mirrors Plot1D)
208
+ "x_axis": x_axis,
209
+ "view_x0": 0.0,
210
+ "view_x1": 1.0,
211
+ "overlay_widgets": [],
212
+ "pointer_settled_ms": 0,
213
+ "pointer_settled_delta": 4,
214
+ "_view_from_python": False,
215
+ }
216
+ self.callbacks = CallbackRegistry()
217
+ self._widgets: dict[str, Widget] = {}
218
+
219
+ def to_state_dict(self) -> dict:
220
+ d = dict(self._state)
221
+ d["overlay_widgets"] = [w.to_dict() for w in self._widgets.values()]
222
+ return d
223
+
224
+ # ------------------------------------------------------------------
225
+ # Data
226
+ # ------------------------------------------------------------------
227
+ def set_data(self, height, x=None, x_labels=None, *, x_centers=None) -> None:
228
+ """Replace bar heights; recalculates the value-axis range automatically.
229
+
230
+ Parameters
231
+ ----------
232
+ height : array-like, shape ``(N,)`` or ``(N, G)``
233
+ New bar heights. For grouped charts the group count *G* must
234
+ match the original.
235
+ x : array-like of numeric, optional
236
+ New bar positions (replaces the stored ``x_centers``). Also
237
+ accepts the legacy keyword alias ``x_centers``.
238
+ x_labels : list of str, optional
239
+ New category labels.
240
+ """
241
+ height_arr = np.asarray(height, dtype=float)
242
+ if height_arr.ndim == 1:
243
+ values_2d = height_arr.reshape(-1, 1)
244
+ elif height_arr.ndim == 2:
245
+ expected_g = self._state.get("groups", 1)
246
+ if height_arr.shape[1] != expected_g:
247
+ raise ValueError(
248
+ f"Group count mismatch: expected {expected_g}, "
249
+ f"got {height_arr.shape[1]}"
250
+ )
251
+ values_2d = height_arr
252
+ else:
253
+ raise ValueError(
254
+ f"height must be 1-D or 2-D, got shape {height_arr.shape}"
255
+ )
256
+
257
+ flat = values_2d.ravel()
258
+ baseline = self._state["baseline"]
259
+ log_scale = self._state.get("log_scale", False)
260
+ dmin, dmax = _bar_range(flat, float(baseline), bool(log_scale))
261
+
262
+ self._state["values"] = values_2d.tolist()
263
+ self._state["data_min"] = dmin
264
+ self._state["data_max"] = dmax
265
+
266
+ # Accept both `x` and legacy `x_centers` keyword
267
+ _x = x if x is not None else x_centers
268
+ if _x is not None:
269
+ xc = np.asarray(_x, dtype=float)
270
+ self._state["x_centers"] = xc.tolist()
271
+ self._state["x_axis"] = _bar_x_axis(xc)
272
+ if x_labels is not None:
273
+ self._state["x_labels"] = list(x_labels)
274
+ self._push()
275
+
276
+ # ------------------------------------------------------------------
277
+ # Display settings
278
+ # ------------------------------------------------------------------
279
+ def set_color(self, color: str) -> None:
280
+ """Set a single colour for all bars."""
281
+ self._state["bar_color"] = color
282
+ self._push()
283
+
284
+ def set_colors(self, colors) -> None:
285
+ """Set per-bar colours (list of CSS colour strings, length N)."""
286
+ self._state["bar_colors"] = list(colors)
287
+ self._push()
288
+
289
+ def set_show_values(self, show: bool) -> None:
290
+ """Show or hide in-bar value annotations."""
291
+ self._state["show_values"] = bool(show)
292
+ self._push()
293
+
294
+ def set_log_scale(self, log_scale: bool) -> None:
295
+ """Enable or disable a logarithmic value axis.
296
+
297
+ When *log_scale* is ``True`` any non-positive values are clamped to
298
+ ``1e-10`` for display; the data-range bounds are recalculated in
299
+ log-space automatically.
300
+ """
301
+ self._state["log_scale"] = bool(log_scale)
302
+ flat = np.asarray(self._state["values"]).ravel()
303
+ baseline = self._state["baseline"]
304
+ dmin, dmax = _bar_range(flat, float(baseline), bool(log_scale))
305
+ self._state["data_min"] = dmin
306
+ self._state["data_max"] = dmax
307
+ self._push()
308
+
309
+ # ------------------------------------------------------------------
310
+ # Display control
311
+ # ------------------------------------------------------------------
312
+ def set_xlabel(self, label: str, fontsize: float | None = None) -> None:
313
+ """Set the x-axis label (mini-TeX allowed; default size 10 px)."""
314
+ self._set_label("x_label", label, "x_label_size", fontsize)
315
+
316
+ def set_ylabel(self, label: str, fontsize: float | None = None) -> None:
317
+ """Set the y-axis / value-axis label (mini-TeX allowed; default size 10 px)."""
318
+ self._set_label("y_label", label, "y_label_size", fontsize)
319
+
320
+ def set_bar_width(self, width: float) -> None:
321
+ """Set the bar width."""
322
+ self._state["bar_width"] = float(width)
323
+ self._push()
324
+
325
+ def set_align(self, align: str) -> None:
326
+ """Set bar alignment: ``'center'`` or ``'edge'``."""
327
+ if align not in ("center", "edge"):
328
+ raise ValueError("align must be 'center' or 'edge'")
329
+ self._state["align"] = align
330
+ self._push()
331
+
332
+ def set_orient(self, orient: str) -> None:
333
+ """Set bar orientation: ``'v'`` (vertical) or ``'h'`` (horizontal)."""
334
+ if orient not in ("v", "h"):
335
+ raise ValueError("orient must be 'v' or 'h'")
336
+ self._state["orient"] = orient
337
+ self._push()
338
+
339
+ def set_group_labels(self, labels) -> None:
340
+ """Replace the category labels on the category axis."""
341
+ self._state["group_labels"] = list(labels)
342
+ self._push()
343
+
344
+ # ------------------------------------------------------------------
345
+ # View (xlim / ylim)
346
+ # ------------------------------------------------------------------
347
+ def set_xlim(self, xmin: float, xmax: float) -> None:
348
+ """Pan/zoom the x-axis to [xmin, xmax] in data coordinates."""
349
+ x_axis = self._state["x_axis"]
350
+ span = x_axis[1] - x_axis[0]
351
+ if span == 0:
352
+ return
353
+ with self._python_view_push():
354
+ self._state["view_x0"] = (xmin - x_axis[0]) / span
355
+ self._state["view_x1"] = (xmax - x_axis[0]) / span
356
+
357
+ def set_ylim(self, y_min: float, y_max: float) -> None:
358
+ """Fix the value-axis range to [y_min, y_max]."""
359
+ self._state["y_range"] = [float(y_min), float(y_max)]
360
+ self._push()
361
+
362
+ def get_ylim(self) -> tuple:
363
+ """Return the current value-axis range as ``(y_min, y_max)``."""
364
+ yr = self._state.get("y_range")
365
+ if yr is not None:
366
+ return (float(yr[0]), float(yr[1]))
367
+ return (float(self._state["data_min"]), float(self._state["data_max"]))
368
+
369
+ def get_xlim(self) -> tuple:
370
+ """Return the current x-axis view range in data coordinates."""
371
+ x_axis = self._state["x_axis"]
372
+ span = x_axis[1] - x_axis[0]
373
+ x0 = x_axis[0] + self._state["view_x0"] * span
374
+ x1 = x_axis[0] + self._state["view_x1"] * span
375
+ return (float(x0), float(x1))
376
+
377
+ def reset_view(self) -> None:
378
+ """Reset pan/zoom to show all bars."""
379
+ with self._python_view_push():
380
+ self._state["view_x0"] = 0.0
381
+ self._state["view_x1"] = 1.0
382
+ self._state["y_range"] = None
383
+
384
+ # ------------------------------------------------------------------
385
+ # Overlay Widgets
386
+ # ------------------------------------------------------------------
387
+ def add_vline_widget(self, x: float, color: str = "#00e5ff") -> _VLineWidget:
388
+ """Add a draggable vertical line at data position *x*."""
389
+ widget = _VLineWidget(lambda: None, x=float(x), color=color)
390
+ widget._push_fn = self._make_widget_push_fn(widget)
391
+ self._widgets[widget.id] = widget
392
+ self._push()
393
+ return widget
394
+
395
+ def add_hline_widget(self, y: float, color: str = "#00e5ff") -> _HLineWidget:
396
+ """Add a draggable horizontal line at value-axis position *y*."""
397
+ widget = _HLineWidget(lambda: None, y=float(y), color=color)
398
+ widget._push_fn = self._make_widget_push_fn(widget)
399
+ self._widgets[widget.id] = widget
400
+ self._push()
401
+ return widget
402
+
403
+ def add_range_widget(self, x0: float, x1: float,
404
+ color: str = "#00e5ff",
405
+ style: str = "band",
406
+ y: float = 0.0,
407
+ _push: bool = True) -> _RangeWidget:
408
+ """Add a draggable range overlay. See :meth:`Plot1D.add_range_widget` for full docs."""
409
+ widget = _RangeWidget(lambda: None, x0=float(x0), x1=float(x1),
410
+ color=color, style=style, y=float(y))
411
+ widget._push_fn = self._make_widget_push_fn(widget)
412
+ self._widgets[widget.id] = widget
413
+ if _push:
414
+ self._push()
415
+ return widget
416
+
417
+ def add_point_widget(self, x: float, y: float,
418
+ color: str = "#00e5ff",
419
+ show_crosshair: bool = True,
420
+ _push: bool = True) -> _PointWidget:
421
+ """Add a freely-draggable control point to this panel."""
422
+ widget = _PointWidget(lambda: None, x=float(x), y=float(y), color=color,
423
+ show_crosshair=show_crosshair)
424
+ widget._push_fn = self._make_widget_push_fn(widget)
425
+ self._widgets[widget.id] = widget
426
+ if _push:
427
+ self._push()
428
+ return widget
429
+
430
+ def __repr__(self) -> str:
431
+ n = len(self._state.get("values", []))
432
+ orient = self._state.get("orient", "v")
433
+ groups = self._state.get("groups", 1)
434
+ if groups > 1:
435
+ return f"PlotBar(n={n}, groups={groups}, orient={orient!r})"
436
+ return f"PlotBar(n={n}, orient={orient!r})"
@@ -0,0 +1,5 @@
1
+ """anyplotlib.plot2d — 2-D image plot panel classes (Plot2D, PlotMesh)."""
2
+ from anyplotlib.plot2d._plot2d import Plot2D
3
+ from anyplotlib.plot2d._plotmesh import PlotMesh
4
+
5
+ __all__ = ["Plot2D", "PlotMesh"]