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..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( @@ -103,6 +104,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 +665,39 @@ export default function ChartContainer({ y: { ...chartOptions.scales.y, min: compRange.min, max: compRange.max } } }; + const compActions = ( + <> + + + + + ); comparisonChart = ( - + - + + + + + + )} + >
-

📊 {title}

+ {actions && ( +
+ {actions} +
+ )}