added barchart component

This commit is contained in:
sqshq 2019-02-18 23:07:32 -05:00
parent e0986b7e31
commit 56fbf0cc0d
9 changed files with 219 additions and 168 deletions

View File

@ -1,12 +1,12 @@
runcharts:
- title: SEARCH ENGINE RESPONSE TIME (sec)
- title: SEARCH ENGINE RESPONSE TIME (sec)
position:
w: 0
h: 0
size:
w: 50
h: 14
precision: 3
scale: 3
items:
- label: GOOGLE
script: curl -o /dev/null -s -w '%{time_total}' https://www.google.com
@ -14,7 +14,7 @@ runcharts:
script: curl -o /dev/null -s -w '%{time_total}' https://search.yahoo.com
- label: BING
script: curl -o /dev/null -s -w '%{time_total}' https://www.bing.com
- title: SEARCH ENGINE RESPONSE TIME 2 (sec)
- title: SEARCH ENGINE RESPONSE TIME 2 (sec)
refresh-rate-ms: 5000
position:
w: 0
@ -32,21 +32,27 @@ runcharts:
script: curl -o /dev/null -s -w '%{time_total}' https://search.yahoo.com
- label: BING
script: curl -o /dev/null -s -w '%{time_total}' https://www.bing.com
- title: MONGO COLLECTIONS COUNT
precision: 0
- title: MONGO COLLECTIONS COUNT
position:
w: 17
h: 14
size:
w: 17
h: 10
scale: 0
items:
- label: ACTIVE
script: mongo --quiet --host=localhost blog --eval "db.getCollection('posts').find({status:'ACTIVE'}).size()"
script: mongo --quiet --host=localhost blog --eval "db.getCollection('posts').find({status:'ACTIVE'}).itcount()"
- label: INACTIVE
script: mongo --quiet --host=localhost blog --eval "db.getCollection('posts').find({status:'INACTIVE'}).size()"
script: mongo --quiet --host=localhost blog --eval "db.getCollection('posts').find({status:'INACTIVE'}).itcount()"
barcharts:
- title: ANCHOR CONTEXT EVENTS
- label: ACTIVE
script: mongo --quiet --host=localhost blog --eval "db.getCollection('posts').find({status:'ACTIVE'}).itcount()"
- label: INACTIVE
script: mongo --quiet --host=localhost blog --eval "db.getCollection('posts').find({status:'INACTIVE'}).itcount()"
asciiboxes:
- title: COUNT
- title: COUNT
position:
w: 34
h: 14
@ -54,7 +60,7 @@ asciiboxes:
w: 16
h: 4
script: date +%r
- title: MODE
- title: MODE
position:
w: 34
h: 18

View File

@ -14,6 +14,7 @@ import (
type Config struct {
Theme *console.Theme `yaml:"theme,omitempty"`
RunCharts []RunChartConfig `yaml:"runcharts,omitempty"`
BarCharts []BarChartConfig `yaml:"barcharts,omitempty"`
AsciiBoxes []AsciiBoxConfig `yaml:"asciiboxes,omitempty"`
}
@ -27,7 +28,13 @@ type ComponentConfig struct {
type RunChartConfig struct {
ComponentConfig `yaml:",inline"`
Legend *LegendConfig `yaml:"legend,omitempty"`
Precision *int `yaml:"precision,omitempty"`
Scale *int `yaml:"scale,omitempty"`
Items []data.Item `yaml:"items"`
}
type BarChartConfig struct {
ComponentConfig `yaml:",inline"`
Scale *int `yaml:"scale,omitempty"`
Items []data.Item `yaml:"items"`
}

View File

@ -7,7 +7,7 @@ import (
const (
defaultRefreshRateMs = 1000
defaultPrecision = 1
defaultScale = 1
defaultTheme = console.ThemeDark
)
@ -29,9 +29,9 @@ func (c *Config) setDefaultValues() {
r := defaultRefreshRateMs
chart.RefreshRateMs = &r
}
if chart.Precision == nil {
p := defaultPrecision
chart.Precision = &p
if chart.Scale == nil {
p := defaultScale
chart.Scale = &p
}
if chart.Legend == nil {
chart.Legend = &LegendConfig{true, true}

2
go.mod
View File

@ -3,7 +3,7 @@ module github.com/sqshq/sampler
require (
github.com/hajimehoshi/go-mp3 v0.1.1
github.com/hajimehoshi/oto v0.1.1
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/mattn/go-runewidth v0.0.4
github.com/mbndr/figlet4go v0.0.0-20170909125910-47ded4d17030
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/sqshq/termui v0.0.0-20190125032456-731556c09f2c

14
main.go
View File

@ -7,6 +7,7 @@ import (
"github.com/sqshq/sampler/event"
"github.com/sqshq/sampler/widgets"
"github.com/sqshq/sampler/widgets/asciibox"
"github.com/sqshq/sampler/widgets/barchart"
"github.com/sqshq/sampler/widgets/runchart"
ui "github.com/sqshq/termui"
"time"
@ -25,7 +26,7 @@ func main() {
for _, c := range cfg.RunCharts {
legend := runchart.Legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details}
chart := runchart.NewRunChart(c.Title, *c.Precision, *c.RefreshRateMs, legend)
chart := runchart.NewRunChart(c.Title, *c.Scale, *c.RefreshRateMs, legend)
layout.AddComponent(chart, c.Title, c.Position, c.Size, config.TypeRunChart)
for _, item := range c.Items {
@ -40,6 +41,17 @@ func main() {
data.NewSampler(box, a.Item, *a.RefreshRateMs)
}
for _, c := range cfg.BarCharts {
chart := barchart.NewBarChart(c.Title, *c.Scale)
layout.AddComponent(chart, c.Title, c.Position, c.Size, config.TypeBarChart)
for _, item := range c.Items {
chart.AddBar(*item.Label, *item.Color)
data.NewSampler(chart, item, *c.RefreshRateMs)
}
}
handler := event.Handler{
Layout: layout,
RenderEvents: time.NewTicker(console.RenderRate).C,

View File

@ -1,83 +1,109 @@
package barchart
//
//import (
// "fmt"
// "image"
//)
//
//type BarChart struct {
// Block
// BarColors []Color
// LabelStyles []Style
// NumStyles []Style // only Fg and Modifier are used
// NumFmt func(float64) string
// Data []float64
// Labels []string
// BarWidth int
// BarGap int
// MaxVal float64
//}
//
//func NewBarChart() *BarChart {
// return &BarChart{
// Block: *NewBlock(),
// BarColors: Theme.BarChart.Bars,
// NumStyles: Theme.BarChart.Nums,
// LabelStyles: Theme.BarChart.Labels,
// NumFmt: func(n float64) string { return fmt.Sprint(n) },
// BarGap: 1,
// BarWidth: 3,
// }
//}
//
//func (self *BarChart) Draw(buf *Buffer) {
// self.Block.Draw(buf)
//
// maxVal := self.MaxVal
// if maxVal == 0 {
// maxVal, _ = GetMaxFloat64FromSlice(self.Data)
// }
//
// barXCoordinate := self.Inner.Min.X
//
// for i, data := range self.Data {
// // draw bar
// height := int((data / maxVal) * float64(self.Inner.Dy()-1))
// for x := barXCoordinate; x < MinInt(barXCoordinate+self.BarWidth, self.Inner.Max.X); x++ {
// for y := self.Inner.Max.Y - 2; y > (self.Inner.Max.Y-2)-height; y-- {
// c := NewCell(' ', NewStyle(ColorClear, SelectColor(self.BarColors, i)))
// buf.SetCell(c, image.Pt(x, y))
// }
// }
//
// // draw label
// if i < len(self.Labels) {
// labelXCoordinate := barXCoordinate +
// int((float64(self.BarWidth) / 2)) -
// int((float64(rw.StringWidth(self.Labels[i])) / 2))
// buf.SetString(
// self.Labels[i],
// SelectStyle(self.LabelStyles, i),
// image.Pt(labelXCoordinate, self.Inner.Max.Y-1),
// )
// }
//
// // draw number
// numberXCoordinate := barXCoordinate + int((float64(self.BarWidth) / 2))
// if numberXCoordinate <= self.Inner.Max.X {
// buf.SetString(
// self.NumFmt(data),
// NewStyle(
// SelectStyle(self.NumStyles, i+1).Fg,
// SelectColor(self.BarColors, i),
// SelectStyle(self.NumStyles, i+1).Modifier,
// ),
// image.Pt(numberXCoordinate, self.Inner.Max.Y-2),
// )
// }
//
// barXCoordinate += (self.BarWidth + self.BarGap)
// }
//}
//
import (
rw "github.com/mattn/go-runewidth"
"github.com/sqshq/sampler/data"
ui "github.com/sqshq/termui"
"image"
"strconv"
)
const (
barSymbol rune = '⠿'
barIndent int = 1
)
type BarChart struct {
ui.Block
bars []Bar
scale int
maxValue float64
}
type Bar struct {
label string
color ui.Color
value float64
}
func NewBarChart(title string, scale int) *BarChart {
block := *ui.NewBlock()
block.Title = title
return &BarChart{
Block: block,
bars: []Bar{},
scale: scale,
maxValue: 0,
}
}
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) {
float, err := strconv.ParseFloat(sample.Value, 64)
if err != nil {
// TODO visual notification + check sample.Error
}
index := -1
for i, bar := range b.bars {
if bar.label == sample.Label {
index = i
}
}
bar := b.bars[index]
bar.value = float
b.bars[index] = bar
}
func (b *BarChart) Draw(buf *ui.Buffer) {
b.Block.Draw(buf)
maxVal := b.maxValue
barXCoordinate := b.Inner.Min.X
for i, data := range b.Data {
// draw bar
height := int((data / maxVal) * float64(b.Inner.Dy()-1))
for x := barXCoordinate; x < ui.MinInt(barXCoordinate+b.BarWidth, b.Inner.Max.X); x++ {
for y := b.Inner.Max.Y - 2; y > (b.Inner.Max.Y-2)-height; y-- {
c := ui.NewCell(barSymbol, ui.NewStyle(ui.SelectColor(b.BarColors, i)))
buf.SetCell(c, image.Pt(x, y))
}
}
// draw label
if i < len(b.Labels) {
labelXCoordinate := barXCoordinate +
int((float64(b.BarWidth) / 2)) -
int((float64(rw.StringWidth(b.Labels[i])) / 2))
buf.SetString(
b.Labels[i],
ui.SelectStyle(b.LabelStyles, i),
image.Pt(labelXCoordinate, b.Inner.Max.Y-1),
)
}
// draw value
numberXCoordinate := barXCoordinate + int((float64(b.BarWidth) / 2))
if numberXCoordinate <= b.Inner.Max.X {
buf.SetString(
b.NumFmt(data),
ui.NewStyle(
ui.SelectStyle(b.NumStyles, i+1).Fg,
ui.SelectColor(b.BarColors, i),
ui.SelectStyle(b.NumStyles, i+1).Modifier,
),
image.Pt(numberXCoordinate, b.Inner.Max.Y-2),
)
}
barXCoordinate += (b.BarWidth + b.BarGap)
}
}

View File

@ -77,13 +77,13 @@ func (c *RunChart) renderAxes(buffer *ui.Buffer) {
for i := 0; i < int(labelsCount); i++ {
value := c.grid.valueExtrema.max - (valuePerY * float64(i) * (yAxisLabelsIndent + yAxisLabelsHeight))
buffer.SetString(
formatValue(value, c.precision),
formatValue(value, c.scale),
ui.NewStyle(ui.ColorWhite),
image.Pt(c.Inner.Min.X, 1+c.Inner.Min.Y+i*(yAxisLabelsIndent+yAxisLabelsHeight)))
}
} else {
buffer.SetString(
formatValue(c.grid.valueExtrema.max, c.precision),
formatValue(c.grid.valueExtrema.max, c.scale),
ui.NewStyle(ui.ColorWhite),
image.Pt(c.Inner.Min.X, c.Inner.Min.Y+c.Inner.Dy()/2))
}

View File

@ -38,7 +38,7 @@ func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
rowCount := (c.Dy() - 2*yAxisLegendIndent) / (height + yAxisLegendIndent)
columnCount := int(math.Ceil(float64(len(c.lines)) / float64(rowCount)))
columnWidth := getColumnWidth(c.mode, c.lines, c.precision)
columnWidth := getColumnWidth(c.mode, c.lines, c.scale)
for col := 0; col < columnCount; col++ {
for row := 0; row < rowCount; row++ {
@ -61,7 +61,7 @@ func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
if c.mode == ModePinpoint {
buffer.SetString(fmt.Sprintf("time %s", line.selectionPoint.time.Format("15:04:05.000")), detailsStyle, image.Pt(x, y+1))
buffer.SetString(fmt.Sprintf("value %s", formatValue(line.selectionPoint.value, c.precision)), detailsStyle, image.Pt(x, y+2))
buffer.SetString(fmt.Sprintf("value %s", formatValue(line.selectionPoint.value, c.scale)), detailsStyle, image.Pt(x, y+2))
continue
}
@ -70,10 +70,10 @@ func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
}
details := [4]string{
fmt.Sprintf("cur %s", formatValue(getCurrentValue(line), c.precision)),
fmt.Sprintf("dlt %s", formatValueWithSign(getDiffWithPreviousValue(line), c.precision)),
fmt.Sprintf("max %s", formatValue(line.extrema.max, c.precision)),
fmt.Sprintf("min %s", formatValue(line.extrema.min, c.precision)),
fmt.Sprintf("cur %s", formatValue(getCurrentValue(line), c.scale)),
fmt.Sprintf("dlt %s", formatValueWithSign(getDiffWithPreviousValue(line), c.scale)),
fmt.Sprintf("max %s", formatValue(line.extrema.max, c.scale)),
fmt.Sprintf("min %s", formatValue(line.extrema.min, c.scale)),
}
for i, detail := range details {
@ -83,13 +83,13 @@ func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
}
}
func getColumnWidth(mode Mode, lines []TimeLine, precision int) int {
func getColumnWidth(mode Mode, lines []TimeLine, scale int) int {
if mode == ModePinpoint {
return len(timeFormat)
}
width := len(formatValue(0, precision))
width := len(formatValue(0, scale))
for _, line := range lines {
if len(line.label) > width {
width = len(line.label)

View File

@ -42,7 +42,7 @@ type RunChart struct {
mutex *sync.Mutex
mode Mode
selection time.Time
precision int
scale int
legend Legend
}
@ -71,7 +71,7 @@ type ValueExtrema struct {
min float64
}
func NewRunChart(title string, precision int, refreshRateMs int, legend Legend) *RunChart {
func NewRunChart(title string, scale int, refreshRateMs int, legend Legend) *RunChart {
block := *ui.NewBlock()
block.Title = title
return &RunChart{
@ -79,7 +79,7 @@ func NewRunChart(title string, precision int, refreshRateMs int, legend Legend)
lines: []TimeLine{},
timescale: calculateTimescale(refreshRateMs),
mutex: &sync.Mutex{},
precision: precision,
scale: scale,
mode: ModeDefault,
legend: legend,
}
@ -285,7 +285,7 @@ func (c *RunChart) getMaxValueLength() int {
for _, line := range c.lines {
for _, point := range line.points {
l := len(formatValue(point.value, c.precision))
l := len(formatValue(point.value, c.scale))
if l > maxValueLength {
maxValueLength = l
}
@ -327,22 +327,22 @@ func getMidRangeTime(r TimeRange) time.Time {
return r.max.Add(-delta / 2)
}
func formatValue(value float64, precision int) string {
func formatValue(value float64, scale int) string {
if math.Abs(value) == math.MaxFloat64 {
return "Inf"
} else {
format := "%." + strconv.Itoa(precision) + "f"
format := "%." + strconv.Itoa(scale) + "f"
return fmt.Sprintf(format, value)
}
}
func formatValueWithSign(value float64, precision int) string {
func formatValueWithSign(value float64, scale int) string {
if value == 0 {
return " 0"
} else if value > 0 {
return "+" + formatValue(value, precision)
return "+" + formatValue(value, scale)
} else {
return formatValue(value, precision)
return formatValue(value, scale)
}
}