Skip to content
design
Instruments

Bar vertical

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

On this page
84
Coolant
°C
405060708090100110
-18
House current
A
0204060-20-40-60
62
Throttle
%
020406080100
4.1
Fuel flow
L/h
02468
Off
Watermaker
bar

Throttle: Cruising, 2,200 rpm

Advice ranges beside the scale and inside the bar, a setpoint the crew adjusts, fill from zero and tint, both scale sides, condensed and flat frames, a value dot and an off state, on a live engine feed.
Code
import {
  AdvicePosition,
  AdviceType,
  BarContainerStyle,
  BarVertical,
  BorderRadiusPosition,
  ExternalScaleSide,
  FillMode,
  FrameStyle,
  InstrumentState,
  Priority,
  Readout,
  ScaleType,
} from "@bosunkit/instruments";
import type { ExternalScaleAdvice, ReadoutProps } from "@bosunkit/instruments";
import { useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, ReactNode, SetStateAction } from "react";

export const description: string =
  "Althea's engine bars on a one-second feed: the coolant's normal range and a hinted caution range beside its scale, " +
  "the house current filled from zero on a condensed scale at the left, the throttle's setpoint the crew adjusts " +
  "and its caution range inside the bar, the fuel flow as a tinted band with a dot at the value on a flat frame, and " +
  "the watermaker off with its pressure setpoint kept.";

/** Feed steps before the throttle sequence repeats. */
const CYCLE: number = 24;
const BAR_HEIGHT: number = 240;

const COOLANT_ADVICE: ExternalScaleAdvice[] = [
  { min: 75, max: 90, type: AdviceType.advice, hinted: false },
  { min: 95, max: 110, type: AdviceType.caution, hinted: true },
];

const THROTTLE_ADVICE: ExternalScaleAdvice[] = [{ min: 85, max: 100, type: AdviceType.caution, hinted: false }];

const CELL_STYLE: CSSProperties = { display: "grid", justifyItems: "center", alignContent: "start", gap: 6 };
const NAME_STYLE: CSSProperties = { fontSize: 12, color: "var(--element-neutral-color)" };

interface Throttle {
  setpoint: number;
  newSetpoint: number | undefined;
  touching: boolean;
  note: string;
}

/**
 * The helmsman's throttle sequence: cruising at 62 percent, takes the lever, previews 90 percent, confirms it into the
 * caution range to motor off a lee shore, then eases back.
 */
function throttleAt(step: number): Throttle {
  if (step < 6) {
    return { setpoint: 62, newSetpoint: undefined, touching: false, note: "Cruising, 2,200 rpm" };
  }
  if (step < 9) {
    return { setpoint: 62, newSetpoint: 90, touching: true, note: "Lever moved, waiting to confirm" };
  }
  if (step < 18) {
    return { setpoint: 90, newSetpoint: undefined, touching: false, note: "Above 85 % for 10 min at most" };
  }
  return { setpoint: 62, newSetpoint: undefined, touching: false, note: "Eased back to cruising" };
}

function Cell({ readout, children }: { readout: ReadoutProps; children: ReactNode }): ReactElement {
  return (
    <div style={CELL_STYLE}>
      <Readout {...readout} />
      {children}
    </div>
  );
}

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 % CYCLE;
  const throttle: Throttle = throttleAt(phase);
  const throttleLevel: number = phase >= 9 && phase < 18 ? Math.min(90, 62 + (phase - 8) * 6) : 62;
  const coolant: number = 84 + 3 * Math.sin(step / 3) + (throttle.setpoint > 85 ? (phase - 9) * 0.9 : 0);
  const batteryCurrent: number = Math.round(-18 + 46 * Math.max(0, Math.sin(step / 5)));
  const fuelFlow: number = 1.2 + (throttleLevel / 100) * 4.6;

  return (
    <div style={{ display: "grid", gap: 10, width: "100%", maxWidth: 620 }}>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 14, justifyContent: "center" }}>
        <Cell readout={{ label: "Coolant", value: coolant, unit: "°C", maxDigits: 3 }}>
          <BarVertical
            height={BAR_HEIGHT}
            minValue={40}
            maxValue={110}
            value={coolant}
            hasBar
            primaryTickmarkInterval={10}
            secondaryTickmarkInterval={5}
            advices={COOLANT_ADVICE}
            advicePosition={AdvicePosition.outer}
            priority={Priority.enhanced}
          />
        </Cell>
        <Cell readout={{ label: "House current", value: batteryCurrent, unit: "A", maxDigits: 2 }}>
          <BarVertical
            height={BAR_HEIGHT}
            minValue={-60}
            maxValue={60}
            value={batteryCurrent}
            fillMin={0}
            fillMax={batteryCurrent}
            hasBar
            side={ExternalScaleSide.left}
            mainTickmarks={[-60, 0, 60]}
            primaryTickmarkInterval={20}
            secondaryTickmarkInterval={10}
            scaleType={ScaleType.condensed}
            scaleBackground
            barContainerStyle={BarContainerStyle.secondary}
          />
        </Cell>
        <Cell readout={{ label: "Throttle", value: throttleLevel, unit: "%", maxDigits: 3 }}>
          <BarVertical
            height={BAR_HEIGHT}
            minValue={0}
            maxValue={100}
            value={throttleLevel}
            hasBar
            mainTickmarks={[0, 100]}
            barThickness={32}
            primaryTickmarkInterval={20}
            secondaryTickmarkInterval={10}
            tertiaryTickmarkInterval={5}
            setpoint={throttle.setpoint}
            newSetpoint={throttle.newSetpoint}
            touching={throttle.touching}
            autoAtSetpointDeadband={3}
            animateSetpoint
            advices={THROTTLE_ADVICE}
            advicePosition={AdvicePosition.center}
            priority={Priority.enhanced}
          />
        </Cell>
        <Cell readout={{ label: "Fuel flow", value: fuelFlow, unit: "L/h", fractionDigits: 1 }}>
          <BarVertical
            height={BAR_HEIGHT}
            minValue={0}
            maxValue={8}
            value={fuelFlow}
            hasBar
            fillMode={FillMode.tint}
            fillMin={2.6}
            fillMax={3.8}
            mainTickmarks={[0, 8]}
            barFillColor="var(--automation-medium-fuel)"
            highlightCurrentValue
            primaryTickmarkInterval={2}
            secondaryTickmarkInterval={1}
            frameStyle={FrameStyle.flat}
            borderRadiusPosition={BorderRadiusPosition.outerLastChild}
            borderRadius={4}
            labelThickness={36}
          />
        </Cell>
        <Cell readout={{ label: "Watermaker", value: 0, unit: "bar", off: true, offText: "Off" }}>
          <BarVertical
            height={BAR_HEIGHT}
            minValue={0}
            maxValue={70}
            value={0}
            hasBar
            state={InstrumentState.off}
            mainTickmarks={[0, 70]}
            setpoint={55}
            primaryTickmarkInterval={10}
            showLabels={false}
            tickThickness={16}
          />
        </Cell>
      </div>
      <p style={{ margin: 0, textAlign: "center", ...NAME_STYLE }}>Throttle: {throttle.note}</p>
    </div>
  );
}

A vertical bar draws a value as a bar and sets a scale of tickmarks and labels beside it. height is the length in pixels, and the width follows from the bands that are on: the bar, the tickmarks and the labels. side puts the scale on the left or right of the bar. The bar fills from fillMin to the value, or tints a fixed range. It takes advice ranges and a setpoint marker.

The vertical gauge is this bar with a fixed layout, a bar and a scale background that are always on.

Examples

Bar

0255075100
A vertical bar filled to 68 on a 0 to 100 scale, with a label every 25.
Code
import { BarVertical } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A vertical bar filled to 68 on a 0 to 100 scale, with a label every 25.";

export default function Bar(): ReactElement {
  return (
    <BarVertical hasBar value={68} minValue={0} maxValue={100} primaryTickmarkInterval={25} showLabels />
  );
}

Align columns and their domains

Choose a common height, scale side and numeric domain when vertical bars form a comparison. Leave room for labels outside the bar; a parent that clips overflow can hide the very limits needed to read it.

A fill from a baseline, a tinted band and a setpoint marker have distinct meanings. State the quantity and unit and keep the exact reading available. Adapt missing or invalid samples before they reach the primitive rather than displaying them as an empty tank or zero load.

Props

PropTypeDefaultDescription
minValuenumber | undefined0Minimum scale value.
maxValuenumber | undefined100Maximum scale value.
heightnumber | undefined320Total height in pixels (including padding bands).
paddingTopnumber | undefinedBAR_CANVAS_PADDINGPadding above the drawing area.
paddingBottomnumber | undefinedBAR_CANVAS_PADDINGPadding below the drawing area.
sideExternalScaleSide | undefinedExternalScaleSide.rightWhich side this scale lives on ('left' | 'right').
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.