import { LineChart } from "semiotic"
LineChart visualizes trends and time series data. Pass your data, specify the x and y accessors, and get a publication-ready chart with hover interactions, axes, and legends — all with sensible defaults.
Date objects and date strings select calendar ticks automatically. Date-only strings such as 2024-01 and 2024-01-01 use UTC midnight; automatic tick labels also use UTC. Timestamps with an explicit offset preserve that instant, while date-time strings without an offset use the runtime's local timezone. Use xScaleType="time" for local calendar ticks and an explicit xFormat for local labels. Numeric timestamps keep a linear scale unless you specify xScaleType="time".
Quick Start
The simplest line chart requires just data, xAccessor, andyAccessor.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, revenue: 12000 }, { month: 2, revenue: 18000 }, { month: 3, revenue: 14000 }, // ...more data points ], /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)" } export default () => { return <LineChart {...frameProps} /> }
Examples
Multiple Lines
Use lineBy to group data points into separate lines, and colorByto color them by category.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, revenue: 12000, product: "Widget" }, { month: 2, revenue: 18000, product: "Widget" }, // ...data with product field for grouping ], /* --- Layout --- */ lineBy: "product", /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)", colorBy: "product" } export default () => { return <LineChart {...frameProps} /> }
With Points and Smooth Curve
Enable showPoints and set curve to "monotoneX" for a smooth interpolation with visible data points.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: salesData, /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)", pointRadius: 5, curve: "monotoneX", showPoints: true } export default () => { return <LineChart {...frameProps} /> }
Area Fill
Set fillArea to fill the area beneath the line.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: salesData, /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)", areaOpacity: 0.25, fillArea: true, curve: "monotoneX" } export default () => { return <LineChart {...frameProps} /> }
Confidence Bands (boundsAccessor)
Use frameProps with StreamXYFrame's boundsAccessor to add confidence intervals or error bands around your line. The accessor receives each data point and returns how far the band extends above and below the line value.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, value: 120, upper: 15, lower: 10 }, { month: 2, value: 135, upper: 18, lower: 12 }, { month: 3, value: 128, upper: 20, lower: 14 }, // ...data with upper/lower bounds per point ], /* --- Process --- */ xAccessor: "month", yAccessor: "value", /* --- Customize --- */ xLabel: "Month", yLabel: "Forecast", pointRadius: 3, curve: "monotoneX", showPoints: true, /* --- Annotate --- */ frameProps: { boundsAccessor: d => Math.max(d.upper, d.lower), boundsStyle: { fill: "#6366f1", fillOpacity: 0.15, stroke: "none", }, } } export default () => { return <LineChart {...frameProps} /> }
The boundsAccessor receives each raw data point and returns an offset value. The band is drawn symmetrically at y +/- offset around the line. For asymmetric confidence intervals, use the larger of the two bounds to ensure the full range is covered. This is useful for forecasts, measurement uncertainty, or any scenario where you want to show a range around a trend.
Asymmetric Min/Max Bands (band)
When the upper and lower bounds are not symmetric around the line value — throughput min/max ribbons, percentile spreads, SLO ranges — use the band prop instead of boundsAccessor. Pass y0Accessor andy1Accessor for the bottom and top of the envelope. The ribbon paints under the line, participates in the y-extent auto-derivation so it can't clip, and is non-interactive by default (hovers pass through to the line on top).
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { hour: 0, avg: 82, min: 64, max: 102 }, // ...one row per hour with min/avg/max throughput ], /* --- Process --- */ xAccessor: "hour", yAccessor: "avg", /* --- Customize --- */ xLabel: "Hour", yLabel: "Requests/sec", curve: "monotoneX", /* --- Interact --- */ tooltip: d => ( <div> <strong>{d.hour}:00</strong> avg: {d.avg} min: {d.band?.y0} / max: {d.band?.y1} </div> ), /* --- Other --- */ band: { y0Accessor: "min", y1Accessor: "max", } } export default () => { return <LineChart {...frameProps} /> }
The hovered datum is enriched with band: { y0, y1 } (first band) andbands: [...] (all bands), so custom tooltip functions can render the envelope values without re-running the accessors. The same enrichment flows through the pointer hover path, multi-mode allSeries entries (each series carries its own band values), and keyboard navigation, so every interaction surface sees the same shape.
The default tooltip surfaces band values automatically — pass aband prop without a custom tooltip function and the rendered tooltip gains one row pair per band (low + high). String accessors become the row labels; function accessors fall back to low / high. The default ribbon style is the parent line color at 0.2fillOpacity; override withband.style for full control.
Percentile fan. Pass an array of bands to draw a forecasting fan — outer band first, inner band second. Each layer stacks visually so overlapping fills darken in the middle, which is the standard percentile-ribbon aesthetic.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { day: 0, value: 100, p10: 80, p25: 90, p75: 110, p90: 122 }, // ...one row per day with percentile columns ], /* --- Process --- */ xAccessor: "day", yAccessor: "value", /* --- Customize --- */ xLabel: "Day", yLabel: "Projected value", curve: "monotoneX", /* --- Other --- */ band: [ // Outer p10-p90 envelope drawn first (lighter) { y0Accessor: "p10", y1Accessor: "p90", style: { fill: "#6366f1", fillOpacity: 0.15, stroke: "none" } }, // Inner p25-p75 envelope on top (darker) { y0Accessor: "p25", y1Accessor: "p75", style: { fill: "#6366f1", fillOpacity: 0.30, stroke: "none" } }, ] } export default () => { return <LineChart {...frameProps} /> }
When paired with lineBy / colorBy, each band defaults toperSeries: true — one ribbon per group, colored to match its line. SetperSeries: false for a single aggregate envelope (e.g. an aggregate min/max across all series). Band y0/y1 values feed yExtent auto-derivation, so a tall envelope can never get clipped; explicit yExtent still wins.
Data Gap Handling
Real-world data often has missing values. The gapStrategy prop controls how LineChart handles null, undefined, or NaN values in your y-accessor field. Three strategies are available: break (default) splits the line at gaps, interpolate connects across them, andzero drops to the baseline.
Splits the line into segments at gap boundaries. Each contiguous run of valid data renders as its own line. This is the safest default — it makes missing data visible rather than hiding it behind a smooth connection.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, revenue: 12000, product: "Widget" }, { month: 2, revenue: 18000, product: "Widget" }, { month: 3, revenue: null, product: "Widget" }, // gap { month: 4, revenue: null, product: "Widget" }, // gap { month: 5, revenue: 19000, product: "Widget" }, { month: 6, revenue: 27000, product: "Widget" }, ], /* --- Layout --- */ lineBy: "product", /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)", colorBy: "product", showPoints: true, gapStrategy: "break" } export default () => { return <LineChart {...frameProps} /> }
JSX
// Break the line at gaps (default) — makes missing data visible <LineChart data={data} gapStrategy="break" /> // Connect across gaps — use for sparse but continuous signals <LineChart data={data} gapStrategy="interpolate" /> // Drop to zero at gaps — use for event counts or cumulative metrics <LineChart data={data} gapStrategy="zero" />
Direct Labels
Label each series where the reader is already looking. Enable directLabel to place category names beside the first or last point of each line, with spacing that adapts to the chart size.
Try direct labels
These products finish close together. Turn labels off to compare the legend, then turn them back on to follow each name to its line. Change the controls and copy the resulting chart code below.
import { LineChart } from "semiotic/xy" const frameProps = { /* --- Data --- */ data: data /* Copy includes every row used in this example */, /* --- Size --- */ margin: { top: 40, bottom: 50, left: 70, right: 110 }, height: 320, /* --- Layout --- */ lineBy: "product", /* --- Process --- */ xAccessor: "month", yAccessor: "value", yExtent: [0,100], /* --- Customize --- */ title: "Product trends", xLabel: "Month", yLabel: "Value", yFormat: (value2) => Number(value2).toPrecision(2), colorBy: "product", showPoints: true, directLabel: { position: "end", fontSize: 12 }, /* --- Other --- */ description: "Product values converge at the endpoints. Direct labels identify each series.", accessibleTable: true } export default () => { return <LineChart {...frameProps} /> }
How placement works
lineBy groups the rows into lines; colorBysupplies the category names and matching colors. Each name starts at its series’ first or last x value, depending on position.- Labels share a column in the side margin. When endpoints are close, labels move vertically to make room. Connector lines keep moved labels attached to their endpoints.
- Spacing uses pixels after the data is scaled. Try changing numeric units: the axis values change, but the label spacing stays the same. Resizing and font changes trigger a new placement.
When labels don’t fit
Select 24 products and reduce the chart height to 180px. Labels that cannot fit are omitted. Increase the height to restore vertical space; increase the label-side margin if a name is too wide. Larger fonts need more room in both directions.
All series remain in the chart’s accessible description even when their visible labels are omitted. This example also enables accessibleTable for the full data. For a dense chart, turn onKeep a legend too or use fewer series per chart.
Use it in your chart
Add directLabel to a grouped LineChart for end labels at the default 11px size. Pass an object to choose the side and font size. The default margin reserves space for names; an explicit margin overrides it, so allow enough room on the label side.
JSX
import { LineChart } from "semiotic/xy" const data = [ { month: 1, value: 20, product: "Atlas" }, { month: 2, value: 32, product: "Atlas" }, { month: 1, value: 40, product: "Beacon" }, { month: 2, value: 33, product: "Beacon" } ] <LineChart data={data} xAccessor="month" yAccessor="value" lineBy="product" colorBy="product" directLabel={{ position: "end", fontSize: 12 }} height={320} title="Product trends" description="Atlas and Beacon finish at similar values." accessibleTable />
directLabel={true} uses the defaults. directLabel={{ position: "start" }} labels the first endpoint. The legend is hidden automatically while direct labels are enabled; showLegend={true}keeps it visible.
Server rendering and layout evidence
Server rendering uses the same placement rules with estimated text measurements. The browser measures the loaded font and updates placement after font loading and resizing. Custom annotation offsets are preserved.
For generated charts, renderChartWithEvidence reports requested, rendered, displaced, and omitted counts in evidence.layout, with reason codes and suggested remedies. Estimated text measurements make the assessment incomplete. Axis collision assessment covers browser SVG ticks on charts with direct labels; server axis geometry remains unassessed.
Empty and Loading States
All chart components support built-in loading and emptyContentprops. See the Chart States page for full documentation and examples.
Set tooltip="multi" to show all series values at the hovered x position with color swatches. The tooltip follows the cursor across the rendered x range, so readers can compare series between sampled points as well as directly over them. In x-extent padding before or after the data, the crosshair and values snap to the first or last sample.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: multiLineData, /* --- Layout --- */ lineBy: "product", /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)", colorBy: "product", curve: "monotoneX", /* --- Interact --- */ tooltip: "multi" } export default () => { return <LineChart {...frameProps} /> }
Axis Options
Configure axis behavior with frameProps.axes: force the domain max tick withincludeMax, auto-rotate crowded labels, or use dashed baselines.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: simpleData, /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ showGrid: true, /* --- Annotate --- */ frameProps: { axes: [ { orient: "bottom", includeMax: true, autoRotate: true, gridStyle: "dashed" }, { orient: "left", includeMax: true }, ], } } export default () => { return <LineChart {...frameProps} /> }
Hover Highlight (Sibling Dimming)
Set hoverHighlight="series" to dim non-hovered series when hovering a line. Requires colorBy to identify series.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: multiLineData, /* --- Layout --- */ lineBy: "product", /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ colorBy: "product", /* --- Other --- */ hoverHighlight: "series" } export default () => { return <LineChart {...frameProps} /> }
Animated Transitions
Set animate to smoothly transition line positions when data changes. Passtrue for defaults (300ms ease-out) or a config object.
JSX
const [data, setData] = useState(datasetA) <LineChart data={data} xAccessor="month" yAccessor="revenue" animate={{ duration: 600, easing: "ease-out" }} showPoints curve="monotoneX" />
Click-to-Lock Crosshair
With linkedHover in "x-position" mode, click a chart to lock the crosshair at that X position. Click again or press Escape to unlock. Hover to see the synced crosshair, then click to lock it in place.
JSX
<LinkedCharts> <LineChart data={revenueData} xAccessor="month" yAccessor="revenue" linkedHover={{ name: "sync", mode: "x-position", xField: "month" }} tooltip /> <LineChart data={costData} xAccessor="month" yAccessor="cost" linkedHover={{ name: "sync", mode: "x-position", xField: "month" }} tooltip /> </LinkedCharts>
Props
| Prop | Type | Required | Default | Description |
|---|
data | array | Yes | — | Array of data points or array of line objects with coordinates. |
xAccessor | string | function | — | "x" | Field name or function to access x values from each data point. |
yAccessor | string | function | — | "y" | Field name or function to access y values from each data point. |
lineBy | string | function | — | — | Field name or function to group data into multiple lines (e.g., by series). |
lineDataAccessor | string | — | "coordinates" | Field name in line objects that contains coordinate arrays. |
colorBy | string | function | — | — | Field name or function to determine line color for multiple lines. |
colorScheme | string | array | — | "category10" | Color scheme name or custom colors array. |
curve | string | — | "linear" | Curve interpolation: "linear", "monotoneX", "step", "basis", "cardinal", "catmullRom". |
showPoints | boolean | — | false | Show data points on the line. |
pointRadius | number | — | 3 | Point radius when showPoints is true. |
fillArea | boolean | — | false | Fill the area under the line. |
areaOpacity | number | — | 0.3 | Opacity of the area fill when fillArea is true. |
lineWidth | number | — | 2 | Stroke width of the line. |
enableHover | boolean | — | true | Enable hover annotations on data points. |
showAxes | boolean | — | mode-driven | Show the axes. Defaults to the chart mode (off in context and sparkline); an explicit value wins, matching responsiveRules win over it, and margins are kept. |
showGrid | boolean | — | false | Show background grid lines. |
showLegend | boolean | — | true (multi-line) | Show a legend. Defaults to true when multiple lines are present. |
tooltip | "multi" | object | function | — | — | Tooltip configuration or render function. Pass "multi" to show every series value at the hovered x position. |
gapStrategy | "break" | "interpolate" | "zero" | — | "break" | How to handle null/undefined/NaN values in data. "break" splits the line at gaps, "interpolate" connects across gaps, "zero" drops to zero. |
band | BandConfig | BandConfig[] | — | — | Asymmetric min/max envelope drawn under the line. `{ y0Accessor, y1Accessor, style?, perSeries?, interactive? }` or an array for percentile fans. Participates in y-extent auto-derivation. Hovered datum gets `band: {y0,y1}` and `bands: [...]` for tooltip access. Distinct from `boundsAccessor` (symmetric ±offset). |
directLabel | boolean | { position?: "start" | "end", fontSize?: number } | — | false | Place category labels at line endpoints instead of using a separate legend. Auto-hides the legend when enabled. |
legendInteraction | "highlight" | "isolate" | "none" | — | "none" | Legend interaction mode. "highlight" dims non-hovered categories to 30% opacity. "isolate" toggles category visibility on click. |
loading | boolean | — | false | Show a skeleton loading placeholder instead of the chart. |
emptyContent | ReactNode | false | — | — | Custom content to show when data is empty. Set to false to disable the default "No data available" message. |
width | number | — | 600 | Chart width in pixels. |
height | number | — | 400 | Chart height in pixels. |
margin | object | — | { top: 50, bottom: 60, left: 70, right: 40 } | Margin around the chart area. |
xLabel | string | — | — | Label for the x-axis. |
yLabel | string | — | — | Label for the y-axis. |
animate | boolean | object | — | false | Enable animated intro and smooth data-change transitions. `true` for defaults (300ms ease-out, intro enabled), or `{ duration, easing, intro }`. Set `{ intro: false }` to disable intro. |
hoverRadius | number | — | 30 | Maximum distance (px) from a data point to trigger hover. Increase for sparse charts, decrease for dense ones. |
title | string | — | — | Chart title displayed at the top. |
frameProps | object | — | — | Additional StreamXYFrame props for advanced customization. Escape hatch to the full Frame API. |
When to Use the Frame
Use StreamXYFrame directly for custom marks, complex annotations, or dual-axis layouts. LineChart delegates to a configuredStreamXYFrame.
Chart (simple)
JSX
import { LineChart } from "semiotic" <LineChart data={salesData} xAccessor="month" yAccessor="revenue" curve="monotoneX" showPoints={true} xLabel="Month" yLabel="Revenue" />
Frame (full control)
JSX
import { StreamXYFrame } from "semiotic" <StreamXYFrame lines={[{ coordinates: salesData }]} xAccessor="month" yAccessor="revenue" lineDataAccessor="coordinates" lineType={{ type: "line", interpolator: curveMonotoneX }} showLinePoints={true} pointStyle={{ fill: "#6366f1", r: 3 }} lineStyle={{ stroke: "#6366f1", strokeWidth: 2 }} axes={[ { orient: "left", label: "Revenue" }, { orient: "bottom", label: "Month" } ]} hoverAnnotation={true} size={[600, 400]} />
The frameProps prop on LineChart lets you pass any StreamXYFrame prop without fully graduating:
JSX
// Use frameProps as an escape hatch <LineChart data={salesData} xAccessor="month" yAccessor="revenue" frameProps={{ annotations: [ { type: "x", month: 6, label: "Mid-year" } ], lineStyle: () => ({ stroke: "red", strokeWidth: 2 }) }} />
- AreaChart — filled area beneath the line (or use
fillArea on LineChart) - Scatterplot — for point-based XY visualizations
- StreamXYFrame — the underlying Frame with full control over every rendering detail
- Annotations — adding callouts, highlights, and notes to any visualization
- Tooltips — custom tooltip content and positioning