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(`%s`, 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 }