Sparkline
A small line or row of bars through recent values, with no axes, for a trend seen at a glance.
On this page
Last hour at anchor, 13:00 to 14:00 UTC
Counted per interval, drawn as bars
Code
import { Card } from "@bosunkit/components/card";
import { Reading } from "@bosunkit/components/reading";
import { Sparkline } from "@bosunkit/components/sparkline";
import { Activity, BatteryMedium, CloudRain, Gauge, Thermometer, Timer, Waves, type LucideIcon } from "lucide-react";
import type { CSSProperties, ReactElement, ReactNode } from "react";
export const description: string =
"The last hour at anchor in Baie de Morgat. Depth is the filtered line over the raw soundings, stale while the " +
"sounder lost the bottom, with the next 20 minutes forecast and its interval reaching the safe depth. Coolant " +
"leaves its normal band, the barometer falls from its ringed high, and the battery has a gap where the monitor " +
"dropped out; beside them, rain per hour and engine hours per day as bars. Point at a trend, or tab to it and use " +
"the arrow keys; the readout goes on into the forecast.";
/** "13:05 UTC" for the sample `index` steps of `stepMinutes` after `startHour`:00 UTC. */
function timeLabel(startHour: number, stepMinutes: number, index: number): string {
const minutes: number = startHour * 60 + index * stepMinutes;
const hour: string = String(Math.floor(minutes / 60) % 24).padStart(2, "0");
const minute: string = String(minutes % 60).padStart(2, "0");
return `${hour}:${minute} UTC`;
}
/** Thirteen samples, one every five minutes from 13:00 to 14:00 UTC. */
const HOUR: readonly string[] = Array.from({ length: 13 }, (_: unknown, index: number): string =>
timeLabel(13, 5, index),
);
/** The forecast's four steps, 14:05 to 14:20 UTC. */
const NEXT_20_MINUTES: readonly string[] = Array.from({ length: 4 }, (_: unknown, index: number): string =>
timeLabel(14, 5, index + 1),
);
/** Twelve hourly totals, 03:00 to 14:00 UTC. */
const RAIN_HOURS: readonly string[] = Array.from({ length: 12 }, (_: unknown, index: number): string =>
timeLabel(3, 60, index),
);
const DAYS: readonly string[] = [
"Mon 28 Sep",
"Tue 29 Sep",
"Wed 30 Sep",
"Thu 1 Oct",
"Fri 2 Oct",
"Sat 3 Oct",
"Sun 4 Oct",
"Mon 5 Oct",
"Tue 6 Oct",
"Wed 7 Oct",
"Thu 8 Oct",
"Fri 9 Oct",
"Sat 10 Oct",
"Sun 11 Oct",
];
const PANEL: CSSProperties = {
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
gap: 16,
width: "100%",
};
const ROW: CSSProperties = {
display: "grid",
gridTemplateColumns: "20px 1fr auto",
alignItems: "center",
columnGap: 10,
rowGap: 2,
paddingTop: 22,
paddingBottom: 6,
borderTop: "1px solid var(--border)",
};
const NOTE: CSSProperties = { gridColumn: "2 / 4", fontSize: 11, color: "var(--text-dim)" };
interface TrendRowProps {
/** The Lucide icon for the quantity. */
icon: LucideIcon;
/** What is measured. */
label: string;
/** The latest value, shown beside the trend. */
value: number;
/** The value's unit. */
unit: string;
/** Decimals the value is shown with. */
fractionDigits: number;
/** One line under the trend: what to notice in it. */
note: string;
/** The sparkline. */
children: ReactNode;
}
function TrendRow({ icon: Icon, label, value, unit, fractionDigits, note, children }: TrendRowProps): ReactElement {
return (
<div style={ROW}>
<Icon size={18} aria-hidden="true" color="var(--text-dim)" />
<span style={{ display: "flex", alignItems: "center", minWidth: 0 }}>{children}</span>
<Reading value={value} unit={unit} label={label} fractionDigits={fractionDigits} />
<span style={NOTE}>{note}</span>
</div>
);
}
export default function Showcase(): ReactElement {
return (
<div style={PANEL}>
<Card title="Last hour at anchor, 13:00 to 14:00 UTC" icon={<Activity />}>
<TrendRow
icon={Waves}
label="Depth"
value={3.6}
fractionDigits={1}
unit="m"
note={
"Filtered over the raw soundings; stale 13:20 to 13:25 UTC while the sounder lost the bottom. " +
"The forecast interval reaches the safe depth of 3 m from 14:10 UTC."
}
>
<Sparkline
data={[8.4, 8.1, 7.6, 7.0, 7.0, 7.0, 6.2, 5.4, 4.8, 4.3, 3.9, 3.7, 3.6]}
rawData={[8.7, 7.8, 7.9, 6.6, null, null, 6.6, 5.0, 5.3, 3.9, 4.2, 3.4, 3.8]}
filterLabel="Hampel and median of 5"
quality={[null, null, null, null, "stale", "stale"]}
qualityNotes={[null, null, null, null, "1 min", "6 min"]}
forecast={{
mean: [3.4, 3.2, 3.0, 2.9],
low: [3.1, 2.8, 2.5, 2.3],
high: [3.7, 3.6, 3.5, 3.5],
pointLabels: NEXT_20_MINUTES,
label: "next 20 minutes, Holt",
}}
width={200}
height={40}
color="var(--data-categorical-operational-04)"
yMin={0}
referenceLines={[{ value: 3, label: "SAFE 3 M" }]}
markExcursions
showHigh
unit="m"
fractionDigits={1}
pointLabels={HOUR}
interactive
background="var(--surface)"
label="Depth, last hour"
/>
</TrendRow>
<TrendRow
icon={Thermometer}
label="Coolant"
value={84}
fractionDigits={0}
unit="°C"
note="Above its normal 75 to 90 °C from 13:25 to 13:35 UTC while motoring to the anchorage."
>
<Sparkline
data={[82, 84, 85, 86, 88, 91, 93, 92, 89, 86, 85, 84, 84]}
width={160}
height={36}
color="var(--data-categorical-operational-03)"
curve="monotone"
normalBand={{ low: 75, high: 90, label: "normal 75 to 90 °C" }}
markExcursions
unit="°C"
pointLabels={HOUR}
interactive
background="var(--surface)"
label="Coolant temperature, last hour"
/>
</TrendRow>
<TrendRow
icon={Gauge}
label="Pressure"
value={1007.6}
fractionDigits={1}
unit="hPa"
note="Down 2.2 hPa in the hour from its high at 13:00 UTC."
>
<Sparkline
data={[
1009.8, 1009.7, 1009.5, 1009.4, 1009.1, 1008.9, 1008.7, 1008.5, 1008.3, 1008.1, 1007.9, 1007.7, 1007.6,
]}
width={160}
height={36}
color="var(--data-categorical-operational-01)"
curve="monotone"
domainPadding={0.15}
showLow
showHigh
unit="hPa"
fractionDigits={1}
pointLabels={HOUR}
interactive
background="var(--surface)"
label="Barometric pressure, last hour"
/>
</TrendRow>
<TrendRow
icon={BatteryMedium}
label="House"
value={12.4}
fractionDigits={1}
unit="V"
note="No reading at 13:30 UTC while the battery monitor restarted; 0.2 V above the low-voltage limit."
>
<Sparkline
data={[12.8, 12.7, 12.7, 12.6, 12.6, 12.6, null, 12.5, 12.5, 12.4, 12.4, 12.4, 12.4]}
width={160}
height={36}
color="var(--data-categorical-operational-02)"
referenceLines={[{ value: 12.2, label: "LOW 12.2 V", priority: "caution" }]}
domainPadding={0.2}
unit="V"
fractionDigits={1}
pointLabels={HOUR}
interactive
background="var(--surface)"
label="House battery voltage, last hour"
/>
</TrendRow>
</Card>
<Card title="Counted per interval, drawn as bars" icon={<CloudRain />}>
<TrendRow
icon={CloudRain}
label="Rain"
value={0.4}
fractionDigits={1}
unit="mm/h"
note="A front through from 06:00 to 09:00 UTC, the heaviest hour ringed. The latest hour is at full strength."
>
<Sparkline
data={[0, 0, 0.2, 1.8, 4.6, 6.2, 3.1, 0.8, 0, 0, 0.1, 0.4]}
plotType="bar"
width={160}
height={36}
color="var(--data-categorical-operational-04)"
yMin={0}
showHigh
unit="mm"
fractionDigits={1}
pointLabels={RAIN_HOURS}
interactive
background="var(--surface)"
label="Rain per hour, last 12 hours"
/>
</TrendRow>
<TrendRow
icon={Timer}
label="Engine"
value={1.6}
fractionDigits={1}
unit="h today"
note="Two days at anchor with the engine off; no log for Wed 7 Oct."
>
<Sparkline
data={[3.5, 5.2, 0, 0, 6.8, 4.1, 2.2, 1.0, 3.7, null, 7.4, 5.9, 0.6, 1.6]}
plotType="bar"
width={160}
height={36}
color="var(--data-categorical-operational-01)"
yMin={0}
normalBand={{ low: 0, high: 6, label: "service plan up to 6 h a day" }}
markExcursions
unit="h"
fractionDigits={1}
pointLabels={DAYS}
interactive
background="var(--surface)"
label="Engine hours per day, last 14 days"
/>
</TrendRow>
</Card>
</div>
);
}
A sparkline is a small drawing of recent values, oldest first, with no axes. It sits beside the reading it belongs to and shows which way that reading has been going: the reading gives the number, the sparkline the last few minutes or hours behind it. It draws a line through the readings, or one bar per reading for an amount per interval. On top of the series it can draw a reference line at a value that matters, a band for the normal range, rings on the lowest and highest reading, and a readout of each point under the pointer or the keyboard. It also carries what bosund knows about each value: how far it can be trusted, the raw samples behind a filtered value, and a forecast ahead of now with its interval.
Compact trends also belong in reports and comparison tables. For an analytical window needing full axes, multiple series and a source table, use Line chart or Bar chart. Operational integrity and forecasts belong to the instrument presentation.
Choosing what to draw
For compact metric cards, barWidth sets a thin bar in drawing pixels. contextColor supplies the muted
bar color, and highlightIndex names a selected interval rather than implying that interval is the latest.
The selection never fills a missing observation or moves to another interval. Keep the metric value and
comparison period in text beside the sparkline.
Line or bars. Use the line for a quantity that has a value at every moment: depth, speed, pressure, temperature, voltage. Use bars for a quantity that is a total over an interval: rain per hour, engine hours or fuel per day, distance per watch. A line between two daily totals would show values between the days that nobody counted. The latest bar is drawn at full strength and the earlier ones dimmed, as the step that contains now is emphasized on every chart.
Reference line or band. A reference line marks one value the crew acts on, taken from the vessel's own
configuration: its safe depth, the battery's low-voltage limit, the reef point. It is thin and solid in its alert
priority's color, warning unless said otherwise, and always carries a short label. A band marks the range a reading
normally stays within, such as 75 to 90 °C for coolant, in a neutral tint. With markExcursions, readings outside
the band are drawn in the warning color, the exceedance tint the data visualization
rules allow; the series itself stays in its data color.
Scale. By default the scale runs from the lowest to the highest reading, which makes small changes visible.
Fix yMin when the distance from zero is the point, as with depth under the keel, and yMax for a quantity with a
natural ceiling. domainPadding keeps the line off the edges. An automatic scale widens to include reference lines
and the band.
Marks. Show the latest dot on a live trend, and ring the low and high when the extreme matters, such as the
high a barometer has fallen from. Use one or two marks at most: a sparkline with every mark on it is not quiet.
Make it interactive where the crew needs to read a past value, and give pointLabels so the readout says when.
Quality. Pass quality with each sample's integrity, in the names bosund gives it: fresh, stale, degraded or
invalid, with qualityNotes for a stale sample's age or a degraded sample's reason. A stale or degraded stretch is
dashed with a hollow ring on each such sample, so it does not depend on color, and an invalid sample is a gap
whatever its value. The readout and the summary say the quality in the words the Reading
uses.
Filtered and raw. bosund smooths most readings before anything reads them. Pass the filtered values as data and
the raw samples as rawData when the crew should see how noisy the signal is, for a sounder over weed or a wind
instrument in gusts. The raw samples are faint dots behind the line and do not widen the scale, so a spike the filter
removed does not flatten the line; add domainPadding to keep the ordinary noise in view.
Forecast. Pass forecast with the mean per step and, when the model gives them, the 10 and 90 percent bounds.
The forecast is dashed after a now marker, with its interval as a faint band that widens as it reaches ahead.
Reference lines and the band apply to it, and with markExcursions a step whose interval reaches across a limit is
drawn in the warning color, which is the point of forecasting a depth or a battery.
Accessibility. Given unit or formatValue, the sparkline's accessible name is a summary of what it draws:
"Depth, last 30 minutes, 4.6 m now, low 3.4 m, high 6.2 m, SAFE 3 M". Put the quantity and the time window in
label. The interactive readout is a status region, so a screen reader hears each value as the keyboard moves;
a sparkline that is not interactive has no tab stop and no live region. The highlight never animates.
Examples
Line
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Speed over ground for the last ten minutes, one reading a minute, with a dot on the latest: 6.4 kn.";
export default function Line(): ReactElement {
return (
<Sparkline
data={[5.1, 5.3, 5.2, 5.6, 5.9, 5.8, 6.0, 6.3, 6.1, 6.2, 6.4]}
unit="kn"
fractionDigits={1}
label="Speed over ground, last 10 minutes"
/>
);
}
Bars
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Rain per hour over the last twelve hours as bars from zero. A dry hour is a thin bar on the baseline, and the " +
"latest hour is drawn at full strength.";
export default function Bars(): ReactElement {
return (
<Sparkline
data={[0, 0, 0.2, 1.8, 4.6, 6.2, 3.1, 0.8, 0, 0, 0.1, 0.4]}
plotType="bar"
yMin={0}
unit="mm"
fractionDigits={1}
label="Rain per hour, last 12 hours"
/>
);
}
Metric cards with a selected interval
Total headcount
Open positions
New hires / 30d
Code
import "@bosunkit/tokens/charts.css";
import { Card } from "@bosunkit/components/card";
import { Sparkline } from "@bosunkit/components/sparkline";
import { ArrowUp } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"Three illustrative workforce metrics pair a quiet, thin bar sparkline with one selected interval, a large supplied value and a named comparison.";
const metrics = [
{
label: "Total headcount",
value: "1,248",
unit: "people",
delta: "+4.2%",
comparison: "vs last month",
changeColor: "var(--bk-metric-positive)",
data: [20, 33, 29, 41, 32, 56, 38, 27, 45, 36, 50],
},
{
label: "Open positions",
value: "42",
unit: "roles",
delta: "+6",
comparison: "new this month",
changeColor: "var(--bk-metric-neutral-change)",
data: [20, 33, 29, 41, 32, 56, 38, 27, 45, 36, 50],
},
{
label: "New hires / 30d",
value: "87",
unit: "hires",
delta: "+12.5%",
comparison: "vs last month",
changeColor: "var(--bk-metric-positive)",
data: [20, 33, 29, 41, 32, 56, 38, 27, 45, 36, 50],
},
] as const;
export default function WorkforceMetrics(): ReactElement {
return (
<div
role="group"
aria-label="Illustrative workforce metrics"
style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-xl)", width: "100%" }}
>
{metrics.map((metric): ReactElement => (
<Card
key={metric.label}
appearance="metric"
style={{ flex: "1 1 18em" }}
title={
<span
style={{
font: "var(--bk-metric-label)",
letterSpacing: "var(--bk-metric-label-spacing)",
color: "var(--text-dim)",
textTransform: "uppercase",
}}
>
{metric.label}
</span>
}
footer={
<div
style={{
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "var(--space-sm)",
flexWrap: "wrap",
}}
>
<span
aria-hidden="true"
style={{
display: "grid",
placeItems: "center",
width: "var(--bk-metric-icon-size)",
height: "var(--bk-metric-icon-size)",
borderRadius: "50%",
color: "var(--data-categorical-analytical-03)",
background: "var(--bk-metric-icon-surface)",
}}
>
<ArrowUp size={20} aria-hidden="true" />
</span>
<span style={{ display: "flex", alignItems: "baseline", gap: "var(--space-sm)", flexWrap: "wrap" }}>
<strong
style={{
font: "var(--bk-metric-delta)",
fontVariantNumeric: "tabular-nums",
color: metric.changeColor,
}}
>
{metric.delta}
</strong>
<span style={{ font: "var(--bk-type-body-reading)", color: "var(--text-dim)" }}>
{metric.comparison}
</span>
</span>
</div>
}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "var(--space-lg)",
minWidth: 0,
flexWrap: "wrap",
}}
>
<span style={{ display: "flex", alignItems: "baseline", gap: "var(--space-sm)", whiteSpace: "nowrap" }}>
<strong style={{ font: "var(--bk-metric-value)", fontVariantNumeric: "tabular-nums" }}>
{metric.value}
</strong>
<span style={{ font: "var(--bk-type-body-reading)", color: "var(--text-dim)" }}>{metric.unit}</span>
</span>
<Sparkline
data={metric.data}
plotType="bar"
width={112}
height={64}
yMin={0}
yMax={56}
barWidth={3}
color="var(--data-categorical-analytical-03)"
contextColor="var(--bk-metric-bar-muted)"
highlightIndex={5}
label={`${metric.label}, illustrative daily observations; sixth interval selected`}
/>
</div>
</Card>
))}
</div>
);
}
Area
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"The house battery over the last six hours with a fill under the line, charging under solar from 12.3 V to 13.6 V.";
export default function Area(): ReactElement {
return (
<Sparkline
data={[12.3, 12.4, 12.6, 12.9, 13.2, 13.4, 13.5, 13.6, 13.6, 13.5, 13.6, 13.6, 13.6]}
area
unit="V"
fractionDigits={1}
label="House battery voltage, last 6 hours"
/>
);
}
Reference line
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Depth under the keel while the vessel swings at anchor, drawn from 0 m, with the vessel's safe depth of 3 m as a " +
"labeled line in the warning color.";
export default function ReferenceLine(): ReactElement {
return (
<Sparkline
data={[6.2, 5.8, 5.1, 4.4, 3.9, 3.6, 3.4, 3.6, 4.1, 4.8, 5.3, 5.0, 4.6]}
width={160}
height={40}
yMin={0}
area
referenceLines={[{ value: 3, label: "SAFE 3 M" }]}
unit="m"
fractionDigits={1}
label="Depth, last 30 minutes"
/>
);
}
Normal band
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Engine coolant over the last 40 minutes with its normal 75 to 90 °C band shaded. Three readings above the band, " +
"climbing out of the harbor at full power, are drawn in the warning color.";
export default function NormalBand(): ReactElement {
return (
<Sparkline
data={[68, 74, 79, 83, 86, 88, 91, 93, 92, 89, 87, 86, 85, 85, 84, 84, 85]}
width={160}
height={40}
normalBand={{ low: 75, high: 90, label: "normal 75 to 90 °C" }}
markExcursions
unit="°C"
label="Coolant temperature, last 40 minutes"
/>
);
}
Low, high and latest
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"A falling barometer over 24 hours, read hourly: the high of 1021.4 hPa 21 hours ago and the low of " +
"1006.2 hPa now are ringed, and the latest has its dot.";
export default function Spots(): ReactElement {
return (
<Sparkline
data={[
1019.8, 1020.6, 1021.1, 1021.4, 1021.2, 1020.9, 1020.3, 1019.6, 1018.9, 1018.0, 1017.2, 1016.1, 1015.3, 1014.2,
1013.4, 1012.5, 1011.6, 1010.9, 1010.0, 1009.2, 1008.5, 1007.7, 1007.0, 1006.6, 1006.2,
]}
width={160}
height={40}
curve="monotone"
domainPadding={0.1}
showLow
showHigh
unit="hPa"
fractionDigits={1}
label="Barometric pressure, last 24 hours"
/>
);
}
Gaps in the data
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Apparent wind with three breaks: the masthead unit sent nothing for two readings, then for one more, and one " +
"reading was not a number. The line breaks at each, the reading alone between two breaks is a dot, and nothing " +
"is drawn at zero.";
export default function Gaps(): ReactElement {
return (
<Sparkline
data={[14.2, 15.1, 15.8, null, null, 16.4, null, 17.2, 16.8, 17.5, Number.NaN, 18.1, 18.6]}
width={160}
height={40}
unit="kn"
fractionDigits={1}
label="Apparent wind speed, last 12 minutes"
/>
);
}
Automatic and fixed scale
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { CSSProperties, ReactElement } from "react";
export const description: string =
"The same depths twice. Scaled to the readings, a fall from 5.2 to 4.6 m fills the height; drawn from 0 m, it is " +
"a small change in plenty of water.";
const FIGURE: CSSProperties = { display: "grid", gap: 4, fontSize: 12, color: "var(--text-dim)" };
const DEPTHS: readonly number[] = [5.2, 5.1, 5.1, 5.0, 4.9, 4.9, 4.8, 4.7, 4.7, 4.6];
export default function Domain(): ReactElement {
return (
<div style={{ display: "flex", gap: 32, flexWrap: "wrap" }}>
<figure style={FIGURE}>
<Sparkline data={DEPTHS} width={160} height={40} unit="m" fractionDigits={1} label="Depth, scaled to the readings" />
<figcaption>Automatic: lowest to highest reading</figcaption>
</figure>
<figure style={FIGURE}>
<Sparkline
data={DEPTHS}
width={160}
height={40}
yMin={0}
unit="m"
fractionDigits={1}
label="Depth, drawn from 0 m"
/>
<figcaption>Fixed: yMin 0</figcaption>
</figure>
</div>
);
}
Interactive readout
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Speed through the water over the last 30 minutes. Point at it, or tab to it and press the arrow keys, Home or " +
"End: the readout above names the time and the speed, and a screen reader hears it.";
const TIMES: readonly string[] = Array.from({ length: 11 }, (_: unknown, index: number): string => {
const minutes: number = 14 * 60 + 30 + index * 3;
return `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")} UTC`;
});
export default function Interactive(): ReactElement {
return (
<div style={{ paddingTop: 24 }}>
<Sparkline
data={[6.8, 6.9, 7.1, 7.0, 6.6, 6.2, 6.4, 6.9, 7.3, 7.4, 7.2]}
width={200}
height={40}
curve="monotone"
unit="kn"
fractionDigits={1}
pointLabels={TIMES}
interactive
label="Speed through the water, last 30 minutes"
/>
</div>
);
}
Straight and smooth
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { CSSProperties, ReactElement } from "react";
export const description: string =
"Sea water temperature over a day, straight and smooth. The smooth curve bends only where the readings do, so " +
"it never shows a temperature above the highest reading or below the lowest.";
const FIGURE: CSSProperties = { display: "grid", gap: 4, fontSize: 12, color: "var(--text-dim)" };
const TEMPERATURES: readonly number[] = [17.8, 17.8, 17.9, 18.4, 19.1, 19.6, 19.6, 19.3, 18.8, 18.4, 18.2, 18.1];
export default function Curve(): ReactElement {
return (
<div style={{ display: "flex", gap: 32, flexWrap: "wrap" }}>
<figure style={FIGURE}>
<Sparkline data={TEMPERATURES} width={160} height={40} unit="°C" fractionDigits={1} label="Sea temperature" />
<figcaption>curve linear</figcaption>
</figure>
<figure style={FIGURE}>
<Sparkline
data={TEMPERATURES}
width={160}
height={40}
curve="monotone"
unit="°C"
fractionDigits={1}
label="Sea temperature"
/>
<figcaption>curve monotone</figcaption>
</figure>
</div>
);
}
Under a reading
Code
import { Card } from "@bosunkit/components/card";
import { Reading } from "@bosunkit/components/reading";
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"A sparkline under the reading it belongs to, as the dashboard's cards place it: the reading carries the value " +
"and its trust, the sparkline only the last few minutes beneath it.";
export default function InAReading(): ReactElement {
return (
<Card style={{ width: 180 }}>
<div style={{ display: "grid", gap: "var(--space-sm)" }}>
<Reading value={14.6} unit="kn" label="TWS" source="Masthead" fractionDigits={1} trend="rising" />
<Sparkline
data={[11.2, 11.8, 12.1, 12.6, 12.4, 13.1, 13.5, 13.4, 14.0, 14.2, 14.6]}
width={150}
height={24}
area
unit="kn"
fractionDigits={1}
label="True wind speed, last 10 minutes"
/>
</div>
</Card>
);
}
Filling its container
width="fill" makes the sparkline as wide as its container. It measures its box and draws at that width, so the
line keeps its stroke width and the dots and labels keep their size in a narrow phone column and across a wide
panel alike. Use it wherever the sparkline sits in a column of the layout; give a number only where the sparkline
sits inline in a line of text or a table cell.
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"The distance from the anchor over the last 90 seconds, as wide as its panel: drag the panel's corner and the " +
"line follows its width, with the stroke, the dot and the swing radius label kept at their size.";
const DISTANCE: readonly number[] = [22, 24, 27, 25, 29, 31, 30, 33, 32, 34, 33, 31, 29, 30, 28];
export default function FillWidth(): ReactElement {
return (
<div
style={{
display: "grid",
gap: "var(--space-xs)",
width: "100%",
maxWidth: "36em",
minWidth: "12em",
padding: "var(--space-sm)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-md)",
resize: "horizontal",
overflow: "hidden",
}}
>
<Sparkline
data={DISTANCE}
width="fill"
height={40}
yMin={0}
area
referenceLines={[{ value: 35, label: "SWING 35 M", priority: "alarm" }]}
unit="m"
label="Distance from the anchor, last 90 seconds"
/>
</div>
);
}
Small multiples
Code
import { Card } from "@bosunkit/components/card";
import { Reading } from "@bosunkit/components/reading";
import { Sparkline } from "@bosunkit/components/sparkline";
import type { CSSProperties, ReactElement } from "react";
export const description: string =
"Four quantities side by side over the same 30 minutes, each on its own scale and in its own data color: true " +
"wind building, depth shoaling toward the safe depth, pressure falling, and the battery under load.";
interface Multiple {
label: string;
value: number;
source: string;
unit: string;
data: readonly number[];
color: string;
fractionDigits: number;
yMin?: number;
referenceLine?: { value: number; label: string };
}
const MULTIPLES: readonly Multiple[] = [
{
label: "True wind",
value: 18.4,
source: "Masthead",
unit: "kn",
data: [12.1, 12.8, 13.5, 13.2, 14.4, 15.1, 15.9, 16.3, 17.2, 17.8, 18.4],
color: "var(--data-categorical-operational-01)",
fractionDigits: 1,
},
{
label: "Depth",
value: 4.1,
source: "Echosounder",
unit: "m",
data: [9.8, 9.1, 8.4, 7.9, 7.1, 6.5, 5.9, 5.3, 4.8, 4.4, 4.1],
color: "var(--data-categorical-operational-04)",
fractionDigits: 1,
yMin: 0,
referenceLine: { value: 3, label: "SAFE 3 M" },
},
{
label: "Pressure",
value: 1011.2,
source: "Barometer",
unit: "hPa",
data: [1012.4, 1012.3, 1012.2, 1012.1, 1011.9, 1011.8, 1011.7, 1011.5, 1011.4, 1011.3, 1011.2],
color: "var(--data-categorical-operational-03)",
fractionDigits: 1,
},
{
label: "House",
value: 12.5,
source: "Shunt",
unit: "V",
data: [12.9, 12.8, 12.8, 12.7, 12.6, 12.4, 12.3, 12.4, 12.5, 12.5, 12.5],
color: "var(--data-categorical-operational-02)",
fractionDigits: 1,
},
];
/** A cell's content: the reading over its sparkline. */
const CELL: CSSProperties = { display: "grid", gap: "var(--space-xs)" };
export default function SmallMultiples(): ReactElement {
return (
<div
style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 8, width: "100%" }}
>
{MULTIPLES.map((multiple: Multiple): ReactElement => (
<Card key={multiple.label}>
<div style={CELL}>
<Reading
value={multiple.value}
unit={multiple.unit}
label={multiple.label}
source={multiple.source}
fractionDigits={multiple.fractionDigits}
/>
<Sparkline
data={multiple.data}
width={116}
height={40}
color={multiple.color}
yMin={multiple.yMin}
referenceLines={multiple.referenceLine === undefined ? [] : [multiple.referenceLine]}
unit={multiple.unit}
fractionDigits={multiple.fractionDigits}
background="var(--surface)"
label={`${multiple.label}, last 30 minutes`}
/>
</div>
</Card>
))}
</div>
);
}
Quality of each sample
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Depth while the sounder struggles over weed: two readings degraded by a weak bottom lock, two stale while it " +
"sent nothing new, and one ruled invalid, which is a gap. Degraded and stale stretches are dashed with hollow " +
"rings, and the readout says why.";
export default function Quality(): ReactElement {
return (
<div style={{ paddingTop: 24 }}>
<Sparkline
data={[6.4, 6.3, 6.1, 6.0, 5.8, 5.8, 5.8, 9.9, 5.5, 5.4, 5.3]}
quality={["fresh", "fresh", "degraded", "degraded", "fresh", "stale", "stale", "invalid", "fresh"]}
qualityNotes={[null, null, "weak bottom lock", "weak bottom lock", null, "40 s", "1 min 40 s"]}
width={200}
height={40}
yMin={0}
unit="m"
fractionDigits={1}
pointLabels={[
"10:00 UTC",
"10:01 UTC",
"10:02 UTC",
"10:03 UTC",
"10:04 UTC",
"10:05 UTC",
"10:06 UTC",
"10:07 UTC",
"10:08 UTC",
"10:09 UTC",
"10:10 UTC",
]}
interactive
label="Depth, last 10 minutes"
/>
</div>
);
}
Filtered over raw
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"Depth as bosund filters it, a Hampel spike filter then a median of five, drawn over the raw soundings as faint " +
"dots. Padding the scale keeps the ordinary noise in view; the spike at 11:04 UTC that the filter removed sits " +
"off the scale and is left out. The readout gives the filtered and the raw value.";
export default function Filtered(): ReactElement {
return (
<div style={{ paddingTop: 24 }}>
<Sparkline
data={[12.1, 12.0, 12.0, 11.9, 11.8, 11.8, 11.6, 11.5, 11.5, 11.3, 11.2]}
rawData={[12.4, 11.7, 12.2, 11.6, 0.6, 12.1, 11.3, 11.8, 11.2, 11.5, 11.0]}
filterLabel="Hampel and median of 5"
domainPadding={0.4}
width={200}
height={40}
unit="m"
fractionDigits={1}
pointLabels={[
"11:00 UTC",
"11:01 UTC",
"11:02 UTC",
"11:03 UTC",
"11:04 UTC",
"11:05 UTC",
"11:06 UTC",
"11:07 UTC",
"11:08 UTC",
"11:09 UTC",
"11:10 UTC",
]}
interactive
label="Depth, last 10 minutes"
/>
</div>
);
}
Forecast with its interval
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"The house battery overnight at anchor: the last two hours measured, then the next two forecast as a dashed line " +
"after the now marker, with its 10 to 90 percent interval widening as it reaches ahead. The steps whose interval " +
"reaches below the 12.2 V low-voltage limit are marked.";
export default function Forecast(): ReactElement {
return (
<div style={{ paddingTop: 24 }}>
<Sparkline
data={[12.9, 12.8, 12.8, 12.7, 12.7, 12.6, 12.6, 12.5, 12.5]}
forecast={{
mean: [12.5, 12.4, 12.4, 12.3, 12.3, 12.2, 12.2, 12.1],
low: [12.4, 12.3, 12.2, 12.1, 12.0, 11.9, 11.8, 11.7],
high: [12.6, 12.5, 12.6, 12.5, 12.6, 12.5, 12.6, 12.5],
pointLabels: [
"02:15 UTC",
"02:30 UTC",
"02:45 UTC",
"03:00 UTC",
"03:15 UTC",
"03:30 UTC",
"03:45 UTC",
"04:00 UTC",
],
label: "next 2 hours, Holt",
}}
referenceLines={[{ value: 12.2, label: "LOW 12.2 V", priority: "caution" }]}
markExcursions
width={200}
height={40}
unit="V"
fractionDigits={1}
pointLabels={[
"00:00 UTC",
"00:15 UTC",
"00:30 UTC",
"00:45 UTC",
"01:00 UTC",
"01:15 UTC",
"01:30 UTC",
"01:45 UTC",
"02:00 UTC",
]}
interactive
label="House battery voltage, last 2 hours"
/>
</div>
);
}
Not enough data
Code
import { Sparkline } from "@bosunkit/components/sparkline";
import type { ReactElement } from "react";
export const description: string =
"A single reading, which cannot make a line: the sparkline draws nothing and a screen reader hears \"depth: not enough data\".";
export default function NotEnoughData(): ReactElement {
return <Sparkline data={[4]} label="depth" />;
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| datarequired | readonly (number | null)[] | – | Series values, oldest first. null is a missing reading, and so is a value that is not a finite number: the line
breaks there rather than drawing it, so no gap is drawn as a real reading. |
| width | number | "fill" | undefined | 120 | The drawing's width in pixels, 120 by default, or fill to take the whole width of its container. A filling
sparkline measures its box and lays the drawing out at that width, so the line, the dots and the labels keep
their drawn size at any width; until the first measurement it scales a 120-pixel drawing to the box, with the
strokes kept at their width. |
| height | number | undefined | 32 | The drawing's height in pixels; 32 by default. |
| color | string | undefined | "var(--accent)" | Any CSS color expression; the accent color by default. |
| plotType | SparklinePlotType | undefined | "line" | line (the default) draws a line through the readings, for a quantity measured continuously (depth, pressure,
voltage). bar draws one bar per reading, for an amount per interval (rain per hour, engine hours per day), growing
from zero when zero is in the scale; the latest bar is drawn at full strength and the earlier ones dimmed. |
| barWidth | number | undefined | – | Width of each bar in drawing pixels, clamped to its slot; 70% of the slot when unset. |
| contextColor | string | undefined | – | Color of unselected bars; unset bars use the series color with reduced opacity. |
| highlightIndex | number | undefined | – | Bar interval to emphasize instead of the latest one; a missing or invalid index has no emphasized bar. |
| area | boolean | undefined | false | Fill the area under the line with a translucent gradient. |
| showLatest | boolean | undefined | true | Mark the most recent value: a dot on the line, or the one bar at full strength; on by default. |
| yMin | number | undefined | – | The value at the bottom edge; the lowest reading by default. A depth trend passes 0 so the water under the keel is drawn to scale. A reading below it is drawn at the edge. |
| yMax | number | undefined | – | The value at the top edge; the highest reading by default. A reading above it is drawn at the edge. |
| domainPadding | number | undefined | 0 | Room added beyond the lowest and highest reading, as a share of their span (0.1 adds a tenth at each end), so the
line does not run along the edges; 0 by default. It applies only to an edge without yMin or yMax. |
| curve | SparklineCurve | undefined | "linear" | linear (the default) joins the readings with straight segments; monotone with a smooth curve that rises or
falls only where the readings do, so it never shows a value beyond the two readings it joins. |
| referenceLines | readonly SparklineReferenceLine[] | undefined | [] | Horizontal lines at values that matter (an alarm limit, the safe depth), thin and solid in their priority's
color. An automatic scale widens to include them; a line beyond a fixed yMin or yMax is left out. |
| normalBand | SparklineBand | undefined | – | The range a reading normally stays within, shaded behind the series in a neutral tint. An automatic scale widens
to include it. A band whose low is above its high, or either is not a finite number, is not drawn. |
| markExcursions | boolean | undefined | false | Draw each reading outside normalBand in the warning color: a dot on the line, or the whole bar. |
| quality | readonly (ReadingIntegrity | null)[] | undefined | – | Each sample's integrity, aligned with data, as bosund names it: domain.Integrity's fresh, stale, degraded and
invalid (packages/bosun/domain/integrity.go:17-45), drawn from the quality monitor's verdicts
(instrument/quality/monitor.go:670, domain/quality.go:27). A stale or degraded stretch is drawn dashed with a hollow
ring on each such reading, and a stale or degraded bar as a dashed outline; an invalid sample is a gap, whatever
its value. A missing entry is fresh. |
| qualityNotes | readonly (string | null)[] | undefined | – | The caption for each stale or degraded sample, as the caller displays it: a stale sample's age ("3 min"), or the quality evaluator's reason a degraded one is below full trust ("weak bottom lock"), as domain.IntegrityAssessment's Reason carries it (domain/integrity.go:101-110). Read out with the value. |
| rawData | readonly (number | null)[] | undefined | – | The raw samples behind a filtered series, aligned with data, which then holds the filtered values. bosund's filter
pipeline republishes each smoothed reading under the source "filter" beside the raw event it came from
(instrument/gate.go:8-14, instrument/filter/wiring.go:81); depth, for one, passes a Hampel spike filter and a
median of five (instrument/filter/defaults.go:17). Each raw sample is a faint dot behind the line, so the crew sees
the noise the filter removed; it does not widen the scale, and a raw sample beyond it is left out. The readout
gives both values. |
| filterLabel | string | undefined | – | What the filter is ("Hampel and median of 5"), as the caller displays it; read out with the sparkline's name. |
| forecast | SparklineForecast | undefined | – | Predicted values after the latest sample. The forecast is drawn dashed after a vertical "now" marker at the latest
sample, with its interval as a faint band; an automatic scale widens to include both, and reference lines and the
normal band apply to it as to the readings. With markExcursions, a step whose value leaves the band, or whose
interval reaches across a reference line from the side the latest reading is on, is drawn in the warning color.
The readout names a step as a forecast with its interval ("forecast 3.6 m ± 0.4"). |
| showLow | boolean | undefined | false | Ring the lowest reading (the most recent of equal ones); its value is in the accessible summary. |
| showHigh | boolean | undefined | false | Ring the highest reading (the most recent of equal ones); its value is in the accessible summary. |
| background | string | undefined | "var(--bg)" | The color behind the sparkline, which outlines each reference label so a line passing behind it cannot break the glyphs; the page background by default. A sparkline on a card passes the card's surface color. |
| strokeWidth | number | undefined | 1.5 | The line's width in pixels; 1.5 by default. |
| unit | string | undefined | – | The unit of the values ("m", "hPa"), written after each one in the accessible summary. Given this or
formatValue, the accessible name becomes a summary: "Depth, 4.2 m now, low 3.1 m, high 6.8 m". |
| fractionDigits | number | undefined | 0 | Fraction digits the values are written with, rounding half to even; 0 by default. |
| formatValue | ((value: number) => string) | undefined | – | Writes one value with its unit, in place of unit and fractionDigits, for a quantity with its own rule (a
depth floored so it never overstates the water). |
| label | string | undefined | – | What the trend shows and over what time ("Depth, last 30 minutes"), as the caller displays it; "trend" in the crew's language by default. |
| interactive | boolean | undefined | false | Let the crew read each point: under the pointer, or with the arrow keys (Home and End for the first and last, Escape to stop) once the sparkline has focus. A highlight marks the point and a readout above the sparkline writes its value, announced to a screen reader as it changes. Off by default; the highlight never animates. |
| pointLabels | readonly string[] | undefined | – | What each point is ("14:20", "Mon"), as the caller displays it; the readout writes it before the value. |
| testId | string | undefined | – | Becomes data-testid on the root; the highlight is ${testId}--highlight. |
| className | string | undefined | – | Classes added to the drawing's own. |
Related
-
Instrument history: accepted raw sources, exact filter output and server-side forecast grids.
-
Data visualization: how measured data, thresholds and amounts are drawn.
-
Reading: the reading a sparkline sits beside.
-
Trend chart: a larger chart with axes, for reading values off a scale.