@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 rendersshapessublayers. Images, labels and points are composed by@spatialdata/visfrom the layers below.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).- Renderers —
createShapesDeckLayer,LabelsLayer, andPointsLayerwith itsPointsDataEngine.
Relationships
| Package | Responsibility |
|---|---|
@spatialdata/core | SpatialData on Zarr: elements, transforms, read APIs, the Resource Resolver |
@spatialdata/avivatorish | OME loaders, tile cache, channel stats, Zustand image state |
@spatialdata/layers | Canonical render-stack schema and deck renderers for shapes, labels and points |
@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.
Shapes, labels and points
Shapes rendering lives in this package rather than in @spatialdata/vis:
- polygon fallback for JS/WKB payloads and a columnar branch for
geoarrow-tableruntime data - row alignment, picking, and tooltip resolution are 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 is representation-agnostic, so the backend can change
without changing saved props.
@geoarrow/deck.gl-geoarrowcannot do dynamic per-feature hide/fade, so shapes stay on our own binary layer over a GeoArrow data plane
For shapes, the 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 follow the same split. @spatialdata/core loads and tiles the data
(Loading points and shapes) and its PointsResolver
owns the cache and lifecycle. PointsDataEngine and PointsLayer here own the
deck-facing renderer: feature filtering and colour-by-feature, resident and
viewport-tiled render strategies, and picking. Per-layer runtime state such as
the hovered feature rides the engine rather than the saved config.
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. The
Resource Resolver 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.