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. Left and right legends reserve space from their longest label when the legend side is omitted or set to "auto".
A numeric margin is a minimum. Semiotic preserves it when it is large enough and grows it when the chart-owned legend needs more room, so a complete margin object composes with automatic reservation:
JSX
// 24px is the baseline; Semiotic grows it to fit the right legend <LineChart {...props} legendPosition="right" margin={{ top: 24, right: 24, bottom: 48, left: 64 }} legend={{ legendGroups: [], legendDistance: 18 }} /> // The same contract works symmetrically on the left <LineChart {...props} legendPosition="left" margin={{ top: 24, right: 24, bottom: 48, left: 24 }} />
legendDistance belongs to the legend configuration and defaults to 10 pixels. It measures the gap between the nearest legend edge and the plot, or from the outside of a configuredlegendLayout.sideGutter. The side gutter is useful when an axis title must remain next to its axis while the legend sits beyond it;MultiAxisLineChart configures that ordering automatically. Neither option adds padding at the outside edge of the SVG.
A bottom legend measures that same gap from the outside of its axis chrome rather than from the plot edge, so it sits below the tick labels instead of on top of them. The reserved band is derived from the axis actually drawn — tick labels alone, or tick labels plus an axis title, widening again when autoRotate turns the tick labels — and is zero for charts with no bottom axis, such as pie and donut.
Auto-measurement is bottom-axis only. A top axis is opt-in throughframeProps.axes, so a top legend reserves nothing unless you set legendLayout.axisGutter yourself. That same option overrides the measured bottom band, andaxisGutter: 0 opts out entirely, anchoring the legend directly to the plot edge.
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.legendDistance | number | — | 10 | Gap in pixels between the nearest legend edge and the plot edge. This does not add padding at the outer SVG edge. |
legendLayout.sideGutter | number | — | 0 | Space reserved between the plot and a left/right legend for adjacent chrome such as axis ticks and an axis title. MultiAxisLineChart supplies this automatically. |
legendLayout.axisGutter | number | — | measured bottom-axis chrome; 0 for a top legend | The top/bottom counterpart of sideGutter. A bottom legend defaults to the chrome of the axis actually drawn (tick labels, plus an axis title when present, wider again for rotated ticks) so it clears the labels instead of overdrawing them, and to zero when there is no bottom axis. Auto-measurement is bottom-axis only — top axes are opt-in, so a top legend needs this set explicitly. Set 0 to anchor the legend directly to the plot edge. |
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", }, }} />