/** * Hemisphere Trade Balance Index (HTBI) Calculation: * HTBI = (Trade volume with Americas) / (Trade volume with Asia) * Where: * - Americas trade includes Latin America + Caribbean * - Asia trade includes all Asian regions * Interpretation: * - HTBI > 1.0: Americas-dominant trade * - HTBI < 1.0: Asia-dominant trade * - HTBI = 1.0: Balanced trade between hemispheres */ function calculateHTBI(americasVolume, asiaVolume) { // Handle division by zero safely if (asiaVolume === 0) return americasVolume > 0 ? Infinity : 0; return americasVolume / asiaVolume; } // Mock data structure for port trade analytics const portData = { west: { imports: { asia: [1200000, 1250000, 1300000, 1180000, 1220000, 1150000], americas: [450000, 470000, 490000, 510000, 530000, 550000], europe: [300000, 310000, 320000, 315000, 325000, 330000], other: [150000, 155000, 160000, 158000, 162000, 165000] }, exports: { asia: [800000, 820000, 840000, 830000, 850000, 860000], americas: [350000, 370000, 390000, 410000, 430000, 450000], europe: [250000, 260000, 270000, 265000, 275000, 280000], other: [100000, 105000, 110000, 108000, 112000, 115000] }, teus: [1800000, 1850000, 1900000, 1880000, 1920000, 1950000], cargoMix: { containerized: 58, bulk: 25, energy: 12, roro: 5 } }, gulf: { imports: { asia: [600000, 620000, 640000, 630000, 650000, 660000], americas: [550000, 570000, 590000, 610000, 630000, 650000], europe: [200000, 210000, 220000, 215000, 225000, 230000], other: [100000, 105000, 110000, 108000, 112000, 115000] }, exports: { asia: [400000, 420000, 440000, 430000, 450000, 460000], americas: [450000, 470000, 490000, 510000, 530000, 550000], europe: [150000, 160000, 170000, 165000, 175000, 180000], other: [80000, 85000, 90000, 88000, 92000, 95000] }, teus: [900000, 920000, 950000, 940000, 960000, 980000], cargoMix: { containerized: 35, bulk: 30, energy: 30, roro: 5 } }, florida: { imports: { asia: [300000, 320000, 340000, 330000, 350000, 360000], americas: [650000, 670000, 690000, 710000, 730000, 750000], europe: [150000, 160000, 170000, 165000, 175000, 180000], other: [70000, 75000, 80000, 78000, 82000, 85000] }, exports: { asia: [200000, 220000, 240000, 230000, 250000, 260000], americas: [550000, 570000, 590000, 610000, 630000, 650000], europe: [100000, 110000, 120000, 115000, 125000, 130000], other: [50000, 55000, 60000, 58000, 62000, 65000] }, teus: [600000, 620000, 650000, 640000, 660000, 680000], cargoMix: { containerized: 45, bulk: 20, energy: 15, roro: 20 } } }; // Initialize all charts function initializeCharts() { updateTradeShareChart('west'); updatePortComparisonChart(); updateTradeIndexChart(); updateCargoChart('west'); updateHTBIChart(); } // Trade Share Chart (Stacked Area) function updateTradeShareChart(portGroup) { const data = portData[portGroup]; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']; const trace1 = { x: months, y: data.imports.asia, name: 'Asia Imports', stackgroup: 'one', line: {color: '#3b82f6'}, fillcolor: 'rgba(59, 130, 246, 0.5)' }; const trace2 = { x: months, y: data.imports.americas, name: 'Americas Imports', stackgroup: 'one', line: {color: '#10b981'}, fillcolor: 'rgba(16, 185, 129, 0.5)' }; const trace3 = { x: months, y: data.imports.europe, name: 'Europe Imports', stackgroup: 'one', line: {color: '#6366f1'}, fillcolor: 'rgba(99, 102, 241, 0.5)' }; const trace4 = { x: months, y: data.imports.other, name: 'Other Imports', stackgroup: 'one', line: {color: '#8b5cf6'}, fillcolor: 'rgba(139, 92, 246, 0.5)' }; const layout = { title: 'Regional Import Share', showlegend: true, legend: {orientation: 'h'}, margin: {t: 30, l: 40, r: 30, b: 40}, hovermode: 'closest' }; Plotly.newPlot('tradeShareChart', [trace1, trace2, trace3, trace4], layout); } // Port Comparison Chart (Bar) function updatePortComparisonChart() { const westTotal = portData.west.imports.asia.reduce((a,b) => a+b, 0) + portData.west.imports.americas.reduce((a,b) => a+b, 0); const gulfTotal = portData.gulf.imports.asia.reduce((a,b) => a+b, 0) + portData.gulf.imports.americas.reduce((a,b) => a+b, 0); const floridaTotal = portData.florida.imports.asia.reduce((a,b) => a+b, 0) + portData.florida.imports.americas.reduce((a,b) => a+b, 0); const trace1 = { x: ['West Coast', 'Gulf Coast', 'Florida'], y: [westTotal, gulfTotal, floridaTotal], name: 'Total Tonnage', type: 'bar', marker: {color: '#3b82f6'} }; const trace2 = { x: ['West Coast', 'Gulf Coast', 'Florida'], y: [ portData.west.teus.reduce((a,b) => a+b, 0)/6, portData.gulf.teus.reduce((a,b) => a+b, 0)/6, portData.florida.teus.reduce((a,b) => a+b, 0)/6 ], name: 'Avg TEUs (Monthly)', type: 'bar', marker: {color: '#10b981'} }; const layout = { title: 'Port System Comparison', barmode: 'group', margin: {t: 30, l: 40, r: 30, b: 40}, hovermode: 'closest' }; Plotly.newPlot('portComparisonChart', [trace1, trace2], layout); } // Asia vs Americas Index (Line) function updateTradeIndexChart() { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']; // Calculate index (Asia Imports / Americas Imports) const westIndex = portData.west.imports.asia.map((val, i) => (val / portData.west.imports.americas[i]).toFixed(2)); const gulfIndex = portData.gulf.imports.asia.map((val, i) => (val / portData.gulf.imports.americas[i]).toFixed(2)); const floridaIndex = portData.florida.imports.asia.map((val, i) => (val / portData.florida.imports.americas[i]).toFixed(2)); const trace1 = { x: months, y: westIndex, name: 'West Coast', type: 'scatter', line: {color: '#3b82f6', width: 3} }; const trace2 = { x: months, y: gulfIndex, name: 'Gulf Coast', type: 'scatter', line: {color: '#ef4444', width: 3} }; const trace3 = { x: months, y: floridaIndex, name: 'Florida', type: 'scatter', line: {color: '#10b981', width: 3} }; const layout = { title: 'Asia vs Americas Trade Index', yaxis: {title: 'Ratio (Asia/Americas)'}, margin: {t: 30, l: 40, r: 30, b: 40}, hovermode: 'closest' }; Plotly.newPlot('tradeIndexChart', [trace1, trace2, trace3], layout); } // Cargo Composition Chart (Pie) function updateCargoChart(portGroup) { const data = portData[portGroup].cargoMix; const trace = { values: [data.containerized, data.bulk, data.energy, data.roro], labels: ['Containerized', 'Bulk', 'Energy', 'Ro-Ro'], type: 'pie', marker: { colors: ['#3b82f6', '#10b981', '#f59e0b', '#6366f1'] } }; const layout = { title: 'Cargo Composition', margin: {t: 30, l: 0, r: 0, b: 0}, height: 380 }; Plotly.newPlot('cargoChart', [trace], layout); } // Event listeners document.addEventListener('DOMContentLoaded', function() { initializeCharts(); document.querySelector('port-group-selector').addEventListener('portGroupChange', (e) => { updateTradeShareChart(e.detail.group); updateCargoChart(e.detail.group); }); // HTBI chart event listener document.querySelector('port-group-selector').addEventListener('portGroupChange', (e) => { updateTradeShareChart(e.detail.group); updateCargoChart(e.detail.group); }); }); // HTBI Chart (Line with reference) function updateHTBIChart() { const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']; // Calculate HTBI for each port group const westHTBI = portData.west.imports.americas.map((val, i) => calculateHTBI(val, portData.west.imports.asia[i])); const gulfHTBI = portData.gulf.imports.americas.map((val, i) => calculateHTBI(val, portData.gulf.imports.asia[i])); const floridaHTBI = portData.florida.imports.americas.map((val, i) => calculateHTBI(val, portData.florida.imports.asia[i])); // Create traces for each port group const trace1 = { x: months, y: westHTBI, name: 'West Coast', type: 'scatter', line: {color: '#3b82f6', width: 3}, hovertemplate: 'West Coast
HTBI: %{y:.2f}
' + '%{y:.2f} = (Americas %{meta.americas} / Asia %{meta.asia})', meta: { americas: portData.west.imports.americas.join(', '), asia: portData.west.imports.asia.join(', ') } }; const trace2 = { x: months, y: gulfHTBI, name: 'Gulf Coast', type: 'scatter', line: {color: '#ef4444', width: 3}, hovertemplate: 'Gulf Coast
HTBI: %{y:.2f}
' + '%{y:.2f} = (Americas %{meta.americas} / Asia %{meta.asia})', meta: { americas: portData.gulf.imports.americas.join(', '), asia: portData.gulf.imports.asia.join(', ') } }; const trace3 = { x: months, y: floridaHTBI, name: 'Florida', type: 'scatter', line: {color: '#10b981', width: 3}, hovertemplate: 'Florida
HTBI: %{y:.2f}
' + '%{y:.2f} = (Americas %{meta.americas} / Asia %{meta.asia})', meta: { americas: portData.florida.imports.americas.join(', '), asia: portData.florida.imports.asia.join(', ') } }; // Add reference line at HTBI = 1.0 const referenceLine = { x: months, y: Array(months.length).fill(1.0), name: 'Balance Threshold', line: { color: '#6b7280', width: 2, dash: 'dot' }, hoverinfo: 'none' }; // Check for regime shifts (HTBI crossing 1.0 for 6+ months) const annotations = []; // Example annotation (would be dynamic in production) annotations.push({ x: 'Mar', y: 0.85, text: 'Regime Shift Detected', showarrow: true, arrowhead: 2, ax: 0, ay: -30, bgcolor: 'rgba(239, 68, 68, 0.2)', bordercolor: '#ef4444' }); const layout = { title: 'Hemisphere Trade Balance Index (Americas vs Asia)', yaxis: { title: 'HTBI Value', range: [0, Math.max(...westHTBI, ...gulfHTBI, ...floridaHTBI) * 1.1] }, margin: {t: 30, l: 40, r: 30, b: 40}, hovermode: 'closest', annotations: annotations, shapes: [{ type: 'line', x0: months[0], x1: months[months.length-1], y0: 1.0, y1: 1.0, line: { color: '#6b7280', width: 2, dash: 'dot' } }] }; Plotly.newPlot('htbiChart', [trace1, trace2, trace3, referenceLine], layout); }