import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import { getSignalStore, getMetaStore } from '../lib/stores'; import type { Widget, SignalValue, SignalMeta } from '../lib/types'; const DEFAULT_VALUE: SignalValue = { value: null, quality: 'unknown', ts: null }; function qualityColor(q: string): string { switch (q) { case 'good': return '#4ade80'; case 'uncertain': return '#fbbf24'; case 'bad': return '#f87171'; default: return '#6b7280'; } } // Arc: -135deg to +135deg (270deg total) const START_DEG = -135; const END_DEG = 135; const TOTAL_DEG = 270; const cx = 50, cy = 54, r = 38; function degToRad(deg: number) { return (deg * Math.PI) / 180; } function polarToXY(deg: number, radius: number) { const rad = degToRad(deg); return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) }; } function arcPath(startDeg: number, endDeg: number, radius: number): string { const s = polarToXY(startDeg, radius); const e = polarToXY(endDeg, radius); const largeArc = endDeg - startDeg > 180 ? 1 : 0; return `M ${s.x} ${s.y} A ${radius} ${radius} 0 ${largeArc} 1 ${e.x} ${e.y}`; } function valueToDeg(v: number, minVal: number, maxVal: number): number { const clamped = Math.max(minVal, Math.min(maxVal, v)); const frac = maxVal === minVal ? 0 : (clamped - minVal) / (maxVal - minVal); return START_DEG + frac * TOTAL_DEG; } interface Props { widget: Widget; onContextMenu?: (e: MouseEvent) => void; } export default function Gauge({ widget, onContextMenu }: Props) { const sigRef = widget.signals[0]; const [sv, setSv] = useState(DEFAULT_VALUE); const [meta, setMeta] = useState(null); useEffect(() => { if (!sigRef) return; const unsubV = getSignalStore(sigRef).subscribe(setSv); const unsubM = getMetaStore(sigRef).subscribe(setMeta); return () => { unsubV(); unsubM(); }; }, [sigRef?.ds, sigRef?.name]); const label = widget.options['label'] || sigRef?.name || ''; const rawUnit = widget.options['unit']; const unit = rawUnit === 'none' ? '' : (rawUnit || meta?.unit || ''); const minVal = parseFloat(widget.options['min'] ?? String(meta?.displayLow ?? 0)); const maxVal = parseFloat(widget.options['max'] ?? String(meta?.displayHigh ?? 100)); const thresholdLow = widget.options['thresholdLow'] ? parseFloat(widget.options['thresholdLow']) : null; const thresholdHigh = widget.options['thresholdHigh'] ? parseFloat(widget.options['thresholdHigh']) : null; const quality = sv.quality; const rawV = sv.value; const rawValue: number | null = rawV === null || rawV === undefined ? null : typeof rawV === 'number' ? rawV : parseFloat(String(rawV)); function displayValue(): string { if (rawValue === null || isNaN(rawValue)) return '---'; return Number.isFinite(rawValue) ? rawValue.toPrecision(4).replace(/\.?0+$/, '') : String(rawValue); } function fillColor(): string { if (rawValue === null) return '#4a9eff'; if (thresholdHigh !== null && rawValue >= thresholdHigh) return '#ef4444'; if (thresholdLow !== null && rawValue <= thresholdLow) return '#f59e0b'; return '#4a9eff'; } const needleDeg = rawValue === null ? START_DEG : valueToDeg(rawValue, minVal, maxVal); const bgPath = arcPath(START_DEG, END_DEG, r); const fillPath = needleDeg > START_DEG ? arcPath(START_DEG, needleDeg, r) : ''; const needlePos = polarToXY(needleDeg, r - 4); const fc = fillColor(); return (
{fillPath && ( )} {displayValue()} {unit && {unit}} {minVal} {maxVal} {label &&
{label}
}
); }