Line chart
Free · MITMulti-series lines that draw in staggered, a legend that toggles series while the scale refits, a crosshair that reads every series at once, and a dashed forecast segment behind a divider.
Installation
$ npx shadcn@latest add https://goodui.dev/r/line-chart.jsonThe file lands in components/goodui/line-chart.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/line-chart works too.
Usage
import { LineChart } from "@/components/goodui/line-chart"
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
export function BookingsChart() {
return (
<LineChart
title="Bookings per month"
categories={months}
series={[
{ key: "this", label: "This year", data: [412, 438, 471, 455, 502, 548, 561, 539, 590, 628, 655, 690] },
{ key: "last", label: "Last year", data: [368, 372, 401, 395, 430, 462, 478, 470, 495, 520, 541, 560] },
]}
forecastFrom={9}
height={240}
/>
)
}When to use
- Two to five series on one scale: this year vs last year, response time by channel, plan vs actual.
- A projection: pass forecastFrom and the tail of every line turns dashed behind a "Forecast" divider.
- Readers who need to hide a noisy series and let the scale refit (legend toggles).
- One metric with a headline: use area-chart.
- Two measures of different scale: never a dual axis; use two charts or index both to 100.
- Rank changes rather than values: use bump-chart.
| If you need | Use |
|---|---|
| Actual vs last year | two series, forecastFrom for the projected months |
| Series with very different ranges | baseline="auto" zooms to the data; toggle series in the legend |
| Exact values | curve="linear" |
| Legend above the plot | legend="top" |
| Control hidden series from the app | hidden + onHiddenChange |
Examples
Minutes, zoomed scale, legend on top
<LineChart title="First response time" categories={weeks} series={byChannel} baseline="auto" legend="top" curve="linear" formatValue={(v) => `${v} min`} />Legend rendered elsewhere
const [hidden, setHidden] = React.useState<string[]>([])
<ChartLegend items={items} hidden={hidden} onHiddenChange={setHidden} />
<LineChart categories={weeks} series={series} hidden={hidden} onHiddenChange={setHidden} legend="none" />API reference
| Prop | Type | Default |
|---|---|---|
| series* | LineSeries[] | — |
| categories* X labels, one per data point. | string[] | — |
| title Chart name for screen readers and the table caption. | React.ReactNode | — |
| forecastFrom Index from which the lines are drawn dashed (a projection); a divider labelled "Forecast" marks it. | number | — |
| hidden Keys of hidden series (controlled). | string[] | — |
| defaultHidden | string[] | — |
| onHiddenChange | (hidden: string[]) => void | — |
| legend Legend placement; a single series shows none. | "top" | "bottom" | "none" | "bottom" |
| height Plot height in px, axes included. The width follows the container. | number | 240 |
| curve | "smooth" | "linear" | "smooth" |
| baseline zero keeps 0 in view; auto zooms to the data. | "zero" | "auto" | "zero" |
| grid | boolean | true |
| axis | boolean | true |
| axisWidth | number | 44 |
| endDots Ring dot on the last point of every line. | boolean | true |
| format Intl.NumberFormat options for values and ticks. | Intl.NumberFormatOptions | — |
| locale | string | "en-US" |
| formatValue | (value: number) => string | — |
| formatTick | (value: number) => string | — |
| samples Resampled points per line; constant, so lines morph when the data changes. | number | 160 |
| duration Seconds one line takes to draw; lines stagger by 0.12 s. | number | 1.1 |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<LineChartLabels> | — |
Also accepts every <div> attribute.
Accessibility
| Tab | Focuses the plot and shows the crosshair on the latest period; then moves on to the legend buttons. |
| ArrowLeft / ArrowRight | Moves the crosshair one period. |
| Home / End | Jumps to the first or last period. |
| Escape | Clears the crosshair. |
| Enter / Space on a legend item | Hides or shows that series. |
- The plot is role="group" with aria-roledescription="line chart" and a summary label ("Bookings per month: 2 series over 12 periods"); your aria-label replaces it.
- A screen-reader-only table has one row per period and one column per series, with forecast periods marked.
- The crosshair position is announced through a polite live region: "Oct: This year 628, Last year 520".
- Legend items are buttons with aria-pressed (chart-legend); hidden series leave the scale and the tooltip.
- All built-in strings (summary, point, forecast, legend, column header, noData) go through labels.
Motion
- On first view each line draws in with pathLength over duration (1.1 s, ease [0.65, 0, 0.35, 1]), staggered 0.12 s per series; the dashed forecast segment fades in as the solid part reaches it; end dots pop on a spring (stiffness 420, damping 22).
- Data changes and legend toggles morph the paths and slide the ticks on a spring (stiffness 190, damping 30, mass 0.9); lines are resampled to `samples` points so shapes with different lengths still morph.
- The crosshair and its dots snap to the pointer on a stiff spring (stiffness 600, damping 40). Hovering a legend item dims the other lines to 25% over 0.2 s.
- A container resize re-lays the chart out instantly (no morph); labels that appear after the entrance fade in over 0.2 s.
- Reduced motion: lines, dots and labels appear at once, morphs are instant.
- One ResizeObserver per chart; paths are rebuilt only when data, size or hidden series change. No timers.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- series[i].data must have one value per category, in the same order. Keys must be unique; colors default to --chart-1 … --chart-5 by series order (give colors explicitly past five series).
- forecastFrom is an index into categories: lines are dashed from that point on. It needs 0 < forecastFrom < categories.length.
- Legends show only for two or more series; a single series has no legend box by design.
- Width follows the parent; height is a px prop. Do not wrap it in a fixed-width container smaller than ~240px.
FAQ
Can I put two different units on one chart?
No, and that is deliberate: a second y-axis invents correlations. Render two charts, or convert both series to an index (100 at the first period).
What happens when every series is hidden?
The scale keeps 0..1, the tooltip is empty and the table still lists the data.
Related
Area chart that draws its line in on view, morphs between 1W / 1M / 1Y ranges, and rolls its headline value as a hairline and tooltip follow the pointer or the arrow keys.
Grouped, stacked or horizontal bars that grow from the baseline on view and morph when data changes, with a gliding hover band, legend toggles, value labels and 2px surface gaps between segments.
Shared interactive legend: aria-pressed toggle buttons with popping square, dot, line or dashed swatches, hidden-state strike-through, optional values and a hover channel that highlights a series in its chart.
Rank-over-time lines that draw in with popping dots and names at the line ends; hovering or arrowing through periods and neighbours highlights one item and reads its change.