/** * 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 = `