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 will orchestrate Viv / scatter / shapes sublayers as they are ported from @spatialdata/vis and MDV-style stacks.
  • 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).

Relationships

PackageResponsibility
@spatialdata/coreSpatialData on Zarr: elements, transforms, read APIs
@spatialdata/avivatorishOME loaders, tile cache, channel stats, Zustand image state
@spatialdata/layersCanonical render-stack schema and how data enters deck via SpatialLayer + props
@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.

Status

SpatialLayer now includes the first shared shapes path:

  • shapes rendering is owned primarily at the @spatialdata/layers tier 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-table runtime 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-resolved rowIndexByFeatureIndex into 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-geoarrow backend 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:

  • 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 should follow the same package split before we make a GeoArrow migration load-bearing:

  • @spatialdata/core discovers SpatialData points stores and exposes coordinate columns, stable point ids, row-index alignment, and Arrow batches/vectors when available
  • @spatialdata/layers owns the deck-facing points renderer, including filtering/styling feature state and backend choice
  • the initial backend can remain ScatterplotLayer over typed coordinate arrays, but the public points config should be representation-agnostic enough to add @geoarrow/deck.gl-geoarrow / GeoArrowScatterplotLayer when 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.