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. func BarChart(s Series, unit string) template.HTML { if s.Colour == "" { s.Colour = "--accent" } var b strings.Builder fmt.Fprintf(&b, ``, chartW, chartH) maxV := 0.0 for _, v := range s.Values { if v > maxV { maxV = v } } // Baseline first so bars sit on it. fmt.Fprintf(&b, ``, chartH-chartPad, chartW, chartH-chartPad) if n := len(s.Values); n > 0 && maxV > 0 { slot := chartW / float64(n) width := slot * 0.72 for i, v := range s.Values { h := (v / maxV) * (chartH - chartPad*2) if h < 1 && v > 0 { // A bar for a real value must be visible, or an hour with // traffic looks identical to one without. h = 1 } x := float64(i)*slot + (slot-width)/2 y := chartH - chartPad - h title := "" if i < len(s.Labels) { title = fmt.Sprintf("%s", escapeText(s.Labels[i])) } fmt.Fprintf(&b, `%s`, x, y, width, h, cssVar(s.Colour), title) } } b.WriteString(``) peak := fmt.Sprintf("peak %s %s", compact(maxV), unit) if maxV == 0 { peak = "nothing recorded" } return template.HTML(`
` + b.String() + //nolint:gosec // built above `` + string(escapeText(peak)) + `
`) } // 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": 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 { switch { case v >= 1e9: return fmt.Sprintf("%.1fB", v/1e9) case v >= 1e6: return fmt.Sprintf("%.1fM", v/1e6) case v >= 1e3: return fmt.Sprintf("%.1fk", v/1e3) case v == float64(int64(v)): return fmt.Sprintf("%d", int64(v)) default: return fmt.Sprintf("%.1f", v) } } // hourLabel is what a bar's tooltip says. func hourLabel(t time.Time, v float64, unit string) string { return fmt.Sprintf("%s — %s %s", t.Local().Format("Mon 15:04"), compact(v), unit) }