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,390 @@
1
+ """
2
+ sphinx_anywidget/_scraper.py
3
+ =============================
4
+
5
+ Generic Sphinx Gallery image scraper for any ``anywidget.AnyWidget`` subclass.
6
+
7
+ Drop-in replacement for the anyplotlib-specific ``_sg_html_scraper.ViewerScraper``.
8
+ Works with **any** library built on anywidget — just add the scraper to your
9
+ ``sphinx_gallery_conf["image_scrapers"]``.
10
+
11
+ Interactive tagging
12
+ -------------------
13
+ If a code block's last expression line contains a ``# Interactive`` comment
14
+ (case-insensitive), the scraper:
15
+
16
+ * embeds the full example Python source in a ``<script type="text/x-python">``
17
+ tag so the Pyodide bridge can re-run it live;
18
+ * adds an **⚡ activation badge** to the figure iframe wrapper.
19
+
20
+ Example::
21
+
22
+ fig, ax = vw.subplots(1, 1, figsize=(640, 400))
23
+ ax.imshow(data)
24
+ fig # Interactive
25
+
26
+ Without the comment the figure is rendered as a plain static iframe with
27
+ no Pyodide wiring.
28
+
29
+ Usage in ``conf.py``::
30
+
31
+ from anyplotlib.sphinx_anywidget import AnywidgetScraper
32
+
33
+ sphinx_gallery_conf = {
34
+ "image_scrapers": (AnywidgetScraper(), "matplotlib"),
35
+ ...
36
+ }
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import json as _json
42
+ import re
43
+ import tempfile
44
+ from html import escape as _html_escape
45
+ from pathlib import Path
46
+ from uuid import uuid4
47
+
48
+ # Maximum iframe width (px) that fits inside the pydata-sphinx-theme column.
49
+ MAX_DOC_WIDTH = 684
50
+
51
+ # Sentinel that marks a code block as interactive.
52
+ _INTERACTIVE_RE = re.compile(r"#\s*interactive\s*$", re.IGNORECASE | re.MULTILINE)
53
+
54
+ # Pattern that extracts _PYODIDE_PACKAGES = [...] declarations from source.
55
+ _PYODIDE_PACKAGES_RE = re.compile(
56
+ r"^_PYODIDE_PACKAGES\s*=\s*(\[[^\]]*\])", re.MULTILINE
57
+ )
58
+
59
+ # Pattern that extracts _PYODIDE_MOCK_PACKAGES = [...] declarations from source.
60
+ # These are passed to micropip.add_mock_package() before the wheel install so
61
+ # that packages unavailable in Pyodide (e.g. dask, rosettasciio) are silently
62
+ # skipped during dependency resolution.
63
+ _PYODIDE_MOCK_PACKAGES_RE = re.compile(
64
+ r"^_PYODIDE_MOCK_PACKAGES\s*=\s*(\[[^\]]*\])", re.MULTILINE
65
+ )
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Helpers
70
+ # ---------------------------------------------------------------------------
71
+
72
+ def _find_widget(globals_dict: dict):
73
+ """Return the most-recently assigned anywidget in *globals_dict*, or None.
74
+
75
+ Accepts any object that:
76
+ * has a ``_repr_html_`` callable, and
77
+ * belongs to a class that either inherits from ``anywidget.AnyWidget``
78
+ (identified by the ``_esm`` attribute) or whose module starts with
79
+ ``anywidget``.
80
+ """
81
+ for val in reversed(list(globals_dict.values())):
82
+ if not callable(getattr(val, "_repr_html_", None)):
83
+ continue
84
+ # Check for anywidget fingerprint: _esm attribute
85
+ if hasattr(val, "_esm") and hasattr(val, "traits"):
86
+ return val
87
+ # Fallback: module check
88
+ module = getattr(type(val), "__module__", "") or ""
89
+ if "widget" in module.lower():
90
+ return val
91
+ return None
92
+
93
+
94
+ def _make_thumbnail_png(widget) -> bytes:
95
+ """Render *widget* in headless Chromium and return a dark-theme PNG screenshot."""
96
+ from anyplotlib.sphinx_anywidget._repr_utils import build_standalone_html
97
+
98
+ html = build_standalone_html(widget, resizable=False)
99
+ html = html.replace(
100
+ "renderFn({ model, el });",
101
+ "renderFn({ model, el }); window._aplReady = true;",
102
+ )
103
+ html = html.replace("background: transparent;", "background: #1e1e2e;")
104
+
105
+ with tempfile.NamedTemporaryFile(
106
+ suffix=".html", mode="w", encoding="utf-8", delete=False
107
+ ) as fh:
108
+ fh.write(html)
109
+ tmp_path = Path(fh.name)
110
+
111
+ def _run_playwright(tmp_path: Path) -> bytes:
112
+ from playwright.sync_api import sync_playwright
113
+
114
+ with sync_playwright() as pw:
115
+ browser = pw.chromium.launch(
116
+ headless=True, args=["--no-sandbox", "--disable-setuid-sandbox"]
117
+ )
118
+ try:
119
+ page = browser.new_page()
120
+ page.emulate_media(color_scheme="dark")
121
+ page.goto(tmp_path.as_uri())
122
+ page.wait_for_function(
123
+ "() => window._aplReady === true", timeout=15_000
124
+ )
125
+ page.evaluate(
126
+ "() => new Promise(r =>"
127
+ " requestAnimationFrame(() => requestAnimationFrame(r)))"
128
+ )
129
+ return page.locator("#widget-root").screenshot()
130
+ finally:
131
+ page.close()
132
+ browser.close()
133
+
134
+ try:
135
+ import asyncio
136
+ import concurrent.futures
137
+
138
+ try:
139
+ loop = asyncio.get_running_loop()
140
+ except RuntimeError:
141
+ loop = None
142
+
143
+ if loop is not None and loop.is_running():
144
+ # Playwright sync API cannot be used inside a running asyncio loop.
145
+ # Run it in a separate thread where there is no event loop.
146
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
147
+ future = pool.submit(_run_playwright, tmp_path)
148
+ png_bytes = future.result()
149
+ else:
150
+ png_bytes = _run_playwright(tmp_path)
151
+ finally:
152
+ tmp_path.unlink(missing_ok=True)
153
+
154
+ return png_bytes
155
+
156
+
157
+ def _iframe_html(
158
+ src: str,
159
+ w: int,
160
+ h: int,
161
+ fig_id: str | None = None,
162
+ interactive: bool = False,
163
+ max_width: int | None = None,
164
+ max_height: int | None = None,
165
+ ) -> str:
166
+ """Return a single-line HTML snippet embedding *src* responsively.
167
+
168
+ Parameters
169
+ ----------
170
+ src : str
171
+ Relative URL to the standalone widget HTML file.
172
+ w, h : int
173
+ Native pixel dimensions of the widget.
174
+ fig_id : str or None
175
+ Stable identifier; used as the ``data-awi-fig`` attribute.
176
+ interactive : bool
177
+ When True, renders the ⚡ activation badge.
178
+ max_width : int or None
179
+ Override the default ``MAX_DOC_WIDTH`` cap (pixels).
180
+ max_height : int or None
181
+ Maximum display height in pixels. When provided, the scale factor is
182
+ also constrained so the rendered iframe never exceeds this height.
183
+ """
184
+ uid = fig_id or f"f{uuid4().hex[:8]}"
185
+ cap = max_width if max_width is not None else MAX_DOC_WIDTH
186
+
187
+ init_scale = min(1.0, cap / w)
188
+ if max_height is not None and h > 0:
189
+ init_scale = min(init_scale, max_height / h)
190
+ init_w = round(w * init_scale)
191
+ init_h = round(h * init_scale)
192
+ scale_css = f"{init_scale:.6f}".rstrip("0").rstrip(".")
193
+
194
+ # JS: re-scale on resize
195
+ resize_js = (
196
+ f"(function(){{"
197
+ f"var wrap=document.getElementById('{uid}'),"
198
+ f"ifr=wrap.querySelector('iframe'),"
199
+ f"nw={w},nh={h};"
200
+ f"function r(){{"
201
+ f"var avail=wrap.parentElement?wrap.parentElement.offsetWidth:0;"
202
+ f"if(!avail)return;"
203
+ f"var s=Math.min(1,avail/nw);"
204
+ f"wrap.style.width=Math.round(nw*s)+'px';"
205
+ f"wrap.style.height=Math.round(nh*s)+'px';"
206
+ f"ifr.style.transform='scale('+s+')';"
207
+ f"}}"
208
+ f"requestAnimationFrame(r);window.addEventListener('resize',r);"
209
+ f"}})()"
210
+ )
211
+
212
+ # Badge HTML — only the ⚡ button when interactive; nothing otherwise.
213
+ badge_parts = []
214
+ if interactive:
215
+ badge_parts.append(
216
+ f'<button class="awi-badge-icon awi-activate-btn" '
217
+ f'data-awi-fig="{uid}" '
218
+ f'title="Make interactive (boots Pyodide — may take ~10 s)">&#x26A1;</button>'
219
+ )
220
+ if not badge_parts:
221
+ badge_html = ""
222
+ else:
223
+ badge_html = (
224
+ f'<div class="awi-badge" data-awi-badge="{uid}">'
225
+ + "".join(badge_parts)
226
+ + "</div>"
227
+ )
228
+
229
+ return (
230
+ f'<div style="display:block;text-align:center;line-height:0;margin:12px 0;">'
231
+ f'<div id="{uid}" class="awi-fig-wrap" data-awi-fig="{uid}" '
232
+ f'style="display:inline-block;overflow:hidden;'
233
+ f'position:relative;width:{init_w}px;height:{init_h}px;">'
234
+ f'<iframe src="{src}" data-awi-fig="{uid}" frameborder="0" scrolling="no" '
235
+ f'style="width:{w}px;height:{h}px;border:none;overflow:hidden;display:block;'
236
+ f'transform-origin:top left;transform:scale({scale_css});'
237
+ f'position:absolute;top:0;left:0;">'
238
+ f'</iframe>'
239
+ f'{badge_html}'
240
+ f'</div>'
241
+ f'<script>{resize_js}</script>'
242
+ f'</div>'
243
+ )
244
+
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Scraper
248
+ # ---------------------------------------------------------------------------
249
+
250
+ class AnywidgetScraper:
251
+ """Sphinx Gallery image scraper for any ``anywidget.AnyWidget`` subclass.
252
+ """
253
+
254
+ def __init__(self):
255
+ # Maps src_file → list of fig_ids emitted so far (creation order).
256
+ self._example_figs: dict = {}
257
+
258
+ def __repr__(self) -> str:
259
+ return "AnywidgetScraper()"
260
+
261
+ def __call__(self, block, block_vars, gallery_conf):
262
+ globals_dict = block_vars.get("example_globals", {})
263
+ widget = _find_widget(globals_dict)
264
+ if widget is None:
265
+ return ""
266
+
267
+ src_file = str(block_vars.get("src_file", ""))
268
+
269
+ # ── detect # Interactive tag ──────────────────────────────────────
270
+ block_source = block[1] if isinstance(block, (list, tuple)) else ""
271
+ is_interactive = bool(_INTERACTIVE_RE.search(block_source))
272
+
273
+ # ── assign a stable fig_id and fig_index ─────────────────────────
274
+ if src_file not in self._example_figs:
275
+ self._example_figs[src_file] = []
276
+ fig_index = len(self._example_figs[src_file])
277
+
278
+ # ── 1. Write the thumbnail PNG ────────────────────────────────────
279
+ image_path_iterator = block_vars["image_path_iterator"]
280
+ png_path = Path(next(image_path_iterator))
281
+ png_path.parent.mkdir(parents=True, exist_ok=True)
282
+ png_path.write_bytes(_make_thumbnail_png(widget))
283
+
284
+ fig_id = png_path.stem # stable, unique stem from Sphinx Gallery
285
+ self._example_figs[src_file].append(fig_id)
286
+
287
+ # ── 2. Write the standalone HTML ──────────────────────────────────
288
+ try:
289
+ from anyplotlib.sphinx_anywidget._repr_utils import (
290
+ build_standalone_html, _widget_px,
291
+ )
292
+ docs_dir = Path(gallery_conf["src_dir"])
293
+ widgets_dir = docs_dir / "_static" / "viewer_widgets"
294
+ widgets_dir.mkdir(parents=True, exist_ok=True)
295
+
296
+ html_name = png_path.stem + ".html"
297
+ html_path = widgets_dir / html_name
298
+
299
+ inner_html = build_standalone_html(widget, resizable=False, fig_id=fig_id)
300
+ html_path.write_text(inner_html, encoding="utf-8")
301
+ w, h = _widget_px(widget)
302
+ have_html = True
303
+ except Exception as exc:
304
+ print(f"[sphinx_anywidget] WARNING: could not write iframe HTML: {exc}")
305
+ have_html = False
306
+
307
+ # ── 3. Return rST ─────────────────────────────────────────────────
308
+ if have_html:
309
+ try:
310
+ src_dir = Path(gallery_conf["src_dir"])
311
+ page_dir = png_path.parent.parent # strip /images
312
+ rel_parts = page_dir.relative_to(src_dir).parts
313
+ depth = len(rel_parts)
314
+ except Exception:
315
+ depth = 1
316
+ prefix = "../" * depth
317
+ src = f"{prefix}_static/viewer_widgets/{html_name}"
318
+
319
+ iframe_block = _iframe_html(
320
+ src, w, h,
321
+ fig_id=fig_id,
322
+ interactive=is_interactive,
323
+ )
324
+
325
+ rst = "\n\n.. raw:: html\n\n " + iframe_block + "\n\n"
326
+
327
+ if is_interactive:
328
+ # Embed the example Python source so the Pyodide bridge can
329
+ # re-execute it and wire live callbacks.
330
+ python_src = ""
331
+ try:
332
+ python_src = Path(src_file).read_text(encoding="utf-8")
333
+ except Exception:
334
+ pass
335
+
336
+ if python_src:
337
+ data_src = _html_escape(_json.dumps(python_src), quote=True)
338
+
339
+ # Detect _PYODIDE_PACKAGES = [...] in the source.
340
+ _pkg_attr = ""
341
+ m = _PYODIDE_PACKAGES_RE.search(python_src)
342
+ if m:
343
+ try:
344
+ import ast as _ast
345
+ pkgs = _ast.literal_eval(m.group(1))
346
+ if pkgs:
347
+ _pkg_attr = (
348
+ f' data-pyodide-packages='
349
+ f'"{_html_escape(_json.dumps(pkgs), quote=True)}"'
350
+ )
351
+ except Exception:
352
+ pass
353
+
354
+ # Detect _PYODIDE_MOCK_PACKAGES = [...] in the source.
355
+ _mock_attr = ""
356
+ m2 = _PYODIDE_MOCK_PACKAGES_RE.search(python_src)
357
+ if m2:
358
+ try:
359
+ import ast as _ast2
360
+ mock_pkgs = _ast2.literal_eval(m2.group(1))
361
+ if mock_pkgs:
362
+ _mock_attr = (
363
+ f' data-pyodide-mock-packages='
364
+ f'"{_html_escape(_json.dumps(mock_pkgs), quote=True)}"'
365
+ )
366
+ except Exception:
367
+ pass
368
+
369
+ python_block = (
370
+ f'<script type="text/x-python"'
371
+ f' data-fig-id="{fig_id}"'
372
+ f' data-fig-index="{fig_index}"'
373
+ f' data-src-file="{Path(src_file).stem}"'
374
+ f'{_pkg_attr}'
375
+ f'{_mock_attr}'
376
+ f' data-src="{data_src}"></script>'
377
+ )
378
+ rst += "\n\n.. raw:: html\n\n " + python_block + "\n\n"
379
+
380
+ return rst
381
+ else:
382
+ return (
383
+ f"\n\n.. image:: {png_path.name}\n"
384
+ f" :width: 100%\n\n"
385
+ )
386
+
387
+
388
+ # Back-compat alias used by the existing anyplotlib docs.
389
+ ViewerScraper = AnywidgetScraper
390
+
@@ -0,0 +1,84 @@
1
+ """
2
+ sphinx_anywidget/_wheel_builder.py
3
+ ====================================
4
+
5
+ Builds a project wheel at docs-build time so the Pyodide bridge can install
6
+ the exact library version that generated the docs — no PyPI release required.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+
16
+ def build_wheel(
17
+ static_dir: Path,
18
+ package_name: str,
19
+ project_root: Path,
20
+ ) -> "Path | None":
21
+ """Build a pure-Python wheel into *static_dir/wheels/*.
22
+
23
+ The wheel is renamed to ``{package_name}-0.0.0-py3-none-any.whl``.
24
+ ``0.0.0`` is a valid PEP 440 sentinel micropip accepts for URL installs.
25
+
26
+ Parameters
27
+ ----------
28
+ static_dir :
29
+ The docs ``_static`` directory; a ``wheels/`` sub-dir is created.
30
+ package_name :
31
+ PyPI / importable name (e.g. ``"anyplotlib"``).
32
+ project_root :
33
+ Directory containing ``pyproject.toml`` / ``setup.py``.
34
+
35
+ Returns
36
+ -------
37
+ Path or None
38
+ Path to the written wheel, or *None* on failure.
39
+ """
40
+ wheels_dir = static_dir / "wheels"
41
+ wheels_dir.mkdir(parents=True, exist_ok=True)
42
+
43
+ # PEP 427 normalises distribution names: hyphens and dots → underscores.
44
+ normalised = re.sub(r"[-.]", "_", package_name)
45
+
46
+ stable = wheels_dir / f"{normalised}-0.0.0-py3-none-any.whl"
47
+
48
+ # Build into a temporary sub-directory so we never clobber the existing
49
+ # stable wheel until we know the new build actually succeeded.
50
+ import tempfile
51
+ with tempfile.TemporaryDirectory(dir=wheels_dir) as tmp_str:
52
+ tmp_dir = Path(tmp_str)
53
+ result = subprocess.run(
54
+ [
55
+ "uv", "build", "--wheel",
56
+ "--out-dir", str(tmp_dir),
57
+ str(project_root),
58
+ ],
59
+ capture_output=True,
60
+ text=True,
61
+ )
62
+
63
+ if result.returncode != 0:
64
+ print(
65
+ f"\n[sphinx_anywidget] WARNING: wheel build failed "
66
+ f"for {package_name!r}:\n{result.stderr}"
67
+ )
68
+ return None
69
+
70
+ new_wheels = sorted(tmp_dir.glob(f"{normalised}*.whl"))
71
+ if not new_wheels:
72
+ print(f"\n[sphinx_anywidget] WARNING: no wheel found for {package_name!r}")
73
+ return None
74
+
75
+ # Build succeeded — now replace the stable wheel atomically.
76
+ stable.unlink(missing_ok=True)
77
+ # Remove any other stale versioned wheels before moving the new one.
78
+ for old in wheels_dir.glob(f"{normalised}*.whl"):
79
+ old.unlink(missing_ok=True)
80
+ new_wheels[-1].rename(stable)
81
+ # ASCII only: Windows consoles (cp1252) can't print '→' during builds
82
+ print(f"[sphinx_anywidget] wheel -> {stable}")
83
+ return stable
84
+