102 lines
2.4 KiB
Go
102 lines
2.4 KiB
Go
// 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())
|
|
}
|