Legends help viewers understand the color, shape, and line encodings in your visualization. Semiotic supports legends on all Frame types through the legend prop. When set to true, Semiotic auto-generates legend entries from your data. You can also pass a configuration object for full control over legend groups, styling, and positioning.
With Charts
Chart components (LineChart, BarChart, etc.) automatically generate legends when you use colorBy to color data by category. The legend is shown by default when multiple series are detected. You can control it with the showLegend prop:
JSX
import { LineChart } from "semiotic" // Legend auto-generated from colorBy categories <LineChart data={salesData} xAccessor="month" yAccessor="revenue" lineBy="region" colorBy="region" showLegend={true} /> // Disable the auto-legend <LineChart data={salesData} xAccessor="month" yAccessor="revenue" lineBy="region" colorBy="region" showLegend={false} />
For more control over the legend from a Chart component, use theframeProps escape hatch to pass a full legend configuration object.
With Frames
Auto-Generated Legend (StreamXYFrame)
On StreamXYFrame, setting legend={true}automatically creates legend items from the lines in your data. The legend type (line swatch vs. fill swatch) is determined by the line type: stacked area types use fill swatches, while regular lines use line swatches. The label for each item comes from thelineIDAccessor.
import { StreamXYFrame } from "semiotic" (d) => ({ stroke: colorMap[d.label] || d.color, strokeWidth: 2 }) const frameProps = { /* --- Data --- */ data: [ { month: 1, value: 12, label: "Revenue", color: "#ac58e5" }, { month: 2, value: 18, label: "Revenue", color: "#ac58e5" }, // ...more flat data points for Revenue, Costs, Profit ], chartType: "line", groupAccessor: "label", /* --- Size --- */ margin: { top: 20, bottom: 50, left: 60, right: 120 }, /* --- Process --- */ xAccessor: "month", yAccessor: "value", /* --- Customize --- */ lineStyle: lineStyle, title: "Revenue, Costs, and Profit", showAxes: true, xLabel: "Month", yLabel: "Value", legend: { title: "Series", legendGroups: [{ type: "line", label: "", styleFn: d => ({ stroke: d.color, strokeWidth: 2, fill: "none" }), items: [ { label: "Revenue", color: "#ac58e5" }, { label: "Costs", color: "#E0488B" }, { label: "Profit", color: "#9fd0cb" }, ], }], } } export default () => { return <StreamXYFrame {...frameProps} /> }
Custom Legend Configuration
For full control, pass an object with legendGroups to define exactly what appears in the legend, how it is styled, and how items are labeled.
import { StreamOrdinalFrame } from "semiotic" d => ({ fill: regionColors[d.region], stroke: "white", strokeWidth: 1, }) const frameProps = { /* --- Data --- */ data: [ { category: "Q1", value: 35, region: "North" }, { category: "Q2", value: 42, region: "North" }, // ...more data for North, South, East ], chartType: "clusterbar", /* --- Size --- */ margin: { top: 20, bottom: 40, left: 60, right: 130 }, /* --- Layout --- */ oPadding: 10, /* --- Process --- */ oAccessor: "category", rAccessor: "value", /* --- Customize --- */ pieceStyle: pieceStyle, title: "Quarterly Sales by Region", showAxes: true, legend: { title: "Region", legendGroups: [ { type: "fill", label: "", styleFn: d => ({ fill: regionColors[d.label], stroke: regionColors[d.label], }), items: [ { label: "North" }, { label: "South" }, { label: "East" }, ], }, ], }, /* --- Annotate --- */ rLabel: "Sales ($K)" } export default () => { return <StreamOrdinalFrame {...frameProps} /> }
Legend Positioning
Chart components accept a legendPosition prop to place the legend on any side of the chart: "right" (default),"left", "top", or "bottom". Top and bottom legends are laid out horizontally and the chart margin adjusts automatically to make room.
Top Legend
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, value: 12, label: "Revenue" }, { month: 2, value: 18, label: "Revenue" }, // ...flat data with label field ], /* --- Layout --- */ lineBy: "label", /* --- Process --- */ xAccessor: "month", yAccessor: "value", /* --- Customize --- */ title: "Legend on Top", xLabel: "Month", yLabel: "Value", colorBy: "label", showLegend: true, legendPosition: "top" } export default () => { return <LineChart {...frameProps} /> }
Bottom Legend
import { BarChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { category: "Q1", value: 35, region: "North" }, { category: "Q2", value: 42, region: "North" }, // ...data with region field ], /* --- Process --- */ valueAccessor: "value", categoryAccessor: "category", /* --- Customize --- */ title: "Legend on Bottom", colorBy: "region", showLegend: true, legendPosition: "bottom" } export default () => { return <BarChart {...frameProps} /> }
Legend Interaction (Charts)
Chart components support built-in legend interaction via thelegendInteraction prop. This provides two interaction modes inspired by the Carbon Design System.
Highlight on Hover
Hover over a legend item to dim all other categories to 30% opacity:
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, value: 12, label: "Revenue" }, { month: 2, value: 18, label: "Revenue" }, // ...flat data with label field ], /* --- Layout --- */ lineBy: "label", /* --- Process --- */ xAccessor: "month", yAccessor: "value", /* --- Customize --- */ xLabel: "Month", yLabel: "Value", colorBy: "label", legendInteraction: "highlight" } export default () => { return <LineChart {...frameProps} /> }
Isolate on Click
Click legend items to toggle category visibility. A checkmark indicates active categories. When all categories are re-selected, the legend resets.
import { StackedBarChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { category: "Q1", value: 35, region: "North" }, { category: "Q2", value: 42, region: "North" }, // ...data with region field ], /* --- Layout --- */ stackBy: "region", /* --- Process --- */ valueAccessor: "value", categoryAccessor: "category", /* --- Customize --- */ colorBy: "region", legendInteraction: "isolate" } export default () => { return <StackedBarChart {...frameProps} /> }
Legend interaction integrates with the selection system, so it composes naturally with LinkedCharts cross-highlighting. Both the legend interaction and linked hover predicates are merged — a datum must satisfy both to remain at full opacity.
Configuration
| Prop | Type | Required | Default | Description |
|---|
legend | boolean | object | — | undefined | Enables and configures the legend. When set to true, Semiotic auto-generates legend items from the data. When an object, allows full customization of legend groups, positioning, and behavior. |
legend.legendGroups | array | — | Auto-generated | Array of legend group objects. Each group has: type ("fill" | "line" | function), styleFn (function returning style object for each item), items (array of { label, ...data }), and label (group label string). |
legend.title | string | false | — | "Legend" | Title displayed above the legend. Set to false to hide the title. |
legend.width | number | — | 100 | Width of the legend area in pixels. |
legend.height | number | — | 20 | Height of the legend area (primarily used for horizontal orientation). |
legend.orientation | "vertical" | "horizontal" | — | "vertical" | Layout direction of the legend items. Vertical stacks items, horizontal places them in a row. |
legend.position | "left" | "right" | — | "right" | Which side of the frame the legend appears on. For Chart components, use the legendPosition prop instead, which also supports "top" and "bottom". |
Legend Groups
A legendGroups array lets you define one or more groups of legend items. Each group has its own type, style function, and items. This is useful when a single visualization encodes multiple dimensions (e.g., color for category and size for magnitude).
JSX
| 1 | <StreamXYFrame |
| 2 | legend={{ |
| 3 | title: "Legend", |
| 4 | legendGroups: [ |
| 5 | { |
| 6 | type: "line", // "fill" for filled swatches, "line" for strokes |
| 7 | label: "Series", |
| 8 | styleFn: d => ({ |
| 9 | stroke: d.color, |
| 10 | strokeWidth: 2, |
| 11 | fill: "none", |
| 12 | }), |
| 13 | items: [ |
| 14 | { label: "Revenue", color: "#ac58e5" }, |
| 15 | { label: "Costs", color: "#E0488B" }, |
| 16 | ], |
| 17 | }, |
| 18 | { |
| 19 | type: "fill", |
| 20 | label: "Region", |
| 21 | styleFn: d => ({ fill: d.color }), |
| 22 | items: [ |
| 23 | { label: "North", color: "#9fd0cb" }, |
| 24 | { label: "South", color: "#e0d33a" }, |
| 25 | ], |
| 26 | }, |
| 27 | ], |
| 28 | }} |
| 29 | /> |
Legend Item Types
The type property on a legend group determines the shape of the swatch:
"fill" — renders a 16×16 filled rectangle. Best for area charts, bars, and filled marks."line" — renders a diagonal line stroke. Best for line charts and stroked marks.- Function — pass a custom render function that receives the item data and returns SVG elements for complete control.
JSX
// Custom legend glyph using a function type { type: item => ( <circle r={item.size || 8} fill={item.color} cx={10} cy={10} /> ), styleFn: () => ({}), items: [ { label: "Small", color: "#ac58e5", size: 4 }, { label: "Medium", color: "#E0488B", size: 8 }, { label: "Large", color: "#9fd0cb", size: 12 }, ], label: "Size", }
Gradient Legends
For continuous or sequential color scales — such as those used inHeatmap — Semiotic supports gradient legends. On the Heatmap component, simply setshowLegend to render a gradient bar that maps the value domain to the active color scheme. For Stream Frames, you can pass agradient configuration in the legend object with a custom color function, domain, and label.
JSX
// Heatmap — gradient legend via showLegend prop import { Heatmap } from "semiotic" <Heatmap data={matrixData} xAccessor="hour" yAccessor="day" valueAccessor="count" colorScheme="viridis" showLegend legendPosition="right" /> // Stream Frame — gradient legend via legend config <StreamXYFrame legend={{ gradient: { colorFn: d3.interpolateViridis, domain: [0, 100], label: "Intensity", }, }} />
Horizontal Legends
Set orientation: "horizontal" to lay out legend items in a row. This works well when placed above or below a chart:
JSX
<StreamXYFrame legend={{ orientation: "horizontal", title: false, // hide title for horizontal layout legendGroups: [{ type: "fill", styleFn: d => ({ fill: d.color }), items: [ { label: "Revenue", color: "#ac58e5" }, { label: "Costs", color: "#E0488B" }, { label: "Profit", color: "#9fd0cb" }, ], label: "", }], }} />
Custom Interactive Legends (Frames)
For Frame-level charts, use the customClickBehaviorcallback on the legend configuration with your own state management:
JSX
| 1 | import { useState } from "react" |
| 2 | |
| 3 | function InteractiveChart({ data }) { |
| 4 | const [hiddenSeries, setHiddenSeries] = useState(new Set()) |
| 5 | |
| 6 | const toggleSeries = item => { |
| 7 | setHiddenSeries(prev => { |
| 8 | const next = new Set(prev) |
| 9 | if (next.has(item.label)) { |
| 10 | next.delete(item.label) |
| 11 | } else { |
| 12 | next.add(item.label) |
| 13 | } |
| 14 | return next |
| 15 | }) |
| 16 | } |
| 17 | |
| 18 | const visibleLines = data.filter( |
| 19 | d => !hiddenSeries.has(d.label) |
| 20 | ) |
| 21 | |
| 22 | return ( |
| 23 | <StreamXYFrame |
| 24 | data={visibleLines} |
| 25 | chartType="line" |
| 26 | lineDataAccessor="coordinates" |
| 27 | xAccessor="month" |
| 28 | yAccessor="value" |
| 29 | lineStyle={d => ({ stroke: d.color, strokeWidth: 2 })} |
| 30 | lineIDAccessor="label" |
| 31 | legend={{ |
| 32 | customClickBehavior: toggleSeries, |
| 33 | legendGroups: [{ |
| 34 | type: "line", |
| 35 | styleFn: d => ({ |
| 36 | stroke: d.color, |
| 37 | opacity: hiddenSeries.has(d.label) ? 0.3 : 1, |
| 38 | }), |
| 39 | items: data.map(d => ({ |
| 40 | label: d.label, |
| 41 | color: d.color, |
| 42 | })), |
| 43 | label: "", |
| 44 | }], |
| 45 | }} |
| 46 | /> |
| 47 | ) |
| 48 | } |
Legend with Chart API
Chart components auto-generate legends from your colorByand data. To customize the auto-generated legend further, pass a legend configuration through frameProps:
JSX
import { LineChart } from "semiotic" <LineChart data={salesData} xAccessor="month" yAccessor="revenue" lineBy="product" colorBy="product" frameProps={{ legend: { title: "Products", orientation: "horizontal", }, }} />