Skip to content
design
Instruments

Gauge trend indicator

A compact trend graph with a cue for the current value, for the space beside other indicators.

On this page
1012.0
Barometer
hPa
Graph zoomed to 1000–1015 hPa
16
True wind
kn
Gusting to 23 kn
7.8
Depth at anchor
m
Tide range 1.8 m, swinging clear
12.90
House voltage
V
Fridge and anchor light on
Both cue types, a graph zoomed in with its own chart range while the cue keeps the full scale, and a cue taken from the last sample, on a live night-watch feed.
Code
import { GaugeTrendIndicator, ObcGaugeTrendIndicatorType, Readout } from "@bosunkit/instruments";
import { useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, SetStateAction } from "react";

export const description: string =
  "The night watch summary on Althea, sampled every two seconds: both cue types, a graph zoomed in with its own chart range while the cue keeps the full scale, and a cue that falls back to the last sample when no value is given.";

/** Samples each graph keeps. */
const SAMPLES: number = 24;

interface Trend {
  name: string;
  reading: number;
  unit: string;
  fractionDigits: number;
  note: string;
  type: ObcGaugeTrendIndicatorType;
  data: number[];
  minValue: number;
  maxValue: number;
  chartMinValue?: number;
  chartMaxValue?: number;
  value?: number;
}

const TILE_STYLE: CSSProperties = {
  display: "grid",
  gridTemplateColumns: "48px 1fr",
  alignItems: "center",
  gap: 10,
  padding: 8,
  borderRadius: 8,
  background: "var(--container-section-color)",
};
const NAME_STYLE: CSSProperties = { fontSize: 12, color: "var(--element-neutral-color)" };

function sampled(level: (sample: number) => number, newest: number): number[] {
  return Array.from({ length: SAMPLES }, (_unused: unknown, index: number): number =>
    level(newest - SAMPLES + 1 + index),
  );
}

function pressureAt(sample: number): number {
  return 1012 - sample * 0.12 + 0.3 * Math.sin(sample / 2);
}

function windAt(sample: number): number {
  return 16 + 5 * Math.sin(sample / 2.5) + 2 * Math.sin(sample * 1.7);
}

function depthAt(sample: number): number {
  return 7.8 + 0.9 * Math.sin(sample / 4);
}

function houseVoltageAt(sample: number): number {
  return 12.9 - sample * 0.006 + 0.03 * Math.sin(sample);
}

function trendsAt(step: number): Trend[] {
  const pressure: number = pressureAt(step);
  const wind: number = windAt(step);
  const depth: number = depthAt(step);
  const voltage: number = houseVoltageAt(step);
  return [
    {
      name: "Barometer",
      reading: pressure,
      unit: "hPa",
      fractionDigits: 1,
      note: "Graph zoomed to 1000–1015 hPa",
      type: ObcGaugeTrendIndicatorType.Fill,
      data: sampled(pressureAt, step),
      minValue: 960,
      maxValue: 1050,
      chartMinValue: 1000,
      chartMaxValue: 1015,
      value: pressure,
    },
    {
      name: "True wind",
      reading: wind,
      unit: "kn",
      fractionDigits: 0,
      note: "Gusting to 23 kn",
      type: ObcGaugeTrendIndicatorType.Point,
      data: sampled(windAt, step),
      minValue: 0,
      maxValue: 40,
      value: wind,
    },
    {
      name: "Depth at anchor",
      reading: depth,
      unit: "m",
      fractionDigits: 1,
      note: "Tide range 1.8 m, swinging clear",
      type: ObcGaugeTrendIndicatorType.Point,
      data: sampled(depthAt, step),
      minValue: 0,
      maxValue: 15,
    },
    {
      name: "House voltage",
      reading: voltage,
      unit: "V",
      fractionDigits: 2,
      note: "Fridge and anchor light on",
      type: ObcGaugeTrendIndicatorType.Fill,
      data: sampled(houseVoltageAt, step),
      minValue: 11.5,
      maxValue: 14.5,
    },
  ];
}

export default function Showcase(): ReactElement {
  const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);

  useEffect((): (() => void) => {
    const timer: number = window.setInterval((): void => {
      setStep((previous: number): number => (previous + 1) % 60);
    }, 2000);
    return (): void => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div
      style={{
        display: "grid",
        gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))",
        gap: 8,
        width: "100%",
        maxWidth: 600,
      }}
    >
      {trendsAt(step).map((trend: Trend): ReactElement => (
        <div key={trend.name} style={TILE_STYLE}>
          <GaugeTrendIndicator
            type={trend.type}
            data={trend.data}
            minValue={trend.minValue}
            maxValue={trend.maxValue}
            chartMinValue={trend.chartMinValue}
            chartMaxValue={trend.chartMaxValue}
            value={trend.value}
          />
          <div style={{ display: "grid", gap: 2, minWidth: 0 }}>
            <Readout label={trend.name} value={trend.reading} unit={trend.unit} fractionDigits={trend.fractionDigits} />
            <span style={{ ...NAME_STYLE, fontSize: 11 }}>{trend.note}</span>
          </div>
        </div>
      ))}
    </div>
  );
}

The gauge trend indicator is the trend in a fixed 48 by 48 pixel footprint. A smooth graph fills the left part, a divider follows, and a cue for the current value sits at the right edge. The Fill type draws the cue as a fill and the Point type as a circular marker. The graph maps its samples to chartMinValue and chartMaxValue, which default to minValue and maxValue; the cue always maps against minValue and maxValue. Without a value the cue shows the last sample.

The graph has no scale or labels. Use the gauge trend when the crew needs to read the values.

Examples

Trend

A small trend graph with the current value of 50 as a fill at the right edge.
Code
import { GaugeTrendIndicator, ObcGaugeTrendIndicatorType } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A small trend graph with the current value of 50 as a fill at the right edge.";

export default function Trend(): ReactElement {
  return (
    <GaugeTrendIndicator
      type={ObcGaugeTrendIndicatorType.Fill}
      data={[45, 52, 48, 55, 62, 58, 52, 40, 32, 37, 55, 68]}
      minValue={0}
      maxValue={100}
      value={50}
    />
  );
}

Two scales in a compact view

The history and current cue can use different ranges. Keep them the same for direct comparison, or explain why the chart is magnified while the cue uses the full operating range. Otherwise the line can appear high while the current marker appears low for the same value.

State the history window and pair the cue with a labeled numeric reading. The last sample is not necessarily a fresh measurement. Show age and availability in the parent view, and use a full trend chart when readers need timing, gaps or precise values.

Props

PropTypeDefaultDescription
typeObcGaugeTrendIndicatorType | undefinedObcGaugeTrendIndicatorType.FillFill draws the current value as a fill at the right edge of the indicator. Point draws it as a circular marker.
datanumber[] | undefined[]Chart samples, oldest first.
minValuenumber | undefined0Lowest value of the right-edge cue, and of the trend graph unless chartMinValue is set.
maxValuenumber | undefined100Highest value of the right-edge cue, and of the trend graph unless chartMaxValue is set.
chartMinValuenumber | undefined–Chart y-range override; falls back to minValue.
chartMaxValuenumber | undefined–Chart y-range override; falls back to maxValue.
valuenumber | undefined–Current value for the right-edge cue; falls back to the last sample.
classNamestring | undefined–Class name added to the root element.
styleCSSProperties | undefined–Inline style added to the root element.