LinkedCharts is a React Context provider that enables cross-highlighting, brushing-and-linking, and cross-filtering between any Semiotic chart components at any depth in the tree. Charts opt in via the selection, linkedHover, andlinkedBrush props.
Shared categories →CategoryColorProvider. When two or more charts inside LinkedCharts encode the same categorical field (e.g. both use colorBy="region"), wrap the whole block inCategoryColorProvider. That gives every chart the same color for each category and tells LinkedCharts to render one unified legend — otherwise each chart renders its own, which is both visually redundant and can use different colors for the same category. SeeUnified Legend below.
Example
Hover over a point in the scatterplot to highlight the matching region in the bar chart, and vice versa:
JSX
import { LinkedCharts, Scatterplot, BarChart } from "semiotic" const salesData = [ { month: "Jan", revenue: 42, region: "North", spend: 18 }, { month: "Feb", revenue: 51, region: "North", spend: 22 }, // ... ] const regionTotals = ["North", "South", "East", "West"].map(region => ({ region, total: salesData .filter(d => d.region === region) .reduce((s, d) => s + d.revenue, 0), })) <LinkedCharts> <Scatterplot data={salesData} xAccessor="revenue" yAccessor="spend" colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} /> <BarChart data={regionTotals} categoryAccessor="region" valueAccessor="total" colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} /> </LinkedCharts>
How It Works
LinkedCharts wraps your charts in a shared selection store. Each chart can produce selections (vialinkedHover or linkedBrush) andconsume them (via selection). When a selection is active, non-matching elements are dimmed.
Selection Props on Charts
All HOC chart components accept these coordination props when used inside LinkedCharts:
JSX
// selection — consume a named selection (dims unmatched elements) selection={{ name: "mySelection", unselectedOpacity: 0.2 }} // linkedHover — produce hover-based selections linkedHover={{ name: "hl", fields: ["category"] }} linkedHover={true} // shorthand: name="hover", auto-detect fields linkedHover="myHoverName" // shorthand: custom name, auto-detect fields // linkedHover modes (mutually exclusive): // "field" (default) — selection keys off the given/auto fields // "x-position" — temporal crosshair: { mode: "x-position", xField: "time" } // "series" — series↔bar cross-highlight: auto-resolves the chart's // series field (colorBy/lineBy/areaBy/stackBy/groupBy) so // hovering a bar highlights the matching line, no fields wiring linkedHover={{ name: "hl", mode: "series" }} linkedHover={{ name: "hl", mode: "series", seriesField: "region" }} // override the resolved field // linkedBrush — produce brush-based selections (Scatterplot, BubbleChart only) linkedBrush={{ name: "brush", xField: "x", yField: "y" }} linkedBrush="selectionName" // shorthand
Brush-and-Link
Drag to brush a region in the scatterplot below — the bar chart updates to show only data within the brushed area:
JSX
import { LinkedCharts, Scatterplot, BarChart, useFilteredData } from "semiotic" function FilteredBarChart({ data }) { const filtered = useFilteredData(data, "brush") const totals = ["North", "South", "East", "West"].map(r => ({ region: r, total: filtered.filter(d => d.region === r).reduce((s, d) => s + d.revenue, 0), })) return ( <BarChart data={totals} categoryAccessor="region" valueAccessor="total" colorBy="region" width={320} height={300} valueLabel="Revenue" /> ) } <LinkedCharts> <Scatterplot data={salesData} xAccessor="revenue" yAccessor="spend" colorBy="region" linkedBrush={{ name: "brush", xField: "revenue", yField: "spend" }} selection={{ name: "brush" }} /> <FilteredBarChart data={salesData} /> </LinkedCharts>
Cross-Filtering
With resolution: "crossfilter", each chart's own brush is excluded from its filter — the standard SPLOM interaction model:
JSX
<LinkedCharts selections={{ dash: { resolution: "crossfilter" } }}> <Scatterplot data={data} xAccessor="age" yAccessor="income" linkedBrush={{ name: "dash", xField: "age", yField: "income" }} selection={{ name: "dash", unselectedOpacity: 0.05 }} /> <BarChart data={data} categoryAccessor="region" valueAccessor="count" selection={{ name: "dash" }} /> </LinkedCharts>
Unified Legend
When charts inside LinkedCharts share categories (viaCategoryColorProvider), LinkedCharts renders a single unified legend and automatically suppresses individual chart legends. The unified legend supports the same legendInteractionmodes as individual charts — hover a legend item below to highlight that region across both charts:
JSX
import { LinkedCharts, CategoryColorProvider, Scatterplot, BarChart } from "semiotic" <CategoryColorProvider categories={["North", "South", "East", "West"]}> <LinkedCharts showLegend legendInteraction="highlight" // or "isolate" legendSelectionName="hl" // matches child selection names legendField="region" // field to filter on > <Scatterplot data={salesData} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} /> <BarChart data={regionTotals} colorBy="region" linkedHover={{ name: "hl", fields: ["region"] }} selection={{ name: "hl" }} /> </LinkedCharts> </CategoryColorProvider>
Use legendInteraction="isolate" to let users click legend items to toggle category visibility with checkmark indicators. SetlegendPosition="bottom" to place the legend below the charts. Set showLegend={false} to disable the unified legend entirely and let child charts manage their own.
LinkedCharts Props
| Prop | Type | Required | Default | Description |
|---|
selections | Record<string, { resolution?: "union" | "intersect" | "crossfilter" }> | — | undefined | Pre-configure named selections with a resolution mode. For cross-filtering dashboards, use { mySelection: { resolution: "crossfilter" } }. |
showLegend | boolean | — | true (when CategoryColorProvider present) | Show a unified legend for all linked charts. When true, child chart legends are automatically suppressed unless explicitly set. |
legendPosition | "top" | "bottom" | — | "top" | Position of the unified legend relative to the linked charts. |
legendInteraction | "highlight" | "isolate" | "none" | — | "none" | Legend interaction mode. "highlight" dims non-hovered categories across all linked charts on hover. "isolate" toggles category visibility on click. |
legendSelectionName | string | — | "legend" | Selection name that the unified legend produces on. Child charts must use the same name in their selection prop. |
legendField | string | — | "category" | Field name that the unified legend uses for cross-chart highlighting. Must match the field used in child charts' linkedHover.fields / colorBy. |
ScatterplotMatrix
The ScatterplotMatrix (SPLOM) renders an N x N grid of scatterplots with built-in cross-filter brushing. Diagonal cells show histograms. Brushing one cell highlights matching points in all other cells. It uses LinkedCharts internally.
setosa
versicolor
virginica
Sepal L
Sepal W
Petal L
Petal W
Sepal L
Sepal W
Petal L
Petal W
JSX
import { ScatterplotMatrix } from "semiotic" <ScatterplotMatrix data={iris} fields={["sepalLength", "sepalWidth", "petalLength", "petalWidth"]} fieldLabels={{ sepalLength: "Sepal L", sepalWidth: "Sepal W", petalLength: "Petal L", petalWidth: "Petal W", }} colorBy="species" cellSize={140} diagonal="histogram" brushMode="crossfilter" />
ScatterplotMatrix Props
| Prop | Type | Required | Default | Description |
|---|
data | TDatum[] | Yes | - | Array of data objects. |
fields | string[] | Yes | - | Array of field names to include in the matrix. |
fieldLabels | Record<string, string> | — | {} | Display labels for each field. |
colorBy | string | function | — | undefined | Color encoding for points. |
cellSize | number | — | 150 | Pixel size of each cell. |
diagonal | "histogram" | "density" | "label" | — | "histogram" | What to render on diagonal cells. |
brushMode | "crossfilter" | "intersect" | false | — | "crossfilter" | Brush interaction mode. Crossfilter excludes the brushing cell from its own filter. |
unselectedOpacity | number | — | 0.1 | Opacity for non-matching points when a selection is active. |
Selection Hooks
For custom coordinated views beyond the built-in chart props, Semiotic exports these hooks:
JSX
import { useSelection, // low-level: full control over selection clauses useLinkedHover, // convenience: hover-based cross-highlighting useBrushSelection, // convenience: brush-based cross-filtering useFilteredData, // returns data filtered by a named selection } from "semiotic" // useSelection — full control const { predicate, isActive, selectPoints, selectInterval, clear } = useSelection({ name: "mySelection" }) // useLinkedHover — hover convenience const { onHover, predicate, isActive } = useLinkedHover({ name: "hover", fields: ["category"] }) // useBrushSelection — brush convenience const { brushInteraction, predicate, isActive, clear } = useBrushSelection({ name: "brush", xField: "x", yField: "y" }) // useFilteredData — derived filtered array const filtered = useFilteredData(data, "mySelection")