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,1099 @@
1
+ /**
2
+ * anywidget_bridge.js
3
+ *
4
+ * Generic Pyodide bridge for anywidget-based interactive documentation.
5
+ *
6
+ * Architecture
7
+ * ────────────
8
+ * Parent page (this script)
9
+ * ├─ Per-figure ⚡ badge (in .awi-badge div, rendered by _scraper.py)
10
+ * ├─ Pyodide WASM runtime (loaded once from CDN on first ⚡ click)
11
+ * ├─ Package wheel at _static/wheels/{pkg}-0.0.0-py3-none-any.whl
12
+ * ├─ <script type="text/x-python" data-fig-id="…"> — example source
13
+ * └─ <iframe data-awi-fig="…" src="…_static/viewer_widgets/….html">
14
+ * anywidget ESM renderer + postMessage listener
15
+ *
16
+ * Data flow after activation
17
+ * ──────────────────────────
18
+ * Python setattr(widget, key, val) [any sync=True traitlet]
19
+ * → traitlets fires observe() on monkey-patched AnyWidget.__init__ observer
20
+ * → observer calls js.window._anywidgetPush(fig_id, key, val_str)
21
+ * → iframe.contentWindow.postMessage({type:'awi_state', key, value})
22
+ * → model.set(key, val) + save_changes() → JS re-renders
23
+ *
24
+ * user interaction → iframe save_changes() forwards awi_event to parent
25
+ * → window 'message' listener → pyodide dispatch
26
+ * → if hasattr(widget, '_dispatch_event'): widget._dispatch_event(data)
27
+ * → Python on_release / on_change callbacks
28
+ *
29
+ * Monkey-patch (installed in Pyodide after anywidget stub is ready)
30
+ * ─────────────────────────────────────────────────────────────────
31
+ * _aw.AnyWidget.__init__ is wrapped to add a traitlets.observe(names=All)
32
+ * observer to every new widget instance. The observer fires for every trait
33
+ * change; it filters to sync=True traits and calls _anywidgetPush when
34
+ * _anywidget_fig_id is set on the instance. Dynamic traits added via
35
+ * add_traits() are automatically covered by the All sentinel.
36
+ *
37
+ * Wheel URL resolution
38
+ * ────────────────────
39
+ * The wheel lives at _static/wheels/{pkg}-0.0.0-py3-none-any.whl relative
40
+ * to the docs root. _DOCS_ROOT is derived from document.currentScript.src
41
+ * so the URL is always version-correct for every deployed copy of the docs.
42
+ */
43
+
44
+ (function () {
45
+ 'use strict';
46
+
47
+ const PYODIDE_VERSION = '0.27.5';
48
+ const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
49
+
50
+ // Derives the docs root from this script's own URL so wheel references are
51
+ // always version-specific (dev vs v0.1.0, etc.)
52
+ const _SCRIPT_SRC = (document.currentScript || {}).src || '';
53
+ const _DOCS_ROOT = _SCRIPT_SRC.replace(/_static\/anywidget_bridge\.js.*$/, '') || './';
54
+
55
+ // One shared Pyodide instance for the whole page, resolved once on first boot.
56
+ let _pyodidePromise = null;
57
+
58
+ // ── helpers ──────────────────────────────────────────────────────────────
59
+
60
+ /** True when the page has at least one interactive anywidget figure. */
61
+ function _hasInteractiveFigures() {
62
+ return document.querySelector('button.awi-activate-btn') !== null;
63
+ }
64
+
65
+ /** Deliver a state-update message into the iframe for figId.
66
+ *
67
+ * rawValue is always a JSON-encoded string (Python serialises every trait
68
+ * with json.dumps so numeric/boolean/object traits are not type-erased when
69
+ * the iframe receives them).
70
+ */
71
+ function _postToIframe(figId, key, rawValue) {
72
+ const iframe = document.querySelector(`iframe[data-awi-fig="${figId}"]`);
73
+ if (iframe && iframe.contentWindow) {
74
+ // JSON.parse recovers the real JS type (number, bool, array, object …).
75
+ // Plain Python strings are also JSON-encoded (quoted), so they round-trip
76
+ // correctly too.
77
+ let value = rawValue;
78
+ try { value = JSON.parse(rawValue); } catch (_) {}
79
+ iframe.contentWindow.postMessage({ type: 'awi_state', key, value }, '*');
80
+ }
81
+ }
82
+
83
+ // ── badge state management ────────────────────────────────────────────────
84
+
85
+ function _allActivateBtns() {
86
+ return Array.from(document.querySelectorAll('button.awi-activate-btn'));
87
+ }
88
+
89
+ function _setBadgesLoading() {
90
+ for (const btn of _allActivateBtns()) {
91
+ btn.textContent = '⏳';
92
+ btn.dataset.state = 'loading';
93
+ btn.title = 'Loading Pyodide…';
94
+ }
95
+ }
96
+
97
+ function _setBadgesActive() {
98
+ for (const btn of _allActivateBtns()) {
99
+ btn.textContent = '✓';
100
+ btn.dataset.state = 'active';
101
+ btn.title = 'Python active';
102
+ const wrap = btn.closest('.awi-fig-wrap');
103
+ if (wrap) wrap.dataset.awiLive = 'true';
104
+ }
105
+ }
106
+
107
+ function _setBadgesError(err) {
108
+ const msg = String(err.message || err);
109
+ const lines = msg.split('\n').map(l => l.trim()).filter(Boolean);
110
+ const summary = lines.length > 1 ? lines[lines.length - 1] : (lines[0] || msg);
111
+ for (const btn of _allActivateBtns()) {
112
+ btn.textContent = '❌';
113
+ btn.dataset.state = 'error';
114
+ btn.title = `Boot failed: ${summary}\n\n${msg}`;
115
+ }
116
+ }
117
+
118
+ // ── Pyodide bootstrap ─────────────────────────────────────────────────────
119
+
120
+ function _boot() {
121
+ if (_pyodidePromise) return _pyodidePromise;
122
+ _pyodidePromise = _doBoot().catch(err => {
123
+ _pyodidePromise = null; // allow retry
124
+ throw err;
125
+ });
126
+ return _pyodidePromise;
127
+ }
128
+
129
+ async function _doBoot() {
130
+ // ── file:// guard ─────────────────────────────────────────────────────
131
+ if (window.location.protocol === 'file:') {
132
+ throw new Error(
133
+ 'Serve docs over HTTP — run: python -m http.server -d build/html 8080'
134
+ );
135
+ }
136
+
137
+ function _step(label, promise) {
138
+ return promise.catch(e => {
139
+ throw new Error('[' + label + '] ' + (e.message || String(e)));
140
+ });
141
+ }
142
+
143
+ // 1. Load pyodide.js from CDN
144
+ if (typeof loadPyodide === 'undefined') {
145
+ await _step('load pyodide.js', new Promise((resolve, reject) => {
146
+ const s = document.createElement('script');
147
+ s.src = PYODIDE_CDN + 'pyodide.js';
148
+ s.onload = resolve;
149
+ s.onerror = () => reject(new Error('Failed to load pyodide.js from CDN'));
150
+ document.head.appendChild(s);
151
+ }));
152
+ }
153
+ console.info('[sphinx_anywidget] pyodide.js ready');
154
+
155
+ // 2. Initialise Pyodide
156
+ const pyodide = await _step('init pyodide',
157
+ loadPyodide({ indexURL: PYODIDE_CDN }));
158
+ console.info('[sphinx_anywidget] Pyodide initialised');
159
+
160
+ // 3. Load micropip + numpy
161
+ await _step('load micropip+numpy',
162
+ pyodide.loadPackage(['micropip', 'numpy']));
163
+ console.info('[sphinx_anywidget] micropip + numpy loaded');
164
+
165
+ // 4. Install pure-Python deps
166
+ await _step('install traitlets/colorcet',
167
+ pyodide.runPythonAsync(`
168
+ import micropip
169
+ await micropip.install(['traitlets', 'colorcet', 'toolz'])
170
+ `));
171
+ console.info('[sphinx_anywidget] traitlets + colorcet installed');
172
+
173
+ // 5. Stub anywidget in sys.modules
174
+ await _step('stub anywidget',
175
+ pyodide.runPythonAsync(`
176
+ import sys, traitlets as _tr
177
+
178
+ class _AnyWidget(_tr.HasTraits):
179
+ _esm = _tr.Unicode("").tag(sync=False)
180
+ _css = _tr.Unicode("").tag(sync=False)
181
+ def __init__(self, **kw):
182
+ super().__init__(**kw)
183
+
184
+ class _AnyWidgetMod:
185
+ AnyWidget = _AnyWidget
186
+
187
+ sys.modules['anywidget'] = _AnyWidgetMod()
188
+ _AWI_REGISTRY = {} # fig_id → widget instance
189
+
190
+ # Pre-compiled interaction-event dispatcher. The JS message handler calls
191
+ # this proxy DIRECTLY per frame instead of pyodide.runPythonAsync(code-string)
192
+ # — recompiling a code string every event costs ~1.2 ms in WASM (vs ~0.01 ms
193
+ # to call a ready function), which is the dominant per-frame cost of the
194
+ # Pyodide interaction path on a drag (30-60 events/sec).
195
+ def _awi_dispatch(fig_id, data):
196
+ _w = _AWI_REGISTRY.get(fig_id)
197
+ if _w is not None and hasattr(_w, '_dispatch_event'):
198
+ _w._dispatch_event(data)
199
+ `));
200
+ console.info('[sphinx_anywidget] anywidget stub installed');
201
+
202
+ // 5.5 Stub traits in sys.modules using traitlets as the backend.
203
+ // traits (from Enthought) has a C extension (ctraits) so it can't be
204
+ // installed via micropip in Pyodide. HyperSpy uses traits for HasTraits,
205
+ // trait types (CBool, Str, Enum, …) and Undefined — all of which have
206
+ // close equivalents in traitlets (the IPython fork of traits). We
207
+ // install shim modules for traits, traits.api, traits.trait_errors, and
208
+ // traits.trait_numeric before the HyperSpy wheel is installed so that
209
+ // micropip's dep-resolution never even sees traits as a requirement.
210
+ await _step('stub traits',
211
+ pyodide.runPythonAsync(`
212
+ import sys
213
+ import traitlets as _tr
214
+
215
+ # ── Patch traits-only methods onto traitlets.HasTraits ────────────────
216
+ # traits.HasTraits has trait_get() / trait_set() / trait_names() etc.
217
+ # that traitlets dropped or renamed. Patch them in so HyperSpy modules
218
+ # that call e.g. obj.trait_get() work transparently.
219
+ #
220
+ # Also patch __init__ to auto-register _{name}_changed methods as
221
+ # traitlets observers (traits magic that traitlets doesn't replicate).
222
+ _orig_HasTraits_init = _tr.HasTraits.__init__
223
+
224
+ def _patched_HasTraits_init(self, *args, **kwargs):
225
+ # Call the original traitlets init first so that the object is fully
226
+ # initialised before we wire any trait-change observers. traits wires
227
+ # _{name}_changed observers only after __init__ completes; traitlets
228
+ # would fire them mid-init, so we defer registration to after init.
229
+ _orig_HasTraits_init(self, *args, **kwargs)
230
+ # Now register _{name}_changed auto-observers.
231
+ cls = type(self)
232
+ import inspect as _inspect
233
+ for attr_name in dir(cls):
234
+ if not attr_name.endswith('_changed'):
235
+ continue
236
+ inner = attr_name[1:-8]
237
+ if not inner:
238
+ continue
239
+ if inner in cls.class_traits():
240
+ method = getattr(cls, attr_name)
241
+ if callable(method) and not isinstance(method, property):
242
+ try:
243
+ sig = _inspect.signature(method)
244
+ # Exclude 'self' from count
245
+ nparams = len([p for p in sig.parameters.values()
246
+ if p.name != 'self'
247
+ and p.default is _inspect.Parameter.empty
248
+ and p.kind not in (_inspect.Parameter.VAR_POSITIONAL,
249
+ _inspect.Parameter.VAR_KEYWORD)])
250
+ except (ValueError, TypeError):
251
+ nparams = 3
252
+ def _make_bridge(m, np):
253
+ def _bridge(change):
254
+ if np == 0:
255
+ m(self)
256
+ elif np == 1:
257
+ m(self, change['new'])
258
+ elif np == 2:
259
+ m(self, change['name'], change['new'])
260
+ else:
261
+ m(self, change['name'], change['old'], change['new'])
262
+ return _bridge
263
+ self.observe(_make_bridge(method, nparams), names=[inner])
264
+
265
+ _tr.HasTraits.__init__ = _patched_HasTraits_init
266
+
267
+ def _trait_get(self, *names):
268
+ """Return a dict of trait name → current value (traits API)."""
269
+ if names:
270
+ return {n: getattr(self, n) for n in names if n in self.traits()}
271
+ return {n: getattr(self, n) for n in self.traits()}
272
+
273
+ def _trait_set(self, trait_change_notify=True, **kw):
274
+ """Set multiple traits at once (traits API). Silently skip coercion
275
+ failures — traits does auto-coerce (CFloat accepts '7.5') but traitlets
276
+ does not; skip rather than crash for preference loading."""
277
+ for k, v in kw.items():
278
+ if k not in self.traits():
279
+ continue
280
+ # Try the value as-is; if validation fails try common coercions.
281
+ for candidate in (v, _coerce_trait_value(self.traits()[k], v)):
282
+ try:
283
+ setattr(self, k, candidate)
284
+ break
285
+ except Exception:
286
+ pass
287
+
288
+ def _coerce_trait_value(trait, v):
289
+ """Best-effort coercion of a string config value to the trait's type."""
290
+ import traitlets as _tr2
291
+ if isinstance(v, str):
292
+ if isinstance(trait, _tr2.Bool):
293
+ return v.lower() not in ('false', '0', 'no', '')
294
+ if isinstance(trait, (_tr2.Int,)):
295
+ try: return int(v)
296
+ except: pass
297
+ if isinstance(trait, (_tr2.Float,)):
298
+ try: return float(v)
299
+ except: pass
300
+ return v
301
+
302
+ def _trait_names(self):
303
+ return list(self.traits().keys())
304
+
305
+ def _get(self, *names):
306
+ return _trait_get(self, *names)
307
+
308
+ def _on_trait_change(self, handler, trait_name, remove=False):
309
+ """Register or remove a traits-style observer.
310
+
311
+ Handles dotted paths like "_axes.slice" (traits list-item observation):
312
+ watches each item in the list trait for changes to the child attribute,
313
+ and re-wires when the list itself changes.
314
+ """
315
+ import inspect
316
+ try:
317
+ sig = inspect.signature(handler)
318
+ nparams = len([p for p in sig.parameters.values()
319
+ if p.default is inspect.Parameter.empty
320
+ and p.kind not in (inspect.Parameter.VAR_POSITIONAL,
321
+ inspect.Parameter.VAR_KEYWORD)])
322
+ except (ValueError, TypeError):
323
+ nparams = 3
324
+
325
+ def _call_handler(change):
326
+ if nparams == 0:
327
+ handler()
328
+ elif nparams == 1:
329
+ handler(change['new'])
330
+ elif nparams == 2:
331
+ handler(change['name'], change['new'])
332
+ else:
333
+ handler(change['name'], change['old'], change['new'])
334
+
335
+ if not hasattr(self, '_oti_bridges'):
336
+ self._oti_bridges = {}
337
+ if not hasattr(self, '_oti_item_bridges'):
338
+ self._oti_item_bridges = {}
339
+
340
+ if '.' in trait_name:
341
+ # dotted path: "list_trait.child_attr"
342
+ list_trait, child_attr = trait_name.split('.', 1)
343
+
344
+ key = (id(handler), trait_name)
345
+ if remove:
346
+ # unobserve child bridges already registered
347
+ for item_bridge, item in self._oti_item_bridges.pop(key, []):
348
+ try:
349
+ item.unobserve(item_bridge, names=[child_attr])
350
+ except Exception:
351
+ pass
352
+ list_bridge = self._oti_bridges.pop(key, None)
353
+ if list_bridge:
354
+ try:
355
+ self.unobserve(list_bridge, names=[list_trait])
356
+ except Exception:
357
+ pass
358
+ return
359
+
360
+ # Track per-item bridges so we can remove them
361
+ self._oti_item_bridges[key] = []
362
+
363
+ def _observe_item(item):
364
+ def _item_bridge(change):
365
+ _call_handler(change)
366
+ _item_bridge.__name__ = getattr(handler, '__name__', 'handler') + '_item'
367
+ item.observe(_item_bridge, names=[child_attr])
368
+ self._oti_item_bridges[key].append((_item_bridge, item))
369
+
370
+ # Observe existing items now
371
+ existing = getattr(self, list_trait, None)
372
+ if existing:
373
+ for item in existing:
374
+ if hasattr(item, 'observe'):
375
+ _observe_item(item)
376
+
377
+ # Watch the list for new items being added
378
+ def _list_changed(change):
379
+ new_list = change.get('new', []) or []
380
+ old_list = change.get('old', []) or []
381
+ added = [x for x in new_list if x not in old_list]
382
+ for item in added:
383
+ if hasattr(item, 'observe'):
384
+ _observe_item(item)
385
+ # Also call handler when list changes (slice assignment etc.)
386
+ _call_handler(change)
387
+
388
+ _list_changed.__name__ = getattr(handler, '__name__', 'handler') + '_list'
389
+ self.observe(_list_changed, names=[list_trait])
390
+ self._oti_bridges[key] = _list_changed
391
+ else:
392
+ # simple trait name
393
+ def _bridge(change):
394
+ _call_handler(change)
395
+ _bridge.__name__ = getattr(handler, '__name__', 'handler')
396
+ key = (id(handler), trait_name)
397
+ if not remove:
398
+ self.observe(_bridge, names=[trait_name])
399
+ self._oti_bridges[key] = _bridge
400
+ else:
401
+ bridge = self._oti_bridges.pop(key, None)
402
+ if bridge:
403
+ try:
404
+ self.unobserve(bridge, names=[trait_name])
405
+ except Exception:
406
+ pass
407
+
408
+ if not hasattr(_tr.HasTraits, 'trait_get'):
409
+ _tr.HasTraits.trait_get = _trait_get
410
+ if not hasattr(_tr.HasTraits, 'trait_set'):
411
+ _tr.HasTraits.trait_set = _trait_set
412
+ if not hasattr(_tr.HasTraits, 'trait_names'):
413
+ _tr.HasTraits.trait_names = _trait_names
414
+ if not hasattr(_tr.HasTraits, 'get'):
415
+ _tr.HasTraits.get = _get
416
+ def _add_trait(self, name, trait_class_or_inst):
417
+ """traits API: add_trait(name, TraitClass) — add a dynamic trait."""
418
+ if isinstance(trait_class_or_inst, type):
419
+ inst = trait_class_or_inst()
420
+ else:
421
+ inst = trait_class_or_inst
422
+ self.add_traits(**{name: inst})
423
+
424
+ if not hasattr(_tr.HasTraits, 'on_trait_change'):
425
+ _tr.HasTraits.on_trait_change = _on_trait_change
426
+ if not hasattr(_tr.HasTraits, 'add_trait'):
427
+ _tr.HasTraits.add_trait = _add_trait
428
+
429
+ # ── Undefined sentinel (traitlets already has one) ──────────────────
430
+ Undefined = _tr.Undefined
431
+
432
+ # ── TraitError ────────────────────────────────────────────────────────
433
+ TraitError = _tr.TraitError
434
+
435
+ # ── Trait descriptor factory ─────────────────────────────────────────
436
+ # traits.Property needs a callable that returns a descriptor. We use a
437
+ # simple traitlets.Any with optional validator/observer hooks.
438
+ class _Property(_tr.Any):
439
+ def __init__(self, *args, fget=None, fset=None, depends_on=None, **kw):
440
+ super().__init__(None, allow_none=True, **kw)
441
+
442
+ class _Event(_tr.Any):
443
+ """Fires-and-forgets; observers receive the new value."""
444
+ def __init__(self, *args, **kw):
445
+ super().__init__(None, allow_none=True, **kw)
446
+
447
+ class _Button(_tr.Any):
448
+ def __init__(self, *args, **kw):
449
+ super().__init__(None, allow_none=True, **kw)
450
+
451
+ # ── Array placeholder ─────────────────────────────────────────────────
452
+ class _Array(_tr.Any):
453
+ def __init__(self, *args, **kw):
454
+ super().__init__(None, allow_none=True, **kw)
455
+
456
+ # ── Str: traits.Str accepts Undefined as a valid value (used as
457
+ # "not set" sentinel in HyperSpy axes). Use Any so any value works. ──
458
+ class _TraitsStr(_tr.Any):
459
+ def __init__(self, default_value='', **kw):
460
+ kw.setdefault('allow_none', True)
461
+ super().__init__(default_value, **kw)
462
+
463
+ # ── List: traits.List(SomeClass) means typed list; traitlets.List needs
464
+ # an Instance trait, not a class, as the first arg. ──────────────────
465
+ class _TraitsList(_tr.List):
466
+ def __init__(self, trait_or_type=None, value=None, **kw):
467
+ if isinstance(trait_or_type, type):
468
+ # traits API: List(SomeClass) — element type
469
+ trait = _tr.Instance(trait_or_type)
470
+ else:
471
+ trait = trait_or_type
472
+ super().__init__(trait=trait, **kw)
473
+
474
+ # ── Enum: traitlets.UseEnum needs an actual enum.Enum class;
475
+ # traits.Enum takes *values* instead. Bridge with a custom class. ──
476
+ class _TraitsEnum(_tr.TraitType):
477
+ def __init__(self, values, **kw):
478
+ self._allowed = list(values)
479
+ super().__init__(self._allowed[0] if self._allowed else None, **kw)
480
+ def validate(self, obj, value):
481
+ if value in self._allowed:
482
+ return value
483
+ raise _tr.TraitError(
484
+ f"The value {value!r} is not in {self._allowed!r}"
485
+ )
486
+
487
+ # ── Range ─────────────────────────────────────────────────────────────
488
+ class _Range(_tr.Any):
489
+ def __init__(self, low=None, high=None, value=None, **kw):
490
+ super().__init__(value if value is not None else low, allow_none=True, **kw)
491
+
492
+ # ── Instance: traits.Instance(SomeClass) allows None by default;
493
+ # traitlets.Instance doesn't. Wrap to force allow_none=True. ─────────
494
+ class _Instance(_tr.Instance):
495
+ def __init__(self, klass=None, args=None, **kw):
496
+ kw.setdefault('allow_none', True)
497
+ super().__init__(klass, args=args, **kw)
498
+
499
+ # ── WeakRef stub ──────────────────────────────────────────────────────
500
+ class _WeakRef(_tr.Any):
501
+ def __init__(self, *a, **kw):
502
+ super().__init__(None, allow_none=True)
503
+
504
+ # ── Delegate / DelegatesTo / PrototypedFrom stubs ─────────────────────
505
+ class _Delegate(_tr.Any):
506
+ def __init__(self, *a, **kw):
507
+ super().__init__(None, allow_none=True)
508
+
509
+ # ── Type stub ─────────────────────────────────────────────────────────
510
+ class _Type(_tr.Type):
511
+ pass
512
+
513
+ # ── Build traits.api namespace ─────────────────────────────────────────
514
+ import types as _types
515
+
516
+ _traits_api = _types.ModuleType('traits.api')
517
+
518
+ # Map the most-used traits names onto traitlets equivalents or our shims.
519
+ _traits_api.HasTraits = _tr.HasTraits
520
+ _traits_api.HasStrictTraits = _tr.HasTraits
521
+ _traits_api.HasPrivateTraits = _tr.HasTraits
522
+ _traits_api.HasRequiredTraits= _tr.HasTraits
523
+ _traits_api.Interface = _tr.HasTraits
524
+ _traits_api.Undefined = Undefined
525
+ _traits_api.TraitError = TraitError
526
+ _traits_api.Bool = _tr.Bool
527
+ _traits_api.CBool = _tr.Bool
528
+ _traits_api.BaseBool = _tr.Bool
529
+ _traits_api.BaseCBool = _tr.Bool
530
+ _traits_api.Int = _tr.Int
531
+ _traits_api.CInt = _tr.Int
532
+ _traits_api.BaseInt = _tr.Int
533
+ _traits_api.BaseCInt = _tr.Int
534
+ _traits_api.Float = _tr.Float
535
+ _traits_api.CFloat = _tr.Float
536
+ _traits_api.BaseFloat = _tr.Float
537
+ _traits_api.BaseCFloat = _tr.Float
538
+ _traits_api.Complex = _tr.Complex
539
+ _traits_api.CComplex = _tr.Complex
540
+ _traits_api.BaseComplex = _tr.Complex
541
+ _traits_api.BaseCComplex = _tr.Complex
542
+ _traits_api.Str = _TraitsStr
543
+ _traits_api.CStr = _TraitsStr
544
+ _traits_api.String = _TraitsStr
545
+ _traits_api.BaseStr = _TraitsStr
546
+ _traits_api.BaseCStr = _TraitsStr
547
+ _traits_api.Bytes = _tr.Bytes
548
+ _traits_api.CBytes = _tr.Bytes
549
+ _traits_api.BaseBytes = _tr.Bytes
550
+ _traits_api.Any = _tr.Any
551
+ _traits_api.List = _TraitsList
552
+ _traits_api.CList = _TraitsList
553
+ _traits_api.Set = _tr.Set
554
+ _traits_api.CSet = _tr.Set
555
+ _traits_api.Dict = _tr.Dict
556
+ _traits_api.Tuple = _tr.Tuple
557
+ _traits_api.Instance = _Instance
558
+ _traits_api.BaseInstance = _Instance
559
+ _traits_api.AdaptedTo = _Instance
560
+ _traits_api.AdaptsTo = _Instance
561
+ _traits_api.Type = _tr.Type
562
+ _traits_api.Subclass = _tr.Type
563
+ _traits_api.Enum = _TraitsEnum
564
+ _traits_api.BaseEnum = _TraitsEnum
565
+ _traits_api.Range = _Range
566
+ _traits_api.BaseRange = _Range
567
+ _traits_api.Property = _Property
568
+ _traits_api.Event = _Event
569
+ _traits_api.Button = _Button
570
+ _traits_api.ToolbarButton = _Button
571
+ _traits_api.WeakRef = _WeakRef
572
+ _traits_api.Delegate = _Delegate
573
+ _traits_api.DelegatesTo = _Delegate
574
+ _traits_api.PrototypedFrom = _Delegate
575
+ _traits_api.Callable = _tr.Callable
576
+ _traits_api.BaseCallable = _tr.Callable
577
+ _traits_api.Either = _tr.Union
578
+ _traits_api.Union = _tr.Union
579
+ _traits_api.ReadOnly = _tr.Any
580
+ _traits_api.Disallow = _tr.Any
581
+ _traits_api.Constant = _tr.Any
582
+ _traits_api.PrefixList = _tr.List
583
+ _traits_api.PrefixMap = _tr.Dict
584
+ _traits_api.Map = _tr.Dict
585
+ _traits_api.Expression = _tr.Any
586
+ _traits_api.PythonValue = _tr.Any
587
+ _traits_api.File = _tr.Unicode
588
+ _traits_api.Directory = _tr.Unicode
589
+ _traits_api.BaseFile = _tr.Unicode
590
+ _traits_api.BaseDirectory = _tr.Unicode
591
+ _traits_api.HTML = _tr.Unicode
592
+ _traits_api.Password = _tr.Unicode
593
+ _traits_api.Regex = _tr.Unicode
594
+ _traits_api.Code = _tr.Unicode
595
+ _traits_api.Title = _tr.Unicode
596
+ _traits_api.UUID = _tr.Unicode
597
+ _traits_api.Date = _tr.Any
598
+ _traits_api.Datetime = _tr.Any
599
+ _traits_api.Time = _tr.Any
600
+ _traits_api.Supports = _tr.Any
601
+ _traits_api.Python = _tr.Any
602
+ _traits_api.Module = _tr.Any
603
+ _traits_api.Self = _tr.Any
604
+ _traits_api.This = _tr.Any
605
+ _traits_api.self = _tr.Any
606
+ _traits_api.ValidatedTuple = _tr.Tuple
607
+ _traits_api.Array = _Array
608
+
609
+ # observers/decorators
610
+ _traits_api.observe = _tr.observe
611
+ _traits_api.on_trait_change = lambda *a, **kw: (lambda f: f)
612
+ _traits_api.cached_property = property
613
+ _traits_api.property_depends_on = lambda *a, **kw: (lambda f: f)
614
+ _traits_api.provides = lambda *a, **kw: (lambda cls: cls)
615
+ _traits_api.isinterface = lambda x: False
616
+
617
+ # meta classes
618
+ _traits_api.MetaHasTraits = type(_tr.HasTraits)
619
+ _traits_api.ABCHasTraits = _tr.HasTraits
620
+ _traits_api.ABCHasStrictTraits= _tr.HasTraits
621
+ _traits_api.ABCMetaHasTraits = type(_tr.HasTraits)
622
+ _traits_api.AbstractViewElement = _tr.HasTraits
623
+
624
+ # misc
625
+ _traits_api.Trait = _tr.Any
626
+ _traits_api.Default = lambda *a, **kw: None
627
+ _traits_api.Vetoable = _tr.HasTraits
628
+ _traits_api.VetoableEvent = _Event
629
+
630
+ # ── traits.trait_errors module ────────────────────────────────────────
631
+ _te = _types.ModuleType('traits.trait_errors')
632
+ _te.TraitError = TraitError
633
+ _te.TraitNotificationError = TraitError
634
+ _te.DelegationError = TraitError
635
+
636
+ # ── traits.trait_numeric module ───────────────────────────────────────
637
+ _tn = _types.ModuleType('traits.trait_numeric')
638
+ _tn.Array = _Array
639
+
640
+ # ── traits.etsconfig module ────────────────────────────────────────────
641
+ _etc = _types.ModuleType('traits.etsconfig')
642
+ _etc_api = _types.ModuleType('traits.etsconfig.api')
643
+ class _ETSConfig:
644
+ toolkit = ''
645
+ _etc_api.ETSConfig = _ETSConfig()
646
+ _etc.api = _etc_api
647
+
648
+ # ── traits.constants ──────────────────────────────────────────────────
649
+ _tc = _types.ModuleType('traits.constants')
650
+ _tc.ComparisonMode = None
651
+ _tc.DefaultValue = None
652
+ _tc.TraitKind = None
653
+ _tc.ValidateTrait = None
654
+ _tc.NO_COMPARE = None
655
+ _tc.OBJECT_IDENTITY_COMPARE = None
656
+ _tc.RICH_COMPARE = None
657
+
658
+ # ── Register all in sys.modules ───────────────────────────────────────
659
+ _traits_root = _types.ModuleType('traits')
660
+ _traits_root.api = _traits_api
661
+ _traits_root.trait_errors= _te
662
+ _traits_root.trait_numeric= _tn
663
+ _traits_root.etsconfig = _etc
664
+ _traits_root.constants = _tc
665
+ _traits_root.__version__ = '9999.0.0'
666
+
667
+ sys.modules['traits'] = _traits_root
668
+ sys.modules['traits.api'] = _traits_api
669
+ sys.modules['traits.trait_errors'] = _te
670
+ sys.modules['traits.trait_numeric'] = _tn
671
+ sys.modules['traits.etsconfig'] = _etc
672
+ sys.modules['traits.etsconfig.api'] = _etc_api
673
+ sys.modules['traits.constants'] = _tc
674
+ print('[sphinx_anywidget] traits shim installed')
675
+ `));
676
+ console.info('[sphinx_anywidget] traits shim installed');
677
+
678
+ // 5.6 Stub rosettasciio (rsciio) in sys.modules.
679
+ // rosettasciio has one Bruker C extension but otherwise is pure Python.
680
+ // HyperSpy imports rsciio.utils.{path,rgb} and rsciio.IO_PLUGINS at
681
+ // module-level. Provide minimal stubs so HyperSpy starts up cleanly
682
+ // without needing the real package installed.
683
+ await _step('stub rsciio',
684
+ pyodide.runPythonAsync(`
685
+ import sys, types as _types
686
+ import numpy as _np
687
+
688
+ # ── rsciio.utils.path ─────────────────────────────────────────────────
689
+ from pathlib import Path as _Path
690
+
691
+ def _append2pathname(filename, to_append):
692
+ p = _Path(filename)
693
+ return _Path(p.parent, p.stem + to_append + p.suffix)
694
+
695
+ def _incremental_filename(filename, i=1):
696
+ filename = _Path(filename)
697
+ if filename.is_file():
698
+ nf = _append2pathname(filename, f"-{i}")
699
+ return _incremental_filename(filename, i+1) if nf.is_file() else nf
700
+ return filename
701
+
702
+ def _ensure_directory(path):
703
+ p = _Path(path)
704
+ p = p.parent if p.is_file() else p
705
+ try: p.mkdir(parents=True, exist_ok=False)
706
+ except FileExistsError: pass
707
+
708
+ def _overwrite(filename): return True # no interactive prompts in Pyodide
709
+
710
+ _rp = _types.ModuleType('rsciio.utils.path')
711
+ _rp.append2pathname = _append2pathname
712
+ _rp.incremental_filename = _incremental_filename
713
+ _rp.ensure_directory = _ensure_directory
714
+ _rp.overwrite = _overwrite
715
+
716
+ # ── rsciio.utils.rgb ──────────────────────────────────────────────────
717
+ _RGBA8 = _np.dtype({"names": ["R","G","B","A"], "formats":["u1","u1","u1","u1"]})
718
+ _RGB8 = _np.dtype({"names": ["R","G","B"], "formats":["u1","u1","u1"]})
719
+ _RGBA16 = _np.dtype({"names": ["R","G","B","A"], "formats":["u2","u2","u2","u2"]})
720
+ _RGB16 = _np.dtype({"names": ["R","G","B"], "formats":["u2","u2","u2"]})
721
+ RGB_DTYPES = {"rgb8": _RGB8, "rgb16": _RGB16, "rgba8": _RGBA8, "rgba16": _RGBA16}
722
+
723
+ def _is_rgba(a): return a.dtype in (_RGBA8, _RGBA16)
724
+ def _is_rgb(a): return a.dtype in (_RGB8, _RGB16)
725
+ def _is_rgbx(a): return _is_rgb(a) or _is_rgba(a)
726
+
727
+ def _rgbx2regular_array(data, plot_friendly=False, show_progressbar=True):
728
+ dt = data.dtype
729
+ names = list(dt.names)
730
+ arr = _np.stack([data[n] for n in names], axis=-1)
731
+ if plot_friendly and arr.dtype != _np.uint8:
732
+ arr = arr.astype(float) / _np.iinfo(arr.dtype).max
733
+ return arr
734
+
735
+ def _regular_array2rgbx(data, **kw):
736
+ n = data.shape[-1]
737
+ names = ["R","G","B","A"][:n]
738
+ dt = _np.dtype({"names": names, "formats": [data.dtype]*n})
739
+ out = _np.empty(data.shape[:-1], dtype=dt)
740
+ for i, nm in enumerate(names):
741
+ out[nm] = data[...,i]
742
+ return out
743
+
744
+ _rrgb = _types.ModuleType('rsciio.utils.rgb')
745
+ _rrgb.RGB_DTYPES = RGB_DTYPES
746
+ _rrgb.is_rgba = _is_rgba
747
+ _rrgb.is_rgb = _is_rgb
748
+ _rrgb.is_rgbx = _is_rgbx
749
+ _rrgb.rgbx2regular_array = _rgbx2regular_array
750
+ _rrgb.regular_array2rgbx = _regular_array2rgbx
751
+
752
+ # ── rsciio.utils (parent) ─────────────────────────────────────────────
753
+ _ru = _types.ModuleType('rsciio.utils')
754
+ _ru.path = _rp
755
+ _ru.rgb = _rrgb
756
+
757
+ # ── rsciio root ───────────────────────────────────────────────────────
758
+ _rsciio = _types.ModuleType('rsciio')
759
+ _rsciio.utils = _ru
760
+ _rsciio.IO_PLUGINS = []
761
+ _rsciio.__version__ = '9999.0.0'
762
+
763
+ sys.modules['rsciio'] = _rsciio
764
+ sys.modules['rsciio.utils'] = _ru
765
+ sys.modules['rsciio.utils.path'] = _rp
766
+ sys.modules['rsciio.utils.rgb'] = _rrgb
767
+ print('[sphinx_anywidget] rsciio shim installed')
768
+
769
+ # ── natsort stub ──────────────────────────────────────────────────────
770
+ _natsort = _types.ModuleType('natsort')
771
+ _natsort.natsorted = sorted
772
+ _natsort.natsort_keygen = lambda *a, **kw: (lambda x: x)
773
+ _natsort.ns = type('ns', (), {'DEFAULT': 0, 'REAL': 1, 'LOCALE': 2})()
774
+ sys.modules['natsort'] = _natsort
775
+
776
+ # ── prettytable stub ──────────────────────────────────────────────────
777
+ _pt = _types.ModuleType('prettytable')
778
+ class _PrettyTable:
779
+ def __init__(self, *a, **kw): self._rows = []
780
+ def add_row(self, row): self._rows.append(row)
781
+ def __str__(self): return str(self._rows)
782
+ _pt.PrettyTable = _PrettyTable
783
+ sys.modules['prettytable'] = _pt
784
+
785
+ print('[sphinx_anywidget] misc stubs installed')
786
+ `));
787
+ console.info('[sphinx_anywidget] rsciio shim installed');
788
+
789
+ // 6. Install package wheel via micropip.
790
+ // anywidget is already stubbed in sys.modules (step 5). Register it as
791
+ // a mock package so micropip does not try to download it from PyPI when
792
+ // it resolves the wheel's declared deps. numpy/traitlets/colorcet are
793
+ // properly installed above so micropip will find them and skip them.
794
+ // We do NOT pass deps=False — that triggers a micropip 0.7.x internal
795
+ // bug ("attempted to install wheel before downloading it").
796
+
797
+ // Collect all _PYODIDE_MOCK_PACKAGES declared by any example on this page
798
+ // so they can be registered as mock packages BEFORE the wheel install.
799
+ // (Dep resolution happens during install, not during exec.)
800
+ const _globalMockPkgs = [];
801
+ for (const s of document.querySelectorAll(
802
+ 'script[type="text/x-python"][data-pyodide-mock-packages]')) {
803
+ try {
804
+ const pkgs = JSON.parse(s.dataset.pyodideMockPackages || '[]');
805
+ for (const p of pkgs) if (!_globalMockPkgs.includes(p)) _globalMockPkgs.push(p);
806
+ } catch (_) {}
807
+ }
808
+
809
+ const wheelUrl = _DOCS_ROOT + '_static/wheels/';
810
+ // Discover wheel name: try the configured package name from the <script>
811
+ // data-package attribute injected by _build_pyodide_wheel, else fall back
812
+ // to scanning data-src-file scripts for a clue.
813
+ const pkgName = _inferPackageName();
814
+ const fullWheelUrl = wheelUrl + pkgName + '-0.0.0-py3-none-any.whl';
815
+ console.info('[sphinx_anywidget] installing wheel from', fullWheelUrl);
816
+ const _mockList = JSON.stringify(['anywidget', ..._globalMockPkgs]);
817
+ await _step('install wheel', pyodide.runPythonAsync(`
818
+ import micropip
819
+ # Register anywidget + any page-level mock packages so micropip skips them
820
+ # during dep resolution. Our sys.modules stub takes priority at import time.
821
+ for _pkg in ${_mockList}:
822
+ try:
823
+ micropip.add_mock_package(_pkg, "9999.0.0")
824
+ except Exception:
825
+ pass
826
+ del _pkg
827
+ await micropip.install(${JSON.stringify(fullWheelUrl)})
828
+ `));
829
+ console.info('[sphinx_anywidget] package wheel installed');
830
+
831
+ // 6.5 Patch BaseDataAxis._update_slice to notify AxesManager
832
+ // When traits observes "_axes.slice" (dotted path), it calls
833
+ // AxesManager._on_slice_changed → _update_attributes() each time any
834
+ // axis's slice changes. Our traitlets shim handles dotted paths by
835
+ // registering item-level observers, but those are set up during
836
+ // AxesManager.__init__ *before* the wheel is installed. Patching
837
+ // _update_slice directly is the most reliable alternative: after each
838
+ // slice assignment the axes_manager (if already set) re-classifies axes.
839
+ await pyodide.runPythonAsync(`
840
+ from hyperspy.axes import BaseDataAxis as _BDA, AxesManager as _AM
841
+ _orig_update_slice = _BDA._update_slice
842
+
843
+ def _patched_update_slice(self, value):
844
+ _orig_update_slice(self, value)
845
+ am = getattr(self, 'axes_manager', None)
846
+ if am is not None and hasattr(am, '_update_attributes'):
847
+ am._update_attributes()
848
+
849
+ _BDA._update_slice = _patched_update_slice
850
+ print('[sphinx_anywidget] BaseDataAxis._update_slice patched for traitlets compat')
851
+ `).catch(e => console.warn('[DIAG] _update_slice patch failed:', e.message));
852
+
853
+ // 7. Install generic anywidget monkey-patch
854
+ // Wraps AnyWidget.__init__ so that every new widget instance automatically
855
+ // gets a traitlets.observe(names=All) callback. The callback pushes any
856
+ // sync=True trait change to the matching iframe via window._anywidgetPush.
857
+ // This works for dynamically-added traits too because traitlets.All
858
+ // matches all trait names, including ones added after __init__ via add_traits().
859
+ window._anywidgetPush = (figId, key, value) =>
860
+ _postToIframe(String(figId), String(key), String(value));
861
+
862
+ await _step('install monkey-patch',
863
+ pyodide.runPythonAsync(`
864
+ import anywidget as _aw
865
+ import traitlets as _tr
866
+ import js
867
+
868
+ _orig_init = _aw.AnyWidget.__init__
869
+
870
+ def _patched_init(self, *args, **kw):
871
+ _orig_init(self, *args, **kw)
872
+
873
+ # Capture self in a closure so each widget has its own push callback.
874
+ _self = self
875
+
876
+ def _push_cb(change):
877
+ fid = getattr(_self, '_anywidget_fig_id', None)
878
+ if fid is None:
879
+ return
880
+ tname = change['name']
881
+ # Only push traits tagged sync=True
882
+ # instance_traits() covers dynamically-added traits from add_traits()
883
+ t = _self.traits().get(tname)
884
+ if t is None or not t.metadata.get('sync'):
885
+ return
886
+ import json as _j
887
+ val = change['new']
888
+ # Always JSON-encode so the JS bridge can JSON.parse to recover the
889
+ # correct type — strings, numbers, bools and objects all round-trip.
890
+ val_str = _j.dumps(val, default=str)
891
+ js.window._anywidgetPush(fid, tname, val_str)
892
+
893
+ self.observe(_push_cb, names=_tr.All)
894
+
895
+ _aw.AnyWidget.__init__ = _patched_init
896
+ print('[sphinx_anywidget] anywidget monkey-patch installed')
897
+ `));
898
+ console.info('[sphinx_anywidget] monkey-patch installed');
899
+
900
+ // 8. Collect text/x-python script blocks, group by src-file so each
901
+ // example source runs exactly once even with multiple figures.
902
+ const srcGroups = new Map(); // srcFile → { src, pairs, packages, mockPackages }
903
+
904
+ for (const script of document.querySelectorAll(
905
+ 'script[type="text/x-python"][data-fig-id]')) {
906
+ const srcFile = script.dataset.srcFile || '__default__';
907
+ const figId = script.dataset.figId;
908
+ const figIndex = parseInt(script.dataset.figIndex || '0', 10);
909
+ let src = '';
910
+ try { src = JSON.parse(script.dataset.src || 'null') || ''; } catch (_) {}
911
+ let packages = [];
912
+ try { packages = JSON.parse(script.dataset.pyodidePackages || 'null') || []; } catch (_) {}
913
+ let mockPackages = [];
914
+ try { mockPackages = JSON.parse(script.dataset.pyodideMockPackages || 'null') || []; } catch (_) {}
915
+
916
+ if (!srcGroups.has(srcFile)) srcGroups.set(srcFile, { src, pairs: [], packages, mockPackages });
917
+ const grp = srcGroups.get(srcFile);
918
+ grp.pairs.push({ figId, figIndex });
919
+ // Merge any packages declared by any script tag for this source file.
920
+ for (const p of packages) if (!grp.packages.includes(p)) grp.packages.push(p);
921
+ for (const p of mockPackages) if (!grp.mockPackages.includes(p)) grp.mockPackages.push(p);
922
+ }
923
+
924
+ for (const g of srcGroups.values())
925
+ g.pairs.sort((a, b) => a.figIndex - b.figIndex);
926
+
927
+ // 9. Run each example source once, assign _anywidget_fig_id in creation
928
+ // order, then push current state into the matching iframes.
929
+ const _execErrors = [];
930
+ for (const [srcFile, { src, pairs, packages, mockPackages }] of srcGroups) {
931
+ const figIdList = JSON.stringify(pairs.map(p => p.figId));
932
+ console.info(`[sphinx_anywidget] running: ${srcFile} (${pairs.length} figure(s))`);
933
+ const _srcFileRepr = JSON.stringify(srcFile);
934
+
935
+ // Load any extra packages declared by this example (e.g. scipy).
936
+ if (packages.length > 0) {
937
+ console.info(`[sphinx_anywidget] loading extra packages for ${srcFile}:`, packages);
938
+ await _step(`load packages for ${srcFile}`,
939
+ pyodide.loadPackage(packages));
940
+ console.info(`[sphinx_anywidget] extra packages loaded for ${srcFile}`);
941
+ }
942
+
943
+ try {
944
+ await pyodide.runPythonAsync(`
945
+ import anywidget as _aw
946
+ import traitlets as _tr
947
+
948
+ _SRC_FILE = ${_srcFileRepr}
949
+ _CREATED_WIDS = []
950
+ _SEEN_IDS = set()
951
+ _orig_init2 = _aw.AnyWidget.__init__
952
+
953
+ def _tracked_init(self, *a, **kw):
954
+ _orig_init2(self, *a, **kw)
955
+ if id(self) not in _SEEN_IDS:
956
+ _SEEN_IDS.add(id(self))
957
+ _CREATED_WIDS.append(self)
958
+
959
+ _aw.AnyWidget.__init__ = _tracked_init
960
+ _exec_error = None
961
+ try:
962
+ exec(${JSON.stringify(src)}, {"__name__": "__main__"})
963
+ except Exception as _e:
964
+ import traceback as _tb
965
+ _exec_error = _tb.format_exc()
966
+ print("[sphinx_anywidget] exec error in " + _SRC_FILE + ":\\n" + _exec_error)
967
+ finally:
968
+ _aw.AnyWidget.__init__ = _orig_init2
969
+
970
+ _fig_ids = ${figIdList}
971
+ _wired = 0
972
+ for _i, _fid in enumerate(_fig_ids):
973
+ if _i < len(_CREATED_WIDS):
974
+ _w = _CREATED_WIDS[_i]
975
+ _w._anywidget_fig_id = _fid
976
+ _AWI_REGISTRY[_fid] = _w
977
+ # Push current state into the iframe immediately
978
+ for _tname, _trait in _w.traits().items():
979
+ if _tname.startswith('_') or not _trait.metadata.get('sync'):
980
+ continue
981
+ import json as _jj
982
+ _val = getattr(_w, _tname, None)
983
+ if _val is None:
984
+ continue
985
+ # Always JSON-encode (matching the live observer above) so the
986
+ # JS bridge can JSON.parse to recover the correct type.
987
+ _vs = _jj.dumps(_val, default=str)
988
+ import js as _js
989
+ _js.window._anywidgetPush(_fid, _tname, _vs)
990
+ _wired += 1
991
+
992
+ print("[sphinx_anywidget] wired " + str(_wired) + "/" + str(len(_fig_ids))
993
+ + " widgets for " + _SRC_FILE)
994
+ if _exec_error:
995
+ raise RuntimeError("exec failed: " + _exec_error)
996
+ `);
997
+ } catch (err) {
998
+ const msg = String(err.message || err);
999
+ console.warn(`[sphinx_anywidget] Pyodide failed for ${srcFile}:`, msg);
1000
+ _execErrors.push(`${srcFile}: ${msg.split('\n').filter(Boolean).pop()}`);
1001
+ }
1002
+ }
1003
+
1004
+ // 10. Route awi_event messages from iframes → Pyodide callbacks.
1005
+ // Call the pre-compiled _awi_dispatch proxy DIRECTLY (no runPythonAsync
1006
+ // code-string recompile per frame — that was ~1.2 ms/event in WASM, the
1007
+ // dominant per-frame cost of the Pyodide interaction path; the proxy is
1008
+ // ~50x faster). Synchronous call: _dispatch_event itself is sync, and
1009
+ // skipping the async wrapper removes a microtask hop per event.
1010
+ // The proxy is fetched lazily + cached (robust to any boot-step ordering),
1011
+ // with a one-shot runPythonAsync fallback if it isn't available.
1012
+ let _awiDispatch = null;
1013
+ window.addEventListener('message', (e) => {
1014
+ if (!e.data || e.data.type !== 'awi_event') return;
1015
+ const { figId, data } = e.data;
1016
+ try {
1017
+ if (!_awiDispatch) {
1018
+ try { _awiDispatch = pyodide.globals.get('_awi_dispatch'); } catch (_) {}
1019
+ }
1020
+ if (_awiDispatch) {
1021
+ _awiDispatch(figId, data);
1022
+ } else {
1023
+ // Fallback (should not happen): recompiled dispatch.
1024
+ pyodide.runPythonAsync(
1025
+ `_awi_dispatch(${JSON.stringify(figId)}, ${JSON.stringify(data)})`);
1026
+ }
1027
+ } catch (err) {
1028
+ console.warn('[sphinx_anywidget] event dispatch error:', err);
1029
+ }
1030
+ });
1031
+
1032
+ if (_execErrors.length > 0) {
1033
+ console.warn('[sphinx_anywidget] some examples failed:', _execErrors);
1034
+ }
1035
+
1036
+ return pyodide;
1037
+ }
1038
+
1039
+ // ── package name inference ────────────────────────────────────────────────
1040
+
1041
+ function _inferPackageName() {
1042
+ // 0. Authoritative: set by anywidget_config.js (written at build time)
1043
+ if (window._anywidgetPackage) return window._anywidgetPackage;
1044
+ // 1. Check for a <meta name="anywidget:package"> tag (set by the extension)
1045
+ const meta = document.querySelector('meta[name="anywidget:package"]');
1046
+ if (meta) return meta.getAttribute('content');
1047
+ // 2. Scan script data-src-file stems for a known module name
1048
+ // (heuristic: first segment before "_" often indicates the package)
1049
+ const script = document.querySelector('script[type="text/x-python"][data-src-file]');
1050
+ if (script) {
1051
+ const stem = script.dataset.srcFile || '';
1052
+ const pkg = stem.split('_')[0];
1053
+ if (pkg) return pkg;
1054
+ }
1055
+ // 3. Fallback: derive from the docs root URL last segment
1056
+ try {
1057
+ const parts = new URL(_DOCS_ROOT).pathname.replace(/\/$/, '').split('/');
1058
+ for (let i = parts.length - 1; i >= 0; i--) {
1059
+ const s = parts[i];
1060
+ if (s && s !== 'dev' && !/^v\d/.test(s)) return s;
1061
+ }
1062
+ } catch (_) {}
1063
+ return 'anyplotlib';
1064
+ }
1065
+
1066
+ // ── per-badge click handler ───────────────────────────────────────────────
1067
+
1068
+ function _attachBadgeHandlers() {
1069
+ // Use event delegation so badges added by SG after DOMContentLoaded also work.
1070
+ document.addEventListener('click', function (e) {
1071
+ const btn = e.target.closest('button.awi-activate-btn');
1072
+ if (!btn || btn.dataset.state) return; // already loading/active/error
1073
+
1074
+ // Disable ALL activate buttons and show ⏳ on all of them
1075
+ _setBadgesLoading();
1076
+
1077
+ _boot()
1078
+ .then(() => _setBadgesActive())
1079
+ .catch(err => {
1080
+ console.error('[sphinx_anywidget] boot failed:', err);
1081
+ _setBadgesError(err);
1082
+ });
1083
+ });
1084
+ }
1085
+
1086
+ // ── init ──────────────────────────────────────────────────────────────────
1087
+
1088
+ function _init() {
1089
+ if (!_hasInteractiveFigures()) return;
1090
+ _attachBadgeHandlers();
1091
+ }
1092
+
1093
+ if (document.readyState === 'loading') {
1094
+ document.addEventListener('DOMContentLoaded', _init);
1095
+ } else {
1096
+ _init();
1097
+ }
1098
+ })();
1099
+