Skip to main content

ZarrCubeVelocityProvider

The ZarrCubeVelocityProvider loads and visualizes 3D vector fields (U, V components) from Zarr datasets as animated particle layers in Cesium using WindLayer from cube-cesium-wind-layer, the NOC-OI fork of the original cesium-wind-layer.

The NOC-OI fork adds cube-aware particle rendering and minVisibleRatio, which clamps camera-driven particle width, trail-length, and speed scaling (0.6 by default, or 1 to retain the overview scale at every zoom). It also resets visible longitude/latitude ranges and pixel scale when zooming back to a globe overview.

This provider enables real-time visualization of:

  • Ocean currents
  • Atmospheric wind fields
  • Depth/height-sliced velocity maps
  • Animated flow patterns through depth

It supports:

  • Zarr v2 and v3
  • Multiscale pyramids
  • Legacy ndpyramid and GeoZarr multiscale layouts
  • GPU-accelerated particle animations
  • Dynamic elevation slicing
  • CF-compliant time decoding for selectors
  • Vertical exaggeration
  • Colormap-based speed visualization
  • Flexible WindLayer configuration

When to Use ZarrCubeVelocityProvider​

Use this provider when you have:

  • Vector fields stored in Zarr
  • 3D volumes representing velocity over depth or height
  • Two variables: u (zonal) and v (meridional)
  • Desire to visualize with animated particles

Typical datasets:

  • Ocean model outputs (NEMO, ROMS, HYCOM)
  • Atmospheric reanalysis winds
  • Climate simulation velocity cubes
  • Any Zarr-based U/V dataset

If you want:


Basic Example​

import { Viewer } from 'cesium';
import { ZarrCubeVelocityProvider } from 'zarr-cesium';

const viewer = new Viewer('cesiumContainer');

const options = {
urls: {
u: 'https://example.com/uo.zarr',
v: 'https://example.com/vo.zarr'
},
variables: { u: 'uo', v: 'vo' },
bounds: { west: -10, south: 30, east: 10, north: 45 }
};
const windCube = new ZarrCubeVelocityProvider(viewer, options);

await windCube.load();

This creates one cube-aware animated particle layer. The layer receives the complete U/V cube and distributes particles across the selected elevation levels.


Options​

interface VelocityOptions {
urls?: { u?: string; v?: string }; // URLs. Each is required unless its store is supplied
stores?: { u?: Readable; v?: Readable }; // Custom U/V stores, including IcechunkStore
variables: { u: string; v: string }; // Zarr array names for U and V
bounds: BoundsProps; // geographic rectangle
latIsAscending?: boolean; // Override latitude array orientation when metadata is incorrect
dimensionNames?: DimensionNamesProps; // Custom dimension names. If not provided, defaults will be used or identified automatically based on CF conventions.
selectors?: Record<string, ZarrSelectorsProps>; // Initial dimension slices
multiscaleLevel?: number; // Index in the metadata's level list; defaults to 0
zarrVersion?: 2 | 3; // Zarr version (auto-detected if not set)
sliceSpacing?: number; // Vertical sampling interval
verticalExaggeration?: number; // Vertical exaggeration factor
elevationMode?: 'height' | 'depth'; // Positive coordinates render above or below sea level
flipElevation?: boolean; // Reverse only the elevation data/index ordering
opacity?: number; // Layer opacity (0–1)
colormap?: ColorMapName; // Name from jsColormaps, based on matplotlib colormaps
scale?: [number, number]; // Min/max for color scaling
windOptions?: Partial<WindLayerOptions>; // Additional WindLayer configuration
crs?: CRS; // Force CRS (auto-detected if not set)
multiscaleFormat?: MultiscaleFormat; // 'auto' (default), 'legacy', or 'geozarr'
requestOverrides?: RequestOverrides; // Static options shared by URL-backed U/V stores
transformRequest?: TransformRequest; // Per-request auth, proxy, or signed URL transform
onAuthError?: OnAuthError; // Called once for HTTP 400/401 responses
}

latIsAscending controls the north/south orientation of rows and is inferred from latitude coordinates by default. It is not a replacement for flipElevation: that option reverses only the vertical placement of elevation data planes without changing their source indices. elevationMode independently selects whether positive coordinates are heights or depths. Use orientation overrides only for datasets whose metadata is absent or wrong.


Icechunk and Custom Stores​

Supply one Zarrita-compatible Readable store for each component. When U and V are arrays in the same Icechunk repository, the same store can be used for both:

import { IcechunkStore } from 'icechunk-js';
import { ZarrCubeVelocityProvider } from 'zarr-cesium';

const store = await IcechunkStore.open(repositoryUrl, {
branch: 'main',
formatVersion: 'v1'
});

const velocity = new ZarrCubeVelocityProvider(viewer, {
stores: { u: store, v: store },
variables: { u: 'uo', v: 'vo' },
bounds: { west: -50, south: -20, east: 10, north: 20 }
});

await velocity.load();

You can mix sources, for example urls.u with stores.v, provided both components have a source.

Private HTTP Stores​

URL-backed U and V stores share request configuration. The transformed fetch and its one-shot authentication callback are reused across both components:

const velocity = new ZarrCubeVelocityProvider(viewer, {
urls: {
u: 'https://data.example.com/private-u.zarr',
v: 'https://data.example.com/private-v.zarr'
},
variables: { u: 'uo', v: 'vo' },
bounds,
transformRequest: async url => ({
url,
headers: { Authorization: `Bearer ${await getAccessToken()}` }
}),
onAuthError: status => refreshSession(status)
});

Use requestOverrides instead when credentials and headers are static.


Loading Data​

await windCube.load();

This loads:

  • u velocity cube
  • v velocity cube
  • Multiscale metadata
  • Dimension values (lon, lat, elevation)
  • Windowed spatial slice (based on bounds)
  • Elevation slice ranges and spacing

Then it creates one cube-aware Cesium WindLayer and adds it to the viewer.

Before rendering, the provider verifies that U and V have identical longitude, latitude, and elevation coordinate grids. A mismatch is rejected because component values cannot be paired reliably. Both components are loaded through the same cube-loading path as ZarrCubeProvider.


How It Renders the Data​

The provider passes the complete elevation-major U/V cube to one WindLayer. Particles are distributed across the enabled elevation levels according to sliceSpacing:

  • sliceSpacing = 1 → particles may occupy every model level
  • sliceSpacing = 2 → particles use every second model level
  • sliceSpacing = n → coarse vertical sampling

The cube contains:

  • a contiguous elevation-major U cube
  • a contiguous elevation-major V cube
  • elevation coordinate values used to compute particle height
  • semantic latitude orientation through latIsAscending

Particles animate based on u/v speed and direction.

WindLayer Integration​

The layer receives a cube windData structure:

{
u: { array: Float32Array, min: -0.5, max: 0.5 },
v: { array: Float32Array, min: -0.5, max: 0.5 },
width,
height,
depth,
elevations,
latIsAscending,
bounds: this.bounds
}

Plus user-configurable particle system settings:

{
speedFactor,
lineWidth,
lineLength,
particlesTextureSize,
minVisibleRatio,
elevationStep,
verticalExaggeration,
elevationMode
...
}

Supported CRS​

Zarr datasets may store coordinate values in:

  • EPSG:4326 (lat, lon degrees)
  • EPSG:3857 (Web Mercator meters)

The provider detects the CRS automatically using:

  • Zarr metadata
  • consolidated metadata
  • coordinate ranges (West/East > 360 → Web Mercator)

For example, you can set the CRS explicitly:

// set CRS to Web Mercator in LayerOptions
crs: 'EPSG:3857';

Multiscale Support​

If the dataset defines Zarr multiscale pyramids, e.g.:

"multiscales": [
{ "datasets": [ {"path": "0"}, {"path": "1"}, {"path": "2"} ] }
]

Then:

  • The provider loads the requested multiscaleLevel
  • multiscaleFormat: 'auto' detects legacy and GeoZarr-style level paths
  • Set multiscaleFormat explicitly if the metadata does not identify its layout
  • multiscaleLevel follows the order stored in the metadata
  • Resolution, W×H×Z, and bounding box adapt
  • Switching levels triggers a reload

For a GeoZarr store whose level 0 is full resolution:

multiscaleFormat: 'geozarr',
multiscaleLevel: 0 // loads the GeoZarr full-resolution level

Legacy ndpyramid stores commonly use the opposite order, with level 0 as the coarsest resolution. Inspect windCube.levelInfos when choosing a level. GeoZarr metadata supplies levels through multiscales.layout[].asset, whereas legacy metadata uses multiscales[0].datasets[].path.


Runtime API​

Update Dimension Selectors​

await windCube.updateSelectors({
selectors: { time: { type: 'index', selected: 5 } },
multiscaleLevel: 1,
bounds: { west: -20, south: 25, east: 20, north: 60 }
});

This reloads the U and V cubes, destroys the old wind layer, and creates a new one.

CF time coordinates are decoded to ISO strings in windCube.dimensionValues. Selectors may therefore use either an array index or a decoded time value, for example { type: 'value', selected: '2020-01-01T00:00:00.000Z' }.

All the parameters are optional. If not provided, the current value is retained.

To get the current selectors, multiscale level, all multiscale levels, and dimensions values, use:

const selectors = windCube.selectors;
const multiscaleLevel = windCube.multiscaleLevel;
const allMultiscaleLevels = windCube.levelInfos;
const dimNames = windCube.dimensionValues;

This returns a mapping of dimension names to their list of values, e.g.:

dimNames = {
time: ['2020-01-01T00:00Z', '2020-01-02T00:00Z', ...],
elevation: [0, 10, 20, 30, ...]
}

And the current selector values, e.g.:

selectors = {
time: { type: 'index', selected: 3 },
depth: { type: 'index', selected: [0, 50] }
};

And the multiscale level information:

levelInfos = ["0", "1", "2", ...];
multiscaleLevel = 1; // index of current level. In this case, that level would be "1"

And with that, you can build UI controls (sliders, dropdowns) to update the layer dynamically.


Update Slice Rendering​

Control vertical sampling or exaggeration:

await windCube.updateSlices({
sliceSpacing: 2,
verticalExaggeration: 8,
elevationMode: 'depth'
});

This:

  • Updates the current cube-aware WindLayer
  • Recomputes heights
  • Applies the new vertical-layout parameters without rereading the cube

All the parameters are optional. If not provided, the current value is retained. Changing elevationMode only changes vertical placement. In height mode, height is elevation * verticalExaggeration; in depth mode, height is (elevation - maximumElevation) * verticalExaggeration. U/V values, latitude, longitude, and elevation-to-data-plane index pairings are unchanged.


Update Style​

Adjust how particles are visualized:

windCube.updateStyle({
opacity: 0.4,
scale: [-1, 1],
colormap: 'plasma',
windOptions: {
particleDensity: 5,
particleScale: 2.0,
maxParticles: 20000
}
});

This applies instantly to the existing layer.

The full list of supported colormaps is available in the Colormaps section.

windOptions allows fine-tuning of particle-system parameters. particleHeight is ignored because height is derived from the Zarr elevation coordinates. For more information, see the WindLayerOptions source in the NOC-OI fork.


Query Velocity Data​

Velocity queries return derived speed alongside aligned U and V components:

const point = await windCube.queryData({
type: 'Point',
coordinates: [-4.2, 50.1]
});

console.log(point.values); // speed
console.log(point.components.u); // U component
console.log(point.components.v); // V component

const profile = await windCube.getVerticalProfile([-4.2, 50.1]);
const series = await windCube.getTimeSeries([-4.2, 50.1]);

Input positions use WGS84 longitude/latitude. A ranged time selector reads the source arrays, while an elevation range returns values across the loaded vertical subset. Pass { signal } to cancel a query.


Remove Layers, Clearing & Destroying​

Remove the velocity layer and release its resources:

windCube.destroy();

Used internally when:

  • Bounds change
  • Multiscale level changes
  • Slice spacing changes
  • Elevation selectors change
  • Vertical exaggeration changes