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. A row must match every other chart's active filter. With two brushes active, a third chart shows their intersection:
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.
Skip to data tablesetosa
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")
Point selections match exact values. Two valid Date objects match when their timestamps are equal; numbers and strings remain distinct. Interval selections use inclusive numeric bounds and accept finite numbers or valid Dates measured in epoch milliseconds. Missing values, strings, booleans, arrays, and invalid Dates do not match an interval, even when it includes zero.