@spatialdata/layers
The deck.gl–native package for composable spatial rendering: a canonical
ordered RenderStack contract, a top-level SpatialLayer
CompositeLayer, and versioned, JSON-serializable SpatialLayerProps.
Role
SpatialLayer— validates props and will orchestrate Viv / scatter / shapes sublayers as they are ported from@spatialdata/visand MDV-style stacks.RenderStack— ordered saved/render state for SpatialData entries, host overlay descriptors, and reserved group entries. This replaces public dependence on parallellayersmaps pluslayerOrderarrays.spatialLayerPropsSchema/migrateSpatialLayerProps— runtime validation and migrations for saved views, URL state, and integrators (e.g. MDV chart config).
Relationships
| Package | Responsibility |
|---|---|
@spatialdata/core | SpatialData on Zarr: elements, transforms, read APIs |
@spatialdata/avivatorish | OME loaders, tile cache, channel stats, Zustand image state |
@spatialdata/layers | Canonical render-stack schema and how data enters deck via SpatialLayer + props |
@spatialdata/vis | SpatialCanvas React/Viv adapter: render stack ↔ resources ↔ viewport |
API tiers
See also the visualization overview: deck-only integrators can use @spatialdata/layers without React; headless loader usage is @spatialdata/avivatorish; full UI is @spatialdata/vis.
Status
SpatialLayer now includes the first shared shapes path:
- shapes rendering is owned primarily at the
@spatialdata/layerstier rather than@spatialdata/vis - the current shipping path is mixed-mode: polygon fallback for JS/WKB payloads and a shared columnar backend branch for
geoarrow-tableruntime data - row alignment, picking, and tooltip resolution are now driven by stable feature identity plus shared row-index metadata
buildShapeFillColorByFeatureId()turns a table column plus already-resolvedrowIndexByFeatureIndexinto per-feature colour maps; it deliberately does not resolve SpatialData table associations itself- the public shapes config stays representation-agnostic so a stronger future
deck.gl-geoarrowbackend can slot in without changing saved props
For shapes, the important contract is stable feature identity plus table-join-driven styling/filtering. @spatialdata/core loads render-oriented shape data, @spatialdata/layers turns that into deck layers, and @spatialdata/vis consumes the shared behavior for viewer use.
Labels now follow that same contract. The colour-encoding core is shared rather than duplicated per kind:
featureColorEncodingholds what is common — theauto/categorical/continuousmode decision,resolveCategoricalPalette(), numeric ramps, and the missing-value policy — plusFeatureColorBuffer, the precomputed-RGBA currency both kinds render from.shapeColorEncodingandlabelColorEncodingare the thin per-kind surfaces:buildShapeFillColorByFeatureId()andbuildLabelFillColorByFeatureId().- Shapes index colour by feature index; labels have no geometry, so
buildLabelColorLut()produces a lookup table indexed by the label's own integer instance id that the bitmask fragment shader samples. Either way a feature-state change re-uploads only the small table, never the tiles.
The default categorical palette is DEFAULT_FEATURE_CATEGORICAL_PALETTE
('oklab'), whose colour is a pure function of the category index and therefore
cannot repeat — see the
behaviour-change note.
Points should follow the same package split before we make a GeoArrow migration load-bearing:
@spatialdata/corediscovers SpatialData points stores and exposes coordinate columns, stable point ids, row-index alignment, and Arrow batches/vectors when available@spatialdata/layersowns the deck-facing points renderer, including filtering/styling feature state and backend choice- the initial backend can remain
ScatterplotLayerover typed coordinate arrays, but the public points config should be representation-agnostic enough to add@geoarrow/deck.gl-geoarrow/GeoArrowScatterplotLayerwhen point data is GeoArrow-encoded or cheaply adaptable
The migration test is the same as shapes: MDV or Vitessce should be able to drive hide/fade/color/radius state without knowing whether the renderer used JS arrays, deck binary attributes, or GeoArrow batches underneath.
Render stack contract
The public saved/render order is renderStack.entries, not a separate
layerOrder array:
import type { RenderStack } from '@spatialdata/layers';
const renderStack: RenderStack = {
schemaVersion: 1,
entries: [
{
kind: 'spatial',
id: 'image-morphology',
source: { elementType: 'image', elementKey: 'morphology_focus' },
props: { opacity: 0.8 },
},
{
kind: 'host',
id: 'deck:scatter',
source: { hostLayerId: 'deck:scatter' },
},
{
kind: 'group',
id: 'group:future-blend',
children: ['image-morphology', 'deck:scatter'],
props: { blendMode: 'reserved' },
},
],
};
source holds structural identity. props holds renderer inputs. Resource
resolvers and deck/Viv updateTriggers decide what actually invalidates IO,
geometry, attributes, or tile data; do not add a second visual-vs-structural
prop registry.
React-agnostic usage
@spatialdata/layers has no React dependency. A minimal deck-only path:
import {
buildShapesPrebuiltData,
createShapesDeckLayer,
buildShapeFillColorByFeatureId,
} from '@spatialdata/layers';
import {
createFeatureTableAlignment,
loadFeatureRowIndexByFeatureIndex,
} from '@spatialdata/core';
// After loading renderData + table column from core:
const fillColorByFeatureId = buildShapeFillColorByFeatureId({
featureIds: renderData.featureIds,
rowIndexByFeatureIndex: alignment.rowIndexByFeatureIndex,
column: obsColumn,
mode: 'categorical',
alpha: 200,
});
const layer = createShapesDeckLayer(
renderData,
{
kind: 'shapes',
elementKey: 'cell_shapes',
visible: true,
featureState: { fillColorByFeatureId },
},
{ id: 'shapes', prebuilt: buildShapesPrebuiltData(renderData) }
);
For Viv images and async SpatialData layer orchestration without the
SpatialCanvas UI, use @spatialdata/vis headless APIs
(Headless viewer guide) or compose
createShapesDeckLayer output with your own Deck / DeckGL stack.