Skip to content

Commit d2b1162

Browse files
committed
Add readme and fix a global display issue
1 parent 6ac442c commit d2b1162

3 files changed

Lines changed: 76 additions & 1 deletion

File tree

docs/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# AIS Port Dynamics — US Coastal Vessel Traffic 2024
2+
3+
Interactive visualization of U.S. vessel traffic for five major ports, built on the 2024 NOAA / U.S. Coast Guard AIS dataset.
4+
5+
---
6+
7+
## Running the site
8+
9+
The site is a fully static SPA — no build step required.
10+
11+
---
12+
13+
## Data
14+
15+
**Source:** [2024 USCG/NOAA AIS on Kaggle](https://www.kaggle.com/datasets/bwandowando/2024-us-coast-guard-noaa-ais-dataset)
16+
17+
The raw Parquet files are not included due to size. Pre-processed JSON files are provided and served directly by the site.
18+
19+
---
20+
21+
## Usage
22+
23+
- **Global view** — 60,000 sampled trajectories colored by speed. Use the date picker, time slider, and vessel type filter to narrow the dataset.
24+
- **Port view** — click a port marker or sidebar entry to zoom in and see activity charts, fleet breakdown, and anchor zones.
25+
- **Compare** — select a second port from the "Compare with" dropdown to view side-by-side charts.
26+
- **▶ Time / ▶ Months** — animate through the day or the year.
27+
28+
---
29+
30+
## Stack
31+
32+
Leaflet 1.9 · D3.js v7 · CartoDB Dark Matter tiles · leaflet.heat
33+
34+
---
35+
36+
## Data preprocessing scripts
37+
38+
The JSON files served by the site were generated by the following scripts in `data/`:
39+
40+
1. `generate_land_mask.py`
41+
2. `rebuild_slim.py`
42+
3. `build_port_trajectories.py`
43+
44+
These scripts are provided for reproducibility. **We do not recommend running them**, as they require the full raw Parquet dataset and may take several hours to complete.

docs/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@
157157
<button class="play-btn" id="play-hour-btn">▶ Time</button>
158158
<button class="play-btn" id="play-month-btn">▶ Months</button>
159159
</div>
160+
<div id="month-anim-label" style="display:none;font-size:11px;color:#8b949e;margin-top:4px;text-align:center">
161+
<span style="color:#f0883e;font-weight:600;font-size:13px" id="month-anim-name"></span>
162+
<span style="color:#8b949e"> 2024</span>
163+
</div>
160164
</div>
161165

162166
<!-- Navigation Pattern -->

docs/js/main.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ function loadGlobalTrajectoriesBackground() {
9191
.then(data => {
9292
State.globalTrajectories = data;
9393
console.log(`[INFO] Global overview loaded: ${data.length}`);
94-
if (!State.selectedPort) redrawTrajectories();
94+
if (!State.selectedPort) {
95+
redrawTrajectories();
96+
updateGlobalVesselStat();
97+
}
9598
})
9699
.catch(e => console.warn("[WARN] vessel_trajectories_global.json not found.", e));
97100
}
@@ -380,10 +383,27 @@ function initDateRangeInputs() {
380383
inputEnd.addEventListener("change", update);
381384
}
382385

386+
function _countGlobalVessels() {
387+
const trajs = getGlobalTrajectories().filter(t => {
388+
if (State.activeTypes && !State.activeTypes.has(t.vessel_type)) return false;
389+
if (!passesTimeFilter(t)) return false;
390+
if (!passesDateFilter(t)) return false;
391+
return true;
392+
});
393+
return new Set(trajs.map(t => t.mmsi)).size;
394+
}
395+
396+
function updateGlobalVesselStat() {
397+
if (!State.selectedPort && State.globalTrajectories.length > 0) {
398+
$("stat-vessels").textContent = fmt(_countGlobalVessels());
399+
}
400+
}
401+
383402
function _onFilterChange() {
384403
updateActivityCharts();
385404
updateFleetChart();
386405
if (State.selectedPort) renderPortDetail(State.selectedPort);
406+
else updateGlobalVesselStat();
387407
redrawTrajectories();
388408
}
389409

@@ -731,6 +751,8 @@ function initPlayButtons() {
731751
clearInterval(monthTimer); monthTimer = null;
732752
monthBtn.textContent = "▶ Months";
733753
monthBtn.classList.remove("playing");
754+
const lbl = $("month-anim-label");
755+
if (lbl) lbl.style.display = "none";
734756
}
735757

736758
// ▶ Time:按时间间隔步进
@@ -758,9 +780,14 @@ function initPlayButtons() {
758780
monthBtn.textContent = "⏹ Months";
759781
monthBtn.classList.add("playing");
760782

783+
const monthAnimLabel = $("month-anim-label");
784+
const monthAnimName = $("month-anim-name");
785+
if (monthAnimLabel) monthAnimLabel.style.display = "block";
786+
761787
monthTimer = setInterval(async () => {
762788
animMonth = (animMonth + 1) % 12;
763789
const [sVal, eVal] = MONTH_DATE_RANGES[animMonth];
790+
if (monthAnimName) monthAnimName.textContent = MONTH_NAMES[animMonth + 1];
764791

765792
// 更新输入框
766793
const inputStart = $("date-input-start");

0 commit comments

Comments
 (0)