timeplot implementation
This commit is contained in:
parent
1156de6dcf
commit
dd2be52339
|
@ -1,9 +0,0 @@
|
|||
package config
|
||||
|
||||
type LineChartConfig struct {
|
||||
Title string `yaml:"title"`
|
||||
Data []Data `yaml:"data"`
|
||||
Position Position `yaml:"position"`
|
||||
RefreshRateMs int `yaml:"refresh-rate-ms"`
|
||||
Scale string `yaml:"scale"`
|
||||
}
|
121
app/main.go
121
app/main.go
|
@ -1,121 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
ui "github.com/sqshq/termui"
|
||||
"github.com/sqshq/termui/widgets"
|
||||
"github.com/sqshq/vcmd/app/config"
|
||||
"log"
|
||||
"math"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
if err := ui.Init(); err != nil {
|
||||
log.Fatalf("failed to initialize termui: %v", err)
|
||||
}
|
||||
|
||||
defer ui.Close()
|
||||
|
||||
cfg := config.Load("/Users/sqshq/config.yml")
|
||||
|
||||
p1 := widgets.NewPlot()
|
||||
p1.Title = "dot-mode line Chart"
|
||||
p1.Marker = widgets.MarkerDot
|
||||
p1.Data = [][]float64{
|
||||
{0, 1, 2, 3},
|
||||
{4, 5, 6, 7},
|
||||
}
|
||||
p1.SetRect(50, 0, 100, 10)
|
||||
p1.DataLabels = []string{"hello"}
|
||||
p1.DotRune = '+'
|
||||
p1.AxesColor = ui.ColorWhite
|
||||
p1.LineColors[0] = ui.ColorCyan
|
||||
p1.DrawDirection = widgets.DrawLeft
|
||||
|
||||
for _, linechart := range cfg.LineCharts {
|
||||
for _, data := range linechart.Data {
|
||||
value, _ := data.NextValue()
|
||||
log.Printf("%s: %s - %v", linechart.Title, data.Label, value)
|
||||
}
|
||||
}
|
||||
|
||||
ui.Render(p1)
|
||||
|
||||
uiEvents := ui.PollEvents()
|
||||
for {
|
||||
e := <-uiEvents
|
||||
switch e.ID {
|
||||
case "q", "<C-c>":
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printChart() {
|
||||
if err := ui.Init(); err != nil {
|
||||
log.Fatalf("failed to initialize termui: %v", err)
|
||||
}
|
||||
defer ui.Close()
|
||||
|
||||
sinData := func() [][]float64 {
|
||||
n := 220
|
||||
data := make([][]float64, 2)
|
||||
data[0] = make([]float64, n)
|
||||
data[1] = make([]float64, n)
|
||||
for i := 0; i < n; i++ {
|
||||
data[0][i] = 1 + math.Sin(float64(i)/5)
|
||||
data[1][i] = 1 + math.Cos(float64(i)/5)
|
||||
}
|
||||
return data
|
||||
}()
|
||||
|
||||
p0 := widgets.NewPlot()
|
||||
p0.Title = "braille-mode Line Chart"
|
||||
p0.Data = sinData
|
||||
p0.SetRect(0, 0, 50, 15)
|
||||
p0.AxesColor = ui.ColorWhite
|
||||
p0.LineColors[0] = ui.ColorGreen
|
||||
|
||||
p1 := widgets.NewPlot()
|
||||
p1.Title = "dot-mode line Chart"
|
||||
p1.Marker = widgets.MarkerDot
|
||||
p1.Data = [][]float64{{1, 2, 3, 4, 5}}
|
||||
p1.SetRect(50, 0, 75, 10)
|
||||
p1.DotRune = '+'
|
||||
p1.AxesColor = ui.ColorWhite
|
||||
p1.LineColors[0] = ui.ColorYellow
|
||||
p1.DrawDirection = widgets.DrawLeft
|
||||
|
||||
p2 := widgets.NewPlot()
|
||||
p2.Title = "dot-mode Scatter Plot"
|
||||
p2.Marker = widgets.MarkerDot
|
||||
p2.Data = make([][]float64, 2)
|
||||
p2.Data[0] = []float64{1, 2, 3, 4, 5}
|
||||
p2.Data[1] = sinData[1][4:]
|
||||
p2.SetRect(0, 15, 50, 30)
|
||||
p2.AxesColor = ui.ColorWhite
|
||||
p2.LineColors[0] = ui.ColorCyan
|
||||
p2.Type = widgets.ScatterPlot
|
||||
|
||||
p3 := widgets.NewPlot()
|
||||
p3.Title = "braille-mode Scatter Plot"
|
||||
p3.Data = make([][]float64, 2)
|
||||
p3.Data[0] = []float64{1, 2, 3, 4, 5}
|
||||
p3.Data[1] = sinData[1][4:]
|
||||
p3.SetRect(45, 15, 80, 30)
|
||||
p3.AxesColor = ui.ColorWhite
|
||||
p3.LineColors[0] = ui.ColorCyan
|
||||
p3.Marker = widgets.MarkerBraille
|
||||
p3.Type = widgets.ScatterPlot
|
||||
|
||||
ui.Render(p0, p1, p2, p3)
|
||||
|
||||
uiEvents := ui.PollEvents()
|
||||
for {
|
||||
e := <-uiEvents
|
||||
switch e.ID {
|
||||
case "q", "<C-c>":
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
theme: dark / bright
|
||||
line-charts:
|
||||
- title: curl-latency
|
||||
data:
|
||||
- label: example.com
|
||||
script: curl -o /dev/null -s -w '%{time_total}' http://example.com
|
||||
color: red
|
||||
- label: google.com
|
||||
script: curl -o /dev/null -s -w '%{time_total}' http://google.com
|
||||
color: yellow
|
||||
refresh-rate-ms: 100
|
||||
style: dots/lines
|
||||
scale: log
|
||||
position:
|
||||
x: 10
|
||||
y: 20
|
||||
size:
|
||||
x: 100
|
||||
y: 50
|
||||
- title: mongo-count
|
||||
data:
|
||||
- label: posts
|
||||
script: mongo --quiet --host=localhost blog --eval "db.getCollection('post').find({}).size()" | grep 2
|
||||
color: red
|
|
@ -0,0 +1,9 @@
|
|||
package config
|
||||
|
||||
type LineChartConfig struct {
|
||||
Title string `yaml:"title"`
|
||||
Data []Data `yaml:"data"`
|
||||
Position Position `yaml:"position"`
|
||||
RefreshRateMs int `yaml:"refresh-rate-ms"`
|
||||
Scale string `yaml:"scale"`
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
ui "github.com/sqshq/termui"
|
||||
"github.com/sqshq/vcmd/config"
|
||||
"github.com/sqshq/vcmd/widgets"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
cfg := config.Load("/Users/sqshq/Go/src/github.com/sqshq/vcmd/config.yml")
|
||||
|
||||
for _, linechart := range cfg.LineCharts {
|
||||
for _, data := range linechart.Data {
|
||||
value, _ := data.NextValue()
|
||||
log.Printf("%s: %s - %v", linechart.Title, data.Label, value)
|
||||
}
|
||||
}
|
||||
|
||||
p1 := widgets.NewTimePlot()
|
||||
p1.Title = " CURL LATENCY STATISTICS (sec) "
|
||||
p1.SetRect(0, 20, 148, 40)
|
||||
p1.LineColors[0] = ui.ColorYellow
|
||||
p1.Marker = widgets.MarkerBraille
|
||||
|
||||
if err := ui.Init(); err != nil {
|
||||
//log.Fatalf("failed to initialize termui: %v", err)
|
||||
}
|
||||
|
||||
defer ui.Close()
|
||||
uiEvents := ui.PollEvents()
|
||||
|
||||
dataTicker := time.NewTicker(200 * time.Millisecond)
|
||||
uiTicker := time.NewTicker(50 * time.Millisecond)
|
||||
|
||||
pause := false
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-dataTicker.C:
|
||||
if !pause {
|
||||
value, err := cfg.LineCharts[0].Data[0].NextValue()
|
||||
if err != nil {
|
||||
log.Printf("failed to get value: %s", err)
|
||||
break
|
||||
}
|
||||
p1.AddValue(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case e := <-uiEvents:
|
||||
switch e.ID {
|
||||
case "q", "<C-c>": // press 'q' or 'C-c' to quit
|
||||
return
|
||||
}
|
||||
//case "<MouseLeft>":
|
||||
// payload := e.Payload.(ui.Mouse)
|
||||
// x, y := payload.X, payload.Y
|
||||
// log.Printf("x: %v, y: %v", x, y)
|
||||
//}
|
||||
switch e.Type {
|
||||
case ui.KeyboardEvent: // handle all key presses
|
||||
//log.Printf("key: %v", e.ID)
|
||||
switch e.ID {
|
||||
// TODO refactor + control moving out of range
|
||||
case "<Left>":
|
||||
rect := p1.GetRect()
|
||||
min := rect.Min
|
||||
max := rect.Max
|
||||
p1.SetRect(min.X-1, min.Y, max.X-1, max.Y)
|
||||
ui.Clear()
|
||||
case "<Right>":
|
||||
rect := p1.GetRect()
|
||||
min := rect.Min
|
||||
max := rect.Max
|
||||
p1.SetRect(min.X+1, min.Y, max.X+1, max.Y)
|
||||
ui.Clear()
|
||||
case "<Down>":
|
||||
rect := p1.GetRect()
|
||||
min := rect.Min
|
||||
max := rect.Max
|
||||
p1.SetRect(min.X, min.Y+1, max.X, max.Y+1)
|
||||
ui.Clear()
|
||||
case "<Up>":
|
||||
rect := p1.GetRect()
|
||||
min := rect.Min
|
||||
max := rect.Max
|
||||
p1.SetRect(min.X, min.Y-1, max.X, max.Y-1)
|
||||
ui.Clear()
|
||||
case "p":
|
||||
pause = !pause
|
||||
}
|
||||
}
|
||||
case <-uiTicker.C:
|
||||
if !pause {
|
||||
ui.Render(p1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package widgets
|
||||
|
||||
import (
|
||||
"image"
|
||||
)
|
||||
|
||||
const (
|
||||
xBrailleMultiplier = 2
|
||||
yBrailleMultiplier = 4
|
||||
)
|
||||
|
||||
func braille(point image.Point) image.Point {
|
||||
return image.Point{X: point.X * xBrailleMultiplier, Y: point.Y * yBrailleMultiplier}
|
||||
}
|
||||
|
||||
func deBraille(point image.Point) image.Point {
|
||||
return image.Point{X: point.X / xBrailleMultiplier, Y: point.Y / yBrailleMultiplier}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package widgets
|
||||
|
||||
import "github.com/sqshq/termui"
|
||||
|
||||
const (
|
||||
ColorDarkGrey termui.Color = 240
|
||||
)
|
|
@ -0,0 +1,265 @@
|
|||
package widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
. "github.com/sqshq/termui"
|
||||
)
|
||||
|
||||
type TimePlot struct {
|
||||
Block
|
||||
DataLabels []string
|
||||
MaxValueTimePoint TimePoint
|
||||
LineColors []Color
|
||||
ShowAxes bool
|
||||
DotRune rune
|
||||
HorizontalScale int
|
||||
Marker PlotMarker
|
||||
timePoints []TimePoint
|
||||
dataMutex *sync.Mutex
|
||||
grid PlotGrid
|
||||
}
|
||||
|
||||
const (
|
||||
xAxisLabelsHeight = 1
|
||||
xAxisLabelsWidth = 8
|
||||
xAxisLabelsGap = 2
|
||||
yAxisLabelsWidth = 5
|
||||
yAxisLabelsGap = 1
|
||||
)
|
||||
|
||||
type TimePoint struct {
|
||||
Value float64
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
type PlotMarker uint
|
||||
|
||||
const (
|
||||
MarkerBraille PlotMarker = iota
|
||||
MarkerDot
|
||||
)
|
||||
|
||||
func NewTimePlot() *TimePlot {
|
||||
return &TimePlot{
|
||||
Block: *NewBlock(),
|
||||
LineColors: Theme.Plot.Lines,
|
||||
DotRune: DOT,
|
||||
HorizontalScale: 1,
|
||||
ShowAxes: true,
|
||||
Marker: MarkerBraille,
|
||||
timePoints: make([]TimePoint, 0),
|
||||
dataMutex: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
type PlotGrid struct {
|
||||
count int
|
||||
maxTimeX int
|
||||
maxTime time.Time
|
||||
minTime time.Time
|
||||
maxValue float64
|
||||
minValue float64
|
||||
spacingDuration time.Duration
|
||||
spacingWidth int
|
||||
}
|
||||
|
||||
func (self *TimePlot) newPlotGrid() PlotGrid {
|
||||
|
||||
count := (self.Inner.Max.X - self.Inner.Min.X) / (xAxisLabelsGap + xAxisLabelsWidth)
|
||||
spacingDuration := time.Duration(time.Second) // TODO support others and/or adjust automatically depending on refresh rate
|
||||
maxTime := time.Now()
|
||||
minTime := maxTime.Add(-time.Duration(spacingDuration.Nanoseconds() * int64(count)))
|
||||
maxPoint, minPoint := GetMaxAndMinValueTimePoints(self.timePoints)
|
||||
|
||||
return PlotGrid{
|
||||
count: count,
|
||||
spacingDuration: spacingDuration,
|
||||
spacingWidth: xAxisLabelsGap + xAxisLabelsWidth,
|
||||
maxTimeX: self.Inner.Max.X - xAxisLabelsWidth/2 - xAxisLabelsGap,
|
||||
maxTime: maxTime,
|
||||
minTime: minTime,
|
||||
maxValue: maxPoint.Value,
|
||||
minValue: minPoint.Value,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TimePlot) Draw(buf *Buffer) {
|
||||
|
||||
self.dataMutex.Lock()
|
||||
self.Block.Draw(buf)
|
||||
self.grid = self.newPlotGrid()
|
||||
|
||||
if self.ShowAxes {
|
||||
self.plotAxes(buf)
|
||||
}
|
||||
|
||||
drawArea := self.Inner
|
||||
if self.ShowAxes {
|
||||
drawArea = image.Rect(
|
||||
self.Inner.Min.X+yAxisLabelsWidth+1, self.Inner.Min.Y,
|
||||
self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1,
|
||||
)
|
||||
}
|
||||
|
||||
self.renderBraille(buf, drawArea)
|
||||
self.dataMutex.Unlock()
|
||||
}
|
||||
|
||||
func (self *TimePlot) AddValue(value float64) {
|
||||
self.dataMutex.Lock()
|
||||
self.timePoints = append(self.timePoints, TimePoint{Value: value, Time: time.Now()})
|
||||
self.trimOutOfRangeValues()
|
||||
self.dataMutex.Unlock()
|
||||
}
|
||||
|
||||
func (self *TimePlot) trimOutOfRangeValues() {
|
||||
|
||||
lastOutOfRangeValueIndex := -1
|
||||
|
||||
for i, timePoint := range self.timePoints {
|
||||
if !self.isTimePointInRange(timePoint) {
|
||||
lastOutOfRangeValueIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
if lastOutOfRangeValueIndex > 0 {
|
||||
self.timePoints = append(self.timePoints[:0], self.timePoints[lastOutOfRangeValueIndex+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TimePlot) renderBraille(buf *Buffer, drawArea image.Rectangle) {
|
||||
|
||||
canvas := NewCanvas()
|
||||
canvas.Rectangle = drawArea
|
||||
|
||||
pointPerX := make(map[int]image.Point)
|
||||
pointsOrder := make([]int, 0)
|
||||
|
||||
for _, timePoint := range self.timePoints {
|
||||
|
||||
if !self.isTimePointInRange(timePoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
timeDeltaWithGridMaxTime := self.grid.maxTime.Sub(timePoint.Time)
|
||||
deltaToSpacingRelation := float64(timeDeltaWithGridMaxTime.Nanoseconds()) / float64(self.grid.spacingDuration.Nanoseconds())
|
||||
x := self.grid.maxTimeX - (int(float64(self.grid.spacingWidth) * deltaToSpacingRelation))
|
||||
|
||||
valuePerYDot := (self.grid.maxValue - self.grid.minValue) / float64(drawArea.Dy()-1)
|
||||
y := int(float64(timePoint.Value-self.grid.minValue) / valuePerYDot)
|
||||
|
||||
if _, exists := pointPerX[x]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
pointPerX[x] = image.Pt(x, drawArea.Max.Y-y-1)
|
||||
pointsOrder = append(pointsOrder, x)
|
||||
}
|
||||
|
||||
for i, x := range pointsOrder {
|
||||
|
||||
currentPoint := pointPerX[x]
|
||||
var previousPoint image.Point
|
||||
|
||||
if i == 0 {
|
||||
previousPoint = currentPoint
|
||||
} else {
|
||||
previousPoint = pointPerX[pointsOrder[i-1]]
|
||||
}
|
||||
|
||||
//buf.SetCell(
|
||||
// NewCell(self.DotRune, NewStyle(SelectColor(self.LineColors, 0))),
|
||||
// currentPoint,
|
||||
//)
|
||||
|
||||
canvas.Line(
|
||||
braille(previousPoint),
|
||||
braille(currentPoint),
|
||||
SelectColor(self.LineColors, 0), //i
|
||||
)
|
||||
}
|
||||
|
||||
canvas.Draw(buf)
|
||||
}
|
||||
|
||||
func (self *TimePlot) isTimePointInRange(point TimePoint) bool {
|
||||
return point.Time.After(self.grid.minTime.Add(self.grid.spacingDuration))
|
||||
}
|
||||
|
||||
func (self *TimePlot) plotAxes(buf *Buffer) {
|
||||
// draw origin cell
|
||||
buf.SetCell(
|
||||
NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||
)
|
||||
|
||||
// draw x axis line
|
||||
for i := yAxisLabelsWidth + 1; i < self.Inner.Dx(); i++ {
|
||||
buf.SetCell(
|
||||
NewCell(HORIZONTAL_DASH, NewStyle(ColorWhite)),
|
||||
image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||
)
|
||||
}
|
||||
|
||||
// draw grid
|
||||
for y := 0; y < self.Inner.Dy()-xAxisLabelsHeight-1; y = y + 2 {
|
||||
for x := 0; x < self.grid.count; x++ {
|
||||
buf.SetCell(
|
||||
NewCell(VERTICAL_DASH, NewStyle(ColorDarkGrey)),
|
||||
image.Pt(self.grid.maxTimeX-x*self.grid.spacingWidth, y+self.Inner.Min.Y+1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// draw y axis line
|
||||
for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
||||
buf.SetCell(
|
||||
NewCell(VERTICAL_DASH, NewStyle(ColorWhite)),
|
||||
image.Pt(self.Inner.Min.X+yAxisLabelsWidth, i+self.Inner.Min.Y),
|
||||
)
|
||||
}
|
||||
|
||||
// draw x axis time labels
|
||||
for i := 0; i < self.grid.count; i++ {
|
||||
labelTime := self.grid.maxTime.Add(time.Duration(-i) * time.Second)
|
||||
buf.SetString(
|
||||
labelTime.Format("15:04:05"),
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(self.grid.maxTimeX-xAxisLabelsWidth/2-i*(self.grid.spacingWidth), self.Inner.Max.Y-1),
|
||||
)
|
||||
}
|
||||
|
||||
// draw y axis labels
|
||||
verticalScale := self.grid.maxValue - self.grid.minValue/float64(self.Inner.Dy()-xAxisLabelsHeight-1)
|
||||
for i := 1; i*(yAxisLabelsGap+1) <= self.Inner.Dy()-1; i++ {
|
||||
buf.SetString(
|
||||
fmt.Sprintf("%.3f", float64(i)*self.grid.minValue*verticalScale*(yAxisLabelsGap+1)),
|
||||
NewStyle(ColorWhite),
|
||||
image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func GetMaxAndMinValueTimePoints(points []TimePoint) (TimePoint, TimePoint) {
|
||||
|
||||
if len(points) == 0 {
|
||||
return TimePoint{0, time.Now()}, TimePoint{0, time.Now()}
|
||||
}
|
||||
|
||||
var max, min = points[0], points[0]
|
||||
|
||||
for _, point := range points {
|
||||
if point.Value > max.Value {
|
||||
max = point
|
||||
}
|
||||
if point.Value < min.Value {
|
||||
min = point
|
||||
}
|
||||
}
|
||||
|
||||
return max, min
|
||||
}
|
Loading…
Reference in New Issue