fix(fires): paint aggregated heat cells warm by density, not by strongest FRP

Coloring each far-zoom cell with the ramp stop of its strongest detection
turned a burning region into a blue / teal / lime mosaic: neighbouring
cells flipped hue on one outlier and the cold stops did not read as fire.
Cells say where it burns, so they go back to the heat-normalized score
(density, night share, max FRP) on the warm end of the ramp — lime, amber,
red — with the cell card accent matching. Single detections keep the full
six-stop FRP ramp.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Fable 5.1 committed 2026-10-27 19:19:40 +04:00
1 parent 1dad6b658a
commit 887b261195
4 files changed
+39 -15

No files matched your search

+2 -1
View File
@@ -2,7 +2,8 @@
cold → hot ramp (blue · teal · lime · amber · orange · red at 0 · 5 · 20 ·
50 · 100 · 300 MW), and add a log-scale minimum-FRP slider whose track is the
ramp, an All / Nominal+ / High confidence switch and an "N of M shown"
counter to the layer row. The filter applies once before rendering, so
counter to the layer row. Far-zoom heat cells keep a warm density fill
(lime · amber · red). The filter applies once before rendering, so
markers, cards, heat cells, context and the top-fires query agree; share
links keep it (`w` token: `f`, `c`), and `get_active_fires` takes matching
`min_frp_mw` and `confidence` arguments. Default 0 MW / All (gekh, #1426).
+6 -3
View File
@@ -2058,9 +2058,12 @@ accessible name remains "Don't show this again". Control behavior is unchanged.
Every FIRMS detection is colored by fire radiative power on a six-stop cold →
hot ramp: blue from 0 MW, teal from 5, lime from 20, amber from 50, orange from
100 and red from 300 MW (`FRP_RAMP` in `src/data/firmsIntensity.js`). Heat
cells, glow sprites and card accents read the same ramp; confidence and
brightness do not affect the color.
100 and red from 300 MW (`FRP_RAMP` in `src/data/firmsIntensity.js`). Glow
sprites and fire-card accents read that ramp; confidence and brightness do
not affect the color. Aggregated heat cells (far zoom) instead take the warm
end of the ramp — lime, amber or red — from their heat-normalized score
(density, night share and strongest FRP), so a region reads as one field
rather than a per-cell mosaic; the cell card's accent matches its fill.
The Active Fires row carries a "Fire intensity (FRP)" slider on a log scale
whose track is painted with the ramp and ticked at the same six values (its
+28 -9
View File
@@ -137,15 +137,33 @@ export function createModel({
}
/**
* Fill color for an aggregated heat cell: the ramp color of its strongest
* detection, so cells, markers and the slider legend read on one scale.
* @param {number} maxFrp - Strongest FRP in the cell, megawatts.
* Severity stop for an aggregated heat cell from its heat-normalized score
* (density, night share and strongest FRP together). Cells say *where* it
* burns, so they stay on the warm end of the ramp — lime, amber, red — and
* read as one field instead of a per-cell mosaic; the cold stops belong to
* single detections, which say *how hard* each one burns.
* @param {number} normalized - 0..1 heat score (sqrt-normalized to the view).
* @returns {{name: string, minFrp: number, color: Cesium.Color}}
*/
function cellColorStop(normalized) {
const name =
normalized > 0.72 ? 'red' : normalized > 0.42 ? 'amber' : 'lime';
return (
DETECTION_COLOR_STOPS.find((stop) => stop.name === name) ||
DETECTION_COLOR_STOPS[0]
);
}
/**
* Fill color for an aggregated heat cell.
* @param {number} normalized - 0..1 heat score (see {@link cellColorStop}).
* @param {number} alpha - Fill alpha (heat-normalized by the caller).
* @returns {Cesium.Color}
*/
function heatColor(maxFrp, alpha) {
return frpColorStop(maxFrp).color.withAlpha(alpha);
function heatColor(normalized, alpha) {
return cellColorStop(normalized).color.withAlpha(alpha);
}
/**
@@ -476,14 +494,14 @@ export function createModel({
}
/**
* Cell card accent: the ramp stop of the cell's strongest detection,
* Cell card accent: the warm severity stop of the cell's heat score,
* mirroring heatColor.
* @param {number} maxFrp - Strongest FRP in the cell, megawatts.
* @param {number} normalized - 0..1 heat score.
* @returns {string} "r, g, b" accent string.
*/
function cellAccent(maxFrp) {
return accentForSeverity(frpColorStop(maxFrp).name);
function cellAccent(normalized) {
return accentForSeverity(cellColorStop(normalized).name);
}
/** Coordinate line like "30.512°N 75.831°E". */
@@ -590,6 +608,7 @@ export function createModel({
cellIntersectsBounds,
heatScore,
heatColor,
cellColorStop,
frpColorStop,
detectionColorStop,
frpPixelSize,
+3 -2
View File
@@ -144,7 +144,7 @@ export function createRendering({
const score = components.model.heatScore(cell);
const normalized = Math.min(1, Math.sqrt(score / maxScore));
const alpha = 0.16 + normalized * 0.5;
const color = components.model.heatColor(cell.maxFrp, alpha);
const color = components.model.heatColor(normalized, alpha);
const centerLon = cell.lonCell + lod.gridDegrees / 2;
const centerLat = cell.latCell + lod.gridDegrees / 2;
const position = Cesium.Cartesian3.fromDegrees(centerLon, centerLat, 0);
@@ -170,12 +170,13 @@ export function createRendering({
});
// Cells arrive heat-sorted, so candidate order doubles as label priority.
// Accent is resolved here because `normalized` only exists in this walk.
layerState._labelCandidates.push({
position,
cullPosition: components.model.cellCullPosition(centerLon, centerLat),
fire: null,
cell,
accent: components.model.cellAccent(cell.maxFrp),
accent: components.model.cellAccent(normalized),
});
}