feat(wasm): examples/wasm-viewer, an HDF5/NetCDF-4 viewer page

Drop a file (or pass ?file=<url>&path=<object>), browse the tree lazily,
see a dataset's type, shape, max shape and attributes, and page through
its values as 50x12 hyperslab windows (leading dims of 3-D+ data held
at chosen indices). build.sh produces pkg/ (not committed) with
wasm-bindgen --target web and checks the CLI matches the crate version.

test/run.sh builds it and runs test.mjs under Node against the h5py/
netCDF4 fixture (250 checks: every dataset whole and as a strided
hyperslab, listings, attributes, error paths, the page's DOM-free
helpers), then browser.sh renders the page in headless Chromium for
eight objects and checks the DOM. The fixture gains LZ4 (read) and Zstd
(refused: links C) datasets and a compound attribute (value null plus
its type). ci-test.sh runs it when node and wasm-bindgen exist; the CI
container has neither, so CI relies on the native h5py_interop test.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 00:05:34 -05:00
co-authored by Claude Opus 5.5
parent a42b646689
commit 1abd93e0f8
11 changed files with 800 additions and 1 deletions
+17
View File
@@ -22,6 +22,11 @@ import h5py
import netCDF4
import numpy as np
try: # registers the LZ4/Zstd filters with libhdf5; optional
import hdf5plugin
except ImportError:
hdf5plugin = None
# netCDF4 1.7 trips numpy 2.5's shape-setting deprecation on assignment.
warnings.filterwarnings("ignore", category=DeprecationWarning)
@@ -37,6 +42,8 @@ with h5py.File(h5, "w") as f:
f.attrs["scale"] = np.array([0.5, 2.0])
f.attrs["big"] = np.uint64(2**63 + 5)
f.attrs.create("vlen_note", "héllo", dtype=h5py.string_dtype())
# No plain JavaScript form: listed with value null and its type.
f.attrs["origin"] = np.array((1.5, 2), dtype=[("x", "<f8"), ("n", "<i4")])
f.create_dataset(
"grid", data=np.arange(60, dtype="<f8").reshape(6, 10) / 4,
chunks=(4, 3), compression="gzip", shuffle=True,
@@ -62,6 +69,12 @@ with h5py.File(h5, "w") as f:
"cube", data=np.arange(2 * 5 * 6, dtype="<i4").reshape(2, 5, 6),
chunks=(1, 2, 3), compression="gzip",
)
if hdf5plugin is not None:
# LZ4 is built into clawhdf5-wasm; Zstd links C and is not.
f.create_dataset("lz4", data=np.arange(40, dtype="<i4"), chunks=(10,),
**hdf5plugin.LZ4())
f.create_dataset("zstd", data=np.arange(40, dtype="<i4"), chunks=(10,),
**hdf5plugin.Zstd())
comp = np.zeros(2, dtype=[("x", "<f8"), ("n", "<i4")])
f.create_dataset("table", data=comp)
g = f.create_group("sensors")
@@ -123,6 +136,8 @@ def entry(ds, slab=None):
def attr(v):
v = np.asarray(v) if not isinstance(v, (str, bytes)) else v
if isinstance(v, np.ndarray) and v.dtype.names:
return {"raw": "compound"}
if isinstance(v, bytes):
return {"string": v.decode()}
if isinstance(v, str):
@@ -160,6 +175,8 @@ def describe(path):
walk(key.rstrip("/") + "/" + n, o)
elif obj.dtype.names:
expected["errors"][key] = "compound"
elif key == "/zstd":
expected["errors"][key] = "unsupported filter: 32015"
else:
expected["datasets"][key] = entry(obj, slab_for(obj))