package web
import (
"fmt"
"html/template"
"strings"
"time"
)
// Inline SVG, generated here.
//
// A chart is a shape, and a column of numbers makes the reader compute the
// shape themselves. Drawing it costs about a hundred lines and no dependency:
// a charting library would mean a JavaScript toolchain in CI, a build step, and
// a megabyte of bundle for what is fundamentally a list of rectangles.
//
// The output is marked template.HTML, so everything interpolated into it must
// be a number this file produced. No caller-supplied string is ever written
// into the markup except through escapeText.
// Series is one set of bars.
type Series struct {
Values []float64
Labels []string
// Colour is a CSS custom property name, so the chart follows the page's
// light and dark palettes instead of pinning its own.
Colour string
}
const (
chartW = 720.0
chartH = 120.0
chartPad = 18.0
)
// BarChart renders a series as bars with a baseline and a peak label.
//
// Empty and all-zero series render as an empty frame rather than nothing: a
// missing chart reads as a broken page, and a flat one reads as no traffic,
// which is the truth.
//
// unit is written after the peak as it is given, so a caller passes it in
// the page's language.
func BarChart(s Series, unit string) template.HTML { return english.BarChart(s, unit) }
// BarChart is the chart in this language: its peak, and the words it says
// when there is none.
func (l *Lang) BarChart(s Series, unit string) template.HTML {
if s.Colour == "" {
s.Colour = "--accent"
}
var b strings.Builder
fmt.Fprintf(&b, ``)
peak := l.T("chart.peak_unit", l.Compact(maxV), unit)
if maxV == 0 {
peak = l.T("chart.nothing_recorded")
}
return template.HTML(`
`)
}
// cssVar restricts what can reach a style attribute to names this package
// defines, so a colour can never become a way to inject CSS.
func cssVar(name string) string {
switch name {
case "--accent", "--ok", "--warn", "--bad",
// The chart palette, and the text tokens charts label themselves with.
// An allowlist rather than a check, because this value is interpolated
// into a style attribute and the set of things that belong there is
// small, known and worth writing down.
"--viz-1", "--viz-2", "--viz-3", "--viz-4", "--viz-grid",
"--fg", "--muted":
return name
default:
return "--accent"
}
}
// escapeText is the only path by which non-numeric content enters the markup.
func escapeText(s string) template.HTML {
return template.HTML(template.HTMLEscapeString(s)) //nolint:gosec // escaped on the line above
}
// compact renders a number the way a person reads one at a glance.
func compact(v float64) string { return english.Compact(v) }
// hourLabel is what a bar's tooltip says.
func hourLabel(t time.Time, v float64, unit string) string { return english.hourLabel(t, v, unit) }
// hourLabel names the day the way this language does: "Mon 15:04", "Pzt
// 15:04". unit is the caller's, in this language already.
func (l *Lang) hourLabel(t time.Time, v float64, unit string) string {
return l.T("chart.hour_label", l.Format(t.Local(), "Mon 15:04"), l.Compact(v), unit)
}