- Convert all .js files to .tsx/.ts - Add types for data structures (MuseumRecord, Metrics, etc.) - Add type declarations for react-chartjs-2 - Configure tsconfig with relaxed strictness for gradual adoption - All components now use TypeScript
185 lines
6.0 KiB
TypeScript
185 lines
6.0 KiB
TypeScript
import React, { useRef, useState, ReactNode } from 'react';
|
|
import JSZip from 'jszip';
|
|
|
|
interface ExportableChartProps {
|
|
children: ReactNode;
|
|
filename?: string;
|
|
title?: string;
|
|
className?: string;
|
|
controls?: ReactNode;
|
|
}
|
|
|
|
// Wrapper component that adds PNG export to any chart
|
|
export function ExportableChart({
|
|
children,
|
|
filename = 'chart',
|
|
title = '',
|
|
className = '',
|
|
controls = null
|
|
}: ExportableChartProps) {
|
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
|
|
const exportAsPNG = () => {
|
|
const chartContainer = chartRef.current;
|
|
if (!chartContainer) return;
|
|
|
|
const canvas = chartContainer.querySelector('canvas');
|
|
if (!canvas) return;
|
|
|
|
// Create a new canvas with white background and title
|
|
const exportCanvas = document.createElement('canvas');
|
|
const ctx = exportCanvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Set dimensions with padding and title space
|
|
const padding = 24;
|
|
const titleHeight = title ? 48 : 0;
|
|
exportCanvas.width = canvas.width + (padding * 2);
|
|
exportCanvas.height = canvas.height + (padding * 2) + titleHeight;
|
|
|
|
// Fill white background
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
|
|
|
|
// Draw title if provided (left-aligned, matching on-screen style)
|
|
if (title) {
|
|
ctx.fillStyle = '#1e293b';
|
|
ctx.font = '600 20px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(title, padding, padding + 24);
|
|
}
|
|
|
|
// Draw the chart
|
|
ctx.drawImage(canvas, padding, padding + titleHeight);
|
|
|
|
// Export
|
|
const link = document.createElement('a');
|
|
link.download = `${filename}-${new Date().toISOString().split('T')[0]}.png`;
|
|
link.href = exportCanvas.toDataURL('image/png', 1.0);
|
|
link.click();
|
|
};
|
|
|
|
return (
|
|
<div className="exportable-chart-wrapper">
|
|
{/* Download button - positioned absolutely in corner */}
|
|
<button
|
|
className="chart-export-btn visible"
|
|
onClick={exportAsPNG}
|
|
title="Download as PNG"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="7 10 12 15 17 10"/>
|
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
</svg>
|
|
</button>
|
|
{title && (
|
|
<div className="chart-header-with-export">
|
|
<h2>{title}</h2>
|
|
{controls && <div className="chart-header-actions">{controls}</div>}
|
|
</div>
|
|
)}
|
|
{!title && controls && <div className="chart-controls">{controls}</div>}
|
|
<div className={`exportable-chart ${className}`}>
|
|
<div ref={chartRef} className="chart-canvas-wrapper">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Utility function to export all charts from a container as a ZIP
|
|
export async function exportAllCharts(containerSelector: string, zipFilename: string = 'charts'): Promise<void> {
|
|
const container = document.querySelector(containerSelector);
|
|
if (!container) return;
|
|
|
|
const zip = new JSZip();
|
|
const chartWrappers = container.querySelectorAll('.exportable-chart-wrapper');
|
|
|
|
for (let i = 0; i < chartWrappers.length; i++) {
|
|
const wrapper = chartWrappers[i];
|
|
const canvas = wrapper.querySelector('canvas');
|
|
const titleEl = wrapper.querySelector('.chart-header-with-export h2');
|
|
const title = titleEl?.textContent || `chart-${i + 1}`;
|
|
|
|
if (!canvas) continue;
|
|
|
|
// Create export canvas with white background and title
|
|
const exportCanvas = document.createElement('canvas');
|
|
const ctx = exportCanvas.getContext('2d');
|
|
if (!ctx) continue;
|
|
|
|
const padding = 32;
|
|
const titleHeight = 56;
|
|
exportCanvas.width = canvas.width + (padding * 2);
|
|
exportCanvas.height = canvas.height + (padding * 2) + titleHeight;
|
|
|
|
// White background
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
|
|
|
|
// Draw title
|
|
ctx.fillStyle = '#1e293b';
|
|
ctx.font = '600 24px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(title, padding, padding + 28);
|
|
|
|
// Draw chart
|
|
ctx.drawImage(canvas, padding, padding + titleHeight);
|
|
|
|
// Convert to blob and add to zip
|
|
const dataUrl = exportCanvas.toDataURL('image/png', 1.0);
|
|
const base64Data = dataUrl.split(',')[1];
|
|
const safeFilename = title.replace(/[^a-zA-Z0-9\u0600-\u06FF\s-]/g, '').replace(/\s+/g, '-');
|
|
zip.file(`${String(i + 1).padStart(2, '0')}-${safeFilename}.png`, base64Data, { base64: true });
|
|
}
|
|
|
|
// Generate and download ZIP
|
|
const blob = await zip.generateAsync({ type: 'blob' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `${zipFilename}-${new Date().toISOString().split('T')[0]}.zip`;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
interface ExportAllButtonProps {
|
|
containerSelector: string;
|
|
zipFilename?: string;
|
|
label: string;
|
|
loadingLabel: string;
|
|
}
|
|
|
|
// Button component for exporting all charts
|
|
export function ExportAllButton({ containerSelector, zipFilename = 'charts', label, loadingLabel }: ExportAllButtonProps) {
|
|
const [exporting, setExporting] = useState(false);
|
|
|
|
const handleExport = async () => {
|
|
setExporting(true);
|
|
try {
|
|
await exportAllCharts(containerSelector, zipFilename);
|
|
} finally {
|
|
setExporting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button
|
|
className="btn-export-all"
|
|
onClick={handleExport}
|
|
disabled={exporting}
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="7 10 12 15 17 10"/>
|
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
</svg>
|
|
{exporting ? loadingLabel : label}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export default ExportableChart;
|