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.
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
Instead of a separate legend, directLabel places category names at the end of each line. This follows the data visualization best practice of labeling data directly when space allows. The legend is auto-hidden when direct labels are active.
import { LineChart } from "semiotic" const frameProps = { /* --- Data --- */ data: [ { month: 1, revenue: 12000, product: "Widget" }, { month: 1, revenue: 8000, product: "Gadget" }, { month: 1, revenue: 5000, product: "Doohickey" }, // ...data with product field ], /* --- Layout --- */ lineBy: "product", /* --- Process --- */ xAccessor: "month", yAccessor: "revenue", /* --- Customize --- */ xLabel: "Month", yLabel: "Revenue ($)", colorBy: "product", directLabel: true } export default () => { return <LineChart {...frameProps} /> }
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.
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. |
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 | — | 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. |
Graduating to the Frame
When you need more control — custom marks, complex annotations, dual-axis layouts — graduate to StreamXYFrame directly. Every LineChartis just a configured StreamXYFrame under the hood.
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" } ], customLineMark: ({ d }) => <circle r={5} fill="red" /> }} />
- 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