diff options
Diffstat (limited to 'internal/render/chart.go')
| -rw-r--r-- | internal/render/chart.go | 47 |
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 +} |
