Skip to content
design
Instruments

Bar horizontal

A horizontal bar with a scale of tickmarks and labels, drawn in SVG at a width you give it.

On this page
0
Bow thruster
%
050100-50-100
2.4
Speed through water
kn
0246810
3
°
Heel
0102030
0.0
Shore power
A
0481216
Starting
Autopilot rudder

Bow thruster: Idle, 30 m to the berth

A thruster setpoint previewed and confirmed, advice ranges outside and inside the bar, a value dot, fill from zero, tint in a medium color, scales above and below, condensed and flat frames and a loading state, on a live docking feed.
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

A bar 200 pixels wide, filled to 40 on the default 0 to 100 scale.
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

PropTypeDefaultDescription
minValuenumber | undefined0Minimum scale value.
maxValuenumber | undefined100Maximum scale value.
widthnumber | undefined320Total width in pixels (including padding bands).
paddingLeftnumber | undefinedBAR_CANVAS_PADDINGPadding left of the drawing area.
paddingRightnumber | undefinedBAR_CANVAS_PADDINGPadding right of the drawing area.
sideExternalScaleSide | undefinedExternalScaleSide.bottomWhich side this scale lives on ('top' | 'bottom').
hasScaleboolean | undefinedtrueShow scale tickmarks.
showLabelsboolean | undefinedtrueShow numeric labels at primary tickmark intervals.
hasBarboolean | undefinedfalseShow bar.
scaleBackgroundboolean | undefinedfalseShow background behind the scale tickmarks.
barContainerStyleBarContainerStyle | undefined–Bar container background style. When undefined, defaults based on scaleBackground.
barThicknessnumber | undefined24Bar/fill thickness in pixels.
tickThicknessnumber | undefined24Tickmark band thickness in pixels.
labelThicknessnumber | undefined60Label band thickness in pixels.
mainTickmarksnumber[] | undefined[]Array of values for main tickmarks. When undefined, no main tickmarks are shown. When empty array [], defaults to [minValue, 0, maxValue].
primaryTickmarkIntervalnumber | undefined–Interval for primary (longest) tickmarks with labels.
secondaryTickmarkIntervalnumber | undefined–Interval for secondary (medium) tickmarks.
tertiaryTickmarkIntervalnumber | undefined–Interval for tertiary (shortest) tickmarks.
scaleTypeScaleType | undefinedScaleType.regularScale display mode: regular or condensed (shorter ticks).
frameStyleFrameStyle | undefinedFrameStyle.regularFrame style: regular (4px gap for all ticks) or flat (main tickmarks touch the edge).
borderRadiusPositionBorderRadiusPosition | undefined–Border radius position based on component layout.
borderRadiusnumber | 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.
priorityPriority | undefinedPriority.regularColor priority: enhanced uses blue instrument colors for bar fill and setpoint.
fillModeFillMode | undefinedFillMode.fillFill visualization mode: fill or tint.
barFillColorstring | undefined–Optional fill paint override; loading/off state still renders transparent.
fillMinnumber | undefined–Minimum fill value (defaults to 0).
fillMaxnumber | undefined–Maximum fill value (defaults to value).
valuenumber | undefined–Current value (bar fill level).
stateInstrumentState | undefinedInstrumentState.activeInstrument state (affects colors and some marker behavior).
advicePositionAdvicePosition | undefinedAdvicePosition.innerAdvice overlay positioning: center (in bar), inner (covers minor ticks), outer (no overlap).
advicesExternalScaleAdvice[] | undefined[]Advice/alert overlays. When undefined or empty, no advice shown.
highlightCurrentValueboolean | undefinedfalseDisplay a dot indicator at the current value position instead of relying on bar fill.
classNamestring | undefined–Class name added to the root element.
styleCSSProperties | undefined–Inline style added to the root element.
setpointnumber | undefined–Target setpoint value.
newSetpointnumber | undefined–Adjustment preview (2-step interface): while defined, the original setpoint marker dims and a focus-state marker renders at this value.
atSetpointboolean | undefined–Manual at-setpoint override; used when autoAtSetpoint is false.
touchingboolean | undefined–User is physically interacting with the control. Forces focus rendering and suppresses at-setpoint detection while true.
autoAtSetpointboolean | undefined–Enable automatic at-setpoint detection (default true).
autoAtSetpointDeadbandnumber | undefined–Tolerance for auto at-setpoint detection (default 2).
setpointAtZeroDeadbandnumber | undefined–Zero-snap tolerance for the marker's equalZero state (default 0.5).
setpointOverrideboolean | undefined–Derive the setpoint color from priority even when loading/off.
animateSetpointboolean | undefined–Enable the CSS-animated confirm transition.