Gauge trend
A trend line with a vertical scale beside it, for a value and where it has been.
On this page
Code
import {
AdviceType,
FillMode,
GaugeTrend,
Priority,
Readout,
ReadoutDirection,
ScaleType,
} from "@bosunkit/instruments";
import type { ExternalScaleAdvice, GaugeTrendPoint } from "@bosunkit/instruments";
import { useCallback, useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, RefCallback, SetStateAction } from "react";
export const description: string =
"A low approaching Althea at anchor in Fornells, on a live feed: the barometer's 24-hour trend with its area, level bar, alarm setpoint and pressure ranges, the wind as a recolored line with a dot on a condensed scale, and a sea temperature sensor collecting its first samples, with its seasonal band tinted.";
/** Barometer samples kept on screen: 24 hours at one every 30 minutes. */
const PRESSURE_SAMPLES: number = 48;
const WIND_SAMPLES: number = 36;
const PRESSURE_ADVICE: ExternalScaleAdvice[] = [
{ min: 985, max: 1000, type: AdviceType.caution, hinted: false },
{ min: 1010, max: 1025, type: AdviceType.advice, hinted: true },
];
const WIND_ADVICE: ExternalScaleAdvice[] = [{ min: 25, max: 40, type: AdviceType.caution, hinted: true }];
const TITLE_STYLE: CSSProperties = { fontSize: 12, color: "var(--element-neutral-color)" };
/** Sea-level pressure in hPa, half-hour sample `sample`: steady near 1016, then falling as the low arrives. */
function pressureAt(sample: number): number {
const fall: number = sample > 20 ? (sample - 20) * 0.42 : 0;
return 1016 - fall + 0.6 * Math.sin(sample / 3);
}
/** True wind speed in knots, ten-minute sample `sample`: freshening with gusts. */
function windAt(sample: number): number {
return 14 + sample * 0.08 + 3.5 * Math.sin(sample * 1.3) + 1.5 * Math.sin(sample * 0.37);
}
function series(level: (sample: number) => number, newest: number, count: number): GaugeTrendPoint[] {
return Array.from({ length: count }, (_unused: unknown, index: number): GaugeTrendPoint => {
const t: number = newest - count + 1 + index;
return { t, value: level(t) };
});
}
/**
* A trend gauge takes its width in pixels, so the showcase measures the column the gauges sit in. A gauge is drawn at
* the width it is given, or at the column's width when the screen is narrower than that.
*/
function useColumnWidth(): [RefCallback<HTMLDivElement>, number] {
const [width, setWidth]: [number, Dispatch<SetStateAction<number>>] = useState<number>(Number.POSITIVE_INFINITY);
const measure: RefCallback<HTMLDivElement> = useCallback(
(column: HTMLDivElement | null): (() => void) | undefined => {
if (column === null) {
return undefined;
}
const observer: ResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]): void => {
const available: number | undefined = entries.at(0)?.contentRect.width;
if (available !== undefined && available > 0) {
setWidth(Math.floor(available));
}
});
observer.observe(column);
return (): void => {
observer.disconnect();
};
},
[],
);
return [measure, width];
}
export default function Showcase(): ReactElement {
const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
const [column, columnWidth]: [RefCallback<HTMLDivElement>, number] = useColumnWidth();
useEffect((): (() => void) => {
const timer: number = window.setInterval((): void => {
setStep((previous: number): number => (previous + 1) % 40);
}, 1500);
return (): void => {
window.clearInterval(timer);
};
}, []);
const newestPressure: number = 40 + step;
const pressure: number = pressureAt(newestPressure);
const pressureThreeHoursAgo: number = pressureAt(newestPressure - 6);
const tendency: number = pressure - pressureThreeHoursAgo;
const wind: number = windAt(60 + step);
return (
<div ref={column} style={{ display: "grid", gap: 16, justifyItems: "center", width: "100%", maxWidth: 620 }}>
<div style={{ display: "grid", gap: 4 }}>
<div style={{ display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }}>
<Readout
direction={ReadoutDirection.horizontal}
label="Barometer, last 24 h"
value={pressure}
unit="hPa"
fractionDigits={1}
maxDigits={4}
/>
<span style={TITLE_STYLE}>
{tendency.toFixed(1)} hPa in 3 h
{tendency <= -2 ? ": falling fast, reef early and check the anchor before dark." : "."}
</span>
</div>
<GaugeTrend
data={series(pressureAt, newestPressure, PRESSURE_SAMPLES)}
width={Math.min(580, columnWidth)}
height={220}
minValue={985}
maxValue={1030}
value={pressure}
hasBar
hasScale
chartFill
setpoint={995}
advices={PRESSURE_ADVICE}
primaryTickmarkInterval={10}
secondaryTickmarkInterval={5}
tertiaryTickmarkInterval={1}
priority={Priority.enhanced}
/>
</div>
<div style={{ display: "flex", gap: 16, flexWrap: "wrap", justifyContent: "center" }}>
<div style={{ display: "grid", gap: 4 }}>
<div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
<Readout
direction={ReadoutDirection.horizontal}
label="True wind, last 6 h"
value={wind}
unit="kn"
maxDigits={2}
/>
</div>
<GaugeTrend
data={series(windAt, 60 + step, WIND_SAMPLES)}
width={Math.min(330, columnWidth)}
height={170}
minValue={0}
maxValue={40}
value={wind}
hasBar={false}
hasScale
chartFill
seriesColor="var(--automation-medium-sea-water)"
barFillColor="var(--automation-medium-sea-water)"
advices={WIND_ADVICE}
primaryTickmarkInterval={10}
secondaryTickmarkInterval={5}
scaleType={ScaleType.condensed}
/>
</div>
<div style={{ display: "grid", gap: 4 }}>
<div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
<Readout
direction={ReadoutDirection.horizontal}
label="Sea temperature"
value={19.4}
unit="°C"
fractionDigits={1}
/>
</div>
<GaugeTrend
data={[{ t: 0, value: 19.4 }]}
width={Math.min(230, columnWidth)}
height={170}
minValue={10}
maxValue={30}
value={19.4}
hasBar
hasScale
fillMode={FillMode.tint}
fillMin={18}
fillMax={22}
primaryTickmarkInterval={5}
/>
</div>
</div>
</div>
);
}
The gauge trend draws a series as a smooth line, with an optional area under it, and sets a vertical scale at its right
edge. The line and the scale share one range, minValue to maxValue. Samples are placed by their time t, so an
uneven series keeps its spacing. The scale shows the value as a bar, or as a dot without hasBar, and takes advice
ranges and a setpoint marker.
width and height are the size in pixels of the chart and the scale together. Until two samples arrive the chart
shows that it is collecting.
Examples
Trend
Code
import { AdviceType, GaugeTrend } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "A trend with the level bar, the scale, a setpoint at 50 and two advice ranges.";
const TREND_SERIES: { t: number; value: number }[] = [
45, 52, 48, 55, 62, 58, 52, 40, 32, 37, 55, 68, 52, 48, 42, 38, 35, 32, 28, 30, 35, 40, 45, 50, 48, 48, 38, 35, 40,
45, 50,
].map((value: number, index: number): { t: number; value: number } => ({ t: index, value }));
export default function Trend(): ReactElement {
return (
<GaugeTrend
data={TREND_SERIES}
width={384}
height={384}
minValue={0}
maxValue={100}
value={50}
hasBar
hasScale
chartFill
setpoint={50}
primaryTickmarkInterval={10}
secondaryTickmarkInterval={5}
advices={[
{ min: 25, max: 45, type: AdviceType.caution, hinted: true },
{ min: 55, max: 75, type: AdviceType.advice, hinted: false },
]}
/>
);
}
History and the current cue
Give the series a named time window and keep timestamp units consistent. The curve shows supplied samples; its smoothing is visual, not additional measurements. Use the same numeric basis for the history, current cue, advice and setpoint.
Two points are enough to draw a line, not to establish complete coverage. The caller must distinguish initial collection, a failed history request and a disconnected live source. Do not insert zeros into gaps. Provide the current value, unit and source age outside the curve, and use a chart with explicit coverage treatment when missing intervals are important to the decision.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| data | GaugeTrendPoint[] | undefined | [] | Series samples, oldest first. Non-finite entries are dropped. |
| minValue | number | undefined | 0 | Minimum of the shared chart/scale range. |
| maxValue | number | undefined | 100 | Maximum of the shared chart/scale range. |
| value | number | undefined | – | Current value displayed on the vertical scale: bar fill level when
hasBar, dot indicator position otherwise. |
| hasBar | boolean | undefined | false | Show the value bar on the scale band. |
| hasScale | boolean | undefined | false | Show scale tickmarks and labels. |
| chartFill | boolean | undefined | false | Fill the area under the trend line (semitransparent). |
| fillMode | FillMode | undefined | FillMode.fill | Fill mode for the scale bar: fill tracks value from fillMin;
tint shows the fixed fillMin..fillMax range. |
| barFillColor | string | undefined | – | Optional paint override for the integrated scale bar. |
| seriesColor | string | undefined | – | Optional paint override for the chart line and its semitransparent area. |
| fillMin | number | undefined | 0 | Fill origin value for the scale bar. |
| fillMax | number | undefined | – | Upper fill bound (tint mode only; defaults to value). |
| advices | ExternalScaleAdvice[] | undefined | [] | Advice overlays on the scale — linear, setpoint-triggered semantics
(unlike the radial gauges' value trigger; both are upstream behavior).
A non-empty array shows them; upstream's separate hasAdvice toggle is
not carried over. |
| primaryTickmarkInterval | number | undefined | – | Interval for primary (longest, labeled) scale tickmarks. |
| secondaryTickmarkInterval | number | undefined | – | Interval for secondary scale tickmarks. |
| tertiaryTickmarkInterval | number | undefined | – | Interval for tertiary scale tickmarks. |
| scaleType | ScaleType | undefined | ScaleType.regular | Scale tick density preset. |
| priority | Priority | undefined | Priority.regular | Regular or enhanced palette of the trend line, its area, the scale bar and the setpoint. |
| state | InstrumentState | undefined | InstrumentState.active | Instrument state; the scale inherits it (locked, as upstream). |
| width | number | undefined | 480 | Total width in px (chart + scale). |
| height | number | undefined | 320 | Total height in px. |
| className | string | undefined | – | Class name added to the root element. |
| style | CSSProperties | undefined | – | Inline style added to the root element. |
| setpoint | number | undefined | – | Target setpoint value. |
| newSetpoint | number | undefined | – | Adjustment preview (2-step interface): while defined, the original setpoint marker dims and a focus-state marker renders at this value. |
| atSetpoint | boolean | undefined | – | Manual at-setpoint override; used when autoAtSetpoint is false. |
| touching | boolean | undefined | – | User is physically interacting with the control. Forces focus rendering and suppresses at-setpoint detection while true. |
| autoAtSetpoint | boolean | undefined | – | Enable automatic at-setpoint detection (default true). |
| autoAtSetpointDeadband | number | undefined | – | Tolerance for auto at-setpoint detection (default 2). |
| setpointAtZeroDeadband | number | undefined | – | Zero-snap tolerance for the marker's equalZero state (default 0.5). |
| setpointOverride | boolean | undefined | – | Derive the setpoint color from priority even when loading/off. |
| animateSetpoint | boolean | undefined | – | Enable the CSS-animated confirm transition. |
Related
- Gauge trend indicator: the compact cue for a trend.
- Gauge vertical: the scale without the chart.
- Data visualization: how a chart is drawn at the helm.
- Instruments: the other instruments.