Enhance metrics graph visuals and tooltip UI

Switches from LineChart to AreaChart for main metrics graph, adds animated gradients and glow effects, and updates legend styling. Tooltip UI is improved with clearer layout, colored dots, and value formatting. Chart colors now use explicit hex values for better SVG compatibility, and LESS styles are updated for improved appearance and interactivity.
This commit is contained in:
Juan Denis committed 2026-01-19 13:30:50 -05:00
1 parent 9bb434c6af
commit 44f3abe8d2
3 files changed
+190 -50

No files matched your search

+2 -2
View File
@@ -6,8 +6,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Dashboard Template</title>
<script type="module" crossorigin src="/assets/index-Bdew6YNt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BLkkqmRP.css">
<script type="module" crossorigin src="/assets/index-DYaRythr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-W4e3hCos.css">
</head>
<body>
+77 -35
View File
@@ -1,11 +1,18 @@
import React, { useState, useEffect } from 'react';
import {
LineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer, Area, AreaChart
Tooltip, ResponsiveContainer, Area, AreaChart, Legend
} from 'recharts';
import { Cpu, MemoryStick, HardDrive, Clock, TrendingUp } from 'lucide-react';
import api from '../services/api';
// Chart colors - using actual hex values for SVG compatibility
const CHART_COLORS = {
cpu: '#10b981', // Green
memory: '#6366f1', // Purple/Indigo
disk: '#f59e0b' // Amber/Orange
};
const MetricsGraph = ({ compact = false }) => {
const [data, setData] = useState(null);
const [period, setPeriod] = useState('1h');
@@ -46,13 +53,13 @@ const MetricsGraph = ({ compact = false }) => {
function getColor(value, type) {
if (type === 'cpu') {
if (value > 80) return 'var(--danger)';
if (value > 60) return 'var(--warning)';
return 'var(--success)';
if (value > 80) return '#ef4444'; // danger
if (value > 60) return '#f59e0b'; // warning
return '#10b981'; // success
}
if (value > 90) return 'var(--danger)';
if (value > 80) return 'var(--warning)';
return 'var(--accent-primary)';
if (value > 90) return '#ef4444'; // danger
if (value > 80) return '#f59e0b'; // warning
return '#6366f1'; // accent
}
if (loading && !data) {
@@ -94,9 +101,10 @@ const MetricsGraph = ({ compact = false }) => {
<div className="metrics-tooltip">
<div className="tooltip-time">{label}</div>
{payload.map((entry, index) => (
<div key={index} className="tooltip-row" style={{ color: entry.color }}>
<span>{entry.name}:</span>
<span>{entry.value}%</span>
<div key={index} className="tooltip-row">
<span className="tooltip-dot" style={{ backgroundColor: entry.stroke || entry.color }} />
<span className="tooltip-label">{entry.name}:</span>
<span className="tooltip-value" style={{ color: entry.stroke || entry.color }}>{entry.value?.toFixed(1)}%</span>
</div>
))}
</div>
@@ -129,9 +137,9 @@ const MetricsGraph = ({ compact = false }) => {
<ResponsiveContainer width="100%" height={120}>
<AreaChart data={chartData} margin={{ top: 5, right: 5, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--success)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--success)" stopOpacity={0} />
<linearGradient id="cpuGradientCompact" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_COLORS.cpu} stopOpacity={0.4} />
<stop offset="95%" stopColor={CHART_COLORS.cpu} stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="time" tick={false} axisLine={false} />
@@ -140,8 +148,8 @@ const MetricsGraph = ({ compact = false }) => {
<Area
type="monotone"
dataKey="cpu"
stroke="var(--success)"
fill="url(#cpuGradient)"
stroke={CHART_COLORS.cpu}
fill="url(#cpuGradientCompact)"
strokeWidth={2}
name="CPU"
/>
@@ -224,63 +232,97 @@ const MetricsGraph = ({ compact = false }) => {
</div>
<div className="metrics-chart-container">
<ResponsiveContainer width="100%" height={250}>
<LineChart data={chartData} margin={{ top: 10, right: 10, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border-subtle)" />
<ResponsiveContainer width="100%" height={280}>
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: -10, bottom: 0 }}>
<defs>
{/* CPU Gradient - Green */}
<linearGradient id="cpuGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_COLORS.cpu} stopOpacity={0.3} />
<stop offset="95%" stopColor={CHART_COLORS.cpu} stopOpacity={0} />
</linearGradient>
{/* Memory Gradient - Purple */}
<linearGradient id="memoryGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_COLORS.memory} stopOpacity={0.3} />
<stop offset="95%" stopColor={CHART_COLORS.memory} stopOpacity={0} />
</linearGradient>
{/* Disk Gradient - Amber */}
<linearGradient id="diskGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={CHART_COLORS.disk} stopOpacity={0.3} />
<stop offset="95%" stopColor={CHART_COLORS.disk} stopOpacity={0} />
</linearGradient>
{/* Glow filters for lines */}
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="2" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#27272a" opacity={0.5} />
<XAxis
dataKey="time"
tick={{ fontSize: 11, fill: 'var(--text-secondary)' }}
axisLine={{ stroke: 'var(--border-subtle)' }}
tick={{ fontSize: 11, fill: '#a1a1aa' }}
axisLine={{ stroke: '#27272a' }}
tickLine={false}
interval="preserveStartEnd"
/>
<YAxis
domain={[0, 100]}
tick={{ fontSize: 11, fill: 'var(--text-secondary)' }}
axisLine={{ stroke: 'var(--border-subtle)' }}
tick={{ fontSize: 11, fill: '#a1a1aa' }}
axisLine={{ stroke: '#27272a' }}
tickLine={false}
tickFormatter={(value) => `${value}%`}
/>
<Tooltip content={<CustomTooltip />} />
<Line
<Area
type="monotone"
dataKey="cpu"
stroke="var(--success)"
strokeWidth={2}
stroke={CHART_COLORS.cpu}
fill="url(#cpuGradient)"
strokeWidth={2.5}
dot={false}
activeDot={{ r: 6, strokeWidth: 2, stroke: CHART_COLORS.cpu, fill: '#18181b' }}
name="CPU"
filter="url(#glow)"
/>
<Line
<Area
type="monotone"
dataKey="memory"
stroke="var(--accent-primary)"
strokeWidth={2}
stroke={CHART_COLORS.memory}
fill="url(#memoryGradient)"
strokeWidth={2.5}
dot={false}
activeDot={{ r: 6, strokeWidth: 2, stroke: CHART_COLORS.memory, fill: '#18181b' }}
name="Memory"
filter="url(#glow)"
/>
<Line
<Area
type="monotone"
dataKey="disk"
stroke="var(--warning)"
strokeWidth={2}
stroke={CHART_COLORS.disk}
fill="url(#diskGradient)"
strokeWidth={2.5}
dot={false}
activeDot={{ r: 6, strokeWidth: 2, stroke: CHART_COLORS.disk, fill: '#18181b' }}
name="Disk"
filter="url(#glow)"
/>
</LineChart>
</AreaChart>
</ResponsiveContainer>
</div>
<div className="metrics-legend">
<div className="legend-item">
<span className="legend-line" style={{ background: 'var(--success)' }} />
<span className="legend-line cpu" />
<span>CPU</span>
</div>
<div className="legend-item">
<span className="legend-line" style={{ background: 'var(--accent-primary)' }} />
<span className="legend-line memory" />
<span>Memory</span>
</div>
<div className="legend-item">
<span className="legend-line" style={{ background: 'var(--warning)' }} />
<span className="legend-line disk" />
<span>Disk</span>
</div>
</div>
@@ -85,19 +85,51 @@
// Chart Container
.metrics-chart-container {
margin-bottom: @space-4;
background: linear-gradient(180deg, rgba(39, 39, 42, 0.3) 0%, transparent 100%);
border-radius: @radius-lg;
padding: @space-2 0;
&.compact {
margin-bottom: @space-3;
background: transparent;
padding: 0;
}
// Recharts path animation
.recharts-area-area {
animation: fadeIn 0.6s ease-out;
}
.recharts-area-curve {
animation: drawLine 1s ease-out;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes drawLine {
from {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
}
to {
stroke-dasharray: 1000;
stroke-dashoffset: 0;
}
}
// Tooltip
.metrics-tooltip {
background: @bg-elevated;
background: rgba(24, 24, 27, 0.95);
backdrop-filter: blur(8px);
border: 1px solid @border-subtle;
border-radius: @radius-md;
padding: @space-3;
box-shadow: @shadow-lg;
border-radius: @radius-lg;
padding: @space-3 @space-4;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
min-width: 140px;
.tooltip-time {
font-size: @font-size-xs;
@@ -105,19 +137,34 @@
margin-bottom: @space-2;
padding-bottom: @space-2;
border-bottom: 1px solid @border-subtle;
text-align: center;
}
.tooltip-row {
display: flex;
justify-content: space-between;
gap: @space-4;
align-items: center;
gap: @space-2;
font-size: @font-size-sm;
padding: @space-1 0;
}
span:last-child {
font-family: @font-mono;
font-weight: @font-weight-semibold;
}
.tooltip-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.tooltip-label {
color: @text-secondary;
flex: 1;
}
.tooltip-value {
font-family: @font-mono;
font-weight: @font-weight-bold;
min-width: 48px;
text-align: right;
}
}
@@ -126,23 +173,60 @@
display: flex;
align-items: center;
justify-content: center;
gap: @space-6;
gap: @space-8;
padding-top: @space-4;
margin-top: @space-2;
border-top: 1px solid @border-subtle;
}
.legend-item {
display: flex;
align-items: center;
gap: @space-2;
gap: @space-3;
font-size: @font-size-sm;
color: @text-secondary;
padding: @space-1 @space-3;
border-radius: @radius-full;
transition: @transition-fast;
&:hover {
background: @bg-hover;
color: @text-primary;
}
}
.legend-line {
width: 20px;
width: 24px;
height: 3px;
border-radius: 2px;
position: relative;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 6px;
height: 6px;
border-radius: 50%;
background: inherit;
}
&.cpu {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
}
&.memory {
background: #6366f1;
box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
}
&.disk {
background: #f59e0b;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
}
}
// Compact Version
@@ -192,12 +276,26 @@
.recharts-cartesian-grid-horizontal line,
.recharts-cartesian-grid-vertical line {
stroke: @border-subtle;
opacity: 0.5;
}
.recharts-text {
fill: @text-secondary;
}
.recharts-area-curve {
filter: drop-shadow(0 0 4px currentColor);
}
.recharts-active-dot {
filter: drop-shadow(0 0 6px currentColor);
}
.recharts-tooltip-cursor {
stroke: @text-tertiary;
stroke-dasharray: 4 4;
}
// Responsive
@media (max-width: @breakpoint-md) {
.metrics-summary-row {