sampler-fork/component/barchart/barchart.go

190 lines
3.8 KiB
Go
Raw Normal View History

package barchart
2019-02-19 04:07:32 +00:00
import (
"fmt"
2019-03-14 03:01:44 +00:00
ui "github.com/gizak/termui/v3"
2019-02-19 04:07:32 +00:00
rw "github.com/mattn/go-runewidth"
2019-03-13 03:19:19 +00:00
"github.com/sqshq/sampler/component"
"github.com/sqshq/sampler/config"
"github.com/sqshq/sampler/console"
2019-02-19 04:07:32 +00:00
"github.com/sqshq/sampler/data"
"image"
"math"
2019-02-19 04:07:32 +00:00
"strconv"
)
const (
2019-02-26 04:36:23 +00:00
barIndent int = 1
2019-02-19 04:07:32 +00:00
)
type BarChart struct {
2019-03-16 23:59:28 +00:00
*ui.Block
*data.Consumer
alert *data.Alert
2019-02-19 04:07:32 +00:00
bars []Bar
scale int
maxValue float64
2019-02-21 04:53:59 +00:00
count int64
palette console.Palette
2019-02-19 04:07:32 +00:00
}
type Bar struct {
label string
color ui.Color
value float64
2019-02-21 04:53:59 +00:00
delta float64
2019-02-19 04:07:32 +00:00
}
func NewBarChart(c config.BarChartConfig, palette console.Palette) *BarChart {
2019-03-08 04:04:46 +00:00
chart := BarChart{
Block: component.NewBlock(c.Title, true, palette),
2019-03-16 23:59:28 +00:00
Consumer: data.NewConsumer(),
bars: []Bar{},
scale: *c.Scale,
maxValue: -math.MaxFloat64,
palette: palette,
2019-02-19 04:07:32 +00:00
}
2019-03-08 04:04:46 +00:00
for _, i := range c.Items {
chart.AddBar(*i.Label, *i.Color)
}
2019-02-19 04:07:32 +00:00
go func() {
for {
select {
case sample := <-chart.SampleChannel:
chart.consumeSample(sample)
2019-03-16 23:59:28 +00:00
case alert := <-chart.AlertChannel:
chart.alert = alert
}
2019-03-08 04:04:46 +00:00
}
}()
return &chart
2019-02-19 04:07:32 +00:00
}
2019-03-16 23:59:28 +00:00
func (b *BarChart) consumeSample(sample *data.Sample) {
2019-02-19 04:07:32 +00:00
2019-02-21 04:53:59 +00:00
b.count++
2019-02-19 04:07:32 +00:00
2019-02-21 04:53:59 +00:00
float, err := strconv.ParseFloat(sample.Value, 64)
2019-03-16 23:59:28 +00:00
2019-02-19 04:07:32 +00:00
if err != nil {
2019-03-16 23:59:28 +00:00
b.AlertChannel <- &data.Alert{
Title: "FAILED TO PARSE NUMBER",
Text: err.Error(),
Color: sample.Color,
}
return
2019-02-19 04:07:32 +00:00
}
index := -1
for i, bar := range b.bars {
if bar.label == sample.Label {
index = i
}
}
2019-02-21 04:53:59 +00:00
bar := b.bars[index]
bar.delta = float - bar.value
bar.value = float
b.bars[index] = bar
if float > b.maxValue {
b.maxValue = float
}
2019-02-21 04:53:59 +00:00
// normalize bars height once in a while
if b.count%500 == 0 {
b.reselectMaxValue()
}
}
2019-03-08 04:04:46 +00:00
func (b *BarChart) AddBar(label string, color ui.Color) {
b.bars = append(b.bars, Bar{label: label, color: color, value: 0})
}
2019-02-21 04:53:59 +00:00
func (b *BarChart) reselectMaxValue() {
maxValue := -math.MaxFloat64
for _, bar := range b.bars {
if bar.value > maxValue {
maxValue = bar.value
}
}
b.maxValue = maxValue
2019-02-19 04:07:32 +00:00
}
2019-03-16 23:59:28 +00:00
func (b *BarChart) Draw(buffer *ui.Buffer) {
b.Block.Draw(buffer)
2019-02-19 04:07:32 +00:00
2019-03-01 03:44:01 +00:00
barWidth := int(math.Ceil(float64(b.Inner.Dx()-2*barIndent-len(b.bars)*barIndent) / float64(len(b.bars))))
2019-02-21 04:53:59 +00:00
barXCoordinate := b.Inner.Min.X + barIndent
2019-02-19 04:07:32 +00:00
labelStyle := ui.NewStyle(b.palette.BaseColor)
for _, bar := range b.bars {
2019-02-21 04:53:59 +00:00
2019-02-19 04:07:32 +00:00
// draw bar
height := int((bar.value / b.maxValue) * float64(b.Inner.Dy()-1))
2019-02-21 04:53:59 +00:00
if height <= 1 {
height = 2
}
maxYCoordinate := b.Inner.Max.Y - height
for x := barXCoordinate; x < ui.MinInt(barXCoordinate+barWidth, b.Inner.Max.X-barIndent); x++ {
for y := b.Inner.Max.Y - 2; y >= maxYCoordinate; y-- {
2019-02-26 04:36:23 +00:00
c := ui.NewCell(console.SymbolShade, ui.NewStyle(bar.color))
2019-03-16 23:59:28 +00:00
buffer.SetCell(c, image.Pt(x, y))
2019-02-19 04:07:32 +00:00
}
}
// draw label
labelXCoordinate := barXCoordinate +
int(float64(barWidth)/2) -
int(float64(rw.StringWidth(bar.label))/2)
2019-03-16 23:59:28 +00:00
buffer.SetString(
bar.label,
labelStyle,
2019-02-21 04:53:59 +00:00
image.Pt(labelXCoordinate, b.Inner.Max.Y-1))
// draw value & delta
value := formatValue(bar.value, b.scale)
if bar.delta != 0 {
value = fmt.Sprintf("%s / %s", value, formatValueWithSign(bar.delta, b.scale))
2019-02-19 04:07:32 +00:00
}
2019-02-21 04:53:59 +00:00
valueXCoordinate := barXCoordinate +
int(float64(barWidth)/2) -
int(float64(rw.StringWidth(value))/2)
2019-03-16 23:59:28 +00:00
buffer.SetString(
2019-02-21 04:53:59 +00:00
value,
labelStyle,
image.Pt(valueXCoordinate, maxYCoordinate-1))
2019-02-19 04:07:32 +00:00
barXCoordinate += barWidth + barIndent
}
2019-03-16 23:59:28 +00:00
component.RenderAlert(b.alert, b.Rectangle, buffer)
}
// TODO extract to utils
func formatValue(value float64, scale int) string {
if math.Abs(value) == math.MaxFloat64 {
return "Inf"
} else {
format := "%." + strconv.Itoa(scale) + "f"
return fmt.Sprintf(format, value)
2019-02-19 04:07:32 +00:00
}
}
2019-02-21 04:53:59 +00:00
// TODO extract to utils
func formatValueWithSign(value float64, scale int) string {
if value == 0 {
return " 0"
} else if value > 0 {
return "+" + formatValue(value, scale)
} else {
return formatValue(value, scale)
}
}