Skip to content

Commit fb4e22d

Browse files
committed
feat: add copy small image button and improve icon
- Add copyChartSmallImage function for clipboard copy with compression - Change icon from Minimize2 to ImageMinus for better clarity - Add copy small image button next to copy image button - Both export and copy small image operations target <50KB file size - Add translations for copySmallImage in zh and en locales
1 parent 4ea15ee commit fb4e22d

File tree

3 files changed

+82
-3
lines changed

3 files changed

+82
-3
lines changed

public/locales/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"exportPNG": "Export PNG",
4444
"exportSmallImage": "Export small image (<50KB)",
4545
"copyImage": "Copy image",
46+
"copySmallImage": "Copy small image (<50KB)",
4647
"exportCSV": "Export CSV",
4748
"copyImageError": "Failed to copy image",
4849
"language.en": "English",

public/locales/zh/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"exportPNG": "导出 PNG",
4444
"exportSmallImage": "导出小图 (<50KB)",
4545
"copyImage": "复制图片",
46+
"copySmallImage": "复制小图 (<50KB)",
4647
"exportCSV": "导出 CSV",
4748
"copyImageError": "复制图片失败",
4849
"language.en": "English",

src/components/ChartContainer.jsx

Lines changed: 80 additions & 3 deletions
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, Minimize2 } from 'lucide-react';
16+
import { ImageDown, Copy, FileDown, ImageMinus } from 'lucide-react';
1717
import { getMinSteps } from "../utils/getMinSteps.js";
1818
import { useTranslation } from 'react-i18next';
1919

@@ -194,6 +194,65 @@ export default function ChartContainer({
194194
}
195195
}, [t]);
196196

197+
const copyChartSmallImage = useCallback(async (id) => {
198+
const chart = chartRefs.current.get(id);
199+
if (!chart || !navigator?.clipboard) return;
200+
201+
const maxSize = 50 * 1024; // 50KB
202+
const canvas = chart.canvas;
203+
204+
const tempCanvas = document.createElement('canvas');
205+
const ctx = tempCanvas.getContext('2d');
206+
207+
let scale = 1;
208+
let quality = 0.8;
209+
let dataUrl;
210+
let attempts = 0;
211+
const maxAttempts = 20;
212+
213+
while (attempts < maxAttempts) {
214+
const width = Math.floor(canvas.width * scale);
215+
const height = Math.floor(canvas.height * scale);
216+
217+
tempCanvas.width = width;
218+
tempCanvas.height = height;
219+
220+
ctx.fillStyle = '#ffffff';
221+
ctx.fillRect(0, 0, width, height);
222+
ctx.drawImage(canvas, 0, 0, width, height);
223+
224+
dataUrl = tempCanvas.toDataURL('image/jpeg', quality);
225+
226+
const base64Length = dataUrl.length - 'data:image/jpeg;base64,'.length;
227+
const fileSize = Math.ceil(base64Length * 0.75);
228+
229+
if (fileSize <= maxSize) {
230+
break;
231+
}
232+
233+
if (quality > 0.3) {
234+
quality -= 0.1;
235+
} else if (scale > 0.3) {
236+
scale -= 0.1;
237+
quality = 0.7;
238+
} else {
239+
break;
240+
}
241+
242+
attempts++;
243+
}
244+
245+
try {
246+
const res = await fetch(dataUrl);
247+
const blob = await res.blob();
248+
await navigator.clipboard.write([
249+
new ClipboardItem({ 'image/jpeg': blob })
250+
]);
251+
} catch (e) {
252+
console.error(t('copyImageError'), e);
253+
}
254+
}, [t]);
255+
197256
const exportChartCSV = useCallback((id) => {
198257
const chart = chartRefs.current.get(id);
199258
if (!chart) return;
@@ -832,7 +891,7 @@ export default function ChartContainer({
832891
aria-label={t('exportSmallImage')}
833892
title={t('exportSmallImage')}
834893
>
835-
<Minimize2 size={16} />
894+
<ImageMinus size={16} />
836895
</button>
837896
<button
838897
type="button"
@@ -843,6 +902,15 @@ export default function ChartContainer({
843902
>
844903
<Copy size={16} />
845904
</button>
905+
<button
906+
type="button"
907+
className="p-1 rounded-md text-gray-600 hover:text-green-600 hover:bg-gray-100"
908+
onClick={() => copyChartSmallImage(`metric-comp-${idx}`)}
909+
aria-label={t('copySmallImage')}
910+
title={t('copySmallImage')}
911+
>
912+
<ImageMinus size={16} />
913+
</button>
846914
<button
847915
type="button"
848916
className="p-1 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-100"
@@ -891,7 +959,7 @@ export default function ChartContainer({
891959
aria-label={t('exportSmallImage')}
892960
title={t('exportSmallImage')}
893961
>
894-
<Minimize2 size={16} />
962+
<ImageMinus size={16} />
895963
</button>
896964
<button
897965
type="button"
@@ -902,6 +970,15 @@ export default function ChartContainer({
902970
>
903971
<Copy size={16} />
904972
</button>
973+
<button
974+
type="button"
975+
className="p-1 rounded-md text-gray-600 hover:text-green-600 hover:bg-gray-100"
976+
onClick={() => copyChartSmallImage(`metric-${idx}`)}
977+
aria-label={t('copySmallImage')}
978+
title={t('copySmallImage')}
979+
>
980+
<ImageMinus size={16} />
981+
</button>
905982
<button
906983
type="button"
907984
className="p-1 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-100"

0 commit comments

Comments
 (0)