Skip to content
design
Instruments

Gauge radial indicator

A 48 pixel radial cue for a value, on a 270 or 180 degree sweep.

On this page
Diesel
62 %, 124 L of 200
Fresh water
41 %, 246 L of 600
House bank
Charging 6.0 A
Holding tank
78 %, pump out before the marina
Engine load
55 % at cruising revs
Autopilot drive
30 % of the ram's force
The ship's-systems strip on a live feed: six cues on both sweeps and both frame treatments.
Code
import { GaugeRadialIndicator, GaugeRadialIndicatorStyle } from "@bosunkit/instruments";
import { BatteryCharging, Droplet, Fuel, Gauge, Toilet } from "lucide-react";
import {
  useEffect,
  useState,
  type CSSProperties,
  type Dispatch,
  type ReactElement,
  type ReactNode,
  type SetStateAction,
} from "react";

export const description: string =
  "The ship's-systems strip of a watch screen on a live feed: six cues on both sweeps and both frame treatments, " +
  "with icons, a scale that runs through zero for the battery current, and one cue without an icon.";

interface Cue {
  name: string;
  detail: string;
  value: number;
  minValue: number;
  maxValue: number;
  sector: 180 | 270;
  styleType: GaugeRadialIndicatorStyle;
  icon: ReactNode;
}

const ICON_SIZE: number = 16;

const STRIP: CSSProperties = {
  display: "grid",
  gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
  gap: 8,
  width: "100%",
  maxWidth: 640,
};

const TILE: CSSProperties = {
  display: "flex",
  alignItems: "center",
  gap: 10,
  padding: "6px 12px 6px 6px",
  border: "1px solid var(--border)",
  borderRadius: "var(--radius-md)",
  background: "var(--surface)",
};

const NAME: CSSProperties = { fontSize: 13, fontWeight: 600, color: "var(--text)" };
const DETAIL: CSSProperties = { fontSize: 12, color: "var(--text-dim)", fontVariantNumeric: "tabular-nums" };

/** A reading with a decimal point and no thousands grouping. */
function format(value: number, digits: number): string {
  return value.toFixed(digits);
}

export default function Showcase(): 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 fridge draw on the house bank while the alternator charges it, so the current swings
  // through zero; the engine load follows the throttle.
  const current: number = 6 + 22 * Math.sin(second / 5);
  const engineLoad: number = 55 + 20 * Math.sin(second / 9);
  const autopilot: number = 30 + 25 * Math.abs(Math.sin(second / 2));
  const fuel: number = 62 - second * 0.01;

  const cues: Cue[] = [
    {
      name: "Diesel",
      detail: `${format(fuel, 0)} %, ${format(fuel * 2, 0)} L of 200`,
      value: fuel,
      minValue: 0,
      maxValue: 100,
      sector: 270,
      styleType: GaugeRadialIndicatorStyle.Regular,
      icon: <Fuel size={ICON_SIZE} />,
    },
    {
      name: "Fresh water",
      detail: "41 %, 246 L of 600",
      value: 41,
      minValue: 0,
      maxValue: 100,
      sector: 270,
      styleType: GaugeRadialIndicatorStyle.Flat,
      icon: <Droplet size={ICON_SIZE} />,
    },
    {
      name: "House bank",
      detail: `${current >= 0 ? "Charging" : "Discharging"} ${format(Math.abs(current), 1)} A`,
      value: current,
      minValue: -50,
      maxValue: 50,
      sector: 270,
      styleType: GaugeRadialIndicatorStyle.Regular,
      icon: <BatteryCharging size={ICON_SIZE} />,
    },
    {
      name: "Holding tank",
      detail: "78 %, pump out before the marina",
      value: 78,
      minValue: 0,
      maxValue: 100,
      sector: 180,
      styleType: GaugeRadialIndicatorStyle.Regular,
      icon: <Toilet size={ICON_SIZE} />,
    },
    {
      name: "Engine load",
      detail: `${format(engineLoad, 0)} % at cruising revs`,
      value: engineLoad,
      minValue: 0,
      maxValue: 100,
      sector: 180,
      styleType: GaugeRadialIndicatorStyle.Flat,
      icon: <Gauge size={ICON_SIZE} />,
    },
    {
      name: "Autopilot drive",
      detail: `${format(autopilot, 0)} % of the ram's force`,
      value: autopilot,
      minValue: 0,
      maxValue: 100,
      sector: 180,
      styleType: GaugeRadialIndicatorStyle.Flat,
      icon: null,
    },
  ];

  return (
    <div style={STRIP}>
      {cues.map((cue: Cue): ReactElement => (
        <div key={cue.name} style={TILE}>
          <GaugeRadialIndicator
            value={cue.value}
            minValue={cue.minValue}
            maxValue={cue.maxValue}
            sector={cue.sector}
            styleType={cue.styleType}
            icon={cue.icon}
            hasIcon={cue.icon !== null}
            style={{ flexShrink: 0 }}
          />
          <div>
            <div style={NAME}>{cue.name}</div>
            <div style={DETAIL}>{cue.detail}</div>
          </div>
        </div>
      ))}
    </div>
  );
}

The radial indicator is the small form of the radial gauge: a fixed 48 by 48 pixel cue for places too small for a full gauge. A needle turns across the sweep, and an optional icon sits at the bottom. sector selects the 270 or 180 degree sweep and styleType the framed or open treatment. It ports obc-gauge-radial-indicator; the overview lists the other instruments.

Examples

Radial

The 48 pixel cue at a third of its range, on the 270 degree sweep.
Code
import { GaugeRadialIndicator, GaugeRadialIndicatorStyle } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "The 48 pixel cue at a third of its range, on the 270 degree sweep.";

export default function Radial(): ReactElement {
  return (
    <GaugeRadialIndicator
      value={33}
      minValue={0}
      maxValue={100}
      sector={270}
      styleType={GaugeRadialIndicatorStyle.Regular}
    />
  );
}

Small cue, explicit context

Use the small dial beside a label and an exact reading. Its optional icon identifies the subject but does not supply a unit, range or accessible description of the value. Keep the domain consistent across repeated cues.

The compact sweep is appropriate for a quick relative impression. Use GaugeRadial when the reader needs ticks, advice bands or a target, and keep source quality visible outside either presentation.

Props

PropTypeDefaultDescription
valuenumber | undefined33Value to show, in the unit of minValue and maxValue. Clamped to the range. Default 33.
minValuenumber | undefined0Value at the start of the sweep. Default 0.
maxValuenumber | undefined100Value at the end of the sweep. Default 100.
sectornumber | undefined270Sweep extent; anything other than 180 normalizes to 270 (as upstream).
styleTypeGaugeRadialIndicatorStyle | undefinedGaugeRadialIndicatorStyle.RegularFrame treatment: regular (default) draws the framed bowl, flat the open one.
iconReactNode | undefined–Glyph rendered at the bottom anchor when hasIcon is set.
hasIconboolean | undefinedfalseShows icon at the bottom of the indicator. Default false.
classNamestring | undefined–Class names added to the instrument's root element.
styleCSSProperties | undefined–Inline style for the indicator's root element.