legend refactoring
This commit is contained in:
parent
aa24aa4bd9
commit
ae6e895d8a
|
@ -8,7 +8,7 @@ runcharts:
|
||||||
script: curl -o /dev/null -s -w '%{time_total}' https://search.yahoo.com/
|
script: curl -o /dev/null -s -w '%{time_total}' https://search.yahoo.com/
|
||||||
- label: BING
|
- label: BING
|
||||||
script: curl -o /dev/null -s -w '%{time_total}' https://www.bing.com/
|
script: curl -o /dev/null -s -w '%{time_total}' https://www.bing.com/
|
||||||
refresh-rate-ms: 200
|
refresh-rate-ms: 500
|
||||||
decimal-places: 3
|
decimal-places: 3
|
||||||
alert:
|
alert:
|
||||||
value:
|
value:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/sqshq/sampler/console"
|
"github.com/sqshq/sampler/console"
|
||||||
"github.com/sqshq/sampler/data"
|
"github.com/sqshq/sampler/data"
|
||||||
. "github.com/sqshq/sampler/widgets"
|
. "github.com/sqshq/sampler/widgets"
|
||||||
|
@ -17,22 +16,28 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunChartConfig struct {
|
type RunChartConfig struct {
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
Items []data.Item `yaml:"items"`
|
Items []data.Item `yaml:"items"`
|
||||||
Position Position `yaml:"position"`
|
Position Position `yaml:"position"`
|
||||||
Size Size `yaml:"size"`
|
Size Size `yaml:"size"`
|
||||||
RefreshRateMs int `yaml:"refresh-rate-ms"`
|
RefreshRateMs int `yaml:"refresh-rate-ms"`
|
||||||
Precision int `yaml:"decimal-places"`
|
Precision int `yaml:"decimal-places"`
|
||||||
|
Legend LegendConfig `yaml:"legend"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load(args []string) *Config {
|
type LegendConfig struct {
|
||||||
|
Enabled bool `yaml:"enabled"`
|
||||||
|
Details bool `yaml:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) < 2 {
|
func Load() *Config {
|
||||||
fmt.Fprintf(os.Stderr, "Please specify config file location. See www.github.com/sqshq/sampler for the reference\n")
|
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
println("Please specify config file location. See www.github.com/sqshq/sampler for the reference")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := readFile(args[1])
|
cfg := readFile(os.Args[1])
|
||||||
cfg.validate()
|
cfg.validate()
|
||||||
cfg.setDefaultValues()
|
cfg.setDefaultValues()
|
||||||
cfg.setDefaultColors()
|
cfg.setDefaultColors()
|
||||||
|
|
7
main.go
7
main.go
|
@ -6,14 +6,14 @@ import (
|
||||||
"github.com/sqshq/sampler/data"
|
"github.com/sqshq/sampler/data"
|
||||||
"github.com/sqshq/sampler/event"
|
"github.com/sqshq/sampler/event"
|
||||||
"github.com/sqshq/sampler/widgets"
|
"github.com/sqshq/sampler/widgets"
|
||||||
|
"github.com/sqshq/sampler/widgets/runchart"
|
||||||
ui "github.com/sqshq/termui"
|
ui "github.com/sqshq/termui"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
cfg := config.Load(os.Args)
|
cfg := config.Load()
|
||||||
csl := console.Console{}
|
csl := console.Console{}
|
||||||
csl.Init()
|
csl.Init()
|
||||||
defer csl.Close()
|
defer csl.Close()
|
||||||
|
@ -23,7 +23,8 @@ func main() {
|
||||||
|
|
||||||
for _, c := range cfg.RunCharts {
|
for _, c := range cfg.RunCharts {
|
||||||
|
|
||||||
chart := widgets.NewRunChart(c.Title, c.Precision, c.RefreshRateMs)
|
legend := runchart.Legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details}
|
||||||
|
chart := runchart.NewRunChart(c.Title, c.Precision, c.RefreshRateMs, legend)
|
||||||
layout.AddComponent(chart, c.Title, c.Position, c.Size, widgets.TypeRunChart)
|
layout.AddComponent(chart, c.Title, c.Position, c.Size, widgets.TypeRunChart)
|
||||||
|
|
||||||
for _, item := range c.Items {
|
for _, item := range c.Items {
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
package widgets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
xBrailleMultiplier = 2
|
|
||||||
yBrailleMultiplier = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
func braillePoint(point image.Point) image.Point {
|
|
||||||
return image.Point{X: point.X * xBrailleMultiplier, Y: point.Y * yBrailleMultiplier}
|
|
||||||
}
|
|
||||||
|
|
||||||
func debraillePoint(point image.Point) image.Point {
|
|
||||||
return image.Point{X: point.X / xBrailleMultiplier, Y: point.Y / yBrailleMultiplier}
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sqshq/sampler/console"
|
"github.com/sqshq/sampler/console"
|
||||||
|
"github.com/sqshq/sampler/widgets/runchart"
|
||||||
ui "github.com/sqshq/termui"
|
ui "github.com/sqshq/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
case MenuOptionPinpoint:
|
case MenuOptionPinpoint:
|
||||||
l.mode = ModeChartPinpoint
|
l.mode = ModeChartPinpoint
|
||||||
l.menu.idle()
|
l.menu.idle()
|
||||||
chart := l.getSelectedComponent().Drawable.(*RunChart)
|
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
||||||
chart.MoveSelection(0)
|
chart.MoveSelection(0)
|
||||||
case MenuOptionResume:
|
case MenuOptionResume:
|
||||||
l.mode = ModeDefault
|
l.mode = ModeDefault
|
||||||
|
@ -94,7 +95,7 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
case console.KeyEsc:
|
case console.KeyEsc:
|
||||||
switch l.mode {
|
switch l.mode {
|
||||||
case ModeChartPinpoint:
|
case ModeChartPinpoint:
|
||||||
chart := l.getSelectedComponent().Drawable.(*RunChart)
|
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
||||||
chart.DisableSelection()
|
chart.DisableSelection()
|
||||||
fallthrough
|
fallthrough
|
||||||
case ModeComponentSelect:
|
case ModeComponentSelect:
|
||||||
|
@ -110,7 +111,7 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
l.selection = 0
|
l.selection = 0
|
||||||
l.menu.highlight(l.getComponent(l.selection))
|
l.menu.highlight(l.getComponent(l.selection))
|
||||||
case ModeChartPinpoint:
|
case ModeChartPinpoint:
|
||||||
chart := l.getSelectedComponent().Drawable.(*RunChart)
|
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
||||||
chart.MoveSelection(-1)
|
chart.MoveSelection(-1)
|
||||||
case ModeComponentSelect:
|
case ModeComponentSelect:
|
||||||
if l.selection > 0 {
|
if l.selection > 0 {
|
||||||
|
@ -129,7 +130,7 @@ func (l *Layout) HandleConsoleEvent(e string) {
|
||||||
l.selection = 0
|
l.selection = 0
|
||||||
l.menu.highlight(l.getComponent(l.selection))
|
l.menu.highlight(l.getComponent(l.selection))
|
||||||
case ModeChartPinpoint:
|
case ModeChartPinpoint:
|
||||||
chart := l.getSelectedComponent().Drawable.(*RunChart)
|
chart := l.getSelectedComponent().Drawable.(*runchart.RunChart)
|
||||||
chart.MoveSelection(1)
|
chart.MoveSelection(1)
|
||||||
case ModeComponentSelect:
|
case ModeComponentSelect:
|
||||||
if l.selection < len(l.components)-1 {
|
if l.selection < len(l.components)-1 {
|
||||||
|
|
|
@ -1,511 +0,0 @@
|
||||||
package widgets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/sqshq/sampler/console"
|
|
||||||
"github.com/sqshq/sampler/data"
|
|
||||||
"image"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
ui "github.com/sqshq/termui"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO split into runchart, grid, legend files
|
|
||||||
const (
|
|
||||||
xAxisLegendWidth = 20
|
|
||||||
xAxisLabelsHeight = 1
|
|
||||||
xAxisLabelsWidth = 8
|
|
||||||
xAxisLabelsGap = 2
|
|
||||||
xAxisGridWidth = xAxisLabelsGap + xAxisLabelsWidth
|
|
||||||
yAxisLabelsHeight = 1
|
|
||||||
yAxisLabelsGap = 1
|
|
||||||
|
|
||||||
historyReserveHrs = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
type ScrollMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
Auto ScrollMode = 0
|
|
||||||
Manual ScrollMode = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
type RunChart struct {
|
|
||||||
ui.Block
|
|
||||||
lines []TimeLine
|
|
||||||
grid ChartGrid
|
|
||||||
timescale time.Duration
|
|
||||||
mutex *sync.Mutex
|
|
||||||
scrollMode ScrollMode
|
|
||||||
selection time.Time
|
|
||||||
precision int
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartGrid struct {
|
|
||||||
timeRange TimeRange
|
|
||||||
timePerPoint time.Duration
|
|
||||||
valueExtrema ValueExtrema
|
|
||||||
linesCount int
|
|
||||||
maxTimeWidth int
|
|
||||||
minTimeWidth int
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimePoint struct {
|
|
||||||
value float64
|
|
||||||
time time.Time
|
|
||||||
coordinate int
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimeLine struct {
|
|
||||||
points []TimePoint
|
|
||||||
color ui.Color
|
|
||||||
label string
|
|
||||||
selection int
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimeRange struct {
|
|
||||||
max time.Time
|
|
||||||
min time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValueExtrema struct {
|
|
||||||
max float64
|
|
||||||
min float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRunChart(title string, precision int, refreshRateMs int) *RunChart {
|
|
||||||
block := *ui.NewBlock()
|
|
||||||
block.Title = title
|
|
||||||
return &RunChart{
|
|
||||||
Block: block,
|
|
||||||
lines: []TimeLine{},
|
|
||||||
timescale: calculateTimescale(refreshRateMs),
|
|
||||||
mutex: &sync.Mutex{},
|
|
||||||
precision: precision,
|
|
||||||
scrollMode: Auto,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) newChartGrid() ChartGrid {
|
|
||||||
|
|
||||||
linesCount := (self.Inner.Max.X - self.Inner.Min.X - self.grid.minTimeWidth) / xAxisGridWidth
|
|
||||||
timeRange := self.getTimeRange(linesCount)
|
|
||||||
|
|
||||||
return ChartGrid{
|
|
||||||
timeRange: timeRange,
|
|
||||||
timePerPoint: self.timescale / time.Duration(xAxisGridWidth),
|
|
||||||
valueExtrema: getValueExtrema(self.lines, timeRange),
|
|
||||||
linesCount: linesCount,
|
|
||||||
maxTimeWidth: self.Inner.Max.X,
|
|
||||||
minTimeWidth: self.getMaxValueLength(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) newTimePoint(value float64) TimePoint {
|
|
||||||
now := time.Now()
|
|
||||||
return TimePoint{
|
|
||||||
value: value,
|
|
||||||
time: now,
|
|
||||||
coordinate: self.calculateTimeCoordinate(now),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) Draw(buffer *ui.Buffer) {
|
|
||||||
|
|
||||||
self.mutex.Lock()
|
|
||||||
self.Block.Draw(buffer)
|
|
||||||
self.grid = self.newChartGrid()
|
|
||||||
|
|
||||||
drawArea := image.Rect(
|
|
||||||
self.Inner.Min.X+self.grid.minTimeWidth+1, self.Inner.Min.Y,
|
|
||||||
self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.renderAxes(buffer)
|
|
||||||
self.renderLines(buffer, drawArea)
|
|
||||||
self.renderLegend(buffer, drawArea)
|
|
||||||
self.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) ConsumeSample(sample data.Sample) {
|
|
||||||
|
|
||||||
float, err := strconv.ParseFloat(sample.Value, 64)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// TODO visual notification + check sample.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
self.mutex.Lock()
|
|
||||||
|
|
||||||
lineIndex := -1
|
|
||||||
|
|
||||||
for i, line := range self.lines {
|
|
||||||
if line.label == sample.Label {
|
|
||||||
lineIndex = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lineIndex == -1 {
|
|
||||||
line := &TimeLine{
|
|
||||||
points: []TimePoint{},
|
|
||||||
color: sample.Color,
|
|
||||||
label: sample.Label,
|
|
||||||
}
|
|
||||||
self.lines = append(self.lines, *line)
|
|
||||||
lineIndex = len(self.lines) - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
line := self.lines[lineIndex]
|
|
||||||
timePoint := self.newTimePoint(float)
|
|
||||||
line.points = append(line.points, timePoint)
|
|
||||||
self.lines[lineIndex] = line
|
|
||||||
|
|
||||||
self.trimOutOfRangeValues()
|
|
||||||
self.mutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) renderLines(buffer *ui.Buffer, drawArea image.Rectangle) {
|
|
||||||
|
|
||||||
canvas := ui.NewCanvas()
|
|
||||||
canvas.Rectangle = drawArea
|
|
||||||
|
|
||||||
if len(self.lines) == 0 || len(self.lines[0].points) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionCoordinate := self.calculateTimeCoordinate(self.selection)
|
|
||||||
selectionPoints := make(map[int]image.Point)
|
|
||||||
|
|
||||||
probe := self.lines[0].points[0]
|
|
||||||
delta := ui.AbsInt(self.calculateTimeCoordinate(probe.time) - probe.coordinate)
|
|
||||||
|
|
||||||
for i, line := range self.lines {
|
|
||||||
|
|
||||||
xPoint := make(map[int]image.Point)
|
|
||||||
xOrder := make([]int, 0)
|
|
||||||
|
|
||||||
if line.selection != 0 {
|
|
||||||
line.selection -= delta
|
|
||||||
self.lines[i].selection = line.selection
|
|
||||||
}
|
|
||||||
|
|
||||||
for j, timePoint := range line.points {
|
|
||||||
|
|
||||||
timePoint.coordinate -= delta
|
|
||||||
line.points[j] = timePoint
|
|
||||||
|
|
||||||
var y int
|
|
||||||
if self.grid.valueExtrema.max == self.grid.valueExtrema.min {
|
|
||||||
y = (drawArea.Dy() - 2) / 2
|
|
||||||
} else {
|
|
||||||
valuePerY := (self.grid.valueExtrema.max - self.grid.valueExtrema.min) / float64(drawArea.Dy()-2)
|
|
||||||
y = int(float64(timePoint.value-self.grid.valueExtrema.min) / valuePerY)
|
|
||||||
}
|
|
||||||
|
|
||||||
point := image.Pt(timePoint.coordinate, drawArea.Max.Y-y-1)
|
|
||||||
|
|
||||||
if _, exists := xPoint[point.X]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !point.In(drawArea) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if line.selection == 0 {
|
|
||||||
if len(line.points) > j+1 && ui.AbsInt(timePoint.coordinate-selectionCoordinate) > ui.AbsInt(line.points[j+1].coordinate-selectionCoordinate) {
|
|
||||||
selectionPoints[i] = point
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if timePoint.coordinate == line.selection {
|
|
||||||
selectionPoints[i] = point
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
xPoint[point.X] = point
|
|
||||||
xOrder = append(xOrder, point.X)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, x := range xOrder {
|
|
||||||
|
|
||||||
currentPoint := xPoint[x]
|
|
||||||
var previousPoint image.Point
|
|
||||||
|
|
||||||
if i == 0 {
|
|
||||||
previousPoint = currentPoint
|
|
||||||
} else {
|
|
||||||
previousPoint = xPoint[xOrder[i-1]]
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.Line(
|
|
||||||
braillePoint(previousPoint),
|
|
||||||
braillePoint(currentPoint),
|
|
||||||
line.color,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.Draw(buffer)
|
|
||||||
|
|
||||||
if self.scrollMode == Manual {
|
|
||||||
for lineIndex, point := range selectionPoints {
|
|
||||||
buffer.SetCell(ui.NewCell(console.SymbolSelection, ui.NewStyle(self.lines[lineIndex].color)), point)
|
|
||||||
if self.lines[lineIndex].selection == 0 {
|
|
||||||
self.lines[lineIndex].selection = point.X
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) renderAxes(buffer *ui.Buffer) {
|
|
||||||
// draw origin cell
|
|
||||||
buffer.SetCell(
|
|
||||||
ui.NewCell(ui.BOTTOM_LEFT, ui.NewStyle(ui.ColorWhite)),
|
|
||||||
image.Pt(self.Inner.Min.X+self.grid.minTimeWidth, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
|
||||||
)
|
|
||||||
|
|
||||||
// draw x axis line
|
|
||||||
for i := self.grid.minTimeWidth + 1; i < self.Inner.Dx(); i++ {
|
|
||||||
buffer.SetCell(
|
|
||||||
ui.NewCell(ui.HORIZONTAL_DASH, ui.NewStyle(ui.ColorWhite)),
|
|
||||||
image.Pt(i+self.Inner.Min.X, self.Inner.Max.Y-xAxisLabelsHeight-1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw grid lines
|
|
||||||
for y := 0; y < self.Inner.Dy()-xAxisLabelsHeight-2; y = y + 2 {
|
|
||||||
for x := 1; x <= self.grid.linesCount; x++ {
|
|
||||||
buffer.SetCell(
|
|
||||||
ui.NewCell(ui.VERTICAL_DASH, ui.NewStyle(console.ColorDarkGrey)),
|
|
||||||
image.Pt(self.grid.maxTimeWidth-x*xAxisGridWidth, y+self.Inner.Min.Y+1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw y axis line
|
|
||||||
for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
|
||||||
buffer.SetCell(
|
|
||||||
ui.NewCell(ui.VERTICAL_DASH, ui.NewStyle(ui.ColorWhite)),
|
|
||||||
image.Pt(self.Inner.Min.X+self.grid.minTimeWidth, i+self.Inner.Min.Y),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw x axis time labels
|
|
||||||
for i := 1; i <= self.grid.linesCount; i++ {
|
|
||||||
labelTime := self.grid.timeRange.max.Add(time.Duration(-i) * self.timescale)
|
|
||||||
buffer.SetString(
|
|
||||||
labelTime.Format("15:04:05"),
|
|
||||||
ui.NewStyle(ui.ColorWhite),
|
|
||||||
image.Pt(self.grid.maxTimeWidth-xAxisLabelsWidth/2-i*(xAxisGridWidth), self.Inner.Max.Y-1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw y axis labels
|
|
||||||
if self.grid.valueExtrema.max != self.grid.valueExtrema.min {
|
|
||||||
labelsCount := (self.Inner.Dy() - xAxisLabelsHeight - 1) / (yAxisLabelsGap + yAxisLabelsHeight)
|
|
||||||
valuePerY := (self.grid.valueExtrema.max - self.grid.valueExtrema.min) / float64(self.Inner.Dy()-xAxisLabelsHeight-3)
|
|
||||||
for i := 0; i < int(labelsCount); i++ {
|
|
||||||
value := self.grid.valueExtrema.max - (valuePerY * float64(i) * (yAxisLabelsGap + yAxisLabelsHeight))
|
|
||||||
buffer.SetString(
|
|
||||||
formatValue(value, self.precision),
|
|
||||||
ui.NewStyle(ui.ColorWhite),
|
|
||||||
image.Pt(self.Inner.Min.X, 1+self.Inner.Min.Y+i*(yAxisLabelsGap+yAxisLabelsHeight)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buffer.SetString(
|
|
||||||
formatValue(self.grid.valueExtrema.max, self.precision),
|
|
||||||
ui.NewStyle(ui.ColorWhite),
|
|
||||||
image.Pt(self.Inner.Min.X, self.Inner.Min.Y+self.Inner.Dy()/2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
|
|
||||||
|
|
||||||
for i, line := range self.lines {
|
|
||||||
|
|
||||||
extremum := getLineValueExtremum(line.points)
|
|
||||||
|
|
||||||
buffer.SetString(
|
|
||||||
string(ui.DOT),
|
|
||||||
ui.NewStyle(line.color),
|
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth-2, self.Inner.Min.Y+1+i*5),
|
|
||||||
)
|
|
||||||
buffer.SetString(
|
|
||||||
fmt.Sprintf("%s", line.label),
|
|
||||||
ui.NewStyle(line.color),
|
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+1+i*5),
|
|
||||||
)
|
|
||||||
buffer.SetString(
|
|
||||||
fmt.Sprintf("cur %s", formatValue(line.points[len(line.points)-1].value, self.precision)),
|
|
||||||
ui.NewStyle(ui.ColorWhite),
|
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+2+i*5),
|
|
||||||
)
|
|
||||||
buffer.SetString(
|
|
||||||
fmt.Sprintf("max %s", formatValue(extremum.max, self.precision)),
|
|
||||||
ui.NewStyle(ui.ColorWhite),
|
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+3+i*5),
|
|
||||||
)
|
|
||||||
buffer.SetString(
|
|
||||||
fmt.Sprintf("min %s", formatValue(extremum.min, self.precision)),
|
|
||||||
ui.NewStyle(ui.ColorWhite),
|
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+4+i*5),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) trimOutOfRangeValues() {
|
|
||||||
|
|
||||||
minRangeTime := self.grid.timeRange.min.Add(-time.Hour * time.Duration(historyReserveHrs))
|
|
||||||
|
|
||||||
for i, item := range self.lines {
|
|
||||||
lastOutOfRangeValueIndex := -1
|
|
||||||
|
|
||||||
for j, point := range item.points {
|
|
||||||
if point.time.Before(minRangeTime) {
|
|
||||||
lastOutOfRangeValueIndex = j
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lastOutOfRangeValueIndex > 0 {
|
|
||||||
item.points = append(item.points[:0], item.points[lastOutOfRangeValueIndex+1:]...)
|
|
||||||
self.lines[i] = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) calculateTimeCoordinate(t time.Time) int {
|
|
||||||
timeDeltaWithGridMaxTime := self.grid.timeRange.max.Sub(t).Nanoseconds()
|
|
||||||
timeDeltaToPaddingRelation := float64(timeDeltaWithGridMaxTime) / float64(self.timescale.Nanoseconds())
|
|
||||||
return self.grid.maxTimeWidth - int(math.Ceil(float64(xAxisGridWidth)*timeDeltaToPaddingRelation))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) getMaxValueLength() int {
|
|
||||||
|
|
||||||
maxValueLength := 0
|
|
||||||
|
|
||||||
for _, line := range self.lines {
|
|
||||||
for _, point := range line.points {
|
|
||||||
l := len(formatValue(point.value, self.precision))
|
|
||||||
if l > maxValueLength {
|
|
||||||
maxValueLength = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxValueLength
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) MoveSelection(shift int) {
|
|
||||||
|
|
||||||
if self.scrollMode == Auto {
|
|
||||||
self.scrollMode = Manual
|
|
||||||
self.selection = getMidRangeTime(self.grid.timeRange)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
self.selection = self.selection.Add(self.grid.timePerPoint * time.Duration(shift))
|
|
||||||
if self.selection.After(self.grid.timeRange.max) {
|
|
||||||
self.selection = self.grid.timeRange.max
|
|
||||||
} else if self.selection.Before(self.grid.timeRange.min) {
|
|
||||||
self.selection = self.grid.timeRange.min
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range self.lines {
|
|
||||||
self.lines[i].selection = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) DisableSelection() {
|
|
||||||
if self.scrollMode == Manual {
|
|
||||||
self.scrollMode = Auto
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMidRangeTime(r TimeRange) time.Time {
|
|
||||||
delta := r.max.Sub(r.min)
|
|
||||||
return r.max.Add(-delta / 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatValue(value float64, precision int) string {
|
|
||||||
format := "%." + strconv.Itoa(precision) + "f"
|
|
||||||
return fmt.Sprintf(format, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getValueExtrema(items []TimeLine, timeRange TimeRange) ValueExtrema {
|
|
||||||
|
|
||||||
if len(items) == 0 {
|
|
||||||
return ValueExtrema{0, 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
var max, min = -math.MaxFloat64, math.MaxFloat64
|
|
||||||
|
|
||||||
for _, item := range items {
|
|
||||||
for _, point := range item.points {
|
|
||||||
if point.value > max && timeRange.isInRange(point.time) {
|
|
||||||
max = point.value
|
|
||||||
}
|
|
||||||
if point.value < min && timeRange.isInRange(point.time) {
|
|
||||||
min = point.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValueExtrema{max: max, min: min}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimeRange) isInRange(time time.Time) bool {
|
|
||||||
return time.After(r.min) && time.Before(r.max)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getLineValueExtremum(points []TimePoint) ValueExtrema {
|
|
||||||
|
|
||||||
if len(points) == 0 {
|
|
||||||
return ValueExtrema{0, 0}
|
|
||||||
}
|
|
||||||
|
|
||||||
var max, min = -math.MaxFloat64, math.MaxFloat64
|
|
||||||
|
|
||||||
for _, point := range points {
|
|
||||||
if point.value > max {
|
|
||||||
max = point.value
|
|
||||||
}
|
|
||||||
if point.value < min {
|
|
||||||
min = point.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValueExtrema{max: max, min: min}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) getTimeRange(linesCount int) TimeRange {
|
|
||||||
|
|
||||||
if self.scrollMode == Manual {
|
|
||||||
return self.grid.timeRange
|
|
||||||
}
|
|
||||||
|
|
||||||
width := time.Duration(self.timescale.Nanoseconds() * int64(linesCount))
|
|
||||||
max := time.Now()
|
|
||||||
|
|
||||||
return TimeRange{
|
|
||||||
max: max,
|
|
||||||
min: max.Add(-width),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// time duration between grid lines
|
|
||||||
func calculateTimescale(refreshRateMs int) time.Duration {
|
|
||||||
|
|
||||||
multiplier := refreshRateMs * xAxisGridWidth / 2
|
|
||||||
timescale := time.Duration(time.Millisecond * time.Duration(multiplier)).Round(time.Second)
|
|
||||||
|
|
||||||
if timescale.Seconds() == 0 {
|
|
||||||
return time.Second
|
|
||||||
} else {
|
|
||||||
return timescale
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
package runchart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sqshq/sampler/console"
|
||||||
|
ui "github.com/sqshq/termui"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChartGrid struct {
|
||||||
|
timeRange TimeRange
|
||||||
|
timePerPoint time.Duration
|
||||||
|
valueExtrema ValueExtrema
|
||||||
|
linesCount int
|
||||||
|
maxTimeWidth int
|
||||||
|
minTimeWidth int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) newChartGrid() ChartGrid {
|
||||||
|
|
||||||
|
linesCount := (c.Inner.Max.X - c.Inner.Min.X - c.grid.minTimeWidth) / xAxisGridWidth
|
||||||
|
timeRange := c.getTimeRange(linesCount)
|
||||||
|
|
||||||
|
return ChartGrid{
|
||||||
|
timeRange: timeRange,
|
||||||
|
timePerPoint: c.timescale / time.Duration(xAxisGridWidth),
|
||||||
|
valueExtrema: getLocalExtrema(c.lines, timeRange),
|
||||||
|
linesCount: linesCount,
|
||||||
|
maxTimeWidth: c.Inner.Max.X,
|
||||||
|
minTimeWidth: c.getMaxValueLength(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) renderAxes(buffer *ui.Buffer) {
|
||||||
|
// draw origin cell
|
||||||
|
buffer.SetCell(
|
||||||
|
ui.NewCell(ui.BOTTOM_LEFT, ui.NewStyle(ui.ColorWhite)),
|
||||||
|
image.Pt(c.Inner.Min.X+c.grid.minTimeWidth, c.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||||
|
)
|
||||||
|
|
||||||
|
// draw x axis line
|
||||||
|
for i := c.grid.minTimeWidth + 1; i < c.Inner.Dx(); i++ {
|
||||||
|
buffer.SetCell(
|
||||||
|
ui.NewCell(ui.HORIZONTAL_DASH, ui.NewStyle(ui.ColorWhite)),
|
||||||
|
image.Pt(i+c.Inner.Min.X, c.Inner.Max.Y-xAxisLabelsHeight-1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw grid lines
|
||||||
|
for y := 0; y < c.Inner.Dy()-xAxisLabelsHeight-2; y = y + 2 {
|
||||||
|
for x := 1; x <= c.grid.linesCount; x++ {
|
||||||
|
buffer.SetCell(
|
||||||
|
ui.NewCell(ui.VERTICAL_DASH, ui.NewStyle(console.ColorDarkGrey)),
|
||||||
|
image.Pt(c.grid.maxTimeWidth-x*xAxisGridWidth, y+c.Inner.Min.Y+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw y axis line
|
||||||
|
for i := 0; i < c.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
||||||
|
buffer.SetCell(
|
||||||
|
ui.NewCell(ui.VERTICAL_DASH, ui.NewStyle(ui.ColorWhite)),
|
||||||
|
image.Pt(c.Inner.Min.X+c.grid.minTimeWidth, i+c.Inner.Min.Y),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw x axis time labels
|
||||||
|
for i := 1; i <= c.grid.linesCount; i++ {
|
||||||
|
labelTime := c.grid.timeRange.max.Add(time.Duration(-i) * c.timescale)
|
||||||
|
buffer.SetString(
|
||||||
|
labelTime.Format("15:04:05"),
|
||||||
|
ui.NewStyle(ui.ColorWhite),
|
||||||
|
image.Pt(c.grid.maxTimeWidth-xAxisLabelsWidth/2-i*(xAxisGridWidth), c.Inner.Max.Y-1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw y axis labels
|
||||||
|
if c.grid.valueExtrema.max != c.grid.valueExtrema.min {
|
||||||
|
labelsCount := (c.Inner.Dy() - xAxisLabelsHeight - 1) / (yAxisLabelsIndent + yAxisLabelsHeight)
|
||||||
|
valuePerY := (c.grid.valueExtrema.max - c.grid.valueExtrema.min) / float64(c.Inner.Dy()-xAxisLabelsHeight-3)
|
||||||
|
for i := 0; i < int(labelsCount); i++ {
|
||||||
|
value := c.grid.valueExtrema.max - (valuePerY * float64(i) * (yAxisLabelsIndent + yAxisLabelsHeight))
|
||||||
|
buffer.SetString(
|
||||||
|
formatValue(value, c.precision),
|
||||||
|
ui.NewStyle(ui.ColorWhite),
|
||||||
|
image.Pt(c.Inner.Min.X, 1+c.Inner.Min.Y+i*(yAxisLabelsIndent+yAxisLabelsHeight)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.SetString(
|
||||||
|
formatValue(c.grid.valueExtrema.max, c.precision),
|
||||||
|
ui.NewStyle(ui.ColorWhite),
|
||||||
|
image.Pt(c.Inner.Min.X, c.Inner.Min.Y+c.Inner.Dy()/2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) getTimeRange(linesCount int) TimeRange {
|
||||||
|
|
||||||
|
if c.mode == Pinpoint {
|
||||||
|
return c.grid.timeRange
|
||||||
|
}
|
||||||
|
|
||||||
|
width := time.Duration(c.timescale.Nanoseconds() * int64(linesCount))
|
||||||
|
max := time.Now()
|
||||||
|
|
||||||
|
return TimeRange{
|
||||||
|
max: max,
|
||||||
|
min: max.Add(-width),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLocalExtrema(items []TimeLine, timeRange TimeRange) ValueExtrema {
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
return ValueExtrema{0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
var max, min = -math.MaxFloat64, math.MaxFloat64
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
started := false
|
||||||
|
for i := len(item.points) - 1; i > 0; i-- {
|
||||||
|
point := item.points[i]
|
||||||
|
if timeRange.isInRange(point.time) {
|
||||||
|
started = true
|
||||||
|
} else if started == true && !timeRange.isInRange(point.time) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if point.value > max && timeRange.isInRange(point.time) {
|
||||||
|
max = point.value
|
||||||
|
}
|
||||||
|
if point.value < min && timeRange.isInRange(point.time) {
|
||||||
|
min = point.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueExtrema{max: max, min: min}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TimeRange) isInRange(time time.Time) bool {
|
||||||
|
return time.After(r.min) && time.Before(r.max)
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package runchart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
ui "github.com/sqshq/termui"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xAxisLegendIndent = 10
|
||||||
|
yAxisLegendIndent = 1
|
||||||
|
heightWithLabelOnly = 3
|
||||||
|
heightWithDetails = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
type Legend struct {
|
||||||
|
Enabled bool
|
||||||
|
Details bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {
|
||||||
|
|
||||||
|
if !c.legend.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
height := heightWithLabelOnly
|
||||||
|
if c.legend.Details {
|
||||||
|
height = heightWithDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCount := (c.Dx() - yAxisLegendIndent) / (height + yAxisLegendIndent)
|
||||||
|
columnCount := int(math.Ceil(float64(len(c.lines)) / float64(rowCount)))
|
||||||
|
columnWidth := getColumnWidth(c.lines, c.precision)
|
||||||
|
|
||||||
|
for col := 0; col < columnCount; col++ {
|
||||||
|
for row := 0; row < rowCount; row++ {
|
||||||
|
|
||||||
|
lineIndex := row + rowCount*col
|
||||||
|
if len(c.lines) <= lineIndex {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
line := c.lines[row+rowCount*col]
|
||||||
|
extrema := getLineValueExtrema(line.points)
|
||||||
|
x := c.Inner.Max.X - (columnWidth+xAxisLegendIndent)*(col+1)
|
||||||
|
y := c.Inner.Min.Y + yAxisLegendIndent + row*(height)
|
||||||
|
|
||||||
|
titleStyle := ui.NewStyle(line.color)
|
||||||
|
detailsStyle := ui.NewStyle(ui.ColorWhite)
|
||||||
|
|
||||||
|
buffer.SetString(string(ui.DOT), titleStyle, image.Pt(x-2, y))
|
||||||
|
buffer.SetString(line.label, titleStyle, image.Pt(x, y))
|
||||||
|
|
||||||
|
if !c.legend.Details {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
details := [4]string{
|
||||||
|
fmt.Sprintf("cur %s", formatValue(line.points[len(line.points)-1].value, c.precision)),
|
||||||
|
fmt.Sprintf("max %s", formatValue(extrema.max, c.precision)),
|
||||||
|
fmt.Sprintf("min %s", formatValue(extrema.min, c.precision)),
|
||||||
|
fmt.Sprintf("dif %s", formatValue(1, c.precision)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, detail := range details {
|
||||||
|
buffer.SetString(detail, detailsStyle, image.Pt(x, y+i+yAxisLegendIndent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getColumnWidth(lines []TimeLine, precision int) int {
|
||||||
|
width := len(formatValue(0, precision))
|
||||||
|
for _, line := range lines {
|
||||||
|
if len(line.label) > width {
|
||||||
|
width = len(line.label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove and use the one from line
|
||||||
|
func getLineValueExtrema(points []TimePoint) ValueExtrema {
|
||||||
|
|
||||||
|
if len(points) == 0 {
|
||||||
|
return ValueExtrema{0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
var max, min = -math.MaxFloat64, math.MaxFloat64
|
||||||
|
|
||||||
|
for _, point := range points {
|
||||||
|
if point.value > max {
|
||||||
|
max = point.value
|
||||||
|
}
|
||||||
|
if point.value < min {
|
||||||
|
min = point.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueExtrema{max: max, min: min}
|
||||||
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
package runchart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/sqshq/sampler/console"
|
||||||
|
"github.com/sqshq/sampler/data"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ui "github.com/sqshq/termui"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
xAxisLabelsHeight = 1
|
||||||
|
xAxisLabelsWidth = 8
|
||||||
|
xAxisLabelsIndent = 2
|
||||||
|
xAxisGridWidth = xAxisLabelsIndent + xAxisLabelsWidth
|
||||||
|
yAxisLabelsHeight = 1
|
||||||
|
yAxisLabelsIndent = 1
|
||||||
|
|
||||||
|
historyReserveMin = 20
|
||||||
|
|
||||||
|
xBrailleMultiplier = 2
|
||||||
|
yBrailleMultiplier = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Default Mode = 0
|
||||||
|
Pinpoint Mode = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type RunChart struct {
|
||||||
|
ui.Block
|
||||||
|
lines []TimeLine
|
||||||
|
grid ChartGrid
|
||||||
|
timescale time.Duration
|
||||||
|
mutex *sync.Mutex
|
||||||
|
mode Mode
|
||||||
|
selection time.Time
|
||||||
|
precision int
|
||||||
|
legend Legend
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimePoint struct {
|
||||||
|
value float64
|
||||||
|
time time.Time
|
||||||
|
coordinate int
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeLine struct {
|
||||||
|
points []TimePoint
|
||||||
|
extrema ValueExtrema
|
||||||
|
color ui.Color
|
||||||
|
label string
|
||||||
|
selection int
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeRange struct {
|
||||||
|
max time.Time
|
||||||
|
min time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueExtrema struct {
|
||||||
|
max float64
|
||||||
|
min float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunChart(title string, precision int, refreshRateMs int, legend Legend) *RunChart {
|
||||||
|
block := *ui.NewBlock()
|
||||||
|
block.Title = title
|
||||||
|
return &RunChart{
|
||||||
|
Block: block,
|
||||||
|
lines: []TimeLine{},
|
||||||
|
timescale: calculateTimescale(refreshRateMs),
|
||||||
|
mutex: &sync.Mutex{},
|
||||||
|
precision: precision,
|
||||||
|
mode: Default,
|
||||||
|
legend: legend,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) newTimePoint(value float64) TimePoint {
|
||||||
|
now := time.Now()
|
||||||
|
return TimePoint{
|
||||||
|
value: value,
|
||||||
|
time: now,
|
||||||
|
coordinate: c.calculateTimeCoordinate(now),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) Draw(buffer *ui.Buffer) {
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
c.Block.Draw(buffer)
|
||||||
|
c.grid = c.newChartGrid()
|
||||||
|
|
||||||
|
drawArea := image.Rect(
|
||||||
|
c.Inner.Min.X+c.grid.minTimeWidth+1, c.Inner.Min.Y,
|
||||||
|
c.Inner.Max.X, c.Inner.Max.Y-xAxisLabelsHeight-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
c.renderAxes(buffer)
|
||||||
|
c.renderLines(buffer, drawArea)
|
||||||
|
c.renderLegend(buffer, drawArea)
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) ConsumeSample(sample data.Sample) {
|
||||||
|
|
||||||
|
float, err := strconv.ParseFloat(sample.Value, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// TODO visual notification + check sample.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mutex.Lock()
|
||||||
|
|
||||||
|
lineIndex := -1
|
||||||
|
|
||||||
|
for i, line := range c.lines {
|
||||||
|
if line.label == sample.Label {
|
||||||
|
lineIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lineIndex == -1 {
|
||||||
|
line := &TimeLine{
|
||||||
|
points: []TimePoint{},
|
||||||
|
color: sample.Color,
|
||||||
|
label: sample.Label,
|
||||||
|
}
|
||||||
|
c.lines = append(c.lines, *line)
|
||||||
|
lineIndex = len(c.lines) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
line := c.lines[lineIndex]
|
||||||
|
timePoint := c.newTimePoint(float)
|
||||||
|
line.points = append(line.points, timePoint)
|
||||||
|
c.lines[lineIndex] = line
|
||||||
|
|
||||||
|
c.trimOutOfRangeValues()
|
||||||
|
c.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) renderLines(buffer *ui.Buffer, drawArea image.Rectangle) {
|
||||||
|
|
||||||
|
canvas := ui.NewCanvas()
|
||||||
|
canvas.Rectangle = drawArea
|
||||||
|
|
||||||
|
if len(c.lines) == 0 || len(c.lines[0].points) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionCoordinate := c.calculateTimeCoordinate(c.selection)
|
||||||
|
selectionPoints := make(map[int]image.Point)
|
||||||
|
|
||||||
|
probe := c.lines[0].points[0]
|
||||||
|
delta := ui.AbsInt(c.calculateTimeCoordinate(probe.time) - probe.coordinate)
|
||||||
|
|
||||||
|
for i, line := range c.lines {
|
||||||
|
|
||||||
|
xPoint := make(map[int]image.Point)
|
||||||
|
xOrder := make([]int, 0)
|
||||||
|
|
||||||
|
if line.selection != 0 {
|
||||||
|
line.selection -= delta
|
||||||
|
c.lines[i].selection = line.selection
|
||||||
|
}
|
||||||
|
|
||||||
|
for j, timePoint := range line.points {
|
||||||
|
|
||||||
|
timePoint.coordinate -= delta
|
||||||
|
line.points[j] = timePoint
|
||||||
|
|
||||||
|
var y int
|
||||||
|
if c.grid.valueExtrema.max == c.grid.valueExtrema.min {
|
||||||
|
y = (drawArea.Dy() - 2) / 2
|
||||||
|
} else {
|
||||||
|
valuePerY := (c.grid.valueExtrema.max - c.grid.valueExtrema.min) / float64(drawArea.Dy()-2)
|
||||||
|
y = int(float64(timePoint.value-c.grid.valueExtrema.min) / valuePerY)
|
||||||
|
}
|
||||||
|
|
||||||
|
point := image.Pt(timePoint.coordinate, drawArea.Max.Y-y-1)
|
||||||
|
|
||||||
|
if _, exists := xPoint[point.X]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !point.In(drawArea) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.selection == 0 {
|
||||||
|
if len(line.points) > j+1 && ui.AbsInt(timePoint.coordinate-selectionCoordinate) > ui.AbsInt(line.points[j+1].coordinate-selectionCoordinate) {
|
||||||
|
selectionPoints[i] = point
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if timePoint.coordinate == line.selection {
|
||||||
|
selectionPoints[i] = point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xPoint[point.X] = point
|
||||||
|
xOrder = append(xOrder, point.X)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, x := range xOrder {
|
||||||
|
|
||||||
|
currentPoint := xPoint[x]
|
||||||
|
var previousPoint image.Point
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
previousPoint = currentPoint
|
||||||
|
} else {
|
||||||
|
previousPoint = xPoint[xOrder[i-1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.Line(
|
||||||
|
braillePoint(previousPoint),
|
||||||
|
braillePoint(currentPoint),
|
||||||
|
line.color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.Draw(buffer)
|
||||||
|
|
||||||
|
if c.mode == Pinpoint {
|
||||||
|
for lineIndex, point := range selectionPoints {
|
||||||
|
buffer.SetCell(ui.NewCell(console.SymbolSelection, ui.NewStyle(c.lines[lineIndex].color)), point)
|
||||||
|
if c.lines[lineIndex].selection == 0 {
|
||||||
|
c.lines[lineIndex].selection = point.X
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) trimOutOfRangeValues() {
|
||||||
|
|
||||||
|
minRangeTime := c.grid.timeRange.min.Add(-time.Minute * time.Duration(historyReserveMin))
|
||||||
|
|
||||||
|
for i, item := range c.lines {
|
||||||
|
lastOutOfRangeValueIndex := -1
|
||||||
|
|
||||||
|
for j, point := range item.points {
|
||||||
|
if point.time.Before(minRangeTime) {
|
||||||
|
lastOutOfRangeValueIndex = j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastOutOfRangeValueIndex > 0 {
|
||||||
|
item.points = append(item.points[:0], item.points[lastOutOfRangeValueIndex+1:]...)
|
||||||
|
c.lines[i] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) calculateTimeCoordinate(t time.Time) int {
|
||||||
|
timeDeltaWithGridMaxTime := c.grid.timeRange.max.Sub(t).Nanoseconds()
|
||||||
|
timeDeltaToPaddingRelation := float64(timeDeltaWithGridMaxTime) / float64(c.timescale.Nanoseconds())
|
||||||
|
return c.grid.maxTimeWidth - int(math.Ceil(float64(xAxisGridWidth)*timeDeltaToPaddingRelation))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add boundaries for values in range
|
||||||
|
func (c *RunChart) getMaxValueLength() int {
|
||||||
|
|
||||||
|
maxValueLength := 0
|
||||||
|
|
||||||
|
for _, line := range c.lines {
|
||||||
|
for _, point := range line.points {
|
||||||
|
l := len(formatValue(point.value, c.precision))
|
||||||
|
if l > maxValueLength {
|
||||||
|
maxValueLength = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxValueLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) MoveSelection(shift int) {
|
||||||
|
|
||||||
|
if c.mode == Default {
|
||||||
|
c.mode = Pinpoint
|
||||||
|
c.selection = getMidRangeTime(c.grid.timeRange)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
c.selection = c.selection.Add(c.grid.timePerPoint * time.Duration(shift))
|
||||||
|
if c.selection.After(c.grid.timeRange.max) {
|
||||||
|
c.selection = c.grid.timeRange.max
|
||||||
|
} else if c.selection.Before(c.grid.timeRange.min) {
|
||||||
|
c.selection = c.grid.timeRange.min
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range c.lines {
|
||||||
|
c.lines[i].selection = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RunChart) DisableSelection() {
|
||||||
|
if c.mode == Pinpoint {
|
||||||
|
c.mode = Default
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMidRangeTime(r TimeRange) time.Time {
|
||||||
|
delta := r.max.Sub(r.min)
|
||||||
|
return r.max.Add(-delta / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatValue(value float64, precision int) string {
|
||||||
|
format := "%." + strconv.Itoa(precision) + "f"
|
||||||
|
return fmt.Sprintf(format, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// time duration between grid lines
|
||||||
|
func calculateTimescale(refreshRateMs int) time.Duration {
|
||||||
|
|
||||||
|
multiplier := refreshRateMs * xAxisGridWidth / 2
|
||||||
|
timescale := time.Duration(time.Millisecond * time.Duration(multiplier)).Round(time.Second)
|
||||||
|
|
||||||
|
if timescale.Seconds() == 0 {
|
||||||
|
return time.Second
|
||||||
|
} else {
|
||||||
|
return timescale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func braillePoint(point image.Point) image.Point {
|
||||||
|
return image.Point{X: point.X * xBrailleMultiplier, Y: point.Y * yBrailleMultiplier}
|
||||||
|
}
|
Loading…
Reference in New Issue