From 201d40c481b26f8f9667608ed38e427b659c7068 Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Mon, 23 Mar 2026 16:46:43 +0530 Subject: [PATCH] Redesign CPU chart with live flow and real-time status - Scrolling chart: latest point always anchors to right edge, older points flow left - LIVE pulsing red badge - Color-coded CPU%: green/amber/red based on load level - Load average chips (1m/5m/15m) with color-coded borders - NOW marker line at right edge - Smooth cubic-bezier transitions on path, dot, and area - Gradient fill color adapts to CPU status Co-Authored-By: Claude Sonnet 4.6 --- client/src/components/dashboard/CpuChart.tsx | 326 ++++++++++++------- 1 file changed, 215 insertions(+), 111 deletions(-) diff --git a/client/src/components/dashboard/CpuChart.tsx b/client/src/components/dashboard/CpuChart.tsx index e1bd496..f2a261e 100644 --- a/client/src/components/dashboard/CpuChart.tsx +++ b/client/src/components/dashboard/CpuChart.tsx @@ -1,157 +1,261 @@ -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { useEffect, useRef, useState } from "react" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { cn } from "@/lib/utils" interface CpuChartProps { history: Array<{ cpuPercent: number; timestamp: string }> loadAvg: [number, number, number] } -const CHART_WIDTH = 600 -const CHART_HEIGHT = 220 -const PADDING = { top: 10, right: 10, bottom: 30, left: 45 } -const INNER_W = CHART_WIDTH - PADDING.left - PADDING.right +const CHART_HEIGHT = 200 +const PADDING = { top: 12, right: 16, bottom: 28, left: 44 } const INNER_H = CHART_HEIGHT - PADDING.top - PADDING.bottom +const STEP = 18 // px between data points +const VISIBLE = 30 // number of points visible at once +const INNER_W = VISIBLE * STEP + +// Map a CPU % value to a y coordinate +function toY(cpu: number) { + return PADDING.top + INNER_H - (Math.min(cpu, 100) / 100) * INNER_H +} + +function cpuColor(cpu: number) { + if (cpu < 50) return { stroke: "#22c55e", fill: "#22c55e", label: "Normal" } + if (cpu < 80) return { stroke: "#f59e0b", fill: "#f59e0b", label: "Elevated" } + return { stroke: "#ef4444", fill: "#ef4444", label: "Critical" } +} + +function loadColor(load: number) { + if (load < 1) return "text-green-600 bg-green-50 border-green-200" + if (load < 2) return "text-amber-600 bg-amber-50 border-amber-200" + return "text-red-600 bg-red-50 border-red-200" +} export function CpuChart({ history, loadAvg }: CpuChartProps) { + const [tick, setTick] = useState(0) + const prevLenRef = useRef(0) const currentCpu = history.length > 0 ? history[history.length - 1].cpuPercent : 0 + const color = cpuColor(currentCpu) - // Build points - const points = history.map((point, i) => { - const x = PADDING.left + (history.length > 1 ? (i / (history.length - 1)) * INNER_W : INNER_W / 2) - const y = PADDING.top + INNER_H - (point.cpuPercent / 100) * INNER_H - return { x, y, time: point.timestamp, cpu: point.cpuPercent } - }) + // Trigger CSS transition whenever a new point arrives + useEffect(() => { + if (history.length !== prevLenRef.current) { + prevLenRef.current = history.length + setTick(t => t + 1) + } + }, [history.length]) - // Build line path - const linePath = points.length > 1 - ? points.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x} ${p.y}`).join(" ") + // Assign fixed x positions: latest point always at right edge, older ones to the left + const pts = history.map((p, i) => ({ + x: PADDING.left + INNER_W - (history.length - 1 - i) * STEP, + y: toY(p.cpuPercent), + cpu: p.cpuPercent, + time: p.timestamp, + })) + + // Only keep points that are within or just left of the chart area + const visible = pts.filter(p => p.x >= PADDING.left - STEP) + + const linePath = visible.length > 1 + ? visible.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ") : "" - // Build area path (line + close to bottom) - const areaPath = points.length > 1 - ? `${linePath} L ${points[points.length - 1].x} ${PADDING.top + INNER_H} L ${points[0].x} ${PADDING.top + INNER_H} Z` + const areaPath = visible.length > 1 + ? `${linePath} L ${visible[visible.length - 1].x.toFixed(1)} ${(PADDING.top + INNER_H).toFixed(1)} L ${visible[0].x.toFixed(1)} ${(PADDING.top + INNER_H).toFixed(1)} Z` : "" - // Y-axis ticks const yTicks = [0, 25, 50, 75, 100] - // X-axis labels (show ~5 evenly spaced) - const xLabelCount = Math.min(5, points.length) - const xLabels = points.length > 1 - ? Array.from({ length: xLabelCount }, (_, i) => { - const idx = Math.round((i / (xLabelCount - 1)) * (points.length - 1)) - const p = points[idx] - const time = new Date(p.time).toLocaleTimeString("en-US", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }) - return { x: p.x, label: time } - }) - : points.length === 1 - ? [{ x: points[0].x, label: new Date(points[0].time).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }) }] - : [] + // X-axis time labels: show 5 evenly spaced from visible points + const xLabelIndexes = visible.length > 1 + ? [0, Math.floor(visible.length * 0.25), Math.floor(visible.length * 0.5), Math.floor(visible.length * 0.75), visible.length - 1] + : visible.length === 1 ? [0] : [] + const xLabels = [...new Set(xLabelIndexes)].map(i => { + const p = visible[i] + if (!p) return null + return { + x: p.x, + label: new Date(p.time).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }), + } + }).filter(Boolean) as { x: number; label: string }[] + + // Gradient colors based on CPU status + const gradientId = `cpuGrad-${color.stroke.replace("#", "")}` return ( - - - CPU & Load Average - - Current: {currentCpu.toFixed(1)}% | Load: {loadAvg.map((l) => l.toFixed(2)).join(", ")} - + + +
+ CPU & Load Average + {/* LIVE badge */} + + + + + + LIVE + +
+ + {/* CPU % + status + load avg chips */} +
+ {/* Big current CPU */} +
+ + {currentCpu.toFixed(1)}% + + + {color.label} + +
+ +
+ + {/* Load avg chips */} +
+ Load: + {([["1m", loadAvg[0]], ["5m", loadAvg[1]], ["15m", loadAvg[2]]] as [string, number][]).map(([label, val]) => ( + + {label} {val.toFixed(2)} + + ))} +
+
- + +
- - - + + + + {/* Clip to chart area */} + + + {/* Grid lines */} {yTicks.map((tick) => { - const y = PADDING.top + INNER_H - (tick / 100) * INNER_H + const y = toY(tick) return ( - + + + {tick}% + + + ) + })} + + {/* Chart paths clipped */} + + {areaPath && ( + - ) - })} + )} + {linePath && ( + + )} - {/* Y-axis labels */} - {yTicks.map((tick) => { - const y = PADDING.top + INNER_H - (tick / 100) * INNER_H - return ( - - {tick}% - - ) - })} + {/* Only show last dot (the live "now" point) */} + {visible.length > 0 && (() => { + const last = visible[visible.length - 1] + return ( + <> + + + + ) + })()} + - {/* Area fill */} - {areaPath && ( - - )} - - {/* Line */} - {linePath && ( - - )} - - {/* Dots */} - {points.map((p, i) => ( - - ))} + {/* NOW line at right edge */} + + + NOW + {/* X-axis labels */} {xLabels.map((label, i) => ( {label.label}