package report
import (
"fmt"
"html"
"html/template"
"math"
"strings"
)
// Row is one app in a chart: a value, or a reason it has none.
type Row struct {
Language string
Value float64
Parts []float64 // stacked charts only
Missing string
Suffix string // appended to the value label
}
// Chart is one horizontal bar chart, apps in fixed report order.
type Chart struct {
Title, Note string
ID string // copy id stem: chart..title and chart..note
Format func(float64) string
Legend []string // stacked charts only
LegendIDs []string // their copy ids
Rows []Row
}
const (
chartW = 680
labelW = 150
rowH = 30
barH = 16
gap = 2
topPad = 8
)
// SVG draws the chart. Bars grow from one baseline, 16px thick, with a
// 4px rounded data end; stacked segments are separated by a 2px gap; the
// value sits at the bar's tip in text ink; native tooltips stand
// in for a hover layer, since the page carries no JavaScript.
func (c Chart) SVG() template.HTML {
max, widest := 0.0, 0.0
for _, r := range c.Rows {
if r.Missing == "" && r.Value > max {
max = r.Value
}
if r.Missing == "" {
widest = math.Max(widest, textWidth(c.Format(r.Value)+r.Suffix))
}
}
// Bars share what's left after the longest value label, so no label
// runs off the edge.
fullBarW := float64(chartW-labelW) - widest - 12
h := topPad*2 + rowH*len(c.Rows)
var s strings.Builder
fmt.Fprintf(&s, ``)
return template.HTML(s.String())
}
// barPath is a bar from (x,y) of width w: square at the baseline, and a
// 4px rounded data end when roundEnd is set.
func barPath(x, y, w float64, roundEnd bool) string {
r := math.Min(4, w/2)
if !roundEnd {
r = 0
}
return fmt.Sprintf("M%.1f %.1fH%.1fQ%.1f %.1f %.1f %.1fV%.1fQ%.1f %.1f %.1f %.1fH%.1fZ",
x, y, x+w-r, x+w, y, x+w, y+r, y+barH-r, x+w, y+barH, x+w-r, y+barH, x)
}
func (c Chart) HasParts() bool { return len(c.Legend) > 0 }
func (c Chart) Parts() []string { return c.Legend }
// textWidth estimates the width in pixels of s in the charts' 13px sans,
// generously, so labels sized with it never overflow.
func textWidth(s string) float64 { return 7.6 * float64(len([]rune(s))) }