Skip to content

Commit 4ea15ee

Browse files
committed
feat: add small image export button (<50KB)
- Add exportChartSmallImage function that uses JPEG compression - Iteratively reduces quality and scale to achieve <50KB file size - Add Minimize2 icon button to chart panels (main and comparison) - Add translations for zh and en locales
1 parent 0912970 commit 4ea15ee

3 files changed

Lines changed: 82 additions & 1 deletion

File tree

public/locales/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"resize.drag": "Drag to resize chart height",
4242
"resize.adjust": "Adjust {{title}} chart height",
4343
"exportPNG": "Export PNG",
44+
"exportSmallImage": "Export small image (<50KB)",
4445
"copyImage": "Copy image",
4546
"exportCSV": "Export CSV",
4647
"copyImageError": "Failed to copy image",

public/locales/zh/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"resize.drag": "拖拽调整图表高度",
4242
"resize.adjust": "调整 {{title}} 图表高度",
4343
"exportPNG": "导出 PNG",
44+
"exportSmallImage": "导出小图 (<50KB)",
4445
"copyImage": "复制图片",
4546
"exportCSV": "导出 CSV",
4647
"copyImageError": "复制图片失败",

src/components/ChartContainer.jsx

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
Legend,
1414
} from 'chart.js';
1515
import zoomPlugin from 'chartjs-plugin-zoom';
16-
import { ImageDown, Copy, FileDown } from 'lucide-react';
16+
import { ImageDown, Copy, FileDown, Minimize2 } from 'lucide-react';
1717
import { getMinSteps } from "../utils/getMinSteps.js";
1818
import { useTranslation } from 'react-i18next';
1919

@@ -118,6 +118,67 @@ export default function ChartContainer({
118118
link.click();
119119
}, []);
120120

121+
const exportChartSmallImage = useCallback((id) => {
122+
const chart = chartRefs.current.get(id);
123+
if (!chart) return;
124+
125+
const maxSize = 50 * 1024; // 50KB
126+
const canvas = chart.canvas;
127+
128+
// Create a temporary canvas for resizing
129+
const tempCanvas = document.createElement('canvas');
130+
const ctx = tempCanvas.getContext('2d');
131+
132+
// Start with original size and reduce if needed
133+
let scale = 1;
134+
let quality = 0.8;
135+
let dataUrl;
136+
let attempts = 0;
137+
const maxAttempts = 20;
138+
139+
// Try to get image under 50KB by adjusting quality and scale
140+
while (attempts < maxAttempts) {
141+
const width = Math.floor(canvas.width * scale);
142+
const height = Math.floor(canvas.height * scale);
143+
144+
tempCanvas.width = width;
145+
tempCanvas.height = height;
146+
147+
// Draw with white background (for JPEG)
148+
ctx.fillStyle = '#ffffff';
149+
ctx.fillRect(0, 0, width, height);
150+
ctx.drawImage(canvas, 0, 0, width, height);
151+
152+
dataUrl = tempCanvas.toDataURL('image/jpeg', quality);
153+
154+
// Calculate approximate file size (base64 is ~33% larger than binary)
155+
const base64Length = dataUrl.length - 'data:image/jpeg;base64,'.length;
156+
const fileSize = Math.ceil(base64Length * 0.75);
157+
158+
if (fileSize <= maxSize) {
159+
break;
160+
}
161+
162+
// Reduce quality first, then scale
163+
if (quality > 0.3) {
164+
quality -= 0.1;
165+
} else if (scale > 0.3) {
166+
scale -= 0.1;
167+
quality = 0.7; // Reset quality for new scale
168+
} else {
169+
// Can't reduce further, use what we have
170+
break;
171+
}
172+
173+
attempts++;
174+
}
175+
176+
const link = document.createElement('a');
177+
link.href = dataUrl;
178+
link.download = `${id}-small.jpg`;
179+
link.click();
180+
}, []);
181+
121182
const copyChartImage = useCallback(async (id) => {
122183
const chart = chartRefs.current.get(id);
123184
if (!chart || !navigator?.clipboard) return;
@@ -764,6 +825,15 @@ export default function ChartContainer({
764825
>
765826
<ImageDown size={16} />
766827
</button>
828+
<button
829+
type="button"
830+
className="p-1 rounded-md text-gray-600 hover:text-green-600 hover:bg-gray-100"
831+
onClick={() => exportChartSmallImage(`metric-comp-${idx}`)}
832+
aria-label={t('exportSmallImage')}
833+
title={t('exportSmallImage')}
834+
>
835+
<Minimize2 size={16} />
836+
</button>
767837
<button
768838
type="button"
769839
className="p-1 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-100"
@@ -814,6 +884,15 @@ export default function ChartContainer({
814884
>
815885
<ImageDown size={16} />
816886
</button>
887+
<button
888+
type="button"
889+
className="p-1 rounded-md text-gray-600 hover:text-green-600 hover:bg-gray-100"
890+
onClick={() => exportChartSmallImage(`metric-${idx}`)}
891+
aria-label={t('exportSmallImage')}
892+
title={t('exportSmallImage')}
893+
>
894+
<Minimize2 size={16} />
895+
</button>
817896
<button
818897
type="button"
819898
className="p-1 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-100"

0 commit comments

Comments
 (0)