Skip to content

Commit 896b766

Browse files
committed
Add two-port comparison views
1 parent e882a5c commit 896b766

5 files changed

Lines changed: 277 additions & 68 deletions

File tree

docs/css/style.css

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ html, body { height: 100%; font-family: 'Inter', -apple-system, BlinkMacSystemFo
5454
}
5555
.port-btn:hover { background: var(--bg-hover); }
5656
.port-btn.active { background: rgba(88,166,255,0.12); border-color: var(--accent); }
57+
.port-btn.compare { background: rgba(255,123,114,0.10); border-color: rgba(255,123,114,0.65); }
5758
.port-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
5859
.port-info { flex: 1; min-width: 0; }
5960
.port-name { font-weight: 500; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
6061
.port-meta { font-size: 11px; color: var(--text-sec); }
6162
.port-chevron { color: var(--text-sec); font-size: 10px; }
6263
.port-btn.active .port-chevron { color: var(--accent); }
64+
.port-btn.compare .port-chevron { color: #ff7b72; }
6365

6466
.filter-row { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
6567
.filter-label { font-size: 11px; color: var(--text-sec); display: flex; justify-content: space-between; }
@@ -130,10 +132,14 @@ input[type="range"] {
130132
.rp-section:last-child { border-bottom: none; }
131133

132134
.chart-wrap { width: 100%; position: relative; }
133-
#chart-hourly { height: 110px; width: 100%; display: block; }
134-
#chart-sog { height: 90px; width: 100%; display: block; }
135-
#chart-types { height: 160px; width: 100%; display: block; }
136-
#chart-status { height: 130px; width: 100%; display: block; }
135+
#chart-hourly,
136+
#chart-hourly-compare { height: 110px; width: 100%; display: block; }
137+
#chart-sog,
138+
#chart-sog-compare { height: 90px; width: 100%; display: block; }
139+
#chart-types,
140+
#chart-types-compare { height: 160px; width: 100%; display: block; }
141+
#chart-status,
142+
#chart-status-compare { height: 130px; width: 100%; display: block; }
137143

138144

139145
.vessel-popup { min-width: 160px; }
@@ -184,6 +190,30 @@ input[type="range"] {
184190
.pdc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
185191
.pdc-dot { width: 12px; height: 12px; border-radius: 50%; }
186192
.pdc-name { font-weight: 700; font-size: 13px; }
193+
.pdc-stat-rows { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
194+
.pdc-stat-row {
195+
background: var(--bg-dark); border: 1px solid var(--border);
196+
border-radius: 6px; padding: 8px;
197+
}
198+
.pdc-stat-row.compare { border-color: rgba(255,123,114,0.45); background: rgba(255,123,114,0.05); }
199+
.pdc-stat-row-head { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
200+
.pdc-row-title { min-width: 0; }
201+
.pdc-role {
202+
font-size: 9px; font-weight: 600; letter-spacing: .06em;
203+
text-transform: uppercase; color: var(--text-sec);
204+
}
205+
.pdc-row-name { font-size: 11px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
206+
.pdc-row-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 5px; }
207+
.pdc-row-metric { background: var(--bg-panel); border-radius: 5px; padding: 6px 7px; min-width: 0; }
208+
.pdc-compare-control { margin: 10px 0 12px; }
209+
.pdc-compare-control label {
210+
display: block; font-size: 10px; font-weight: 600; letter-spacing: .06em;
211+
text-transform: uppercase; color: var(--text-sec); margin-bottom: 5px;
212+
}
213+
.pdc-compare-control select {
214+
width: 100%; background: var(--bg-dark); border: 1px solid var(--border);
215+
border-radius: 5px; color: var(--text-prim); padding: 6px 8px; font-size: 11px;
216+
}
187217
.pdc-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 10px; }
188218
.pdc-stat { background: var(--bg-dark); border-radius: 5px; padding: 7px 9px; }
189219
.pdc-stat-val { font-size: 15px; font-weight: 700; line-height: 1; }
@@ -201,13 +231,20 @@ input[type="range"] {
201231
border-bottom: 1px solid var(--border);
202232
display: flex; align-items: center; gap: 6px;
203233
}
234+
.activity-context-bar.compare { border-top: 1px solid rgba(255,123,114,0.35); }
235+
.compare-chart-group { display: none; }
236+
.compare-chart-group.visible { display: block; }
204237
#activity-context-label,
205-
#fleet-context-label {
238+
#fleet-context-label,
239+
#activity-compare-context-label,
240+
#fleet-compare-context-label {
206241
font-size: 10px; font-weight: 600; letter-spacing: .05em;
207242
text-transform: uppercase; color: var(--text-sec);
208243
}
209244
#activity-context-label.port-mode,
210245
#fleet-context-label.port-mode { color: var(--accent); }
246+
#activity-compare-context-label,
247+
#fleet-compare-context-label { color: #ff7b72; }
211248
.chart-scope-note {
212249
font-size: 9px; font-weight: 400; text-transform: none;
213250
letter-spacing: 0; color: var(--text-sec); opacity: .75;

docs/index.html

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,26 @@
132132
<div class="chart-wrap"><div id="chart-sog"></div></div>
133133
</div>
134134
<div class="rp-section">
135-
<div class="section-title">Navigation Status <span class="chart-scope-note" id="status-scope-note"></span></div>
135+
<div class="section-title">Navigation Status / Pattern <span class="chart-scope-note" id="status-scope-note"></span></div>
136136
<div class="chart-wrap"><div id="chart-status"></div></div>
137137
</div>
138+
<div class="compare-chart-group" id="activity-compare-group">
139+
<div class="activity-context-bar compare">
140+
<span id="activity-compare-context-label">Comparison port</span>
141+
</div>
142+
<div class="rp-section">
143+
<div class="section-title">Hourly Activity</div>
144+
<div class="chart-wrap"><div id="chart-hourly-compare"></div></div>
145+
</div>
146+
<div class="rp-section">
147+
<div class="section-title">Speed over Ground (knots)</div>
148+
<div class="chart-wrap"><div id="chart-sog-compare"></div></div>
149+
</div>
150+
<div class="rp-section">
151+
<div class="section-title">Navigation Pattern</div>
152+
<div class="chart-wrap"><div id="chart-status-compare"></div></div>
153+
</div>
154+
</div>
138155
</div>
139156
<div id="tab-fleet" class="tab-pane">
140157
<div id="fleet-context-bar" class="activity-context-bar">
@@ -144,6 +161,15 @@
144161
<div class="section-title">Unique Vessels by Type</div>
145162
<div class="chart-wrap"><div id="chart-types"></div></div>
146163
</div>
164+
<div class="compare-chart-group" id="fleet-compare-group">
165+
<div class="activity-context-bar compare">
166+
<span id="fleet-compare-context-label">Comparison port</span>
167+
</div>
168+
<div class="rp-section">
169+
<div class="section-title">Unique Vessels by Type</div>
170+
<div class="chart-wrap"><div id="chart-types-compare"></div></div>
171+
</div>
172+
</div>
147173
</div>
148174

149175
</aside>

docs/js/charts.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ const Charts = (() => {
66
d3.select(el).selectAll("*").remove();
77
}
88

9-
const CHART_HEIGHTS = { "chart-hourly": 110, "chart-sog": 90, "chart-types": 160, "chart-status": 130 };
9+
const CHART_HEIGHTS = {
10+
"chart-hourly": 110,
11+
"chart-hourly-compare": 110,
12+
"chart-sog": 90,
13+
"chart-sog-compare": 90,
14+
"chart-types": 160,
15+
"chart-types-compare": 160,
16+
"chart-status": 130,
17+
"chart-status-compare": 130,
18+
};
1019

1120
function getSize(el) {
1221
const r = el.getBoundingClientRect();
@@ -16,8 +25,9 @@ const Charts = (() => {
1625
return { w: Math.max(w, 180), h: Math.max(h, fallbackH) };
1726
}
1827

19-
function drawHourly(data, highlightHour = null) {
20-
const el = sel("chart-hourly");
28+
function drawHourly(data, highlightHour = null, targetId = "chart-hourly") {
29+
const el = sel(targetId);
30+
if (!el) return;
2131
clearSvg(el);
2232
const { w, h } = getSize(el);
2333
const mg = { top: 6, right: 6, bottom: 20, left: 32 };
@@ -58,8 +68,9 @@ const Charts = (() => {
5868
.selectAll("text").style("fill", "#8b949e").style("font-size", "10px");
5969
}
6070

61-
function drawSog(data) {
62-
const el = sel("chart-sog");
71+
function drawSog(data, targetId = "chart-sog") {
72+
const el = sel(targetId);
73+
if (!el) return;
6374
clearSvg(el);
6475
const { w, h } = getSize(el);
6576
const mg = { top: 4, right: 6, bottom: 18, left: 32 };
@@ -78,7 +89,8 @@ const Charts = (() => {
7889
.y0(ih).y1(d => y(d.count))
7990
.curve(d3.curveCatmullRom);
8091

81-
svg.append("linearGradient").attr("id", "sog-grad")
92+
const gradId = `sog-grad-${targetId}`;
93+
svg.append("linearGradient").attr("id", gradId)
8294
.attr("gradientUnits", "userSpaceOnUse").attr("x1", 0).attr("y1", 0).attr("x2", 0).attr("y2", ih)
8395
.selectAll("stop").data([
8496
{ offset: "0%", color: "#58a6ff", opacity: 0.5 },
@@ -88,7 +100,7 @@ const Charts = (() => {
88100
.attr("stop-color", d => d.color)
89101
.attr("stop-opacity", d => d.opacity);
90102

91-
svg.append("path").datum(data).attr("fill", "url(#sog-grad)").attr("d", area);
103+
svg.append("path").datum(data).attr("fill", `url(#${gradId})`).attr("d", area);
92104

93105
const line = d3.line().x(d => x(d.bin_center)).y(d => y(d.count)).curve(d3.curveCatmullRom);
94106
svg.append("path").datum(data).attr("fill", "none").attr("stroke", "#58a6ff")
@@ -101,8 +113,9 @@ const Charts = (() => {
101113
svg.selectAll(".domain").style("stroke", "#30363d");
102114
}
103115

104-
function drawVesselTypes(data, activeTypes = null) {
105-
const el = sel("chart-types");
116+
function drawVesselTypes(data, activeTypes = null, targetId = "chart-types") {
117+
const el = sel(targetId);
118+
if (!el) return;
106119
clearSvg(el);
107120
const { w, h } = getSize(el);
108121
const mg = { top: 4, right: 40, bottom: 6, left: 4 };
@@ -154,8 +167,9 @@ const Charts = (() => {
154167
});
155168
}
156169

157-
function drawStatus(data) {
158-
const el = sel("chart-status");
170+
function drawStatus(data, targetId = "chart-status", centerLabel = "Status") {
171+
const el = sel(targetId);
172+
if (!el) return;
159173
clearSvg(el);
160174
const { w, h } = getSize(el);
161175
const r = Math.min(w * 0.22, h * 0.42);
@@ -171,6 +185,10 @@ const Charts = (() => {
171185
"Restricted maneuverability": "#ffa657",
172186
"Not under command": "#ff7b72",
173187
"Undefined": "#6e7681",
188+
"Inbound": "#3fb950",
189+
"Outbound": "#2f81f7",
190+
"Anchored / Waiting": "#f0883e",
191+
"Coastal": "#bc8cff",
174192
};
175193

176194
const top = data.slice(0, 6);
@@ -188,7 +206,7 @@ const Charts = (() => {
188206
.attr("stroke", "#161b22").attr("stroke-width", 1.5)
189207
.attr("opacity", 0.9);
190208

191-
g.append("text").text("Status").attr("text-anchor", "middle")
209+
g.append("text").text(centerLabel).attr("text-anchor", "middle")
192210
.attr("y", -6).style("font-size", "10px").style("fill", "#8b949e");
193211
g.append("text").text(d3.format(".2s")(total)).attr("text-anchor", "middle")
194212
.attr("y", 8).style("font-size", "13px").style("font-weight", "700").style("fill", "#e6edf3");

0 commit comments

Comments
 (0)