Skip to content
8 changes: 8 additions & 0 deletions .changeset/render-stack-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@spatialdata/layers": minor
"@spatialdata/vis": minor
---

Add the render stack contract for ordered SpatialData and host-layer rendering, with React viewer adapters for resolving stack entries into Viv/deck output.

Expose richer SpatialCanvas feature-pick events for labels and shapes, including `elementKind`, `spatialElement`, tooltip metadata, and runtime SpatialData context.
61 changes: 61 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SpatialData.js Rendering Context

Canonical language for SpatialData.js rendering, renderer integration, and MDV-facing state work.

## Language

**Render Stack**:
An ordered, serializable description of what should be drawn in a spatial viewport.
_Avoid_: `layerOrder`, `stackOrder`, parallel layer maps

**Stack Entry**:
One ordered item in a **Render Stack**, identified by a stable `id` and split into structural `source` identity plus renderer `props`.
_Avoid_: ad-hoc layer descriptor

**Spatial Entry**:
A **Stack Entry** whose source is a SpatialData element such as an image, shapes, points, or labels element.
_Avoid_: treating every entry as a deck layer

**Host Overlay**:
A **Stack Entry** whose source is owned by the host application and resolved at runtime into one or more deck.gl layers.
_Avoid_: external layer tail, raw saved deck layer

**Group Entry**:
A reserved **Stack Entry** that names ordered children for future blending or aggregation behavior.
_Avoid_: framebuffer layer until the rendering behavior exists

**Resource Resolver**:
The store-agnostic boundary that turns structural **Render Stack** inputs into stable loaded resources for renderers.
_Avoid_: viewer-local cache, periodic snapshotter

**Renderer Adapter**:
The code that turns resolved resources plus entry props into Viv/deck layer instances.
_Avoid_: state store

**Runtime Attachment**:
An unsaved function or object supplied by the host application alongside a **Render Stack**, such as `hostLayerResolver`, `onFeatureHover`, `onFeatureClick`, raw deck handlers, DOM portal targets, or deck layer factories.
_Avoid_: serializable prop, stack entry prop

**MobX Control Island**:
A small MDV/control-layer UI area that edits observable state directly while passing plain values through renderer and third-party boundaries.
_Avoid_: MobX renderer contract

## Relationships

- A **Render Stack** contains zero or more ordered **Stack Entries**.
- A **Spatial Entry** is resolved by a **Resource Resolver** before a **Renderer Adapter** creates Viv/deck output.
- A **Host Overlay** is saved as a descriptor and materialized by the host application at runtime.
- A **Group Entry** may order child entries, but does not yet imply framebuffer or blending behavior.
- A **Runtime Attachment** may observe or materialize stack entries, but is not part of saved **Render Stack** config.
- A **MobX Control Island** may edit MDV state directly, but default SpatialData.js renderer APIs remain plain-object APIs.

## Example Dialogue

> **Dev:** "Can I put MDV scatter between an image and labels?"
> **Domain expert:** "Yes — make the scatter a **Host Overlay** entry in the **Render Stack** and let MDV resolve it into a deck layer."

## Flagged Ambiguities

- "Layer" was used for SpatialData elements, deck.gl layer instances, UI rows, and saved config entries. Resolved: use **Stack Entry** for saved/render order, **Spatial Entry** for SpatialData-backed entries, and deck.gl layer only for runtime renderer output.
- "Snapshot" was used for both persisted config and temporary UI/render state. Resolved: persisted config is a **Render Stack**; live MDV direct-edit areas are **MobX Control Islands** and should not periodically snapshot the whole stack during interaction.
- "Props" was used for both serialized renderer inputs and runtime callback objects. Resolved: `entry.props` must remain serializable renderer input; listeners, factories, portals, and raw deck integration points are **Runtime Attachments**.
5 changes: 5 additions & 0 deletions docs/adr/0001-render-stack-owned-by-layers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Render Stack Owned By Layers

The canonical ordered render description lives in `@spatialdata/layers` as `RenderStack`, while `@spatialdata/vis` adapts that stack into React, Viv, and deck.gl rendering. Host overlays are saved as descriptors and resolved by the host application at runtime, so MDV can interleave scatter, gates, selections, and SpatialData entries without storing raw deck layer instances or reintroducing parallel `layerOrder` / `stackOrder` state.

MobX may be used by MDV-facing control UI, but it is not part of the `@spatialdata/layers` contract or the default renderer API. MobX-controlled panels should be explicit control islands, especially while adopting React Compiler, because observable direct editing and automatic memoization have different assumptions.
22 changes: 16 additions & 6 deletions docs/docs/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ React components and the SpatialCanvas stack (Viv images + deck vectors).
**Two entry modes:**

1. **Batteries included** — `SpatialCanvas` with coordinate-system picker, layer list, properties panels, tooltips.
2. **Headless / controlled** — `SpatialCanvasViewer` or `useSpatialCanvasRenderer`: same render path, **your** state for `layers`, `layerOrder`, `viewState`, plus optional `deckLayers` for app overlays.
2. **Headless / controlled** — `SpatialCanvasViewer` or `useSpatialCanvasRenderer`: same render path, **your** state for `renderStack.entries` and `viewState`, plus host overlay descriptors resolved by your app at runtime.

The vis package is more experimental than `core`/`layers` because of the
Viv/deck/luma dependency stack. The near-term goal is an MDV-consumable
Expand All @@ -154,18 +154,28 @@ that smoke test.
### Headless vis example

```tsx
import { SpatialCanvasViewer, type LayerConfig } from '@spatialdata/vis';
import { SpatialCanvasViewer, type RenderStack } from '@spatialdata/vis';

const renderStack: RenderStack = {
schemaVersion: 1,
entries: [
{ kind: 'spatial', id: 'image', source: { elementType: 'image', elementKey: 'morphology' } },
{ kind: 'host', id: 'deck:selection', source: { hostLayerId: 'deck:selection' } },
{ kind: 'spatial', id: 'shapes', source: { elementType: 'shapes', elementKey: 'cells' } },
],
};

<SpatialCanvasViewer
spatialData={spatialData}
coordinateSystem="global"
layers={layers}
layerOrder={['image', 'shapes']}
renderStack={renderStack}
viewState={viewState}
onViewStateChange={setViewState}
deckLayers={myMdvOverlays}
hostLayerResolver={(entry) =>
entry.source.hostLayerId === 'deck:selection' ? mySelectionLayer : undefined
}
renderTooltip={false}
onShapeHover={handleShapeHover}
onFeatureHover={handleFeatureHover}
/>
```

Expand Down
48 changes: 45 additions & 3 deletions docs/docs/layers/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ sidebar_position: 1

# `@spatialdata/layers`

The **deck.gl–native** package for composable spatial rendering: a top-level **`SpatialLayer`** `CompositeLayer` and a versioned, JSON-serializable **`SpatialLayerProps`** contract.
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
Expand All @@ -17,8 +22,8 @@ The **deck.gl–native** package for composable spatial rendering: a top-level *
|--------|-----------------|
| `@spatialdata/core` | SpatialData on Zarr: elements, transforms, read APIs |
| `@spatialdata/avivatorish` | OME loaders, tile cache, channel stats, Zustand image state |
| `@spatialdata/layers` | How data enters **deck** via `SpatialLayer` + props |
| `@spatialdata/vis` | **SpatialCanvas** React shell: UI ↔ props |
| `@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

Expand Down Expand Up @@ -55,6 +60,43 @@ 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:

```ts
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:
Expand Down
Loading
Loading