Spaces:
Running
Running
| /** | |
| * 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: '<b>West Coast</b><br>HTBI: %{y:.2f}<br>' + | |
| '%{y:.2f} = (Americas %{meta.americas} / Asia %{meta.asia})<extra></extra>', | |
| 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: '<b>Gulf Coast</b><br>HTBI: %{y:.2f}<br>' + | |
| '%{y:.2f} = (Americas %{meta.americas} / Asia %{meta.asia})<extra></extra>', | |
| 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: '<b>Florida</b><br>HTBI: %{y:.2f}<br>' + | |
| '%{y:.2f} = (Americas %{meta.americas} / Asia %{meta.asia})<extra></extra>', | |
| 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); | |
| } | |