From 1506e0fd8da7a757943138699ab454becef6f469 Mon Sep 17 00:00:00 2001 From: JavaZero <71128095+JavaZeroo@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:46:40 +0800 Subject: [PATCH 1/2] feat: add chart export options --- README.md | 2 + src/components/ChartContainer.jsx | 74 ++++++++++++++++++++++++++++++- src/components/ResizablePanel.jsx | 9 +++- 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b3bba84..1c78e29 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - 🔬 **对比分析**:提供 Normal、Absolute、Relative 等多种差值模式,并计算统计指标。 - 🎛️ **灵活展示**:可独立启用或禁用文件和图表,支持调整数据范围与图表尺寸,图表高度可拖拽调整。 - ⚡ **易用特性**:支持匹配预览、智能推荐解析规则,图表 Shift+拖动可快速缩放。 +- 💾 **数据导出**:每个图表均支持导出 PNG、复制图片到剪贴板以及导出 CSV 数据。 ## 快速上手 ⚡ @@ -28,6 +29,7 @@ 2. 在弹出的配置面板中选择解析方式(关键字或正则),并可添加自定义指标。 3. 查看自动生成的图表,必要时可上传第二个文件进行对比分析。 4. 调整图表显示或数据范围以获得更精确的结果。 +5. 使用图表右上角的按钮导出 PNG、复制图片或下载 CSV 数据。 ## 部署 diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index a5f124a..99cda78 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -103,6 +103,59 @@ export default function ChartContainer({ chartRefs.current.set(id, inst); }, []); + const exportChartPNG = useCallback((id) => { + const chart = chartRefs.current.get(id); + if (!chart) return; + const url = chart.toBase64Image(); + const link = document.createElement('a'); + link.href = url; + link.download = `${id}.png`; + link.click(); + }, []); + + const copyChartImage = useCallback(async (id) => { + const chart = chartRefs.current.get(id); + if (!chart || !navigator?.clipboard) return; + const url = chart.toBase64Image(); + const res = await fetch(url); + const blob = await res.blob(); + try { + await navigator.clipboard.write([ + new ClipboardItem({ 'image/png': blob }) + ]); + } catch (e) { + console.error('复制图片失败', e); + } + }, []); + + const exportChartCSV = useCallback((id) => { + const chart = chartRefs.current.get(id); + if (!chart) return; + const datasets = chart.data.datasets || []; + const xValues = new Set(); + datasets.forEach(ds => { + (ds.data || []).forEach(p => xValues.add(p.x)); + }); + const sortedX = Array.from(xValues).sort((a, b) => a - b); + const header = ['step', ...datasets.map(ds => ds.label || '')]; + const rows = sortedX.map(x => { + const cols = [x]; + datasets.forEach(ds => { + const pt = (ds.data || []).find(p => p.x === x); + cols.push(pt ? pt.y : ''); + }); + return cols.join(','); + }); + const csv = [header.join(','), ...rows].join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `${id}.csv`; + link.click(); + URL.revokeObjectURL(url); + }, []); + const syncHoverToAllCharts = useCallback((step, sourceId) => { if (syncLockRef.current) return; syncLockRef.current = true; @@ -611,8 +664,15 @@ export default function ChartContainer({ y: { ...chartOptions.scales.y, min: compRange.min, max: compRange.max } } }; + const compActions = ( + <> + + + + + ); comparisonChart = ( - + - + + + + + + )} + >
-

📊 {title}

+ {actions && ( +
+ {actions} +
+ )}
Date: Mon, 25 Aug 2025 16:56:20 +0800 Subject: [PATCH 2/2] style: replace chart action buttons with icons --- src/components/ChartContainer.jsx | 61 ++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/components/ChartContainer.jsx b/src/components/ChartContainer.jsx index 99cda78..e886e3d 100644 --- a/src/components/ChartContainer.jsx +++ b/src/components/ChartContainer.jsx @@ -13,6 +13,7 @@ import { Legend, } from 'chart.js'; import zoomPlugin from 'chartjs-plugin-zoom'; +import { ImageDown, Copy, FileDown } from 'lucide-react'; import { getMinSteps } from "../utils/getMinSteps.js"; ChartJS.register( @@ -666,9 +667,33 @@ export default function ChartContainer({ }; const compActions = ( <> - - - + + + ); comparisonChart = ( @@ -692,9 +717,33 @@ export default function ChartContainer({ initialHeight={440} actions={( <> - - - + + + )} >