Homepage strip: Susquehanna stage + 48h flood forecast line; clamp map edge labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+51
-1
@@ -89,6 +89,10 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site);
|
|||||||
<span class="live-dot" id="knb-dot" aria-hidden="true" />
|
<span class="live-dot" id="knb-dot" aria-hidden="true" />
|
||||||
<span id="live-knb" />
|
<span id="live-knb" />
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sby-stats" class="live-stats" hidden>
|
||||||
|
<span class="live-dot" id="sby-dot" aria-hidden="true" />
|
||||||
|
<span id="live-sby" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<figure id="knb-map" class="knb-map" hidden>
|
<figure id="knb-map" class="knb-map" hidden>
|
||||||
<figcaption class="knb-map-head">
|
<figcaption class="knb-map-head">
|
||||||
@@ -374,7 +378,7 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site);
|
|||||||
.sort((a, b) => a.lon - b.lon);
|
.sort((a, b) => a.lon - b.lon);
|
||||||
top.forEach((r, i) => {
|
top.forEach((r, i) => {
|
||||||
const t = document.createElementNS(NS, 'text');
|
const t = document.createElementNS(NS, 'text');
|
||||||
t.setAttribute('x', String(px(r.lon)));
|
t.setAttribute('x', String(Math.min(594, Math.max(46, px(r.lon)))));
|
||||||
t.setAttribute('y', String(py(r.lat) + (i % 2 ? 20 : -11)));
|
t.setAttribute('y', String(py(r.lat) + (i % 2 ? 20 : -11)));
|
||||||
t.setAttribute('text-anchor', 'middle');
|
t.setAttribute('text-anchor', 'middle');
|
||||||
t.textContent = `${r.name.toLowerCase()} ${Math.round(r.wait_min)}m`;
|
t.textContent = `${r.name.toLowerCase()} ${Math.round(r.wait_min)}m`;
|
||||||
@@ -390,6 +394,52 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site);
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Homepage Susquehanna strip: live stage + the 48h quantile stage
|
||||||
|
// forecast from the sunbury-flood pipeline. Fail-closed.
|
||||||
|
const sbyRow = document.getElementById('sby-stats');
|
||||||
|
if (sbyRow) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://api.c0smere.net/sunbury/forecast');
|
||||||
|
if (!res.ok) throw new Error(String(res.status));
|
||||||
|
const f = await res.json();
|
||||||
|
const h48 =
|
||||||
|
f.horizons.find((h: any) => h.horizon_h === 48) ??
|
||||||
|
f.horizons[f.horizons.length - 1];
|
||||||
|
const maxProb = Math.max(
|
||||||
|
...f.horizons.map((h: any) => h.prob_above_flood),
|
||||||
|
);
|
||||||
|
document
|
||||||
|
.getElementById('sby-dot')!
|
||||||
|
.classList.add(
|
||||||
|
maxProb >= 0.3
|
||||||
|
? 'knb-dot-packed'
|
||||||
|
: maxProb >= 0.05
|
||||||
|
? 'knb-dot-typical'
|
||||||
|
: 'knb-dot-quiet',
|
||||||
|
);
|
||||||
|
const slot = document.getElementById('live-sby')!;
|
||||||
|
const put = (text: string, strong = false) => {
|
||||||
|
const el = document.createElement('span');
|
||||||
|
if (strong) el.className = 'v';
|
||||||
|
el.textContent = text;
|
||||||
|
slot.append(el);
|
||||||
|
};
|
||||||
|
put('susquehanna @ sunbury: ');
|
||||||
|
put(`${f.current_stage_ft} ft`, true);
|
||||||
|
put(' · 48h forecast: ');
|
||||||
|
put(`${h48.p50} ft`, true);
|
||||||
|
put(` (${h48.p10}–${h48.p90})`);
|
||||||
|
put(' · flood odds ');
|
||||||
|
put(maxProb < 0.005 ? '<1%' : `${Math.round(maxProb * 100)}%`, true);
|
||||||
|
sbyRow.hidden = false;
|
||||||
|
document.getElementById('live-strips')!.hidden = false;
|
||||||
|
} catch {
|
||||||
|
/* API down: row stays hidden */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Odometer: fetch totals once, then animate forward at the lifetime
|
// Odometer: fetch totals once, then animate forward at the lifetime
|
||||||
// average rate. Looks live, but deliberately never exposes current
|
// average rate. Looks live, but deliberately never exposes current
|
||||||
|
|||||||
Reference in New Issue
Block a user