Skip to main content

ZarrLayerProvider

The ZarrLayerProvider allows you to render 2D raster data from Zarr datasets as a Cesium imagery layer. It handles:

  • Reading Zarr metadata and multiscale pyramids
  • Legacy ndpyramid, GeoZarr, and TopoZarr multiscale layouts
  • WebGL rendering of tiles (fast GPU-based color mapping)
  • Dynamic slicing of dimensions (e.g., time, elevation)
  • CF-compliant time decoding
  • Runtime style updates (colormap, scale, opacity)
  • CRS detection (EPSG:4326 / EPSG:3857)
  • Ascending or descending latitude axes and wrapped 0…360 longitude grids
  • Custom fetch options for authenticated or protected stores
  • Smooth integration with Cesium’s ImageryLayer collection

This provider works as a drop-in replacement for Cesium imagery layers, enabling you to visualize 2D scalar fields (e.g., temperature, salinity, chlorophyll) stored in Zarr format, without any server-side preprocessing or conversion.

When to Use ZarrLayerProvider

Use ZarrLayerProvider when your dataset is:

  • 2D (lat × lon)
  • Optionally has extra dimensions: time, elevation, depth, etc.
  • Stored as a Zarr array, possibly multiscale
  • Represents scalar fields (not vectors)

If you need:


Basic Example

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

const viewer = new Viewer('cesiumContainer');

const options = {
url: 'https://example.com/data.zarr',
variable: 'salinity',
colormap: 'viridis',
scale: [30, 40]
};

const zbLayer = await ZarrLayerProvider.createLayer(viewer, options);

viewer.imageryLayers.add(zbLayer);

This method:

  1. Creates the provider
  2. Loads Zarr metadata
  3. Detects CRS
  4. Sets tiling scheme
  5. Loads the first dimension slice
  6. Returns a ready-to-use ZarrImageryLayer

Options

export interface LayerOptions {
url: string; // Public Zarr store
variable: string; // Zarr array name
latIsAscending?: boolean; // Optional override for latitude ordering (south->north if true)
scale?: [number, number]; // Min/max for color scaling
colormap?: ColorMapName; // Name from jsColormaps, based on matplotlib colormaps
opacity?: number; // Imagery opacity (0–1)
tileWidth?: number; // Cesium tile size (default 256)
tileHeight?: number; // Cesium tile size (default 256)
minimumLevel?: number; // Min zoom level
maximumLevel?: number; // Max zoom level
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
zarrVersion?: 2 | 3; // Zarr version (auto-detected if not set)
crs?: 'EPSG:4326' | 'EPSG:3857'; // Force CRS (auto-detected if not set)
noDataMin?: number; // Custom no-data minimum value. Overrides _FillValue/missing_value.
noDataMax?: number; // Custom no-data maximum value. Overrides _FillValue/missing_value.
requestOverrides?: RequestInit; // Fetch options for protected stores, headers, credentials, etc.
multiscaleFormat?: MultiscaleFormat; // Metadata layout; defaults to 'auto'.
}

latIsAscending is optional. If omitted, the provider infers latitude orientation from the coordinate values loaded at startup.

Time coordinates with CF units such as hours since 2000-01-01 00:00:00 are exposed as ISO date strings in dimensionValues. This allows a time selector to use either an index or a decoded value.


Dimension Selectors

If your Zarr dataset has dimensions like: time, level related (e.g. depth, elevation), and others, you can slice them using the selectors option.

You can set an initial slice using:

// LayerOptions
selectors: {
time: { type: 'index', selected: 0 },
elevation: { type: 'index', selected: 10 }
}

Or slice by value instead of index:

// LayerOptions
selectors: {
time: { type: 'value', selected: '2020-01-01T00:00Z' },
elevation: { type: 'value', selected: 50 } // meters
}

The provider automatically converts value→index using nearest-neighbor lookup. The default behavior (if no selectors are provided) is to select the first index (0) for each dimension.


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';

Regular global EPSG:4326 grids using either -180…180 or 0…360 longitudes are supported. A global 0…360 domain is exposed to Cesium as full-world coverage and tile requests are mapped back to the wrapped source coordinates. Latitude orientation is inferred automatically; use latIsAscending only when the coordinate metadata is missing or incorrect.


Multiscale Support

If the dataset contains Zarr multiscale metadata (generated by ndpyramid):

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

The provider automatically:

  • chooses the best level based on Cesium zoom
  • caches levels (LRU = 3)
  • correctly slices each level
  • uses metadata-supported shapes for accurate scaling

By default, multiscaleFormat: 'auto' detects level paths from either the original ndpyramid-style metadata or GeoZarr metadata. Set the format explicitly for GeoZarr/TopoZarr pyramids so automatic zoom selection also uses their full-to-coarse level ordering:

const options = {
// ...
multiscaleFormat: 'geozarr' // 'auto' | 'legacy' | 'geozarr' | 'topozarr'
};

GeoZarr and TopoZarr use a layout array with asset paths instead of the legacy datasets array with path values. They also commonly place full resolution at level 0, the opposite of the legacy layout. With multiscaleFormat: 'geozarr' or 'topozarr', the provider maps that ordering correctly when selecting a level for a Cesium zoom.

For example, GeoZarr-style metadata is shaped like:

{
"multiscales": {
"layout": [
{ "asset": "0" },
{ "asset": "1" },
{ "asset": "2" }
]
}
}

Protected Stores and Request Options

Use requestOverrides to pass standard RequestInit options to requests made for the Zarr store. This is useful for credentials or authorization headers:

const options = {
url: 'https://example.com/protected.zarr',
variable: 'temperature',
requestOverrides: {
credentials: 'include',
headers: { Authorization: `Bearer ${token}` }
}
};

No-data Handling

Automatically applies:

  • _FillValue
  • missing_value
  • valid_min / valid_max
  • or custom range

Example of custom range:

// LayerOptions
noDataMin: -9999, // defaults is to Zarr attributes and then -9999
noDataMax: 9999 // defaults is to Zarr attributes and then 9999

Runtime API

When added to Cesium, the layer behaves like a normal ImageryLayer, plus extra Zarr-specific features.

const zbLayer = await ZarrLayerProvider.createLayer(viewer, options);
viewer.imageryLayers.add(zbLayer);

Update Style (colormap, scale and opacity)

  • colormap
zbLayer.updateStyle({ colormap: 'plasma' });

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

  • scale range
zbLayer.updateStyle({ scale: [20, 35] });
  • opacity
zbLayer.updateStyle({ opacity: 0.5 });
// or using Cesium ImageryLayer API
// zbLayer.alpha = 0.5;

After updating style, the layer performs a soft refresh so Cesium re-renders the tiles in the viewport with the new style.

Update dimension slicing

zbLayer.updateSelectors({
time: { type: 'index', selected: 5 }
});

You can get the list of current dimension and current selectors values using:

const dimValues = zbLayer.provider.dimensionValues;
const selectors = zbLayer.provider.selectors;

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 selectors:

selectors = {
time: { type: 'index', selected: 5 },
elevation: { type: 'index', selected: 2 }
};

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

Remove Layer

To remove the layer and free resources:

viewer.imageryLayers.remove(zbLayer);
zbLayer.destroy();

It is important to call destroy() after removing the layer from the viewer, because this cleans up all internal resources (abort pending requests, etc.).