import React, { useEffect, useRef, useState } from 'react'; import { Loader2, AlertCircle, Download, Maximize2, Minimize2 } from 'lucide-react'; interface PlotData { type: 'plotly' | 'chartjs'; name: string; data: any; created_at: string; } interface PlotRendererProps { plotData?: PlotData; plotUrl?: string; // Fallback for legacy HTML plots title: string; onClose?: () => void; } // Lazy load Plotly to reduce bundle size const loadPlotly = (): Promise => { return new Promise((resolve, reject) => { if ((window as any).Plotly) { resolve((window as any).Plotly); return; } const script = document.createElement('script'); script.src = 'https://cdn.plot.ly/plotly-2.27.0.min.js'; script.async = true; script.onload = () => resolve((window as any).Plotly); script.onerror = () => reject(new Error('Failed to load Plotly')); document.head.appendChild(script); }); }; export const PlotRenderer: React.FC = ({ plotData, plotUrl, title, onClose }) => { const containerRef = useRef(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { if (!plotData && !plotUrl) { setError('No plot data provided'); setLoading(false); return; } const renderPlot = async () => { try { setLoading(true); setError(null); if (plotData && plotData.type === 'plotly') { // Render Plotly chart from JSON data const Plotly = await loadPlotly(); if (containerRef.current) { // Extract data and layout from the plot data const { data, layout, config } = plotData.data; // Apply dark theme const darkLayout = { ...layout, paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)', font: { color: '#ffffff' }, xaxis: { ...layout?.xaxis, gridcolor: 'rgba(255,255,255,0.1)', linecolor: 'rgba(255,255,255,0.2)' }, yaxis: { ...layout?.yaxis, gridcolor: 'rgba(255,255,255,0.1)', linecolor: 'rgba(255,255,255,0.2)' }, margin: { t: 40, r: 20, b: 40, l: 60 } }; const darkConfig = { ...config, responsive: true, displayModeBar: true, displaylogo: false, modeBarButtonsToRemove: ['lasso2d', 'select2d'] }; Plotly.newPlot(containerRef.current, data, darkLayout, darkConfig); } } setLoading(false); } catch (err) { console.error('Error rendering plot:', err); setError(err instanceof Error ? err.message : 'Failed to render plot'); setLoading(false); } }; renderPlot(); // Cleanup return () => { if (containerRef.current && (window as any).Plotly) { (window as any).Plotly.purge(containerRef.current); } }; }, [plotData, plotUrl]); // Handle window resize useEffect(() => { const handleResize = () => { if (containerRef.current && (window as any).Plotly && plotData) { (window as any).Plotly.Plots.resize(containerRef.current); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [plotData]); const handleDownload = () => { if (containerRef.current && (window as any).Plotly) { (window as any).Plotly.downloadImage(containerRef.current, { format: 'png', width: 1200, height: 800, filename: title.replace(/\s+/g, '_') }); } }; const toggleFullscreen = () => { setIsFullscreen(!isFullscreen); }; // If we only have a URL (legacy HTML plot), use iframe if (!plotData && plotUrl) { return (