Interactivity guide
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.
How selection groups work
Section titled “How selection groups work”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.
Prerequisite: load field metadata first
Section titled “Prerequisite: load field metadata first”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.
Linking two charts
Section titled “Linking two 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:
- Chart A writes a pending selection into group
dashboard. - Chart B highlights the matching datapoints. No query runs.
- The user clicks Apply selections. Pending selections become applied.
- Every chart in the group re-queries with the selections as filters. Chart B now shows filtered data.
Auto-apply variant
Section titled “Auto-apply variant”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.
Driving selections from your own UI
Section titled “Driving selections from your own UI”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,selectValueswrites directly toapplied. Charts write pending selections explicitly. CallsetSelectionMode('manual', GROUP)if selections from your code should also wait for confirmation. - Each field and kind has at most one selection. Calling
selectValuesagain replaces it instead of appending values, so build the complete value list first. - Selections written by charts carry extra properties beyond the documented
AstratoSelectionshape (extended field descriptors, date parts). Read what you need; write the documented shape.
Reacting to selection changes
Section titled “Reacting to selection changes”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.
Filters: how selections reach the query
Section titled “Filters: how selections reach the query”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.
Limitations
Section titled “Limitations”- 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, andsliderobjects, 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.