Skip to content
quartiledocsv0.1
GitHub

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.

SelectionNothing selected. Brush the daily bars or click a bar.

Revenue per day of Date, Sep 7 to Oct 6: total $383,658.96. Highest: Sep 23 ($15,310.22).

Revenue per day
DateRevenue
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.

Revenue by channel
ChannelRevenue
Organic search$119.9K
Paid social$117.3K
Email$91.1K
Direct$55.4K
North America1,235$160.0K
Europe796$104.0K
Asia Pacific513$68.7K
Latin America244$31.8K
Middle East & Africa156$19.1K
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

ComponentPublishesReads
LineChart, AreaChartbrush: a between range on xyes
Histogrambrush: a between range on xyes
BarChartselect: toggles the x valueyes
BarList, DonutChart, BoxPlotselect: toggles the categoryyes
ScatterPlotselect: the color, label or a named fieldyes
Sankeyselect: a node on from or toyes
CalendarHeatmapselect: a day or a range of daysyes
Heatmap, Funnel, Sparkline—yes
FilterBarchip menus publish in predicatesyes
DataTableselect: the row's field valueyes
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>
  ));
}
Filtering runs in memory over the rows you pass. Recorded benchmarks cover specific workloads, not a universal row limit. See Performance and bounded worker queries.
Report an issueBuilt Oct 21, 2026