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.
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
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
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
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
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
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
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
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
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
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | undefined | 0 | Current 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. |
| max | number | undefined | 100 | Capacity of the tank, in the same unit as value. A capacity that is not a positive number counts as 1. |
| tag | ReactNode | undefined | "" | Name of the tank, shown on its tag, for example "Fuel port". A plain string also names the tank for screen readers. |
| ariaLabel | string | undefined | – | Accessible name of the tank. Falls back to tag when that is a non-empty string, then to "Tank". |
| type | TankType | undefined | TankType.generic | Silhouette of the tank: generic, atmospheric (domed top), pressurized or battery. A battery always uses the enhanced palette. |
| orientation | TankOrientation | undefined | TankOrientation.vertical | Whether the tank stands upright or lies on its side. |
| positioning | TankPositioning | undefined | TankPositioning.button | button 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. |
| medium | AutomationMedium | undefined | AutomationMedium.freshWater | What the tank holds. Sets the fill color from the ISO 14726 medium colors. A battery ignores it. |
| chartMode | TankChartMode | undefined | TankChartMode.bar | What the chart area shows: the level bar, the trend graph, or the graph with a bar. |
| chartData | GaugeTrendPoint[] | 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. |
| compact | boolean | undefined | false | Draws the compact layout: the tank outline with its chart, and the trend arrow and percentage below it. |
| static | boolean | undefined | false | Marks 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. |
| clickable | boolean | undefined | true | Renders the cell as a button, so it takes focus and a tap. A static tank is never clickable. |
| activated | boolean | undefined | false | Draws the cell as selected, for the tank whose details are open. |
| onClick | (() => void) | undefined | – | Called when the crew taps the tank. |
| badges | ReactNode | undefined | – | Content for the badge row, for example a pump or valve state. |
| currentValue | ReactNode | undefined | – | Replaces the content figure in the default readout. Defaults to value rounded to a whole number. |
| maxValue | ReactNode | undefined | – | Replaces the capacity figure in the default readout. Defaults to max rounded to a whole number. |
| unit | ReactNode | 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. |
| rich | ReactNode | undefined | – | Extra content below the default readout, set off by a divider. |
| trend | TankTrend | undefined | TankTrend.stable | Direction in which the level is moving, shown as an arrow beside the percentage. |
| showTrendSymbol | boolean | undefined | true | Shows the trend arrow beside the percentage. |
| percentFractionDigits | number | undefined | 0 | Decimal places of the percentage, from 0 to 20. A value that is not a finite number gives 0. |
| priority | Priority | undefined | Priority.regular | Regular or enhanced palette. A battery is always enhanced. |
| advice | ExternalScaleAdvice[] | 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. |
| hasAdvice | boolean | undefined | false | Draws the advice ranges on the bar. |
| hasGraphIcon | boolean | undefined | false | Draws a tank glyph (a battery glyph for a battery) over the chart. |
| alert | boolean | undefined | false | Surrounds the cell with an alert frame. |
| alertFrameType | ObcAlertFrameType | undefined | ObcAlertFrameType.SmallSideFlip | Layout of the alert frame: where its label flap sits. |
| alertFrameThickness | ObcAlertFrameThickness | undefined | ObcAlertFrameThickness.Small | Border weight of the alert frame. |
| alertFrameStatus | AlertType | undefined | AlertType.Alarm | Severity of the alert, which sets the color of the frame. |
| alertFrameMode | ObcAlertFrameMode | undefined | ObcAlertFrameMode.ackedActive | Acknowledgement state of the alert: acked-active (default) is steady, unacked-active flashes,
unacked-rectified flashes a dashed frame. |
| showAlertCategoryIcon | boolean | undefined | true | Shows the alert category icon on the frame. |
| showAlertIcon | boolean | undefined | false | Shows alertIcon on the frame's label flap. |
| alertIcon | ReactNode | undefined | – | Icon on the alert frame's label flap. Drawn only when showAlertIcon is set. |
| alertLabel | ReactNode | undefined | – | Text on the alert frame's label flap. |
| alertTimer | ReactNode | undefined | – | Elapsed time on the alert frame's label flap. |
| 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 vertical: a single level on a scale.
- Gauge trend: the trend chart a tank draws in its graph modes.
- Alarms and severity: the severity colors an alert frame takes.
- Instruments: the other instruments.