Restore working state from f17e19f (before mobile overhaul)
Reverting all my changes that broke the desktop layout. Starting fresh for mobile improvements.
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import React, { useRef } from 'react';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
// Wrapper component that adds PNG export to any chart
|
||||
export function ExportableChart({ children, filename = 'chart', title = '', className = '', controls = null }) {
|
||||
export function ExportableChart({ children, filename = 'chart', className = '' }) {
|
||||
const chartRef = useRef(null);
|
||||
|
||||
const exportAsPNG = () => {
|
||||
@@ -12,30 +11,21 @@ export function ExportableChart({ children, filename = 'chart', title = '', clas
|
||||
const canvas = chartContainer.querySelector('canvas');
|
||||
if (!canvas) return;
|
||||
|
||||
// Create a new canvas with white background and title
|
||||
// Create a new canvas with white background
|
||||
const exportCanvas = document.createElement('canvas');
|
||||
const ctx = exportCanvas.getContext('2d');
|
||||
|
||||
// Set dimensions with padding and title space
|
||||
const padding = 24;
|
||||
const titleHeight = title ? 48 : 0;
|
||||
// Set dimensions with padding
|
||||
const padding = 20;
|
||||
exportCanvas.width = canvas.width + (padding * 2);
|
||||
exportCanvas.height = canvas.height + (padding * 2) + titleHeight;
|
||||
exportCanvas.height = canvas.height + (padding * 2);
|
||||
|
||||
// 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);
|
||||
ctx.drawImage(canvas, padding, padding);
|
||||
|
||||
// Export
|
||||
const link = document.createElement('a');
|
||||
@@ -45,118 +35,23 @@ export function ExportableChart({ children, filename = 'chart', title = '', clas
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="exportable-chart-wrapper">
|
||||
{title && (
|
||||
<div className="chart-header-with-export">
|
||||
<h2>{title}</h2>
|
||||
<div className="chart-header-actions">
|
||||
{controls}
|
||||
<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>
|
||||
</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 className={`exportable-chart ${className}`}>
|
||||
<button
|
||||
className="chart-export-btn"
|
||||
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>
|
||||
<div ref={chartRef} className="chart-canvas-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility function to export all charts from a container as a ZIP
|
||||
export async function exportAllCharts(containerSelector, zipFilename = 'charts') {
|
||||
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');
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Button component for exporting all charts
|
||||
export function ExportAllButton({ containerSelector, zipFilename, label, loadingLabel }) {
|
||||
const [exporting, setExporting] = React.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;
|
||||
|
||||
Reference in New Issue
Block a user