Repository navigation
Docs: a live browser demo and a How it works explainer - #266
Merged
Merged
Conversation
alxmrs
commented
Oct 10, 2026
alxmrs
force-pushed
the
browser-demo
branch
2 times, most recently
from
October 10, 2026 04:01
67a7236 to
ee57c29
Compare
alxmrs
commented
Oct 10, 2026
alxmrs
left a comment
Member
Author
There was a problem hiding this comment.
Feedback, overall looks great.
A static page that registers NOAA GFS forecasts from dynamical.org's Zarr store on DuckDB, with xarray-sql and DuckDB running in Pyodide in a Web Worker. Queries download only the chunks that survive chunk pruning. JSPI cannot suspend while DuckDB or pyarrow calls back into Python, so chunks cannot be fetched mid-scan. ChunkCache records the chunks a query misses (scanning fill values in their place), fetches them concurrently with Dataset.load_async, and re-runs the query until a run has no misses. wasm-shell gains the shared libraries Playwright's headless Chromium needs, for testing the page on NixOS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
The demo moves from benchmarks/browser to docs/demo, and its README becomes a "Run in the browser" guide that embeds it in an iframe. When embedded, the demo hides its own title and introduction. Startup and query errors now show the exception line rather than the first line of the traceback, which goes to the console. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
ChunkCache now serves reads through an xarray BackendArray instead of a dask array, assembling each selection from the cache chunks it overlaps, and the demo registers with explicit chunks. The worker no longer installs dask, which saves 3-6 s of startup (mostly micropip resolving dask and its dependencies from PyPI). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
The guide's Pyodide snippet shows to_dataset(lazy=True), notes that in the browser lazy access needs in-memory or cached data, and the page passes ?wheel= through to the embedded demo for testing local builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
The result grid is a deck.gl BitmapLayer reprojected from latitude and longitude onto a CARTO Positron or Dark Matter basemap, with pan, zoom, and per-cell tooltips. MapLibre and deck.gl load with the first map result rather than at startup. CARTO's raster tiles now need an API key; its vector styles do not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
- docs/how-it-works.md embeds an interactive explainer (docs/explainer/) after Getting started: a Dataset as a table, partitions pruned by SQL filters, and the result back as a Dataset. It runs in the page, with no downloads. "Run in the browser" moves under Concepts. - The demo shows an animated grid of chunks and its three startup steps while Pyodide and the app load. - The browser guide opens a Dataset in its Pyodide example and drops the paragraph about dask. - Point the docs iframes at ../demo/ and ../explainer/: raw HTML URLs are not rewritten, so demo/ resolved under the page's own directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
The How it works explainer now uses real ERA5 2 m temperature: 12:00 UTC on eight days in July 2024, at six latitudes and eight longitudes. make_snapshot.py selects it from ARCO-ERA5 with the code the explainer shows and saves it as a 2.8 KB era5.json, so the page still loads instantly. Step 2 notes that ARCO-ERA5's own chunks (one hour of the whole globe) would let time filters prune but not bounding boxes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
- Retry a run that fails on placeholder fill values (e.g. CAST to INTEGER) instead of surfacing the error, when it missed chunks. - Raise MemoryError when a query re-misses a chunk it already fetched, rather than thrashing until the round limit; drop the limit, since every retry now fetches new chunks. - Serialize Decimal results and NaN nested in lists or structs. - Skip the map when no value is finite or when several rows share a point; map decimal columns. - Slice the body when a server answers a ranged GET with 200. - Fail fast when worker.js cannot fetch a Python module. - Show the explainer's "One point" result as (time,), as to_dataset returns it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h
alxmrs
marked this pull request as ready for review
October 11, 2026 01:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds two pages to the docs site: a live browser demo and a "How it works" explainer.
Run in the browser (
docs/browser.md,docs/demo/)The demo queries NOAA GFS forecasts from dynamical.org's Zarr store with xarray-sql and DuckDB running in Pyodide, in a web worker. There is no server: each query downloads only the chunks that survive xarray-sql's chunk pruning, and maps are drawn with deck.gl over a MapLibre basemap. A loading animation shows the boot steps (Pyodide and packages, xarray-sql, the forecast store).
JSPI can't suspend while DuckDB or pyarrow calls back into Python, so chunks can't be fetched in the middle of a scan.
ChunkCacheworks around this. It records the chunks a query misses and scans fill values in their place, then fetches the missing chunks concurrently withDataset.load_asyncand re-runs the query until a run has no misses. That final run used only real data, so its result is exact. A run that fails on the fill values (e.g. aCASTtoINTEGERover NaN) is retried the same way, and a query that needs more chunks than the cache holds raisesMemoryErrorinstead of thrashing.The guide also shows plain Pyodide usage without dask: open a remote Zarr through a small
fetch-based store,load_asynca slice, query it, and turn the result back into a Dataset withto_dataset.The embedded demo works only after a release ships the Pyodide wheel. It installs xarray-sql from PyPI, and 0.5.0 requires DataFusion, which has no Pyodide wheel, so until the next release micropip fails to install it. The release workflow already uploads the wasm wheel (PEP 783). For local testing,
?wheel=./<wheel>loads a local build (see the guide).How it works (
docs/how-it-works.md,docs/explainer/)An interactive, static walkthrough of the three steps, using a tiny real slice of ARCO-ERA5 2 m temperature (8 days × 6 latitudes × 8 longitudes, saved as
era5.jsonbymake_snapshot.py):to_datasetrepr and a map or line chart of the result.It needs no Python, so it works as soon as the docs deploy.
Testing
smoke.mjsruns every demo example in Node Pyodide against the live store; all pass with a wheel built by the wasm CI recipe. The review's failure cases (integer casts, decimal sums, all-NaN results, duplicate points, NaN in lists) also pass there.Startup
About 15 s cold and 13 s warm on my connection. About 40 MB is downloaded, about 85% of it Pyodide, pyarrow, DuckDB, pandas and numpy. Even with a warm cache, unpacking and compiling the wasm packages takes ~7 s and Python imports plus dataset setup ~6 s. The worker fetches Pyodide's packages in parallel while the runtime boots. The next lever is a Pyodide memory snapshot.
Other changes
flake.nix:wasm-shellgains the shared libraries Playwright's Chromium needs..gitignore: wheels copied intodocs/demo/for local testing.zensical.toml: "How it works" after Getting started; "Run in the browser" under Concepts.Known limits (see the guide):
lead_timefilters don't prune because DuckDB doesn't push downINTERVALpredicates, and dimension-only scans build the full grid.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h