Gauge horizontal
A horizontal gauge with a bar and a scale, in a fixed layout for single values.
On this page
Engine: Cruising at 65 %, 2,300 rpm.
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
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
| Prop | Type | Default | Description |
|---|---|---|---|
| minValue | number | undefined | 0 | Minimum scale value. |
| maxValue | number | undefined | 100 | Maximum scale value. |
| side | ExternalScaleSide | undefined | ExternalScaleSide.bottom | Which side of the chart area this scale lives on ('top' | 'bottom'). |
| showLabels | boolean | undefined | true | Show numerical value labels at primary tickmarks. |
| width | number | undefined | DEFAULT_GAUGE_LENGTH | Total 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. |
| mainTickmarks | number[] | undefined | [] | Array of values for main tickmarks. When undefined, no main tickmarks are shown. When empty array [], defaults to [minValue, 0, maxValue]. |
| primaryTickmarkInterval | number | undefined | – | Interval for primary (longest) tickmarks with labels. |
| secondaryTickmarkInterval | number | undefined | – | Interval for secondary (medium) tickmarks. |
| tertiaryTickmarkInterval | number | undefined | – | Interval for tertiary (shortest) tickmarks. |
| borderRadiusPosition | BorderRadiusPosition | undefined | BorderRadiusPosition.innerFirstChild | Border radius position based on component layout. |
| priority | Priority | undefined | Priority.regular | Color priority: enhanced uses blue instrument colors for bar fill and setpoint. |
| fillMode | FillMode | undefined | FillMode.fill | Fill visualization mode: 'fill' shows bar from fillMin to fillMax; 'tint' adds a marker at the value. |
| fillMin | number | undefined | – | Minimum fill value (defaults to 0). |
| fillMax | number | undefined | – | Maximum fill value (defaults to value). |
| value | number | undefined | – | Current value (bar fill level). |
| state | InstrumentState | undefined | InstrumentState.active | Instrument state: active, loading, or off. |
| advices | ExternalScaleAdvice[] | undefined | [] | Advice/alert overlays. When undefined or empty, no advice shown. |
| highlightCurrentValue | boolean | undefined | false | Display a dot indicator at the current value position instead of relying on bar fill. |
| 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
- Bar horizontal: the same scale with a layout you set.
- Gauge vertical: the gauge on a vertical axis.
- Instruments: the other instruments.