thermograph/frontend/server/internal/format/bands.go

72 lines
3.2 KiB
Go
Raw Normal View History

// Percentile grading bands, ported EXACTLY from backend/data/grading.py —
// the cross-repo display contract both this domain's CLAUDE.md and the
// backend's call out. The backend is the source of truth for the tier names
// and thresholds; an off-by-one on any boundary paints a tier a different
// colour than the label the API returned. A test in bands_test.go re-parses
// the backend's grading.py (when the monorepo checkout is present) and
// asserts these tables have not drifted.
package format
// Band is one percentile tier: Threshold is the tier's LOWER bound; a tier
// spans [lower, next-lower) — lower-inclusive, upper-exclusive — except the
// TOP tier, which is strict (pct > threshold): see BandFor.
type Band struct {
Threshold float64
Label string
Class string
}
// TempBands maps a percentile to (grade label, css class) for temperature.
// Higher percentile = warmer. 9 symmetric tiers around the middle 40-60%
// "Normal", escalating to "Near Record" at both edges. Boundaries sit on
// multiples of 5 (plus the 1/99 record edges). Source of truth:
// backend/data/grading.py TEMP_BANDS.
var TempBands = []Band{
{99, "Near Record", "rec-hot"}, // >99 extreme high (danger)
{90, "Very High", "very-hot"}, // 90-99
{75, "High", "hot"}, // 75-90
{60, "Above Normal", "warm"}, // 60-75
{40, "Normal", "normal"}, // 40-60 (the middle)
{25, "Below Normal", "cool"}, // 25-40
{10, "Low", "cold"}, // 10-25
{1, "Very Low", "very-cold"}, // 1-10
{0, "Near Record", "rec-cold"}, // <1 extreme low (danger)
}
// RainBands grades precipitation among days with ANY rain (> 0) in the
// seasonal window — a "rain percentile". Rain is one-directional (heavier =
// more extreme), so these 8 tiers are sequential light->heavy, using the SAME
// cut points as temperature. Dry days (no rain at all) are handled separately
// (the "dry" class, colored by dry streak in the UI). Same [lower, upper)
// convention as TempBands. The eight tiers fill the wet-2..wet-9 colour ramp
// with no gap. Source of truth: backend/data/grading.py RAIN_BANDS.
var RainBands = []Band{
{99, "Extreme", "wet-9"}, // >99 heaviest rain for the season (darkest)
{95, "Severe", "wet-8"}, // 95-99 (top half of the old Very Heavy)
{90, "Very Heavy", "wet-7"}, // 90-95 (lower half)
{60, "Heavy", "wet-6"}, // 60-90
{40, "Typical", "wet-5"}, // 40-60
{25, "Brisk", "wet-4"}, // 25-40
{10, "Light", "wet-3"}, // 10-25
{0, "Trace", "wet-2"}, // <10 the lightest measurable rain
}
// BandFor places a percentile on a band table — the port of grading.py's
// _band. The top tier is strict (pct > its threshold): "Near Record" high
// means strictly beyond the 99th percentile — the top <1% — mirroring the
// strictly-below-1st bottom tier (its lower neighbor already catches
// pct >= 1). Everything in between stays lower-inclusive, upper-exclusive.
func BandFor(pct float64, bands []Band) (label, class string) {
top := bands[0]
if pct > top.Threshold {
return top.Label, top.Class
}
for _, b := range bands[1:] {
if pct >= b.Threshold {
return b.Label, b.Class
}
}
last := bands[len(bands)-1]
return last.Label, last.Class
}