Skip to content
design
Instruments

Gauge trend

A trend line with a vertical scale beside it, for a value and where it has been.

On this page
1008.0
Barometer, last 24 h
hPa
-1.5 hPa in 3 h.
9901000101010201030
20
True wind, last 6 h
kn
010203040
19.4
Sea temperature
°C
Collecting data…
1015202530
A filled trend with its level bar, an alarm setpoint and pressure ranges, a recolored line with a value dot on a condensed scale, and a sensor collecting its first samples with a tinted band, on a live barometer and wind feed.
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

0102030405060708090100
A trend with the level bar, the scale, a setpoint at 50 and two advice ranges.
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

PropTypeDefaultDescription
dataGaugeTrendPoint[] | undefined[]Series samples, oldest first. Non-finite entries are dropped.
minValuenumber | undefined0Minimum of the shared chart/scale range.
maxValuenumber | undefined100Maximum of the shared chart/scale range.
valuenumber | undefined–Current value displayed on the vertical scale: bar fill level when hasBar, dot indicator position otherwise.
hasBarboolean | undefinedfalseShow the value bar on the scale band.
hasScaleboolean | undefinedfalseShow scale tickmarks and labels.
chartFillboolean | undefinedfalseFill the area under the trend line (semitransparent).
fillModeFillMode | undefinedFillMode.fillFill mode for the scale bar: fill tracks value from fillMin; tint shows the fixed fillMin..fillMax range.
barFillColorstring | undefined–Optional paint override for the integrated scale bar.
seriesColorstring | undefined–Optional paint override for the chart line and its semitransparent area.
fillMinnumber | undefined0Fill origin value for the scale bar.
fillMaxnumber | undefined–Upper fill bound (tint mode only; defaults to value).
advicesExternalScaleAdvice[] | 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.
primaryTickmarkIntervalnumber | undefined–Interval for primary (longest, labeled) scale tickmarks.
secondaryTickmarkIntervalnumber | undefined–Interval for secondary scale tickmarks.
tertiaryTickmarkIntervalnumber | undefined–Interval for tertiary scale tickmarks.
scaleTypeScaleType | undefinedScaleType.regularScale tick density preset.
priorityPriority | undefinedPriority.regularRegular or enhanced palette of the trend line, its area, the scale bar and the setpoint.
stateInstrumentState | undefinedInstrumentState.activeInstrument state; the scale inherits it (locked, as upstream).
widthnumber | undefined480Total width in px (chart + scale).
heightnumber | undefined320Total height in px.
classNamestring | undefined–Class name added to the root element.
styleCSSProperties | undefined–Inline style added to the root element.
setpointnumber | undefined–Target setpoint value.
newSetpointnumber | undefined–Adjustment preview (2-step interface): while defined, the original setpoint marker dims and a focus-state marker renders at this value.
atSetpointboolean | undefined–Manual at-setpoint override; used when autoAtSetpoint is false.
touchingboolean | undefined–User is physically interacting with the control. Forces focus rendering and suppresses at-setpoint detection while true.
autoAtSetpointboolean | undefined–Enable automatic at-setpoint detection (default true).
autoAtSetpointDeadbandnumber | undefined–Tolerance for auto at-setpoint detection (default 2).
setpointAtZeroDeadbandnumber | undefined–Zero-snap tolerance for the marker's equalZero state (default 0.5).
setpointOverrideboolean | undefined–Derive the setpoint color from priority even when loading/off.
animateSetpointboolean | undefined–Enable the CSS-animated confirm transition.