Skip to content

Interactivity guide

5 min read · Updated 30 Oct 2026

Charts in the same selection group share selections: brushing in one chart can filter the others. This page explains the model and the wiring. It assumes you can already render a chart — see the Visualizations reference.

The examples assume that the SDK client is already authenticated. First choose which app model applies, then follow the matching quickstart and Authentication.

A selection group is identified by a plain string ID. Charts join a group through groupId on their runtime instance. When a user brushes or clicks datapoints, the chart writes a pending selection into the group. Pending selections highlight linked charts but do not re-query anything. When pending selections are applied, every chart in the group re-runs its query with the selections as filters. Your page must apply them explicitly.

Cross-chart filtering checks that fields are connected in the semantic layer’s join graph. That check needs the metadata snapshot, and it throws if the snapshot was never loaded. Always load it before mounting:

const getFieldMetadata = client.services.engine.fields.getFieldMetadata;
if (!getFieldMetadata) {
throw new Error('Astrato field metadata is not available.');
}
const fieldMetadata = await getFieldMetadata({
semanticLayerId: 'your-semantic-layer-id',
tenantId: 'your-tenant-id',
});

Pass the result into each instance’s dataBinding.fieldMetadata.

Charts silently ignore selections from fields that are not connected to their dimensions. If two charts use unrelated views, brushing one does not affect the other. Use fields from connected views when linking charts.

The example below uses only the public SDK and visualization APIs.

import { createAstratoClient } from '@astrato-io/sdk';
import {
createVisualizationRuntime,
registerAstratoElements,
registry,
createDefaultVSpec,
} from '@astrato-io/visualizations';
import {
compileVisualizationDefinition,
createVisualizationAvailableFields,
} from '@astrato-io/visualizations/definitions';
import '@astrato-io/visualizations/styles.css';
const tenantId = 'your-tenant-id';
const semanticLayerId = 'your-semantic-layer-id';
const GROUP = 'dashboard'; // any string; same string = linked charts
const client = createAstratoClient({
baseUrl: window.location.origin,
tenantId,
// Your same-origin backend authenticates requests; use a local proxy during development.
visualizationEngine: true,
});
// 1. Field metadata first. Required for cross-chart filtering.
const fieldMetadata = await client.services.engine.fields.getFieldMetadata!({
semanticLayerId,
tenantId,
});
const availableFields = createVisualizationAvailableFields(fieldMetadata);
const runtime = createVisualizationRuntime({ client, registry });
// 2. Two charts, same groupId.
async function addBar(id: string, dimensionKey: string, measureKey: string) {
const compilation = await compileVisualizationDefinition({
definition: {
version: 1,
type: 'bar',
data: {
dimensions: [{ ref: 'd', fieldKey: dimensionKey }],
measures: [{ ref: 'm', fieldKey: measureKey, aggregation: 'SUM' }],
},
},
availableFields,
baseVSpec: createDefaultVSpec('bar'),
});
if (!compilation.success) throw new Error(JSON.stringify(compilation.issues));
runtime.createInstance({
id,
type: 'bar',
groupId: GROUP,
vSpec: compilation.vSpec,
dataBinding: {
tenantId,
semanticLayerId,
fieldMetadata,
queryContext: { objectId: id, tenantId },
},
});
}
await addBar('chart-a', '[Cars].[Manufacturer]', '[Cars].[Sales In Thousands]');
await addBar('chart-b', '[Cars].[Vehicle Type]', '[Cars].[Sales In Thousands]');
registerAstratoElements({ runtime });
// 3. Add controls for applying and clearing selections.
const selections = client.services.selections;
document.querySelector('#apply')!.addEventListener('click', () => {
selections.applyPendingSelections(GROUP);
});
document.querySelector('#clear')!.addEventListener('click', () => {
selections.clearAllSelections(GROUP);
});
// 4. Optional: show the current state.
selections.onActiveSelectionChange(GROUP).subscribe(({ applied, pending }) => {
console.log('applied:', applied, 'pending:', pending);
});
<astrato-visualization instance-id="chart-a"></astrato-visualization>
<astrato-visualization instance-id="chart-b"></astrato-visualization>
<button id="apply">Apply selections</button>
<button id="clear">Clear</button>
<style>
astrato-visualization {
display: block;
width: 100%;
height: 400px;
}
</style>

What happens when the user brushes chart A:

  1. Chart A writes a pending selection into group dashboard.
  2. Chart B highlights the matching datapoints. No query runs.
  3. The user clicks Apply selections. Pending selections become applied.
  4. Every chart in the group re-queries with the selections as filters. Chart B now shows filtered data.

If you want brush-to-filter without an Apply button, apply pending selections shortly after they arrive:

const APPLY_DELAY_MS = 350;
let applyTimer: ReturnType<typeof setTimeout> | undefined;
selections.onActiveSelectionChangePending(GROUP).subscribe((state) => {
clearTimeout(applyTimer);
if (state.pending.length > 0) {
applyTimer = setTimeout(() => selections.applyPendingSelections(GROUP), APPLY_DELAY_MS);
}
});

The delay groups a run of clicks or a brush into one apply, so each chart re-queries once. Applying from a timer also keeps applyPendingSelections, which emits synchronously, out of the subscriber.

Keep the Clear control from the example above. Applying or cancelling pending selections can leave pending empty while filters remain applied; an empty pending state is not a request to clear them. For per-filter removal, use removeSelection from the selection API.

The Workbook Editor uses an explicit Apply button. Applied selections trigger one query per chart in the group, so apply without a delay turns rapid brushing into query bursts.

Your page can also write selections directly, which is useful for custom filter panels:

selections.selectValues(GROUP, {
field: { fieldKey: '[Cars].[Manufacturer]' },
operator: 'inList',
values: ['Volvo', 'Saab'],
});

Keep these behaviors in mind:

  • In the default 'auto' mode, selectValues writes directly to applied. Charts write pending selections explicitly. Call setSelectionMode('manual', GROUP) if selections from your code should also wait for confirmation.
  • Each field and kind has at most one selection. Calling selectValues again replaces it instead of appending values, so build the complete value list first.
  • Selections written by charts carry extra properties beyond the documented AstratoSelection shape (extended field descriptors, date parts). Read what you need; write the documented shape.

All three observables emit the full { applied, pending } state and replay the current value on subscribe:

const subscription = selections.onActiveSelectionChangeApplied(GROUP).subscribe((state) => {
renderFilterChips(state.applied);
});

If your subscriber writes back into the selection service (for example, absorbing a selection into your own filter state and removing it), guard against re-entry: removeSelection emits synchronously from inside your subscriber.

You do not translate selections into query filters — charts do it themselves at query-build time. Only applied selections of kind 'VALUE' become filters, and only for charts whose fields are connected to the selection’s field.

For chart-local filters that are part of the chart’s configuration (not user interaction), use the setStaticFilterGroup operation — see the Visualizations reference. It replaces all of an object’s static filters with one AND/OR group, and every value is a string:

{
name: 'setStaticFilterGroup',
payload: {
operator: 'and',
filters: [{ fieldKey: '[Sales].[Region]', operator: 'equals', values: { value: 'West' } }],
},
}

Static filters live in the vSpec and are independent of selection groups. Do not use selections for configuration or static filters for interactivity. After an upgrade, check that a filtered object’s query still returns filtered rows; see Operation names are per type.

For a fixed date range on a date view field, use UTC epoch milliseconds as decimal strings in values.valueFrom and values.valueTo. Use the exact view-field key exposed by the visualization. For example, the bounds below represent midnight UTC on 1 January and 31 December 2019:

{
name: 'setStaticFilterGroup',
payload: {
operator: 'and',
filters: [{
fieldKey: '[Sales].[Order Date]',
operator: 'isBetween',
values: { valueFrom: '1546300800000', valueTo: '1577750400000' },
}],
},
}

Convert a calendar date with new Date('2019-01-01T00:00:00Z').getTime().toString(). An ISO date string satisfies the operation’s generic string schema but does not become a fixed date filter in the visualization’s date-value conversion. The SDK getData() query filter has a different shape and uses ISO valueDate bounds; see Periods and date ranges. After mounting or changing the chart, verify that the rendered data reflects the filter.

  • Variables are not an interactivity mechanism. They are read-only, per-instance, and charts get no notification when they change. Use selections.
  • Selections do not persist. They are stored in memory and are lost when the page reloads. These packages do not provide URL serialization or bookmarks.
  • Drill-through, chart actions, and navigation are not part of the SDK or visualizations packages. They are available in Workbooks but not in standalone visualizations.
  • No filter-bar component. The packages ship filter, date-picker, and slider objects, but the Workbook filter bar (with its confirm/history UI) is not included. The page provides any Apply, clear, or filter-summary controls its interaction model needs.
  • Visualization objects that summarize current selections render blank when used standalone. Display the selection state in the surrounding page instead.