diff --git a/src/components/charts/NetworkChart.tsx b/src/components/charts/NetworkChart.tsx index 3fa0ed6..11b462c 100644 --- a/src/components/charts/NetworkChart.tsx +++ b/src/components/charts/NetworkChart.tsx @@ -1,6 +1,7 @@ 'use client'; import dynamic from 'next/dynamic'; +import { useCallback, useRef } from 'react'; import type { NetworkMetrics } from '@/types/metrics'; import { formatTimestamp, formatBytes } from '@/lib/formatters'; @@ -13,6 +14,9 @@ interface NetworkChartProps { } export default function NetworkChart({ title = 'Network Traffic', data, height = 300 }: NetworkChartProps) { + const chartRef = useRef(null); + const soloIndex = useRef(null); + if (!data.receive.length && !data.transmit.length) { return (
@@ -26,6 +30,7 @@ export default function NetworkChart({ title = 'Network Traffic', data, height = type: 'line' as const, smooth: true, symbol: 'none', + emphasis: { focus: 'series' as const }, areaStyle: { opacity: 0.3 }, data: inst.dataPoints.map((d) => [d.timestamp * 1000, d.value]), })); @@ -35,25 +40,44 @@ export default function NetworkChart({ title = 'Network Traffic', data, height = type: 'line' as const, smooth: true, symbol: 'none', + emphasis: { focus: 'series' as const }, lineStyle: { type: 'dashed' as const }, data: inst.dataPoints.map((d) => [d.timestamp * 1000, -d.value]), })); + const allSeries = [...rxSeries, ...txSeries]; + const seriesNames = allSeries.map((s) => s.name); + const option = { title: { text: title, left: 'center', textStyle: { fontSize: 14 } }, tooltip: { trigger: 'axis', - formatter: (params: any[]) => { + axisPointer: { type: 'cross' }, + formatter: (params: any) => { + if (!Array.isArray(params)) params = [params]; + if (params.length === 0) return ''; const time = formatTimestamp(params[0].data[0] / 1000); const lines = params.map( (p: any) => `${p.marker} ${p.seriesName}: ${formatBytes(Math.abs(p.data[1]))}/s` ); - return `${time}
${lines.join('
')}`; + return `${time}
${lines.join('
')}`; }, }, - legend: { bottom: 0, type: 'scroll' }, + legend: { + bottom: 0, + type: 'scroll', + selectedMode: 'multiple', + }, grid: { left: '3%', right: '3%', bottom: '15%', top: '15%', containLabel: true }, - xAxis: { type: 'time' }, + xAxis: { + type: 'time', + axisLabel: { + formatter: (val: number) => { + const d = new Date(val); + return `${(d.getMonth() + 1).toString().padStart(2, '0')}/${d.getDate().toString().padStart(2, '0')}\n${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; + }, + }, + }, yAxis: { type: 'value', axisLabel: { @@ -64,8 +88,37 @@ export default function NetworkChart({ title = 'Network Traffic', data, height = { type: 'inside', start: 0, end: 100 }, { type: 'slider', start: 0, end: 100, height: 20, bottom: 30 }, ], - series: [...rxSeries, ...txSeries], + series: allSeries, }; - return ; + const onEvents = useCallback(() => ({ + click: (params: any) => { + const chart = chartRef.current?.getEchartsInstance(); + if (!chart) return; + + if (soloIndex.current === params.seriesIndex) { + const selected: Record = {}; + seriesNames.forEach((name) => { selected[name] = true; }); + chart.setOption({ legend: { selected } }); + soloIndex.current = null; + } else { + const selected: Record = {}; + seriesNames.forEach((name) => { selected[name] = false; }); + selected[params.seriesName] = true; + chart.setOption({ legend: { selected } }); + soloIndex.current = params.seriesIndex; + } + }, + }), [seriesNames]); + + return ( + + ); } diff --git a/src/components/charts/TimeSeriesChart.tsx b/src/components/charts/TimeSeriesChart.tsx index 8614c01..a1be109 100644 --- a/src/components/charts/TimeSeriesChart.tsx +++ b/src/components/charts/TimeSeriesChart.tsx @@ -1,6 +1,7 @@ 'use client'; import dynamic from 'next/dynamic'; +import { useCallback, useRef } from 'react'; import type { MetricInstance } from '@/types/metrics'; import { formatTimestamp } from '@/lib/formatters'; @@ -19,11 +20,15 @@ export default function TimeSeriesChart({ unit = '%', height = 300, }: TimeSeriesChartProps) { + const chartRef = useRef(null); + const soloIndex = useRef(null); + const series = instances.map((inst) => ({ - name: inst.instance, + name: inst.instance || 'total', type: 'line' as const, smooth: true, symbol: 'none', + emphasis: { focus: 'series' as const }, data: inst.dataPoints.map((d) => [d.timestamp * 1000, d.value.toFixed(2)]), markArea: { silent: true, @@ -39,15 +44,24 @@ export default function TimeSeriesChart({ title: { text: title, left: 'center', textStyle: { fontSize: 14 } }, tooltip: { trigger: 'axis', - formatter: (params: any[]) => { + axisPointer: { type: 'cross' }, + formatter: (params: any) => { + if (!Array.isArray(params)) params = [params]; + if (params.length === 0) return ''; const time = formatTimestamp(params[0].data[0] / 1000); - const lines = params.map( - (p: any) => `${p.marker} ${p.seriesName}: ${parseFloat(p.data[1]).toFixed(1)}${unit}` - ); - return `${time}
${lines.join('
')}`; + const lines = params + .sort((a: any, b: any) => parseFloat(b.data[1]) - parseFloat(a.data[1])) + .map( + (p: any) => `${p.marker} ${p.seriesName}: ${parseFloat(p.data[1]).toFixed(1)}${unit}` + ); + return `${time}
${lines.join('
')}`; }, }, - legend: { bottom: 0, type: 'scroll' }, + legend: { + bottom: 0, + type: 'scroll', + selectedMode: 'multiple', + }, grid: { left: '3%', right: '3%', bottom: '15%', top: '15%', containLabel: true }, xAxis: { type: 'time', @@ -69,5 +83,37 @@ export default function TimeSeriesChart({ series, }; - return ; + const onEvents = useCallback(() => ({ + click: (params: any) => { + const chart = chartRef.current?.getEchartsInstance(); + if (!chart) return; + + const seriesNames = instances.map((inst) => inst.instance || 'total'); + + if (soloIndex.current === params.seriesIndex) { + const selected: Record = {}; + seriesNames.forEach((name) => { selected[name] = true; }); + chart.dispatchAction({ type: 'legendSelect', batch: seriesNames.map(name => ({ name })) }); + chart.setOption({ legend: { selected } }); + soloIndex.current = null; + } else { + const selected: Record = {}; + seriesNames.forEach((name) => { selected[name] = false; }); + selected[params.seriesName] = true; + chart.setOption({ legend: { selected } }); + soloIndex.current = params.seriesIndex; + } + }, + }), [instances]); + + return ( + + ); }