Skip to main content

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());
}