aboutsummaryrefslogtreecommitdiff
path: root/internal/render/scale.go
blob: a2eb2dcb383b332a258cb3bc088bba096d60b13e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package render

import "math"

// Temperature bands. The two ends are IMGW's own warning criteria, so a red
// temperature means the met office would issue a warning about it rather than
// that it looked hot to whoever wrote this:
//
//	Tmin <= -15   silny mroz, stopien 1   -> bright blue
//	Tmax >= 30    upal, stopien 1         -> red
//	Tmax >  35    the higher heat level   -> bright red
//
// The splits between (0, 10, 20) are round numbers, not thresholds from any
// source; they only subdivide the range nobody warns about.
var tempBands = []struct {
	below float64
	code  string
}{
	{0, Blue}, {10, Cyan}, {20, Green}, {30, Yellow},
}

// Deg rounds to whole degrees. Rounding through int conversion avoids "-0",
// which is arithmetically fine and visually wrong.
func Deg(t float64) int {
	return int(math.Round(t))
}

// TempStyle is the ANSI code for a temperature in Celsius.
//
// The warning edges are written out rather than folded into tempBands so they
// match IMGW's criteria exactly, inclusive and exclusive included.
//
// The value is rounded first, so the colour always matches the number printed
// beside it: otherwise -14.6 prints as "-15" in a different colour than a true
// -15 and looks like a rendering bug.
func TempStyle(celsius float64) string {
	t := float64(Deg(celsius))
	switch {
	case t <= -15:
		return BrightBlue
	case t > 35:
		return BrightRed
	case t >= 30:
		return Red
	}
	for _, b := range tempBands {
		if t < b.below {
			return b.code
		}
	}
	return Yellow
}

// Pollen bands in grains/m3, from Polish clinical sources. Each entry is an
// upper bound (exclusive) and a band key; a nil bound means "everything above".
//
//	grass   -- alergen.info.pl symptom table: 20 = first nasal symptoms in 25%
//	           of sufferers, 50 = symptoms in all tested, 65 = intensified in
//	           over 75%, 120 = dyspnoea after 30 minutes of exposure.
//	birch   -- mp.pl: 80 provokes symptoms in over 95% of allergics.
//	mugwort -- mp.pl: over 70 counts as high, intensified symptoms.
//
// Birch and mugwort have a single published anchor each, so they get a two-way
// split rather than four bands: their "low" is weaker evidence than grass's.
// Alder, olive and ragweed have no Polish threshold that could be sourced and
// are deliberately left unbanded rather than banded on a guess.
var pollenBands = map[string][]struct {
	below float64
	band  string
}{
	"grass":   {{20, "low"}, {50, "medium"}, {65, "high"}, {math.Inf(1), "very high"}},
	"birch":   {{80, "low"}, {math.Inf(1), "high"}},
	"mugwort": {{70, "low"}, {math.Inf(1), "high"}},
}

// PollenBand is the qualitative level for a count, or "" where no threshold
// exists for that species.
func PollenBand(species string, value float64) string {
	if value < 1 {
		return "none"
	}
	bands, ok := pollenBands[species]
	if !ok {
		return ""
	}
	for _, b := range bands {
		if value < b.below {
			return b.band
		}
	}
	return ""
}