MDV integration roadmap
This page explains architecture and phased context. For the branch-level shipping checklist, see MDV release checklist.
This library is intended to align with MDV and Vitessce 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 2D 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. 3D orbit/volume rendering is out of scope for this integration path. MDV is the first "use it in anger" sanity check; Vitessce compatibility remains a priority design target rather than a later afterthought.
Current state
@spatialdata/vis exports both the full SpatialCanvas UI and a headless
SpatialCanvasViewer (plus useSpatialCanvasRenderer, composeSpatialDeckLayers, and related helpers) from the public package entry. The rendering path matches MDV's existing MDVivViewer pattern:
- image layers are rendered through Viv
DetailView - extra deck.gl layers are composed above images
- non-image spatial layers use the same deck.gl view model
- shape, point, and labels renderers exist
- shapes support table-driven fill colour and per-feature state on
ShapesLayerConfig @spatialdata/coreexposesFeatureTableAlignment/createFeatureTableAlignment()@spatialdata/layersowns shape deck rendering andbuildShapeFillColorByFeatureId()
Remaining MDV blockers are packaging and polish, not the absence of a headless component:
- publish and smoke-test the npm
nextprerelease in MDV - tooltip/pick row resolution should finish converging on the shared core resolver
- points layer is minimal scatter only (v1.1 for MDV parity)
Viv/deck stack: this repo pins @hms-dbmi/viv@0.21.0 and deck.gl 9.2.9. MDV's
matching upgrade is almost ready to merge — treat shared Viv/deck/luma versions as
effectively aligned for the first @spatialdata/vis smoke test. Residual risk is
extension-specific visual regression, not version skew between the two codebases.
SpatialCanvas still owns a full UI shell for demos. MDV should embed SpatialCanvasViewer and drive view state, layers, style, filtering, highlighting, and custom overlays from chart/data-store state.
Priority: headless first
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 (see Headless viewer guide):
-
demo/headless: localblobs.zarrfixture viaSpatialCanvasViewer(noSpatialCanvassidebars). -
demo/headless-leva: external control panel variant of the above. -
demo/headless-custom-layers: pass arbitrary deck.gl layers into the viewer, matching the way MDV will pass scatter, gates, contours, and ROI JSON. -
demo/headless-controlled-view: keep view state fully controlled by an outer component, including programmatic pan/zoom/reset and saved/restored state. -
demo/headless-tooltips: disable internal tooltip UI and route picking into an externally owned tooltip renderer.
Acceptance signal: demos should import the same public API MDV uses (SpatialCanvasViewer from @spatialdata/vis). That bar is met; the gap is example apps, not missing exports.
Phase 0: rendering stack and package sanity
- Confirm package names, entry points, and exports for local MDV consumption:
@spatialdata/core@spatialdata/react@spatialdata/layers@spatialdata/vis- optional:
@spatialdata/avivatorish - lower-level dependency:
zarrextra
- 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.
- Upgrade to
@hms-dbmi/viv@0.21.0/ deck.gl9.2.9in this repo (Viv PR hms-dbmi/viv#924: uniform-buffer-backed shader props,model.shaderInputs, variable channel counts). - MDV Viv/deck/luma upgrade to the same stack — almost ready to merge; treat as done for integration planning.
- Residual extension audit on both sides after MDV merge (shader-input paths, channel-count assumptions, extension prop passthrough through
deckProps/SpatialCanvasViewer). - Run a clean
pnpm buildand pack the packages, then install them into~/code/www/MDV. - Add one minimal MDV smoke chart that imports the package and renders a known fixture before attempting a full chart replacement.
Phase 1: headless SpatialCanvas API
Mostly landed. @spatialdata/vis exports SpatialCanvasViewer as a separate controlled component (not a mode prop on SpatialCanvas).
Current surface:
<SpatialCanvasViewer
spatialData={spatialData}
coordinateSystem={coordinateSystem}
renderStack={renderStack}
viewState={viewState}
onViewStateChange={setViewState}
hostLayerResolver={resolveMdvHostLayer}
deckProps={deckProps}
renderTooltip={false}
onFeatureHover={onFeatureHover}
/>
Implementation status:
- Split viewer core (
SpatialCanvasViewer/useSpatialCanvasRenderer) fromSpatialCanvasUI shell. - Export
SpatialCanvasViewerfrom public@spatialdata/visentry. - Controlled
coordinateSystem,renderStack, andviewState. - Host overlay descriptors plus
hostLayerResolverfor MDV scatter, gates, contours, ROI overlays. - Deprecated compatibility path for
layers,layerOrder, anddeckLayers. -
showLoadingOverlayandrenderTooltip={false}for external tooltip ownership. -
onFeatureHover/onFeatureClickfor shape and label picking, withonShapeHover/onShapeClickkept as compatibility callbacks. - Stable externally supplied Viv/deck view id or layer id suffix (
getVivIdcompatibility). - Viv extension passthrough:
ImageLayerConfig.vivLayerProps,vivImageExtensionResolver,vivImagePropsResolveronSpatialCanvasViewer(see Issue #56 APIs below).
See Headless viewer guide for experimentation steps. The /headless demo (packages/vis/demo) wires MDV-style ColorPaletteExtension + VivContrastExtension through vivImageExtensionResolver; brightness/contrast sliders feed vivImagePropsResolver (or saved vivLayerProps).
Issue #56 channel and extension APIs
Tracked in GitHub issue #56. These APIs let MDV replace its local MobX↔Zustand channel bridge.
Channel config (Phase 1 ADR)
- Struct-of-arrays on
renderStack.entries[*].props.channels:channelIds,colors,contrastLimits,channelsVisible,selections. - Array-of-structs deferred to vNext.
ChannelConfigdoes not carry extension-specific fields. Host apps own extension schema.
@spatialdata/avivatorish
useLayerChannelState({ config, defaults, layerId, onChannelsChange })— echo-safe layer-local channel adapter with stablechannelIds, add/remove, parallel-array sync.getChannelSelectionStats/getSingleSelectionStats({ includeRaster: true })— optional 2D raster{ width, height, data }for histogram UI.
@spatialdata/vis
useImageLayerContext(elementKey)— per-element loader, defaults, OMEchannelNames(insideSpatialCanvasViewer).useLayerChannelStatere-exported from@spatialdata/visfor single-package MDV imports.ImageChannelPanelremains internal toSpatialCanvasfor now (not exported from@spatialdata/vis); MDV should useuseLayerChannelStatewith its own UI.
Viv extension passthrough (extension-agnostic)
Serializable on image stack entry props:
props: {
channels: { /* core Viv channel fields only */ },
vivLayerProps: { brightness: [0.5], contrast: [0.5] }, // host-owned keys
}
Runtime on SpatialCanvasViewer:
<SpatialCanvasViewer
vivImageExtensionResolver={({ layerId, elementKey, channelCount, loader, channels }) => [
new ColorPaletteExtension(),
new VivContrastExtension(),
]}
vivImagePropsResolver={({ channelCount }) => ({
brightness: toneBrightness,
contrast: toneContrast,
})}
/>
Merge order into detailView.getLayers({ props }): saved vivLayerProps → vivImagePropsResolver → extensions from vivImageExtensionResolver (or global vivImageExtensions). VivSpatialViewer must not spread layer.props afterward.
Phase 2: MDV adapter chart
Create a new MDV React chart alongside VivMdvRegionReact rather than replacing it in place.
Possible MDV chart name: SpatialDataMdvRegionReact.
Adapter responsibilities:
- derive
source/ SpatialData store URL from MDV project state - select the active coordinate system from MDV region metadata
- map MDV chart config to
renderStack.entries - map MDV view state to
SpatialCanvasview state - resolve existing MDV deck layers from host overlay descriptors:
- scatterplot layer
- grey/background scatter layer
- selection layer
- gate display layer
- gate label layer
- contour/field layers
- ROI GeoJSON layer
- preserve MDV chart linking via
useViewStateLink - keep MDV tooltip behavior via
useOuterContainerDeckTooltip - keep MDV keyboard/selection behavior around the viewer container
The first-pass chart can deliberately avoid replacing the channel dialog and most image editing UI. It only needs enough layer config to render an image/labels/shapes baseline and enough view-state synchronization to sanity-check against the current Viv chart.
Render stack and MDV state model
The next MDV pass should treat @spatialdata/layers RenderStack as the
canonical saved/render order:
const renderStack = {
schemaVersion: 1,
entries: [
{ kind: 'spatial', id: 'image', source: { elementType: 'image', elementKey: 'morphology' } },
{ kind: 'host', id: 'deck:scatter', source: { hostLayerId: 'deck:scatter' } },
{ kind: 'spatial', id: 'labels', source: { elementType: 'labels', elementKey: 'cells' } },
],
};
Host overlays are descriptors in saved config. MDV owns the runtime resolver
that turns deck:scatter, gates, selections, contours, or ROI descriptors into
deck.gl layers. This avoids storing raw deck layer instances in project state
and avoids parallel stackOrder / layerOrder arrays.
Listeners follow the same split. Saved renderStack.entries[*].props should
contain serializable renderer input only. MDV-owned callbacks such as
onFeatureHover / onFeatureClick, tooltip portal wiring, and host overlay
factories are runtime attachments supplied beside the stack. The feature events
cover shapes and labels and include the same tooltip payload that built-in
SpatialData.js tooltips use, so MDV does not need to decode deck.gl pick objects
for normal table-backed hover/click interactions.
Constrained MobX
MobX is acceptable for MDV-facing direct-edit control state, but it should be
kept out of @spatialdata/layers and out of the default @spatialdata/vis
renderer contract:
- Use small MobX control islands for layer rows and controls.
- Let each observer component read only the observable fields it renders.
- Do not pass broad observable stack/config objects into MUI, dnd-kit, deck.gl, Viv, or non-observer components; pass plain values at those boundaries.
- Sliders should patch renderer-visible props immediately and persist config intentionally, not through periodic whole-stack snapshots.
- Avoid
toJSon large stack/config objects during drag interactions.
React Compiler adoption makes this boundary more important. MobX observer
components rely on observable reads rather than React's immutable-prop
memoization assumptions, so MobX-heavy components should be explicit islands
that can be excluded from compiler optimization or marked with "use no memo"
where needed. The default SpatialData.js viewer path should remain
plain-object and compiler-friendly.
Acceptance for the MDV UI path is separate from tile-fetch correctness: opacity dragging should not re-render the whole layer dialog/list, should not show a periodic stop-start GUI cadence, and should not create large stack snapshots on every granular mutation.
Tables, AnnData.js, and zarrita DataLoader
MDV chart config ultimately drives table-backed shape colour, tooltips, and filters. The shared contract spans three packages:
| Concern | Owner |
|---|---|
region / region_key / instance_key semantics, row alignment | @spatialdata/core |
| Column → per-feature colour maps, deck feature state | @spatialdata/layers |
| Which column to show, loading columns for UI, passing config into the viewer | @spatialdata/vis / MDV |
Today: TableElement loads AnnData stores via anndata.js on top of
zarrita (readZarr on the table's zarr subtree). Association helpers
(loadFeatureRowIndexByFeatureIndex, loadAssociatedTableFeatureRows) use
targeted obs-column loaders rather than a single high-level DataLoader API.
Near-term roadmap: expose coherent tables access through
AnnData.js (and zarrita-backed DataLoader where appropriate) so
integrators can read obs, var, selected X columns, obsm, and future
uns / obsp surfaces without ad hoc string-column lookups or dropping to
getAnnDataJS() for every chart. Richer query patterns should push upstream
into anndata.js where possible; SpatialData.js should thin-wrap table
elements and preserve Python association semantics.
MDV should pass resolved featureState and fillColorByColumn on the
target shapes stack entry props when selection or colour mappings change — not
re-load geometry. See Feature table associations.
Phase 3: layer styling, filtering, and highlighting
The current SpatialData renderers mostly accept constant style values:
- shapes:
fillColor,strokeColor,strokeWidth - points:
pointSize,color - labels: channel colors/opacities/stroke widths
MDV needs feature-aware styling:
- color labels/shapes by MDV filter state
- fade or hide filtered-out features
- highlight selected/hovered/gated features
- color by categorical or numeric table columns
- keep image channels independent from feature-layer styling
Required changes:
- Define a stable feature identity model for shapes and labels.
- shapes now preserve row/feature ids in a render-oriented core payload and expose them through shared layers picking/styling helpers
- labels need label ids mapped to associated table rows
- Extend layer configs with feature-state props keyed by stable feature id.
- Decide where MDV-specific filtering is evaluated:
- MDV computes style arrays and passes them in
- or shared package accepts filter/highlight sets and evaluates them
- Add picking metadata that MDV can map back to datasource rows.
- Add tests for picked shape/label id -> associated table row.
- Add a fast path for large tables and many polygons. Avoid per-feature MobX reads in deck.gl accessors.
Suggested first design:
type FeatureStyleState = {
visibleIds?: Set<string | number>;
highlightedIds?: Set<string | number>;
selectedIds?: Set<string | number>;
colorById?: Map<string | number, [number, number, number, number]>;
};
MDV can compute this state from its datastore and filters, while @spatialdata/layers now owns the deck-facing shapes implementation, pick/tooltip resolution, and backend choice. @spatialdata/vis consumes that shared path.
For the adapter boundary, MDV should be able to own only serialized featureState, layer order/visibility, and any precomputed style/filter values keyed by stable featureId, without needing to know which shapes backend is active underneath.
Phase 4: nicer GUI in MDV
Once the headless viewer works, MDV can selectively reuse or replace pieces of the current SpatialCanvas UI:
- layer visibility and order
- image channel controls
- labels channel controls
- tooltip-field selection
- color-by controls for associated tables
- shape/label filter/highlight controls
The current in-component sidebars should not appear inside MDV. Reusable panels should become standalone exports that accept controlled props and callbacks, so MDV can place them in its settings dialogs or chart menus.
Vitessce as a design target
Vitessce is a priority consumer for the design, even though MDV is the first staging environment. The point of integrating into MDV first is to get a concrete sanity check under real application pressure, not to produce an MDV-specific architecture.
Current signals from ~/code/www/vitessce:
- Vitessce currently uses older Viv/deck/luma versions than this repo, with Viv
0.16.x, deck.gl8.8.x, and luma.gl8.5.xin its workspace lockfile. - Its spatial views already compose Viv image layers, scale bars, expression/scatter layers, selection layers, and channel/controller state.
- It has both current and beta spatial view paths, plus accelerated/3D paths that are likely out of scope for the first headless 2D viewer.
Likely requirements for Vitessce usefulness:
- A headless React viewer with no MDV assumptions, no MobX dependency, and no app-specific tooltip or settings UI.
- A plain layer/props schema that can be produced from Vitessce coordination values.
- Compatibility with Vitessce's channel controller concepts: selections, colors, visibility, contrast/domain, colormap/rendering mode.
- Explicit hooks for custom deck.gl layers and layer ordering.
- A renderer API that does not require
@spatialdata/reactcontext if Vitessce wants to resolve/load data through its own loaders. - A separate investigation for 3D/volume paths after the 2D image/labels/shapes story is stable.
For now, the Vitessce note should be treated as an API pressure test and a success criterion: if the headless demo API can plausibly be driven by MDV chart config, Leva state, and Vitessce coordination values, it is probably the right shape. If it only works naturally in MDV, the abstraction is too narrow.
Arrow, Parquet, and deck.gl boundaries
We should track upstream deck.gl / loaders.gl / deck.gl-community work around Arrow, GeoArrow, and GeoParquet carefully. This affects the boundary between @spatialdata/core, @spatialdata/layers, @spatialdata/vis, deck loaders, and app-specific adapters.
Current upstream read:
geoarrow/deck.gl-geoarrowis the renamed home for the formergeoarrow/deck.gl-layersproject. The published package to evaluate is@geoarrow/deck.gl-geoarrow; it targets deck.gl 9 and Apache Arrow JS.- The useful layer for SpatialData points is likely
GeoArrowScatterplotLayer, but it expects GeoArrow point/multipoint data, not arbitraryx/ycolumns. SpatialData points currently store coordinate columns in Parquet, so an adapter still has to build or expose a GeoArrow point column/batch. - The library is most useful when we can keep Arrow chunks columnar all the way to deck.gl's binary attribute interface. It is less compelling if we first materialise every point as JS objects or as the current ndarray-ish wrapper.
- deck.gl-community's Arrow layers are a second signal in the same direction, but the community docs explicitly warn about maintenance bandwidth. Treat that as an API pressure test rather than a dependency to bet the public contract on.
Current local state:
@spatialdata/corecurrently loads Parquet bytes/tables throughparquet-wasminVTableSource, inherited by points and shapes sources.- points currently return an ndarray-ish
{ shape, data }object with axis columns loaded from Parquet. - shapes currently expose a render-oriented core payload with stable feature ids, shared row-index alignment, and a mixed backend path in
@spatialdata/layers. - labels are still their own image/tile rendering path.
- Vitessce-derived code already has more advanced point handling in places, including tiled point loading, viewport filtering, feature-index filtering, and
DataFilterExtensionuse. VTableSourcerecognisespoints/<key>/points.parquetandpoints/<key>/points.parquet/part.0.parquet, but it does not yet model a multi-file Parquet dataset as multiple chunks/batches. That is the wrong shape for large point stores.
Points-specific target shape:
coreshould expose aPointsRenderData-style payload, parallel toShapesRenderData, with stable point ids, row-index alignment, coordinate axis names, optionalfeature_key/instance_keycolumns, and the original Arrow table or record batches when available.layersshould own a shared points renderer. The renderer should choose between:- a current fallback
ScatterplotLayerover typed coordinate arrays - a binary deck.gl attribute path for
x/y/ optionalz - a
GeoArrowScatterplotLayerpath when data is already GeoArrow point encoded, or when the adapter can build that point column without copying too much
- a current fallback
- Points need the same feature-state language as shapes: hide, fade, color, radius, and filtered opacity by stable point id or row index. MDV/Vitessce filters should update feature-state or filter columns; they should not force a full Parquet reload.
- Viewport/row-group filtering belongs behind the points data adapter, not in
SpatialCanvasUI code. A multi-file Parquet directory can naturally map to progressive chunks/layers first, then later to row-group or bounding-box pruning when metadata is available.
Upstream signals to monitor:
- deck.gl layers can load via loaders.gl and every layer supports
loadOptions, but deck.gl core layers still generally consume arrays, binary attributes, flat GeoJSON, or app-supplied data rather than owning all Parquet semantics. @loaders.gl/parquet/GeoParquetLoaderexists but is documented as experimental/beta, with important limitations around large files and partial filtering.- deck.gl-community's Arrow layers accept Apache Arrow / GeoArrow tables directly and use deck.gl's binary data interface to avoid intermediate JS objects.
- GeoParquet and GeoArrow are closely related but not identical. GeoParquet commonly stores geometry as WKB today; GeoArrow-native encodings are the direction to watch.
- Foundational work on deck.gl v10, loaders.gl v5, and luma.gl v10 points even more strongly toward native Arrow / GeoArrow handling as a first-class direction:
- deck.gl v10 is expected to rely more heavily on shared columnar GPU data handling rather than bespoke per-layer CPU-side mapping
- loaders.gl v5 is introducing optimized Arrow / GeoArrow load paths across loaders
- luma.gl v10 is exploring Arrow-like GPU-side data structures, batched GPU data, and GPU-side column transformations so full binary uploads can stay columnar even when raw Arrow blocks do not match GPU attribute/storage-buffer layouts directly
- the practical implication for us is that representation-agnostic public APIs matter even more: we want our contracts to survive a shift from today’s fallback/bespoke handling to more native Arrow-to-GPU paths later
Boundary options:
-
coreowns Parquet and returns simple JS/typed-array structures.- This keeps apps and renderers decoupled from deck.gl.
- It is easiest to test and use outside visualization.
- It risks duplicating work that upstream deck/loaders/arrow layers may solve better.
-
coreowns data discovery and returns Arrow tables/vectors.coreremains deck-free but exposes columnar data close to the source format.viscan choose deck.gl binary attributes, deck.gl-community Arrow layers, or fallback JS accessors.- This probably gives the best medium-term boundary.
-
visowns deck-specific loaders and consumes URLs/metadata fromcore.- This lets deck/loaders evolve naturally.
- It risks making non-visual
coreAPIs too thin for MDV/Vitessce table association, feature ids, and filtering.
-
apps own their existing loaders, and
visonly receives ready-to-render layer data.- This is useful for Vitessce compatibility and headless demos.
- It does not solve shared SpatialData loading unless paired with another path.
Recommended direction for now:
- Keep
@spatialdata/corefree of deck.gl dependencies. - Align
@spatialdata/core's points and shapes support with Vitessce's SpatialData-derived loaders so we do not fall behind format coverage while the rendering backend evolves. - Move toward
coreexposing Arrow-ish columnar primitives for points/shapes/tables, while preserving convenience methods for simple JS arrays. For points, that means preserving Arrow batches/vectors alongside the current coordinate-array convenience path. - Make
@spatialdata/layersresponsible for choosing the rendering backend:- current polygon fallback for compatibility
- current
geoarrow-tableruntime branch for shared columnar payloads @geoarrow/deck.gl-geoarrowas the intended stronger near-term fast path when the external dependency is adopted cleanly- future Arrow/community-layer backends without changing the public shapes config
- Keep feature identity, table association, coordinate transforms, and metadata interpretation in
core; keep GPU filtering, tiling, layer construction, and picking/render props inlayers. - Avoid baking
parquet-wasmas the only long-term path. Treat it as the current implementation behind a replaceable interface. - Upgrade points before adopting GeoArrow broadly: first add stable point identity, row-index alignment, feature-state filtering/styling, and multi-part Parquet discovery; then add the GeoArrow renderer as an adapter behind the same public points config.
One more API-design note to preserve: the current table-association helpers are still obs-oriented and string-column-oriented because that is enough for the first feature-id join path. A future revision should widen the shared contract so style/filter inputs can come coherently from all of the AnnData surfaces we care about: obs, var, selected X columns for chosen var rows, obsm, potential future obsp graph/network data, and uns, without forcing integrators to encode everything as ad hoc string column lookups. In principle, many of those richer access patterns should likely be pushed upstream into anndata.js rather than duplicated forever in SpatialData.js.
- Treat Vitessce parity tests as compatibility fixtures: when Vitessce supports a points/shapes SpatialData layout,
coreshould either support it too or document why not. - Add a small compatibility matrix for points and shapes:
- WKB GeoParquet -> JS fallback
- WKB GeoParquet -> decoded Arrow/GeoArrow if available
- GeoArrow-native geometry -> Arrow layer or binary deck attributes
- tiled/row-group-aware points -> Vitessce-style path or upstream deck/loaders path
Open questions:
- Should
PointsElement.loadPoints()return Arrow vectors/tables in addition to typed arrays? - Should
PointsElementexpose a chunked/multipart API (loadPointBatches) so largepoints.parquet/part-*.parquetdirectories can progressively render without pretending they are one file? - Should
ShapesElement.loadPolygonShapes()preserve feature ids alongside geometry in a first-class row object or columnar structure? - Where should row-group / viewport filtering live:
core,vis, or app adapter? - Can deck.gl-community Arrow layers become a dependency of
@spatialdata/vis, or should they be optional peer/adapter code? - How do we keep MDV filter/highlight state efficient if the underlying feature data is Arrow vectors rather than JS arrays?
- How much of Vitessce's point tiling/filtering should be upstreamed into shared
visutilities versus left as a Vitessce-specific adapter until deck.gl upstream stabilizes?
Images, labels, and deck.gl-raster
Viv is still the current image rendering foundation in this repo, MDV, and Vitessce-derived paths, but we should not assume it remains the only viable long-term raster path.
Upstream deck.gl-raster is moving quickly:
- PR developmentseed/deck.gl-raster#467 merged an AlphaEarth Foundations GeoZarr mosaic example using
ZarrLayer. - That example renders user-selected RGB composites from Zarr data, uploads 64-band tiles to a
Texture2DArray, and performs band selection / dequantization / rescaling in shader code. - Recent
deck.gl-rasterreleases also add higher-level GeoTIFF/COG and multi-band COG support.
Implications for us:
- Track whether
deck.gl-raster'sZarrLayer/ raster pipeline can cover parts of our image rendering without Viv. - Track whether labels/segmentation rasters can be represented as a specialized raster shader pipeline rather than a Viv-derived labels layer.
- Keep image/labels props in
@spatialdata/visabstract enough that the backend can be Viv today anddeck.gl-raster/deck-native later. - Avoid coupling the public
SpatialCanvasAPI to Viv-specific concepts such asDetailView, Viv selections, or Viv extension classes where a backend-neutral expression is possible. - Consider proposing upstream Viv support for deck.gl-raster primitives if Viv remains useful for OME/NGFF metadata, channel semantics, and viewer ergonomics.
- Add a raster backend spike after the headless demo exists:
- render one image-like Zarr layer through Viv
- render the same or analogous data through
deck.gl-raster - compare coordinate transforms, channel controls, tile loading, shader extensibility, picking/labels feasibility, and dependency weight
Open image/labels questions:
- Is Viv primarily a metadata/channel/viewer abstraction for us, or is it also the rendering primitive we want long term?
- Can
deck.gl-rasterhandle OME-NGFF / SpatialData Zarr layouts directly, or do we need an adapter layer for axes, multiscales, transformations, and channel metadata? - Are labels better treated as raster category data, image overlays, or feature layers with table-backed identities?
- Can one backend handle both continuous images and integer segmentation labels, or do labels need a dedicated layer either way?
- If raster backends diverge, what is the minimum common image-layer prop schema MDV and Vitessce can both drive?
OME-TIFF, JP2K, and raster codecs
MDV currently supports OME-TIFF, including JP2K/JPEG2000-encoded image paths, but this repo currently focuses on SpatialData/OME-Zarr-style stores. We should keep OME-TIFF in the roadmap because it is already useful in MDV and may be an important bridge format for real spatial workflows.
Current state:
- MDV can render OME-TIFF through its Viv/Avivator-derived path.
- This repo does not yet expose an OME-TIFF-backed
SpatialCanvasimage source. - SpatialData workflows generally point toward Zarr-backed rasters, but users may already have pyramidal OME-TIFF assets, including tiled JPEG2000-compressed images.
- OME-TIFF has strong bioimaging metadata support and can represent tiled multi-resolution images.
- Zarr v3 supports codec pipelines, including standard codecs such as bytes, gzip, blosc, zstd, sharding, transpose, and CRC32C. NGFF notes that future image-specific codecs may be adopted as they emerge.
Potential directions:
- Treat OME-TIFF as an image-source backend that can participate in the same headless viewer API as SpatialData Zarr images.
- Decide whether OME-TIFF belongs in
@spatialdata/core, a sibling image-source package, or only in@spatialdata/vis. - Preserve a path for JP2K/HTJ2K-style image compression benefits where browser/runtime support is practical.
- Clarify how an OME-TIFF image should attach to a SpatialData project:
- external image reference in project metadata
- converted/derived OME-Zarr image
- MDV-specific region metadata
- future community-standard image reference pattern
- Add an image-source abstraction that can represent:
- OME-Zarr / SpatialData Zarr image
- OME-TIFF / pyramidal TIFF image
- deck.gl-raster Zarr/GeoZarr image
- labels/segmentation raster
- Keep codec-specific behavior below the public layer config where possible. Users should configure channel/selection/rendering intent, while loaders/backends handle storage codecs.
Community questions to clarify:
- Should SpatialData workflows explicitly support external OME-TIFF references, or should OME-TIFF primarily be converted into Zarr/OME-Zarr/SpatialData stores?
- What is the expected story for JP2K/HTJ2K-compressed microscopy imagery in web SpatialData viewers?
- Which Zarr raster codecs are considered acceptable or desirable for OME-NGFF / SpatialData image arrays?
- Are image-specific codecs likely to become standard enough that we should avoid investing too much in TIFF-specific paths?
- How should labels/segmentation arrays be encoded in Zarr when storage efficiency and random tile access both matter?
- Should MDV's existing OME-TIFF support become a compatibility adapter around the same headless image-source API, or remain an MDV-specific path until the community guidance is clearer?
Zarr beyond SpatialData
Future MDV should probably support useful Zarr rasters that are not SpatialData stores. GIS, Earth observation, climate, and other geospatial workflows increasingly use Zarr-family formats with their own metadata conventions. The viewer architecture should be able to support these without pretending they are SpatialData.
Relevant standards and conventions:
- OGC has endorsed Zarr as a community standard for cloud-friendly multidimensional arrays.
- GeoZarr is developing modular conventions for geospatial Zarr, including CRS metadata, spatial transforms, and multiscale pyramids.
- GeoZarr aims to support both Zarr v2 and v3 and bridge scientific/geospatial conventions such as CF, STAC, OGC Tile Matrix Sets, and affine geotransforms.
- GDAL can expose some Zarr datasets as rasters, but plain Zarr does not itself define spatial reference semantics. Metadata conventions matter.
Potential MDV use cases:
- GIS / GeoZarr raster layers alongside biological SpatialData layers.
- Earth-observation or environmental rasters as contextual backgrounds.
- Non-SpatialData Zarr arrays used as image-like overlays in projects.
- Mixed workflows where MDV displays SpatialData-derived cell/shape/label layers over geospatial rasters.
Design implications:
- Separate the concepts of "SpatialData object" and "renderable Zarr raster source".
- Add an image-source interface that can describe axes, CRS/coordinate transform, multiscales/pyramids, chunking, channels/bands, and storage location without requiring SpatialData-specific metadata.
- Keep SpatialData parsing in
@spatialdata/core, but consider a sibling or lower-level package for generic Zarr raster descriptors. - Let
@spatialdata/visconsume normalized raster descriptors regardless of whether they came from SpatialData, OME-Zarr, GeoZarr, OME-TIFF, or an MDV adapter. - Track GeoZarr conventions for CRS, spatial transforms, and multiscales before inventing our own GIS-Zarr metadata contract.
- Avoid overloading biological channel semantics for GIS bands. A common layer API can expose bands/channels, but app UI should label them according to source context.
- Consider MapView / geospatial projection support separately from the current Viv-style Cartesian detail view. GIS rasters may need deck.gl
MapView, basemap alignment, and longitude/latitude coordinate handling.
Open questions:
- Should the package names remain
@spatialdata/*if we add generic Zarr/GIS support, or should generic raster source parsing live outside that namespace? - What minimum descriptor is needed to render a non-SpatialData Zarr raster in Cartesian MDV views?
- What additional descriptor is needed to render the same source in geospatial
MapView/ MapLibre-aligned views? - Should MDV treat GIS Zarr as contextual imagery only, or should filtering/picking/measurement workflows apply there too?
- How should coordinate-system linking work when a project mixes SpatialData coordinate systems and geospatial CRS?
Compatibility risks
- Viv/deck version skew: largely resolved — both codebases target Viv
0.21.0/ deck.gl9.2.9; MDV's upgrade PR is almost ready to merge. Remaining risk is extension-specific regression, not mismatched package versions. - Arrow/Parquet churn: loaders.gl Parquet and deck.gl-community Arrow layers are promising but still moving targets. We should avoid locking public
coreAPIs to one loader implementation too early. - Raster backend churn:
deck.gl-rastermay 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.
- Generic Zarr scope creep: MDV may need GeoZarr/GIS/non-SpatialData Zarr support, but that should be modeled as normalized raster sources rather than by weakening the SpatialData-specific APIs.
- View state shape: MDV's Viv chart uses Viv
DetailViewview states with view ids.SpatialCanvasuses a smaller{ target, zoom }shape. The adapter needs careful conversion so linked charts and saved configs do not drift. - Layer ids: MDV relies on
getVivId(...)tokens for layer filtering.VivSpatialViewerhas its own generated ids. MDV embedding may need a suppliedviewId/layerIdSuffix. - UI ownership: current
SpatialCanvasresets its local store when coordinate systems change. In controlled MDV mode this could wipe chart-driven layers if not separated. - Tooltip ownership: both libraries have tooltip systems. MDV should own tooltip portals for now.
- Feature ids: shapes now carry stable
featureIdvalues in render data and layer picking; labels association and tests still need to catch up to the shapes path. - Performance: shape/label styling from MDV filters must avoid expensive observable lookups inside render accessors.
- Data model mismatch: MDV's regions/image metadata is not the same as a full SpatialData object. The first adapter may need a compatibility layer while MDV projects transition.
- CSS/layout:
SpatialCanvashas hard-coded dark UI styles and minimum height. Headless mode should render with parent-owned layout and no border/sidebar styles.
First-pass acceptance criteria
- MDV can import
@spatialdata/visfrom a packed or prerelease package. - This repo has at least one headless demo whose state is driven from an external UI rather than from
SpatialCanvas's built-in panels. - The same headless API can be described in terms of Vitessce coordination values without requiring MDV chart classes or MobX concepts.
- A new MDV chart can render a SpatialData-backed image layer using the same region/view size as the current Viv chart.
- The same chart can compose at least one MDV custom deck.gl layer above the image.
-
SpatialCanvasUI controls do not appear inside MDV. - MDV controls view state, and existing chart linking still works for pan/zoom.
- A labels or shapes layer renders above the image with constant styling.
- Hover/pick returns enough information to identify a label id or shape feature.
- The old
VivMdvRegionReactpath remains available during rollout.
Existing migration checklist
- Replace local
src/react/components/avivatorish/with@spatialdata/avivatorishwhere doing so does not destabilize MDV. - Replace
viv_loader_cache.tswithcreateLoader/loadOmeZarrMultiscalesDatafrom this repo. Optional shared caching can be reintroduced later; shared loader code should remain MobX-free. -
SpatialLayernaming: MDV today uses@/webgl/SpatialLayer; this repo exports aSpatialLayerCompositeLayerfrom@spatialdata/layers. Converge names or use a temporary alias such asSpatialDataCompositeLayer. -
VivScatterComponent/scatter_state: keep view-id conventions (getVivId) and metadata hooks compatible. Long term, scatter props should accept table-backed descriptions aligned with@spatialdata/coreTableElement. -
contour_state: Phase 1: extract pure functions (columnar data + view params -> contour / deck extension props) into@spatialdata/layers; leave MobX + MDV GUI in MDV. Phase 2: optional MobX-free hooks invis.