Runtime State¶
Named data¶
Named data sources allow data to be provided at runtime instead of loading it
from a URL. Declare every named dataset in the datasets property of the view
that owns it, and reference it with data.name:
{
"datasets": {
"myResults": []
},
"data": {
"name": "myResults"
},
...
}
datasets declarations are lexically scoped through nested views. A view can
reference a dataset declared on itself or an enclosing view. A more deeply
nested declaration with the same name shadows the enclosing declaration.
Repeated import instances therefore own independent datasets. The declaration
establishes the owner used for scoped runtime updates.
Updating named data¶
Use the embed-level datasets API for a dataset declared by the top-level input
specification:
const api = await embed("#container", spec);
api.datasets.set("myResults", [
{ x: 1, y: 2 },
{ x: 2, y: 3 },
]);
api.datasets always targets the top-level authored specification, even when
GenomeSpy adds an implicit layout wrapper. It never searches nested views by
name.
For a declaration in a nested or imported view, use the exact declaring view's
datasets API:
const owner = api.views.get({
scope: ["translationA"],
view: "translationA",
});
owner.datasets.set("geneticCode", rows);
Updates do not search ancestors or descendants. Exact ownership keeps updates unambiguous when multiple subtrees use the same dataset name.
datasets.reset() removes the runtime override and restores the values from
the declaration:
api.datasets.reset("myResults");
To avoid an initially empty dataset, a host can place initial rows in the JavaScript specification before embedding:
spec.datasets.myResults = initialRows;
const api = await embed("#container", spec);
Named data can be updated dynamically, but it does not automatically react to user interactions. For practical examples, see the embed-examples package.
Loading binary data¶
Use datasets.load() to replace a declared dataset directly from Arrow IPC or
Parquet data in memory:
await api.datasets.load("myResults", arrowData, { type: "arrow" });
await api.datasets.load("myResults", parquetData, { type: "parquet" });
The binary input can be an ArrayBuffer or any ArrayBufferView, including a
Uint8Array or the DataView received from an AnyWidget binary trait. Arrow
IPC files and streams are both supported.
The same exact-owner rule applies to binary data. Load a nested declaration through the declaring view's handle:
const owner = api.views.get({
scope: ["translationA"],
view: "translationA",
});
await owner.datasets.load("geneticCode", arrowData, { type: "arrow" });
load() resolves after decoding, dataflow propagation, and render scheduling.
It does not wait for the next animation frame. The existing visualization,
canvas, parameters, and event listeners remain live; scales, selections, and
other state derived from the replaced rows may react to the new data.
Dataset operations use latest-operation-wins ordering. If loads overlap, only
the most recently started load(), set(), or reset() can update the
dataset. A decoding failure from the current operation rejects the promise and
leaves the previous rows unchanged.
The default and full entry points register both binary readers. With the minimal entry point, import each reader that the host uses:
import "@genome-spy/core/rendering/webgl.js";
import "@genome-spy/core/rendering/canvas.js";
import { embed } from "@genome-spy/core/minimal";
import "@genome-spy/core/data/formats/arrow.js";
import "@genome-spy/core/data/formats/parquet.js";
Deprecated global APIs¶
api.updateNamedData(name, data) and the namedDataProvider embed option are
deprecated. They use embed-wide name lookup, which cannot address repeated or
shadowed declarations reliably.
Existing integrations may continue using them during the compatibility period.
A global update throws when the name identifies multiple scoped datasets.
Migrate by adding a datasets declaration to the intended owner and calling
api.datasets.set() or api.datasets.reset() for a top-level declaration, or
the owning view handle's datasets methods for a nested declaration.
Undeclared data.name references also retain their embed-wide fallback
behavior temporarily. New specifications should declare every named dataset
explicitly.
Named scales¶
A scale resolution is the scale instance shared by one or more view encodings. Composition settings determine whether child views share a scale resolution or use independent ones. See Scale, Axis, and Legend Resolution.
Name a scale resolution with scale.name to access it from the embed API with
getScaleResolutionByName(name). The name identifies the resolved scale, not an
individual view encoding. Shared encodings can use the same scale.name, while
independent scale resolutions must have unique names.
getScaleResolutionByName(name) returns undefined when the name is not
registered.
Named scales are useful when host application code needs to read the current domain, zoom a view programmatically, or synchronize domains between embedded GenomeSpy instances. See also Scale.
{
"encoding": {
"x": {
"field": "x",
"type": "quantitative",
"scale": {
"name": "detailScale",
"domain": [0, 100],
"zoom": true
}
}
}
}
const detailScale = api.getScaleResolutionByName("detailScale");
if (!detailScale) {
throw new Error("Missing named scale: detailScale");
}
console.log(detailScale.getDomain());
await detailScale.zoomTo([25, 50]);
Domains¶
getDomain() returns the current domain used by the scale. For locus scales,
this is the linearized, zero-based half-open numeric domain.
getComplexDomain() returns the current domain converted back to complex
domain values when available. For locus scales, its chromosome-relative endpoints
are zero-based and inclusive, covering the integer bases in the viewport. See
Specifying the domain for locus
domain syntax.
const genomeScale = api.getScaleResolutionByName("genomeScale");
if (!genomeScale) {
throw new Error("Missing named scale: genomeScale");
}
console.log(genomeScale.getDomain());
console.log(genomeScale.getComplexDomain());
For a locus scale, zoomTo() accepts either a linearized numeric interval or a
complex genomic interval:
await genomeScale.zoomTo([{ chrom: "chr8" }, { chrom: "chr10" }]);
await genomeScale.zoomTo([400_000_000, 500_000_000]);
Locus zoomTo() currently interprets explicit upper endpoints as inclusive in
both forms. Subtract one from a half-open upper endpoint before navigation,
including intervals from getDomain(), coordinate parsing, or
datum search. Whole-chromosome shortcuts without an explicit upper
position retain their full extent. Ordinary continuous numeric scales keep their
usual endpoint semantics.
This legacy locus convention is retained for compatibility. Standardizing genomic interval APIs on half-open endpoints is tracked for v2.0 (#620).
Pass { duration: true } or { duration: milliseconds } to animate the zoom:
await genomeScale.zoomTo([{ chrom: "chr1" }, { chrom: "chrM" }], {
duration: true,
});
Categorical scale viewports¶
For band and point scales, getDomain() returns the full ordered category list.
Read and change the visible rank interval with getViewport() and
zoomToViewport():
const categoryScale = api.getScaleResolutionByName("categories");
if (!categoryScale) {
throw new Error("Missing named scale: categories");
}
await categoryScale.zoomToViewport([1, 3], { duration: 500 });
const savedViewport = categoryScale.getViewport();
await categoryScale.zoomToViewport(null); // Fit all categories.
await categoryScale.zoomToViewport(savedViewport);
A viewport is a half-open [start, end] rank interval or null to fit all
categories. Intervals narrower than one category expand around their midpoint
to span one category. Fractional positions inside grouping gaps are not part of
the interval, so restoring or linking an interval can shift the placement when
a viewport edge lies inside a gap.
Listen to "viewport" events for navigation changes; "domain" events report
category membership or order changes.
These methods are available only for band and point scales. See
categorical scale viewports.
Two embeds with the same category order can link their rank intervals:
const targetScale = otherApi.getScaleResolutionByName("categories");
if (!targetScale) {
throw new Error("Missing target scale: categories");
}
categoryScale.addEventListener("viewport", () => {
void targetScale.zoomToViewport(categoryScale.getViewport(), {
renderImmediately: true,
});
});
Domain events¶
Named scales emit a "domain" event when their domain changes. Listen to the
event when external UI must reflect zoom or pan state:
const genomeScale = api.getScaleResolutionByName("genomeScale");
if (!genomeScale) {
throw new Error("Missing named scale: genomeScale");
}
const listener = (event) => {
console.log(event.scaleResolution.getComplexDomain());
};
genomeScale.addEventListener("domain", listener);
Remove listeners when they are no longer needed:
genomeScale.removeEventListener("domain", listener);
Domain events can also drive application-level linking. For example, a brush
parameter in one embed can call zoomTo() on a named scale in another embed.
For examples, see the scaleApi, brushLinkingApi, and linkedEmbeds pages
in the
embed-examples
package.
Parameters¶
Named parameters can be accessed through getParam(). The returned handle can
read and write the parameter value and subscribe to changes:
const api = await embed(container, spec);
const threshold = api.getParam("threshold");
console.log(threshold.getValue());
threshold.setValue(5);
const unsubscribe = threshold.subscribe((value) => {
console.log("threshold changed", value);
});
Variable parameters and interval selections can be read and written.
intervalSelection() constructs interval selection values:
import { embed, intervalSelection } from "@genome-spy/core";
const brush = api.getParam("brush");
brush.setValue(intervalSelection({ x: [10, 20] }));
Interval selection snapshots keep these numeric ranges in intervals. They
also expose complexIntervals: locus channels use { chrom, pos } endpoints,
numeric channels remain numeric, and cleared channels are null. The same
snapshot shape is returned by getValue() and passed to subscriptions.
Numeric legend intervals use their declared channel, for example
intervalSelection({color: [0.3, Infinity]}). The upper Infinity endpoint means
no upper limit; snapshots and selectionBounds(range, "color") preserve it.
Reading, observing, and clearing interval selections do not require an on-screen
brush host. Only contains({x, y}) needs one unambiguous interaction host in the
selected scope. Change subscriptions observe settled updates during a drag;
commit subscriptions report its changed result on release. Programmatic
replacement or clearing cancels an active drag, so later pointer events cannot
restore stale bounds. Cancellation alone retains the current bounds without
emitting a completion.
Current limitations:
- Parameters are addressed by name only. If the name resolves to multiple
independent parameters,
getParam()throws an ambiguity error. - Parameters declared with
push: "outer"are resolved as aliases of the outer parameter they write to. - Computed
exprparameters are readable but cannot be written. - Point selection snapshots are available through
api.params.getSelection(); the genericgetParam()handle does not write point selections.
For spec-side parameter behavior, including input bindings, selections, and
push: "outer" for linking scale domains, see
Parameters.
For examples, see the paramApi and brushLinkingApi pages in the
embed-examples
package.
To read a selection, use api.params.getSelection(name) or the corresponding
view.params namespace. It provides detached point and interval snapshots,
future-only subscriptions, interval containment, point-selection writes,
and clear(). See
Marks and scoped interaction for the
selection and lifecycle rules shared by these handles.
Setting record point selections¶
Obtain a point selection through the unit view supplying its records, including
when the parameter is declared in an ancestor. setValue(keys) replaces native
membership and drives the same subscriptions and conditional encodings as picking:
const track = api.views.get({ scope: [], view: "observations" });
const selected = track.params.getSelection("selected");
if (selected.type === "point" && selected.membership === "records") {
selected.setValue([["B"], ["D"]]);
console.log(selected.getValue().data);
}
Tuples follow encoding.key order: {field: "id"} takes ["B"]; a composite
[{field: "chrom"}, {field: "pos"}] takes ["chr1", 123]. Keys must be unique,
non-null strings, booleans, or finite numbers in ready loaded transformed records.
Invalid, missing, or ambiguous keys reject the entire write without changing
membership. Single selections (toggle: false) accept at most one tuple; []
clears without requiring data or a key. The setter works across mark types,
including text and rectangles. Membership binds the current publication; apps
clear or reapply it after replacing datasets.
Setting categorical point selections¶
Selections declared with select: {type: "point", encodings: ["color"]} store
raw categories. Their capabilities and snapshots have type: "point" and
membership: "categories". After narrowing to point, check membership before
reading record data or category encodings and values.
const focus = api.params.getSelection("focus");
if (focus.type === "point" && focus.membership === "categories") {
focus.setValue([["resting"], ["activated"]]);
console.log(focus.getValue());
// {type: "point", membership: "categories", active: true,
// encodings: ["color"], values: [["resting"], ["activated"]]}
}
Each tuple contains one string, boolean, or finite number. Numeric 1 and string
"1" are distinct; duplicates are removed in insertion order. Invalid tuples or
more than one distinct value with toggle: false reject the entire write.
Snapshots and subscriptions return detached tuples. Both change and commit
subscriptions receive the settled point update.
The owner can be a container or an ancestor of the source mark. Category writes
and clear() require no loaded data or encoding.key. Selected values stay
active when absent from the current data or scale domain, and match again when
they return. GenomeSpy App history and bookmarks preserve these raw values;
record expansion remains specific to record selections.
Specification initializers use channel-keyed objects such as
value: [{color: "activated"}], rather than the API's tuples. See
categorical point selection
for linked views and source declarations.
Updating appearance¶
The default theme follows CSS appearance settings automatically when the browser preference or attributes on the container or its ancestors change.
For exclusive programmatic control, declare defaultThemeColors in the root
params before embedding. This disables automatic CSS updates:
{
"params": [
{
"name": "defaultThemeColors",
"value": {
"foreground": "#352719",
"grid": "#ded2c0"
}
}
]
}
Palette fields are foreground, stroke, grid, chromosomeTick,
symbolStroke, and background. Omitted roles use the built-in light defaults,
including colors introduced in future versions. Set background to null for
transparency.
Update the palette through the parameter API:
const colors = api.params.get("defaultThemeColors");
colors.setValue({
foreground: "#352719",
grid: "#ded2c0",
});
Each write replaces the object; omitted roles revert to their defaults rather
than previous values. getValue() returns the supplied object. Mutating its
properties does not publish an update. Without an explicit declaration, manual
writes to the automatic palette last until the next CSS refresh.
An authored expr parameter can compute a partial palette instead; it remains
read-only. Descendant views can shadow defaultThemeColors through normal
parameter scoping. The canvas background uses the root palette.
The light and dark mode example shows System, Light, Dark, and Custom modes on one running visualization.