diff --git a/weed/admin/dash/chart.go b/weed/admin/dash/chart.go
new file mode 100644
index 000000000..76c10aca7
--- /dev/null
+++ b/weed/admin/dash/chart.go
@@ -0,0 +1,174 @@
+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
+}