Linked selections
Wrap views in <Selection> and brushing or clicking one view cross-filters every other view; a view is never filtered by its own selection.
How it works
A <Selection> holds a list of predicates, one per field: eq, in or between. Components inside it publish predicates (a brush publishes between, a click toggles a value in in) tagged with their own source id. Every component reads its rows through the predicates of the other sources, so the view you are brushing keeps showing all of its data.
Revenue per day of Date, Sep 7 to Oct 6: total $383,658.96. Highest: Sep 23 ($15,310.22).
| Date | Revenue |
|---|---|
| Sep 7 | $10,351.36 |
| Sep 8 | $11,382.74 |
| Sep 9 | $12,681.26 |
| Sep 10 | $11,817.67 |
| Sep 11 | $12,131.45 |
| Sep 12 | $9,551.33 |
| Sep 13 | $10,160.03 |
| Sep 14 | $12,586.99 |
| Sep 15 | $12,098.52 |
| Sep 16 | $13,253.70 |
| Sep 17 | $12,248.66 |
| Sep 18 | $15,193.38 |
| Sep 19 | $11,383.75 |
| Sep 20 | $10,556.65 |
| Sep 21 | $13,912.14 |
| Sep 22 | $14,638.15 |
| Sep 23 | $15,310.22 |
| Sep 24 | $14,331.12 |
| Sep 25 | $14,904.38 |
| Sep 26 | $13,249.96 |
| Sep 27 | $11,743.45 |
| Sep 28 | $12,505.08 |
| Sep 29 | $13,760.55 |
| Sep 30 | $15,036.34 |
| Oct 1 | $12,636.15 |
| Oct 2 | $13,479.01 |
| Oct 3 | $13,249.25 |
| Oct 4 | $10,684.70 |
| Oct 5 | $15,016.97 |
| Oct 6 | $13,804.00 |
4 bars. Highest Organic search at $119.9K; lowest Direct at $55.4K.
| Channel | Revenue |
|---|---|
| Organic search | $119.9K |
| Paid social | $117.3K |
| $91.1K | |
| Direct | $55.4K |
import {
BarChart, Button, DataTable, Histogram, Selection, describePredicate, useSelection,
} from '@quartile/react';
function SelectionLog() {
const selection = useSelection();
return (
<div>
{selection.predicates.map((p) => <code key={p.field}>{describePredicate(p)}</code>)}
<Button size="sm" variant="ghost" onClick={() => selection.clear()}>Clear</Button>
</div>
);
}
type Order = { id: string; date: string; region: string; channel: string; amount: number };
export function Orders({ orders }: { orders: Order[] }) {
return (
<Selection id="orders">
<SelectionLog />
<Histogram data={orders} x="date" value="amount" interval="day" brush />
<BarChart data={orders} x="channel" y="amount" select sort="desc" />
<DataTable
data={orders}
groupBy="region"
defaultSort="-revenue"
columns={[
{ field: 'region' },
{ key: 'orders', field: 'id', label: 'Orders', aggregate: 'count' },
{ key: 'revenue', field: 'amount', label: 'Revenue', aggregate: 'sum', cell: 'bar', format: 'currency-compact' },
]}
/>
</Selection>
);
}What publishes and what reads
| Component | Publishes | Reads |
|---|---|---|
| LineChart, AreaChart | brush: a between range on x | yes |
| Histogram | brush: a between range on x | yes |
| BarChart | select: toggles the x value | yes |
| BarList, DonutChart, BoxPlot | select: toggles the category | yes |
| ScatterPlot | select: the color, label or a named field | yes |
| Sankey | select: a node on from or to | yes |
| CalendarHeatmap | select: a day or a range of days | yes |
| Heatmap, Funnel, Sparkline | — | yes |
| FilterBar | chip menus publish in predicates | yes |
| DataTable | select: the row's field value | yes |
| KPI, KPIGroup | — | yes, with data |
Naming and scoping selections
Components use the nearest <Selection>. Give one an id and pass selection="orders" to read it from elsewhere in the tree, or selection={false} to opt a component out. onChange on <Selection> receives every predicate after each change, and each data component has onSelect for its own.
Your own views
useSelection() reads and writes the store: predicates, set, toggle, clear and filter. useLinkedRows gives a component its rows with its own predicates left out, which is what the built-in components use.
import { useLinkedRows, useSelection, useSourceId } from '@quartile/react';
type RegionalRow = { region: string };
export function RegionChips({ rows }: { rows: RegionalRow[] }) {
const source = useSourceId();
// Rows filtered by every predicate except the ones this component published.
const { rows: visible } = useLinkedRows(rows, { source });
const selection = useSelection();
const regions = [...new Set(visible.map((r) => r.region))];
return regions.map((region) => (
<button key={region} onClick={() => selection.toggle('region', region, { source })}>
{region}
</button>
));
}