Skip to content
design
Instruments

Automation tank

A tank cell for the automation display, with its level, trend, capacity and an optional alert frame.

On this page

Diesel: 152.0 L, about 242 nmi at 6.5 kn before the 33 L reserve.

Every silhouette, both orientations, the three chart modes, advice ranges, a setpoint, badges, rich text, selection and an alert frame, on a live feed of Althea's tanks.
Code
import { Badge } from "@bosunkit/components/badge";
import {
  AdviceType,
  AlertType,
  AutomationMedium,
  AutomationTank,
  ObcAlertFrameThickness,
  ObcAlertFrameType,
  Priority,
  TankChartMode,
  TankOrientation,
  TankPositioning,
  TankTrend,
  TankType,
} from "@bosunkit/instruments";
import type { ExternalScaleAdvice, GaugeTrendPoint } from "@bosunkit/instruments";
import { Droplets, Flame, Fuel, TriangleAlert, Waves } from "lucide-react";
import { useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, SetStateAction } from "react";

export const description: string =
  "Althea's tank room on a live feed: every silhouette, both orientations, the three chart modes, advice ranges, a setpoint, badges, rich text, a selected tank and two alert frames.";

/** One sample every two seconds; the feed loops after this many. */
const CYCLE: number = 120;
const TREND_SAMPLES: number = 30;

const DIESEL_CAPACITY_LITERS: number = 220;
const DIESEL_BURN_LITERS_PER_HOUR: number = 3.2;
const CRUISING_SPEED_KNOTS: number = 6.5;
const DIESEL_RESERVE_LITERS: number = 33;

function dieselLiters(step: number): number {
  return 152 - 0.35 * step + 1.2 * Math.sin(step * 0.9);
}

function freshWaterLiters(step: number): number {
  return 248 + 0.9 * step;
}

function blackWaterLiters(step: number): number {
  return 64 + 0.1 * step;
}

function houseBatteryPercent(step: number): number {
  return 74 - 0.05 * step;
}

/** `TREND_SAMPLES` readings of a tank up to `step`, four feed steps apart, oldest first. */
function trendOf(level: (sample: number) => number, step: number): GaugeTrendPoint[] {
  return Array.from({ length: TREND_SAMPLES }, (_unused: unknown, index: number): GaugeTrendPoint => {
    const t: number = step - (TREND_SAMPLES - 1 - index) * 4;
    return { t, value: level(t) };
  });
}

const DIESEL_ADVICE: ExternalScaleAdvice[] = [
  { min: 0, max: DIESEL_RESERVE_LITERS, type: AdviceType.caution, hinted: true },
];

const FRESH_WATER_ADVICE: ExternalScaleAdvice[] = [
  { min: 0, max: 60, type: AdviceType.caution, hinted: true },
  { min: 360, max: 400, type: AdviceType.advice, hinted: false },
];

const RICH_STYLE: CSSProperties = { fontSize: 12, lineHeight: 1.3, color: "var(--element-neutral-color)" };

type TankId = "diesel" | "fresh-water" | "black-water" | "house" | "calorifier" | "engine-oil";

/** What the panel says about the tank the crew tapped; the diesel line takes its figures from the feed. */
const TANK_NOTES: Record<Exclude<TankId, "diesel">, string> = {
  "fresh-water": "Fresh water: the watermaker fills toward the 380 L setpoint at 27 L/h.",
  "black-water": "Black water: pump out at the next harbor, or discharge beyond 3 nmi offshore where it is allowed.",
  house:
    "House battery: discharging at 6 A with the fridge and instruments on; the alternator charges it under engine.",
  calorifier: "Calorifier: 40 L of hot water at 62 °C, heated by the engine loop.",
  "engine-oil": "Engine oil sump: 7.4 L of 9 L at the morning engine round, above the 6 L minimum mark.",
};

export default function Showcase(): ReactElement {
  const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
  const [selected, setSelected]: [TankId, Dispatch<SetStateAction<TankId>>] = useState<TankId>("diesel");

  useEffect((): (() => void) => {
    const timer: number = window.setInterval((): void => {
      setStep((previous: number): number => (previous + 1) % CYCLE);
    }, 2000);
    return (): void => {
      window.clearInterval(timer);
    };
  }, []);

  const diesel: number = dieselLiters(step);
  const rangeNauticalMiles: number =
    ((diesel - DIESEL_RESERVE_LITERS) / DIESEL_BURN_LITERS_PER_HOUR) * CRUISING_SPEED_KNOTS;
  const freshWater: number = freshWaterLiters(step);
  const blackWater: number = blackWaterLiters(step);
  const house: number = houseBatteryPercent(step);

  function select(id: TankId): () => void {
    return (): void => {
      setSelected(id);
    };
  }

  return (
    <div style={{ width: "100%", maxWidth: 620, display: "grid", gap: 12, justifyItems: "center" }}>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 12, justifyContent: "center" }}>
        <div style={{ width: 192, height: 282 }}>
          <AutomationTank
            ariaLabel="Diesel tank"
            tag="Diesel"
            value={diesel}
            max={DIESEL_CAPACITY_LITERS}
            unit="L"
            currentValue={diesel.toFixed(1)}
            type={TankType.atmospheric}
            medium={AutomationMedium.fuel}
            chartMode={TankChartMode.graphAndBar}
            chartData={trendOf(dieselLiters, step)}
            trend={TankTrend.falling}
            percentFractionDigits={1}
            advice={DIESEL_ADVICE}
            hasAdvice
            badges={
              <Badge variant="dim">
                <Fuel size={12} aria-hidden />
                Feed
              </Badge>
            }
            rich={<span style={RICH_STYLE}>{rangeNauticalMiles.toFixed(0)} nmi to reserve</span>}
            activated={selected === "diesel"}
            onClick={select("diesel")}
          />
        </div>
        <div style={{ width: 192, height: 282 }}>
          <AutomationTank
            ariaLabel="Fresh water tank"
            tag="Fresh water"
            value={freshWater}
            max={400}
            unit="L"
            medium={AutomationMedium.freshWater}
            trend={TankTrend.rising}
            setpoint={380}
            advice={FRESH_WATER_ADVICE}
            hasAdvice
            badges={
              <Badge variant="dim">
                <Droplets size={12} aria-hidden />
                Watermaker
              </Badge>
            }
            rich={<span style={RICH_STYLE}>Full by 16:40</span>}
            activated={selected === "fresh-water"}
            onClick={select("fresh-water")}
          />
        </div>
        <div style={{ width: 192, height: 314 }}>
          <AutomationTank
            ariaLabel="Black water tank"
            tag="Black water"
            value={blackWater}
            max={80}
            unit="L"
            medium={AutomationMedium.waste}
            chartMode={TankChartMode.graph}
            chartData={trendOf(blackWaterLiters, step)}
            trend={TankTrend.fastRising}
            alert
            alertFrameType={ObcAlertFrameType.BottomFlip}
            alertFrameThickness={ObcAlertFrameThickness.Large}
            alertFrameStatus={AlertType.Warning}
            showAlertIcon
            alertIcon={<TriangleAlert size={16} aria-hidden />}
            alertLabel="Pump out soon"
            alertTimer="14m"
            activated={selected === "black-water"}
            onClick={select("black-water")}
          />
        </div>
      </div>
      <div style={{ display: "flex", flexWrap: "wrap", gap: 12, justifyContent: "center", alignItems: "flex-start" }}>
        <div style={{ width: 108, height: 179 }}>
          <AutomationTank
            ariaLabel="House battery"
            tag="House"
            value={house}
            max={100}
            type={TankType.battery}
            trend={TankTrend.falling}
            hasGraphIcon
            compact
            activated={selected === "house"}
            onClick={select("house")}
          />
        </div>
        <div style={{ width: 108, height: 179 }}>
          <AutomationTank
            ariaLabel="Calorifier"
            tag="Calorifier"
            value={40}
            max={45}
            type={TankType.pressurized}
            medium={AutomationMedium.hot}
            priority={Priority.enhanced}
            trend={TankTrend.stable}
            badges={<Flame size={14} aria-hidden />}
            compact
            activated={selected === "calorifier"}
            onClick={select("calorifier")}
          />
        </div>
        <div style={{ width: 108, height: 179 }}>
          <AutomationTank
            ariaLabel="Grey water tank, level unknown"
            tag="Grey water"
            max={80}
            maxValue="80"
            unit="L"
            medium={AutomationMedium.waste}
            static
          />
        </div>
        {/* A point-positioned tank draws centered on its anchor point, so its own box sits half a tank to the
            right of what it draws. The clip keeps that empty half from widening the row on a narrow screen. */}
        <div style={{ position: "relative", width: 244, height: 236, overflow: "clip", overflowClipMargin: 8 }}>
          <AutomationTank
            style={{ position: "absolute", left: 122, top: 20 }}
            ariaLabel="Engine oil sump"
            tag="Engine oil"
            value={7.4}
            max={9}
            orientation={TankOrientation.horizontal}
            positioning={TankPositioning.point}
            medium={AutomationMedium.oil}
            trend={TankTrend.closed}
            showTrendSymbol
            badges={<Waves size={14} aria-hidden />}
            compact
            activated={selected === "engine-oil"}
            onClick={select("engine-oil")}
          />
        </div>
      </div>
      <p style={{ margin: 0, fontSize: 13, color: "var(--element-neutral-color)", textAlign: "center" }}>
        {selected === "diesel"
          ? `Diesel: ${diesel.toFixed(1)} L, about ${rangeNauticalMiles.toFixed(0)} nmi at 6.5 kn before the 33 L reserve.`
          : TANK_NOTES[selected]}
      </p>
    </div>
  );
}

An automation tank shows how full a tank is, or how charged a battery is. It draws one of four silhouettes (generic, atmospheric, pressurized or battery) in the ISO color of the medium it holds, with the level as a bar, a trend graph or both, and a readout of the percentage and the capacity. compact keeps the outline and the percentage; static marks a tank that is present but whose level is unknown, and shows its capacity only. In Night the media whose ISO color is green, cyan or blue (fresh and sea water, steam, non-flammable gas, waste, cold) take amber and grey shades of their own, each at least 10 ΔE*ab from every medium with a different ISO color, and the tank's label names the medium.

The cell is a button that calls onClick; set clickable to false for a tank that only reports. alert wraps the cell in an alert frame with a label and a timer; alertFrameMode carries the acknowledgement state, so an unacknowledged tank's frame flashes on the shared tempo clock and a rectified one flashes dashed.

Examples

Atmospheric fuel

Fuel
68%
68/
100m3
An atmospheric tank at 68 percent, with the domed top and the fuel color.
Code
import { AutomationMedium, AutomationTank, TankType } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "An atmospheric tank at 68 percent, with the domed top and the fuel color.";

export default function AtmosphericFuel(): ReactElement {
  return (
    <div style={{ width: 256, height: 376 }}>
      <AutomationTank
        ariaLabel="Atmospheric fuel tank"
        tag="Fuel"
        value={68}
        max={100}
        type={TankType.atmospheric}
        medium={AutomationMedium.fuel}
        clickable={false}
      />
    </div>
  );
}

Compact fresh water

54%
Fresh water
The compact layout keeps the outline and the percentage, and drops the readout.
Code
import { AutomationMedium, AutomationTank, TankType } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "The compact layout keeps the outline and the percentage, and drops the readout.";

export default function CompactFreshWater(): ReactElement {
  return (
    <div style={{ width: 170, height: 282 }}>
      <AutomationTank
        ariaLabel="Compact fresh water tank"
        tag="Fresh water"
        value={54}
        max={100}
        type={TankType.atmospheric}
        medium={AutomationMedium.freshWater}
        compact
        clickable={false}
      />
    </div>
  );
}

Pressurized waste

Waste
37%
37/
100m3
A pressurized waste tank at 37 percent, in the waste medium color.
Code
import { AutomationMedium, AutomationTank, TankType } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A pressurized waste tank at 37 percent, in the waste medium color.";

export default function PressurizedWaste(): ReactElement {
  return (
    <div style={{ width: 256, height: 376 }}>
      <AutomationTank
        ariaLabel="Pressurized waste tank"
        tag="Waste"
        value={37}
        max={100}
        type={TankType.pressurized}
        medium={AutomationMedium.waste}
        clickable={false}
      />
    </div>
  );
}

Horizontal oil

Oil
81%
81/
100m3
A tank on its side: horizontal orientation draws it wide instead of tall.
Code
import { AutomationMedium, AutomationTank, TankOrientation } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A tank on its side: horizontal orientation draws it wide instead of tall.";

export default function HorizontalOil(): ReactElement {
  return (
    <div style={{ width: 420, height: 256 }}>
      <AutomationTank
        ariaLabel="Horizontal oil tank"
        tag="Oil"
        value={81}
        max={100}
        orientation={TankOrientation.horizontal}
        medium={AutomationMedium.oil}
        clickable={false}
      />
    </div>
  );
}

Battery charge

74%
House
A battery reads as a state of charge, in the enhanced palette.
Code
import { AutomationTank, TankType } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A battery reads as a state of charge, in the enhanced palette.";

export default function BatteryCharge(): ReactElement {
  return (
    <div style={{ width: 170, height: 282 }}>
      <AutomationTank
        ariaLabel="House battery state of charge"
        tag="House"
        value={74}
        max={100}
        type={TankType.battery}
        compact
        clickable={false}
      />
    </div>
  );
}

Unknown level

A static tank is present but its level is unknown, so it shows its capacity.
Code
import { AutomationMedium, AutomationTank } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A static tank is present but its level is unknown, so it shows its capacity.";

export default function UnknownLevel(): ReactElement {
  return (
    <div style={{ width: 170, height: 282 }}>
      <AutomationTank
        ariaLabel="Unknown tank level"
        tag="Unknown"
        max={180}
        maxValue="180"
        unit="L"
        medium={AutomationMedium.masses}
        static
      />
    </div>
  );
}

Graph and bar

Trend
50%
50/
100m3
The graph and bar mode draws the `chartData` trend with the level bar.
Code
import { AutomationMedium, AutomationTank, TankChartMode } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "The graph and bar mode draws the `chartData` trend with the level bar.";

const TREND_SERIES: { t: number; value: number }[] = [
  45, 52, 48, 55, 62, 58, 52, 40, 32, 37, 55, 68, 52, 48, 42, 38, 35, 32, 28, 30, 35, 40, 45, 50, 48, 48, 38, 35, 40,
  45, 50,
].map((value: number, index: number): { t: number; value: number } => ({ t: index, value }));

export default function GraphAndBar(): ReactElement {
  return (
    <div style={{ width: 256, height: 376 }}>
      <AutomationTank
        ariaLabel="Tank graph and bar"
        tag="Trend"
        value={50}
        max={100}
        chartMode={TankChartMode.graphAndBar}
        chartData={TREND_SERIES}
        medium={AutomationMedium.seaWater}
        clickable={false}
      />
    </div>
  );
}

In alert

92%
Alert
High level
2m
An alarm frame with a label and a timer around a compact tank at 92 percent.
Code
import { AlertType, AutomationMedium, AutomationTank, ObcAlertFrameType } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "An alarm frame with a label and a timer around a compact tank at 92 percent.";

export default function InAlert(): ReactElement {
  return (
    <div style={{ width: 170, height: 314 }}>
      <AutomationTank
        ariaLabel="Tank alert frame example"
        tag="Alert"
        value={92}
        max={100}
        medium={AutomationMedium.fuel}
        compact
        clickable={false}
        alert
        alertFrameType={ObcAlertFrameType.BottomFlip}
        alertFrameStatus={AlertType.Alarm}
        alertLabel="High level"
        alertTimer="2m"
      />
    </div>
  );
}

Capacity and availability

value and max share the same unit; the percentage derives from their ratio. A sensor's raw level percentage is not necessarily a volume percentage in an irregular tank. Apply calibration in the data layer before making claims about contents or remaining capacity.

Name the tank and medium in text, even when the silhouette and color identify them visually. Use the static presentation for a known tank with unknown level; do not turn a missing sensor into an empty tank. Set clickable={false} when no detail action exists. Alert frames reflect supplied state and do not detect leaks, overfill or battery faults.

Props

PropTypeDefaultDescription
valuenumber | undefined0Current content of the tank, in the tank's capacity unit (cubic meters by default). The bar fills to value / max; a value that is not a finite number draws as 0.
maxnumber | undefined100Capacity of the tank, in the same unit as value. A capacity that is not a positive number counts as 1.
tagReactNode | undefined""Name of the tank, shown on its tag, for example "Fuel port". A plain string also names the tank for screen readers.
ariaLabelstring | undefined–Accessible name of the tank. Falls back to tag when that is a non-empty string, then to "Tank".
typeTankType | undefinedTankType.genericSilhouette of the tank: generic, atmospheric (domed top), pressurized or battery. A battery always uses the enhanced palette.
orientationTankOrientation | undefinedTankOrientation.verticalWhether the tank stands upright or lies on its side.
positioningTankPositioning | undefinedTankPositioning.buttonbutton fills the parent at a fixed aspect ratio. point draws the cell at its own fixed size, anchored at its top center, for placing on a diagram.
mediumAutomationMedium | undefinedAutomationMedium.freshWaterWhat the tank holds. Sets the fill color from the ISO 14726 medium colors. A battery ignores it.
chartModeTankChartMode | undefinedTankChartMode.barWhat the chart area shows: the level bar, the trend graph, or the graph with a bar.
chartDataGaugeTrendPoint[] | undefined[]Trend samples for the graph modes, oldest first. Each sample has a time t (any increasing number) and a value in the tank's capacity unit.
compactboolean | undefinedfalseDraws the compact layout: the tank outline with its chart, and the trend arrow and percentage below it.
staticboolean | undefinedfalseMarks a tank that is present but whose level is unknown. It hides the chart and the percentage and shows the capacity only. It wins over compact and clickable.
clickableboolean | undefinedtrueRenders the cell as a button, so it takes focus and a tap. A static tank is never clickable.
activatedboolean | undefinedfalseDraws the cell as selected, for the tank whose details are open.
onClick(() => void) | undefined–Called when the crew taps the tank.
badgesReactNode | undefined–Content for the badge row, for example a pump or valve state.
currentValueReactNode | undefined–Replaces the content figure in the default readout. Defaults to value rounded to a whole number.
maxValueReactNode | undefined–Replaces the capacity figure in the default readout. Defaults to max rounded to a whole number.
unitReactNode | undefined( <> {/* eslint-disable-line @bosunkit/no-literal-ui-string -- the unit m³, the same in every language */}m<sup>3</sup> </> )Unit shown after the capacity figure. Defaults to cubic meters.
richReactNode | undefined–Extra content below the default readout, set off by a divider.
trendTankTrend | undefinedTankTrend.stableDirection in which the level is moving, shown as an arrow beside the percentage.
showTrendSymbolboolean | undefinedtrueShows the trend arrow beside the percentage.
percentFractionDigitsnumber | undefined0Decimal places of the percentage, from 0 to 20. A value that is not a finite number gives 0.
priorityPriority | undefinedPriority.regularRegular or enhanced palette. A battery is always enhanced.
adviceExternalScaleAdvice[] | undefined[]Level ranges to mark on the bar, each with a minimum and a maximum in the tank's capacity unit. Drawn only when hasAdvice is set.
hasAdviceboolean | undefinedfalseDraws the advice ranges on the bar.
hasGraphIconboolean | undefinedfalseDraws a tank glyph (a battery glyph for a battery) over the chart.
alertboolean | undefinedfalseSurrounds the cell with an alert frame.
alertFrameTypeObcAlertFrameType | undefinedObcAlertFrameType.SmallSideFlipLayout of the alert frame: where its label flap sits.
alertFrameThicknessObcAlertFrameThickness | undefinedObcAlertFrameThickness.SmallBorder weight of the alert frame.
alertFrameStatusAlertType | undefinedAlertType.AlarmSeverity of the alert, which sets the color of the frame.
alertFrameModeObcAlertFrameMode | undefinedObcAlertFrameMode.ackedActiveAcknowledgement state of the alert: acked-active (default) is steady, unacked-active flashes, unacked-rectified flashes a dashed frame.
showAlertCategoryIconboolean | undefinedtrueShows the alert category icon on the frame.
showAlertIconboolean | undefinedfalseShows alertIcon on the frame's label flap.
alertIconReactNode | undefined–Icon on the alert frame's label flap. Drawn only when showAlertIcon is set.
alertLabelReactNode | undefined–Text on the alert frame's label flap.
alertTimerReactNode | undefined–Elapsed time on the alert frame's label flap.
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.