Instrument radial
The radial gauge building block, which composes the watch face with tickmarks, a value arc, a needle and advice zones.
On this page
Code
import {
AdviceType,
InstrumentRadial,
InstrumentState,
ObcGaugeRadialType,
Priority,
TickmarkStyle,
type GaugeRadialAdvice,
type RadialFrame,
} from "@bosunkit/instruments";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"The electrical panel of the vessel on a live feed: the house bank's charge filling a full dial toward its " +
"setpoint, the battery current on a half dial running through zero, the shore power limit being adjusted on a " +
"zoomed quarter dial, and the generator switched off on a face too small for its labels.";
const CHARGE_ADVICES: GaugeRadialAdvice[] = [
{ minValue: 0, maxValue: 20, type: AdviceType.caution, hinted: true },
{ minValue: 80, maxValue: 100, type: AdviceType.advice, hinted: true },
];
const CURRENT_ADVICES: GaugeRadialAdvice[] = [{ minValue: -60, maxValue: -40, type: AdviceType.caution, hinted: true }];
const SHORE_ADVICES: GaugeRadialAdvice[] = [{ minValue: 14, maxValue: 16, type: AdviceType.caution, hinted: true }];
const PANEL: CSSProperties = {
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))",
gap: 20,
alignItems: "center",
justifyItems: "center",
width: "100%",
maxWidth: 660,
};
const FIGURE: CSSProperties = { display: "flex", flexDirection: "column", alignItems: "center", gap: 6, margin: 0 };
const CAPTION: CSSProperties = { fontSize: 13, color: "var(--text-dim)", textAlign: "center", maxWidth: 220 };
/** A half dial across the top: -60 A at nine o'clock, zero at twelve, 60 A at three. */
function halfDialAngle(value: number): number {
return (value / 60) * 90;
}
/** A quarter dial from twelve to three o'clock for 0 to 16 A. */
function quarterDialAngle(value: number): number {
return (value / 16) * 90;
}
export default function Showcase(): ReactElement {
const [second, setSecond]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
const [generatorLabelsHidden, setGeneratorLabelsHidden]: [boolean, Dispatch<SetStateAction<boolean>>] =
useState<boolean>(false);
useEffect((): (() => void) => {
const interval: number = window.setInterval((): void => {
setSecond((previous: number): number => previous + 1);
}, 1000);
return (): void => {
window.clearInterval(interval);
};
}, []);
// The fridge compressor cycles while the solar panels charge, so the current swings through zero.
const current: number = 12 + 30 * Math.sin(second / 6);
const charge: number = 46 + 10 * ((1 - Math.cos(second / 20)) / 2);
const shoreDraw: number = 9 + 3 * Math.sin(second / 4);
// The skipper raises the shore power limit from 10 to 16 A over two seconds, then sets it back, every ten seconds.
const phase: number = second % 10;
const limit: number = Math.floor(second / 10) % 2 === 0 ? 10 : 16;
const adjusting: boolean = phase >= 8;
function recordGeneratorFrame(frame: RadialFrame): void {
setGeneratorLabelsHidden(frame.labelsHidden);
}
return (
<div style={PANEL}>
<figure style={FIGURE}>
<InstrumentRadial
value={charge}
minValue={0}
maxValue={100}
type={ObcGaugeRadialType.filled}
priority={Priority.enhanced}
tickmarkStyle={TickmarkStyle.enhanced}
primaryTickmarkInterval={25}
secondaryTickmarkInterval={5}
tertiaryTickmarkInterval={1}
showLabels
advices={CHARGE_ADVICES}
setpoint={50}
autoAtSetpoint
autoAtSetpointDeadband={2}
animateSetpoint
faceDiameter={180}
/>
<figcaption style={CAPTION}>House bank charge, %, against the 50 % floor</figcaption>
</figure>
<figure style={FIGURE}>
<InstrumentRadial
value={current}
minValue={-60}
maxValue={60}
getAngle={halfDialAngle}
type={ObcGaugeRadialType.bar}
barColor="var(--instrument-enhanced-secondary-color)"
primaryTickmarkInterval={30}
secondaryTickmarkInterval={10}
showLabels
endLabelsMaxMin
clipBottom={45}
advices={CURRENT_ADVICES}
faceDiameter={180}
/>
<figcaption style={CAPTION}>Battery current, A, charging to the right</figcaption>
</figure>
<figure style={FIGURE}>
<div style={{ width: 200, height: 160 }}>
<InstrumentRadial
value={shoreDraw}
minValue={0}
maxValue={16}
getAngle={quarterDialAngle}
type={ObcGaugeRadialType.needle}
needleColor="var(--instrument-enhanced-primary-color)"
primaryTickmarkInterval={4}
secondaryTickmarkInterval={1}
showLabels
tickmarksInside
advices={SHORE_ADVICES}
setpoint={limit}
newSetpoint={adjusting ? (limit === 10 ? 16 : 10) : undefined}
touching={adjusting}
setpointAtZeroDeadband={0.2}
animateSetpoint
zoomToFitArc
/>
</div>
<figcaption style={CAPTION}>Shore power draw, A, with the breaker limit being set</figcaption>
</figure>
<figure style={FIGURE}>
<InstrumentRadial
value={0}
minValue={0}
maxValue={100}
state={InstrumentState.off}
setpoint={70}
setpointOverride
atSetpoint={false}
primaryTickmarkInterval={25}
secondaryTickmarkInterval={5}
showLabels
clipTop={0}
clipLeft={0}
clipRight={0}
faceDiameter={90}
onFrameChange={recordGeneratorFrame}
/>
<figcaption style={CAPTION}>
Generator off{generatorLabelsHidden ? "; scale labels left out at this size" : ""}
</figcaption>
</figure>
</div>
);
}
The instrument radial is the building block under the radial gauge. It composes the watch face with tickmarks at the
intervals it is given, a value arc or a needle, advice zones and a setpoint marker, and sizes the face to fit its
container. A screen that shows a value on a dial uses Gauge radial, which adds the
sector windows and the readout. It ports obc-instrument-radial; the overview lists the other
instruments.
Examples
Half full
Code
import { InstrumentRadial } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "The gauge building block at the middle of its range.";
export default function HalfFull(): ReactElement {
return (
<div style={{ width: 200, height: 200 }}>
<InstrumentRadial value={50} />
</div>
);
}
Left-leaning quarter dial
Code
import { AdviceType, InstrumentRadial, ObcGaugeRadialType, type GaugeRadialAdvice } from "@bosunkit/instruments";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A left-leaning quarter dial, the mirror of the shore power dial: the inverter's load runs from nine o'clock up to " +
"twelve, with the pivot at the bottom right, so a pair of quarter dials can sit back to back.";
const INVERTER_ADVICES: GaugeRadialAdvice[] = [
{ minValue: 2400, maxValue: 3000, type: AdviceType.caution, hinted: true },
];
const FIGURE: CSSProperties = { display: "flex", flexDirection: "column", alignItems: "center", gap: 6, margin: 0 };
const CAPTION: CSSProperties = { fontSize: 13, color: "var(--text-dim)", textAlign: "center", maxWidth: 220 };
/** A quarter dial from nine to twelve o'clock for 0 to 3000 W. */
function leftQuarterAngle(value: number): number {
return -90 + (value / 3000) * 90;
}
export default function LeftLeaningQuarter(): ReactElement {
const [second, setSecond]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
useEffect((): (() => void) => {
const interval: number = window.setInterval((): void => {
setSecond((previous: number): number => previous + 1);
}, 1000);
return (): void => {
window.clearInterval(interval);
};
}, []);
// The watermaker and the kettle share the inverter, so its load steps between them.
const load: number = 1400 + 700 * Math.sin(second / 5);
return (
<figure style={FIGURE}>
<div style={{ width: 200, height: 160 }}>
<InstrumentRadial
value={load}
minValue={0}
maxValue={3000}
getAngle={leftQuarterAngle}
type={ObcGaugeRadialType.needle}
needleColor="var(--instrument-enhanced-primary-color)"
primaryTickmarkInterval={1000}
secondaryTickmarkInterval={250}
showLabels
tickmarksInside
advices={INVERTER_ADVICES}
zoomToFitArc
/>
</div>
<figcaption style={CAPTION}>Inverter load, W, leaning left from nine to twelve o'clock</figcaption>
</figure>
);
}
Primitive responsibilities
This primitive arranges radial geometry. The containing instrument supplies the quantity name, unit, numeric reading, source and quality state. Prefer GaugeRadial when those standard parts are required.
Keep the value-to-angle mapping consistent for ticks, advice and setpoints. Exercise both ends of the range and any signed zero, and verify label clearance at the smallest intended face. Clamping geometry cannot validate an input or establish that equipment remains within its operating limits.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| state | InstrumentState | undefined | InstrumentState.active | Instrument state (active, loading, off). |
| priority | Priority | undefined | Priority.regular | Color priority (enhanced = blue palette, regular = gray palette). |
| value | number | undefined | 0 | Value to show, in the unit of minValue and maxValue. Clamped to the range. A value that is not a number shows
no reading and triggers no advice. Default 0. |
| maxValue | number | undefined | 100 | Value at the end of the scale. Default 100. |
| minValue | number | undefined | 0 | Value at the start of the scale. Default 0. |
| getAngle | ((value: number) => number) | undefined | – | Value-to-angle mapping. When omitted (or returning a non-finite angle,
guarded to 0°), falls back to defaultGaugeAngle's linear 270° sweep. |
| needleColor | string | undefined | – | Needle color override; derived from state/priority when omitted. |
| barColor | string | undefined | – | Bar color override; derived from state/priority/type when omitted. |
| showLabels | boolean | undefined | false | Show numeric labels at primary ticks and the zero/min/max positions. |
| primaryTickmarkInterval | number | undefined | 50 | Interval for primary tickmarks in value units; undefined/<= 0 shows none. |
| secondaryTickmarkInterval | number | undefined | 10 | Interval for secondary tickmarks in value units; undefined/<= 0 shows none. |
| tertiaryTickmarkInterval | number | undefined | – | Interval for tertiary tickmarks in value units; undefined/<= 0 shows none. |
| type | ObcGaugeRadialType | undefined | ObcGaugeRadialType.filled | How the value is drawn: filled (default) fills the arc from zero, or from the nearest end of the range, to the
value; bar draws that arc in the quieter tertiary color with a short marker at the value; needle draws a
pointer from the center instead of an arc. |
| tickmarksInside | boolean | undefined | false | Draws the tickmarks and their labels inside the ring instead of outside it. Default false. |
| tickmarkStyle | TickmarkStyle | undefined | TickmarkStyle.regular | Tickmark color style. regular (default) draws every tick in the dim tertiary color; enhanced draws ticks in the
brighter primary color and tertiary ticks in the secondary color. |
| advices | GaugeRadialAdvice[] | undefined | [] | Zones on the scale, in value units. A zone is drawn as triggered while the value lies inside it, and as a hint when
hinted is true and the value lies outside. |
| clipTop | number | undefined | 0 | Top clip, % of height. Ignored when zoomToFitArc is true. |
| clipBottom | number | undefined | 0 | Bottom clip, % of height. Ignored when zoomToFitArc is true. |
| clipLeft | number | undefined | 0 | Left clip, % of width. Ignored when zoomToFitArc is true. |
| clipRight | number | undefined | 0 | Right clip, % of width. Ignored when zoomToFitArc is true. |
| endLabelsMaxMin | boolean | undefined | false | Place the horizontal end labels (±90°, e.g. min/max) below the tick instead of beside it (the "Max-min" radial label placement). |
| zoomToFitArc | boolean | undefined | false | Enlarges the visible arc to fill the container instead of keeping the full circle's scale. Default false.
Replaces the clip props. |
| faceDiameter | number | undefined | – | Outer-ring diameter in CSS pixels. When set, the instrument renders at a fixed intrinsic size derived from the ring, arc shape and label reserve — so instruments sharing the same value have identical ring circumference regardless of label width or arc extent. When unset (default), the instrument fills its container. |
| onFrameChange | ((frame: RadialFrame) => void) | undefined | – | Called after render whenever the computed radial frame changed (viewBox,
label visibility, or pinned host size). This replaces the upstream
frame-changed CustomEvent — a plain callback prop, no DOM event
choreography. Wrappers use it to align sibling overlays/readouts. |
| className | string | undefined | – | Class names added to the instrument's 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
- Gauge radial: the gauge built on this block.
- Watch: the face this block draws on.