portpulse-navigator / script.js
alphonse86's picture
Modify the existing port trade dashboard to add a new derived metric called:
1e02b96 verified
Raw
History Blame Contribute Delete
10.9 kB
/**
* 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);
}