Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions docs/docs/vis/mdv-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ sidebar_position: 3

# MDV integration roadmap

This page explains architecture and phased context. For the branch-level shipping checklist, see [MDV release checklist](./mdv-release-checklist).

This library is intended to align with **[MDV](https://github.com/Taylor-CCB-Group/MDV)** and **[Vitessce](https://vitessce.io/)** so both projects can render SpatialData images, labels, shapes, and points from shared packages instead of relying on diverging local implementations.

The near-term target is not a full replacement for every MDV spatial feature. The target is a baseline `SpatialCanvas`-backed MDV chart that can stand in for `VivScatterComponent` where appropriate, accept MDV-controlled state, compose MDV custom deck.gl layers, and avoid showing this repo's demo/editor UI inside MDV. MDV is the first "use it in anger" sanity check; Vitessce compatibility remains a priority design target rather than a later afterthought.
Expand All @@ -24,6 +26,10 @@ The blocker for MDV use is mostly API shape. `SpatialCanvas` currently owns a fu

The top priority is a headless viewer path. Before MDV or Vitessce integration work gets clever, this repository should prove that the renderer can be driven by external state and external controls.

The first implementation should use a **bridge path** rather than a full layers-first rewrite: expose a public React `SpatialCanvasViewer` that reuses the existing Viv/deck rendering stack, accepts externally controlled state, and composes caller-provided deck.gl layers above SpatialData-rendered layers. This keeps MDV progress unblocked while preserving a migration path toward `@spatialdata/layers`.

The layers-first direction is still likely the cleaner long-term architecture, especially for MDV and Vitessce. For now, `@spatialdata/layers` should mature through narrow renderer slices rather than becoming the blocking path for headless mode: the current `SpatialLayer` package defines a schema/contract, but real image, labels, shapes, and points sublayer factories still need to be ported deliberately.

Useful in-repo validation demos:

- [ ] `demo/headless-basic`: render a fixed SpatialData image/labels/shapes stack with all props owned by the demo component, no `SpatialCanvas` sidebars.
Expand All @@ -44,7 +50,7 @@ Acceptance signal: the demos should import the same public API that MDV would us
- optional: `@spatialdata/avivatorish`
- [ ] Decide whether MDV first consumes via local `npm link` / packed tarballs / workspace path / published prerelease.
- [ ] Treat this repo as the place where Viv/deck/luma versions are selected. MDV should follow those versions, not constrain them.
- [ ] Track the next Viv/deck upgrade as a first-class migration. Viv PR [hms-dbmi/viv#924](https://github.com/hms-dbmi/viv/pull/924) is currently open and targets deck.gl `9.2.9`, uniform-buffer-backed shader props, `model.shaderInputs`, and variable channel counts.
- [ ] Track upgrading to `@hms-dbmi/viv@0.21.0` / deck.gl `9.2.9` as a first-class migration. Viv PR [hms-dbmi/viv#924](https://github.com/hms-dbmi/viv/pull/924) is merged there (uniform-buffer-backed shader props, `model.shaderInputs`, and variable channel counts).
- [ ] Update shader extensions here and in MDV in the same wave:
- replace deprecated `setUniforms` paths with `model.shaderInputs.setProps(...)`
- prefer `updateState()` for shader input updates where appropriate
Expand Down Expand Up @@ -365,7 +371,7 @@ Open questions:
## Compatibility risks

- **Viv/deck version skew:** MDV uses Viv `0.19.x`; this repo uses Viv `0.20.x`. Mixed deck/luma packages can fail in subtle WebGL ways.
- **Upcoming Viv/deck migration:** Viv PR `924` moves shader props toward UBOs and `model.shaderInputs` while targeting deck.gl `9.2.9`. Custom shader extensions in this repo and MDV should be migrated deliberately, not patched piecemeal.
- **Viv/deck migration (Viv `0.21.0` / PR `#924`):** Merged upstream in `@hms-dbmi/viv@0.21.0` with deck.gl `9.2.9`; shader props use UBOs and `model.shaderInputs`. This repo and MDV still need to adopt that stack and migrate custom shader extensions deliberately, not piecemeal.
- **Arrow/Parquet churn:** loaders.gl Parquet and deck.gl-community Arrow layers are promising but still moving targets. We should avoid locking public `core` APIs to one loader implementation too early.
- **Raster backend churn:** `deck.gl-raster` may absorb image/Zarr responsibilities that currently sit in Viv or custom labels layers. Public props should leave room for a backend swap.
- **Image format/codecs uncertainty:** OME-TIFF, JP2K-compressed TIFF, OME-Zarr, SpatialData Zarr, and future Zarr image codecs may all matter. Avoid encoding one storage format too deeply into viewer props.
Expand Down
204 changes: 204 additions & 0 deletions docs/docs/vis/mdv-release-checklist.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
---
sidebar_position: 4
---

# MDV release checklist

Goal for this PR branch: release a `@spatialdata/vis` version that MDV can consume now, without replacing Viv, but using this in-place where possible.

Scope constraints:

- keep Viv as the raster/image foundation
- keep this repo focused on SpatialData-oriented rendering/library contracts
- ensure MDV can compose its own deck layers and state cleanly alongside this library
- avoid app-specific abstractions leaking into `@spatialdata/*` packages

## What "ready for MDV" means

A version is MDV-ready when MDV can embed `SpatialCanvasViewer` as a headless renderer while preserving existing chart behavior from:

- `~/code/www/MDV/src/react/components/VivScatterComponent.tsx`
- `~/code/www/MDV/src/react/scatter_state.ts`
- `~/code/www/MDV/src/react/spatial_context.tsx` (current replacement for expected `SpatialLayers.ts`)

## Integration contract for this release

Use this as the contract boundary between this repo and MDV.

### MDV-controlled state (primary model)

MDV is the **source of truth** for viewer state. `SpatialCanvasViewer` is a **controlled renderer**: it accepts MDV state and produces deck/Viv output; it does not own chart config or selection semantics.

MDV passes and updates (from chart / datastore / MobX):

- `spatialData`, `coordinateSystem`
- `layers` (`Record<string, LayerConfig>`) — visibility, opacity, per-layer style, and (for shapes v1) filter/colour driven by table rows
- `layerOrder`
- `viewState` + `onViewStateChange`
- optional `deckLayers` / `deckProps` for overlays MDV builds directly

When MDV selection or colour mappings change, MDV updates `layers` (and/or `deckLayers`) and React re-renders the viewer. This repo loads SpatialData geometry/images and maps **MDV’s layer config** to deck.gl layers.

### What `@spatialdata/vis` owns

- SpatialData-backed discovery and async loading (images, shapes, labels)
- Viv image path and labels bitmask path
- Turning **caller-supplied** `LayerConfig` into deck layers (`useLayerData` / renderers)
- Composing generated layers with caller `deckLayers` / `deckProps.layers`
- Stable picking/tooltip hooks keyed by layer id (for MDV tooltip portals)

### What MDV owns (v1)

- Chart config and app state (MobX / zustand)
- Filtering and highlighting semantics (same as table rows today)
- Scatter, gates, contours, ROI JSON, and other bespoke deck layers
- View linking (`useViewStateLink`), outer tooltip (`useOuterContainerDeckTooltip`)
- Viv **extensions** on image channels (passed through layer config / `deckProps`; regressions here are bugs)

### v1 layer scope

| Layer kind | v1 |
|------------|----|
| Images | Yes (Viv) |
| Shapes | Yes — MDV drives style/filter via `layers` state |
| Labels | Render when configured; segmentation vs shapes experiment is not a release gate |
| Points | v1.1 |

### Custom deck layers (additive, not a replacement for state)

MDV can still pass layers it constructs entirely in app code (for example from `spatial_context.ts`) via `deckLayers` or `deckProps.layers`. Those sit **above** SpatialData-generated layers in the composed stack.

Over time, patterns that repeat in MDV (shared shape styling, common overlays) may move into `@spatialdata/vis` or `@spatialdata/layers`; v1 does not require that migration.

### Shapes v1: filter and colour via `layers` state

For v1, shapes must support the same *effect* as MDV table-row colouring/filtering, without MDV re-implementing SpatialData loading:

- [ ] `ShapesLayerConfig` accepts MDV-driven per-feature style (accessors or equivalent state on the config object MDV updates when selection changes)
- [ ] Feature index / `instance_key` alignment with the associated table is stable for picking and styling (same join used for tooltips)
- [ ] Document that MDV updates `layers[layerId]` when datastore selection or colour columns change

Optional later: MDV-only `PolygonLayer` built from exported geometry helpers; not required if config-driven styling is sufficient.

### Bespoke layer extension path

- Primary extension for **state-owned** SpatialData layers: MDV-controlled `layers` + `layerOrder`
- Primary extension for **fully custom** deck layers: `deckLayers` / `deckProps.layers` on `SpatialCanvasViewer`
- Composition order: SpatialData-generated layers, then MDV `deckLayers`, with scale bar last (see `composeSpatialDeckLayers`)

Practical checks for this branch:

- [ ] MDV can drive a shapes layer only by updating `layers` props (no `SpatialCanvas` zustand store)
- [ ] An MDV-style custom layer from `spatial_context.ts` can be supplied via `deckLayers` without importing `packages/vis/src/*` internals

## Release checklist (this branch)

### 1) API and export sanity

- [ ] `@spatialdata/vis` exports `SpatialCanvasViewer` as public API.
- [ ] Viewer helper exports are public and documented:
- `useSpatialCanvasRenderer`
- `composeSpatialDeckLayers`
- `shouldAutoFitSpatialView`
- `shouldRenderInternalTooltip`
- [ ] `SpatialViewer` supports passthrough `deckProps` safely.
- [ ] No MDV integration requires importing from `packages/vis/src/...` internals.

### 2) Headless behavior parity checks

- [ ] `SpatialCanvasViewer` renders with externally controlled:
- `coordinateSystem`
- `layers`
- `layerOrder`
- `viewState`
- [ ] It composes caller deck layers above SpatialData-rendered layers.
- [ ] It allows MDV-owned tooltip flow (internal tooltip disabled or overridden).
- [ ] It supports MDV-owned controller settings through `deckProps`.

### 3) MDV touchpoint checks

- [ ] `VivScatterComponent.tsx` style embedding is possible without sidebars/editor UI from `SpatialCanvas`.
- [ ] `scatter_state.ts`-style scatter/filter/highlight layers can be passed as external deck layers.
- [ ] `spatial_context.tsx`-style selection and editable overlay layers can be passed through unchanged.
- [ ] View ID and layer ID conventions remain compatible with MDV filtering assumptions (current `getVivId(...)` patterns).

### 4) Version and packaging hygiene

- [ ] `pnpm -r --filter @spatialdata/vis test` passes.
- [ ] package build succeeds for `@spatialdata/vis` and dependent workspace packages.
- [ ] produce installable package(s) for MDV testing (pack or prerelease).
- [ ] smoke-test install in MDV workspace and verify viewer renders.

### 5) Non-goals (explicit)

- [ ] No attempt to replace Viv in this release.
- [ ] No in-repo implementation of broad GIS support in this release.
- [ ] No hard coupling of `core` API to deck-specific loaders for this release.

## Viv extensions and near-term Viv upgrade

With Viv PR [#924](https://github.com/hms-dbmi/viv/pull/924) merged in `@hms-dbmi/viv@0.21.0` (deck.gl `9.2.9`, uniform-buffer-backed shader props, `model.shaderInputs`, variable channel counts), we should treat extension compatibility as a release gate when adopting that stack.

### Why this matters now

- MDV relies on Viv extensions and deck composition behavior today.
- Extension breakage can look subtle (missing colormap behavior, stale shader props, or dropped extension props during cloning/spreading).
- The move from deprecated uniform paths to shader-input paths can break custom layers/extensions if we do not audit deliberately.

### Branch checklist for Viv-upgrade readiness

- [ ] Audit `@spatialdata/vis` Viv paths for extension prop preservation:
- `packages/vis/src/SpatialCanvas/VivSpatialViewer.tsx`
- ensure layer cloning/composition does not drop non-enumerable extension/default props.
- [ ] Audit channel-count assumptions and constants:
- `packages/avivatorish/src/constants.ts`
- `packages/vis/src/SpatialCanvas/ImageChannelPanel.tsx`
- `packages/vis/src/SpatialCanvas/LabelsChannelPanel.tsx`
- ensure behavior is coherent if Viv supports wider channel ranges.
- [ ] Audit shader/uniform update paths in custom deck/Viv-adjacent layers:
- `packages/layers/src/LabelsBitmaskTileLayer.ts`
- confirm `shaderInputs.setProps(...)` path is primary and any `setUniforms(...)` usage is fallback-only.
- [ ] Verify app-level extension passthrough remains intact with `deckProps` and `SpatialCanvasViewer`.
- [ ] Run visual smoke tests with extension-heavy image views (2D at minimum) before publishing package versions.

### Fast acceptance criteria

- [ ] No regression in extension-driven rendering for image layers (including opacity/channel state updates).
- [ ] No regression in composed app overlays when Viv layers are present.
- [ ] No obvious channel-limit regressions introduced by mismatched constants.
- [ ] No hard dependency on deprecated uniform-only update patterns in custom layers/extensions.

## Suggested MDV smoke-test sequence

1. Install branch build of `@spatialdata/vis` into MDV.
2. Add a minimal wrapper chart that renders `SpatialCanvasViewer` with existing MDV view state.
3. Pass current MDV overlays from `VivScatterComponent.tsx` as `deckProps.layers`.
4. Route tooltip through MDV's outer-container tooltip path only.
5. Validate pan/zoom linking and selection behavior parity.
6. Validate at least one labels/shapes overlay above image.

## Follow-up work after release

After this MDV-ready release, pursue incremental improvements in separate PRs:

- formalize stable layer identity contract for cross-layer picking/highlighting
- broaden feature-property contract beyond tooltip fields
- improve points styling/filtering parity with MDV needs
- evaluate optional Arrow-friendly data paths without forcing deck dependencies into `core`

## Future consideration: editable layers and write support

**Not in v1.** v1 keeps selection/annotation as **MDV-owned deck layers** passed through `deckLayers` (for example editable ROI / lasso layers from `spatial_context.ts`), with this repo focused on **read** paths into SpatialData.

Later, consider implementing here (and/or in `@spatialdata/core`) when write APIs exist:

- **Editable overlay layers** — drawing, editing vertices, selection geometries synced with chart state (today MDV implements these; candidates to lift into `@spatialdata/vis` if the interaction model stabilizes).
- **Annotation persistence** — round-trip edits to SpatialData-compatible stores (shapes tables, labels, or sidecar layers), not only ephemeral deck state.
- **General write support** — align with `@spatialdata/core` moving beyond read-only (see [core overview](../core/overview.mdx)); vis would expose mutation helpers only once core defines safe, tested write contracts.

Design constraints to decide before building:

- Whether edits target **Zarr/SpatialData on disk**, **in-memory session state**, or **MDV’s own project files**.
- How editable layers share **picking ids** and **coordinate systems** with read-only SpatialData layers.
- Whether Vitessce/MDV both need the same editable-layer API or app-specific extensions remain sufficient.

38 changes: 38 additions & 0 deletions packages/vis/src/Sketch/demoUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/** Default SpatialData Zarr URL for the vis demo / docs Sketch embed. */
export const DEFAULT_DEMO_SPATIALDATA_URL =
'https://storage.googleapis.com/vitessce-demo-data/spatialdata-august-2025/visium_hd_3.0.0.spatialdata.zarr';

/**
* Read `?url=` from a query string (e.g. for MDV links into the demo).
* Returns `fallback` when the param is missing or blank.
*/
export function getSpatialDataUrlFromSearchParams(
searchParams: URLSearchParams,
fallback: string = DEFAULT_DEMO_SPATIALDATA_URL
): string {
const raw = searchParams.get('url');
if (raw == null) {
return fallback;
}
const trimmed = raw.trim();
if (trimmed === '') {
return fallback;
}
try {
return decodeURIComponent(trimmed);
} catch {
return trimmed;
}
}

/** Build a demo page href with the given SpatialData store URL in `?url=`. */
export function buildDemoPageHref(
spatialDataUrl: string,
base: string | URL = typeof window !== 'undefined'
? window.location.href
: 'http://127.0.0.1:5173/'
): string {
const page = new URL(base);
page.searchParams.set('url', spatialDataUrl);
return page.href;
}
48 changes: 41 additions & 7 deletions packages/vis/src/Sketch/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useState, type CSSProperties } from 'react';
import { useEffect, useState, type CSSProperties } from 'react';
import { SpatialDataProvider, useSpatialData } from '@spatialdata/react';
import SpatialDataTree from '../Tree';
import Table from '../Table';
import ImageView from '../ImageView';
import Transforms from '../Transforms';
import SpatialCanvas from '../SpatialCanvas';

const defaultUrl =
'https://storage.googleapis.com/vitessce-demo-data/spatialdata-august-2025/visium_hd_3.0.0.spatialdata.zarr';
import {
DEFAULT_DEMO_SPATIALDATA_URL,
buildDemoPageHref,
getSpatialDataUrlFromSearchParams,
} from './demoUrl';

const dataSourceBarStyle: CSSProperties = {
flexShrink: 0,
Expand All @@ -16,21 +18,53 @@ const dataSourceBarStyle: CSSProperties = {
background: '#1e1e1e',
};

function getInitialDemoUrl(): string {
if (typeof window === 'undefined') {
return DEFAULT_DEMO_SPATIALDATA_URL;
}
return getSpatialDataUrlFromSearchParams(new URLSearchParams(window.location.search));
}

function DataSource({ children }: React.PropsWithChildren) {
const [url, setUrl] = useState(defaultUrl);
const [url, setUrl] = useState(getInitialDemoUrl);

useEffect(() => {
const nextHref = buildDemoPageHref(url);
if (window.location.href !== nextHref) {
window.history.replaceState(null, '', nextHref);
}
}, [url]);

const shareHref =
typeof window !== 'undefined'
? buildDemoPageHref(url, `${window.location.origin}${window.location.pathname}`)
: '';

const source = url.trim() || DEFAULT_DEMO_SPATIALDATA_URL;

return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
<div style={dataSourceBarStyle}>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>SpatialData URL</div>
<div style={{ fontSize: 12, color: '#888', marginBottom: 4 }}>
SpatialData URL <span style={{ color: '#666' }}>(or open with ?url=…)</span>
</div>
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
style={{ width: '100%', boxSizing: 'border-box', padding: '6px 8px' }}
/>
{shareHref ? (
<a
href={shareHref}
style={{ display: 'inline-block', marginTop: 6, fontSize: 11, color: '#8af' }}
>
Link to this dataset
</a>
) : null}
</div>
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
<SpatialDataProvider source={url}>{children}</SpatialDataProvider>
<SpatialDataProvider source={source}>{children}</SpatialDataProvider>
</div>
</div>
);
Expand Down
Loading
Loading