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…360longitude grids - Custom fetch options for authenticated or protected stores
- Smooth integration with Cesium’s
ImageryLayercollection
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:
- 3D rendering → use ZarrCubeProvider
- 3D vector fields → use ZarrCubeVelocityProvider
- 2D tiled imagery using a backend server → use titiler
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:
- Creates the provider
- Loads Zarr metadata
- Detects CRS
- Sets tiling scheme
- Loads the first dimension slice
- 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:
_FillValuemissing_valuevalid_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.).