200 lines
4.7 KiB
Go
200 lines
4.7 KiB
Go
package history
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"time"
|
|
|
|
chart "github.com/wcharczuk/go-chart/v2"
|
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
|
)
|
|
|
|
// ChartOptions controls rendering of the node network PNG.
|
|
type ChartOptions struct {
|
|
Title string
|
|
Subtitle string
|
|
Width int
|
|
Height int
|
|
Percentile float64
|
|
}
|
|
|
|
// RenderNodeNetworkPNG returns a PNG of the node-wide Rx+Tx time series with
|
|
// the requested percentile drawn as a horizontal annotation line.
|
|
func RenderNodeNetworkPNG(points []NodeSamplePoint, opts ChartOptions) ([]byte, error) {
|
|
if opts.Width <= 0 {
|
|
opts.Width = 1280
|
|
}
|
|
if opts.Height <= 0 {
|
|
opts.Height = 640
|
|
}
|
|
if opts.Percentile <= 0 {
|
|
opts.Percentile = 95
|
|
}
|
|
if opts.Title == "" {
|
|
opts.Title = "Node network usage"
|
|
}
|
|
|
|
if len(points) < 2 {
|
|
return renderEmptyChart(opts, "insufficient history — need at least 2 samples")
|
|
}
|
|
|
|
xs := make([]time.Time, 0, len(points))
|
|
rxs := make([]float64, 0, len(points))
|
|
txs := make([]float64, 0, len(points))
|
|
totals := make([]float64, 0, len(points))
|
|
maxVal := 0.0
|
|
for _, p := range points {
|
|
xs = append(xs, p.Timestamp)
|
|
rxs = append(rxs, p.RxMbps)
|
|
txs = append(txs, p.TxMbps)
|
|
totals = append(totals, p.TotalMbps)
|
|
if p.TotalMbps > maxVal {
|
|
maxVal = p.TotalMbps
|
|
}
|
|
}
|
|
|
|
pct := PercentileMbps(points, opts.Percentile)
|
|
yMax := maxVal * 1.15
|
|
if pct*1.10 > yMax {
|
|
yMax = pct * 1.10
|
|
}
|
|
if yMax <= 0 {
|
|
yMax = 1
|
|
}
|
|
|
|
percentileSeries := chart.ContinuousSeries{
|
|
Name: fmt.Sprintf("P%.0f = %.1f Mbps", opts.Percentile, pct),
|
|
Style: chart.Style{
|
|
StrokeColor: drawing.ColorFromHex("e74c3c"),
|
|
StrokeWidth: 2.0,
|
|
StrokeDashArray: []float64{6, 4},
|
|
},
|
|
XValues: []float64{chart.TimeToFloat64(xs[0]), chart.TimeToFloat64(xs[len(xs)-1])},
|
|
YValues: []float64{pct, pct},
|
|
}
|
|
|
|
graph := chart.Chart{
|
|
Title: opts.Title,
|
|
TitleStyle: chart.Style{
|
|
FontSize: 16,
|
|
},
|
|
Width: opts.Width,
|
|
Height: opts.Height,
|
|
Background: chart.Style{
|
|
Padding: chart.Box{Top: 60, Left: 40, Right: 40, Bottom: 40},
|
|
FillColor: drawing.Color{
|
|
R: 0xf8, G: 0xf9, B: 0xfa, A: 0xff,
|
|
},
|
|
},
|
|
XAxis: chart.XAxis{
|
|
Style: chart.Style{FontSize: 9},
|
|
ValueFormatter: chart.TimeValueFormatterWithFormat("15:04:05\n02 Jan"),
|
|
},
|
|
YAxis: chart.YAxis{
|
|
Name: "Mbps",
|
|
Style: chart.Style{FontSize: 9},
|
|
Range: &chart.ContinuousRange{Min: 0, Max: yMax},
|
|
ValueFormatter: func(v any) string {
|
|
if f, ok := v.(float64); ok {
|
|
return formatMbps(f)
|
|
}
|
|
return ""
|
|
},
|
|
},
|
|
Series: []chart.Series{
|
|
chart.TimeSeries{
|
|
Name: "Total Rx+Tx",
|
|
Style: chart.Style{
|
|
StrokeColor: drawing.ColorFromHex("2d7dd2"),
|
|
StrokeWidth: 2.0,
|
|
FillColor: drawing.ColorFromHex("2d7dd2").WithAlpha(50),
|
|
},
|
|
XValues: xs,
|
|
YValues: totals,
|
|
},
|
|
chart.TimeSeries{
|
|
Name: "Rx",
|
|
Style: chart.Style{
|
|
StrokeColor: drawing.ColorFromHex("3cb371"),
|
|
StrokeWidth: 1.5,
|
|
},
|
|
XValues: xs,
|
|
YValues: rxs,
|
|
},
|
|
chart.TimeSeries{
|
|
Name: "Tx",
|
|
Style: chart.Style{
|
|
StrokeColor: drawing.ColorFromHex("ffa500"),
|
|
StrokeWidth: 1.5,
|
|
},
|
|
XValues: xs,
|
|
YValues: txs,
|
|
},
|
|
percentileSeries,
|
|
},
|
|
}
|
|
|
|
if opts.Subtitle != "" {
|
|
graph.Elements = []chart.Renderable{subtitleRenderable(opts.Subtitle)}
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
if err := graph.Render(chart.PNG, buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func subtitleRenderable(text string) chart.Renderable {
|
|
return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) {
|
|
r.SetFont(chartDefaults.GetFont())
|
|
r.SetFontColor(drawing.Color{R: 90, G: 90, B: 90, A: 0xff})
|
|
r.SetFontSize(10)
|
|
r.Text(text, cb.Left+10, cb.Top+30)
|
|
}
|
|
}
|
|
|
|
func renderEmptyChart(opts ChartOptions, note string) ([]byte, error) {
|
|
graph := chart.Chart{
|
|
Title: opts.Title,
|
|
Width: opts.Width,
|
|
Height: opts.Height,
|
|
Background: chart.Style{
|
|
Padding: chart.Box{Top: 60, Left: 40, Right: 40, Bottom: 40},
|
|
},
|
|
Series: []chart.Series{
|
|
chart.ContinuousSeries{
|
|
XValues: []float64{0, 1},
|
|
YValues: []float64{0, 0},
|
|
Style: chart.Style{
|
|
StrokeColor: drawing.ColorTransparent,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
graph.Elements = []chart.Renderable{
|
|
func(r chart.Renderer, cb chart.Box, cs chart.Style) {
|
|
r.SetFont(cs.GetFont())
|
|
r.SetFontColor(drawing.Color{R: 120, G: 120, B: 120, A: 0xff})
|
|
r.SetFontSize(14)
|
|
r.Text(note, cb.Left+20, cb.Top+cb.Height()/2)
|
|
},
|
|
}
|
|
buf := &bytes.Buffer{}
|
|
if err := graph.Render(chart.PNG, buf); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func formatMbps(v float64) string {
|
|
switch {
|
|
case v >= 1000:
|
|
return fmt.Sprintf("%.2f Gbps", v/1000)
|
|
case v >= 1:
|
|
return fmt.Sprintf("%.1f Mbps", v)
|
|
default:
|
|
return fmt.Sprintf("%.0f Kbps", v*1000)
|
|
}
|
|
}
|