sparkline main functionality
This commit is contained in:
parent
db3888e540
commit
e67ac7c19a
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -3,18 +3,24 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SparkLine struct {
|
type SparkLine struct {
|
||||||
*ui.Block
|
*ui.Block
|
||||||
*data.Consumer
|
*data.Consumer
|
||||||
alert *data.Alert
|
alert *data.Alert
|
||||||
values []float64
|
values []float64
|
||||||
palette console.Palette
|
maxValue float64
|
||||||
|
minValue float64
|
||||||
|
scale int
|
||||||
|
color ui.Color
|
||||||
|
palette console.Palette
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSparkLine(c config.SparkLineConfig, palette console.Palette) *SparkLine {
|
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),
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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}'
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package console
|
package console
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SymbolSelection rune = '▲'
|
SymbolSelection rune = '▲'
|
||||||
SymbolVerticalBar rune = '▎'
|
SymbolVerticalBar rune = '▎'
|
||||||
SymbolShade rune = '═'
|
SymbolHorizontalBar rune = '═'
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue