2019-02-18 04:49:40 +00:00
|
|
|
package barchart
|
|
|
|
|
2019-02-19 04:07:32 +00:00
|
|
|
import (
|
2019-02-20 04:03:19 +00:00
|
|
|
"fmt"
|
2019-02-19 04:07:32 +00:00
|
|
|
rw "github.com/mattn/go-runewidth"
|
2019-02-20 04:03:19 +00:00
|
|
|
"github.com/sqshq/sampler/console"
|
2019-02-19 04:07:32 +00:00
|
|
|
"github.com/sqshq/sampler/data"
|
|
|
|
ui "github.com/sqshq/termui"
|
|
|
|
"image"
|
2019-02-20 04:03:19 +00:00
|
|
|
"math"
|
2019-02-19 04:07:32 +00:00
|
|
|
"strconv"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
barSymbol rune = '⠿'
|
|
|
|
barIndent int = 1
|
|
|
|
)
|
|
|
|
|
|
|
|
type BarChart struct {
|
|
|
|
ui.Block
|
|
|
|
bars []Bar
|
|
|
|
scale int
|
|
|
|
maxValue float64
|
2019-02-21 04:53:59 +00:00
|
|
|
count int64
|
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(title string, scale int) *BarChart {
|
|
|
|
block := *ui.NewBlock()
|
|
|
|
block.Title = title
|
|
|
|
return &BarChart{
|
|
|
|
Block: block,
|
|
|
|
bars: []Bar{},
|
|
|
|
scale: scale,
|
2019-02-20 04:03:19 +00:00
|
|
|
maxValue: -math.MaxFloat64,
|
2019-02-19 04:07:32 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *BarChart) AddBar(label string, color ui.Color) {
|
|
|
|
b.bars = append(b.bars, Bar{label: label, color: color, value: 0})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *BarChart) ConsumeSample(sample data.Sample) {
|
|
|
|
|
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-02-19 04:07:32 +00:00
|
|
|
if err != nil {
|
|
|
|
// TODO visual notification + check sample.Error
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2019-02-20 04:03:19 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
func (b *BarChart) Draw(buf *ui.Buffer) {
|
|
|
|
b.Block.Draw(buf)
|
|
|
|
|
2019-02-21 04:53:59 +00:00
|
|
|
barWidth := (b.Inner.Dx() - 2*barIndent - len(b.bars)) / len(b.bars)
|
|
|
|
barXCoordinate := b.Inner.Min.X + barIndent
|
2019-02-19 04:07:32 +00:00
|
|
|
|
2019-02-20 04:03:19 +00:00
|
|
|
labelStyle := ui.NewStyle(console.ColorWhite)
|
|
|
|
|
|
|
|
for _, bar := range b.bars {
|
2019-02-21 04:53:59 +00:00
|
|
|
|
2019-02-19 04:07:32 +00:00
|
|
|
// draw bar
|
2019-02-20 04:03:19 +00:00
|
|
|
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-20 04:03:19 +00:00
|
|
|
c := ui.NewCell(barSymbol, ui.NewStyle(bar.color))
|
2019-02-19 04:07:32 +00:00
|
|
|
buf.SetCell(c, image.Pt(x, y))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// draw label
|
2019-02-20 04:03:19 +00:00
|
|
|
labelXCoordinate := barXCoordinate +
|
|
|
|
int(float64(barWidth)/2) -
|
|
|
|
int(float64(rw.StringWidth(bar.label))/2)
|
|
|
|
buf.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)
|
|
|
|
buf.SetString(
|
|
|
|
value,
|
|
|
|
labelStyle,
|
|
|
|
image.Pt(valueXCoordinate, maxYCoordinate-1))
|
2019-02-19 04:07:32 +00:00
|
|
|
|
2019-02-20 04:03:19 +00:00
|
|
|
barXCoordinate += barWidth + barIndent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|