ComponentsLine Chart
Line Chart
Trends over time on a single y-axis. The crosshair finds the x position so readers never aim at a 2px line, and every value is also available as a table.
On this page
Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.
View registry filesImplemented with React and browser APIs, without an additional behavior or rendering library.
Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.
Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.
Import the component into your page or component file.
import { LineChart } from "@dethink/components";Three series in currency, a single series with gaps, and controlled filtering with the table view.
Revenue vs spend
Hover, or focus the chart and use the arrow keys: the tooltip lists every series at that month. Hovering a legend item dims the others; clicking it hides that series without repainting the rest.
Revenue vs spend, 2026
| Month | Revenue | Expenses | Payroll |
|---|---|---|---|
| Jan | $42.1K | $31.2K | $18.4K |
| Feb | $44.8K | $32.1K | $18.4K |
| Mar | $43.9K | $34.8K | $19.1K |
| Apr | $48.2K | $33.9K | $19.1K |
| May | $51.6K | $35.6K | $19.8K |
| Jun | $55.3K | $37.2K | $21.2K |
| Jul | $54.1K | $38.9K | $21.2K |
| Aug | $58.9K | $38.1K | $21.9K |
| Sep | $62.4K | $40.3K | $22.6K |
| Oct | $61.8K | $42.6K | $22.6K |
| Nov | $66.2K | $41.9K | $23.4K |
| Dec | $71.5K | $44.1K | $23.4K |
Series: Revenue, Expenses, Payroll. 12 points from Jan to Dec. Use the arrow keys, Home and End to read values.
Show sourceexamples/line-chart/revenue.tsx
"use client";
import { LineChart } from "@dethink/components";
const months = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const revenue = [
42_100, 44_800, 43_900, 48_200, 51_600, 55_300, 54_100, 58_900, 62_400,
61_800, 66_200, 71_500,
];
const expenses = [
31_200, 32_100, 34_800, 33_900, 35_600, 37_200, 38_900, 38_100, 40_300,
42_600, 41_900, 44_100,
];
const payroll = [
18_400, 18_400, 19_100, 19_100, 19_800, 21_200, 21_200, 21_900, 22_600,
22_600, 23_400, 23_400,
];
const data = months.map((month, i) => ({
month,
revenue: revenue[i],
expenses: expenses[i],
payroll: payroll[i],
}));
export function LineChartRevenue() {
return (
<div className="w-full">
<h3 id="revenue-heading" className="mb-3 text-sm font-medium">
Revenue vs spend, 2026
</h3>
<LineChart
aria-labelledby="revenue-heading"
data={data}
index="month"
indexLabel="Month"
series={[
{ key: "revenue", label: "Revenue" },
{ key: "expenses", label: "Expenses" },
{ key: "payroll", label: "Payroll" },
]}
formatOptions={{ style: "currency", currency: "USD" }}
/>
</div>
);
}Single series with gaps
One series needs no legend. Missing readings stay gaps, and includeZero={false} fits the axis to the data.
API p95 latency (ms)
| Time | p95 |
|---|---|
| 09:00 | 182 ms |
| 09:30 | 176 ms |
| 10:00 | 190 ms |
| 10:30 | 204 ms |
| 11:00 | — |
| 11:30 | — |
| 12:00 | 231 ms |
| 12:30 | 218 ms |
| 13:00 | 197 ms |
| 13:30 | 188 ms |
| 14:00 | 179 ms |
| 14:30 | 184 ms |
| 15:00 | 176 ms |
| 15:30 | 171 ms |
Series: p95. 14 points from 09:00 to 15:30. Use the arrow keys, Home and End to read values.
Show sourceexamples/line-chart/latency.tsx
"use client";
import { LineChart } from "@dethink/components";
// Gaps (null) stay gaps: the collector was down for two readings.
const p95 = [
182,
176,
190,
204,
null,
null,
231,
218,
197,
188,
179,
184,
176,
171,
];
const data = p95.map((value, i) => ({
time: `${String(9 + Math.floor(i / 2)).padStart(2, "0")}:${i % 2 ? "30" : "00"}`,
p95: value,
}));
export function LineChartLatency() {
return (
<div className="w-full">
<h3 id="latency-heading" className="mb-3 text-sm font-medium">
API p95 latency (ms)
</h3>
<LineChart
aria-labelledby="latency-heading"
data={data}
index="time"
indexLabel="Time"
series={[{ key: "p95", label: "p95", color: "chart-7" }]}
includeZero={false}
curve="linear"
height={220}
formatValue={(value) => `${Math.round(value)} ms`}
/>
</div>
);
}Controlled filters and table view
hiddenSeries and showTable can be controlled, for example to persist them in the URL.
| Week | Free | Pro | Team |
|---|---|---|---|
| W1 | 1,240 | 310 | 64 |
| W2 | 1,380 | 342 | 71 |
| W3 | 1,310 | 365 | 83 |
| W4 | 1,520 | 398 | 90 |
| W5 | 1,610 | 421 | 102 |
| W6 | 1,590 | 447 | 118 |
Series: Pro, Team. 6 points from W1 to W6. Use the arrow keys, Home and End to read values.
Hidden: free · View: chart
Show sourceexamples/line-chart/table-view.tsx
"use client";
import { useState } from "react";
import { LineChart } from "@dethink/components";
const data = [
{ week: "W1", free: 1_240, pro: 310, team: 64 },
{ week: "W2", free: 1_380, pro: 342, team: 71 },
{ week: "W3", free: 1_310, pro: 365, team: 83 },
{ week: "W4", free: 1_520, pro: 398, team: 90 },
{ week: "W5", free: 1_610, pro: 421, team: 102 },
{ week: "W6", free: 1_590, pro: 447, team: 118 },
];
export function LineChartTableView() {
const [hidden, setHidden] = useState<string[]>(["free"]);
const [table, setTable] = useState(false);
return (
<div className="grid w-full gap-2">
<LineChart
aria-label="Weekly signups by plan"
data={data}
index="week"
indexLabel="Week"
series={[
{ key: "free", label: "Free" },
{ key: "pro", label: "Pro" },
{ key: "team", label: "Team" },
]}
hiddenSeries={hidden}
onHiddenSeriesChange={setHidden}
showTable={table}
onShowTableChange={setTable}
height={240}
/>
<p className="text-muted-foreground text-xs">
Hidden: {hidden.length ? hidden.join(", ") : "none"} · View:{" "}
{table ? "table" : "chart"}
</p>
</div>
);
}| Prop | What it does | Default |
|---|---|---|
dataArray<Record<string, unknown>> | One record per x position, in order. Non-numeric values leave a gap. | Not set |
indexstring | Record key holding the x value, such as "month". | Not set |
seriesArray<{ key, label, color? }> | Lines on one y-axis. color is a palette slot or CSS color; it defaults to the slot for the series' position, so filtering never repaints survivors. | Not set |
curve"monotone" | "linear" | "step" | Monotone never overshoots the data. | "monotone" |
heightnumber | string | Plot height. Width always fills the container. | 288 |
yDomain / includeZero[number, number] / boolean | Fix the value domain, or let the niced data extent drop zero. | — / true |
formatValue / formatOptions(value) => string / Intl.NumberFormatOptions & { locale? } | Formats axis ticks, tooltip, readout and table values. | Compact en-US |
formatIndex / indexLabel(value, index) => string / string | Labels x values and names the table's first column. | String(value) / index |
legendboolean | Toggle buttons that show or hide series; hovering one dims the others. | series.length >= 2 |
hiddenSeries / defaultHiddenSeries / onHiddenSeriesChangestring[] / string[] / (hidden) => void | Controlled or uncontrolled series filtering. The last visible series cannot be hidden. | — / [] / — |
showTable / defaultShowTable / onShowTableChangeboolean / boolean / (show) => void | Swap the plot for a visible data table. The table is always present for assistive technology. | — / false / — |
tableToggleboolean | Render the Table view toggle button. | true |
loading / loadingLabel / emptyLabelboolean / ReactNode / ReactNode | Refetches keep the previous render at reduced opacity. | false / "Loading…" / "No data" |
animateboolean | Draw-in on mount. Always off under reduced motion. | true |
The plot is a single tab stop named by aria-label or aria-labelledby, with a description listing the series and range. Arrow keys move the crosshair one point at a time, Home and End jump to the ends, and Escape clears it; each move is announced through a polite live region, listing every series. The data table is always in the page for assistive technology and can be shown visually with Table view. Legend items are toggle buttons with aria-pressed. Series identity never relies on color alone: the legend, tooltip and table all name each series. Tooltip content renders as text, never HTML.