Internals & Architecture
:::caution For Contributors & Advanced Users
This page documents internal implementation details of @spatialdata/core. These APIs are not part of the public interface and may change without notice.
If you're building an application with this library, you likely don't need anything on this page—see the other documentation sections instead.
:::
Module Architecture
The package is organized into these internal modules:
@spatialdata/core/
├── store/ # SpatialData class, readZarr entry point
│ ├── index.ts # Main store class
│ └── zarrUtils.ts # Zarr store parsing utilities
├── models/ # Element class implementations
│ ├── index.ts # Element classes and factory
│ ├── VShapesSource.ts # Shapes geometry loader
│ ├── VTableSource.ts # Table loader utilities
│ └── VZarrDataSource.ts # Zarr data source base
├── schemas/ # Zod validation schemas
│ └── index.ts # OME-NGFF and SpatialData schemas
├── transformations/
│ ├── transformations.ts # Transformation classes
│ └── operations.ts # Helper functions
└── types.ts # Core type definitions
Documented Divergences
Some internal loaders started from upstream Vitessce implementations but have already diverged in a few deliberate ways. We try to keep those divergences small, documented, and tied to higher-level API goals rather than ad hoc fixes.
VShapesSource.ts
VShapesSource.ts began as a Vitessce-derived shapes loader. The current
intentional divergences are:
- TypeScript cleanup and normalization.
- Modern
ngff:shapesmetadata revisions are routed through the same parquet-backed path as the older supported modern format, instead of requiring one exact version string.
Why this exists:
ShapesElement.loadFeatureIds()is part of the high-level feature-association story used by vis tooltip / picked-feature workflows.- In practice, stable feature-id loading is the behavior we need to preserve, even when upstream metadata version strings move forward.
Planned evolution:
- keep
VShapesSource.tsnarrowly scoped to geometry / feature-id loading - review how that relates to use of e.g. https://github.com/geoarrow/deck.gl-layers/ in future
- avoid pushing more vis policy into this file
- if the divergence grows, prefer moving compatibility logic into a more explicitly SpatialData.js-owned layer rather than silently accumulating it here
Table Access Helpers
TableElement.getAnnDataJS() is still exposed, but feature-association helpers
such as loadObsIndex() and loadObsColumns() now prefer the direct
zarr/parquet loader path in VTableSource.ts.
Why this exists:
- these reads sit on the hot path for table-to-spatial-feature association
- we want them to remain usable even when wrapper libraries lag behind the relevant zarr/string-dtype support
Planned evolution:
- add clearer typed dataframe-column accessors for general table use
- keep the current convenience helpers focused on association / tooltip use
- continue feeding capability gaps back upstream to
anndata.js
Feature Identity Convergence (labels and shapes)
Current state:
- shapes expose stable feature ids through
ShapesElement.loadFeatureIds()(plus render data carryingfeatureIdsand row-alignment metadata); - labels currently expose feature identity from picked raster values during interactive rendering/tooltips.
Near-term unification direction:
- Add a labels-side feature-id API in
@spatialdata/core(for example non-zero unique ids at the selected scale). - Route both labels and shapes through the same association helper shape:
featureId -> table row index. - Keep render-time picking as one producer of feature ids, not the only one.
This keeps the table contract (region, region_key, instance_key) intact
while making labels/shapes more symmetric in higher-level APIs.
Experimental metadata idea (non-spec):
we may prototype optional table metadata that asserts equivalence classes across
elements (for example one canonical row identity corresponding to both
labels/<key>/<id> and shapes/<key>/<id>). This should remain explicitly
experimental and advisory until there is upstream spec support.
Element Factory (Internal)
Elements are created internally when loading stores. These functions are not intended for application use:
// Internal - do not use directly
import { createElement, loadElements } from '@spatialdata/core';
// createElement instantiates a single element from parsed metadata
const element = createElement('images', sdataProps, 'my_image');
// loadElements creates all elements of a type from the parsed store
const allImages = loadElements(sdataProps, 'images');
Application code should always obtain elements via readZarr():
// Correct usage
const sdata = await readZarr('https://example.com/data.zarr');
const image = sdata.images?.['my_image'];
Transformation Parsing
The transformation system parses NGFF coordinate transformations from zarr metadata into typed classes:
parseTransformEntry
Parses a single transformation object from metadata:
import { parseTransformEntry } from '@spatialdata/core';
const entry = { type: 'scale', scale: [0.5, 0.5] };
const transform = parseTransformEntry(entry);
// Returns: Scale { scale: [0.5, 0.5] }
parseTransforms
Parses an array of transformations, returning a single BaseTransformation:
import { parseTransforms } from '@spatialdata/core';
const transforms = parseTransforms([
{ type: 'scale', scale: [0.5, 0.5] },
{ type: 'translation', translation: [1000, 2000] },
]);
// Returns: Sequence containing Scale and Translation
buildMatrix4FromTransforms
Convenience function to get a Matrix4 directly from metadata:
import { buildMatrix4FromTransforms } from '@spatialdata/core';
const matrix = buildMatrix4FromTransforms([
{ type: 'scale', scale: [2, 2] },
]);
composeTransforms
Composes element-level and dataset-level transforms:
import { composeTransforms } from '@spatialdata/core';
const fullMatrix = composeTransforms(elementTransforms, datasetTransforms);
Transformation Classes
These classes represent parsed transformations internally. They're returned by element methods but shouldn't be constructed directly in application code:
| Class | Constructor | Description |
|---|---|---|
Identity | new Identity(input?, output?) | Identity matrix |
Scale | new Scale(scale[], input?, output?) | Scale transform |
Translation | new Translation(translation[], input?, output?) | Translation |
Affine | new Affine(matrix[][], input?, output?) | Full affine |
Sequence | new Sequence(transforms[], input?, output?) | Composed transforms |
Rotation | new Rotation(matrix[][], input?, output?) | Rotation matrix (square, no translation) |
MapAxis | new MapAxis(mapAxis[], input?, output?) | Axis remapping/permutation |
Not yet implemented: byDimension, bijection, displacements, and affine/rotation
"by path" (parameters stored in an external zarr array). See
Coordinate Transformations
for the current support summary, and
dev_scripts/conformance-dingus/ for the RFC-5 conformance harness that tracks
this backlog.
All classes extend BaseTransformation and provide:
abstract class BaseTransformation {
readonly input?: CoordinateSystemRef;
readonly output?: CoordinateSystemRef;
abstract toArray(): number[]; // 16-element column-major
toMatrix(): Matrix4; // Matrix4 from @math.gl/core
inverse(): Matrix4; // Inverted Matrix4; throws if singular
abstract get type(): string;
}
Type Utilities
Type aliases for working with elements generically:
import type {
ElementInstanceMap, // Maps element names to class types
SpatialElement, // Union of spatial element types
AnyElement, // Union of all element types
ElementName, // 'images' | 'shapes' | 'labels' | 'points' | 'tables'
} from '@spatialdata/core';
Zod Schemas
Validation schemas for metadata are defined in schemas/index.ts:
rasterAttrsSchema- Image/Labels OME-NGFF metadatashapesAttrsSchema- Shapes element metadatapointsAttrsSchema- Points element metadatatableAttrsSchema- AnnData table metadatacoordinateTransformationSchema- Transformation arraysspatialDataSchema- Root store metadata
These are used internally during element construction to validate and type metadata.
Store Parsing
Parsing zarr store contents lives in zarrextra, not in core:
openExtraConsolidated(source)- Resolves consolidated metadata and builds theZarrTree, returning aResultserializeZarrTree(tree)- Serializes for JSON output, converting the symbol-keyed attributes and array metadata to string keys
Element construction walks that tree with the guards and accessors described in
tree nodes — AbstractElement narrows an element's own
node to a group once, so no element subclass has to discriminate group from array
itself.
Result Type Implementation
The Result type and utilities are implemented in zarrextra and re-exported from @spatialdata/core for convenience:
// In zarrextra
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });
const isOk = <T, E>(result: Result<T, E>): result is { ok: true; value: T } => result.ok;
const isErr = <T, E>(result: Result<T, E>): result is { ok: false; error: E } => !result.ok;
const unwrap = <T, E>(result: Result<T, E>): T => { ... };
const unwrapOr = <T, E>(result: Result<T, E>, defaultValue: T): T => { ... };
You can import Result and its utilities from either @spatialdata/core or zarrextra.
:::note Future considerations
This is currently a custom implementation for simplicity and to avoid dependencies. We may review using an existing Result library (such as neverthrow or ts-result) in the future, but for now this provides a lightweight, dependency-free solution.
:::