feat: BatteryLifeChart component for power usage visualization

This commit is contained in:
2026-05-09 23:04:20 +08:00
parent f03411a8be
commit 4998337913
+101
View File
@@ -0,0 +1,101 @@
"use client"
import { EChartWrapper, getDeviceColor } from "./EChartWrapper"
interface BatteryLifePoint {
id: string
hardwareSlug: string
tdpWatts: number
estimatedBatteryMin: number
estimatedBatteryHours: number
wattHours: number | null
tdpMax: number | null
estimatedAtMaxTdpMin: number | null
}
interface BatteryLifeChartProps {
data: BatteryLifePoint[]
deviceNames?: Record<string, string>
}
export function BatteryLifeChart({ data, deviceNames }: BatteryLifeChartProps) {
if (!data || data.length === 0) {
return <div className="flex items-center justify-center h-48 text-sm text-text/40">No battery data available</div>
}
// Group by device for separate series
const deviceGroups = new Map<string, BatteryLifePoint[]>()
for (const point of data) {
const existing = deviceGroups.get(point.hardwareSlug) || []
existing.push(point)
deviceGroups.set(point.hardwareSlug, existing)
}
// Build trend lines: for each device, compute wattHours / tdp = hours for a range of TDPs
const series: any[] = []
let seriesIdx = 0
for (const [slug, points] of deviceGroups.entries()) {
const color = getDeviceColor(seriesIdx)
const name = deviceNames?.[slug] || slug
// Scatter points: TDP vs battery hours
series.push({
name,
type: "scatter" as const,
data: points.map((p) => [p.tdpWatts, p.estimatedBatteryHours]),
itemStyle: { color },
symbolSize: 10,
})
// Trend line: compute theoretical curve using average wattHours for this device
const avgWh = points.reduce((sum, p) => sum + (p.wattHours ?? 0), 0) / points.length
if (avgWh > 0) {
const tdpRange = [2, 5, 8, 10, 12, 15, 18, 20, 25, 30].filter(
(tdp) => tdp <= (points[0].tdpMax ?? 30),
)
series.push({
name: `${name} (est.)`,
type: "line" as const,
data: tdpRange.map((tdp) => [tdp, Math.round((avgWh / tdp) * 10) / 10]),
lineStyle: { color, type: "dashed" as const, width: 1 },
symbol: "none",
silent: true,
})
}
seriesIdx++
}
const option = {
tooltip: {
trigger: "item" as const,
formatter: (params: any) => {
if (params.seriesName.includes("(est.)")) return ""
return `${params.seriesName}<br/>TDP: ${params.value[0]}W<br/>Battery: ~${params.value[1]}h`
},
},
legend: {
textStyle: { color: "#999" },
top: 0,
},
grid: { left: 60, right: 20, top: 40, bottom: 40 },
xAxis: {
type: "value" as const,
name: "TDP (W)",
nameTextStyle: { color: "#999" },
splitLine: { lineStyle: { color: "#333" } },
axisLabel: { color: "#999" },
},
yAxis: {
type: "value" as const,
name: "Battery (h)",
nameTextStyle: { color: "#999" },
splitLine: { lineStyle: { color: "#333" } },
axisLabel: { color: "#999" },
},
series,
}
return <EChartWrapper option={option} height={300} />
}