From e67ac7c19a78a13db918ad584b3390e440ef5bd0 Mon Sep 17 00:00:00 2001 From: sqshq Date: Mon, 25 Mar 2019 23:29:23 -0400 Subject: [PATCH] sparkline main functionality --- component/alert.go | 13 ++----- component/barchart/barchart.go | 2 +- component/menu.go | 2 +- component/runchart/grid.go | 5 +-- component/runchart/legend.go | 13 +++---- component/runchart/runchart.go | 23 ++---------- component/sparkline/sparkline.go | 60 ++++++++++++++++++++++++++++++-- component/util/format.go | 26 ++++++++++++++ component/util/math.go | 14 ++++++++ config.yml | 8 ++--- config/default.go | 7 ++++ console/symbol.go | 6 ++-- 12 files changed, 128 insertions(+), 51 deletions(-) create mode 100644 component/util/format.go create mode 100644 component/util/math.go diff --git a/component/alert.go b/component/alert.go index 41e31a8..4fe9aae 100644 --- a/component/alert.go +++ b/component/alert.go @@ -3,6 +3,7 @@ package component import ( "fmt" ui "github.com/gizak/termui/v3" + "github.com/sqshq/sampler/component/util" "github.com/sqshq/sampler/console" "github.com/sqshq/sampler/data" "image" @@ -21,7 +22,7 @@ func RenderAlert(alert *data.Alert, area image.Rectangle, buffer *ui.Buffer) { color = *alert.Color } - width := max(len(alert.Title), len(alert.Text)) + 10 + width := util.Max([]int{len(alert.Title), len(alert.Text)}) + 10 if width > area.Dx() { width = area.Dx() @@ -55,15 +56,7 @@ func RenderAlert(alert *data.Alert, area image.Rectangle, buffer *ui.Buffer) { } } -//TODO move to utils -func max(a int, b int) int { - if a > b { - return a - } else { - return b - } -} - +// TODO move to utils func getRectCoordinates(area image.Rectangle, width int, height int) (int, int, int, int) { x1 := area.Min.X + area.Dx()/2 - width/2 y1 := area.Min.Y + area.Dy()/2 - height diff --git a/component/barchart/barchart.go b/component/barchart/barchart.go index 101a8f1..8c71080 100644 --- a/component/barchart/barchart.go +++ b/component/barchart/barchart.go @@ -134,7 +134,7 @@ func (b *BarChart) Draw(buffer *ui.Buffer) { 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-- { - c := ui.NewCell(console.SymbolShade, ui.NewStyle(bar.color)) + c := ui.NewCell(console.SymbolHorizontalBar, ui.NewStyle(bar.color)) buffer.SetCell(c, image.Pt(x, y)) } } diff --git a/component/menu.go b/component/menu.go index 00c50b8..951a262 100644 --- a/component/menu.go +++ b/component/menu.go @@ -198,7 +198,7 @@ func (m *Menu) printAllDirectionsArrowSign(buffer *ui.Buffer, y int) { func (m *Menu) renderOptions(buffer *ui.Buffer) { - highlightedStyle := ui.NewStyle(m.palette.BaseColor, console.ColorOlive) + highlightedStyle := ui.NewStyle(m.palette.ReverseColor, console.ColorOlive) regularStyle := ui.NewStyle(m.palette.BaseColor, m.palette.ReverseColor) offset := 1 diff --git a/component/runchart/grid.go b/component/runchart/grid.go index 231d74f..161515e 100644 --- a/component/runchart/grid.go +++ b/component/runchart/grid.go @@ -2,6 +2,7 @@ package runchart import ( ui "github.com/gizak/termui/v3" + "github.com/sqshq/sampler/component/util" "image" "math" "time" @@ -76,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.scale), + util.FormatValue(value, c.scale), ui.NewStyle(c.palette.BaseColor), image.Pt(c.Inner.Min.X, 1+c.Inner.Min.Y+i*(yAxisLabelsIndent+yAxisLabelsHeight))) } } else { buffer.SetString( - formatValue(c.grid.valueExtrema.max, c.scale), + util.FormatValue(c.grid.valueExtrema.max, c.scale), ui.NewStyle(c.palette.BaseColor), image.Pt(c.Inner.Min.X, c.Inner.Min.Y+c.Inner.Dy()/2)) } diff --git a/component/runchart/legend.go b/component/runchart/legend.go index 2145c98..a35b555 100644 --- a/component/runchart/legend.go +++ b/component/runchart/legend.go @@ -3,6 +3,7 @@ package runchart import ( "fmt" ui "github.com/gizak/termui/v3" + "github.com/sqshq/sampler/component/util" "image" "math" ) @@ -61,7 +62,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.scale)), detailsStyle, image.Pt(x, y+2)) + buffer.SetString(fmt.Sprintf("value %s", util.FormatValue(line.selectionPoint.value, c.scale)), detailsStyle, image.Pt(x, y+2)) continue } @@ -70,10 +71,10 @@ func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) { } details := [4]string{ - 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)), + fmt.Sprintf("cur %s", util.FormatValue(getCurrentValue(line), c.scale)), + fmt.Sprintf("dlt %s", util.FormatValueWithSign(getDiffWithPreviousValue(line), c.scale)), + fmt.Sprintf("max %s", util.FormatValue(line.extrema.max, c.scale)), + fmt.Sprintf("min %s", util.FormatValue(line.extrema.min, c.scale)), } for i, detail := range details { @@ -89,7 +90,7 @@ func getColumnWidth(mode Mode, lines []TimeLine, scale int) int { return len(timeFormat) } - width := len(formatValue(0, scale)) + width := len(util.FormatValue(0, scale)) for _, line := range lines { if len(line.label) > width { width = len(line.label) diff --git a/component/runchart/runchart.go b/component/runchart/runchart.go index 9773773..1672bdb 100644 --- a/component/runchart/runchart.go +++ b/component/runchart/runchart.go @@ -1,8 +1,8 @@ package runchart import ( - "fmt" "github.com/sqshq/sampler/component" + "github.com/sqshq/sampler/component/util" "github.com/sqshq/sampler/config" "github.com/sqshq/sampler/console" "github.com/sqshq/sampler/data" @@ -324,7 +324,7 @@ func (c *RunChart) getMaxValueLength() int { for _, line := range c.lines { for _, point := range line.points { - l := len(formatValue(point.value, c.scale)) + l := len(util.FormatValue(point.value, c.scale)) if l > maxValueLength { maxValueLength = l } @@ -366,25 +366,6 @@ func getMidRangeTime(r TimeRange) time.Time { return r.max.Add(-delta / 2) } -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) - } -} - -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) - } -} - // time duration between grid lines func calculateTimescale(refreshRateMs int) time.Duration { diff --git a/component/sparkline/sparkline.go b/component/sparkline/sparkline.go index d373eed..182bb5a 100644 --- a/component/sparkline/sparkline.go +++ b/component/sparkline/sparkline.go @@ -3,18 +3,24 @@ package sparkline import ( ui "github.com/gizak/termui/v3" "github.com/sqshq/sampler/component" + "github.com/sqshq/sampler/component/util" "github.com/sqshq/sampler/config" "github.com/sqshq/sampler/console" "github.com/sqshq/sampler/data" + "image" "strconv" ) type SparkLine struct { *ui.Block *data.Consumer - alert *data.Alert - values []float64 - palette console.Palette + alert *data.Alert + values []float64 + maxValue float64 + minValue float64 + scale int + color ui.Color + palette console.Palette } func NewSparkLine(c config.SparkLineConfig, palette console.Palette) *SparkLine { @@ -23,6 +29,8 @@ func NewSparkLine(c config.SparkLineConfig, palette console.Palette) *SparkLine Block: component.NewBlock(c.Title, true, palette), Consumer: data.NewConsumer(), values: []float64{}, + scale: *c.Scale, + color: *c.Item.Color, palette: palette, } @@ -54,8 +62,54 @@ func (s *SparkLine) consumeSample(sample *data.Sample) { s.values = append(s.values, float) // TODO cleanup old ones + + for i := len(s.values) - 1; i >= 0; i-- { + if len(s.values)-i > s.Dx() { + break + } + if s.values[i] > s.maxValue { + s.maxValue = s.values[i] + } + if s.values[i] < s.minValue { + s.minValue = s.values[i] + } + } } +// TODO make sure that 0 value is still printed +// TODO make sure that cur value is printed on the same Y as sparkline (include in for loop for last iteratiton) +// TODO gradient color func (s *SparkLine) Draw(buffer *ui.Buffer) { + + textStyle := ui.NewStyle(s.palette.BaseColor) + lineStyle := ui.NewStyle(s.color) + + minValue := util.FormatValue(s.minValue, s.scale) + maxValue := util.FormatValue(s.maxValue, s.scale) + curValue := util.FormatValue(s.values[len(s.values)-1], s.scale) + + buffer.SetString(minValue, textStyle, image.Pt(s.Min.X+2, s.Max.Y-2)) + buffer.SetString(maxValue, textStyle, image.Pt(s.Min.X+2, s.Min.Y+2)) + + curY := int((s.values[len(s.values)-1]/s.maxValue)*float64(s.Dy())) - 1 + buffer.SetString(curValue, textStyle, image.Pt(s.Max.X-len(curValue)-2, s.Max.Y-util.Max([]int{curY, 2}))) + + indent := 2 + util.Max([]int{ + len(minValue), len(maxValue), len(curValue), + }) + + for i := len(s.values) - 1; i >= 0; i-- { + + n := len(s.values) - i + + if n > s.Dx()-indent*2-2 { + break + } + + for j := 1; j < int((s.values[i]/s.maxValue)*float64(s.Dy()-2))+2; j++ { + buffer.SetString("▪", lineStyle, image.Pt(s.Inner.Max.X-n-indent, s.Inner.Max.Y-j)) + } + } + s.Block.Draw(buffer) } diff --git a/component/util/format.go b/component/util/format.go new file mode 100644 index 0000000..fa692e3 --- /dev/null +++ b/component/util/format.go @@ -0,0 +1,26 @@ +package util + +import ( + "fmt" + "math" + "strconv" +) + +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) + } +} + +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) + } +} diff --git a/component/util/math.go b/component/util/math.go new file mode 100644 index 0000000..e3cd01d --- /dev/null +++ b/component/util/math.go @@ -0,0 +1,14 @@ +package util + +func Max(numbers []int) int { + + max := numbers[0] + + for _, n := range numbers { + if n > max { + max = n + } + } + + return max +} diff --git a/config.yml b/config.yml index af117d4..a043b61 100644 --- a/config.yml +++ b/config.yml @@ -1,9 +1,9 @@ runcharts: - title: SEARCH ENGINE RESPONSE TIME (sec) - position: [[0, 0], [53, 16]] + position: [[0, 0], [52, 16]] triggers: - title: Latency threshold exceeded - condition: echo "$prev < 0.35 && $cur > 0.35" |bc -l + condition: echo "$prev < 0.55 && $cur > 0.55" |bc -l actions: terminal-bell: true sound: true @@ -31,7 +31,7 @@ runcharts: barcharts: - title: EVENTS BY STATUS refresh-rate-ms: 1000 - position: [[0, 17], [28, 12]] + position: [[0, 17], [27, 12]] scale: 0 items: - label: NEW @@ -85,6 +85,6 @@ asciiboxes: font: 3d sparklines: - title: CPU usage - position: [[28, 17], [25, 12]] + position: [[27, 17], [25, 5]] scale: 0 value: ps -A -o %cpu | awk '{s+=$1} END {print s}' diff --git a/config/default.go b/config/default.go index 63bebf9..28633be 100644 --- a/config/default.go +++ b/config/default.go @@ -172,6 +172,13 @@ func (c *Config) setDefaultColors() { } } + for i, s := range c.SparkLines { + if s.Item.Color == nil { + s.Item.Color = &palette.ContentColors[i%colorsCount] + c.SparkLines[i] = s + } + } + for i, g := range c.Gauges { if g.Color == nil { g.Color = &palette.ContentColors[i%colorsCount] diff --git a/console/symbol.go b/console/symbol.go index b5bf093..0dc7e53 100644 --- a/console/symbol.go +++ b/console/symbol.go @@ -1,7 +1,7 @@ package console const ( - SymbolSelection rune = '▲' - SymbolVerticalBar rune = '▎' - SymbolShade rune = '═' + SymbolSelection rune = '▲' + SymbolVerticalBar rune = '▎' + SymbolHorizontalBar rune = '═' )