Readout
A value with an optional setpoint, advice, label, unit and source, laid out vertically or horizontally.
On this page
Autopilot turning to 064° for the Sóller lighthouse; speed is under the 6.5 kn advised for an 18:00 arrival.
Code
import {
ObcTextboxFontWeight,
Readout,
ReadoutDataQuality,
ReadoutDirection,
ReadoutPriority,
ReadoutSize,
ReadoutValueType,
} from "@bosunkit/instruments";
import { Gauge, Navigation, Target, Wind } from "lucide-react";
import { useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, SetStateAction } from "react";
export const description: string =
"Althea's navigation readouts on a one-second feed: the autopilot heading closing on its setpoint, speed against the advised passage speed, the depth shoaling under the anchorage limit, and horizontal rows for wind of low integrity, an invalid sensor, a radar on standby, a text status and a label-only row.";
const PANEL_STYLE: CSSProperties = {
display: "grid",
gap: "var(--space-lg)",
padding: "var(--space-lg)",
borderRadius: "var(--radius-md)",
background: "var(--container-section-color)",
width: "100%",
maxWidth: "40em",
};
/** The three vertical readouts in equal columns, their feet on one line, so their sources line up. */
const HEADLINE_STYLE: CSSProperties = {
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
alignItems: "end",
justifyItems: "center",
gap: "var(--space-lg)",
};
/** The horizontal rows under a rule, each starting on the same edge. */
const ROWS_STYLE: CSSProperties = {
display: "grid",
justifyItems: "start",
gap: "var(--space-sm)",
paddingBlockStart: "var(--space-lg)",
borderBlockStart: "1px solid var(--border-divider-color)",
};
const NOTE_STYLE: CSSProperties = { margin: 0, font: "var(--bk-type-caption)", color: "var(--element-neutral-color)" };
/** A depth in tenths of a meter, floored, so it never shows more water than there is. */
function tenths(depth: number): string {
const floored: number = Math.floor(depth * 10);
return `${String(Math.trunc(floored / 10))}.${String(floored % 10)}`;
}
/** The autopilot's course to steer after the crew dialed 10° to starboard for the Sóller lighthouse. */
const HEADING_SETPOINT: number = 64;
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);
}, 1000);
return (): void => {
window.clearInterval(timer);
};
}, []);
const phase: number = step % 30;
const heading: number = Math.min(HEADING_SETPOINT, 54 + phase * 0.8);
const speed: number = 6.1 + 0.3 * Math.sin(step / 3);
const depth: number = 4.2 - phase * 0.05;
const shallow: boolean = depth < 3.5;
const trueWind: number = 17 + 2 * Math.sin(step / 2);
return (
<div style={PANEL_STYLE}>
<div style={HEADLINE_STYLE}>
<Readout
label="HDG"
src="Fluxgate compass"
value={heading}
hasDegree
maxDigits={3}
size={ReadoutSize.large}
priority={ReadoutPriority.enhanced}
hasSetpoint
setpoint={HEADING_SETPOINT}
setpointIcon={<Navigation size={14} aria-hidden />}
valueOptions={{ weight: ObcTextboxFontWeight.bold, hintedZeros: true }}
setpointOptions={{ hintedZeros: true }}
/>
<Readout
label="SOG"
unit="kn"
src="GPS"
value={speed}
fractionDigits={1}
maxDigits={2}
size={ReadoutSize.medium}
hasAdvice
advice={6.5}
adviceIcon={<Target size={14} aria-hidden />}
unitOptions={{ spaceReserver: "kn" }}
/>
<Readout
label="Depth"
unit="m"
src="Echosounder"
value={depth}
fractionDigits={1}
maxDigits={2}
size={ReadoutSize.medium}
/>
</div>
<div style={ROWS_STYLE}>
<Readout
direction={ReadoutDirection.horizontal}
label="TWS"
unit="kn"
src="Masthead, last calibrated 2025"
value={trueWind}
fractionDigits={1}
dataQuality={ReadoutDataQuality.lowIntegrity}
valueIcon={<Wind size={16} aria-hidden />}
valueOptions={{ hasIcon: true }}
/>
<Readout
direction={ReadoutDirection.horizontal}
label="Sea temp"
unit="°C"
src="Transducer"
value={null}
dataQuality={ReadoutDataQuality.invalid}
hasDegreeSpacer
/>
<Readout
direction={ReadoutDirection.horizontal}
label="Radar"
src="Halo 20+"
value={0}
off
offText="Standby"
valueIcon={<Gauge size={16} aria-hidden />}
valueOptions={{ hasIcon: true }}
/>
<Readout
direction={ReadoutDirection.horizontal}
label="AIS status"
src="Class B transponder"
value="Under way using engine"
valueType={ReadoutValueType.text}
/>
<Readout direction={ReadoutDirection.horizontal} label="Anchor watch, armed at 21:40" hasValue={false} />
</div>
<p style={NOTE_STYLE}>
{shallow
? `Depth ${tenths(depth)} m is under the 3.5 m limit set for this anchorage: slow down and turn toward deeper water to the north.`
: "Autopilot turning to 064° for the Sóller lighthouse; speed is under the 6.5 kn advised for an 18:00 arrival."}
</p>
</div>
);
}
A readout shows one measured value with the parts around it: a setpoint, an advice value, a label, a unit and the source
of the reading. The parts are laid out vertically or horizontally, in three sizes. fractionDigits sets the decimals
and maxDigits reserves the width of the integer digits, so the layout does not move as the value changes.
A value of null shows a dash, and valueType="text" shows text as given. dataQuality marks a low-integrity or
invalid value, and alert draws an alert frame around the readout. The source can open a picker or a flyout.
Examples
Value
Code
import { Readout } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "A value of 9.87 shown to one decimal, with room for three integer digits.";
export default function Value(): ReactElement {
return (
<Readout value={9.87} fractionDigits={1} maxDigits={3} />
);
}
No value
Code
import { Readout } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "A value that is not available shows a dash, so the layout does not move.";
export default function NoValue(): ReactElement {
return (
<Readout value={null} />
);
}
A complete reading
Keep the quantity, unit and reference together: a depth below the surface and an under-keel clearance need different labels. Set precision from the measurement and domain policy, and reserve enough digits for the full operating range. Do not resize the value every time it crosses a digit boundary.
dataQuality is supplied by the caller; the component does not decide whether telemetry is stale. Use the
application's age and integrity adapter, or Reading for its richer integrity
presentation. A source picker needs an actual choice flow and a clear accessible name.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | undefined | – | Name of the quantity, for example "Depth". |
| unit | string | undefined | – | Unit shown beside the value, for example "m". Nothing is shown when it is empty. |
| src | string | undefined | – | Name of the source of the reading, for example "Sonar". A blank string shows no source row. |
| hasValue | boolean | undefined | true | Layout switch: false renders a deliberately value-less (label-only)
readout that hugs its remaining parts. For a temporarily missing value
keep hasValue and set value to null instead — the dash keeps the
value block at full size, so the layout does not shift when data arrives. |
| value | number | string | null | undefined | null | The value; null renders a dash. A number by default, or text when
{@link ReadoutProps.valueType} is text. |
| valueType | ReadoutValueType | undefined | ReadoutValueType.number | How {@link ReadoutProps.value} is interpreted. number (default) formats
it via fractionDigits; text renders it verbatim and ignores the
numeric format options. |
| off | boolean | undefined | false | Render the value as offText (e.g. equipment powered down). Affects the value only. |
| offText | string | undefined | "OFF" | Text shown in place of the value when off is true. Available when off==true. |
| hasSetpoint | boolean | undefined | false | Shows the setpoint block beside the value. |
| setpoint | number | undefined | – | Available when hasSetpoint==true. |
| hasAdvice | boolean | undefined | false | Shows the advice block beside the value. |
| advice | number | undefined | – | Available when hasAdvice==true. |
| size | ReadoutSize | undefined | – | Density of the readout: small (default), medium or large. Larger sizes use larger digits. |
| priority | ReadoutPriority | undefined | – | Color emphasis of the value: regular (default) or enhanced, the in-command accent color. |
| direction | ReadoutDirection | undefined | – | vertical (default) stacks the blocks above the label and unit. horizontal puts every segment on one row. |
| stacking | ReadoutStacking | undefined | – | Available when direction==vertical. |
| alignment | ReadoutAlignment | undefined | – | Available when direction==vertical && stacking==stacked. |
| hasDegree | boolean | undefined | false | Appends a degree sign (°) after each number, for headings and angles. |
| hasDegreeSpacer | boolean | undefined | false | Available when hasDegree==false. |
| fractionDigits | number | undefined | 0 | Also formats the numeric setpoint / advice blocks, which stay numeric even when the value is text. Available when valueType==number || hasSetpoint || hasAdvice. |
| maxDigits | number | undefined | 0 | Same availability as {@link ReadoutProps.fractionDigits}. |
| dataQuality | ReadoutDataQuality | undefined | – | Marks the value as low-integrity or invalid with an outline. |
| alert | boolean | AlertFrameConfig | undefined | false | Readout-level alert frame. true normalizes to a default frame; an
{@link AlertFrameConfig} object configures it; false renders none. |
| valueOptions | ReadoutValueOptions | undefined | – | Available when hasValue==true. |
| setpointOptions | ReadoutSetpointOptions | undefined | – | Available when hasSetpoint==true. |
| adviceOptions | ReadoutAdviceOptions | undefined | – | Available when hasAdvice==true. |
| unitOptions | ReadoutReserverOptions | undefined | – | Available when unit!=''. |
| srcOptions | ReadoutSourceOptions | undefined | – | Available when src!=''. |
| showDebugOverlay | boolean | undefined | false | Development aid: outline the readout building blocks (red), the degree columns (blue) and the degree spacer (green) so reserver widths / alignment are visible. Off by default. |
| valueIcon | ReactNode | undefined | – | Icon before the value (the upstream value-icon slot). Available when valueOptions.hasIcon==true. |
| setpointIcon | ReactNode | undefined | – | Overrides the default setpoint icon (the upstream setpoint-icon slot). |
| adviceIcon | ReactNode | undefined | – | Overrides the default advice icon (the upstream advice-icon slot). |
| srcPickerContent | ReactNode | undefined | – | Source picker pop-over content (the upstream src-picker-content slot).
Rendered below the source row while the picker is open. The upstream
element parsed slotted obc-navigation-items into a context menu and
fired source-change; the React translation renders this content
directly, so consumers attach their own selection handlers to it. |
| onSourceFlyoutClick | ((src: string) => void) | undefined | – | Fired when the source row is clicked while srcOptions.interaction == 'flyout'. |
| className | string | undefined | – | Class name added to the root element. |
Related
- Readout list item: the same parts as a row in a list.
- Readout block: the block each part is built from.
- Data integrity: how a live value shows how far to trust it.
- Instruments: the other instruments.