Line chart
Compare sampled quantities across an analytical window with gaps, formatted inspection and a source table.
On this page
Processing time
Illustrative data · Monday through Sunday
- Service
- Comparison
Code
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import type { ChartConfig } from "@bosunkit/components/chart-container";
import { ReferenceLine } from "recharts";
import type { ReactElement } from "react";
export const description: string =
"Illustrative processing times over a week, with a missing Wednesday and a reference at eight milliseconds.";
interface Sample {
readonly day: string;
readonly service: number | null;
readonly comparison: number | null;
}
const data: readonly Sample[] = [
{ day: "Mon", service: 6, comparison: 8 },
{ day: "Tue", service: 5, comparison: 7 },
{ day: "Wed", service: null, comparison: 7 },
{ day: "Thu", service: 4, comparison: 6 },
{ day: "Fri", service: 3, comparison: 5 },
{ day: "Sat", service: 4, comparison: 5 },
{ day: "Sun", service: 2, comparison: 4 },
];
// These are supplied display strings for the fixed, illustrative fixture. A consumer passes its quantity formatter.
const values: Readonly<Record<number, string>> = {
0: "0 ms",
1: "1 ms",
2: "2 ms",
3: "3 ms",
4: "4 ms",
5: "5 ms",
6: "6 ms",
7: "7 ms",
8: "8 ms",
9: "9 ms",
10: "10 ms",
};
const writeValue: (value: number) => string = (value: number): string => values[value] ?? "";
const writeDay: (value: string | number) => string = (value: string | number): string =>
typeof value === "string" ? value : "";
const config: ChartConfig<Sample> = {
service: { label: "Service", formatValue: writeValue },
comparison: { label: "Comparison", formatValue: writeValue },
};
export default function WeeklyLatency(): ReactElement {
return (
<ChartPanel
title="Processing time"
description="Illustrative data · Monday through Sunday"
legend={<ChartLegend config={config} label="Processing time series" />}
footer="Milliseconds per request · illustrative observations"
>
<LineChart
data={data}
categoryKey="day"
config={config}
series={["service", "comparison"]}
formatCategory={writeDay}
formatAxisValue={writeValue}
valueDomain={[0, 10]}
label="Illustrative processing times, Monday through Sunday"
categoryLabel="Day"
dataTableLabel="View processing times"
missingValueText="Not measured"
>
<ReferenceLine y={8} label={{ value: "Reference · 8 ms", position: "top" }} />
</LineChart>
</ChartPanel>
);
}
A line chart compares numeric series across a window. Each series has one registry entry for its name, palette slot, line pattern and observation formatter. Category spacing is equal; linear and time scales preserve numeric distance between samples. Numeric categories are finite and increasing.
Use
For an operational instrument reading, use Reading with Sparkline, or TrendChart where its axes are needed. Analytical windows use LineChart. Sparklines also belong in reports when a compact trend is enough. The line is linear and missing values break it; a singleton observation is a dot. Use a shared unit and comparison basis for all series. An explicit value domain clips observations outside its bounds. Filtering preserves the domain calculated from the complete registry.
bands attaches typed lower and upper fields to a series. The band uses that series' palette slot, follows its visibility and widens an automatic domain. It has a caller-supplied label and range formatter for inspection and the source table. Missing bounds remain gaps; reversed finite bounds are rejected. Keep observed and forecast values in separate series and name the model and quantile levels in the surrounding panel.
Set a raw comparison series to emphasis="context" in the registry. Keep the filtered trace primary, use distinct patterns and retain both values in inspection. The chart performs no signal processing.
A series can declare circularPeriod for a directional trace. The drawing follows the shortest increment across the period boundary, while inspection retains the supplied observation. Axis formatters express the chosen unwrapped scale or normalize ticks to the caller’s directional notation. Circular series require a line chart and do not accept ordinary numeric interval bands. maxGap bounds elapsed continuity on numeric axes; it adds drawing gaps without adding source observations.
Examples
Composition
Processing time
Illustrative data · Monday through Sunday
- Service
- Comparison
Code
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import type { ChartConfig } from "@bosunkit/components/chart-container";
import { ReferenceLine } from "recharts";
import type { ReactElement } from "react";
export const description: string =
"Illustrative processing times over a week, with a missing Wednesday and a reference at eight milliseconds.";
interface Sample {
readonly day: string;
readonly service: number | null;
readonly comparison: number | null;
}
const data: readonly Sample[] = [
{ day: "Mon", service: 6, comparison: 8 },
{ day: "Tue", service: 5, comparison: 7 },
{ day: "Wed", service: null, comparison: 7 },
{ day: "Thu", service: 4, comparison: 6 },
{ day: "Fri", service: 3, comparison: 5 },
{ day: "Sat", service: 4, comparison: 5 },
{ day: "Sun", service: 2, comparison: 4 },
];
// These are supplied display strings for the fixed, illustrative fixture. A consumer passes its quantity formatter.
const values: Readonly<Record<number, string>> = {
0: "0 ms",
1: "1 ms",
2: "2 ms",
3: "3 ms",
4: "4 ms",
5: "5 ms",
6: "6 ms",
7: "7 ms",
8: "8 ms",
9: "9 ms",
10: "10 ms",
};
const writeValue: (value: number) => string = (value: number): string => values[value] ?? "";
const writeDay: (value: string | number) => string = (value: string | number): string =>
typeof value === "string" ? value : "";
const config: ChartConfig<Sample> = {
service: { label: "Service", formatValue: writeValue },
comparison: { label: "Comparison", formatValue: writeValue },
};
export default function WeeklyLatency(): ReactElement {
return (
<ChartPanel
title="Processing time"
description="Illustrative data · Monday through Sunday"
legend={<ChartLegend config={config} label="Processing time series" />}
footer="Milliseconds per request · illustrative observations"
>
<LineChart
data={data}
categoryKey="day"
config={config}
series={["service", "comparison"]}
formatCategory={writeDay}
formatAxisValue={writeValue}
valueDomain={[0, 10]}
label="Illustrative processing times, Monday through Sunday"
categoryLabel="Day"
dataTableLabel="View processing times"
missingValueText="Not measured"
>
<ReferenceLine y={8} label={{ value: "Reference · 8 ms", position: "top" }} />
</LineChart>
</ChartPanel>
);
}
Raw and filtered
Raw and filtered
Illustrative accepted observations and separately supplied filter output
- Accepted raw
- Filter output
Code
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import { Switch } from "@bosunkit/components/switch";
import type { ChartConfig, ChartValueKey } from "@bosunkit/components/chart-container";
import { useId, useState, type Dispatch, type SetStateAction, type ReactElement } from "react";
export const description: string =
"A controlled switch shows accepted raw observations behind supplied filter output; the domain stays fixed when raw is hidden.";
interface Sample {
readonly day: string;
readonly raw: number | null;
readonly filtered: number | null;
}
const data: readonly Sample[] = [
{ day: "Mon", raw: 3, filtered: 3 },
{ day: "Tue", raw: 8, filtered: 5 },
{ day: "Wed", raw: 4, filtered: 5 },
{ day: "Thu", raw: 9, filtered: 6 },
{ day: "Fri", raw: 5, filtered: 6 },
{ day: "Sat", raw: null, filtered: null },
{ day: "Sun", raw: 4, filtered: 5 },
];
const values: Readonly<Record<number, string>> = {
0: "0 units",
1: "1 unit",
2: "2 units",
3: "3 units",
4: "4 units",
5: "5 units",
6: "6 units",
7: "7 units",
8: "8 units",
9: "9 units",
10: "10 units",
};
const write: (value: number) => string = (value: number): string => values[value] ?? "";
const config: ChartConfig<Sample> = {
raw: { label: "Accepted raw", formatValue: write, color: "01", pattern: "dotted", emphasis: "context" },
filtered: { label: "Filter output", formatValue: write, color: "01", pattern: "solid" },
};
export default function RawAndFiltered(): ReactElement {
const id: string = useId();
const [showRaw, setShowRaw]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const series: readonly ChartValueKey<Sample>[] = showRaw ? ["raw", "filtered"] : ["filtered"];
return (
<ChartPanel
title="Raw and filtered"
description="Illustrative accepted observations and separately supplied filter output"
controls={
<label htmlFor={id} style={{ display: "flex", alignItems: "center", gap: "var(--space-md)" }}>
<Switch id={id} checked={showRaw} onCheckedChange={setShowRaw} />
Show accepted raw observations
</label>
}
legend={<ChartLegend config={config} series={series} label="Raw and filtered series" />}
footer="Same timestamps, unit and scale · supplied values, no browser smoothing"
>
<LineChart
data={data}
categoryKey="day"
config={config}
series={series}
valueDomain={[0, 10]}
formatCategory={(value: string | number): string => (typeof value === "string" ? value : "")}
formatAxisValue={write}
label="Illustrative raw and filtered observations, Monday through Sunday"
categoryLabel="Day"
dataTableLabel="View raw and filtered observations"
missingValueText="Not observed"
/>
</ChartPanel>
);
}
Forecast quantiles
Observed and forecast
Illustrative amounts · observations end at 14:20 UTC
- Observed
- Forecast
Code
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import type { ChartConfig, ChartBand } from "@bosunkit/components/chart-container";
import * as Recharts from "recharts";
import type { ReactElement } from "react";
export const description: string =
"Illustrative observed and forecast amounts have separate series, a dashed forecast, a cutoff and explicit 10th–90th quantile bounds.";
interface Sample {
readonly time: number;
readonly observed: number | null;
readonly forecast: number | null;
readonly q10: number | null;
readonly q90: number | null;
}
const start: number = 1791986400000;
const step: number = 600000;
const data: readonly Sample[] = [
{ time: start, observed: 6, forecast: null, q10: null, q90: null },
{ time: start + step, observed: 5, forecast: null, q10: null, q90: null },
{ time: start + step * 2, observed: 6, forecast: null, q10: null, q90: null },
{ time: start + step * 3, observed: null, forecast: 7, q10: 6, q90: 8 },
{ time: start + step * 4, observed: null, forecast: 8, q10: 6, q90: 10 },
{ time: start + step * 5, observed: null, forecast: 9, q10: 5, q90: 10 },
];
const values: Readonly<Record<number, string>> = {
0: "0 items",
1: "1 item",
2: "2 items",
3: "3 items",
4: "4 items",
5: "5 items",
6: "6 items",
7: "7 items",
8: "8 items",
9: "9 items",
10: "10 items",
};
const times: Readonly<Record<number, string>> = {
[start]: "14:00 UTC",
[start + step]: "14:10 UTC",
[start + step * 2]: "14:20 UTC",
[start + step * 3]: "14:30 UTC",
[start + step * 4]: "14:40 UTC",
[start + step * 5]: "14:50 UTC",
};
const write: (value: number) => string = (value: number): string => values[value] ?? "";
const writeTime: (value: string | number) => string = (value: string | number): string =>
typeof value === "number" ? (times[value] ?? "") : "";
const config: ChartConfig<Sample> = {
observed: { label: "Observed", formatValue: write, pattern: "solid" },
forecast: { label: "Forecast", formatValue: write, pattern: "dashed" },
};
const ranges: Readonly<Record<string, string>> = { "6:8": "6–8 items", "6:10": "6–10 items", "5:10": "5–10 items" };
const bands: readonly ChartBand<Sample>[] = [
{
series: "forecast",
low: "q10",
high: "q90",
label: "10th–90th forecast quantiles",
formatRange: (low: number, high: number): string => ranges[`${String(low)}:${String(high)}`] ?? "",
},
];
export default function ForecastLine(): ReactElement {
return (
<ChartPanel
title="Observed and forecast"
description="Illustrative amounts · observations end at 14:20 UTC"
legend={<ChartLegend config={config} label="Observation and forecast series" />}
footer="Illustrative only · band shows supplied 10th–90th quantiles, not observed extrema"
>
<LineChart
data={data}
categoryKey="time"
categoryScale="time"
config={config}
series={["observed", "forecast"]}
bands={bands}
formatCategory={writeTime}
formatAxisValue={write}
valueDomain={[0, 10]}
label="Illustrative observations and forecast, 14:00 to 14:50 UTC"
categoryLabel="Time, UTC"
dataTableLabel="View observations and quantiles"
missingValueText="Not supplied"
>
<Recharts.ReferenceLine x={start + step * 3} label={{ value: "Forecast starts", position: "insideTopRight" }} />
</LineChart>
</ChartPanel>
);
}
Latency percentiles
P50, P95 and P99 summarize the same request population per day. Preserve the daily percentile calculations; averaging daily percentiles does not produce the percentile of the complete window.
Latency percentiles
P50, P95 and P99 response time per day, all models.
Code
import { LineChart } from "@bosunkit/components/line-chart";
import { ChartPanel } from "@bosunkit/components/chart-panel";
import type { ChartConfig } from "@bosunkit/components/chart-container";
import type { ReactElement } from "react";
export const description: string =
"Illustrative daily P50, P95 and P99 response times share one scale and retain their percentile definitions.";
interface Sample {
readonly day: string;
readonly p50: number;
readonly p95: number;
readonly p99: number;
}
const data: readonly Sample[] = [
{ day: "Sep 27", p50: 412, p95: 1700, p99: 3100 },
{ day: "Sep 28", p50: 405, p95: 1800, p99: 3300 },
{ day: "Sep 29", p50: 440, p95: 1900, p99: 3200 },
{ day: "Sep 30", p50: 470, p95: 2400, p99: 3900 },
{ day: "Oct 1", p50: 410, p95: 1700, p99: 3400 },
{ day: "Oct 2", p50: 395, p95: 1700, p99: 3200 },
{ day: "Oct 3", p50: 401, p95: 1800, p99: 3300 },
{ day: "Oct 4", p50: 412, p95: 1900, p99: 3400 },
];
// Fixed display strings for the fixture; a consumer supplies its quantity formatter.
const values: Readonly<Record<number, string>> = {
0: "0 s",
395: "395 ms",
401: "401 ms",
405: "405 ms",
410: "410 ms",
412: "412 ms",
440: "440 ms",
470: "470 ms",
1000: "1 s",
1700: "1.7 s",
1800: "1.8 s",
1900: "1.9 s",
2000: "2 s",
2400: "2.4 s",
3000: "3 s",
3100: "3.1 s",
3200: "3.2 s",
3300: "3.3 s",
3400: "3.4 s",
3900: "3.9 s",
4000: "4 s",
};
const formatValue: (value: number) => string = (value: number): string => values[value] ?? "";
const config: ChartConfig<Sample> = {
p50: { label: "P50 · median", formatValue, palette: "analytical", color: "01", pattern: "solid" },
p95: { label: "P95", formatValue, palette: "analytical", color: "03", pattern: "solid" },
p99: { label: "P99", formatValue, palette: "analytical", color: "09", pattern: "dashed" },
};
export default function LatencyPercentiles(): ReactElement {
return (
<ChartPanel
appearance="analytical"
title="Latency percentiles"
description="P50, P95 and P99 response time per day, all models."
controls={
<div
role="group"
aria-label="Latest daily latency percentiles"
style={{ display: "flex", gap: "var(--space-2xl)", flexWrap: "wrap" }}
>
{(["p50", "p95", "p99"] as const).map((key) => (
<div key={key}>
<div
style={{
display: "flex",
alignItems: "center",
gap: "var(--space-xs)",
font: "var(--bk-type-heading-component)",
}}
>
<span
aria-hidden="true"
style={{
width: "calc(var(--bk-chart-dot-radius) * 2)",
height: "calc(var(--bk-chart-dot-radius) * 2)",
borderRadius: "50%",
background: `var(--data-categorical-analytical-${config[key]?.color})`,
}}
/>
{formatValue(data[data.length - 1]![key])}
</div>
<span style={{ font: "var(--bk-type-body)", color: "var(--bk-analysis-muted)" }}>
{config[key]?.label}
</span>
</div>
))}
</div>
}
>
<LineChart
data={data}
categoryKey="day"
config={config}
series={["p50", "p95", "p99"]}
formatCategory={(value: string | number): string => (typeof value === "string" ? value : "")}
formatAxisValue={formatValue}
valueDomain={[0, 4000]}
label="Illustrative daily P50, P95 and P99 request latency, Sep 27 to Oct 4, 2026 UTC"
categoryLabel="Day · UTC"
dataTableLabel="View daily latency percentiles"
missingValueText="Not measured"
/>
</ChartPanel>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| callout | ChartCallout | undefined | – | A visible summary bubble with caller-formatted value and context. |
| onCategoryDomainChange | ((range: readonly [number, number]) => void) | undefined | – | Receives a numeric viewport after drag-to-zoom or Shift-drag pan; requires categoryDomain. |
| maxGap | number | undefined | – | Maximum elapsed distance connected by a line; numeric categories only. |
| categoryDomain | readonly [number, number] | undefined | – | Controlled numeric/time viewport; emits no requests by itself. |
| syncId | string | undefined | – | Charts sharing this identifier synchronize inspection by category value or nearest time. |
| syncTolerance | number | undefined | – | Maximum distance for synchronized numeric categories; zero requires exact matches. |
| interpolation | "linear" | "stepAfter" | undefined | – | Linear segments or step changes; linear by default. Missing values remain gaps. |
| describeObservation | ((row: T) => string) | undefined | – | Caller-formatted source, processing stage or quality shown alongside every source-table row. |
| datarequired | readonly T[] | – | Rows in display order; numeric time categories must be increasing. |
| categoryKeyrequired | ChartCategoryKey<NoInfer<T>> | – | The row field naming a category or locating a sample on a numeric axis. |
| configrequired | ChartConfig<NoInfer<T>> | – | Definitions for numeric series, shared with legends and controls. |
| seriesrequired | readonly ChartValueKey<NoInfer<T>>[] | – | The visible numeric fields, in display order. Filtering preserves the complete registry's domain. |
| formatCategoryrequired | (value: string | number) => string | – | Writes category ticks, tooltip headings and the first table column. |
| formatAxisValuerequired | (value: number) => string | – | Writes value-axis ticks; separate from each series' observation formatter. |
| labelrequired | string | – | The drawing's accessible name, naming the quantity and time window. |
| categoryLabelrequired | string | – | The first table column's heading, translated by the caller. |
| dataTableLabelrequired | string | – | The disclosure button and source table's accessible name, translated by the caller. |
| missingValueTextrequired | string | – | Display text for missing values in the tooltip and source table. |
| height | string | undefined | – | The plot height as a CSS length; the chart height token by default. |
| valueDomain | readonly [number, number] | undefined | – | An explicit increasing value domain that clips outlying values; bar domains always include zero. |
| showGrid | boolean | undefined | – | Draw the grid; on by default. |
| showAxes | boolean | undefined | – | Draw axes; on by default. Values remain available in the table when off. |
| motion | MotionProp | undefined | – | Entrance and exit motion; off by default. Measurements never interpolate. |
| children | ReactNode | undefined | – | Recharts annotations appended inside the plot, such as ReferenceLine. Use ChartContainer for a custom root. |
| className | string | undefined | – | Classes added to the root. |
| testId | string | undefined | – | Becomes data-testid on the root, with --plot and --table parts. |
| ref | Ref<HTMLDivElement> | undefined | – | The chart root element. |
| categoryScale | "category" | "linear" | "time" | undefined | – | Category spacing, numeric spacing, or epoch milliseconds on a time scale; category by default. |
| bands | readonly ChartBand<NoInfer<T>>[] | undefined | – | Typed intervals around visible series; gaps remain gaps. Stacked areas do not accept intervals. |
Processing time
Illustrative data · Monday through Sunday
- Service
- Comparison
Keep a gap and describe the missing measurement in the source table.
Related
- Instrument history: accepted raw sources, exact filter output, forecast quantiles and window loading.
- Data visualization: scales, missing measurements and comparable evidence.
- Chart panel: titles, controls and source information.
- Sparkline: compact trends with operational integrity and forecast information.