// 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 }