package dash
import (
"fmt"
"strings"
)
type chartSeries struct {
name string
color string
data []float64
area bool
}
type chartOptions struct {
unit string
threshold *float64
labels []string
}
func renderChartSVG(series []chartSeries, opts chartOptions) string {
const w, h = 560.0, 190.0
const l, r, t, b = 46.0, 8.0, 10.0, 22.0
if len(series) == 0 || len(series[0].data) < 2 {
return flatChart(w, h, l, r, t, b)
}
n := len(series[0].data)
min, max := computeRange(series, opts.threshold)
if min > 0 {
min = 0
}
if max == min {
max = min + 1
}
span := max - min
px := func(i int) float64 { return l + float64(i)*(w-l-r)/float64(n-1) }
py := func(v float64) float64 { return t + (h-t-b)*(1-(v-min)/span) }
var s strings.Builder
for g := 0; g <= 4; g++ {
v := min + span*float64(g)/4
y := py(v)
fmt.Fprintf(&s, ``, l, y, w-r, y)
fmt.Fprintf(&s, `%s`, l-6, y+3, formatChartValue(v, opts.unit))
}
for _, i := range []int{0, n / 2, n - 1} {
fmt.Fprintf(&s, `%s`, px(i), h-6, xLabel(opts, i, n))
}
if opts.threshold != nil {
y := py(*opts.threshold)
fmt.Fprintf(&s, ``, l, y, w-r, y)
}
for _, se := range series {
var d strings.Builder
for i, v := range se.data {
if i == 0 {
fmt.Fprintf(&d, "M%.1f %.1f", px(i), py(v))
} else {
fmt.Fprintf(&d, " L%.1f %.1f", px(i), py(v))
}
}
if se.area {
fmt.Fprintf(&s, ``, d.String(), px(n-1), py(0), px(0), py(0), se.color)
}
fmt.Fprintf(&s, ``, d.String(), se.color)
}
return fmt.Sprintf(``, w, h, s.String())
}
func flatChart(w, h, l, r, t, b float64) string {
return fmt.Sprintf(``, w, h, l, (t + (h-t-b)/2), w-r, (t + (h-t-b)/2))
}
func computeRange(series []chartSeries, threshold *float64) (float64, float64) {
min, max := 1e9, -1e9
for _, se := range series {
for _, v := range se.data {
if v < min {
min = v
}
if v > max {
max = v
}
}
}
if threshold != nil && *threshold > max {
max = *threshold * 1.15
}
return min, max
}
func xLabel(opts chartOptions, i, n int) string {
if i < len(opts.labels) {
return opts.labels[i]
}
return fmt.Sprintf("-%dm", n-1-i)
}
func formatChartValue(v float64, unit string) string {
switch unit {
case "bytes":
return chartFormatBytes(int64(v))
case "bps":
return chartFormatBytes(int64(v)) + "/s"
case "ms":
return fmt.Sprintf("%.0f ms", v)
case "pct":
return fmt.Sprintf("%.0f%%", v)
default:
if v >= 1000 {
return fmt.Sprintf("%.1fk", v/1000)
}
if v == float64(int64(v)) {
return fmt.Sprintf("%d", int64(v))
}
return fmt.Sprintf("%.1f", v)
}
}
func chartFormatBytes(b int64) string {
const u = 1024
if b < u {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(u), 0
for n := b / u; n >= u; n /= u {
div *= u
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
}
func renderLegend(series []chartSeries) string {
var b strings.Builder
b.WriteString(`
`)
for _, se := range series {
fmt.Fprintf(&b, `%s`, se.color, se.name)
}
b.WriteString(`
`)
return b.String()
}
func sampleTimes(n int) []string {
out := make([]string, n)
for i := 0; i < n; i++ {
m := -(n - 1 - i)
if m == 0 {
out[i] = "now"
} else {
out[i] = fmt.Sprintf("%dm", m)
}
}
return out
}
func seriesFromSamples(samples []metricsSample) []float64 {
out := make([]float64, len(samples))
for i, s := range samples {
if v, ok := s.values[""]; ok {
out[i] = v
}
}
return out
}
func timeLabels(samples []metricsSample) []string {
out := make([]string, len(samples))
for i, s := range samples {
out[i] = s.t.Format("15:04")
}
return out
}