Skip to content
design
Instruments

Gauge horizontal

A horizontal gauge with a bar and a scale, in a fixed layout for single values.

On this page
65
Engine load
%
020406080100
5.6
Speed over ground
kn
0246810
48
Alternator
A
020406080100120
Starting
Apparent wind
kn

Engine: Cruising at 65 %, 2,300 rpm.

A setpoint previewed and confirmed into a caution range, hinted and triggered advice, a value dot on a scale above, a tinted normal band and a loading state, on a live engine feed.
Code
import {
  AdviceType,
  BorderRadiusPosition,
  ExternalScaleSide,
  FillMode,
  GaugeHorizontal,
  InstrumentState,
  Priority,
  Readout,
  ReadoutDirection,
} from "@bosunkit/instruments";
import type { ExternalScaleAdvice, ReadoutProps } from "@bosunkit/instruments";
import { useCallback, useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, ReactNode, RefCallback, SetStateAction } from "react";

export const description: string =
  "Althea motoring out of Mahón on a one-second feed: an engine load setpoint previewed and confirmed into its caution range, speed with a value dot and a hinted hull-speed range on a scale above, alternator current tinting its normal band, and a wind sensor still loading.";

/** Feed steps before the engine sequence repeats. */
const CYCLE: number = 24;
/** The gauges' width where the column allows it, the width OpenBridge draws them at. */
const GAUGE_WIDTH: number = 384;

const LOAD_ADVICE: ExternalScaleAdvice[] = [
  { min: 60, max: 75, type: AdviceType.advice, hinted: true },
  { min: 85, max: 100, type: AdviceType.caution, hinted: false },
];

const SPEED_ADVICE: ExternalScaleAdvice[] = [{ min: 8.5, max: 10, type: AdviceType.caution, hinted: true }];

const NAME_STYLE: CSSProperties = { fontSize: 12, color: "var(--element-neutral-color)" };

interface Load {
  setpoint: number;
  newSetpoint: number | undefined;
  touching: boolean;
  note: string;
}

/**
 * The engine load through the harbor entrance: 65 percent at cruise, the skipper pushes the lever toward 90 percent to
 * punch the tide in the entrance, confirms it into the caution range, then throttles back once clear.
 */
function loadAt(step: number): Load {
  if (step < 6) {
    return { setpoint: 65, newSetpoint: undefined, touching: false, note: "Cruising at 65 %, 2,300 rpm." };
  }
  if (step < 9) {
    return { setpoint: 65, newSetpoint: 90, touching: true, note: "Lever moved to 90 %, waiting to confirm." };
  }
  if (step < 18) {
    return {
      setpoint: 90,
      newSetpoint: undefined,
      touching: false,
      note: "Above 85 % against the tide: keep it under 10 minutes and watch the coolant.",
    };
  }
  return { setpoint: 65, newSetpoint: undefined, touching: false, note: "Clear of the entrance, back to 65 %." };
}

function Row({ readout, children }: { readout: ReadoutProps; children: ReactNode }): ReactElement {
  return (
    <div style={{ display: "grid", justifyItems: "start", gap: 2 }}>
      <Readout direction={ReadoutDirection.horizontal} {...readout} />
      {children}
    </div>
  );
}

/**
 * A horizontal gauge takes its width in pixels, so the showcase measures the column the gauges sit in and draws them
 * at `preferred`, or at the column's width when the screen is narrower than that.
 */
function useFittedWidth(preferred: number): [RefCallback<HTMLDivElement>, number] {
  const [width, setWidth]: [number, Dispatch<SetStateAction<number>>] = useState<number>(preferred);
  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.min(preferred, Math.floor(available)));
        }
      });
      observer.observe(column);
      return (): void => {
        observer.disconnect();
      };
    },
    [preferred],
  );
  return [measure, width];
}

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

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

  const phase: number = step % CYCLE;
  const load: Load = loadAt(phase);
  const engineLoad: number = phase >= 9 && phase < 18 ? Math.min(90, 65 + (phase - 8) * 5) : 65 + Math.sin(step);
  const speed: number = 5.6 + (engineLoad - 65) * 0.04 + 0.15 * Math.sin(step / 2);
  const alternator: number = 48 + 6 * Math.sin(step / 3);

  return (
    <div ref={column} style={{ display: "grid", gap: 12, justifyItems: "center", width: "100%", maxWidth: 620 }}>
      <Row readout={{ label: "Engine load", value: engineLoad, unit: "%", maxDigits: 3 }}>
        <GaugeHorizontal
          width={gaugeWidth}
          minValue={0}
          maxValue={100}
          value={engineLoad}
          mainTickmarks={[0, 100]}
          primaryTickmarkInterval={20}
          secondaryTickmarkInterval={10}
          tertiaryTickmarkInterval={5}
          setpoint={load.setpoint}
          newSetpoint={load.newSetpoint}
          touching={load.touching}
          autoAtSetpointDeadband={3}
          animateSetpoint
          advices={LOAD_ADVICE}
          priority={Priority.enhanced}
        />
      </Row>
      <Row readout={{ label: "Speed over ground", value: speed, unit: "kn", fractionDigits: 1 }}>
        <GaugeHorizontal
          width={gaugeWidth}
          minValue={0}
          maxValue={10}
          value={speed}
          side={ExternalScaleSide.top}
          mainTickmarks={[0, 10]}
          primaryTickmarkInterval={2}
          secondaryTickmarkInterval={1}
          advices={SPEED_ADVICE}
          highlightCurrentValue
          borderRadiusPosition={BorderRadiusPosition.outerLastChild}
        />
      </Row>
      <Row readout={{ label: "Alternator", value: alternator, unit: "A", maxDigits: 3 }}>
        <GaugeHorizontal
          width={gaugeWidth}
          minValue={0}
          maxValue={120}
          value={alternator}
          fillMode={FillMode.tint}
          fillMin={30}
          fillMax={80}
          mainTickmarks={[0, 120]}
          primaryTickmarkInterval={20}
          secondaryTickmarkInterval={10}
          setpoint={55}
          setpointOverride
          borderRadiusPosition={BorderRadiusPosition.middleRoundedChild}
        />
      </Row>
      <Row readout={{ label: "Apparent wind", value: null, unit: "kn", off: true, offText: "Starting" }}>
        <GaugeHorizontal
          width={gaugeWidth}
          minValue={0}
          maxValue={50}
          value={0}
          mainTickmarks={[0, 50]}
          primaryTickmarkInterval={10}
          showLabels={false}
          state={InstrumentState.loading}
        />
      </Row>
      <p style={{ margin: 0, textAlign: "center", ...NAME_STYLE }}>Engine: {load.note}</p>
    </div>
  );
}

A horizontal gauge shows one value, such as speed, power or progress, against a scale that is always drawn. It is the horizontal bar with its layout fixed: a length of 384 pixels, and a bar and a scale background that are always on, so every gauge has the same proportions. It fills to the value, or tints a range, and takes advice ranges and a setpoint marker.

Use the horizontal bar when the length or the bands need to change.

Examples

Gauge

020406080100
A horizontal gauge at 60 with a setpoint at 45.
Code
import { GaugeHorizontal } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A horizontal gauge at 60 with a setpoint at 45.";

export default function Gauge(): ReactElement {
  return (
    <GaugeHorizontal value={60} primaryTickmarkInterval={20} secondaryTickmarkInterval={10} setpoint={45} />
  );
}

Comparing values on a fixed scale

Use matching limits, units and tick intervals for gauges that readers compare. A fixed layout gives consistent proportions; it does not choose the measurement range. Keep a descriptive label and exact value beside the scale.

A fill, tinted range and setpoint communicate different things. Name the normal operating range separately from the requested target, and check which value triggers the selected advice treatment. Range clipping is a visual constraint, not validation of a command or rejection of a sensor reading.

Props

PropTypeDefaultDescription
minValuenumber | undefined0Minimum scale value.
maxValuenumber | undefined100Maximum scale value.
sideExternalScaleSide | undefinedExternalScaleSide.bottomWhich side of the chart area this scale lives on ('top' | 'bottom').
showLabelsboolean | undefinedtrueShow numerical value labels at primary tickmarks.
widthnumber | undefinedDEFAULT_GAUGE_LENGTHTotal width in pixels, including the 32px padding bands at both ends. Default 384, the width upstream locks; a narrower width fits the gauge to a phone's column, with the same band thicknesses and type size.
mainTickmarksnumber[] | undefined[]Array of values for main tickmarks. When undefined, no main tickmarks are shown. When empty array [], defaults to [minValue, 0, maxValue].
primaryTickmarkIntervalnumber | undefined–Interval for primary (longest) tickmarks with labels.
secondaryTickmarkIntervalnumber | undefined–Interval for secondary (medium) tickmarks.
tertiaryTickmarkIntervalnumber | undefined–Interval for tertiary (shortest) tickmarks.
borderRadiusPositionBorderRadiusPosition | undefinedBorderRadiusPosition.innerFirstChildBorder radius position based on component layout.
priorityPriority | undefinedPriority.regularColor priority: enhanced uses blue instrument colors for bar fill and setpoint.
fillModeFillMode | undefinedFillMode.fillFill visualization mode: 'fill' shows bar from fillMin to fillMax; 'tint' adds a marker at the value.
fillMinnumber | undefined–Minimum fill value (defaults to 0).
fillMaxnumber | undefined–Maximum fill value (defaults to value).
valuenumber | undefined–Current value (bar fill level).
stateInstrumentState | undefinedInstrumentState.activeInstrument state: active, loading, or off.
advicesExternalScaleAdvice[] | undefined[]Advice/alert overlays. When undefined or empty, no advice shown.
highlightCurrentValueboolean | undefinedfalseDisplay a dot indicator at the current value position instead of relying on bar fill.
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.