feat: discharge-driven river animation speed, replay stat tiles, hourly doc render
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 24s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 15s
Documentation / Validate Documentation (push) Failing after 10s
Documentation / Generate API Documentation (push) Successful in 11s
Documentation / Build Sphinx Documentation (push) Successful in 20s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 24s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 15s
Documentation / Validate Documentation (push) Failing after 10s
Documentation / Generate API Documentation (push) Successful in 11s
Documentation / Build Sphinx Documentation (push) Successful in 20s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 1s
Documentation / Documentation Summary (push) Successful in 3s
- river dash animation now flows at a speed continuously derived from each segment's discharge (period 260/(Q+45) s, clamped 0.5-5.5 s) via inline per-path animation-duration, so it updates live and per-frame during the replay (CSS speed classes removed - setStyle cannot change classes) - the replay drives the Combined discharge and Strongest flow stat tiles each frame (marked '2024 replay'), restored on finish - docs: hour-by-hour detection detail render (22-28 Sep 2024) showing the model alert at 24 Sep 01:00, flooding at 25 Sep 01:00, and the 24 h warning between them; embedded with commentary - Matrix alerts now link to https://water.buildfor.life/ (override via ALERT_DASHBOARD_URL), replacing the Grafana public dashboard link
This commit is contained in:
@@ -306,6 +306,16 @@ honestly: the predicted peak tops out ~0.4 m short of the actual 5.30 m record
|
|||||||
noisier on the recession limbs. The same model track drives the dashboard's
|
noisier on the recession limbs. The same model track drives the dashboard's
|
||||||
"Replay Oct 2024 flood" feature, so this chart can be watched live on the map.
|
"Replay Oct 2024 flood" feature, so this chart can be watched live on the map.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The hour-by-hour detail of the detection window shows the sequence exactly: the
|
||||||
|
predicted 24 h peak (amber) starts pulling away from the observed level late on
|
||||||
|
23 September as upstream gauges rise, the warning probability snaps from ~0 to
|
||||||
|
1.0 at **01:00 on 24 September**, and the river crosses 3.70 m at **01:00 on
|
||||||
|
25 September** — a clean 24-hour warning, delivered while the river in town
|
||||||
|
still looked normal at 2.8 m.
|
||||||
|
|
||||||
- **25 September cold start.** P.1's first warning crossing of the episode was
|
- **25 September cold start.** P.1's first warning crossing of the episode was
|
||||||
alerted **24–26 hours ahead**. This is the genuinely impressive case: the river
|
alerted **24–26 hours ahead**. This is the genuinely impressive case: the river
|
||||||
was in normal state, and the alert came from upstream routing alone.
|
was in normal state, and the alert came from upstream routing alone.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
+3
-3
@@ -155,9 +155,9 @@ class MatrixNotifier:
|
|||||||
if alert.message:
|
if alert.message:
|
||||||
message += f"\n**Details:** {alert.message}\n"
|
message += f"\n**Details:** {alert.message}\n"
|
||||||
|
|
||||||
# Add Grafana public dashboard link
|
# Add live dashboard link
|
||||||
grafana_url = "https://metrics.b4l.co.th/public-dashboards/655730aa044f44f49b355d01386018ca"
|
dashboard_url = os.getenv("ALERT_DASHBOARD_URL", "https://water.buildfor.life/")
|
||||||
message += f"\n📈 **View Dashboard:** {grafana_url}"
|
message += f"\n📈 **View Dashboard:** {dashboard_url}"
|
||||||
|
|
||||||
return self.send_message(message)
|
return self.send_message(message)
|
||||||
|
|
||||||
|
|||||||
+25
-14
@@ -116,12 +116,7 @@
|
|||||||
opacity: .36; animation: pulse 2.2s ease-out infinite;
|
opacity: .36; animation: pulse 2.2s ease-out infinite;
|
||||||
}
|
}
|
||||||
@keyframes pulse { 0% { transform: scale(.72); opacity: .55; } 75%,100% { transform: scale(1.35); opacity: 0; } }
|
@keyframes pulse { 0% { transform: scale(.72); opacity: .55; } 75%,100% { transform: scale(1.35); opacity: 0; } }
|
||||||
.flow-line { animation: riverMove 3s linear infinite; }
|
.flow-line { animation: riverMove 3s linear infinite; } /* per-segment duration set inline from discharge */
|
||||||
.flow-idle { animation-duration: 5.5s; }
|
|
||||||
.flow-slow { animation-duration: 3s; }
|
|
||||||
.flow-med { animation-duration: 1.9s; }
|
|
||||||
.flow-fast { animation-duration: 1.15s; }
|
|
||||||
.flow-surge { animation-duration: .7s; }
|
|
||||||
@keyframes riverMove { to { stroke-dashoffset: -40; } }
|
@keyframes riverMove { to { stroke-dashoffset: -40; } }
|
||||||
@media (prefers-reduced-motion: reduce) { .flow-line, .flow-marker::before { animation: none; } }
|
@media (prefers-reduced-motion: reduce) { .flow-line, .flow-marker::before { animation: none; } }
|
||||||
.line-swatch { width: 24px; height: 4px; border-radius: 2px; flex: none; }
|
.line-swatch { width: 24px; height: 4px; border-radius: 2px; flex: none; }
|
||||||
@@ -353,12 +348,18 @@
|
|||||||
return q == null ? 2.5 : Math.max(3, Math.min(9, 2.5 + Math.sqrt(Math.max(0, q)) * .38));
|
return q == null ? 2.5 : Math.max(3, Math.min(9, 2.5 + Math.sqrt(Math.max(0, q)) * .38));
|
||||||
}
|
}
|
||||||
|
|
||||||
function riverSpeedClass(q) {
|
// Continuous dash-animation period from discharge: calm water drifts (~5.5 s
|
||||||
if (q == null) return 'flow-idle';
|
// per cycle), a flood races (~0.5 s). Applied as inline animation-duration on
|
||||||
if (q < 25) return 'flow-slow';
|
// each SVG path so it can be updated live (setStyle cannot change classes).
|
||||||
if (q < 100) return 'flow-med';
|
function flowDuration(q) {
|
||||||
if (q < 250) return 'flow-fast';
|
if (q == null) return 5.5;
|
||||||
return 'flow-surge';
|
return Math.max(.5, Math.min(5.5, 260 / (Number(q) + 45)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFlowSpeed(layerGroup, flows) {
|
||||||
|
layerGroup.eachLayer((layer) => {
|
||||||
|
if (layer._path) layer._path.style.animationDuration = `${flowDuration(flows.get(layer.feature))}s`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function riverGauges(stations, readings) {
|
function riverGauges(stations, readings) {
|
||||||
@@ -387,12 +388,13 @@
|
|||||||
return {
|
return {
|
||||||
color: q == null ? '#69b7d0' : flowColor(q),
|
color: q == null ? '#69b7d0' : flowColor(q),
|
||||||
weight: riverWeight(q), opacity: .92, lineCap: 'round',
|
weight: riverWeight(q), opacity: .92, lineCap: 'round',
|
||||||
dashArray: '6 14', className: `flow-line ${riverSpeedClass(q)}`
|
dashArray: '6 14', className: 'flow-line'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}).addTo(state.map);
|
}).addTo(state.map);
|
||||||
flow.bringToBack();
|
flow.bringToBack();
|
||||||
casing.bringToBack();
|
casing.bringToBack();
|
||||||
|
applyFlowSpeed(flow, flowBySegment);
|
||||||
state.layers.push(casing, flow);
|
state.layers.push(casing, flow);
|
||||||
state.riverNet = riverNetwork;
|
state.riverNet = riverNetwork;
|
||||||
state.riverStations = stations;
|
state.riverStations = stations;
|
||||||
@@ -410,6 +412,7 @@
|
|||||||
const q = flows.get(f);
|
const q = flows.get(f);
|
||||||
return { color: q == null ? '#69b7d0' : flowColor(q), weight: riverWeight(q), opacity: .92 };
|
return { color: q == null ? '#69b7d0' : flowColor(q), weight: riverWeight(q), opacity: .92 };
|
||||||
});
|
});
|
||||||
|
applyFlowSpeed(state.riverFlow, flows);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStationMarker(code, discharge) {
|
function updateStationMarker(code, discharge) {
|
||||||
@@ -820,13 +823,21 @@
|
|||||||
const frame = Math.min(i, n - 1);
|
const frame = Math.min(i, n - 1);
|
||||||
const readings = new Map();
|
const readings = new Map();
|
||||||
let totalFlow = 0;
|
let totalFlow = 0;
|
||||||
|
let peakFlow = null;
|
||||||
Object.entries(data.stations).forEach(([code, series]) => {
|
Object.entries(data.stations).forEach(([code, series]) => {
|
||||||
const level = series.level[frame];
|
const level = series.level[frame];
|
||||||
const discharge = series.discharge[frame];
|
const discharge = series.discharge[frame];
|
||||||
readings.set(code, { water_level: level, discharge });
|
readings.set(code, { water_level: level, discharge });
|
||||||
updateStationMarker(code, discharge);
|
updateStationMarker(code, discharge);
|
||||||
if (discharge != null) totalFlow += discharge;
|
if (discharge != null) {
|
||||||
|
totalFlow += discharge;
|
||||||
|
if (!peakFlow || discharge > peakFlow.value) peakFlow = { code, value: discharge };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
// stat tiles follow the replay (restored by loadDashboard on finish)
|
||||||
|
$('total-flow').textContent = totalFlow.toLocaleString(undefined, { maximumFractionDigits: 1 });
|
||||||
|
$('peak-flow').textContent = peakFlow ? peakFlow.value.toFixed(1) : '—';
|
||||||
|
$('peak-station').textContent = peakFlow ? `${peakFlow.code} · m³/s · 2024 replay` : 'No discharge reported';
|
||||||
restyleRiver(readings);
|
restyleRiver(readings);
|
||||||
const p1Level = data.stations['P.1'] ? data.stations['P.1'].level[frame] : null;
|
const p1Level = data.stations['P.1'] ? data.stations['P.1'].level[frame] : null;
|
||||||
if (p1Level != null) state.p1Now = Number(p1Level);
|
if (p1Level != null) state.p1Now = Number(p1Level);
|
||||||
|
|||||||
Reference in New Issue
Block a user