Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,8 @@ dmypy.json
# diffs from visual regression tests
examples/desktop/diffs/*.png
docs/source/_gallery/


# not sure why this spammed my diff
docs/source/api/
docs/source/_static/*.whl
63 changes: 63 additions & 0 deletions docs/source/_static/_pyodide_iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!doctype html>
<html>
<!-- adapted from pygfx, adapted from wgpu-py, adapted from rendercanvas -->
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Fastplotlib examples in the browser using Pyodide</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js"></script>
</head>

<body>
<dialog id="loading" style='outline: none; border: none; background: transparent;'>
<h1>click the button to load pyodide</h1>
<button id="load_button" type="button" onclick="main()">load in browser!</button>
</dialog>
<canvas id='canvas' style='width:calc(100%); height:560px; background-color: #ddd;'></canvas>
<div id="output" style="white-space: per-line; overflow-y: auto; height:90px; background:#eee; border:1px solid #ccc;">
<p>Output:</p>
</div>
<script type="text/javascript">
let loading = document.getElementById('loading');
loading.showModal();
async function main() {
let button = document.getElementById("load_button");
button.disabled = true;
button.innerText = "loading...";
try {
let example_name = document.location.hash.slice(1);
pythonCode = await (await fetch(window.parent.location.pathname.replace(".html", ".py"))).text();
let pyodide = await loadPyodide();
pyodide.setStdout({
batched: (s) => {
el = document.getElementById("output");
el.innerHTML += "<br>" + s.replace(/</g, "&lt;").replace(/>/g, "&gt;");
el.scrollTop = el.scrollHeight; // scroll to bottom
console.log(s); // so we also have it formatted
}
});
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install([
'https://wgpu-py--753.org.readthedocs.build/en/753/_static/wgpu-0.31.0-py3-none-any.whl',
'imgui-bundle',
"https://pygfx--1273.org.readthedocs.build/1273/_static/uharfbuzz-0.54.1-cp310-abi3-pyodide_2025_0_wasm32.whl",
'hsluv', 'pylinalg', 'jinja2', 'httpx', 'trimesh', 'gltflib', 'imageio',
"https://pygfx--1273.org.readthedocs.build/1273/_static/pygfx-0.16.0-py3-none-any.whl"
]);
await micropip.install("fastplotlib") // this one should be replaced by the wheel during docs build.
await pyodide.loadPackagesFromImports(pythonCode); // additional non pypi packages from the examples... like imageio (not patched...)
pyodide.setDebug(false);
let ret = await pyodide.runPythonAsync(pythonCode);
// maybe we can trigger a resize event from the js side to get the canvas to fit the iframe when it's in the docs?
console.log("Example finished:", ret);
loading.close();
} catch (err) {
// TODO: this could be formatted better as this overlaps and is unreadable...
loading.innerHTML = "Failed to load: " + err;
console.error(err); // so we have it here too
}
}
</script>
</body>

</html>
5 changes: 5 additions & 0 deletions docs/source/_static/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
section[id^="interactive-example"] iframe {
width: 100%;
height: 520px;
border: none;
}
96 changes: 96 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,94 @@
"sphinx_gallery.gen_gallery",
]

# note this is largely copied from the pygfx PR branch: https://github.com/pygfx/pygfx/pull/1273
# -- Build wheel so Pyodide examples can use exactly this version of fpl -----------------------------------------------------
import subprocess
import shutil

short_version = ".".join(str(i) for i in fastplotlib.version_info[:3])
wheel_name = f"fastplotlib-{short_version}-py3-none-any.whl"

# Build the wheel
subprocess.run([sys.executable, "-m", "build", "-nw"], cwd=ROOT_DIR)
wheel_filename = os.path.join(ROOT_DIR, "dist", wheel_name)
assert os.path.isfile(wheel_filename), f"{wheel_name} does not exist"

# Copy into static
# TODO: you can use --outdir on the build command directly. also use the html_static_path in this namespace
print("Copy wheel to _static dir")
shutil.copy(
wheel_filename,
os.path.join(ROOT_DIR, "docs", "source", "_static", wheel_name),
)

# -- Sphix Gallery -----------------------------------------------------

## pyodide demos, adapted from wgpu, adapted from rendercanvas... might make sense to put this in the scaper?
iframe_placeholder_rst = """
.. only:: html

Interactive example
-------------------

Try this example in your browser using Pyodide. Might not work with all examples and all devices. Check the output and your browser's console for details.

.. raw:: html

<iframe src="./../pyodide.html#example.py"></iframe>
"""
python_files = {}

# I have a feeling this import might be really sketchy to have on CI... but hey - this is a hack for a hack
from examples.server_browser_examples import patch_imageio_for_pyodide


def add_pyodide_to_examples(app):
if app.builder.name != "html":
return

gallery_dir = ROOT_DIR / "docs" / "source" / "_gallery"
example_files = gallery_dir.glob("**/*.py")

for py_file in example_files:
fname = py_file.name
with open(py_file, "rb") as f:
py = f.read().decode()
py = patch_imageio_for_pyodide(py)
if fname:
print("Adding Pyodide example to", fname)
fname_rst = py_file.with_suffix(".rst")
# Update rst file
rst = iframe_placeholder_rst.replace("example.py", fname)
# we likely don't want append here?
with open(fname_rst, "ab") as f:
# TODO: skip if it already ends with the placeholder? otherwise the append will keep on appending (we have to hook this into the gen_rst to skip if possible?)
f.write(rst.encode())
python_files[py_file.relative_to(gallery_dir)] = py

def add_files_to_run_pyodide_examples(app, exception):
if app.builder.name != "html":
return

gallery_build_dir = os.path.join(app.outdir, "_gallery")

# Write html file that can load pyodide examples
with open(
os.path.join(ROOT_DIR, "docs", "source", "_static", "_pyodide_iframe.html"), "rb"
) as f:
html = f.read().decode()
html = html.replace('"fastplotlib"', f'"../_static/{wheel_name}"')
with open(os.path.join(gallery_build_dir, "pyodide.html"), "wb") as f:
f.write(html.encode())

# Write the python files
for fname, py in python_files.items():
print("Writing", fname)
with open(os.path.join(gallery_build_dir, fname), "wb") as f:
f.write(py.encode())



sphinx_gallery_conf = {
"gallery_dirs": "_gallery",
"notebook_extensions": {}, # remove the download notebook button
Expand Down Expand Up @@ -137,3 +225,11 @@
"rendercanvas": ("https://rendercanvas.readthedocs.io/stable/", None),
# "fastplotlib": ("https://www.fastplotlib.org/", None),
}

html_css_files = [
"style.css",
]

def setup(app):
app.connect("builder-inited", add_pyodide_to_examples)
app.connect("build-finished", add_files_to_run_pyodide_examples)
Loading