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,328 @@
1
+ """
2
+ callbacks.py
3
+ ============
4
+
5
+ Event system used by all plot objects and widgets.
6
+
7
+ :class:`Event`
8
+ Flat dataclass carrying all event fields as typed top-level attributes.
9
+
10
+ :class:`CallbackRegistry`
11
+ Per-object handler store. (Full implementation added in Tasks 2-3.)
12
+
13
+ :class:`_EventMixin`
14
+ Mixin added to every plot class and widget. (Added in Task 4.)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import time
19
+ from collections import defaultdict, deque
20
+ from contextlib import contextmanager
21
+ from dataclasses import dataclass, field
22
+ from typing import Any, Callable
23
+
24
+ VALID_EVENT_TYPES = frozenset({
25
+ "pointer_down", "pointer_up", "pointer_move", "pointer_settled",
26
+ "pointer_enter", "pointer_leave", "double_click", "wheel",
27
+ "key_down", "key_up", "close", "*",
28
+ })
29
+
30
+
31
+ @dataclass
32
+ class Event:
33
+ """A single interactive event with all payload fields as typed attributes.
34
+
35
+ Universal fields (every event):
36
+ event_type, source, time_stamp, modifiers
37
+
38
+ Pointer fields (pointer_* and double_click events):
39
+ x, y — canvas coordinates within the panel (float pixels)
40
+ button — 0=left 1=middle 2=right; None on move/enter/leave/settled
41
+ buttons — bitmask of currently held buttons
42
+ xdata, ydata — data-space coordinates (None for Plot3D)
43
+ ray — Plot3D only: {"origin": [...], "direction": [...]}
44
+ line_id — Plot1D only: set when pointer is over a line
45
+ dwell_ms — pointer_settled only: actual dwell time
46
+
47
+ PlotBar extra fields (pointer_down only):
48
+ bar_index, value, x_label, group_index
49
+
50
+ Wheel fields:
51
+ dx, dy — scroll deltas
52
+
53
+ Key fields:
54
+ key — key name e.g. "q", "Enter", "ArrowLeft"
55
+ last_widget_id — id of the last widget the user clicked, or None
56
+
57
+ Propagation:
58
+ stop_propagation — set True inside a handler to halt remaining handlers
59
+ """
60
+ event_type: str
61
+ source: Any = None
62
+ time_stamp: float = field(default_factory=time.perf_counter)
63
+ modifiers: list[str] = field(default_factory=list)
64
+ # Pointer
65
+ x: float | None = None
66
+ y: float | None = None
67
+ button: int | None = None
68
+ buttons: int = 0
69
+ xdata: float | None = None
70
+ ydata: float | None = None
71
+ ray: dict | None = None
72
+ line_id: str | None = None
73
+ dwell_ms: float | None = None
74
+ # PlotBar
75
+ bar_index: int | None = None
76
+ value: float | None = None
77
+ x_label: str | None = None
78
+ group_index: int | None = None
79
+ # Wheel
80
+ dx: float | None = None
81
+ dy: float | None = None
82
+ # Key
83
+ key: str | None = None
84
+ last_widget_id: str | None = None
85
+ # Propagation (not repr'd)
86
+ stop_propagation: bool = field(default=False, repr=False)
87
+
88
+ def __repr__(self) -> str:
89
+ src = type(self.source).__name__ if self.source is not None else "None"
90
+ parts = [f"event_type={self.event_type!r}", f"source={src}"]
91
+ for fname in ("x", "y", "xdata", "ydata", "button", "key",
92
+ "line_id", "bar_index", "dwell_ms"):
93
+ v = getattr(self, fname)
94
+ if v is not None:
95
+ parts.append(f"{fname}={v!r}")
96
+ if self.modifiers:
97
+ parts.append(f"modifiers={self.modifiers!r}")
98
+ return "Event(" + ", ".join(parts) + ")"
99
+
100
+
101
+ class CallbackRegistry:
102
+ """Per-object handler store.
103
+
104
+ Supports:
105
+ - Priority ordering (``order`` kwarg — lower fires first)
106
+ - Wildcard ``"*"`` type receives every dispatched event
107
+ - ``stop_propagation`` on the event halts remaining handlers
108
+ - ``disconnect_fn(fn, *types)`` removes by callback reference
109
+ - ``pause_events`` / ``hold_events`` context managers (added in Task 3)
110
+ """
111
+
112
+ def __init__(self) -> None:
113
+ # {event_type: [(order, cid, fn), ...]} — sorted by order
114
+ self._handlers: dict[str, list[tuple[float, int, Callable]]] = defaultdict(list)
115
+ self._next_cid: int = 1
116
+ # {cid: set[str]} — which types this cid is registered under
117
+ self._cid_map: dict[int, set[str]] = {}
118
+ # {id(fn): set[int]} — which cids this fn owns
119
+ self._fn_map: dict[int, set[int]] = defaultdict(set)
120
+ # pause/hold state (populated in Task 3)
121
+ self._pause_counts: dict[str, int] = {}
122
+ self._hold_counts: dict[str, int] = {}
123
+ self._held: deque[Event] = deque()
124
+
125
+ # ── registration ─────────────────────────────────────────────────────
126
+
127
+ def connect(self, event_type: str, fn: Callable, *, order: float = 0) -> int:
128
+ """Register fn for event_type. Returns integer CID."""
129
+ if event_type not in VALID_EVENT_TYPES:
130
+ raise ValueError(
131
+ f"Invalid event_type {event_type!r}. "
132
+ f"Valid types: {sorted(t for t in VALID_EVENT_TYPES if t != '*')} or '*'"
133
+ )
134
+ cid = self._next_cid
135
+ self._next_cid += 1
136
+ self._handlers[event_type].append((order, cid, fn))
137
+ self._handlers[event_type].sort(key=lambda t: t[0])
138
+ self._cid_map.setdefault(cid, set()).add(event_type)
139
+ self._fn_map[id(fn)].add(cid)
140
+ return cid
141
+
142
+ def disconnect(self, cid: int) -> None:
143
+ """Remove handler by CID. Silent if not found."""
144
+ types = self._cid_map.pop(cid, set())
145
+ for et in types:
146
+ self._handlers[et] = [
147
+ (o, c, f) for o, c, f in self._handlers[et] if c != cid
148
+ ]
149
+ for fn_cids in self._fn_map.values():
150
+ fn_cids.discard(cid)
151
+
152
+ def disconnect_fn(self, fn: Callable, *types: str) -> None:
153
+ """Remove fn from the given types (all types if none given)."""
154
+ for cid in list(self._fn_map.get(id(fn), set())):
155
+ cid_types = self._cid_map.get(cid, set())
156
+ if not types or cid_types & set(types):
157
+ self.disconnect(cid)
158
+
159
+ # ── dispatch ─────────────────────────────────────────────────────────
160
+
161
+ def fire(self, event: Event) -> None:
162
+ """Dispatch event to matching handlers (respects pause/hold)."""
163
+ et = event.event_type
164
+ if self._pause_counts.get(et, 0) > 0 or self._pause_counts.get("*", 0) > 0:
165
+ return
166
+ if self._hold_counts.get(et, 0) > 0 or self._hold_counts.get("*", 0) > 0:
167
+ self._held.append(event)
168
+ return
169
+ self._dispatch(event)
170
+
171
+ def _dispatch(self, event: Event) -> None:
172
+ et = event.event_type
173
+ specific = list(self._handlers.get(et, []))
174
+ wildcard = list(self._handlers.get("*", []))
175
+ merged = sorted(specific + wildcard, key=lambda t: t[0])
176
+ for _order, _cid, fn in merged:
177
+ if event.stop_propagation:
178
+ break
179
+ fn(event)
180
+
181
+ def _flush(self) -> None:
182
+ while self._held:
183
+ self._dispatch(self._held.popleft())
184
+
185
+ @contextmanager
186
+ def pause_events(self, *types: str):
187
+ """Suppress events of the given types while inside this context.
188
+ All types are paused when called with no arguments.
189
+ Pause wins over hold for the same type."""
190
+ target = types if types else ("*",)
191
+ for t in target:
192
+ self._pause_counts[t] = self._pause_counts.get(t, 0) + 1
193
+ try:
194
+ yield
195
+ finally:
196
+ for t in target:
197
+ self._pause_counts[t] -= 1
198
+ if self._pause_counts[t] == 0:
199
+ del self._pause_counts[t]
200
+
201
+ @contextmanager
202
+ def hold_events(self, *types: str):
203
+ """Buffer events of the given types; flush when the outermost hold exits.
204
+ All types are held when called with no arguments."""
205
+ target = types if types else ("*",)
206
+ for t in target:
207
+ self._hold_counts[t] = self._hold_counts.get(t, 0) + 1
208
+ try:
209
+ yield
210
+ finally:
211
+ for t in target:
212
+ self._hold_counts[t] -= 1
213
+ if self._hold_counts[t] == 0:
214
+ del self._hold_counts[t]
215
+ if not self._hold_counts:
216
+ self._flush()
217
+
218
+ def __bool__(self) -> bool:
219
+ return any(bool(v) for v in self._handlers.values())
220
+
221
+
222
+ class _EventMixin:
223
+ """Mixin for plot classes and widgets.
224
+
225
+ Provides ``add_event_handler`` / ``remove_handler`` / ``pause_events`` /
226
+ ``hold_events``. The host class must set ``self.callbacks = CallbackRegistry()``
227
+ in its ``__init__``.
228
+ """
229
+
230
+ callbacks: CallbackRegistry
231
+
232
+ def add_event_handler(
233
+ self,
234
+ fn_or_type,
235
+ *args,
236
+ order: float = 0,
237
+ ms: int = 300,
238
+ delta: float = 4,
239
+ ):
240
+ """Register an event handler. Works as a direct call or decorator.
241
+
242
+ Direct call::
243
+
244
+ plot.add_event_handler(fn, "pointer_down")
245
+ plot.add_event_handler(fn, "pointer_down", "pointer_up")
246
+
247
+ Decorator::
248
+
249
+ @plot.add_event_handler("pointer_down")
250
+ def handler(event): ...
251
+
252
+ @plot.add_event_handler("pointer_settled", ms=400, delta=5)
253
+ def on_settle(event): ...
254
+
255
+ Parameters
256
+ ----------
257
+ fn_or_type : callable or str
258
+ Handler function (direct call) or first event type string (decorator).
259
+ *args : str
260
+ Remaining event type strings.
261
+ order : float
262
+ Priority. Lower fires first. Default 0.
263
+ ms : int
264
+ ``pointer_settled`` dwell threshold in milliseconds. Default 300.
265
+ Raises ``ValueError`` if provided without ``"pointer_settled"`` in types.
266
+ delta : float
267
+ ``pointer_settled`` pixel radius. Default 4.
268
+ Raises ``ValueError`` if provided without ``"pointer_settled"`` in types.
269
+ """
270
+ if callable(fn_or_type):
271
+ return self._register(fn_or_type, args, order=order, ms=ms, delta=delta)
272
+ else:
273
+ all_types = (fn_or_type,) + args
274
+ def _decorator(fn: Callable) -> Callable:
275
+ self._register(fn, all_types, order=order, ms=ms, delta=delta)
276
+ return fn
277
+ return _decorator
278
+
279
+ def _register(
280
+ self, fn: Callable, types: tuple, *, order: float, ms: int, delta: float
281
+ ) -> Callable:
282
+ has_settled = "pointer_settled" in types
283
+ _ms_changed = ms != 300
284
+ _delta_changed = delta != 4
285
+ if (_ms_changed or _delta_changed) and not has_settled:
286
+ raise ValueError(
287
+ "ms/delta kwargs are only valid when 'pointer_settled' is in the event types"
288
+ )
289
+ for event_type in types:
290
+ self.callbacks.connect(event_type, fn, order=order)
291
+ if has_settled:
292
+ self._configure_pointer_settled(ms, delta)
293
+ fn._event_types = getattr(fn, "_event_types", set()) | set(types)
294
+ return fn
295
+
296
+ def remove_handler(self, cid_or_fn, *types: str) -> None:
297
+ """Remove a registered handler.
298
+
299
+ Parameters
300
+ ----------
301
+ cid_or_fn : int or callable
302
+ CID returned by ``callbacks.connect()`` or the handler function.
303
+ *types : str
304
+ If given, only remove from these types. If omitted, remove from all.
305
+ """
306
+ had_settled = bool(self.callbacks._handlers.get("pointer_settled"))
307
+ if isinstance(cid_or_fn, int):
308
+ self.callbacks.disconnect(cid_or_fn)
309
+ else:
310
+ self.callbacks.disconnect_fn(cid_or_fn, *types)
311
+ if had_settled and not self.callbacks._handlers.get("pointer_settled"):
312
+ self._configure_pointer_settled(0, 0)
313
+
314
+ def _configure_pointer_settled(self, ms: int, delta: float) -> None:
315
+ """Override in plot subclasses to push thresholds to JS."""
316
+ pass
317
+
318
+ @contextmanager
319
+ def pause_events(self, *types: str):
320
+ """Suppress events of the given types (all types if none given)."""
321
+ with self.callbacks.pause_events(*types):
322
+ yield
323
+
324
+ @contextmanager
325
+ def hold_events(self, *types: str):
326
+ """Buffer events of the given types; flush when context exits."""
327
+ with self.callbacks.hold_events(*types):
328
+ yield
anyplotlib/embed.py ADDED
@@ -0,0 +1,194 @@
1
+ """
2
+ embed.py
3
+ ========
4
+
5
+ Use anyplotlib figures **outside Jupyter** — in Electron apps, MDI
6
+ sub-windows, kiosk dashboards, or any plain web page. No kernel, no
7
+ ipywidgets, no anywidget runtime in the page.
8
+
9
+ Three levels of integration
10
+ ---------------------------
11
+
12
+ **1. Static / self-contained (no Python at runtime)** — export a fully
13
+ self-contained HTML page (renderer + data inlined) and load it anywhere a
14
+ browser engine runs, e.g. an Electron ``BrowserWindow`` or ``<webview>``::
15
+
16
+ import anyplotlib as apl
17
+ fig, ax = apl.subplots(1, 1)
18
+ ax.imshow(data)
19
+ fig.save_html("plot.html") # win.loadFile('plot.html')
20
+
21
+ All client-side interactivity (pan, zoom, widgets, markers) works; Python
22
+ callbacks obviously do not.
23
+
24
+ **2. JS-driven (your app owns the data)** — ship ``figure_esm.js`` with your
25
+ app and mount figures from JavaScript using the exported ``mount()``::
26
+
27
+ import { mount } from './figure_esm.js';
28
+ const handle = mount(container, state, { onEvent: ev => ... });
29
+ handle.setPanelState(panelId, newPanelState); // live updates
30
+ handle.resize(w, h); handle.dispose();
31
+
32
+ ``state`` is the JSON dict produced by :func:`figure_state` — generate it
33
+ once from Python (build time, or a one-shot script) or construct it in JS.
34
+ Each ``mount()`` is fully self-contained, so one window can host many
35
+ figures (MDI-style) by mounting into separate containers.
36
+
37
+ **3. Live Python backend** — run Python alongside your app (sidecar process,
38
+ local WebSocket server, …) and keep figures fully interactive with Python
39
+ callbacks via :class:`FigureBridge`, which is transport-agnostic::
40
+
41
+ # Python side (e.g. behind a websocket)
42
+ bridge = FigureBridge(fig, send=lambda key, value: ws.send(
43
+ json.dumps({"key": key, "value": value})))
44
+ ws.on_message = lambda m: bridge.receive(**json.loads(m))
45
+
46
+ // JS side
47
+ const handle = mount(el, snapshot, {
48
+ onSync: (key, value) => ws.send(JSON.stringify({key, value})),
49
+ });
50
+ ws.onmessage = (m) => { const u = JSON.parse(m.data);
51
+ handle.applyUpdate(u.key, u.value); };
52
+
53
+ See ``docs/embedding.rst`` for a complete Electron walkthrough.
54
+ """
55
+
56
+ from __future__ import annotations
57
+
58
+ import pathlib
59
+
60
+ from anyplotlib._repr_utils import build_standalone_html, _widget_state
61
+
62
+ __all__ = ["figure_state", "to_html", "save_html", "esm_path", "FigureBridge"]
63
+
64
+
65
+ def figure_state(fig) -> dict:
66
+ """Return the figure's full serialised state as a plain JSON-safe dict.
67
+
68
+ The dict contains every synced trait — ``layout_json``, ``fig_width``,
69
+ ``fig_height``, ``event_json``, and one ``panel_<id>_json`` entry per
70
+ panel — and is exactly what the JS ``mount(el, state)`` entry point
71
+ expects.
72
+
73
+ Parameters
74
+ ----------
75
+ fig : Figure
76
+
77
+ Returns
78
+ -------
79
+ dict
80
+ """
81
+ # _widget_state also picks up ipywidgets infrastructure traits (layout,
82
+ # tabbable, …) whose values aren't JSON. The renderer only reads scalar
83
+ # traits, so keep exactly those.
84
+ return {k: v for k, v in _widget_state(fig).items()
85
+ if isinstance(v, (str, int, float, bool)) or v is None}
86
+
87
+
88
+ def to_html(fig, *, resizable: bool = True) -> str:
89
+ """Return a fully self-contained HTML page rendering *fig*.
90
+
91
+ The page inlines the renderer and all figure data; it needs no network,
92
+ kernel, or Python at view time. Client-side interactivity (pan, zoom,
93
+ overlay widgets) is preserved.
94
+
95
+ Parameters
96
+ ----------
97
+ fig : Figure
98
+ resizable : bool, optional
99
+ Keep the figure's drag-to-resize handle. Default ``True``.
100
+ """
101
+ return build_standalone_html(fig, resizable=resizable)
102
+
103
+
104
+ def save_html(fig, path, *, resizable: bool = True) -> pathlib.Path:
105
+ """Write :func:`to_html` output to *path* and return it as a ``Path``."""
106
+ p = pathlib.Path(path)
107
+ p.write_text(to_html(fig, resizable=resizable), encoding="utf-8")
108
+ return p
109
+
110
+
111
+ def esm_path() -> pathlib.Path:
112
+ """Return the path to ``figure_esm.js`` for bundling into a JS app.
113
+
114
+ Copy (or import) this file into your Electron / web build; it exports
115
+ ``mount`` and ``createLocalModel`` alongside the anywidget ``render``.
116
+ """
117
+ return pathlib.Path(__file__).parent / "figure_esm.js"
118
+
119
+
120
+ class FigureBridge:
121
+ """Transport-agnostic two-way sync between a live ``Figure`` and a
122
+ remote JS view mounted with ``mount(el, state, {onSync})``.
123
+
124
+ You supply the pipe (WebSocket, Electron IPC via a sidecar, stdio, …);
125
+ the bridge supplies the protocol: plain ``(key, value)`` pairs.
126
+
127
+ Parameters
128
+ ----------
129
+ fig : Figure
130
+ The live figure. All Python-side mutations (``plot.set_data(...)``,
131
+ marker/widget updates, layout changes) are forwarded automatically.
132
+ send : callable(key: str, value) -> None
133
+ Called for every outbound state change. Wire it to your transport.
134
+
135
+ Notes
136
+ -----
137
+ * **Python → JS**: any synced trait change triggers ``send(key, value)``;
138
+ deliver it to ``handle.applyUpdate(key, value)`` in JS.
139
+ * **JS → Python**: deliver each JS ``onSync(key, value)`` message to
140
+ :meth:`receive`. Interaction events (``event_json``) are dispatched to
141
+ the figure's callback registries exactly as in Jupyter, so
142
+ ``@plot.add_event_handler(...)`` handlers fire unchanged.
143
+ * Echo is suppressed in both directions.
144
+ """
145
+
146
+ def __init__(self, fig, send) -> None:
147
+ self._fig = fig
148
+ self._send = send
149
+ self._applying = False
150
+ # names=traitlets.All: also covers panel traits added dynamically
151
+ # after the bridge is created (Figure.add_traits on new panels).
152
+ import traitlets
153
+ fig.observe(self._on_trait_change, names=traitlets.All)
154
+
155
+ # ── outbound (Python → JS) ────────────────────────────────────────────
156
+ def _on_trait_change(self, change) -> None:
157
+ if self._applying:
158
+ return
159
+ name = change["name"]
160
+ trait = self._fig.traits().get(name)
161
+ if trait is None or not trait.metadata.get("sync") or name.startswith("_"):
162
+ return
163
+ self._send(name, change["new"])
164
+
165
+ def snapshot(self) -> dict:
166
+ """Full state dict for the initial ``mount()`` on the JS side."""
167
+ return figure_state(self._fig)
168
+
169
+ # ── inbound (JS → Python) ─────────────────────────────────────────────
170
+ def receive(self, key: str, value) -> None:
171
+ """Apply one inbound ``(key, value)`` message from the JS view.
172
+
173
+ ``event_json`` messages are dispatched to plot/widget callbacks;
174
+ other keys (e.g. a panel's view state after a JS-side 3D rotate)
175
+ are stored on the figure without echoing back.
176
+ """
177
+ if key == "event_json":
178
+ self._fig._dispatch_event(value)
179
+ return
180
+ if not self._fig.has_trait(key):
181
+ return
182
+ self._applying = True
183
+ try:
184
+ setattr(self._fig, key, value)
185
+ finally:
186
+ self._applying = False
187
+
188
+ def close(self) -> None:
189
+ """Stop forwarding (unobserve the figure)."""
190
+ import traitlets
191
+ try:
192
+ self._fig.unobserve(self._on_trait_change, names=traitlets.All)
193
+ except ValueError:
194
+ pass
@@ -0,0 +1,7 @@
1
+ """anyplotlib.figure — Figure widget, grid spec, and subplots factory."""
2
+
3
+ from anyplotlib.figure._figure import Figure
4
+ from anyplotlib.figure._gridspec import GridSpec, SubplotSpec
5
+ from anyplotlib.figure._subplots import subplots
6
+
7
+ __all__ = ["Figure", "GridSpec", "SubplotSpec", "subplots"]