diff --git a/config.yml b/config.yml index f6fe1a9..2fdeb63 100644 --- a/config.yml +++ b/config.yml @@ -1,65 +1,71 @@ runcharts: -- title: SEARCH ENGINE RESPONSE TIME (sec) - position: - w: 0 - h: 0 - size: - w: 50 - h: 14 - precision: 3 - items: - - label: GOOGLE - script: curl -o /dev/null -s -w '%{time_total}' https://www.google.com - - label: YAHOO - 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) - refresh-rate-ms: 5000 - position: - w: 0 - h: 14 - size: - w: 17 - h: 10 - legend: - enabled: true - details: false - items: - - label: GOOGLE - script: curl -o /dev/null -s -w '%{time_total}' https://www.google.com - - label: YAHOO - 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 - position: - w: 17 - h: 14 - size: - w: 17 - h: 10 - items: + - title: SEARCH ENGINE RESPONSE TIME (sec) + position: + w: 0 + h: 0 + size: + w: 50 + h: 14 + scale: 3 + items: + - label: GOOGLE + script: curl -o /dev/null -s -w '%{time_total}' https://www.google.com + - label: YAHOO + 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) + refresh-rate-ms: 5000 + position: + w: 0 + h: 14 + size: + w: 17 + h: 10 + legend: + enabled: true + details: false + items: + - label: GOOGLE + script: curl -o /dev/null -s -w '%{time_total}' https://www.google.com + - label: YAHOO + 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 + 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'}).itcount()" + - label: INACTIVE + 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'}).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()" asciiboxes: -- title: COUNT - position: - w: 34 - h: 14 - size: - w: 16 - h: 4 - script: date +%r -- title: MODE - position: - w: 34 - h: 18 - size: - w: 16 - h: 6 - script: date +%r - font: 3d + - title: COUNT + position: + w: 34 + h: 14 + size: + w: 16 + h: 4 + script: date +%r + - title: MODE + position: + w: 34 + h: 18 + size: + w: 16 + h: 6 + script: date +%r + font: 3d diff --git a/config/config.go b/config/config.go index f973ee1..dbf878e 100644 --- a/config/config.go +++ b/config/config.go @@ -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,10 +28,16 @@ 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"` +} + type AsciiBoxConfig struct { ComponentConfig `yaml:",inline"` data.Item `yaml:",inline"` diff --git a/config/default.go b/config/default.go index 2adda0a..b2e04e5 100644 --- a/config/default.go +++ b/config/default.go @@ -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} diff --git a/go.mod b/go.mod index 9c9bf3d..7512023 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/main.go b/main.go index 1c48755..4a20d70 100644 --- a/main.go +++ b/main.go @@ -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, diff --git a/widgets/barchart/barchart.go b/widgets/barchart/barchart.go index 40ab3db..9136baf 100644 --- a/widgets/barchart/barchart.go +++ b/widgets/barchart/barchart.go @@ -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) + } +} diff --git a/widgets/runchart/grid.go b/widgets/runchart/grid.go index 6ad1b9b..faa835c 100644 --- a/widgets/runchart/grid.go +++ b/widgets/runchart/grid.go @@ -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)) } diff --git a/widgets/runchart/legend.go b/widgets/runchart/legend.go index d643fa5..9b3a39a 100644 --- a/widgets/runchart/legend.go +++ b/widgets/runchart/legend.go @@ -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) diff --git a/widgets/runchart/runchart.go b/widgets/runchart/runchart.go index 02ed076..1b9f181 100644 --- a/widgets/runchart/runchart.go +++ b/widgets/runchart/runchart.go @@ -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) } }