Some checks failed
secrets-guard / encrypted (push) Successful in 24s
shell-lint / shellcheck (push) Successful in 26s
Deploy frontend to LAN dev server / build (push) Successful in 2m11s
Build + push frontend image (Forgejo registry) / build-push (push) Successful in 2m20s
Build + push backend image (Forgejo registry) / build-push (push) Successful in 2m28s
Deploy backend to LAN dev server / build (push) Successful in 2m45s
PR build (required check) / changes (pull_request) Successful in 16s
secrets-guard / encrypted (pull_request) Successful in 16s
shell-lint / shellcheck (pull_request) Successful in 15s
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
Deploy frontend to LAN dev server / deploy (push) Successful in 39s
PR build (required check) / build-backend (pull_request) Successful in 1m21s
PR build (required check) / gate (pull_request) Successful in 3s
Deploy backend to LAN dev server / deploy (push) Failing after 2m31s
71 lines
3.2 KiB
Go
71 lines
3.2 KiB
Go
// 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
|
|
}
|