mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 12:29:34 +00:00
refactor(metrics-modal): mark min/max on chart + improve grid contrast
Drop the Current/Min/Avg/Max stats row and Live auto-refresh toggle — clutter that didn't earn its space. Min/max are now rendered as colored dots on the chart itself (green ▼ for min, orange ▲ for max), which exposes both the value AND the time-axis position of each extremum at a glance. Tooltip now formats the timestamp fully (with date prefix when the sample crosses a day boundary). Switch CartesianGrid stroke from var(--ant-color-border-secondary) to rgba(128,128,140,0.35) so the gridlines stay readable in light theme against the chart-wrap's faint primary tint — the AntD variable resolved to near-zero alpha and the gridlines disappeared. XrayMetricsModal keeps its implicit 2s observatory polling.
This commit is contained in:
@@ -3,6 +3,8 @@ import {
|
||||
Area,
|
||||
AreaChart,
|
||||
CartesianGrid,
|
||||
ReferenceDot,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
@@ -10,6 +12,23 @@ import {
|
||||
} from 'recharts';
|
||||
import './Sparkline.css';
|
||||
|
||||
export interface SparklineReferenceLine {
|
||||
y: number;
|
||||
label?: string;
|
||||
color?: string;
|
||||
dash?: string;
|
||||
}
|
||||
|
||||
export interface SparklineExtrema {
|
||||
show?: boolean;
|
||||
formatter?: (v: number) => string;
|
||||
minColor?: string;
|
||||
maxColor?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_MIN_COLOR = '#52c41a';
|
||||
const DEFAULT_MAX_COLOR = '#fa541c';
|
||||
|
||||
interface SparklineProps {
|
||||
data: number[];
|
||||
labels?: (string | number)[];
|
||||
@@ -29,6 +48,9 @@ interface SparklineProps {
|
||||
valueMax?: number | null;
|
||||
yFormatter?: (v: number) => string;
|
||||
tooltipFormatter?: ((v: number) => string) | null;
|
||||
tooltipLabelFormatter?: ((label: string) => string) | null;
|
||||
referenceLines?: SparklineReferenceLine[];
|
||||
extrema?: SparklineExtrema;
|
||||
}
|
||||
|
||||
interface ChartPoint {
|
||||
@@ -56,6 +78,9 @@ export default function Sparkline({
|
||||
valueMax = 100,
|
||||
yFormatter = (v: number) => `${Math.round(v)}%`,
|
||||
tooltipFormatter = null,
|
||||
tooltipLabelFormatter = null,
|
||||
referenceLines,
|
||||
extrema,
|
||||
}: SparklineProps) {
|
||||
const reactId = useId();
|
||||
const safeId = reactId.replace(/[^a-zA-Z0-9]/g, '');
|
||||
@@ -103,6 +128,22 @@ export default function Sparkline({
|
||||
|
||||
const fmtTooltip = tooltipFormatter ?? yFormatter;
|
||||
|
||||
const extremaPoints = useMemo(() => {
|
||||
if (!extrema?.show || points.length < 2) return null;
|
||||
let minIdx = 0;
|
||||
let maxIdx = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
if (points[i].value < points[minIdx].value) minIdx = i;
|
||||
if (points[i].value > points[maxIdx].value) maxIdx = i;
|
||||
}
|
||||
if (minIdx === maxIdx) return null;
|
||||
return { min: points[minIdx], max: points[maxIdx] };
|
||||
}, [points, extrema?.show]);
|
||||
|
||||
const fmtExtrema = extrema?.formatter ?? yFormatter;
|
||||
const minColor = extrema?.minColor ?? DEFAULT_MIN_COLOR;
|
||||
const maxColor = extrema?.maxColor ?? DEFAULT_MAX_COLOR;
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={height} className="sparkline-svg">
|
||||
<AreaChart data={points} margin={{ top: 6, right: 6, bottom: showAxes ? 14 : 4, left: showAxes ? 4 : 4 }}>
|
||||
@@ -113,7 +154,7 @@ export default function Sparkline({
|
||||
</linearGradient>
|
||||
</defs>
|
||||
{showGrid && (
|
||||
<CartesianGrid stroke="var(--ant-color-border-secondary)" strokeDasharray="2 4" vertical={false} />
|
||||
<CartesianGrid stroke="rgba(128, 128, 140, 0.35)" strokeDasharray="3 4" vertical={false} />
|
||||
)}
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
@@ -140,16 +181,73 @@ export default function Sparkline({
|
||||
contentStyle={{
|
||||
background: 'var(--ant-color-bg-elevated)',
|
||||
border: '1px solid var(--ant-color-border-secondary)',
|
||||
borderRadius: 4,
|
||||
borderRadius: 6,
|
||||
fontSize: 12,
|
||||
padding: '4px 8px',
|
||||
padding: '6px 10px',
|
||||
boxShadow: '0 4px 14px rgba(0, 0, 0, 0.12)',
|
||||
}}
|
||||
labelStyle={{ color: 'var(--ant-color-text-tertiary)', marginBottom: 2 }}
|
||||
itemStyle={{ color: 'var(--ant-color-text)', padding: 0 }}
|
||||
labelStyle={{ color: 'var(--ant-color-text-tertiary)', marginBottom: 4, fontSize: 11 }}
|
||||
itemStyle={{ color: 'var(--ant-color-text)', padding: 0, fontWeight: 500 }}
|
||||
formatter={(v) => [fmtTooltip(Number(v) || 0), '']}
|
||||
labelFormatter={(label) => (tooltipLabelFormatter ? tooltipLabelFormatter(String(label)) : String(label))}
|
||||
separator=""
|
||||
/>
|
||||
)}
|
||||
{referenceLines?.map((rl, idx) => (
|
||||
<ReferenceLine
|
||||
key={`ref-${idx}-${rl.y}`}
|
||||
y={rl.y}
|
||||
stroke={rl.color || stroke}
|
||||
strokeDasharray={rl.dash || '5 4'}
|
||||
strokeWidth={1.4}
|
||||
label={rl.label ? {
|
||||
value: rl.label,
|
||||
position: 'insideTopRight',
|
||||
fill: rl.color || stroke,
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
} : undefined}
|
||||
ifOverflow="extendDomain"
|
||||
/>
|
||||
))}
|
||||
{extremaPoints && (
|
||||
<>
|
||||
<ReferenceDot
|
||||
x={extremaPoints.max.label}
|
||||
y={extremaPoints.max.value}
|
||||
r={4.5}
|
||||
fill={maxColor}
|
||||
stroke="var(--ant-color-bg-elevated)"
|
||||
strokeWidth={2}
|
||||
label={{
|
||||
value: `▲ ${fmtExtrema(extremaPoints.max.value)}`,
|
||||
position: 'top',
|
||||
fontSize: 10.5,
|
||||
fill: maxColor,
|
||||
fontWeight: 600,
|
||||
offset: 8,
|
||||
}}
|
||||
ifOverflow="extendDomain"
|
||||
/>
|
||||
<ReferenceDot
|
||||
x={extremaPoints.min.label}
|
||||
y={extremaPoints.min.value}
|
||||
r={4.5}
|
||||
fill={minColor}
|
||||
stroke="var(--ant-color-bg-elevated)"
|
||||
strokeWidth={2}
|
||||
label={{
|
||||
value: `▼ ${fmtExtrema(extremaPoints.min.value)}`,
|
||||
position: 'bottom',
|
||||
fontSize: 10.5,
|
||||
fill: minColor,
|
||||
fontWeight: 600,
|
||||
offset: 8,
|
||||
}}
|
||||
ifOverflow="extendDomain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
|
||||
Reference in New Issue
Block a user