Spaces:
Running
You are a senior full-stack engineer and data engineer building a macro-trade analytics dashboard.
Browse filesOBJECTIVE
Build a web dashboard that tracks and compares U.S. port import/export activity to measure:
1) Asia ↔ United States trade intensity
2) Intra-Americas trade intensity (Latin America ↔ U.S. ↔ Caribbean)
The dashboard must clearly show whether Gulf Coast (Houston) and Florida ports are gaining economic importance relative to West Coast ports over time.
PRIMARY PORT GROUPS
Group ports into three systems:
- West Coast: Los Angeles, Long Beach, Oakland, Seattle/Tacoma
- Gulf Coast: Houston, Corpus Christi
- Florida: Miami, Port Everglades, Jacksonville, Tampa
CORE METRICS
For each port group:
- Import tonnage (monthly)
- Export tonnage (monthly)
- Container volume (TEUs, monthly)
- Cargo category (containerized, bulk, energy, Ro/Ro)
- Trade partner region:
- Asia
- Latin America & Caribbean
- Europe
- Other
DATA SOURCES
If live APIs are unavailable, use realistic mock or historical datasets shaped like:
- U.S. Census / USA Trade Online
- U.S. EIA (energy imports/exports)
- Port authority annual/monthly statistics
All mock data must be clearly labeled.
DASHBOARD VIEWS
1. REGIONAL TRADE SHARE
- Stacked area chart:
- % of total trade by partner region
- One chart per port group
- Time range selector (1Y / 5Y / 10Y)
2. PORT SYSTEM COMPARISON
- Side-by-side comparison of:
- West Coast vs Gulf Coast vs Florida
- Show:
- Total tonnage
- TEUs
- Growth rate (YoY)
3. ASIA VS AMERICAS INDEX
- Create a derived index:
Asia_Trade_Share / Americas_Trade_Share
- Plot index over time for each port group
- Highlight divergence or convergence
4. CARGO COMPOSITION VIEW
- Bar chart:
- Energy
- Containerized goods
- Bulk commodities
- Vehicles / Ro-Ro
- Compare how cargo mix differs by region
5. SIGNALS & ANNOTATIONS
- Automatically flag:
- Structural shifts (>10% change sustained for 6+ months)
- Allow manual annotation:
- Policy changes
- Canal disruptions
- Major infrastructure expansions
TECH STACK
- Backend: Python (FastAPI)
- Data processing: Pandas
- Frontend: React or Next.js
- Charts: Plotly (preferred)
- Storage: SQLite
- Deployment: local Replit workspace
DATA LOGIC REQUIREMENTS
- Normalize data so port groups are comparable
- Clearly separate:
- Weight-based metrics
- Container-based metrics
- All derived metrics must be explained in code comments
UX REQUIREMENTS
- Analyst-style layout (Bloomberg / FRED-like)
- Clear legends and region labels
- No consumer-style UI elements
- Dark mode optional
NON-GOALS
- Do not scrape proprietary APIs
- Do not forecast prices
- Do not attempt company-level attribution
DELIVERABLES
- Working dashboard running locally
- Mock datasets (CSV)
- README explaining:
- Assumptions
- Data structure
- How to replace mock data with real sources
SUCCESS CRITERIA
- User can visually identify whether:
- Asia-linked trade is slowing or growing
- Americas-linked trade is strengthening
- Houston & Florida trends are clearly distinguishable from West Coast trends
- README.md +8 -5
- components/port-group-selector.js +63 -0
- index.html +130 -19
- script.js +245 -0
- style.css +35 -18
|
@@ -1,10 +1,13 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: PortPulse Navigator 🚢📊
|
| 3 |
+
colorFrom: pink
|
| 4 |
+
colorTo: yellow
|
| 5 |
+
emoji: 🐳
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- deepsite-v3
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Welcome to your new DeepSite project!
|
| 13 |
+
This project was created with [DeepSite](https://huggingface.co/deepsite).
|
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class PortGroupSelector extends HTMLElement {
|
| 2 |
+
connectedCallback() {
|
| 3 |
+
this.attachShadow({ mode: 'open' });
|
| 4 |
+
this.shadowRoot.innerHTML = `
|
| 5 |
+
<style>
|
| 6 |
+
.selector-container {
|
| 7 |
+
display: flex;
|
| 8 |
+
gap: 1rem;
|
| 9 |
+
margin-bottom: 2rem;
|
| 10 |
+
}
|
| 11 |
+
.port-group {
|
| 12 |
+
flex: 1;
|
| 13 |
+
padding: 1rem;
|
| 14 |
+
border-radius: 0.5rem;
|
| 15 |
+
cursor: pointer;
|
| 16 |
+
transition: all 0.2s ease;
|
| 17 |
+
border: 1px solid #e5e7eb;
|
| 18 |
+
background: white;
|
| 19 |
+
}
|
| 20 |
+
.port-group.active {
|
| 21 |
+
border-color: #3b82f6;
|
| 22 |
+
background: #eff6ff;
|
| 23 |
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
| 24 |
+
}
|
| 25 |
+
.port-group h3 {
|
| 26 |
+
margin: 0 0 0.5rem 0;
|
| 27 |
+
font-size: 1rem;
|
| 28 |
+
font-weight: 600;
|
| 29 |
+
}
|
| 30 |
+
.port-group p {
|
| 31 |
+
margin: 0;
|
| 32 |
+
font-size: 0.875rem;
|
| 33 |
+
color: #6b7280;
|
| 34 |
+
}
|
| 35 |
+
</style>
|
| 36 |
+
<div class="selector-container">
|
| 37 |
+
<div class="port-group active" data-group="west">
|
| 38 |
+
<h3>West Coast</h3>
|
| 39 |
+
<p>LA, Long Beach, Oakland, Seattle</p>
|
| 40 |
+
</div>
|
| 41 |
+
<div class="port-group" data-group="gulf">
|
| 42 |
+
<h3>Gulf Coast</h3>
|
| 43 |
+
<p>Houston, Corpus Christi</p>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="port-group" data-group="florida">
|
| 46 |
+
<h3>Florida</h3>
|
| 47 |
+
<p>Miami, Everglades, Jacksonville</p>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
`;
|
| 51 |
+
|
| 52 |
+
this.shadowRoot.querySelectorAll('.port-group').forEach(group => {
|
| 53 |
+
group.addEventListener('click', () => {
|
| 54 |
+
this.shadowRoot.querySelectorAll('.port-group').forEach(g => g.classList.remove('active'));
|
| 55 |
+
group.classList.add('active');
|
| 56 |
+
this.dispatchEvent(new CustomEvent('portGroupChange', {
|
| 57 |
+
detail: { group: group.dataset.group }
|
| 58 |
+
}));
|
| 59 |
+
});
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
customElements.define('port-group-selector', PortGroupSelector);
|
|
@@ -1,19 +1,130 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="h-full bg-gray-100">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TradeFlow Navigator - Macro Trade Analytics Dashboard</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
|
| 10 |
+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
| 11 |
+
<script src="components/navbar.js"></script>
|
| 12 |
+
<script src="components/metric-card.js"></script>
|
| 13 |
+
<script src="components/port-group-selector.js"></script>
|
| 14 |
+
<script src="components/time-selector.js"></script>
|
| 15 |
+
<script src="https://unpkg.com/feather-icons"></script>
|
| 16 |
+
<script>
|
| 17 |
+
tailwind.config = {
|
| 18 |
+
theme: {
|
| 19 |
+
extend: {
|
| 20 |
+
colors: {
|
| 21 |
+
primary: {
|
| 22 |
+
500: '#3b82f6',
|
| 23 |
+
},
|
| 24 |
+
secondary: {
|
| 25 |
+
500: '#10b981',
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
</script>
|
| 32 |
+
</head>
|
| 33 |
+
<body class="h-full">
|
| 34 |
+
<custom-navbar></custom-navbar>
|
| 35 |
+
|
| 36 |
+
<div class="container mx-auto px-4 py-8">
|
| 37 |
+
<!-- Dashboard Header -->
|
| 38 |
+
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8">
|
| 39 |
+
<div>
|
| 40 |
+
<h1 class="text-3xl font-bold text-gray-800">TradeFlow Navigator</h1>
|
| 41 |
+
<p class="text-gray-600">Macro trade analytics dashboard for U.S. port activity</p>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="mt-4 md:mt-0">
|
| 44 |
+
<custom-time-selector></custom-time-selector>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
<!-- Port Group Selector -->
|
| 48 |
+
<port-group-selector></port-group-selector>
|
| 49 |
+
<!-- Key Metrics -->
|
| 50 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
| 51 |
+
<custom-metric-card
|
| 52 |
+
title="Total Tonnage"
|
| 53 |
+
value="4.2M"
|
| 54 |
+
change="+2.8%"
|
| 55 |
+
icon="package">
|
| 56 |
+
</custom-metric-card>
|
| 57 |
+
<custom-metric-card
|
| 58 |
+
title="TEUs"
|
| 59 |
+
value="1.8M"
|
| 60 |
+
change="+1.5%"
|
| 61 |
+
icon="box">
|
| 62 |
+
</custom-metric-card>
|
| 63 |
+
<custom-metric-card
|
| 64 |
+
title="Asia Share"
|
| 65 |
+
value="58%"
|
| 66 |
+
change="-3.2%"
|
| 67 |
+
icon="globe">
|
| 68 |
+
</custom-metric-card>
|
| 69 |
+
<custom-metric-card
|
| 70 |
+
title="Americas Share"
|
| 71 |
+
value="32%"
|
| 72 |
+
change="+4.1%"
|
| 73 |
+
icon="map-pin">
|
| 74 |
+
</custom-metric-card>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<!-- Main Charts -->
|
| 78 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
| 79 |
+
<div class="bg-white p-6 rounded-lg shadow">
|
| 80 |
+
<h2 class="text-xl font-semibold mb-4">Regional Trade Share</h2>
|
| 81 |
+
<div id="tradeShareChart" class="h-96"></div>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="bg-white p-6 rounded-lg shadow">
|
| 84 |
+
<h2 class="text-xl font-semibold mb-4">Port System Comparison</h2>
|
| 85 |
+
<div id="portComparisonChart" class="h-96"></div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
| 90 |
+
<div class="bg-white p-6 rounded-lg shadow">
|
| 91 |
+
<h2 class="text-xl font-semibold mb-4">Asia vs Americas Index</h2>
|
| 92 |
+
<div id="tradeIndexChart" class="h-96"></div>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="bg-white p-6 rounded-lg shadow">
|
| 95 |
+
<h2 class="text-xl font-semibold mb-4">Cargo Composition</h2>
|
| 96 |
+
<div id="cargoChart" class="h-96"></div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<!-- Signals Section -->
|
| 101 |
+
<div class="bg-white p-6 rounded-lg shadow mb-8">
|
| 102 |
+
<h2 class="text-xl font-semibold mb-4">Signals & Annotations</h2>
|
| 103 |
+
<div class="space-y-4">
|
| 104 |
+
<div class="p-4 bg-red-50 border-l-4 border-red-500">
|
| 105 |
+
<div class="flex items-start">
|
| 106 |
+
<i data-feather="alert-triangle" class="text-red-500 mr-3"></i>
|
| 107 |
+
<div>
|
| 108 |
+
<h3 class="font-medium text-red-800">Structural Shift Detected</h3>
|
| 109 |
+
<p class="text-red-600 text-sm">Gulf Coast ports showing 12.3% sustained increase in Americas trade share over past 8 months</p>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="p-4 bg-blue-50 border-l-4 border-blue-500">
|
| 114 |
+
<div class="flex items-start">
|
| 115 |
+
<i data-feather="info" class="text-blue-500 mr-3"></i>
|
| 116 |
+
<div>
|
| 117 |
+
<h3 class="font-medium text-blue-800">Policy Impact</h3>
|
| 118 |
+
<p class="text-blue-600 text-sm">New infrastructure investments in Florida ports showing early signs of increased capacity</p>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
|
| 126 |
+
<script src="script.js"></script>
|
| 127 |
+
<script src="script.js"></script>
|
| 128 |
+
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
|
| 129 |
+
</body>
|
| 130 |
+
</html>
|
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Mock data structure for port trade analytics
|
| 2 |
+
const portData = {
|
| 3 |
+
west: {
|
| 4 |
+
imports: {
|
| 5 |
+
asia: [1200000, 1250000, 1300000, 1180000, 1220000, 1150000],
|
| 6 |
+
americas: [450000, 470000, 490000, 510000, 530000, 550000],
|
| 7 |
+
europe: [300000, 310000, 320000, 315000, 325000, 330000],
|
| 8 |
+
other: [150000, 155000, 160000, 158000, 162000, 165000]
|
| 9 |
+
},
|
| 10 |
+
exports: {
|
| 11 |
+
asia: [800000, 820000, 840000, 830000, 850000, 860000],
|
| 12 |
+
americas: [350000, 370000, 390000, 410000, 430000, 450000],
|
| 13 |
+
europe: [250000, 260000, 270000, 265000, 275000, 280000],
|
| 14 |
+
other: [100000, 105000, 110000, 108000, 112000, 115000]
|
| 15 |
+
},
|
| 16 |
+
teus: [1800000, 1850000, 1900000, 1880000, 1920000, 1950000],
|
| 17 |
+
cargoMix: {
|
| 18 |
+
containerized: 58,
|
| 19 |
+
bulk: 25,
|
| 20 |
+
energy: 12,
|
| 21 |
+
roro: 5
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
gulf: {
|
| 25 |
+
imports: {
|
| 26 |
+
asia: [600000, 620000, 640000, 630000, 650000, 660000],
|
| 27 |
+
americas: [550000, 570000, 590000, 610000, 630000, 650000],
|
| 28 |
+
europe: [200000, 210000, 220000, 215000, 225000, 230000],
|
| 29 |
+
other: [100000, 105000, 110000, 108000, 112000, 115000]
|
| 30 |
+
},
|
| 31 |
+
exports: {
|
| 32 |
+
asia: [400000, 420000, 440000, 430000, 450000, 460000],
|
| 33 |
+
americas: [450000, 470000, 490000, 510000, 530000, 550000],
|
| 34 |
+
europe: [150000, 160000, 170000, 165000, 175000, 180000],
|
| 35 |
+
other: [80000, 85000, 90000, 88000, 92000, 95000]
|
| 36 |
+
},
|
| 37 |
+
teus: [900000, 920000, 950000, 940000, 960000, 980000],
|
| 38 |
+
cargoMix: {
|
| 39 |
+
containerized: 35,
|
| 40 |
+
bulk: 30,
|
| 41 |
+
energy: 30,
|
| 42 |
+
roro: 5
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
florida: {
|
| 46 |
+
imports: {
|
| 47 |
+
asia: [300000, 320000, 340000, 330000, 350000, 360000],
|
| 48 |
+
americas: [650000, 670000, 690000, 710000, 730000, 750000],
|
| 49 |
+
europe: [150000, 160000, 170000, 165000, 175000, 180000],
|
| 50 |
+
other: [70000, 75000, 80000, 78000, 82000, 85000]
|
| 51 |
+
},
|
| 52 |
+
exports: {
|
| 53 |
+
asia: [200000, 220000, 240000, 230000, 250000, 260000],
|
| 54 |
+
americas: [550000, 570000, 590000, 610000, 630000, 650000],
|
| 55 |
+
europe: [100000, 110000, 120000, 115000, 125000, 130000],
|
| 56 |
+
other: [50000, 55000, 60000, 58000, 62000, 65000]
|
| 57 |
+
},
|
| 58 |
+
teus: [600000, 620000, 650000, 640000, 660000, 680000],
|
| 59 |
+
cargoMix: {
|
| 60 |
+
containerized: 45,
|
| 61 |
+
bulk: 20,
|
| 62 |
+
energy: 15,
|
| 63 |
+
roro: 20
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
// Initialize all charts
|
| 69 |
+
function initializeCharts() {
|
| 70 |
+
updateTradeShareChart('west');
|
| 71 |
+
updatePortComparisonChart();
|
| 72 |
+
updateTradeIndexChart();
|
| 73 |
+
updateCargoChart('west');
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Trade Share Chart (Stacked Area)
|
| 77 |
+
function updateTradeShareChart(portGroup) {
|
| 78 |
+
const data = portData[portGroup];
|
| 79 |
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
|
| 80 |
+
|
| 81 |
+
const trace1 = {
|
| 82 |
+
x: months,
|
| 83 |
+
y: data.imports.asia,
|
| 84 |
+
name: 'Asia Imports',
|
| 85 |
+
stackgroup: 'one',
|
| 86 |
+
line: {color: '#3b82f6'},
|
| 87 |
+
fillcolor: 'rgba(59, 130, 246, 0.5)'
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
const trace2 = {
|
| 91 |
+
x: months,
|
| 92 |
+
y: data.imports.americas,
|
| 93 |
+
name: 'Americas Imports',
|
| 94 |
+
stackgroup: 'one',
|
| 95 |
+
line: {color: '#10b981'},
|
| 96 |
+
fillcolor: 'rgba(16, 185, 129, 0.5)'
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
const trace3 = {
|
| 100 |
+
x: months,
|
| 101 |
+
y: data.imports.europe,
|
| 102 |
+
name: 'Europe Imports',
|
| 103 |
+
stackgroup: 'one',
|
| 104 |
+
line: {color: '#6366f1'},
|
| 105 |
+
fillcolor: 'rgba(99, 102, 241, 0.5)'
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
const trace4 = {
|
| 109 |
+
x: months,
|
| 110 |
+
y: data.imports.other,
|
| 111 |
+
name: 'Other Imports',
|
| 112 |
+
stackgroup: 'one',
|
| 113 |
+
line: {color: '#8b5cf6'},
|
| 114 |
+
fillcolor: 'rgba(139, 92, 246, 0.5)'
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
const layout = {
|
| 118 |
+
title: 'Regional Import Share',
|
| 119 |
+
showlegend: true,
|
| 120 |
+
legend: {orientation: 'h'},
|
| 121 |
+
margin: {t: 30, l: 40, r: 30, b: 40},
|
| 122 |
+
hovermode: 'closest'
|
| 123 |
+
};
|
| 124 |
+
|
| 125 |
+
Plotly.newPlot('tradeShareChart', [trace1, trace2, trace3, trace4], layout);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Port Comparison Chart (Bar)
|
| 129 |
+
function updatePortComparisonChart() {
|
| 130 |
+
const westTotal = portData.west.imports.asia.reduce((a,b) => a+b, 0) +
|
| 131 |
+
portData.west.imports.americas.reduce((a,b) => a+b, 0);
|
| 132 |
+
const gulfTotal = portData.gulf.imports.asia.reduce((a,b) => a+b, 0) +
|
| 133 |
+
portData.gulf.imports.americas.reduce((a,b) => a+b, 0);
|
| 134 |
+
const floridaTotal = portData.florida.imports.asia.reduce((a,b) => a+b, 0) +
|
| 135 |
+
portData.florida.imports.americas.reduce((a,b) => a+b, 0);
|
| 136 |
+
|
| 137 |
+
const trace1 = {
|
| 138 |
+
x: ['West Coast', 'Gulf Coast', 'Florida'],
|
| 139 |
+
y: [westTotal, gulfTotal, floridaTotal],
|
| 140 |
+
name: 'Total Tonnage',
|
| 141 |
+
type: 'bar',
|
| 142 |
+
marker: {color: '#3b82f6'}
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
const trace2 = {
|
| 146 |
+
x: ['West Coast', 'Gulf Coast', 'Florida'],
|
| 147 |
+
y: [
|
| 148 |
+
portData.west.teus.reduce((a,b) => a+b, 0)/6,
|
| 149 |
+
portData.gulf.teus.reduce((a,b) => a+b, 0)/6,
|
| 150 |
+
portData.florida.teus.reduce((a,b) => a+b, 0)/6
|
| 151 |
+
],
|
| 152 |
+
name: 'Avg TEUs (Monthly)',
|
| 153 |
+
type: 'bar',
|
| 154 |
+
marker: {color: '#10b981'}
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
const layout = {
|
| 158 |
+
title: 'Port System Comparison',
|
| 159 |
+
barmode: 'group',
|
| 160 |
+
margin: {t: 30, l: 40, r: 30, b: 40},
|
| 161 |
+
hovermode: 'closest'
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
Plotly.newPlot('portComparisonChart', [trace1, trace2], layout);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
// Asia vs Americas Index (Line)
|
| 168 |
+
function updateTradeIndexChart() {
|
| 169 |
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
|
| 170 |
+
|
| 171 |
+
// Calculate index (Asia Imports / Americas Imports)
|
| 172 |
+
const westIndex = portData.west.imports.asia.map((val, i) =>
|
| 173 |
+
(val / portData.west.imports.americas[i]).toFixed(2));
|
| 174 |
+
|
| 175 |
+
const gulfIndex = portData.gulf.imports.asia.map((val, i) =>
|
| 176 |
+
(val / portData.gulf.imports.americas[i]).toFixed(2));
|
| 177 |
+
|
| 178 |
+
const floridaIndex = portData.florida.imports.asia.map((val, i) =>
|
| 179 |
+
(val / portData.florida.imports.americas[i]).toFixed(2));
|
| 180 |
+
|
| 181 |
+
const trace1 = {
|
| 182 |
+
x: months,
|
| 183 |
+
y: westIndex,
|
| 184 |
+
name: 'West Coast',
|
| 185 |
+
type: 'scatter',
|
| 186 |
+
line: {color: '#3b82f6', width: 3}
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
const trace2 = {
|
| 190 |
+
x: months,
|
| 191 |
+
y: gulfIndex,
|
| 192 |
+
name: 'Gulf Coast',
|
| 193 |
+
type: 'scatter',
|
| 194 |
+
line: {color: '#ef4444', width: 3}
|
| 195 |
+
};
|
| 196 |
+
|
| 197 |
+
const trace3 = {
|
| 198 |
+
x: months,
|
| 199 |
+
y: floridaIndex,
|
| 200 |
+
name: 'Florida',
|
| 201 |
+
type: 'scatter',
|
| 202 |
+
line: {color: '#10b981', width: 3}
|
| 203 |
+
};
|
| 204 |
+
|
| 205 |
+
const layout = {
|
| 206 |
+
title: 'Asia vs Americas Trade Index',
|
| 207 |
+
yaxis: {title: 'Ratio (Asia/Americas)'},
|
| 208 |
+
margin: {t: 30, l: 40, r: 30, b: 40},
|
| 209 |
+
hovermode: 'closest'
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
Plotly.newPlot('tradeIndexChart', [trace1, trace2, trace3], layout);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// Cargo Composition Chart (Pie)
|
| 216 |
+
function updateCargoChart(portGroup) {
|
| 217 |
+
const data = portData[portGroup].cargoMix;
|
| 218 |
+
|
| 219 |
+
const trace = {
|
| 220 |
+
values: [data.containerized, data.bulk, data.energy, data.roro],
|
| 221 |
+
labels: ['Containerized', 'Bulk', 'Energy', 'Ro-Ro'],
|
| 222 |
+
type: 'pie',
|
| 223 |
+
marker: {
|
| 224 |
+
colors: ['#3b82f6', '#10b981', '#f59e0b', '#6366f1']
|
| 225 |
+
}
|
| 226 |
+
};
|
| 227 |
+
|
| 228 |
+
const layout = {
|
| 229 |
+
title: 'Cargo Composition',
|
| 230 |
+
margin: {t: 30, l: 0, r: 0, b: 0},
|
| 231 |
+
height: 380
|
| 232 |
+
};
|
| 233 |
+
|
| 234 |
+
Plotly.newPlot('cargoChart', [trace], layout);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// Event listeners
|
| 238 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 239 |
+
initializeCharts();
|
| 240 |
+
|
| 241 |
+
document.querySelector('port-group-selector').addEventListener('portGroupChange', (e) => {
|
| 242 |
+
updateTradeShareChart(e.detail.group);
|
| 243 |
+
updateCargoChart(e.detail.group);
|
| 244 |
+
});
|
| 245 |
+
});
|
|
@@ -1,28 +1,45 @@
|
|
|
|
|
|
|
|
| 1 |
body {
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
margin-top: 0;
|
| 9 |
}
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
}
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
}
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 2 |
+
|
| 3 |
body {
|
| 4 |
+
font-family: 'Inter', sans-serif;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
/* Custom scrollbar */
|
| 8 |
+
::-webkit-scrollbar {
|
| 9 |
+
width: 8px;
|
| 10 |
+
height: 8px;
|
| 11 |
}
|
| 12 |
|
| 13 |
+
::-webkit-scrollbar-track {
|
| 14 |
+
background: #f1f1f1;
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
+
::-webkit-scrollbar-thumb {
|
| 18 |
+
background: #888;
|
| 19 |
+
border-radius: 4px;
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
+
::-webkit-scrollbar-thumb:hover {
|
| 23 |
+
background: #555;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
+
/* Animation for metric cards */
|
| 27 |
+
@keyframes fadeInUp {
|
| 28 |
+
from {
|
| 29 |
+
opacity: 0;
|
| 30 |
+
transform: translateY(20px);
|
| 31 |
+
}
|
| 32 |
+
to {
|
| 33 |
+
opacity: 1;
|
| 34 |
+
transform: translateY(0);
|
| 35 |
+
}
|
| 36 |
}
|
| 37 |
+
|
| 38 |
+
.metric-card {
|
| 39 |
+
animation: fadeInUp 0.5s ease-out;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Tooltip styling */
|
| 43 |
+
.plotly-notifier {
|
| 44 |
+
font-family: 'Inter', sans-serif !important;
|
| 45 |
+
}
|