Element Classes
Each element type in a SpatialData store is represented by a typed class that provides access to metadata, coordinate transformations, and data loading methods.
:::info Elements are loaded, not constructed
Elements are obtained by loading a store via readZarr(). You don't create element instances directly—they're populated from the store's metadata and provide a read-only interface to the underlying data.
const sdata = await readZarr('https://example.com/data.zarr');
const image = sdata.images?.['my_image']; // ImageElement from the store
:::
Common Properties
All element classes share these properties:
interface BaseElement {
readonly kind: ElementName; // 'images' | 'shapes' | 'labels' | 'points' | 'tables'
readonly key: string; // The element's name within its category
readonly path: string; // Path of the element's zarr group within the store, e.g. 'images/my_image'
readonly url?: string; // Full URL to the group; undefined for zip and folder stores
readonly attrs: object; // Validated attributes (type varies by element)
// Problems with how the element is stored; see Element Issues
getIssues(): readonly ElementIssue[];
assessIssues(): Promise<readonly ElementIssue[]>;
}
Spatial Elements
Spatial elements (everything except tables) inherit from AbstractSpatialElement and provide coordinate transformation methods:
interface SpatialElement {
// Get all coordinate systems this element can transform to
readonly coordinateSystems: string[];
// Get transformation to a specific coordinate system (Result type)
getTransformation(toCoordinateSystem?: string): Result<BaseTransformation, CoordinateSystemNotFoundError>;
// Get all transformations as a Map
getAllTransformations(): Map<string, BaseTransformation>;
}
ImageElement
Images are multiscale raster data following the OME-NGFF specification.
const sdata = await readZarr('https://example.com/data.zarr');
const image = sdata.images?.['my_image'];
// Metadata access
image.attrs.multiscales[0].axes; // Axis definitions
image.channels; // Channel metadata from omero
image.ndim; // 2 or 3
image.isMultiscale; // true if multiple resolution levels
image.scaleLevels; // ['0', '1', '2', ...] paths to each level
// Spatial properties
image.spatialAxes; // Only space-type axes
image.getStore(); // zarr store rooted at this element, for opening arrays
// Get transformation including per-resolution-level transforms
const transforms = image.getTransformationForLevel(0, 'global');
// Returns { element: Result<BaseTransformation>, dataset: BaseTransformation },
// or undefined if the level does not exist
Attributes Schema
type RasterAttrs = {
multiscales: Array<{
name?: string;
axes: Array<{ name: string; type: 'space' | 'time' | 'channel' }>;
datasets: Array<{
path: string;
coordinateTransformations?: CoordinateTransformation;
}>;
coordinateTransformations?: CoordinateTransformation;
}>;
omero?: {
channels: Array<{
window?: { start: number; end: number; min: number; max: number };
label?: string;
color?: string;
active?: boolean;
}>;
};
spatialdata_attrs?: { version: string };
};
LabelsElement
Labels are segmentation masks stored as OME-NGFF multiscale rasters, identical in structure to images.
Labels are rendered from raster values, and a label's feature id is its integer
instance id. Unlike shapes, LabelsElement has no feature-id loading method:
ids come from picked pixels at render time.
const labels = sdata.labels?.['my_segmentation'];
// Same properties as ImageElement
labels.scaleLevels;
labels.ndim;
labels.getTransformation('global');
ShapesElement
Shapes represent vector geometries like polygons and circles.
const shapes = sdata.shapes?.['cell_boundaries'];
// Load geometry data
const renderData = await shapes.loadRenderData(); // Auto-detects polygons, circles (cell_circles), points (xenium_landmarks)
const polygons = await shapes.loadPolygonShapes(); // For polygon geometries only
const circles = await shapes.loadCircleShapes(); // For circle/point geometries only
const featureIds = await shapes.loadFeatureIds();
// Access metadata
shapes.attrs.axes; // e.g., ['x', 'y']
shapes.attrs['encoding-type']; // e.g., 'ngff:shapes'
shapes.attrs.coordinateTransformations;
loadFeatureIds() is the method to use when you want to associate picked
shapes with table rows. See Loading points and shapes.
Attributes Schema
type ShapesAttrs = {
'encoding-type'?: string;
axes?: string[];
coordinateTransformations?: CoordinateTransformation;
spatialdata_attrs?: { version: string };
};
PointsElement
Points represent point cloud data such as transcript locations.
const points = sdata.points?.['transcripts'];
points.attrs.axes; // e.g., ['x', 'y']
points.coordinateSystems; // Available coordinate systems
points.storage; // 'parquet' | 'points-zarr'
const result = await points.loadPoints(); // columnar { shape, data }, capped in memory
Loading, streaming, feature selection and tiled stores are covered in Loading points and shapes.
TableElement
Tables are AnnData objects that can annotate spatial elements via region keys.
const table = sdata.tables?.['annotations'];
// Get as AnnData.js object for full access
const adata = await table.getAnnDataJS();
// Load row ids / selected obs columns through SpatialData.js loaders
const rowIds = await table.loadObsIndex();
const regionColumns = await table.loadObsColumns(['region']);
// What those columns ARE, without loading their values. Synchronous: opening the
// store already read the metadata this is derived from.
const kinds = table.getObsColumnKinds(['leiden', 'area']);
// -> ['categorical', 'numeric'] (also 'string' | 'boolean', or undefined if unknown)
// Normalized association metadata (Python `get_table_keys()` equivalent)
const { region, regionKey, instanceKey } = table.getTableKeys();
// Find tables that annotate a spatial element
const associated = sdata.getAssociatedTable('shapes', 'cell_boundaries');
const [tableName, associatedTable] = associated ?? [];
// Raw attrs are also available; association keys may be absent on some tables
table.attrs.instance_key; // Column name for feature IDs / instance IDs
table.attrs.region; // Element(s) this table annotates
table.attrs.region_key; // Column name linking to region
getTableKeys() always returns normalized keys: region is a string array,
and when association metadata is missing region is [] while
regionKey / instanceKey are empty strings. Prefer this method over
reading table.attrs directly when matching tables to spatial elements.
SpatialData.getAssociatedTables() / getAssociatedTable() use the same
normalization and ignore tables without association metadata.
Equivalent region encodings (not implemented)
The table contract maps each row to one region and instance. A table could
assert that a row is equivalent across elements (for example
labels/cell_labels and shapes/cell_shapes) by carrying optional, advisory
metadata under table.uns.spatialdata_attrs, keeping the canonical
region_key / instance_key mapping unchanged. Nothing reads such metadata
today and it is not part of the SpatialData spec.
Implementation note:
loadObsIndex() and loadObsColumns() are used by the feature-association
helpers and currently stay on the direct zarr/parquet loader path rather than
depending on anndata.js. getAnnDataJS() remains available for higher-level
AnnData access.
getObsColumnKinds() is deliberately synchronous. Opening a store already
reads every node's attributes and array metadata into the tree — the same tree
getObsColumnNames() reads names from — so the encoding-type that separates
an AnnData categorical from a string array, and the dtype that separates a float
from a bool, are in memory before anyone asks for values. That lets a caller ask
what a column is before deciding whether to load it, which is what a
"colour by" UI needs in order to offer the right affordance up front. The
classifier reads both zarr generations (v3 spells dtypes out, v2 uses numpy
typestrings). Kinds are best-effort: a source that cannot report one yields
undefined and consumers fall back to inspecting values.
loadObsColumns() is a convenience for association and tooltip lookups. Access
to var, X, obsm, obsp and uns is only through getAnnDataJS();
richer query APIs are expected to land upstream in anndata.js rather than here.
Attributes Schema
type TableAttrs = {
instance_key?: string | null;
region?: string | string[] | null;
region_key?: string | null;
'spatialdata-encoding-type': 'ngff:regions_table';
};
Association metadata is optional at the schema level: some tables only carry
the ngff:regions_table encoding marker, and real stores may persist missing
keys as JSON null. Use getTableKeys() to read the normalized association
contract rather than assuming all three keys are present strings.
Working with Coordinate Systems
All spatial elements share a common interface for coordinate transformations:
import { unwrap } from '@spatialdata/core';
const image = sdata.images?.['my_image'];
// Check available coordinate systems
console.log(image.coordinateSystems); // ['global', 'anatomical', ...]
// Get a specific transformation (returns Result type)
const result = image.getTransformation('global');
if (result.ok) {
const matrix = result.value.toMatrix(); // Matrix4 from @math.gl/core
const array = result.value.toArray(); // 16-element column-major array
} else {
console.log('Available:', result.error.availableCoordinateSystems);
}
// Or unwrap to throw on error
const transform = unwrap(image.getTransformation('global'));
// Get all transformations at once
const allTransforms = image.getAllTransformations();
for (const [csName, transform] of allTransforms) {
console.log(`${csName}:`, transform.toMatrix());
}