aboutsummaryrefslogtreecommitdiff
path: root/internal/render/chart.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/render/chart.go')
-rw-r--r--internal/render/chart.go47
1 files changed, 40 insertions, 7 deletions
diff --git a/internal/render/chart.go b/internal/render/chart.go
index cc20ca9..ce41364 100644
--- a/internal/render/chart.go
+++ b/internal/render/chart.go
@@ -28,7 +28,9 @@ type column struct {
// hourly points and would otherwise wrap into mush.
func (x ctx) chart(v View) []string {
height := x.cfg.GraphHeight
- const gutter = 5 // "NN°" label plus the axis rule
+ // Wide enough for the widest label the axis can produce -- "16.0°" when the
+ // range is too narrow for whole degrees -- plus the axis rule.
+ const gutter = 6
cols := x.width - gutter - 1
if cols < 8 {
cols = 8
@@ -82,18 +84,25 @@ func (x ctx) chart(v View) []string {
}
}
+ // Whole degrees cannot label a narrow range: a span of 1.2 degrees makes the
+ // middle and bottom rows both read "15", which says the two rows are the
+ // same temperature when they are not. Below the threshold, use a decimal.
+ // Falls back to whole degrees if a decimal label would not fit the gutter,
+ // which only happens well below freezing.
+ axisLabel := tempLabeller(lo, hi, gutter-1)
+
out := []string{""}
for r := 0; r < height; r++ {
full := (height - r) * 2
value := lo + (hi-lo)*float64(height-1-r)/float64(height-1)
- label := " "
+ label := strings.Repeat(" ", gutter-1)
switch {
case r == 0:
- label = x.c(TempStyle(x.celsius(hi)), PadLeft(fmt.Sprintf("%d°", Deg(hi)), 4))
+ label = x.c(TempStyle(x.celsius(hi)), PadLeft(axisLabel(hi), gutter-1))
case r == height-1:
- label = x.c(TempStyle(x.celsius(lo)), PadLeft(fmt.Sprintf("%d°", Deg(lo)), 4))
+ label = x.c(TempStyle(x.celsius(lo)), PadLeft(axisLabel(lo), gutter-1))
case height >= 5 && r == height/2:
- label = x.c(TempStyle(x.celsius(value)), PadLeft(fmt.Sprintf("%d°", Deg(value)), 4))
+ label = x.c(TempStyle(x.celsius(value)), PadLeft(axisLabel(value), gutter-1))
}
cells := make([]Cell, 0, len(drawn))
for _, d := range drawn {
@@ -109,7 +118,7 @@ func (x ctx) chart(v View) []string {
out = append(out, label+x.c(Dim, "│")+Paint(cells, x.c))
}
- note := fmt.Sprintf("%d-%d°", Deg(lo), Deg(hi))
+ note := axisLabel(lo) + "-" + axisLabel(hi)
if len(groups) < len(v.Rows) {
note += fmt.Sprintf(" %.0f%s", float64(len(v.Rows))/float64(len(groups)), x.cat.Word("h_per_col"))
}
@@ -124,7 +133,7 @@ func (x ctx) chart(v View) []string {
for i, d := range drawn {
series[i] = d.rain
}
- out = append(out, x.c(Dim, PadLeft(x.cat.Word("rain_row"), 4)+"│")+
+ out = append(out, x.c(Dim, PadLeft(x.cat.Word("rain_row"), gutter-1)+"│")+
x.c(Cyan, spark(series))+
x.c(Dim, fmt.Sprintf(" %s %.1fmm", x.cat.Word("max"), maxRain)))
}
@@ -203,3 +212,27 @@ func maxInt(a, b int) int {
func ceilDiv(a, b int) int { return (a + b - 1) / b }
var _ = openmeteo.Row{}
+
+// minSpanForWholeDegrees is the range below which whole-degree axis labels stop
+// distinguishing rows. With five rows a 2-degree span puts adjacent labels half
+// a degree apart, which still rounds to distinct values; below that they
+// collide.
+const minSpanForWholeDegrees = 2.0
+
+// tempLabeller returns a formatter for the y-axis, choosing precision from the
+// range so that labels stay distinct, and never exceeding maxWidth cells.
+func tempLabeller(lo, hi float64, maxWidth int) func(float64) string {
+ whole := func(v float64) string { return fmt.Sprintf("%d°", Deg(v)) }
+ if hi-lo >= minSpanForWholeDegrees {
+ return whole
+ }
+ decimal := func(v float64) string { return fmt.Sprintf("%.1f°", v) }
+ // A decimal label is two characters longer; if that will not fit, whole
+ // degrees are wrong but legible, which beats a sheared column.
+ for _, v := range []float64{lo, hi} {
+ if DisplayWidth(decimal(v)) > maxWidth {
+ return whole
+ }
+ }
+ return decimal
+}