@fxi/zartigl
v0.2.15
Published
MapLibre GL JS plugin for animated particle flow from Zarr ocean current data
Maintainers
Readme
zartigl
A MapLibre GL JS plugin for exploring cloud-hosted Zarr geoscience data directly in the browser.
Zartigl renders scalar rasters and vector particle fields from multidimensional Zarr stores without a dedicated tile server. It is aimed at expert analysis workflows where time, depth or pressure level, point inspection, and reproducible map state matter as much as the visual layer.
Features
- Browser-side Zarr v2 access with chunk-aware loading and LRU caching
- Scalar raster rendering with palettes, opacity, log scale, and value inspection
- Vector rendering with GPU particle advection and optional raster magnitude display
- Time and vertical-dimension controls for depth, elevation, level, or pressure-like axes
- Point queries for time series and vertical profiles when a point-series store is available
- Catalog-backed datasets with automatic Zarr/WMTS backend selection for scalar layers
- MapLibre custom layer integration, including Mercator and globe rendering paths
- Pre-rendered GeoVideo backends with true polar globe draping and portable masks
- No server component required for public CORS-enabled stores
Quick Start
npm install @fxi/zartigl maplibre-glimport maplibregl from "maplibre-gl";
import { Zartigl } from "@fxi/zartigl";
import { catalog } from "@fxi/zartigl/catalog";
const map = new maplibregl.Map({
container: "map",
style: "https://demotiles.maplibre.org/style.json",
center: [0, 20],
zoom: 2,
});
const z = new Zartigl({
id: "zarr-layer",
map,
catalog,
backend: "auto",
// Optional: expose only the month ending at the latest available timestamp.
timeRange: { trailing: "P1M" },
geoVideo: { autoplay: false, loop: true, playbackRate: 1 },
});
await z.setLayer("ocean-current-velocity");
z.setTimeAndDepth(new Date("2025-01-01T00:00:00Z"), 0);
z.updateSettings({
palette: "rdylbu",
opacity: 0.9,
});setLayer() resolves after the field store's consolidated metadata and coordinate axes have loaded. Metadata getters are therefore authoritative after the awaited call, including exact values for irregular time axes.
The root import does not bundle the catalog presets. Import catalog data from @fxi/zartigl/catalog only when you want the built-in catalog.
Demo
The production demo is the default Vite app and is published at
fxi.io/zartigl.
npm install
npm run devBuild the same app that the public demo publishes:
npm run build:prodFor the smaller public API demo:
npm run dev:minimalThe demos read the built-in catalog, which points to public Copernicus Marine ARCO stores. No local data download is needed; visible chunks are fetched directly from cloud object storage or, for some scalar layers, from WMTS when that is the safer backend.
Public API
The main entry point is the catalog-backed Zartigl facade:
const time = z.getTimeMeta();
const depth = z.getDepthMeta();
const legend = z.getLegend();
z.on("loaded", (meta) => {
console.log(meta.min, meta.max, meta.unit);
});
z.on("status", (status) => {
// metadata → fetching → rendering → ready, or blocked/error
console.log(status.phase);
});
// GeoVideo uses native media playback and reports scientific time updates.
z.on("timeChange", (time) => console.log(new Date(time)));
z.on("playbackChange", (playing) => console.log({ playing }));
z.setPlaybackRate(2);
z.setLoop(false);
// Embedding applications can stop rendering and abort field requests while
// retaining the latest requested time/depth for one reload on resume.
z.suspend();
z.setTime(new Date("2025-02-01T00:00:00Z"));
z.resume();
const series = await z.queryTimeSeries({
longitude: 7.4,
latitude: 46.9,
depth: 0,
maxPoints: 256,
});New layers initially select the latest advertised timestamp that is not in the
future. Forecast timestamps remain available through getTimeMeta().values.
Missing remote chunks are interpreted as Zarr fill data. A wholly unavailable
frame is not cached, so selecting it again can recover after an upstream store
finishes publishing it; a renderable sparse frame is cached normally.
VectorLayer, ScalarLayer, ArcoLayer, and ZarrSource remain exported for advanced use cases that need direct control over renderer internals. Ordinary MapLibre integrations should prefer Zartigl.
Alternatives And Project Fit
Zartigl overlaps with several useful projects. The goal is not to replace them, but to cover a specific MapX/ARCO workflow: public cloud Zarr stores, MapLibre integration, animated vector particles, scalar rasters, time/depth controls, point inspection, and reproducible widget configuration without a dedicated tile server.
The table below is a fit matrix based on documented project scope. "Not specified" means the feature is not a stated focus in the referenced project, not that it is impossible.
| Project | Best fit | Zarr raster | Vector particles | Time/depth UI | Query API | Polar/globe rendering | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- |
| zartigl | MapX-ready ARCO-like ocean layers | Yes, Zarr v2 | Yes | Built in | Time series and vertical profiles when a point-series store is available | MapLibre globe/ECEF path for lon/lat scalar and vector layers, including over-pole views | Catalog defaults, WMTS fallback for selected scalar layers, MapX snippet generation |
| Copernicus Marine MyOcean Pro | Copernicus end-user exploration | Service-managed | Product-dependent | Application UI | Product UI, not a reusable library API | Service-managed | Reference viewer, not a MapLibre/WebGL library to embed in MapX |
| @carbonplan/zarr-layer | General Zarr raster layers for MapLibre/Mapbox | Yes, Zarr v2/v3 | No, raster-focused | Selector API | GeoJSON queries | Full polar coverage documented for MapLibre with suitable untiled datasets | Strong closest alternative for raster Zarr; supports custom stores, CRS reprojection, custom fragment shaders |
| @carbonplan/maps | React maps for prepared multidimensional rasters | Yes, prepared Zarr pyramids | No | React selector workflow | Not its main focus | Web Mercator-focused | Higher-level React framework; its core MapLibre path is documented as Web Mercator-only |
| zarr-gl | Lightweight Zarr layer for Mapbox/MapLibre | Yes | No | Limited | Not specified | Not specified | The README now recommends using @carbonplan/zarr-layer instead |
| maplibre-gl-wind | Wind particle layer for deck.gl/MapLibre | No | Yes | No | No | Not specified | Takes wind textures or point data, not Zarr metadata/chunks |
The closest general alternative is @carbonplan/zarr-layer. It has a broader documented scope for raster Zarr rendering: Zarr v2/v3, custom stores, CRS reprojection, custom fragment shaders, GeoJSON queries, and full polar coverage in MapLibre for suitable untiled datasets. If the requirement were only "draw a multidimensional Zarr raster on a MapLibre map", using or contributing to @carbonplan/zarr-layer would be a strong option.
Zartigl remains useful because it combines pieces that are not currently covered together by those projects:
- ARCO-oriented catalog entries and defaults for Copernicus Marine products.
- U/V vector products rendered as animated particles, with optional raster magnitude in the same layer.
- Time and vertical-axis metadata exposed as application controls.
- Depth/elevation labeling, ordering, and point inspection tuned for ocean products.
- MapLibre globe rendering for both scalar and vector fields, including polar scenes beyond the usual Web Mercator
85.0511°latitude limit. - GPU particle-state fallbacks for browser and hardware differences, including float, half-float, and
RGBA8compatibility mode. - A facade API that can generate MapX widget snippets and keep widget state reproducible.
deck.gl, Three.js, or @carbonplan/zarr-layer could still be useful integration targets. They would not remove the ARCO catalog, time/depth metadata, vector particle simulation, point-query workflow, or MapX widget-state work that zartigl currently owns.
Catalog Metadata
To validate the built-in catalog:
npm run catalog:validateTo add or update catalog entries:
uv run scripts/catalog_builder/skills/list_layers.py
uv run scripts/catalog_builder/skills/search_products.py wave
uv run scripts/catalog_builder/skills/query_dataset.py <dataset_id>
uv run scripts/catalog_builder/skills/validate_catalog.pyThis requires Python >= 3.12, uv, and a free Copernicus Marine account.
GeoVideo
GeoVideo pre-renders an expensive scalar Zarr timeline into a short, streamable H.264 MP4 while retaining spatial bounds, scientific dates, provenance, units, palette, and color domain in a sidecar manifest. Version 2 transports quantized scalar values in luminance and a static lossless mask as a separate PNG, so the browser can apply palette and scalar styling in WebGL. It is a visualization backend only: point queries always use the authoritative Zarr point-series store.
Unlike MapLibre's native four-corner video source, GeoVideoLayer maps an
equirectangular scalar-luma texture through zartigl's subdivided lon/lat mesh.
Globe mode therefore reaches both poles. A separate lossless image supplies the
static validity mask without doubling the video width.
Displaying a GeoVideo
The built-in catalog includes an Arctic sea-ice thickness animation. Select the
GeoVideo backend explicitly and enable autoplay when creating the Zartigl
instance:
import maplibregl from "maplibre-gl";
import { Zartigl } from "@fxi/zartigl";
import { catalog } from "@fxi/zartigl/catalog";
const map = new maplibregl.Map({
container: "map",
style: "https://demotiles.maplibre.org/style.json",
center: [-132.454826, 85.051129],
zoom: 0.511,
});
map.setProjection({ type: "globe" });
const z = new Zartigl({
map,
catalog,
backend: "geovideo",
geoVideo: {
autoplay: true,
loop: true,
playbackRate: 1,
},
});
await z.setLayer("sea-ice-thickness");backend: "geovideo" selects the published GeoVideo manifest from the
catalog's sea-ice-thickness entry. Globe projection demonstrates its polar
coverage. loop and playbackRate are optional playback settings. Browsers
may restrict autoplay, so embedding applications should provide a user-gesture
fallback when necessary.
Try the configured instance directly in the Arctic sea-ice GeoVideo example.
uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json --dry-run
uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json
uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json --uploadThe Arctic sea-ice configuration covers June 2022 through August 2026 and is
available at
scripts/geovideo/examples/sea-ice-thickness-arctic.json.
Generation requires FFmpeg. Upload credentials remain in .env; no private
key is bundled into the demo or written to a manifest. See
scripts/geovideo/README.md for the configuration
and smoke-test workflow.
Dataset Scope
The built-in catalog focuses on Copernicus Marine ARCO products, including ocean currents and scalar ocean/ice variables. Other public cloud-native Zarr v2 stores can be used when their store URLs and variables are described in a compatible catalog entry and runtime compatibility is verified. Time, vertical, spatial, and variable metadata are loaded live from consolidated Zarr metadata and coordinate chunks.
Good fits include:
- Ocean currents, Stokes drift, and wave fields
- Sea-ice drift and ice-related scalar products
- Atmospheric wind or pressure-level products
- Climate model outputs with time and vertical dimensions
- Hydrodynamic model rasters or vectors
Vector layers need either U/V component variables or a catalog derivation from direction and magnitude. Scalar layers need one numeric variable and enough coordinate metadata to locate chunks by longitude, latitude, time, and optional vertical dimension.
Catalog validation checks the JSON contract, not remote service behavior. For built-in catalog additions, use the ARCO-oriented catalog-builder scripts first. WMTS is only an optional scalar shortcut; for polar products, prefer Zarr unless WMTS coverage is verified to reach the poles.
How It Works
1. Zarr Is A Multidimensional Raster Cube
Think of a Cloud Optimized GeoTIFF extended into longitude, latitude, time, and depth or pressure. The data is split into chunks. Zartigl requests only the chunks that intersect the current viewport at the selected time and vertical coordinate, similar to a COG tile request.
2. Chunks Become GPU Textures
Scalar chunks are normalized into a raster texture and colorized by the selected palette. By default the color domain follows the loaded frame; scalar layers can instead set colorDomain: [min, max] to keep colors comparable across viewports and dates. Vector chunks are converted into a two-channel field texture, with validity carried as a mask.
For vector U/V data:
R = eastward component
G = northward component
A = valid data mask3. Particles Are Virtual Drifters
For vector particle rendering, thousands of virtual drifters are seeded across the viewport. Every frame, the GPU samples the vector texture, moves each particle, and respawns particles that leave the valid domain.
Particle state lives in GPU textures using a ping-pong framebuffer pattern, so positions do not need to round-trip through CPU arrays each frame. Positions are stored at full float precision where the GPU supports it (falling back to 16-bit packing), which keeps trails crisp when zoomed in. Motion is zoom-compensated so a single speed setting yields roughly constant visual speed from low to high zoom.
Trail history currently remains a screen-space texture. During camera movement it is faded aggressively rather than cleared every frame, which avoids flicker while limiting displaced trail artifacts on both Mercator and globe projections. A world-aligned or reprojected trail buffer is a possible experimental direction, but globe reprojection and polar continuity require additional work.
4. Queries Use The Same Source Logic
Point inspection, time-series sampling, and vertical-profile sampling reuse the same Zarr metadata, coordinate lookup, chunk decoding, and cache machinery as the renderer. The query API is intentionally separate from the visual interaction layer, so applications can build their own analysis UI.
Build And Release Checks
npm test
npm run build:prod
npm run build:lib
npm run catalog:validatenpm run release:check runs the main validation path before publishing.
License
MIT
