Skip to main content

@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 renders shapes sublayers. Images, labels and points are composed by @spatialdata/vis from the layers below.
  • RenderStack — ordered saved/render state for SpatialData entries, host overlay descriptors, and reserved group entries. This replaces public dependence on parallel layers maps plus layerOrder arrays.
  • spatialLayerPropsSchema / migrateSpatialLayerProps — runtime validation and migrations for saved views, URL state, and integrators (e.g. MDV chart config).
  • Renderers — createShapesDeckLayer, LabelsLayer, and PointsLayer with its PointsDataEngine.

Relationships​

PackageResponsibility
@spatialdata/coreSpatialData on Zarr: elements, transforms, read APIs, the Resource Resolver
@spatialdata/avivatorishOME loaders, tile cache, channel stats, Zustand image state
@spatialdata/layersCanonical render-stack schema and deck renderers for shapes, labels and points
@spatialdata/visSpatialCanvas 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-table runtime 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-resolved rowIndexByFeatureIndex into 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-geoarrow cannot 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:

  • featureColorEncoding holds what is common — the auto/categorical/ continuous mode decision, resolveCategoricalPalette(), numeric ramps, and the missing-value policy — plus FeatureColorBuffer, the precomputed-RGBA currency both kinds render from.
  • shapeColorEncoding and labelColorEncoding are the thin per-kind surfaces: buildShapeFillColorByFeatureId() and buildLabelFillColorByFeatureId().
  • 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.