diff --git a/electron/src/control/MinMaxValue.tsx b/electron/src/control/MinMaxValue.tsx new file mode 100644 index 00000000..bb543945 --- /dev/null +++ b/electron/src/control/MinMaxValue.tsx @@ -0,0 +1,173 @@ +import { Icon, IconName } from "@/components/Icon"; +import React, { useState, useMemo } from "react"; +import { + getUnitIcon, + renderValueToReactNode, + renderUnitSymbol, + Unit, +} from "./units"; +import { Label } from "./Label"; +import { TimeSeries, TimeSeriesValue } from "@/lib/timeseries"; + +type TimeframeOption = { + label: string; + value: number; // in milliseconds +}; + +export const TIMEFRAME_OPTIONS: TimeframeOption[] = [ + { label: "1 min", value: 1 * 60 * 1000 }, + { label: "5 min", value: 5 * 60 * 1000 }, + { label: "10 min", value: 10 * 60 * 1000 }, + { label: "30 min", value: 30 * 60 * 1000 }, + { label: "1 hour", value: 60 * 60 * 1000 }, +]; + +type Props = { + label: string; + unit?: Unit; + timeseries: TimeSeries; + icon?: IconName; + renderValue?: (value: number) => string; + defaultTimeframe?: number; // in milliseconds, defaults to 5 minutes + /** Optional externally-controlled timeframe (ms). When provided, component becomes controlled. */ + timeframe?: number; + /** Optional callback when timeframe is changed via this component's selector */ + onTimeframeChange?: (t: number) => void; + /** When true, hide the built-in timeframe selector (useful when sharing a selector outside) */ + hideSelector?: boolean; +}; + +export function MinMaxValue({ + unit, + timeseries, + icon, + label, + renderValue, + defaultTimeframe = 5 * 60 * 1000, // 5 minutes default + timeframe, + onTimeframeChange, + hideSelector = false, +}: Props) { + const [internalTimeframe, setInternalTimeframe] = useState(defaultTimeframe); + const selectedTimeframe = timeframe ?? internalTimeframe; + + // Calculate min/max from the timeseries using the selected timeframe + const { min, max, hasData } = useMemo(() => { + // Use short series for timeframes <= 5 seconds, otherwise use long series + // Short series typically has ~5 second retention with high resolution + // Long series typically has ~1 hour retention with lower resolution + const series = + selectedTimeframe <= 5000 ? timeseries.short : timeseries.long; + + // Manually calculate min/max to exclude zero values (which might be invalid readings) + const cutoffTime = series.lastTimestamp - selectedTimeframe; + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + let hasValidData = false; + let dataCount = 0; + + const { values: raw, index, size, validCount } = series; + + const startIdx = validCount < size ? 0 : index; + + for (let i = 0; i < validCount; i++) { + const idx = (startIdx + i) % size; + const val = raw[idx]; + + if ( + val && + val.timestamp > 0 && + val.timestamp >= cutoffTime && + val.value > 0 + ) { + hasValidData = true; + dataCount++; + if (val.value < min) min = val.value; + if (val.value > max) max = val.value; + } + } + + if (!hasValidData) { + return { min: 0, max: 0, hasData: false }; + } + + return { min, max, hasData: true }; + }, [ + timeseries.short, + timeseries.long, + timeseries.current, + selectedTimeframe, + ]); + + return ( +