// 可缩放面积图 — 光标拖拽选区 / 宽屏弹框展开 // Logan.Li import { useState, useMemo, useCallback, useId, useEffect, forwardRef, useImperativeHandle, } from 'react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceArea, ReferenceLine, } from 'recharts'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { ChartToolbar } from './ChartToolbar'; import { MonitorChartTooltip } from '../shared/monitorChartTheme'; import { ChartRangeHint } from '../shared/ChartRangeHint'; import { useTranslation } from 'react-i18next'; export interface ChartSeries { dataKey: string; color: string; label: string; } export interface ChartReferenceLine { value: number; label?: string; color?: string; } export interface ZoomableChartHandle { expand: () => void; resetZoom: () => void; } interface ZoomRange { left: string; right: string; } interface Props { data: Record[]; series: ChartSeries[]; xDataKey?: string; height?: number | string; unit?: string; yDomain?: [number, number | 'auto']; chartId: string; title?: string; subtitle?: string; showHint?: boolean; /** header=按钮由父组件放在卡片标题栏;overlay=浮在图表右上角(无卡片场景) // Logan.Li */ toolbarPlacement?: 'header' | 'overlay'; onZoomedChange?: (zoomed: boolean) => void; referenceLines?: ChartReferenceLine[]; } const dialogClass = '!max-w-[min(96vw,88rem)] sm:!max-w-[min(96vw,88rem)] w-[min(96vw,88rem)] max-h-[92vh] flex flex-col gap-2 p-4 sm:p-5 bg-[var(--dg-bg-primary)] border-[var(--dg-border)] overflow-hidden'; function ChartBody({ displayData, series, xDataKey, height, unit, yDomain, gradPrefix, refLeft, refRight, onMouseDown, onMouseMove, onMouseUp, compact, referenceLines, }: { displayData: Record[]; series: ChartSeries[]; xDataKey: string; height: number | string; unit: string; yDomain: [number, number | 'auto']; gradPrefix: string; zoomRange: ZoomRange | null; refLeft: string; refRight: string; onMouseDown: (e: { activeLabel?: string }) => void; onMouseMove: (e: { activeLabel?: string }) => void; onMouseUp: () => void; compact?: boolean; referenceLines?: ChartReferenceLine[]; }) { return (
{series.map((s) => ( ))} max * 1.15] : yDomain as [number, number]} tickFormatter={(v: number) => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : String(Math.round(v * 10) / 10))} /> series.find((s) => s.dataKey === name)?.label || name} valueFormatter={(v) => `${Number(v).toFixed(2)}${unit ? ` ${unit}` : ''}`} /> )} /> {referenceLines?.map((line) => ( ))} {series.map((s) => ( ))} {refLeft && refRight ? ( ) : null}
); } export const ZoomableAreaChart = forwardRef(function ZoomableAreaChart({ data, series, xDataKey = 'time', height = 192, unit = '', yDomain = [0, 'auto'], chartId, title, subtitle, showHint = true, toolbarPlacement = 'header', onZoomedChange, referenceLines, }, ref) { const { t } = useTranslation(); const uid = useId().replace(/:/g, ''); const gradPrefix = `grad-${chartId}-${uid}`; const [zoomRange, setZoomRange] = useState(null); const [refLeft, setRefLeft] = useState(''); const [refRight, setRefRight] = useState(''); const [expanded, setExpanded] = useState(false); const dataFingerprint = useMemo(() => { if (data.length === 0) return 'empty'; const first = data[0]?.[xDataKey]; const last = data[data.length - 1]?.[xDataKey]; return `${data.length}|${String(first)}|${String(last)}`; }, [data, xDataKey]); useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect setZoomRange(null); setRefLeft(''); setRefRight(''); setExpanded(false); }, [dataFingerprint]); useEffect(() => { onZoomedChange?.(!!zoomRange); }, [zoomRange, onZoomedChange]); const displayData = useMemo(() => { if (!zoomRange || data.length === 0) return data; const li = data.findIndex((d) => d[xDataKey] === zoomRange.left); const ri = data.findIndex((d) => d[xDataKey] === zoomRange.right); if (li < 0 || ri < 0) return data; const start = Math.min(li, ri); const end = Math.max(li, ri); if (end - start < 2) return data; return data.slice(start, end + 1); }, [data, zoomRange, xDataKey]); const zoomOut = useCallback(() => { setZoomRange(null); setRefLeft(''); setRefRight(''); }, []); useImperativeHandle(ref, () => ({ expand: () => setExpanded(true), resetZoom: zoomOut, }), [zoomOut]); const onMouseDown = useCallback((e: { activeLabel?: string }) => { if (!e?.activeLabel) return; setRefLeft(String(e.activeLabel)); setRefRight(String(e.activeLabel)); }, []); const onMouseMove = useCallback((e: { activeLabel?: string }) => { if (!refLeft || !e?.activeLabel) return; setRefRight(String(e.activeLabel)); }, [refLeft]); const onMouseUp = useCallback(() => { if (!refLeft || !refRight || refLeft === refRight) { setRefLeft(''); setRefRight(''); return; } setZoomRange({ left: refLeft, right: refRight }); setRefLeft(''); setRefRight(''); }, [refLeft, refRight]); const chartProps = { displayData, series, xDataKey, unit, yDomain, gradPrefix, zoomRange, refLeft, refRight, onMouseDown, onMouseMove, onMouseUp, referenceLines, }; if (data.length === 0) { return (
{t('monitor.lbl.noData')}
); } return ( <>
{toolbarPlacement === 'overlay' && (
setExpanded(true)} onResetZoom={zoomOut} />
)} {showHint && toolbarPlacement === 'header' && ( )}
{title || t('monitor.lbl.metricDetail')} {subtitle && (

{subtitle}

)} {t('monitor.lbl.selectHost')}
setExpanded(true)} onResetZoom={zoomOut} />
{series.length > 1 && (
{series.map((s) => ( {s.label} ))}
)} {/* 宽屏趋势区:21:8 比例,监控弹框标准形态 // Logan.Li */}
{zoomRange && (
)}
); });