import React, { useState, useRef, useEffect, useMemo } from 'react'; import { Card, Button, Badge, Select, Modal } from '../../ui'; import type { ChartWidget as ChartWidgetType, ChartSeries, ChartConfig, ChartFilter, ExportFormat, ChartType } from './types'; interface ChartWidgetProps { widget: ChartWidgetType; data?: ChartSeries[]; loading?: boolean; error?: string; onConfigChange?: (config: Partial) => void; onFiltersChange?: (filters: ChartFilter[]) => void; onRefresh?: () => void; onExport?: (format: ExportFormat) => void; onFullscreen?: () => void; interactive?: boolean; showControls?: boolean; showTitle?: boolean; showSubtitle?: boolean; className?: string; } interface ChartPoint { x: number; y: number; label: string; } const CHART_TYPE_LABELS: Record = { line: 'Líneas', bar: 'Barras', pie: 'Circular', area: 'Área', scatter: 'Dispersión', doughnut: 'Dona', radar: 'Radar', mixed: 'Mixto', }; const EXPORT_FORMATS: ExportFormat[] = ['png', 'svg', 'pdf', 'csv', 'excel']; export const ChartWidget: React.FC = ({ widget, data = [], loading = false, error, onConfigChange, onFiltersChange, onRefresh, onExport, onFullscreen, interactive = true, showControls = true, showTitle = true, showSubtitle = true, className = '', }) => { const canvasRef = useRef(null); const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [hoveredPoint, setHoveredPoint] = useState<{ seriesIndex: number; pointIndex: number } | null>(null); const [selectedExportFormat, setSelectedExportFormat] = useState('png'); const chartDimensions = useMemo(() => ({ width: widget.dimensions?.width || '100%', height: widget.dimensions?.height || 400, aspectRatio: widget.dimensions?.aspect_ratio || '16:9', }), [widget.dimensions]); // Simple chart rendering logic useEffect(() => { if (!canvasRef.current || !data.length || loading) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size const rect = canvas.getBoundingClientRect(); canvas.width = rect.width; canvas.height = rect.height; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw based on chart type switch (widget.type) { case 'bar': drawBarChart(ctx, canvas, data); break; case 'line': drawLineChart(ctx, canvas, data); break; case 'pie': drawPieChart(ctx, canvas, data); break; case 'area': drawAreaChart(ctx, canvas, data); break; default: drawBarChart(ctx, canvas, data); } }, [data, widget.type, loading]); const drawBarChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { if (!chartData.length) return; const padding = 60; const chartWidth = canvas.width - 2 * padding; const chartHeight = canvas.height - 2 * padding; // Get all data points const allPoints = chartData.flatMap(series => series.data); const maxY = Math.max(...allPoints.map(point => point.y)); const minY = Math.min(...allPoints.map(point => point.y)); const firstSeries = chartData[0]; const barWidth = chartWidth / firstSeries.data.length / chartData.length; const barSpacing = barWidth * 0.1; // Draw grid ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = 1; for (let i = 0; i <= 5; i++) { const y = padding + (chartHeight * i) / 5; ctx.beginPath(); ctx.moveTo(padding, y); ctx.lineTo(padding + chartWidth, y); ctx.stroke(); } // Draw bars chartData.forEach((series, seriesIndex) => { const color = series.color || widget.config.color_scheme[seriesIndex % widget.config.color_scheme.length]; ctx.fillStyle = color; series.data.forEach((point, pointIndex) => { const x = padding + (pointIndex * chartWidth) / firstSeries.data.length + (seriesIndex * barWidth); const barHeight = ((point.y - minY) / (maxY - minY)) * chartHeight; const y = padding + chartHeight - barHeight; ctx.fillRect(x + barSpacing, y, barWidth - barSpacing, barHeight); // Add value labels if enabled if (widget.config.show_tooltips) { ctx.fillStyle = '#374151'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(point.y.toString(), x + barWidth / 2, y - 5); ctx.fillStyle = color; } }); }); // Draw axes ctx.strokeStyle = '#374151'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(padding, padding); ctx.lineTo(padding, padding + chartHeight); ctx.lineTo(padding + chartWidth, padding + chartHeight); ctx.stroke(); // Draw labels ctx.fillStyle = '#6b7280'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; // X-axis labels firstSeries.data.forEach((point, index) => { const x = padding + (index + 0.5) * (chartWidth / firstSeries.data.length); ctx.fillText(point.x.toString(), x, padding + chartHeight + 20); }); // Y-axis labels ctx.textAlign = 'right'; for (let i = 0; i <= 5; i++) { const y = padding + (chartHeight * i) / 5; const value = maxY - ((maxY - minY) * i) / 5; ctx.fillText(value.toFixed(0), padding - 10, y + 4); } }; const drawLineChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { if (!chartData.length) return; const padding = 60; const chartWidth = canvas.width - 2 * padding; const chartHeight = canvas.height - 2 * padding; const allPoints = chartData.flatMap(series => series.data); const maxY = Math.max(...allPoints.map(point => point.y)); const minY = Math.min(...allPoints.map(point => point.y)); // Draw grid ctx.strokeStyle = '#e5e7eb'; ctx.lineWidth = 1; for (let i = 0; i <= 5; i++) { const y = padding + (chartHeight * i) / 5; ctx.beginPath(); ctx.moveTo(padding, y); ctx.lineTo(padding + chartWidth, y); ctx.stroke(); } // Draw lines chartData.forEach((series, seriesIndex) => { if (!series.visible) return; const color = series.color || widget.config.color_scheme[seriesIndex % widget.config.color_scheme.length]; ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); series.data.forEach((point, pointIndex) => { const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; if (pointIndex === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); // Draw points ctx.fillStyle = color; series.data.forEach((point, pointIndex) => { const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; ctx.beginPath(); ctx.arc(x, y, 4, 0, 2 * Math.PI); ctx.fill(); }); }); // Draw axes ctx.strokeStyle = '#374151'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(padding, padding); ctx.lineTo(padding, padding + chartHeight); ctx.lineTo(padding + chartWidth, padding + chartHeight); ctx.stroke(); }; const drawPieChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { if (!chartData.length) return; const centerX = canvas.width / 2; const centerY = canvas.height / 2; const radius = Math.min(canvas.width, canvas.height) / 3; const firstSeries = chartData[0]; const total = firstSeries.data.reduce((sum, point) => sum + point.y, 0); let startAngle = -Math.PI / 2; firstSeries.data.forEach((point, index) => { const sliceAngle = (point.y / total) * 2 * Math.PI; const color = widget.config.color_scheme[index % widget.config.color_scheme.length]; ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle); ctx.closePath(); ctx.fill(); // Draw slice border ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.stroke(); // Draw labels const labelAngle = startAngle + sliceAngle / 2; const labelX = centerX + Math.cos(labelAngle) * (radius + 30); const labelY = centerY + Math.sin(labelAngle) * (radius + 30); ctx.fillStyle = '#374151'; ctx.font = '12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(point.x.toString(), labelX, labelY); const percentage = ((point.y / total) * 100).toFixed(1); ctx.fillText(`${percentage}%`, labelX, labelY + 15); startAngle += sliceAngle; }); }; const drawAreaChart = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, chartData: ChartSeries[]) => { if (!chartData.length) return; const padding = 60; const chartWidth = canvas.width - 2 * padding; const chartHeight = canvas.height - 2 * padding; const allPoints = chartData.flatMap(series => series.data); const maxY = Math.max(...allPoints.map(point => point.y)); const minY = Math.min(...allPoints.map(point => point.y)); chartData.forEach((series, seriesIndex) => { if (!series.visible) return; const color = series.color || widget.config.color_scheme[seriesIndex % widget.config.color_scheme.length]; // Create gradient const gradient = ctx.createLinearGradient(0, padding, 0, padding + chartHeight); gradient.addColorStop(0, color + '80'); // Semi-transparent gradient.addColorStop(1, color + '10'); // Very transparent ctx.fillStyle = gradient; ctx.beginPath(); // Start from bottom-left const firstX = padding; const firstY = padding + chartHeight; ctx.moveTo(firstX, firstY); // Draw the area path series.data.forEach((point, pointIndex) => { const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; ctx.lineTo(x, y); }); // Close the path at bottom-right const lastX = padding + chartWidth; const lastY = padding + chartHeight; ctx.lineTo(lastX, lastY); ctx.closePath(); ctx.fill(); // Draw the line on top ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); series.data.forEach((point, pointIndex) => { const x = padding + (pointIndex * chartWidth) / (series.data.length - 1); const y = padding + chartHeight - ((point.y - minY) / (maxY - minY)) * chartHeight; if (pointIndex === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); }); }; const handleExport = () => { if (onExport) { onExport(selectedExportFormat); } }; const handleFullscreen = () => { setIsFullscreen(!isFullscreen); if (onFullscreen) { onFullscreen(); } }; const renderLegend = () => { if (!widget.config.show_legend || !data.length) return null; return (
{data.map((series, index) => (
{series.name}
))}
); }; const renderControls = () => { if (!showControls || !interactive) return null; return (
{onRefresh && ( )}
); }; if (error) { return (

Error al cargar el gráfico

{error}

{onRefresh && ( )}
); } return ( <> {/* Header */}
{showTitle && (

{widget.title}

)} {showSubtitle && widget.subtitle && (

{widget.subtitle}

)}
{renderControls()}
{/* Chart Area */}
{loading ? (
) : data.length === 0 ? (

No hay datos disponibles

Ajusta los filtros o verifica la conexión de datos

) : (
{renderLegend()}
)}
{/* Last Updated */} {widget.last_updated && (
Actualizado: {new Date(widget.last_updated).toLocaleTimeString()}
)}
{/* Configuration Modal */} setIsConfigModalOpen(false)} title="Configuración del Gráfico" >
); }; export default ChartWidget;