carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1package report
2
3import (
4 "fmt"
5 "html"
6 "html/template"
7 "math"
8 "strings"
9)
10
11// Row is one app in a chart: a value, or a reason it has none.
12type Row struct {
13 Language string
14 Value float64
15 Parts []float64 // stacked charts only
16 Missing string
17 Suffix string // appended to the value label
18}
19
20// Chart is one horizontal bar chart, apps in fixed report order.
21type Chart struct {
22 Title, Note string
23 ID string // copy id stem: chart.<ID>.title and chart.<ID>.note
24 Format func(float64) string
25 Legend []string // stacked charts only
26 LegendIDs []string // their copy ids
27 Rows []Row
28}
29
30const (
31 chartW = 680
32 labelW = 150
33 rowH = 30
34 barH = 16
35 gap = 2
36 topPad = 8
37)
38
39// SVG draws the chart. Bars grow from one baseline, 16px thick, with a
40// 4px rounded data end; stacked segments are separated by a 2px gap; the
41// value sits at the bar's tip in text ink; native <title> tooltips stand
42// in for a hover layer, since the page carries no JavaScript.
43func (c Chart) SVG() template.HTML {
44 max, widest := 0.0, 0.0
45 for _, r := range c.Rows {
46 if r.Missing == "" && r.Value > max {
47 max = r.Value
48 }
49 if r.Missing == "" {
50 widest = math.Max(widest, textWidth(c.Format(r.Value)+r.Suffix))
51 }
52 }
53 // Bars share what's left after the longest value label, so no label
54 // runs off the edge.
55 fullBarW := float64(chartW-labelW) - widest - 12
56 h := topPad*2 + rowH*len(c.Rows)
57 var s strings.Builder
58 fmt.Fprintf(&s, `<svg class="chart" viewBox="0 0 %d %d" role="img" aria-label="%s">`, chartW, h, html.EscapeString(c.Title))
59 fmt.Fprintf(&s, `<line class="axis" x1="%d" x2="%d" y1="%d" y2="%d"/>`, labelW, labelW, topPad, h-topPad)
60 for i, r := range c.Rows {
61 y := topPad + i*rowH
62 mid := y + rowH/2
63 fmt.Fprintf(&s, `<text class="label" x="%d" y="%d">%s</text>`, labelW-10, mid+5, html.EscapeString(r.Language))
64 if r.Missing != "" {
65 fmt.Fprintf(&s, `<text class="missing" x="%d" y="%d">%s</text>`, labelW+8, mid+5, html.EscapeString(r.Missing))
66 continue
67 }
68 w := 0.0
69 if max > 0 {
70 w = r.Value / max * fullBarW
71 }
72 by := mid - barH/2
73 tip := html.EscapeString(r.Language + ": " + c.Format(r.Value))
74 if c.HasParts() && len(r.Parts) > 0 {
75 x := float64(labelW)
76 for k, part := range r.Parts {
77 pw := part / max * fullBarW
78 if k > 0 {
79 x += gap
80 pw -= gap
81 }
82 if pw > 0 {
83 last := k == len(r.Parts)-1
84 fmt.Fprintf(&s, `<path class="s%d" d="%s"><title>%s</title></path>`, k+1, barPath(x, float64(by), pw, last), html.EscapeString(fmt.Sprintf("%s: %s %s", r.Language, c.Legend[k], c.Format(part))))
85 }
86 x += math.Max(pw, 0)
87 }
88 } else if w > 0 {
89 fmt.Fprintf(&s, `<path class="s1" d="%s"><title>%s</title></path>`, barPath(labelW, float64(by), w, true), tip)
90 }
91 fmt.Fprintf(&s, `<text class="value" x="%.1f" y="%d">%s</text>`, float64(labelW)+w+6, mid+5, html.EscapeString(c.Format(r.Value)+r.Suffix))
92 }
93 s.WriteString(`</svg>`)
94 return template.HTML(s.String())
95}
96
97// barPath is a bar from (x,y) of width w: square at the baseline, and a
98// 4px rounded data end when roundEnd is set.
99func barPath(x, y, w float64, roundEnd bool) string {
100 r := math.Min(4, w/2)
101 if !roundEnd {
102 r = 0
103 }
104 return fmt.Sprintf("M%.1f %.1fH%.1fQ%.1f %.1f %.1f %.1fV%.1fQ%.1f %.1f %.1f %.1fH%.1fZ",
105 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)
106}
107
108func (c Chart) HasParts() bool { return len(c.Legend) > 0 }
109func (c Chart) Parts() []string { return c.Legend }
110
111// textWidth estimates the width in pixels of s in the charts' 13px sans,
112// generously, so labels sized with it never overflow.
113func textWidth(s string) float64 { return 7.6 * float64(len([]rune(s))) }
114