/** * Advanced Chart Visualizations * Provides different chart types for international trade data visualization */ const TradeCharts = (function() { // Store chart instances to destroy when needed const chartInstances = {}; // Color schemes for different chart types const colorSchemes = { default: [ 'rgba(25, 118, 210, 0.7)', // Primary blue 'rgba(229, 57, 53, 0.7)', // Red 'rgba(67, 160, 71, 0.7)', // Green 'rgba(251, 192, 45, 0.7)', // Yellow 'rgba(156, 39, 176, 0.7)', // Purple 'rgba(0, 188, 212, 0.7)', // Cyan 'rgba(255, 152, 0, 0.7)', // Orange 'rgba(121, 85, 72, 0.7)', // Brown 'rgba(96, 125, 139, 0.7)', // Blue Grey 'rgba(233, 30, 99, 0.7)' // Pink ], borders: [ 'rgba(25, 118, 210, 1)', // Primary blue 'rgba(229, 57, 53, 1)', // Red 'rgba(67, 160, 71, 1)', // Green 'rgba(251, 192, 45, 1)', // Yellow 'rgba(156, 39, 176, 1)', // Purple 'rgba(0, 188, 212, 1)', // Cyan 'rgba(255, 152, 0, 1)', // Orange 'rgba(121, 85, 72, 1)', // Brown 'rgba(96, 125, 139, 1)', // Blue Grey 'rgba(233, 30, 99, 1)' // Pink ], gradients: function(ctx) { return [ createGradient(ctx, [25, 118, 210]), createGradient(ctx, [229, 57, 53]), createGradient(ctx, [67, 160, 71]), createGradient(ctx, [251, 192, 45]), createGradient(ctx, [156, 39, 176]), createGradient(ctx, [0, 188, 212]), createGradient(ctx, [255, 152, 0]), createGradient(ctx, [121, 85, 72]), createGradient(ctx, [96, 125, 139]), createGradient(ctx, [233, 30, 99]) ]; } }; // Create a gradient color function createGradient(ctx, rgbColor) { const gradient = ctx.createLinearGradient(0, 0, 0, 400); gradient.addColorStop(0, `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.8)`); gradient.addColorStop(1, `rgba(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]}, 0.2)`); return gradient; } // Load Chart.js if not present function ensureChartJsLoaded() { return new Promise((resolve, reject) => { if (window.Chart) { resolve(window.Chart); return; } const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js'; script.onload = () => resolve(window.Chart); script.onerror = () => reject(new Error('Failed to load Chart.js')); document.body.appendChild(script); }); } // Helper function to destroy existing chart function destroyChart(containerId) { if (chartInstances[containerId]) { chartInstances[containerId].destroy(); delete chartInstances[containerId]; } } // Helper to extract top N items function getTopItems(data, valueField, labelField, n = 10) { return [...data] .sort((a, b) => b[valueField] - a[valueField]) .slice(0, n) .map(item => ({ value: item[valueField], label: item[labelField] })); } // Create bar chart async function createBarChart(containerId, data, options = {}) { await ensureChartJsLoaded(); const container = document.getElementById(containerId); if (!container) return null; // Create or get canvas let canvas = container.querySelector('canvas'); if (!canvas) { canvas = document.createElement('canvas'); container.innerHTML = ''; container.appendChild(canvas); } destroyChart(containerId); // Default options const defaults = { valueField: 'value', labelField: 'country', title: 'Trade Data', horizontal: false, limit: 10, showLegend: false, animation: true }; const chartOptions = { ...defaults, ...options }; // Prepare data - limit to top N items and process data let chartData; if (Array.isArray(data.rows)) { chartData = getTopItems( data.rows, chartOptions.valueField, chartOptions.labelField, chartOptions.limit ); } else if (Array.isArray(data)) { chartData = getTopItems( data, chartOptions.valueField, chartOptions.labelField, chartOptions.limit ); } else { console.error('Invalid data format for bar chart'); return null; } // Get context const ctx = canvas.getContext('2d'); // Create chart chartInstances[containerId] = new Chart(ctx, { type: chartOptions.horizontal ? 'horizontalBar' : 'bar', data: { labels: chartData.map(item => item.label), datasets: [{ label: chartOptions.title, data: chartData.map(item => item.value), backgroundColor: colorSchemes.default, borderColor: colorSchemes.borders, borderWidth: 1 }] }, options: { indexAxis: chartOptions.horizontal ? 'y' : 'x', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: chartOptions.showLegend }, title: { display: true, text: chartOptions.title, font: { size: 16 } }, tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += new Intl.NumberFormat().format( chartOptions.horizontal ? context.parsed.x : context.parsed.y ); } return label; } } } }, animation: chartOptions.animation, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { if (value >= 1000000000) { return (value / 1000000000).toFixed(1) + 'B'; } else if (value >= 1000000) { return (value / 1000000).toFixed(1) + 'M'; } else if (value >= 1000) { return (value / 1000).toFixed(1) + 'K'; } return value; } } } } } }); return chartInstances[containerId]; } // Create pie chart async function createPieChart(containerId, data, options = {}) { await ensureChartJsLoaded(); const container = document.getElementById(containerId); if (!container) return null; // Create or get canvas let canvas = container.querySelector('canvas'); if (!canvas) { canvas = document.createElement('canvas'); container.innerHTML = ''; container.appendChild(canvas); } destroyChart(containerId); // Default options const defaults = { valueField: 'value', labelField: 'country', title: 'Trade Distribution', limit: 10, showLegend: true, animation: true, doughnut: false }; const chartOptions = { ...defaults, ...options }; // Prepare data - limit to top N items let chartData; if (Array.isArray(data.rows)) { chartData = getTopItems( data.rows, chartOptions.valueField, chartOptions.labelField, chartOptions.limit ); } else if (Array.isArray(data)) { chartData = getTopItems( data, chartOptions.valueField, chartOptions.labelField, chartOptions.limit ); } else { console.error('Invalid data format for pie chart'); return null; } // Get context const ctx = canvas.getContext('2d'); // Create chart chartInstances[containerId] = new Chart(ctx, { type: chartOptions.doughnut ? 'doughnut' : 'pie', data: { labels: chartData.map(item => item.label), datasets: [{ data: chartData.map(item => item.value), backgroundColor: colorSchemes.default, borderColor: colorSchemes.borders, borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: chartOptions.showLegend, position: 'right' }, title: { display: true, text: chartOptions.title, font: { size: 16 } }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.raw; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((value / total) * 100).toFixed(1); return `${label}: ${new Intl.NumberFormat().format(value)} (${percentage}%)`; } } } }, animation: chartOptions.animation } }); return chartInstances[containerId]; } // Create line chart async function createLineChart(containerId, data, options = {}) { await ensureChartJsLoaded(); const container = document.getElementById(containerId); if (!container) return null; // Create or get canvas let canvas = container.querySelector('canvas'); if (!canvas) { canvas = document.createElement('canvas'); container.innerHTML = ''; container.appendChild(canvas); } destroyChart(containerId); // Default options const defaults = { valueField: 'value', labelField: 'year', title: 'Trade Trends', showLegend: true, animation: true, fill: true, seriesField: null, // If provided, creates multiple series based on this field timeScale: false }; const chartOptions = { ...defaults, ...options }; // Get context const ctx = canvas.getContext('2d'); // Prepare datasets let datasets = []; if (chartOptions.seriesField) { // Group data by series field const seriesData = {}; const sourceData = Array.isArray(data.rows) ? data.rows : data; sourceData.forEach(item => { const seriesKey = item[chartOptions.seriesField]; if (!seriesData[seriesKey]) { seriesData[seriesKey] = []; } seriesData[seriesKey].push({ x: item[chartOptions.labelField], y: item[chartOptions.valueField] }); }); // Create a dataset for each series let colorIndex = 0; for (const seriesKey in seriesData) { datasets.push({ label: seriesKey, data: seriesData[seriesKey], backgroundColor: chartOptions.fill ? colorSchemes.gradients(ctx)[colorIndex % 10] : colorSchemes.default[colorIndex % 10], borderColor: colorSchemes.borders[colorIndex % 10], borderWidth: 2, fill: chartOptions.fill, tension: 0.1 }); colorIndex++; } } else { // Single series const sourceData = Array.isArray(data.rows) ? data.rows : data; // Sort data by label (typically year) sourceData.sort((a, b) => { if (chartOptions.timeScale) { return new Date(a[chartOptions.labelField]) - new Date(b[chartOptions.labelField]); } return a[chartOptions.labelField] - b[chartOptions.labelField]; }); datasets.push({ label: chartOptions.title, data: sourceData.map(item => ({ x: item[chartOptions.labelField], y: item[chartOptions.valueField] })), backgroundColor: chartOptions.fill ? colorSchemes.gradients(ctx)[0] : colorSchemes.default[0], borderColor: colorSchemes.borders[0], borderWidth: 2, fill: chartOptions.fill, tension: 0.1 }); } // Create chart chartInstances[containerId] = new Chart(ctx, { type: 'line', data: { datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: chartOptions.showLegend }, title: { display: true, text: chartOptions.title, font: { size: 16 } }, tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += new Intl.NumberFormat().format(context.parsed.y); } return label; } } } }, animation: chartOptions.animation, scales: { x: { type: chartOptions.timeScale ? 'time' : 'category', time: chartOptions.timeScale ? { unit: 'year', displayFormats: { year: 'yyyy' } } : undefined }, y: { beginAtZero: true, ticks: { callback: function(value) { if (value >= 1000000000) { return (value / 1000000000).toFixed(1) + 'B'; } else if (value >= 1000000) { return (value / 1000000).toFixed(1) + 'M'; } else if (value >= 1000) { return (value / 1000).toFixed(1) + 'K'; } return value; } } } } } }); return chartInstances[containerId]; } // Create treemap for product/country hierarchies async function createTreemap(containerId, data, options = {}) { // This is a simplified treemap using divs since Chart.js doesn't have built-in treemap const container = document.getElementById(containerId); if (!container) return null; // Default options const defaults = { valueField: 'value', labelField: 'country', title: 'Trade Distribution', limit: 20 }; const chartOptions = { ...defaults, ...options }; // Prepare data - limit to top N items let chartData; if (Array.isArray(data.rows)) { chartData = getTopItems( data.rows, chartOptions.valueField, chartOptions.labelField, chartOptions.limit ); } else if (Array.isArray(data)) { chartData = getTopItems( data, chartOptions.valueField, chartOptions.labelField, chartOptions.limit ); } else { console.error('Invalid data format for treemap'); return null; } // Calculate total for percentages const total = chartData.reduce((sum, item) => sum + item.value, 0); // Create treemap container container.innerHTML = `
${chartOptions.title}
`; const treemapContainer = container.querySelector('.treemap-container'); treemapContainer.style.display = 'flex'; treemapContainer.style.flexWrap = 'wrap'; treemapContainer.style.height = '400px'; treemapContainer.style.position = 'relative'; // Create rectangles chartData.forEach((item, index) => { const percentage = (item.value / total * 100).toFixed(1); const div = document.createElement('div'); div.className = 'treemap-item'; div.style.backgroundColor = colorSchemes.default[index % colorSchemes.default.length]; div.style.color = '#fff'; div.style.padding = '8px'; div.style.boxSizing = 'border-box'; div.style.overflow = 'hidden'; div.style.fontSize = '12px'; div.style.position = 'relative'; div.style.flexGrow = item.value; // Size must be proportional to value div.style.width = `${Math.sqrt(percentage)}%`; div.style.height = `${Math.sqrt(percentage) * 2}%`; div.style.margin = '2px'; // Text with truncation div.innerHTML = `
${item.label}
${percentage}%
`; // Tooltip on hover div.title = `${item.label}: ${new Intl.NumberFormat().format(item.value)} (${percentage}%)`; treemapContainer.appendChild(div); }); return treemapContainer; } // Create a world map visualization async function createWorldMapChart(containerId, data, options = {}) { // Load leaflet script if not already loaded if (!window.L) { await new Promise((resolve, reject) => { // Load CSS const leafletCss = document.createElement('link'); leafletCss.rel = 'stylesheet'; leafletCss.href = 'https://unpkg.com/leaflet/dist/leaflet.css'; document.head.appendChild(leafletCss); // Load script const script = document.createElement('script'); script.src = 'https://unpkg.com/leaflet/dist/leaflet.js'; script.onload = resolve; script.onerror = reject; document.body.appendChild(script); }); } const container = document.getElementById(containerId); if (!container) return null; // Default options const defaults = { valueField: 'value', labelField: 'country', countryCodeField: 'code', title: 'World Trade Map', colorScale: ['#e6f7ff', '#0077be'], zoom: 2 }; const chartOptions = { ...defaults, ...options }; // Set container height if not already set if (!container.style.height || container.style.height === 'auto') { container.style.height = '400px'; } // Clear previous map container.innerHTML = ''; // Create map const map = L.map(containerId).setView([20, 0], chartOptions.zoom); // Add tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap contributors' }).addTo(map); // Process data const sourceData = Array.isArray(data.rows) ? data.rows : data; // Find min/max for color scaling const values = sourceData.map(item => item[chartOptions.valueField]); const min = Math.min(...values); const max = Math.max(...values); // Function to compute color based on value function getColor(value) { const ratio = (value - min) / (max - min || 1); // Linear interpolation between start and end colors const startColor = chartOptions.colorScale[0]; const endColor = chartOptions.colorScale[1]; // Parse hex colors const startRGB = { r: parseInt(startColor.slice(1, 3), 16), g: parseInt(startColor.slice(3, 5), 16), b: parseInt(startColor.slice(5, 7), 16) }; const endRGB = { r: parseInt(endColor.slice(1, 3), 16), g: parseInt(endColor.slice(3, 5), 16), b: parseInt(endColor.slice(5, 7), 16) }; // Interpolate const r = Math.round(startRGB.r + ratio * (endRGB.r - startRGB.r)); const g = Math.round(startRGB.g + ratio * (endRGB.g - startRGB.g)); const b = Math.round(startRGB.b + ratio * (endRGB.b - startRGB.b)); return `rgb(${r}, ${g}, ${b})`; } // Add country polygons if country GeoJSON is available // For now we'll use circles at country coordinates as a simplified version // Find coordinates for countries (simplified - real app would use GeoJSON) const countryCoordinates = { // Sample coordinates for major countries '842': [37.0902, -95.7129], // USA '156': [35.8617, 104.1954], // China '276': [51.1657, 10.4515], // Germany '392': [36.2048, 138.2529], // Japan '826': [55.3781, -3.4360], // UK '250': [46.2276, 2.2137], // France '380': [41.8719, 12.5674], // Italy '124': [56.1304, -106.3468], // Canada '410': [35.9078, 127.7669], // South Korea '484': [23.6345, -102.5528], // Mexico // Default coordinates for unknown countries 'default': [0, 0] }; // Add circles for each country sourceData.forEach(item => { const code = item[chartOptions.countryCodeField]; const value = item[chartOptions.valueField]; const coords = countryCoordinates[code] || countryCoordinates.default; if (coords[0] !== 0 || coords[1] !== 0) { // Size circle based on value const radius = Math.max(5, Math.min(20, 5 + (value - min) / (max - min || 1) * 15)); L.circleMarker(coords, { radius: radius, fillColor: getColor(value), color: '#fff', weight: 1, opacity: 1, fillOpacity: 0.8 }) .addTo(map) .bindPopup(` ${item[chartOptions.labelField]}
Value: ${new Intl.NumberFormat().format(value)} `); } }); // Add legend const legend = L.control({ position: 'bottomright' }); legend.onAdd = function() { const div = L.DomUtil.create('div', 'info legend'); div.style.backgroundColor = 'white'; div.style.padding = '10px'; div.style.borderRadius = '5px'; div.style.boxShadow = '0 0 5px rgba(0,0,0,0.2)'; div.innerHTML = `
${chartOptions.title}
${new Intl.NumberFormat().format(min)}
${new Intl.NumberFormat().format(max)}
`; return div; }; legend.addTo(map); return map; } // Public API return { createBarChart, createPieChart, createLineChart, createTreemap, createWorldMapChart, destroyChart }; })();