Skip to content
Dethink Components

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 files

Implemented 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.

Usage
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

Line chart
MonthRevenueExpensesPayroll
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
examples/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)

Line chart
Timep95
09:00182 ms
09:30176 ms
10:00190 ms
10:30204 ms
11:00—
11:30—
12:00231 ms
12:30218 ms
13:00197 ms
13:30188 ms
14:00179 ms
14:30184 ms
15:00176 ms
15:30171 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
examples/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.

Weekly signups by plan
WeekFreeProTeam
W11,24031064
W21,38034271
W31,31036583
W41,52039890
W51,610421102
W61,590447118

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
examples/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>
  );
}
LineChart props
PropWhat it doesDefault
dataArray<Record<string, unknown>>One record per x position, in order. Non-numeric values leave a gap.Not set
indexstringRecord 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 | stringPlot height. Width always fills the container.288
yDomain / includeZero[number, number] / booleanFix 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 / stringLabels x values and names the table's first column.String(value) / index
legendbooleanToggle buttons that show or hide series; hovering one dims the others.series.length >= 2
hiddenSeries / defaultHiddenSeries / onHiddenSeriesChangestring[] / string[] / (hidden) => voidControlled or uncontrolled series filtering. The last visible series cannot be hidden.— / [] / —
showTable / defaultShowTable / onShowTableChangeboolean / boolean / (show) => voidSwap the plot for a visible data table. The table is always present for assistive technology.— / false / —
tableTogglebooleanRender the Table view toggle button.true
loading / loadingLabel / emptyLabelboolean / ReactNode / ReactNodeRefetches keep the previous render at reduced opacity.false / "Loading…" / "No data"
animatebooleanDraw-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.