Fixed battery padding
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ChargeVizPanel.tsx
|
// ChargeVizPanel.tsx
|
||||||
// Responsive Grafana battery visual with trimmed SVG padding.
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -19,11 +18,13 @@ interface ChargeVizOptions {
|
|||||||
|
|
||||||
interface Props extends PanelProps<ChargeVizOptions> {}
|
interface Props extends PanelProps<ChargeVizOptions> {}
|
||||||
|
|
||||||
|
// Utility: safely convert any value to a finite number.
|
||||||
const toFinite = (v: any): number | undefined => {
|
const toFinite = (v: any): number | undefined => {
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
return Number.isFinite(n) ? n : undefined;
|
return Number.isFinite(n) ? n : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Compute least-squares slope (% per minute) for stable trend estimation.
|
||||||
function slopePercentPerMinute(times: number[], values: number[]): number {
|
function slopePercentPerMinute(times: number[], values: number[]): number {
|
||||||
const pts: Array<[number, number]> = [];
|
const pts: Array<[number, number]> = [];
|
||||||
for (let i = 0; i < times.length; i++) {
|
for (let i = 0; i < times.length; i++) {
|
||||||
@@ -60,6 +61,9 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options }
|
|||||||
const useRegression = options.useRegressionForRate ?? false;
|
const useRegression = options.useRegressionForRate ?? false;
|
||||||
const hideText = options.hideText ?? false;
|
const hideText = options.hideText ?? false;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Data extraction and basic stats
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
let currentValue = 0;
|
let currentValue = 0;
|
||||||
let rate = 0;
|
let rate = 0;
|
||||||
let low = 0;
|
let low = 0;
|
||||||
@@ -101,11 +105,17 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clamp the percentage range.
|
||||||
const fillLevel = Math.max(0, Math.min(100, currentValue));
|
const fillLevel = Math.max(0, Math.min(100, currentValue));
|
||||||
|
|
||||||
|
// Select colour by thresholds.
|
||||||
let fillColour = highColour;
|
let fillColour = highColour;
|
||||||
if (fillLevel < 30) fillColour = lowColour;
|
if (fillLevel < 30) fillColour = lowColour;
|
||||||
else if (fillLevel < 70) fillColour = midColour;
|
else if (fillLevel < 70) fillColour = midColour;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Gradient setup for charge/discharge effect
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
const gradientId = 'batteryGradient';
|
const gradientId = 'batteryGradient';
|
||||||
let gradientDef: React.ReactNode = null;
|
let gradientDef: React.ReactNode = null;
|
||||||
if (gradientFill) {
|
if (gradientFill) {
|
||||||
@@ -129,57 +139,51 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic scaling (Option B)
|
// The SVG’s internal fill height (for the battery graphic)
|
||||||
const aspectRatio = 0.5;
|
|
||||||
const panelRatio = height / width;
|
|
||||||
let batteryWidth: number;
|
|
||||||
let batteryHeight: number;
|
|
||||||
|
|
||||||
if (panelRatio < aspectRatio) {
|
|
||||||
batteryHeight = height * 0.9;
|
|
||||||
batteryWidth = batteryHeight / aspectRatio;
|
|
||||||
} else {
|
|
||||||
batteryWidth = width * (hideText ? 0.95 : 0.6);
|
|
||||||
batteryHeight = batteryWidth * aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fillHeightPx = (fillLevel / 100) * 220;
|
const fillHeightPx = (fillLevel / 100) * 220;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Render
|
// Render layout
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: hideText ? 'center' : 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
fontFamily: 'Inter, sans-serif',
|
fontFamily: 'Inter, sans-serif',
|
||||||
color: theme.colors.text.primary,
|
color: theme.colors.text.primary,
|
||||||
padding: 2,
|
padding: 0,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
gap: hideText ? '0px' : '6px',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Battery SVG with reduced horizontal padding */}
|
{/* Independent battery element */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: '0 0 auto',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
style={{
|
style={{
|
||||||
height: `${batteryHeight}px`,
|
height: '100%', // fill available panel height
|
||||||
width: `${batteryWidth}px`,
|
width: 'auto', // preserve natural aspect ratio – crucial fix
|
||||||
flex: '0 0 auto',
|
|
||||||
}}
|
}}
|
||||||
viewBox="0 0 112 240" // narrower overall width
|
viewBox="0 0 110 240"
|
||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
>
|
>
|
||||||
{gradientDef}
|
{gradientDef}
|
||||||
|
|
||||||
{/* Outline - starts closer to edges */}
|
{/* Battery outline */}
|
||||||
<rect
|
<rect
|
||||||
x="2"
|
x="2"
|
||||||
y="10"
|
y="10"
|
||||||
width="108" // narrower outline width
|
width="106"
|
||||||
height="220"
|
height="220"
|
||||||
rx="6"
|
rx="6"
|
||||||
ry="6"
|
ry="6"
|
||||||
@@ -190,7 +194,7 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options }
|
|||||||
|
|
||||||
{/* Terminal */}
|
{/* Terminal */}
|
||||||
<rect
|
<rect
|
||||||
x="46"
|
x="45"
|
||||||
y="0"
|
y="0"
|
||||||
width="20"
|
width="20"
|
||||||
height="8"
|
height="8"
|
||||||
@@ -199,18 +203,18 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options }
|
|||||||
fill={theme.colors.text.secondary}
|
fill={theme.colors.text.secondary}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Fill - adjusted for new inner width */}
|
{/* Fill area */}
|
||||||
<rect
|
<rect
|
||||||
x="4"
|
x="5"
|
||||||
y={230 - fillHeightPx}
|
y={230 - fillHeightPx}
|
||||||
width="104"
|
width="100"
|
||||||
height={fillHeightPx}
|
height={fillHeightPx}
|
||||||
fill={gradientFill ? `url(#${gradientId})` : fillColour}
|
fill={gradientFill ? `url(#${gradientId})` : fillColour}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Text inside battery */}
|
{/* Percentage text inside the battery */}
|
||||||
<text
|
<text
|
||||||
x="56"
|
x="55"
|
||||||
y="120"
|
y="120"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
@@ -221,23 +225,25 @@ export const ChargeVizPanel: React.FC<Props> = ({ data, width, height, options }
|
|||||||
{fillLevel.toFixed(0)}%
|
{fillLevel.toFixed(0)}%
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Optional stats text */}
|
{/* Optional side text – vertically centred next to battery */}
|
||||||
{!hideText && (
|
{!hideText && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
flex: '1 1 auto',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flex: '1 1 auto',
|
alignItems: 'flex-start',
|
||||||
minWidth: 120,
|
marginLeft: '10px',
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
lineHeight: '1.4em',
|
overflow: 'hidden',
|
||||||
|
lineHeight: '1.5em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '1.05em', fontWeight: 600 }}>
|
<div style={{ fontSize: '1.1em', fontWeight: 600 }}>
|
||||||
Rate: {rate.toFixed(dp)} %/min
|
Rate: {rate.toFixed(dp)} %/min
|
||||||
</div>
|
</div>
|
||||||
<div>Low: {low.toFixed(dp)}%</div>
|
<div>Low: {low.toFixed(dp)}%</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user