sparkline main functionality

This commit is contained in:
sqshq 2019-03-25 23:29:23 -04:00
parent db3888e540
commit e67ac7c19a
12 changed files with 128 additions and 51 deletions

View File

@ -3,6 +3,7 @@ package component
import ( import (
"fmt" "fmt"
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/component/util"
"github.com/sqshq/sampler/console" "github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/data" "github.com/sqshq/sampler/data"
"image" "image"
@ -21,7 +22,7 @@ func RenderAlert(alert *data.Alert, area image.Rectangle, buffer *ui.Buffer) {
color = *alert.Color 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() { if width > area.Dx() {
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 // TODO move to utils
func max(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
func getRectCoordinates(area image.Rectangle, width int, height int) (int, int, int, int) { func getRectCoordinates(area image.Rectangle, width int, height int) (int, int, int, int) {
x1 := area.Min.X + area.Dx()/2 - width/2 x1 := area.Min.X + area.Dx()/2 - width/2
y1 := area.Min.Y + area.Dy()/2 - height y1 := area.Min.Y + area.Dy()/2 - height

View File

@ -134,7 +134,7 @@ func (b *BarChart) Draw(buffer *ui.Buffer) {
maxYCoordinate := b.Inner.Max.Y - height maxYCoordinate := b.Inner.Max.Y - height
for x := barXCoordinate; x < ui.MinInt(barXCoordinate+barWidth, b.Inner.Max.X-barIndent); x++ { for x := barXCoordinate; x < ui.MinInt(barXCoordinate+barWidth, b.Inner.Max.X-barIndent); x++ {
for y := b.Inner.Max.Y - 2; y >= maxYCoordinate; y-- { 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)) buffer.SetCell(c, image.Pt(x, y))
} }
} }

View File

@ -198,7 +198,7 @@ func (m *Menu) printAllDirectionsArrowSign(buffer *ui.Buffer, y int) {
func (m *Menu) renderOptions(buffer *ui.Buffer) { 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) regularStyle := ui.NewStyle(m.palette.BaseColor, m.palette.ReverseColor)
offset := 1 offset := 1

View File

@ -2,6 +2,7 @@ package runchart
import ( import (
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/component/util"
"image" "image"
"math" "math"
"time" "time"
@ -76,13 +77,13 @@ func (c *RunChart) renderAxes(buffer *ui.Buffer) {
for i := 0; i < int(labelsCount); i++ { for i := 0; i < int(labelsCount); i++ {
value := c.grid.valueExtrema.max - (valuePerY * float64(i) * (yAxisLabelsIndent + yAxisLabelsHeight)) value := c.grid.valueExtrema.max - (valuePerY * float64(i) * (yAxisLabelsIndent + yAxisLabelsHeight))
buffer.SetString( buffer.SetString(
formatValue(value, c.scale), util.FormatValue(value, c.scale),
ui.NewStyle(c.palette.BaseColor), ui.NewStyle(c.palette.BaseColor),
image.Pt(c.Inner.Min.X, 1+c.Inner.Min.Y+i*(yAxisLabelsIndent+yAxisLabelsHeight))) image.Pt(c.Inner.Min.X, 1+c.Inner.Min.Y+i*(yAxisLabelsIndent+yAxisLabelsHeight)))
} }
} else { } else {
buffer.SetString( buffer.SetString(
formatValue(c.grid.valueExtrema.max, c.scale), util.FormatValue(c.grid.valueExtrema.max, c.scale),
ui.NewStyle(c.palette.BaseColor), ui.NewStyle(c.palette.BaseColor),
image.Pt(c.Inner.Min.X, c.Inner.Min.Y+c.Inner.Dy()/2)) image.Pt(c.Inner.Min.X, c.Inner.Min.Y+c.Inner.Dy()/2))
} }

View File

@ -3,6 +3,7 @@ package runchart
import ( import (
"fmt" "fmt"
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/component/util"
"image" "image"
"math" "math"
) )
@ -61,7 +62,7 @@ func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
if c.mode == ModePinpoint { 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("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 continue
} }
@ -70,10 +71,10 @@ func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
} }
details := [4]string{ details := [4]string{
fmt.Sprintf("cur %s", formatValue(getCurrentValue(line), c.scale)), fmt.Sprintf("cur %s", util.FormatValue(getCurrentValue(line), c.scale)),
fmt.Sprintf("dlt %s", formatValueWithSign(getDiffWithPreviousValue(line), c.scale)), fmt.Sprintf("dlt %s", util.FormatValueWithSign(getDiffWithPreviousValue(line), c.scale)),
fmt.Sprintf("max %s", formatValue(line.extrema.max, c.scale)), fmt.Sprintf("max %s", util.FormatValue(line.extrema.max, c.scale)),
fmt.Sprintf("min %s", formatValue(line.extrema.min, c.scale)), fmt.Sprintf("min %s", util.FormatValue(line.extrema.min, c.scale)),
} }
for i, detail := range details { for i, detail := range details {
@ -89,7 +90,7 @@ func getColumnWidth(mode Mode, lines []TimeLine, scale int) int {
return len(timeFormat) return len(timeFormat)
} }
width := len(formatValue(0, scale)) width := len(util.FormatValue(0, scale))
for _, line := range lines { for _, line := range lines {
if len(line.label) > width { if len(line.label) > width {
width = len(line.label) width = len(line.label)

View File

@ -1,8 +1,8 @@
package runchart package runchart
import ( import (
"fmt"
"github.com/sqshq/sampler/component" "github.com/sqshq/sampler/component"
"github.com/sqshq/sampler/component/util"
"github.com/sqshq/sampler/config" "github.com/sqshq/sampler/config"
"github.com/sqshq/sampler/console" "github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/data" "github.com/sqshq/sampler/data"
@ -324,7 +324,7 @@ func (c *RunChart) getMaxValueLength() int {
for _, line := range c.lines { for _, line := range c.lines {
for _, point := range line.points { for _, point := range line.points {
l := len(formatValue(point.value, c.scale)) l := len(util.FormatValue(point.value, c.scale))
if l > maxValueLength { if l > maxValueLength {
maxValueLength = l maxValueLength = l
} }
@ -366,25 +366,6 @@ func getMidRangeTime(r TimeRange) time.Time {
return r.max.Add(-delta / 2) 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 // time duration between grid lines
func calculateTimescale(refreshRateMs int) time.Duration { func calculateTimescale(refreshRateMs int) time.Duration {

View File

@ -3,9 +3,11 @@ package sparkline
import ( import (
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"github.com/sqshq/sampler/component" "github.com/sqshq/sampler/component"
"github.com/sqshq/sampler/component/util"
"github.com/sqshq/sampler/config" "github.com/sqshq/sampler/config"
"github.com/sqshq/sampler/console" "github.com/sqshq/sampler/console"
"github.com/sqshq/sampler/data" "github.com/sqshq/sampler/data"
"image"
"strconv" "strconv"
) )
@ -14,6 +16,10 @@ type SparkLine struct {
*data.Consumer *data.Consumer
alert *data.Alert alert *data.Alert
values []float64 values []float64
maxValue float64
minValue float64
scale int
color ui.Color
palette console.Palette palette console.Palette
} }
@ -23,6 +29,8 @@ func NewSparkLine(c config.SparkLineConfig, palette console.Palette) *SparkLine
Block: component.NewBlock(c.Title, true, palette), Block: component.NewBlock(c.Title, true, palette),
Consumer: data.NewConsumer(), Consumer: data.NewConsumer(),
values: []float64{}, values: []float64{},
scale: *c.Scale,
color: *c.Item.Color,
palette: palette, palette: palette,
} }
@ -54,8 +62,54 @@ func (s *SparkLine) consumeSample(sample *data.Sample) {
s.values = append(s.values, float) s.values = append(s.values, float)
// TODO cleanup old ones // 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) { 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) s.Block.Draw(buffer)
} }

26
component/util/format.go Normal file
View File

@ -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)
}
}

14
component/util/math.go Normal file
View File

@ -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
}

View File

@ -1,9 +1,9 @@
runcharts: runcharts:
- title: SEARCH ENGINE RESPONSE TIME (sec) - title: SEARCH ENGINE RESPONSE TIME (sec)
position: [[0, 0], [53, 16]] position: [[0, 0], [52, 16]]
triggers: triggers:
- title: Latency threshold exceeded - 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: actions:
terminal-bell: true terminal-bell: true
sound: true sound: true
@ -31,7 +31,7 @@ runcharts:
barcharts: barcharts:
- title: EVENTS BY STATUS - title: EVENTS BY STATUS
refresh-rate-ms: 1000 refresh-rate-ms: 1000
position: [[0, 17], [28, 12]] position: [[0, 17], [27, 12]]
scale: 0 scale: 0
items: items:
- label: NEW - label: NEW
@ -85,6 +85,6 @@ asciiboxes:
font: 3d font: 3d
sparklines: sparklines:
- title: CPU usage - title: CPU usage
position: [[28, 17], [25, 12]] position: [[27, 17], [25, 5]]
scale: 0 scale: 0
value: ps -A -o %cpu | awk '{s+=$1} END {print s}' value: ps -A -o %cpu | awk '{s+=$1} END {print s}'

View File

@ -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 { for i, g := range c.Gauges {
if g.Color == nil { if g.Color == nil {
g.Color = &palette.ContentColors[i%colorsCount] g.Color = &palette.ContentColors[i%colorsCount]

View File

@ -3,5 +3,5 @@ package console
const ( const (
SymbolSelection rune = '▲' SymbolSelection rune = '▲'
SymbolVerticalBar rune = '▎' SymbolVerticalBar rune = '▎'
SymbolShade rune = '═' SymbolHorizontalBar rune = '═'
) )