Skip to content

Sphinx-anywidget#9

Merged
CSSFrancis merged 13 commits intomainfrom
sphinx-anywidget
May 6, 2026
Merged

Sphinx-anywidget#9
CSSFrancis merged 13 commits intomainfrom
sphinx-anywidget

Conversation

@CSSFrancis
Copy link
Copy Markdown
Owner

This pull request introduces several enhancements and new features focused on improving interactive documentation support, widget communication, and robustness of the plotting library. The most significant addition is the new sphinx_anywidget Sphinx extension, which enables fully interactive figures in documentation using Pyodide, without requiring a server or Jupyter kernel. Additionally, the PR improves the widget–parent page communication bridge, makes colormap handling more robust, and ensures deterministic panel IDs for subplot panels. Minor changes include code comments for Sphinx Gallery compatibility and a new end-to-end test for the Playwright-based thumbnail scraper.

Sphinx extension for interactive documentation:

  • Added the new anyplotlib.sphinx_anywidget Sphinx extension, which enables interactive anywidget-based figures in documentation via Pyodide, provides a custom RST directive (.. anywidget-figure::), and integrates with Sphinx Gallery. This includes automatic wheel building for the target package and seamless asset injection. [1] [2]

Widget–parent page communication improvements:

  • Enhanced the widget HTML/JS bridge in _repr_utils.py to support robust two-way communication between widgets and the parent page using postMessage, including loop-prevention flags, event forwarding, and handling inbound state updates for Pyodide-based interactivity. [1] [2] [3] [4] [5]

Plotting library robustness and determinism:

  • Improved colormap LUT building in figure_plots.py to prefer colorcet but gracefully fall back to matplotlib or a gray ramp if unavailable, making color mapping more robust across environments.
  • Made subplot panel IDs deterministic by deriving them from the panel's position, ensuring stable IDs across runs and after code edits.

Testing and Sphinx Gallery compatibility:

  • Added a quick end-to-end test for the Playwright-based scraper thumbnail functionality in _test_scraper.py.
  • Added # Interactive comments to several example scripts for Sphinx Gallery compatibility. [1] [2] [3] [4] [5]

Event dispatching improvements:

  • Refactored event handling in figure.py to centralize event dispatch logic, allowing both traitlet observers and the Pyodide bridge to process interaction events consistently.

CSSFrancis added 5 commits May 3, 2026 14:01
Adds anyplotlib/sphinx_anywidget/ — a self-contained Sphinx extension that
makes any anywidget.AnyWidget-based figure interactive in documentation
pages via Pyodide, with no server or Jupyter kernel required.
Key changes
-----------
anyplotlib/sphinx_anywidget/
  __init__.py           Sphinx extension entry point; auto-injects JS/CSS,
                        builds wheel at docs-build time, registers directive
  _scraper.py           Generic AnywidgetScraper (replaces ViewerScraper);
                        detects '# Interactive' comment tag to opt-in per block
  _repr_utils.py        Self-contained HTML builder (no anyplotlib dep);
                        uses awi_state/awi_event message protAdds anyplotlib/sphinx_anywidget/ — a self-contained Sphinx extension that
makes any anywidget.AnyWidget-based figure interactive in documentation
pages via Pyodide, with no server or Jupyter kernel required.
Key changes
-----------
anyplotlib/sphinx_anywidget/
  __init__.py           Sphinx extension entry point;?akes any anywidget.AnyWidget-based figure inywidget monkey-patch replaces thpages via Pyodide, with no server or Jupyter kernel required.
Key chanecKey changes
-----------
anyplotlib/sphinx_anywidget/
  _      ----------inanyplotlibivate-btn, awi-static-icon)
any                        builds wheel at docs-build time, registers directf-  _scraper.py           Generic Anywie_fig_id attribute
  - _push() / _push_layout() / _push_widget() always write to traitlets;
    the generic monkey-  _repr_utils.py        Sethe change via
    traitlets.observe(names=All) — no                        uses awi_state/awi_event message protAdds anyplstmakes any anywidget.AnyWidget-based figure interactive in documentation
pages via Pyodide, with no server or Jupyter kernel required.
Keespages via Pyodide, with no server or Jupyter kernel required.
Key channsKey changes
-----------
anyplotlib/sphinx_anywidget/
  __ini_g----------f
anyplotlibtm  __init__.py           SphitKey chanecKey changes
-----------
anyplotlib/sphinx_anywidget/
  _      ----------inanyplotlibivate-btn, awi-static-icon)
any                        builds wheel at docs-build time, registers to-------raits directly
anyplotlib b  _      ----------inanyplogeany                        builds wheel at docs-build timta  - _push() / _push_layout() / _push_widget() always write to traitlets;
    the generic monkey-  _repr_utils.py ton
  - Update mock r    the generic monkey-  _repr_(_patched_init pattern)
Usage in other pro    traitlets.observe(names=All) — no                       nxpages via Pyodide, with no server or Jupyter kernel required.
Keespages via Pyodide, with no server or Jupyter kernel required.
Key channsKey changes
-----------
anyplotlib/sphinxteracKeespages via Pyodide, with no server or Jupyter kerngit log --oneline -3
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 3, 2026

Codecov Report

❌ Patch coverage is 4.85934% with 372 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.52%. Comparing base (ba0db6e) to head (4a3d13f).

Files with missing lines Patch % Lines
anyplotlib/sphinx_anywidget/_scraper.py 0.00% 129 Missing ⚠️
anyplotlib/sphinx_anywidget/_directive.py 0.00% 89 Missing ⚠️
anyplotlib/sphinx_anywidget/__init__.py 0.00% 62 Missing ⚠️
anyplotlib/sphinx_anywidget/_repr_utils.py 0.00% 57 Missing ⚠️
anyplotlib/sphinx_anywidget/_wheel_builder.py 0.00% 24 Missing ⚠️
anyplotlib/figure_plots.py 59.25% 11 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main       #9       +/-   ##
===========================================
- Coverage   87.79%   73.52%   -14.27%     
===========================================
  Files           7       12        +5     
  Lines        1901     2278      +377     
===========================================
+ Hits         1669     1675        +6     
- Misses        232      603      +371     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@CSSFrancis
Copy link
Copy Markdown
Owner Author

@ericpre

Long time coming.... But live responsive interactive documentation should be possible with this... I need to set up github to host docs from PRs still but once that is up it should be really nice for demoing a feature, writing documentation etc.

Screen.Recording.2026-05-03.at.2.30.58.PM.mov

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new anyplotlib.sphinx_anywidget Sphinx extension for Pyodide-backed interactive docs, updates the existing anyplotlib iframe bridge to support parent-page communication, and adds related tests/docs plumbing. It also includes a smaller plotting change for deterministic panel IDs and more resilient colormap lookup.

Changes:

  • Added a generic sphinx_anywidget extension with a scraper, directive, wheel builder, static bridge assets, and docs integration.
  • Expanded iframe/parent messaging and added Pyodide-oriented end-to-end tests plus example markers for interactive docs.
  • Improved plotting internals by making panel IDs deterministic and adding fallback colormap resolution.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
tests/test_pyodide_e2e.py Adds multi-tier Playwright/Pyodide bridge tests for iframe messaging and mocked boot flow.
test_sphinx_anywidget.py Adds a root-level smoke test script for the new extension imports/helpers.
docs/conf.py Enables the new Sphinx extension and adds docs-time wheel-building logic.
docs/_static/pyodide_bridge.js Introduces a docs-side Pyodide bridge script for anyplotlib figures.
docs/_sg_html_scraper.py Replaces the old scraper implementation with a compatibility shim.
anyplotlib/sphinx_anywidget/static/anywidget_overlay.css Adds styling for per-figure activation badges.
anyplotlib/sphinx_anywidget/static/anywidget_bridge.js Implements the generic anywidget Pyodide bridge and message routing.
anyplotlib/sphinx_anywidget/_wheel_builder.py Adds helper logic to build and rename a stable wheel for Pyodide installs.
anyplotlib/sphinx_anywidget/_scraper.py Adds the generic Sphinx Gallery scraper and interactive iframe generation.
anyplotlib/sphinx_anywidget/_repr_utils.py Adds standalone HTML generation utilities for generic anywidget widgets.
anyplotlib/sphinx_anywidget/_directive.py Adds the .. anywidget-figure:: directive for embedding widgets in RST pages.
anyplotlib/sphinx_anywidget/__init__.py Registers the extension, assets, directive, and wheel-build hook.
anyplotlib/figure_plots.py Makes panel IDs deterministic and adds colormap fallback handling.
anyplotlib/figure.py Refactors event dispatch into a reusable _dispatch_event() method.
anyplotlib/_repr_utils.py Extends the anyplotlib iframe model bridge for inbound/outbound postMessage state/events.
_test_scraper.py Adds a root-level scraper thumbnail smoke test script.
Examples/Interactive/plot_segment_by_contrast.py Marks the example as interactive for Sphinx Gallery/Pyodide docs.
Examples/Interactive/plot_point_widget.py Marks the example as interactive for Sphinx Gallery/Pyodide docs.
Examples/Interactive/plot_key_bindings.py Marks the example as interactive for Sphinx Gallery/Pyodide docs.
Examples/Interactive/plot_interactive_fitting.py Marks the example as interactive and declares extra Pyodide packages.
Examples/Interactive/plot_interactive_fft.py Marks the example as interactive for Sphinx Gallery/Pyodide docs.
.gitignore Adds ignore rules for build, docs, wheel, and local environment artifacts.
.github/workflows/docs.yml Builds the Pyodide wheel in docs workflows before running Sphinx.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread anyplotlib/sphinx_anywidget/__init__.py
Comment on lines +98 to +105
# Use a stable ID based on the source file name
stem = src_path.stem
fig_id = f"rst_{stem}"

docs_static = Path(env.app.outdir).parent / "_static" / "viewer_widgets"
docs_static.mkdir(parents=True, exist_ok=True)
html_name = f"{fig_id}.html"
html_path = docs_static / html_name
Comment on lines +64 to +71
wheels = sorted(wheels_dir.glob(f"{package_name}*.whl"))
if not wheels:
print(f"\n[sphinx_anywidget] WARNING: no wheel found for {package_name!r}")
return None

stable = wheels_dir / f"{package_name}-0.0.0-py3-none-any.whl"
stable.unlink(missing_ok=True)
wheels[-1].rename(stable)
Comment on lines +320 to +322
_vs = _val if isinstance(_val, str) else _jj.dumps(_val, default=str)
import js as _js
_js.window._anywidgetPush(_fid, _tname, _vs)
Comment on lines +70 to +71
is_interactive = "interactive" in self.options
max_width = self.options.get("width", 684)
Comment on lines +83 to +88
if not pkg:
print(
"[sphinx_anywidget] WARNING: anywidget_pyodide_package not set; "
"Pyodide interactive mode disabled."
)
return
Comment on lines +226 to +229
import json as _j
val = change['new']
val_str = val if isinstance(val, str) else _j.dumps(val, default=str)
js.window._anywidgetPush(fid, tname, val_str)
Comment on lines +141 to +147
script_tag = (
f'<script type="text/x-python"'
f' data-fig-id="{fig_id}"'
f' data-fig-index="0"'
f' data-src-file="{stem}"'
f' data-src="{data_src}"></script>'
)
Comment thread _test_scraper.py Outdated
@@ -0,0 +1,39 @@
"""Quick end-to-end test for the Playwright-based scraper thumbnail."""
Comment thread tests/test_pyodide_e2e.py
@CSSFrancis
Copy link
Copy Markdown
Owner Author

@copilot re-review after changes

Copy link
Copy Markdown
Contributor

Copilot AI commented May 4, 2026

@copilot re-review after changes

All unresolved review comments have been addressed. The main remaining items were the two test files living outside testpaths = ["tests"] and therefore never collected by CI:

  • test_sphinx_anywidget.py (root) → converted to proper pytest functions in tests/test_sphinx_anywidget.py
  • _test_scraper.py (root) → converted to proper pytest functions in tests/test_scraper.py

The root-level copies were removed. The earlier-resolved issues (fig_id hash, wheel name normalisation, trait type round-trip, :width: option, output directory fix, compat shim re-exports, interactive-disabled flag, data-pyodide-packages attribute) were already in place from previous commits.

Commit: 25921c6

@ericpre
Copy link
Copy Markdown

ericpre commented May 4, 2026

This looks great indeed! Please let me know if I can be of any help for the PR doc preview.

@ericpre
Copy link
Copy Markdown

ericpre commented May 5, 2026

I get the following error:
image

@CSSFrancis
Copy link
Copy Markdown
Owner Author

@ericpre it actually has to be served as an HTML server for it to work.

Otherwise it's just static

@ericpre
Copy link
Copy Markdown

ericpre commented May 6, 2026

Yes, after starting a server, it works indeed!

@CSSFrancis CSSFrancis merged commit c4fc350 into main May 6, 2026
9 of 12 checks passed
@CSSFrancis
Copy link
Copy Markdown
Owner Author

@ericpre should be live now.

My idea here is mostly that this should be a starting point for the hyperspy docs but also for general interactive code snippets. I'd like to make it easy to go from a sphinx gallery example --> Interactive documentation as a way to help people present their data. It should be possible to load a simple 10 mb dataset and make that interactive which is kind of what I'm looking for. I'll make a couple more examples (and hopefully I can publish a couple of things along the way).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants