// --------------------------------------------------------------------------- // ChargeVizPanel.tsx // --------------------------------------------------------------------------- import React from 'react'; import { PanelProps } from '@grafana/data'; import { useTheme2 } from '@grafana/ui'; interface ChargeVizOptions { lowColour: string; midColour: string; highColour: string; gradientFill: boolean; decimalPlaces: number; useRegressionForRate: boolean; hideText: boolean; } interface Props extends PanelProps {} // Utility: safely convert any value to a finite number. const toFinite = (v: any): number | undefined => { const n = Number(v); return Number.isFinite(n) ? n : undefined; }; // Compute least-squares slope (% per minute) for stable trend estimation. function slopePercentPerMinute(times: number[], values: number[]): number { const pts: Array<[number, number]> = []; for (let i = 0; i < times.length; i++) { const t = toFinite(times[i]); const v = toFinite(values[i]); if (t !== undefined && v !== undefined) pts.push([t, v]); } if (pts.length < 2) return 0; const xs = pts.map(p => (p[0] - pts[0][0]) / 60000); const ys = pts.map(p => p[1]); const xMean = xs.reduce((a, b) => a + b, 0) / xs.length; const yMean = ys.reduce((a, b) => a + b, 0) / ys.length; let num = 0; let den = 0; for (let i = 0; i < xs.length; i++) { const dx = xs[i] - xMean; const dy = ys[i] - yMean; num += dx * dy; den += dx * dx; } return den === 0 ? 0 : num / den; } export const ChargeVizPanel: React.FC = ({ data, width, height, options }) => { const theme = useTheme2(); const lowColour = options.lowColour || '#F44336'; const midColour = options.midColour || '#FF9800'; const highColour = options.highColour || '#4CAF50'; const gradientFill = options.gradientFill ?? true; const dp = Number.isFinite(options.decimalPlaces) ? options.decimalPlaces : 2; const useRegression = options.useRegressionForRate ?? false; const hideText = options.hideText ?? false; // ----------------------------------------------------------------------- // Data extraction and basic stats // ----------------------------------------------------------------------- let currentValue = 0; let rate = 0; let low = 0; let high = 0; if (data.series.length > 0) { const series = data.series[0]; const valueField = series.fields.find(f => f.type === 'number'); const timeField = series.fields.find(f => f.type === 'time'); if (valueField && timeField) { const rawValues = valueField.values.toArray(); const rawTimes = timeField.values.toArray(); const values: number[] = []; const times: number[] = []; for (let i = 0; i < rawValues.length; i++) { const v = toFinite(rawValues[i]); const t = toFinite(rawTimes[i]); if (v !== undefined && t !== undefined) { values.push(v); times.push(t); } } if (values.length > 0) { currentValue = values[values.length - 1]; low = Math.min(...values); high = Math.max(...values); if (useRegression && values.length >= 2) { rate = slopePercentPerMinute(times, values); } else if (values.length >= 2) { const deltaPercent = values[values.length - 1] - values[0]; const deltaMinutes = (times[times.length - 1] - times[0]) / 60000; rate = deltaMinutes !== 0 ? deltaPercent / deltaMinutes : 0; } } } } // Clamp the percentage range. const fillLevel = Math.max(0, Math.min(100, currentValue)); // Select colour by thresholds. let fillColour = highColour; if (fillLevel < 30) fillColour = lowColour; else if (fillLevel < 70) fillColour = midColour; // ----------------------------------------------------------------------- // Gradient setup for charge/discharge effect // ----------------------------------------------------------------------- const gradientId = 'batteryGradient'; let gradientDef: React.ReactNode = null; if (gradientFill) { let startColour = fillColour; let endColour = fillColour; if (rate > 0) { startColour = fillColour; endColour = theme.colors.success.text; } else if (rate < 0) { startColour = theme.colors.error.text; endColour = fillColour; } gradientDef = ( ); } // The SVG’s internal fill height (for the battery graphic) const fillHeightPx = (fillLevel / 100) * 220; // ----------------------------------------------------------------------- // Render layout // ----------------------------------------------------------------------- return (
{/* Independent battery element */}
{gradientDef} {/* Battery outline */} {/* Terminal */} {/* Fill area */} {/* Percentage text inside the battery */} {fillLevel.toFixed(0)}%
{/* Optional side text – vertically centred next to battery */} {!hideText && (
Rate: {rate.toFixed(dp)} %/min
Low: {low.toFixed(dp)}%
High: {high.toFixed(dp)}%
{rate > 0 ? 'Charging ↑' : rate < 0 ? 'Discharging ↓' : 'Stable'}
)}
); };