Warning
This is a work in progress, and I've had Claude (Opus 4.8) scaffold it for me. Because of that, it might look good (IDK), but it is certainly not complete or and has not been drive-tested in any meaningful sense of the word. Claims about functionality in this README should be considered probable at best, and aspirational at worst. Use at your own caution (whilst this warning is still up. I'll get rid of it once I'm confident in the codebase).
A minimal, read-only xarray metadata layer for zarr v3 / icechunk in the browser.
xarray-ts reinterprets the zarr stores produced by serialising an xarray
Dataset (e.g. to an icechunk store) into an
xarray-shaped object — dimensions, coordinates, data variables, attributes —
that view layers can introspect, while streaming actual slices on demand through
zarrita.
It is not a reimplementation of xarray. It does exactly one job: turn a zarr group into the metadata you need to drive a viewer, and make selecting a slice a one-liner.
icechunk-js / zarrita FetchStore → xarray-ts → your view layer
(bytes, chunks, AsyncReadable) (this lib) (rendering)
npm install xarray-ts zarrita
# optional, for icechunk repos:
npm install icechunk-jsimport { openDataset, fromIcechunk } from "xarray-ts";
const store = await fromIcechunk("https://bucket.s3.amazonaws.com/repo");
const ds = await openDataset(store);
ds.dims; // { time: 365, y: 720, x: 1440 }
ds.attrs; // group-level attributes
ds.coords.time.dates(); // Date[] — CF time axis decoded for you
ds.coords.x.values; // number[] — eagerly loaded
// Stream just the slice you need; coordinates are metadata, data is lazy.
const frame = await ds.get("temperature").isel({ time: 0 }).load();
frame.data; // a (y, x) TypedArray, fetched via zarrita
frame.shape; // [720, 1440]Plain zarr v3 over HTTP works too:
import { openDataset, fromHttp } from "xarray-ts";
const ds = await openDataset(fromHttp("https://example.com/data.zarr"));Any zarrita Readable store is accepted, so openDataset(store) works with
FetchStore, icechunk-js, an in-memory Map, or your own store.
- Dimensions from the zarr v3
dimension_namesmetadata field (synthetic*_dim_*names with a warning if absent). - Coordinates vs data variables using xarray's rule: a variable is a
coordinate if it is named after a dimension, or referenced by some data
variable's
coordinatesattribute. Everything else is a data variable. - Hybrid coordinate loading — dimension coordinates (1-D, named after their
dimension) are small and drive label lookup, so their values are read and cached
up front and
.sel()/coord.valuesstay synchronous. Auxiliary, scalar and N-d coordinates (e.g. curvilinear 2-Dlat(y, x)/lon(y, x)grids) are lazy — not read at open, materialised on demand viaawait ds.coords.lat.load(). Data variables stay lazy too. UseisLazyCoord(coord)to tell the two apart. - CF time decoding — coordinates with
units = "<unit> since <reference>"are decoded following a primitives-plus-sidecar model.coord.valuesstays primitive: epoch milliseconds for standard / proleptic-Gregorian calendars (withDate[]viacoord.dates()), or the raw encoded numbers otherwise (coord.decoded === false). Every recognised CF calendar — including non-standard ones likenoleapand360_day— is additionally decoded into calendar-awarecftime-tsdatetimes viacoord.cftimes(), and can be selected with.sel()using aCFDatetimeor ISO string.coord.calendarnames the resolved calendar.
It deliberately does not apply scale_factor / add_offset / _FillValue
masking, and does not implement computation, alignment, or writing.
DataArray and Dataset both support positional (isel) and label-based
(sel) selection. Both return new lazy views — nothing is fetched until
.load() / .values().
const da = ds.get("temperature");
da.isel({ time: 0 }); // integer index drops the dim
da.isel({ x: { start: 100, stop: 200 } }); // half-open slice keeps the dim
da.sel({ time: new Date("2020-06-01") }); // label lookup via the coordinate
da.sel({ time: someDate }, { method: "nearest" });
da.sel({ y: { start: -40, stop: 40 } }); // inclusive label range
await da.isel({ time: 0 }).load(); // -> { data, shape, stride } (zarrita Chunk)
await da.isel({ time: 0 }).values(); // -> just the TypedArray (or a scalar)
ds.isel({ time: 0 }); // selects across every variable -> new DatasetTo list a group's variables without per-array round-trips, openDataset uses
consolidated metadata when present (a single fetch). xarray and icechunk
write this by default. If a store has no consolidated metadata and cannot list
its own children, pass the names explicitly:
await openDataset(store, { variables: ["time", "x", "y", "temperature"] });This package opens a single group at a path (openDataset(store, { path }))
and returns a flat Dataset. Recursive, nested-group / DataTree traversal is
intentionally out of scope here and owned by a separate library, which plugs
into this one through a clearly defined seam:
datasetFromGroup(group, arrayNames)— the reusable core that turns one already-resolved zarritaGroup(plus the names of its child arrays) into aDataset. A hierarchy walker calls this per node, so the metadata interpretation lives in exactly one place.childArrayNames(contents, groupPath)— given a consolidated-metadata listing, the direct-child array names of a group.GroupNode— the documented tree-node contract ({ path, dataset, children }) the external library produces/consumes.openDatatree()— a stub that throwsNotImplementedErrorpointing here.
import { datasetFromGroup, type GroupNode } from "xarray-ts";| Export | Description |
|---|---|
openDataset(store, opts?) |
Open a zarr v3 store as a Dataset. |
openZarr |
Alias of openDataset. |
fromIcechunk(url, opts?) |
Open an icechunk repo as a store (needs icechunk-js). |
fromHttp(url, opts?) |
Open a plain zarr v3 store over HTTP (FetchStore). |
Dataset |
dims, coords, data_vars, variables, attrs, get, dropVars, pickVars, renameVars, renameDims, setCoords, resetCoords, isel, sel. |
DataArray |
dims, shape, coords, attrs, dtype, rename, isel, sel, load, values. |
Coord |
Eager dimension coordinate: values, dims, attrs, isTime, decoded, calendar, dates(), cftimes(). |
LazyCoord |
Lazy auxiliary / N-d coordinate: dims, attrs, isTime, load(), values(). Narrow with isLazyCoord. |
datasetFromGroup, childArrayNames, GroupNode |
The nested-group seam. |
openDatatree |
Stub (throws NotImplementedError). |
npm test # run the hermetic in-memory unit tests
npm run test:watch # watch mode
npm run typecheck # tsc --noEmit (strict)
npm run build # emit ESM + .d.ts to dist/
npm run check:package # assert the public package surface stays self-contained
# opt-in integration test against a real icechunk repo:
ICECHUNK_TEST_URL=https://… npm testApache 2.0, see LICENSE.
xarray-ts is a TypeScript reimplementation of parts of the xarray data model. It uses Apache-2.0 for license congruence with pydata/xarray, which is also Apache-2.0.
This is an independent project and is not affiliated with or endorsed by the xarray maintainers.