web/sparkline: inline-SVG sparkline renderer (empty / single / multi)
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// Package sparkline renders inline SVG sparklines and trend
|
||||
// charts for the dashboard and host repo page. All output is
|
||||
// pure server-rendered SVG with no JavaScript, no external
|
||||
// stylesheet, and no client library.
|
||||
package sparkline
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RenderSparkline returns an inline SVG <svg> element of the
|
||||
// given size containing a single polyline normalised across the
|
||||
// full y-range of points. NaN entries break the polyline. With
|
||||
// fewer than two real points the SVG still renders but contains
|
||||
// only a faint baseline + an em-dash placeholder.
|
||||
func RenderSparkline(points []float64, width, height int) template.HTML {
|
||||
const pad = 2
|
||||
w := width
|
||||
h := height
|
||||
innerW := w - 2*pad
|
||||
innerH := h - 2*pad
|
||||
|
||||
var real []float64
|
||||
for _, p := range points {
|
||||
if !math.IsNaN(p) {
|
||||
real = append(real, p)
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b,
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" class="repo-sparkline" role="img" aria-label="repo size trend">`,
|
||||
w, h)
|
||||
|
||||
if len(real) < 2 {
|
||||
fmt.Fprintf(&b,
|
||||
`<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="currentColor" stroke-opacity="0.15" stroke-dasharray="2,2"/>`,
|
||||
pad, h/2, w-pad, h/2)
|
||||
fmt.Fprintf(&b,
|
||||
`<text x="%d" y="%d" text-anchor="middle" font-size="%d" fill="currentColor" fill-opacity="0.4">—</text>`,
|
||||
w/2, h/2+4, h-6)
|
||||
b.WriteString(`</svg>`)
|
||||
return template.HTML(b.String())
|
||||
}
|
||||
|
||||
min, max := real[0], real[0]
|
||||
for _, v := range real {
|
||||
if v < min {
|
||||
min = v
|
||||
}
|
||||
if v > max {
|
||||
max = v
|
||||
}
|
||||
}
|
||||
span := max - min
|
||||
if span == 0 {
|
||||
span = 1
|
||||
}
|
||||
|
||||
stepX := 0.0
|
||||
if len(points) > 1 {
|
||||
stepX = float64(innerW) / float64(len(points)-1)
|
||||
}
|
||||
|
||||
var seg strings.Builder
|
||||
flush := func() {
|
||||
if seg.Len() == 0 {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(&b,
|
||||
`<polyline fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" points="%s"/>`,
|
||||
strings.TrimSpace(seg.String()))
|
||||
seg.Reset()
|
||||
}
|
||||
for i, v := range points {
|
||||
if math.IsNaN(v) {
|
||||
flush()
|
||||
continue
|
||||
}
|
||||
x := float64(pad) + stepX*float64(i)
|
||||
y := float64(pad) + float64(innerH) - (v-min)/span*float64(innerH)
|
||||
fmt.Fprintf(&seg, "%.2f,%.2f ", x, y)
|
||||
}
|
||||
flush()
|
||||
|
||||
cur := real[len(real)-1]
|
||||
first := real[0]
|
||||
delta := cur - first
|
||||
sign := "+"
|
||||
if delta < 0 {
|
||||
sign = "-"
|
||||
delta = -delta
|
||||
}
|
||||
fmt.Fprintf(&b, `<title>current %.0f, %s%.0f over window</title>`, cur, sign, delta)
|
||||
|
||||
b.WriteString(`</svg>`)
|
||||
return template.HTML(b.String())
|
||||
}
|
||||
Reference in New Issue
Block a user