diff --git a/frontend/src/components/Dashboard/BarWidget.tsx b/frontend/src/components/Dashboard/BarWidget.tsx index be8c23f..ea126bf 100644 --- a/frontend/src/components/Dashboard/BarWidget.tsx +++ b/frontend/src/components/Dashboard/BarWidget.tsx @@ -1,6 +1,97 @@ +import { useLiveValue } from "@/components/Flow/liveStore" +import { cn } from "@/lib/utils" import type { WidgetProps } from "./widgets" -/** A bar — a stub until the bar widget is written. */ -export function BarWidget(_props: WidgetProps) { - return

+// Local copies: `widgets.tsx` imports this module, so its helpers cannot be +// imported back without closing the cycle (`ChartWidget.tsx` does the same). +const config = (widget: WidgetProps["widget"]) => + (widget.config ?? {}) as Record + +const text = (value: unknown) => (value == null ? "" : String(value)) + +const num = (value: unknown, fallback: number) => { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : fallback +} + +/** Below this the reading no longer fits on the fill and moves off its end. */ +const FITS = 0.3 + +/** + * A level, drawn as a horizontal bar with its reading written on it. + * + * A second message can be nested inside the first — the PV share of an + * inverter's input — and is drawn on top of the fill on the same scale, so + * containment is what the picture shows rather than something to work out. + */ +export function BarWidget({ widget }: WidgetProps) { + const cfg = config(widget) + const message = text(cfg.message) + const nested = text(cfg.inner) + const outer = useLiveValue(message || undefined) + const inner = useLiveValue(nested || undefined) + if (!message) + return

Pick a message.

+ + const min = num(cfg.min, 0) + const max = num(cfg.max, 100) + const span = max - min || 1 + const precision = cfg.precision === undefined ? 1 : num(cfg.precision, 1) + const unit = cfg.unit ? text(cfg.unit) : "" + + const reading = (live: unknown) => (typeof live === "number" ? live : null) + const fractionOf = (value: number | null) => + value === null ? 0 : Math.min(1, Math.max(0, (value - min) / span)) + const write = (value: number | null) => + value === null ? "—" : `${value.toFixed(precision)}${unit}` + + const value = reading(outer?.value) + const innerValue = nested ? reading(inner?.value) : null + const fraction = fractionOf(value) + const fits = fraction >= FITS + + return ( +
+
+
+ {innerValue === null ? null : ( +
+ )} + + {write(value)} + +
+ {innerValue === null ? null : ( +

+ {nested} {write(innerValue)} +

+ )} +
+ ) }