Trend chart
A series drawn as a line over a time axis, with reference lines and a marker.
On this page
Barometer · 6 h behind, 90 min forecast
Depth at anchor · 4 h
Engine coolant · since start
Code
import { Card } from "@bosunkit/components/card";
import { SectionHeading } from "@bosunkit/components/section-heading";
import { TrendChart, type TrendPoint } from "@bosunkit/components/trend-chart";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A watch's instruments on a live feed: the barometer falling toward a gale with two reference lines, a now " +
"marker and the forecast past it, a depth line without area or axis against the safety depth, and a coolant " +
"chart collecting its first readings.";
/** The start of the example's record, 18:00 UTC; each tick of the feed is ten minutes of it. */
const RECORD_START: number = Date.UTC(2026, 9, 10, 18, 0);
const STEP_MS: number = 10 * 60_000;
/** How many readings the barometer chart shows behind the marker, and how many forecast steps after it. */
const HISTORY_POINTS: number = 36;
const FORECAST_POINTS: number = 9;
/** How many ticks the coolant chart collects before the engine is stopped and it starts over. */
const COOLANT_CYCLE: number = 14;
function pressureAt(step: number): number {
// A front passing every 200 readings: steady near 1014 hPa, falling about 1.5 hPa an hour to near 1001, then
// recovering, with the small daily wave on top.
const phase: number = step % 200;
const fall: number = phase < 40 ? 0 : phase < 90 ? (phase - 40) * 0.25 : Math.max(0, 12.5 - (phase - 90) * 0.15);
return 1014 - fall + Math.sin(step / 7) * 0.4;
}
function depthAt(step: number): number {
// Swinging at anchor over a sloping bottom while the tide rises and falls.
return 7.2 + Math.sin(step / 40) * 1.4 + Math.sin(step / 2.5) * 0.6;
}
function coolantAt(step: number): number {
// A cold engine warming toward its thermostat.
return 82 - 64 * Math.exp(-step / 4);
}
function series(from: number, count: number, valueAt: (step: number) => number): TrendPoint[] {
return Array.from({ length: count }, (_: unknown, index: number): TrendPoint => ({
time: RECORD_START + (from + index) * STEP_MS,
value: valueAt(from + index),
}));
}
function hectopascals(value: number): string {
return value.toFixed(0);
}
function meters(value: number): string {
return `${value.toFixed(1)} m`;
}
function celsius(value: number): string {
return `${value.toFixed(0)} °C`;
}
function utc(t: number): string {
const date: Date = new Date(t);
return `${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")} UTC`;
}
/** A chart's card: its heading over the chart. */
const STACK: CSSProperties = { display: "flex", flexDirection: "column", gap: "var(--space-sm)" };
export default function Showcase(): ReactElement {
const [tick, setTick]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
useEffect((): (() => void) => {
const timer: ReturnType<typeof setInterval> = setInterval((): void => {
setTick((current: number): number => current + 1);
}, 1000);
return (): void => {
clearInterval(timer);
};
}, []);
// The newest reading is "now"; the forecast runs on past it.
const now: number = HISTORY_POINTS + tick;
const pressure: TrendPoint[] = series(now - HISTORY_POINTS + 1, HISTORY_POINTS + FORECAST_POINTS, pressureAt);
const depth: TrendPoint[] = series(now - 24 + 1, 24, depthAt);
const coolant: TrendPoint[] = series(0, tick % COOLANT_CYCLE, coolantAt);
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)", width: "100%" }}>
<Card>
<div style={STACK}>
<SectionHeading level={3}>Barometer · 6 h behind, 90 min forecast</SectionHeading>
<TrendChart
data={pressure}
width={640}
height={200}
color="var(--accent)"
thresholds={[
{ value: 1008, label: "Falling fast · 1008" },
{ value: 1004, color: "var(--alert-alarm-color)", label: "Gale risk · 1004" },
]}
markerTime={RECORD_START + now * STEP_MS}
yTickCount={5}
formatValue={hectopascals}
formatTime={utc}
label="Barometric pressure in hectopascals, the last six hours and the next ninety minutes"
/>
</div>
</Card>
<div
style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: "var(--space-md)" }}
>
<Card>
<div style={STACK}>
<SectionHeading level={3}>Depth at anchor · 4 h</SectionHeading>
<TrendChart
data={depth}
width={320}
height={120}
color="var(--cyan)"
area={false}
yTickCount={0}
thresholds={[{ value: 5.6, color: "var(--alert-warning-color)", label: "Safety depth 5.6 m" }]}
formatValue={meters}
formatTime={utc}
label="Depth under the transducer at anchor, the last four hours"
/>
</div>
</Card>
<Card>
<div style={STACK}>
<SectionHeading level={3}>Engine coolant · since start</SectionHeading>
<TrendChart
data={coolant}
width={320}
height={120}
color="var(--amber)"
yTickCount={3}
thresholds={[{ value: 95, color: "var(--alert-alarm-color)", label: "Alarm 95 °C" }]}
formatValue={celsius}
formatTime={utc}
label="Engine coolant temperature since the engine started"
/>
</div>
</Card>
</div>
</div>
);
}
A trend chart draws a series of readings as a line with a value axis and a time axis, for a curve the crew read
closely, such as a tide or a pressure falling. thresholds draw horizontal reference lines with a label, such as an
alarm limit or the chart datum. markerTime draws a vertical line, such as "now" on a prediction. The value axis writes
one decimal and the time axis hours and minutes in UTC, in the crew's language, with "UTC" after them, unless
formatValue and formatTime say otherwise. With
fewer than two points the chart shows that it is collecting data and draws no plot. The line stops at a missing
sample (value: null) and, with maxGap, across a longer time between two samples, and starts again after it, so an
outage is never drawn as a measured change. For a trend with no axes,
use a sparkline.
Use this chart for operational readings whose reference lines and instrument context matter. For comparisons across an analytical window, use Line chart, with a series registry and source table.
Examples
Trend
Code
import { TrendChart, type TrendPoint } from "@bosunkit/components/trend-chart";
import type { ReactElement } from "react";
export const description: string = "Four readings drawn as a filled line, with the time axis in minutes from the start.";
const SERIES: readonly TrendPoint[] = [
{ time: 0, value: 1.0 },
{ time: 60_000, value: 2.4 },
{ time: 120_000, value: 1.8 },
{ time: 180_000, value: 3.1 },
];
function minutes(t: number): string {
return `${String(t / 60_000)} min`;
}
export default function Trend(): ReactElement {
return <TrendChart data={SERIES} label="tide curve" formatTime={minutes} />;
}
Threshold and marker
Code
import { TrendChart, type TrendPoint } from "@bosunkit/components/trend-chart";
import type { ReactElement } from "react";
export const description: string = "A reference line labeled SAFE at 2.0 and a vertical marker between the second and third readings.";
const SERIES: readonly TrendPoint[] = [
{ time: 0, value: 1.0 },
{ time: 60_000, value: 2.4 },
{ time: 120_000, value: 1.8 },
{ time: 180_000, value: 3.1 },
];
function minutes(t: number): string {
return `${String(t / 60_000)} min`;
}
export default function ThresholdAndMarker(): ReactElement {
return (
<TrendChart data={SERIES} thresholds={[{ value: 2.0, label: "SAFE" }]} markerTime={90_000} formatTime={minutes} />
);
}
Collecting
Code
import { TrendChart } from "@bosunkit/components/trend-chart";
import type { ReactElement } from "react";
export const description: string =
"No readings yet: the chart says \"Collecting data…\" in place of an empty plot.";
export default function Collecting(): ReactElement {
return <TrendChart data={[]} label="tide curve" />;
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| datarequired | readonly TrendPoint[] | – | Series points, oldest first. |
| width | number | undefined | 480 | The drawing's width in the chart's own units; 480 by default. |
| height | number | undefined | 160 | The drawing's height in the chart's own units; 160 by default. |
| color | string | undefined | "var(--accent)" | The line's CSS color; the accent color by default. |
| area | boolean | undefined | true | Fill the area under the line; on by default. |
| thresholds | readonly TrendThreshold[] | undefined | [] | Horizontal reference lines (alarm limits, chart datum, ...). |
| maxGap | number | undefined | – | The longest time between two samples that the line still joins, in the units of a point's time: across a
longer gap the line stops and starts again, so an outage is never drawn as measured. Every sample is joined when
unset. |
| markerTime | number | undefined | – | Vertical marker (for example "now" on a prediction curve), in the units of a point's time. |
| yTickCount | number | undefined | 4 | How many ticks the y axis draws; 0 draws no axis. |
| formatValue | ((value: number) => string) | undefined | defaultFormatValue | Writes a y-axis value; one decimal by default. |
| formatTime | ((t: number) => string) | undefined | – | Writes an x-axis time; by default hours and minutes in UTC, in the crew's language, followed by "UTC". |
| label | string | undefined | – | What the chart shows, as the caller displays it; "trend chart" in the crew's language by default. |
| className | string | undefined | – | Classes added to the drawing's own. |
Establish the reference
Name the quantity, unit, time zone and displayed period outside the plot. Supply formatValue and formatTime
when the vessel or report uses a specific reference; browser defaults are not a declaration of ship's time.
Explain the source of each threshold and distinguish a forecast from observed points in the surrounding text.
Choose an axis range suitable for the decision, and keep that range stable across comparisons. Do not fill a missing interval with zeros or invented observations. If the available series cannot express an important gap, state the coverage limitation or choose a chart that can. “Collecting” describes insufficient points; the caller must distinguish first load, a disconnected source and a failed history request.
Related
- Data visualization: how measured, forecast and threshold data are drawn.