Gauge vertical
A vertical gauge with a bar and a scale, in a fixed layout for single values.
On this page
Watermaker: Making 27 L/h at 55 bar.
Code
import {
AdviceType,
BorderRadiusPosition,
ExternalScaleSide,
FillMode,
GaugeVertical,
InstrumentState,
Priority,
Readout,
} 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's watermaker and engine on a one-second feed: a feed pressure setpoint the crew previews and confirms into its caution range, oil pressure with a value dot and a hinted low range on a left-hand scale, coolant tinting its normal band, and a generator that is off.";
/** Feed steps before the watermaker sequence repeats. */
const CYCLE: number = 24;
const MEMBRANE_ADVICE: ExternalScaleAdvice[] = [
{ min: 50, max: 58, type: AdviceType.advice, hinted: true },
{ min: 60, max: 70, type: AdviceType.caution, hinted: false },
];
const OIL_ADVICE: ExternalScaleAdvice[] = [{ min: 0, max: 1, type: AdviceType.caution, hinted: true }];
const CELL_STYLE: CSSProperties = { display: "grid", justifyItems: "center", alignContent: "start", gap: 4 };
const NAME_STYLE: CSSProperties = { fontSize: 12, color: "var(--element-neutral-color)" };
/** The gauges' height where the screen allows it, the height OpenBridge draws them at. */
const GAUGE_HEIGHT: number = 384;
/**
* An empty box as tall as a gauge may be: 384 px, or on a screen whose height is known (`100cqb`, a phone on its side)
* that height less the stage's padding and the readout above the gauge.
*/
const ROOM_STYLE: CSSProperties = {
position: "absolute",
width: 0,
height: "min(384px, calc(100cqb - 136px))",
visibility: "hidden",
};
interface Membrane {
setpoint: number;
newSetpoint: number | undefined;
touching: boolean;
note: string;
}
/**
* The watermaker's feed pressure: running at 55 bar, the crew drags the setpoint to 63 bar to make water faster in
* cold sea water, confirms it into the caution range, then brings it back to protect the membrane.
*/
function membraneAt(step: number): Membrane {
if (step < 6) {
return { setpoint: 55, newSetpoint: undefined, touching: false, note: "Making 27 L/h at 55 bar." };
}
if (step < 9) {
return { setpoint: 55, newSetpoint: 63, touching: true, note: "Setpoint moved to 63 bar, waiting to confirm." };
}
if (step < 18) {
return {
setpoint: 63,
newSetpoint: undefined,
touching: false,
note: "Above 60 bar the membrane wears faster: run here for an hour at most.",
};
}
return { setpoint: 55, newSetpoint: undefined, touching: false, note: "Back to 55 bar." };
}
function Cell({ readout, children }: { readout: ReadoutProps; children: ReactNode }): ReactElement {
return (
<div style={CELL_STYLE}>
<Readout {...readout} />
{children}
</div>
);
}
/**
* A vertical gauge takes its height in pixels, so the showcase measures the room box and draws the gauges at
* `preferred`, or at the room's height when the screen is shorter than that.
*/
function useFittedHeight(preferred: number): [RefCallback<HTMLDivElement>, number] {
const [height, setHeight]: [number, Dispatch<SetStateAction<number>>] = useState<number>(preferred);
const measure: RefCallback<HTMLDivElement> = useCallback(
(room: HTMLDivElement | null): (() => void) | undefined => {
if (room === null) {
return undefined;
}
const observer: ResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]): void => {
const available: number | undefined = entries.at(0)?.contentRect.height;
if (available !== undefined && available > 0) {
setHeight(Math.min(preferred, Math.floor(available)));
}
});
observer.observe(room);
return (): void => {
observer.disconnect();
};
},
[preferred],
);
return [measure, height];
}
export default function Showcase(): ReactElement {
const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
const [room, gaugeHeight]: [RefCallback<HTMLDivElement>, number] = useFittedHeight(GAUGE_HEIGHT);
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 membrane: Membrane = membraneAt(phase);
const feedPressure: number =
phase >= 9 && phase < 18 ? Math.min(63, 55 + (phase - 8) * 1.6) : 55 + 0.4 * Math.sin(step);
const oilPressure: number = 3.8 + 0.25 * Math.sin(step / 2);
const coolant: number = 83 + 2.5 * Math.sin(step / 4);
return (
<div style={{ position: "relative", display: "grid", gap: 10, width: "100%", maxWidth: 620 }}>
<div ref={room} aria-hidden style={ROOM_STYLE} />
<div style={{ display: "flex", flexWrap: "wrap", gap: 16, justifyContent: "center" }}>
<Cell readout={{ label: "Feed pressure", value: feedPressure, unit: "bar", maxDigits: 2 }}>
<GaugeVertical
height={gaugeHeight}
minValue={0}
maxValue={70}
value={feedPressure}
mainTickmarks={[0, 70]}
primaryTickmarkInterval={10}
secondaryTickmarkInterval={5}
tertiaryTickmarkInterval={1}
setpoint={membrane.setpoint}
newSetpoint={membrane.newSetpoint}
touching={membrane.touching}
autoAtSetpointDeadband={1}
animateSetpoint
advices={MEMBRANE_ADVICE}
priority={Priority.enhanced}
/>
</Cell>
<Cell readout={{ label: "Oil pressure", value: oilPressure, unit: "bar", fractionDigits: 1 }}>
<GaugeVertical
height={gaugeHeight}
minValue={0}
maxValue={6}
value={oilPressure}
side={ExternalScaleSide.left}
mainTickmarks={[0, 6]}
primaryTickmarkInterval={1}
secondaryTickmarkInterval={0.5}
advices={OIL_ADVICE}
highlightCurrentValue
borderRadiusPosition={BorderRadiusPosition.outerLastChild}
/>
</Cell>
<Cell readout={{ label: "Coolant", value: coolant, unit: "°C", maxDigits: 3 }}>
<GaugeVertical
height={gaugeHeight}
minValue={40}
maxValue={110}
value={coolant}
fillMode={FillMode.tint}
fillMin={75}
fillMax={90}
primaryTickmarkInterval={10}
secondaryTickmarkInterval={5}
setpoint={82}
setpointOverride
borderRadiusPosition={BorderRadiusPosition.middleRoundedChild}
/>
</Cell>
<Cell readout={{ label: "Generator", value: 0, unit: "A", off: true, offText: "Off" }}>
<GaugeVertical
height={gaugeHeight}
minValue={0}
maxValue={30}
value={0}
mainTickmarks={[0, 30]}
primaryTickmarkInterval={10}
secondaryTickmarkInterval={5}
showLabels={false}
state={InstrumentState.off}
/>
</Cell>
</div>
<p style={{ margin: 0, textAlign: "center", ...NAME_STYLE }}>Watermaker: {membrane.note}</p>
</div>
);
}
A vertical gauge shows one value, such as a tank level, a pressure or a temperature, against a scale that is always drawn. It is the vertical 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.
An advice range is triggered when the setpoint is inside it, not the value. Use the vertical bar when the length or the bands need to change.
Examples
Gauge
Code
import { GaugeVertical } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "A vertical gauge at 75 with a setpoint at 80.";
export default function Gauge(): ReactElement {
return (
<GaugeVertical value={75} primaryTickmarkInterval={20} secondaryTickmarkInterval={10} setpoint={80} />
);
}
Advice triggered
Code
import { AdviceType, GaugeVertical } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "An advice range is triggered when the setpoint is inside it, here 85 in 80 to 100.";
export default function AdviceTriggered(): ReactElement {
return (
<GaugeVertical
value={62}
setpoint={85}
advices={[{ min: 80, max: 100, type: AdviceType.caution, hinted: false }]}
primaryTickmarkInterval={20}
secondaryTickmarkInterval={10}
/>
);
}
Advice hinted
Code
import { AdviceType, GaugeVertical } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "A hinted advice range stays hinted while the setpoint is outside it.";
export default function AdviceHinted(): ReactElement {
return (
<GaugeVertical
value={40}
setpoint={50}
advices={[{ min: 80, max: 100, type: AdviceType.advice, hinted: true }]}
primaryTickmarkInterval={20}
secondaryTickmarkInterval={10}
/>
);
}
Value dot
Code
import { GaugeVertical } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "`highlightCurrentValue` marks the value with a dot on the scale.";
export default function ValueDot(): ReactElement {
return (
<GaugeVertical value={64} highlightCurrentValue primaryTickmarkInterval={20} secondaryTickmarkInterval={10} />
);
}
Tint band
Code
import { FillMode, GaugeVertical } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "The tint fill mode shades the fixed range from 30 to 70, whatever the value is.";
export default function TintBand(): ReactElement {
return (
<GaugeVertical
value={50}
fillMode={FillMode.tint}
fillMin={30}
fillMax={70}
primaryTickmarkInterval={20}
secondaryTickmarkInterval={10}
/>
);
}
Ranges and requested values
Keep units, limits and tick intervals consistent across adjacent columns. Reserve enough room for negative values, large labels and any unit or source text. A changing scale makes equal-height bars difficult to compare.
Advice in this component is triggered by the setpoint. A highlighted band therefore cannot stand alone as proof that the measured value crossed a threshold. Use a separate alert or reading state for that fact, and distinguish loading from a valid value at the bottom of the scale.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| minValue | number | undefined | 0 | Minimum scale value. |
| maxValue | number | undefined | 100 | Maximum scale value. |
| side | ExternalScaleSide | undefined | ExternalScaleSide.right | Which side of the chart area this scale lives on ('left' | 'right'). |
| showLabels | boolean | undefined | true | Show numerical value labels at primary tickmarks. |
| height | number | undefined | DEFAULT_GAUGE_LENGTH | Total height in pixels, including the 32px padding bands at both ends. Default 384, the height upstream locks; a shorter height fits the gauge to a phone held on its side, 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 vertical: the same scale with a layout you set.
- Gauge horizontal: the gauge on a horizontal axis.
- Automation tank: a level shown as a tank.
- Instruments: the other instruments.