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
|
|
@ -17,7 +17,7 @@ func Load(location string) *Config {
|
||||||
log.Fatalf("Can't read config file: %s", location)
|
log.Fatalf("Can't read config file: %s", location)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := new(Config)
|
cfg := new(Config)
|
||||||
err = yaml.Unmarshal(yamlFile, cfg)
|
err = yaml.Unmarshal(yamlFile, cfg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -8,8 +8,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Label string `yaml:"label"`
|
Label string `yaml:"label"`
|
||||||
Color string `yaml:"color"`
|
Color string `yaml:"color"`
|
||||||
Script string `yaml:"script"`
|
Script string `yaml:"script"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
}
|
|
@ -3,4 +3,4 @@ package config
|
||||||
type Position struct {
|
type Position struct {
|
||||||
X int `yaml:"x"`
|
X int `yaml:"x"`
|
||||||
Y int `yaml:"y"`
|
Y int `yaml:"y"`
|
||||||
}
|
}
|
|
@ -3,4 +3,4 @@ package config
|
||||||
type Size struct {
|
type Size struct {
|
||||||
X int `yaml:"x"`
|
X int `yaml:"x"`
|
||||||
Y int `yaml:"y"`
|
Y int `yaml:"y"`
|
||||||
}
|
}
|
|
@ -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