Skip to content

Docs: a live browser demo and a How it works explainer - #266

Merged
alxmrs merged 13 commits into
mainfrom
browser-demo
Oct 11, 2026
Merged

alxmrs merged 13 commits into
mainfrom
browser-demo

Conversation

@alxmrs

@alxmrs alxmrs commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

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. ChunkCache works around this. It records the chunks a query misses and scans fill values in their place, then fetches the missing chunks concurrently with Dataset.load_async and 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. a CAST to INTEGER over NaN) is retried the same way, and a query that needs more chunks than the cache holds raises MemoryError instead of thrashing.

The guide also shows plain Pyodide usage without dask: open a remote Zarr through a small fetch-based store, load_async a slice, query it, and turn the result back into a Dataset with to_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.json by make_snapshot.py):

  1. A Dataset is a table: a heatmap with chunk boundaries linked to the 384-row table and its partitions.
  2. SQL reads only the chunks it needs: five example queries, with a partition cube showing which partitions are read or pruned.
  3. Results come back as a Dataset: the to_dataset repr and a map or line chart of the result.

It needs no Python, so it works as soon as the docs deploy.

Testing

  • smoke.mjs runs 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.
  • Headless Chromium at desktop and phone widths, light and dark: the demo standalone and inside the built docs site, and every explainer step.

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-shell gains the shared libraries Playwright's Chromium needs.
  • .gitignore: wheels copied into docs/demo/ for local testing.
  • zensical.toml: "How it works" after Getting started; "Run in the browser" under Concepts.

Known limits (see the guide): lead_time filters don't prune because DuckDB doesn't push down INTERVAL predicates, and dimension-only scans build the full grid.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyN7idACeDMh94qrnFMu6h

@alxmrs alxmrs changed the title Browser demo: SQL over remote Zarr with DuckDB in Pyodide Browser demo on the docs site: SQL over remote Zarr with DuckDB in Pyodide Oct 10, 2026

@alxmrs alxmrs left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Feedback for the app.

Comment thread docs/demo/chunk_cache.py
Comment thread docs/demo/index.html Outdated
@alxmrs
alxmrs force-pushed the browser-demo branch 2 times, most recently from 67a7236 to ee57c29 Compare October 10, 2026 04:01

@alxmrs alxmrs left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Feedback, overall looks great.

Comment thread docs/browser.md Outdated
Comment thread docs/browser.md Outdated
Comment thread docs/demo/index.html

@alxmrs alxmrs left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

New idea.

Comment thread zensical.toml Outdated
Base automatically changed from wasm-support to main October 10, 2026 22:57
alxmrs and others added 11 commits October 10, 2026 15:58
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
- 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
alxmrs and others added 2 commits October 10, 2026 17:09
- 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
alxmrs marked this pull request as ready for review October 11, 2026 01:22
@alxmrs alxmrs changed the title Browser demo on the docs site: SQL over remote Zarr with DuckDB in Pyodide Docs: a live browser demo and a How it works explainer Oct 11, 2026
@alxmrs
alxmrs merged commit e9921c5 into main Oct 11, 2026
13 checks passed
@alxmrs
alxmrs deleted the browser-demo branch October 11, 2026 01:36
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.

1 participant