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}
+
+ )}