ZarrLayer: support band/channel-last arrays ([y, x, band]) — multiscale GeoZarr RGB(A) pyramids fail validateSpatialDimOrder
Summary
ZarrLayer requires the two spatial dimensions to be the last entries in an
array's dimension_names. A GeoZarr store that lays its data out as
[lat, lon, band] (spatial first, channel/band last) — a natural layout for a
pre-rendered RGB(A) pyramid, where each pixel's RGBA is contiguous — cannot be
rendered. prepare/_parseZarr throws before any tile loads.
This blocks an otherwise-valid, georeferenced, multiscale GeoZarr pyramid whose
only "problem" is channel-last axis order.
Environment
@developmentseed/deck.gl-zarr 0.6.1 (monorepo developmentseed/deck.gl-raster)
- zarrita v3 store, consolidated metadata
Reproduction
Public store (Zarr v3, no auth):
https://tessera-embeddings.s3.us-west-2.amazonaws.com/v1.1/cambridge.zarr
The renderable layer is the multiscale RGB pyramid subgroup global_rgb/:
- Root group attrs declare the
zarr-conventions/multiscales v1 layout
(multiscales: { layout: [{ asset: "0", transform: { scale, translation } }, …] },
levels 0=native … 9=coarsest) plus the spatial:/proj: conventions
(proj:code: "EPSG:4326", spatial:bbox: [-180, -90, 180, 90]).
- Each level is an array at
global_rgb/<level>/rgb with:
dimension_names: ["lat", "lon", "band"]
shape: [H, W, 4] (e.g. level 0 = [1800000, 3600000, 4], level 9 = [3515, 7031, 4])
dtype: uint8, chunk_shape: [512, 512, 4]
Minimal driver (pass the global_rgb group as node, with synthesized GeoZarr
metadata whose spatial:dimensions = ["lat", "lon"] and one layout entry
per level pointing at <level>/rgb):
new ZarrLayer({
node: globalRgbGroup, // zarr.Group
metadata: synthesizedGeoZarr, // multiscales.layout → "<level>/rgb", spatial:dimensions ["lat","lon"]
selection: { band: null }, // band is the only non-spatial dim
getTileData, // would upload [H, W, 4] uint8 as rgba8unorm
renderTile,
});
What happens
_parseZarr() → validateSpatialDimOrder() throws:
Spatial dims must be the last two entries in dimension_names:
expected ["lat", "lon"] at positions [1, 2], got ["lon", "band"].
Transpose is not implemented yet.
Even if that check were relaxed, a second assumption blocks it — tile sizes are
read from the trailing dims in zarr-layer.js:
const chunkSizes = arrays.map((arr) => ({
width: arr.chunks[arr.chunks.length - 1], // = 4 (band), should be 512 (lon)
height: arr.chunks[arr.chunks.length - 2], // = 512 (lat), should be 512
}));
So the tiler would derive a 4-px-wide tile from the band chunk.
Why it matters
[y, x, band] (channel-last) is a common interleaved layout for RGB(A) imagery —
each pixel's channels are contiguous, which is exactly what a GPU rgba8unorm
texture upload wants (no transpose, no per-channel gather). A producer writing a
pre-rendered web pyramid will reach for it. Today such stores are unrenderable
through ZarrLayer regardless of being fully GeoZarr-compliant.
Expected
ZarrLayer should render a georeferenced multiscale array whose spatial dims are
not the last two — at minimum the common channel-last RGB(A) case
([y, x, band]).
Possible directions
- Honor
spatial:dimensions positionally. Use the declared spatial-dim
indices (yAxisIndex/xAxisIndex) to drive both the sliceSpec and the
chunkSizes (arr.chunks[yAxisIndex] / arr.chunks[xAxisIndex]) instead of
assuming len-2 / len-1. The non-spatial dims (and their order) then fall
out of selection, and getTileData receives the chunk in the store's native
axis order — the consumer can upload it as-is.
- Implement the transpose the error message already anticipates ("Transpose
is not implemented yet") — at least for a trailing channel dim.
- If neither is in scope short-term, a documented escape hatch (e.g. let the
caller supply chunkSizes / a tile-shape override and skip
validateSpatialDimOrder) would unblock channel-last sources.
Code pointers (0.6.1)
dist/validation.js → validateSpatialDimOrder (the throw)
dist/zarr-layer.js → _parseZarr, chunkSizes = arr.chunks[len-1] / [len-2]
Workaround
None through ZarrLayer without rewriting the store to [band, y, x]. The
alternative is to bypass ZarrLayer and drive RasterTileLayer with a
hand-built AffineTileset descriptor, doing the channel-last chunk upload in a
custom getTileData.
ZarrLayer: support band/channel-last arrays ([y, x, band]) — multiscale GeoZarr RGB(A) pyramids failvalidateSpatialDimOrderSummary
ZarrLayerrequires the two spatial dimensions to be the last entries in anarray's
dimension_names. A GeoZarr store that lays its data out as[lat, lon, band](spatial first, channel/band last) — a natural layout for apre-rendered RGB(A) pyramid, where each pixel's RGBA is contiguous — cannot be
rendered.
prepare/_parseZarrthrows before any tile loads.This blocks an otherwise-valid, georeferenced, multiscale GeoZarr pyramid whose
only "problem" is channel-last axis order.
Environment
@developmentseed/deck.gl-zarr0.6.1 (monorepodevelopmentseed/deck.gl-raster)Reproduction
Public store (Zarr v3, no auth):
The renderable layer is the multiscale RGB pyramid subgroup
global_rgb/:zarr-conventions/multiscalesv1 layout(
multiscales: { layout: [{ asset: "0", transform: { scale, translation } }, …] },levels
0=native …9=coarsest) plus thespatial:/proj:conventions(
proj:code: "EPSG:4326",spatial:bbox: [-180, -90, 180, 90]).global_rgb/<level>/rgbwith:dimension_names: ["lat", "lon", "band"]shape: [H, W, 4](e.g. level 0 =[1800000, 3600000, 4], level 9 =[3515, 7031, 4])dtype: uint8,chunk_shape: [512, 512, 4]Minimal driver (pass the
global_rgbgroup asnode, with synthesized GeoZarrmetadatawhosespatial:dimensions=["lat", "lon"]and onelayoutentryper level pointing at
<level>/rgb):What happens
_parseZarr()→validateSpatialDimOrder()throws:Even if that check were relaxed, a second assumption blocks it — tile sizes are
read from the trailing dims in
zarr-layer.js:So the tiler would derive a 4-px-wide tile from the band chunk.
Why it matters
[y, x, band](channel-last) is a common interleaved layout for RGB(A) imagery —each pixel's channels are contiguous, which is exactly what a GPU
rgba8unormtexture upload wants (no transpose, no per-channel gather). A producer writing a
pre-rendered web pyramid will reach for it. Today such stores are unrenderable
through
ZarrLayerregardless of being fully GeoZarr-compliant.Expected
ZarrLayershould render a georeferenced multiscale array whose spatial dims arenot the last two — at minimum the common channel-last RGB(A) case
(
[y, x, band]).Possible directions
spatial:dimensionspositionally. Use the declared spatial-dimindices (
yAxisIndex/xAxisIndex) to drive both thesliceSpecand thechunkSizes(arr.chunks[yAxisIndex]/arr.chunks[xAxisIndex]) instead ofassuming
len-2/len-1. The non-spatial dims (and their order) then fallout of
selection, andgetTileDatareceives the chunk in the store's nativeaxis order — the consumer can upload it as-is.
is not implemented yet") — at least for a trailing channel dim.
caller supply
chunkSizes/ a tile-shape override and skipvalidateSpatialDimOrder) would unblock channel-last sources.Code pointers (0.6.1)
dist/validation.js→validateSpatialDimOrder(the throw)dist/zarr-layer.js→_parseZarr,chunkSizes = arr.chunks[len-1] / [len-2]Workaround
None through
ZarrLayerwithout rewriting the store to[band, y, x]. Thealternative is to bypass
ZarrLayerand driveRasterTileLayerwith ahand-built
AffineTilesetdescriptor, doing the channel-last chunk upload in acustom
getTileData.