Bar horizontal
A horizontal bar with a scale of tickmarks and labels, drawn in SVG at a width you give it.
On this page
Bow thruster: Idle, 30 m to the berth
Code
import {
AdvicePosition,
AdviceType,
BarContainerStyle,
BarHorizontal,
BorderRadiusPosition,
ExternalScaleSide,
FillMode,
FrameStyle,
InstrumentState,
Priority,
Readout,
ReadoutDirection,
ReadoutValueType,
ScaleType,
} 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 coming alongside in Port Grimaud on a one-second feed: a bow thruster the crew pushes from a preview to a confirmed setpoint, speed with a hull-speed caution range and a value dot, heel with reef and caution ranges in the bar, shore power tinted in its normal band, and an autopilot bar still loading.";
/** Feed steps before the docking sequence repeats. */
const CYCLE: number = 20;
/** The bars' width where the column allows it; on a narrower screen they take the column's width. */
const BAR_WIDTH: number = 520;
const SPEED_ADVICE: ExternalScaleAdvice[] = [
{ min: 5.5, max: 7, type: AdviceType.advice, hinted: false },
{ min: 8.5, max: 10, type: AdviceType.caution, hinted: true },
];
const HEEL_ADVICE: ExternalScaleAdvice[] = [
{ min: 15, max: 20, type: AdviceType.advice, hinted: true },
{ min: 20, max: 30, type: AdviceType.caution, hinted: false },
];
const ROW_STYLE: CSSProperties = { display: "grid", justifyItems: "start", gap: 2 };
const NAME_STYLE: CSSProperties = { fontSize: 12, color: "var(--element-neutral-color)" };
interface Thruster {
setpoint: number;
newSetpoint: number | undefined;
touching: boolean;
note: string;
}
/**
* The skipper's joystick while the bow swings onto the berth: idle, a push to starboard previewed at 40 percent, the
* push confirmed, a short burst to port to stop the swing, then idle again.
*/
function thrusterAt(step: number): Thruster {
if (step < 4) {
return { setpoint: 0, newSetpoint: undefined, touching: false, note: "Idle, 30 m to the berth" };
}
if (step < 7) {
return { setpoint: 0, newSetpoint: 40, touching: true, note: "Joystick to starboard, waiting to confirm" };
}
if (step < 13) {
return { setpoint: 40, newSetpoint: undefined, touching: false, note: "Bow swinging to starboard" };
}
if (step < 16) {
return { setpoint: -25, newSetpoint: undefined, touching: false, note: "Burst to port to stop the swing" };
}
return { setpoint: 0, newSetpoint: undefined, touching: false, note: "Alongside, lines ashore" };
}
function Row({ readout, children }: { readout: ReadoutProps; children: ReactNode }): ReactElement {
return (
<div style={ROW_STYLE}>
<Readout direction={ReadoutDirection.horizontal} {...readout} />
{children}
</div>
);
}
/**
* A bar takes its width in pixels, so the showcase measures the column the bars 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, barWidth]: [RefCallback<HTMLDivElement>, number] = useFittedWidth(BAR_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 thruster: Thruster = thrusterAt(phase);
const thrust: number = thruster.touching ? 0 : thruster.setpoint;
const speed: number = Math.max(0.3, 2.4 - phase * 0.11 + 0.2 * Math.sin(step));
const heel: number = 3 + 1.5 * Math.sin(step / 2);
const shoreCurrent: number = phase >= 16 ? 6.2 + 0.4 * Math.sin(step) : 0;
return (
<div ref={column} style={{ display: "grid", gap: 14, width: "100%", maxWidth: 560 }}>
<Row
readout={{
label: "Bow thruster",
value: thrust === 0 ? "0" : `${thrust > 0 ? "Stbd" : "Port"} ${String(Math.abs(thrust))}`,
valueType: ReadoutValueType.text,
unit: "%",
}}
>
<BarHorizontal
width={barWidth}
minValue={-100}
maxValue={100}
value={thrust}
fillMin={0}
fillMax={thrust}
hasBar
mainTickmarks={[-100, 0, 100]}
primaryTickmarkInterval={50}
secondaryTickmarkInterval={25}
scaleType={ScaleType.condensed}
scaleBackground
barContainerStyle={BarContainerStyle.secondary}
setpoint={thruster.setpoint}
newSetpoint={thruster.newSetpoint}
touching={thruster.touching}
animateSetpoint
priority={Priority.enhanced}
/>
</Row>
<Row readout={{ label: "Speed through water", value: speed, unit: "kn", fractionDigits: 1 }}>
<BarHorizontal
width={barWidth}
minValue={0}
maxValue={10}
value={speed}
hasBar
side={ExternalScaleSide.top}
mainTickmarks={[0, 10]}
primaryTickmarkInterval={2}
secondaryTickmarkInterval={1}
tertiaryTickmarkInterval={0.5}
advices={SPEED_ADVICE}
advicePosition={AdvicePosition.outer}
highlightCurrentValue
/>
</Row>
<Row readout={{ label: "Heel", value: heel, hasDegree: true, maxDigits: 2 }}>
<BarHorizontal
width={barWidth}
minValue={0}
maxValue={30}
value={heel}
hasBar
mainTickmarks={[0, 30]}
primaryTickmarkInterval={10}
secondaryTickmarkInterval={5}
advices={HEEL_ADVICE}
advicePosition={AdvicePosition.center}
priority={Priority.enhanced}
/>
</Row>
<Row readout={{ label: "Shore power", value: shoreCurrent, unit: "A", fractionDigits: 1 }}>
<BarHorizontal
width={barWidth}
minValue={0}
maxValue={16}
value={shoreCurrent}
hasBar
fillMode={FillMode.tint}
fillMin={4}
fillMax={10}
barFillColor="var(--automation-medium-hot)"
highlightCurrentValue
mainTickmarks={[0, 16]}
primaryTickmarkInterval={4}
secondaryTickmarkInterval={2}
frameStyle={FrameStyle.flat}
borderRadiusPosition={BorderRadiusPosition.outerLastChild}
borderRadius={4}
labelThickness={20}
/>
</Row>
<Row readout={{ label: "Autopilot rudder", value: null, hasDegree: true, off: true, offText: "Starting" }}>
<BarHorizontal
width={barWidth}
minValue={-35}
maxValue={35}
value={0}
hasBar
state={InstrumentState.loading}
mainTickmarks={[-35, 0, 35]}
primaryTickmarkInterval={10}
showLabels={false}
tickThickness={16}
/>
</Row>
<p style={{ margin: 0, textAlign: "center", ...NAME_STYLE }}>Bow thruster: {thruster.note}</p>
</div>
);
}
A horizontal bar draws a value as a bar and sets a scale of tickmarks and labels along it. width is the length in
pixels, and the height follows from the bands that are on: the bar, the tickmarks and the labels. side puts the scale
above or below the bar. The bar fills from fillMin to the value, or tints a fixed range. It takes advice ranges and a
setpoint marker.
The horizontal gauge is this bar with a fixed layout, a bar and a scale background that are always on.
Examples
Bar
Code
import { BarHorizontal } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "A bar 200 pixels wide, filled to 40 on the default 0 to 100 scale.";
export default function Bar(): ReactElement {
return (
<BarHorizontal hasBar value={40} width={200} />
);
}
Flexible geometry with a stable meaning
Use this primitive when the fixed GaugeHorizontal layout does not fit the composition. Choose the length for the available space, but keep the numeric domain and units stable. Ticks need enough room for their labels; reducing their count is preferable to squeezing overlapping text.
fillMin defines where the fill begins. For a signed quantity, show a meaningful zero; for a tinted operating
band, explain the two bounds. Pair the bar with a labeled numeric reading and source state. Its end position
alone cannot communicate an out-of-range or missing value.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| minValue | number | undefined | 0 | Minimum scale value. |
| maxValue | number | undefined | 100 | Maximum scale value. |
| width | number | undefined | 320 | Total width in pixels (including padding bands). |
| paddingLeft | number | undefined | BAR_CANVAS_PADDING | Padding left of the drawing area. |
| paddingRight | number | undefined | BAR_CANVAS_PADDING | Padding right of the drawing area. |
| side | ExternalScaleSide | undefined | ExternalScaleSide.bottom | Which side this scale lives on ('top' | 'bottom'). |
| hasScale | boolean | undefined | true | Show scale tickmarks. |
| showLabels | boolean | undefined | true | Show numeric labels at primary tickmark intervals. |
| hasBar | boolean | undefined | false | Show bar. |
| scaleBackground | boolean | undefined | false | Show background behind the scale tickmarks. |
| barContainerStyle | BarContainerStyle | undefined | – | Bar container background style. When undefined, defaults based on scaleBackground. |
| barThickness | number | undefined | 24 | Bar/fill thickness in pixels. |
| tickThickness | number | undefined | 24 | Tickmark band thickness in pixels. |
| labelThickness | number | undefined | 60 | Label band thickness in pixels. |
| 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. |
| scaleType | ScaleType | undefined | ScaleType.regular | Scale display mode: regular or condensed (shorter ticks). |
| frameStyle | FrameStyle | undefined | FrameStyle.regular | Frame style: regular (4px gap for all ticks) or flat (main tickmarks touch the edge). |
| borderRadiusPosition | BorderRadiusPosition | undefined | – | Border radius position based on component layout. |
| borderRadius | number | undefined | – | Border radius in pixels. Upstream reads this from the
--instrument-components-watchface-frame-regular-border-radius CSS
variable via a MutationObserver; this port takes the numeric value
directly, defaulting to 8px regular / 4px condensed. |
| 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 or tint. |
| barFillColor | string | undefined | – | Optional fill paint override; loading/off state still renders transparent. |
| 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 (affects colors and some marker behavior). |
| advicePosition | AdvicePosition | undefined | AdvicePosition.inner | Advice overlay positioning: center (in bar), inner (covers minor ticks), outer (no overlap). |
| 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
- Gauge horizontal: the same bar with a fixed layout.
- Bar vertical: the bar on a vertical axis.
- Instruments: the other instruments.