added mouse selection handling

This commit is contained in:
sqshq 2019-02-02 22:30:45 -05:00
parent 41abf40647
commit d713d5be0a
8 changed files with 200 additions and 98 deletions

View File

@ -8,7 +8,7 @@ run-charts:
script: curl -o /dev/null -s -w '%{time_total}' http://yahoo.com script: curl -o /dev/null -s -w '%{time_total}' http://yahoo.com
- label: YANDEX - label: YANDEX
script: curl -o /dev/null -s -w '%{time_total}' http://yandex.com script: curl -o /dev/null -s -w '%{time_total}' http://yandex.com
refresh-rate-ms: 300 refresh-rate-ms: 200 # TODO consider remove time-scale-sec property, and adjust it automatically based on refresh-rate-ms
time-scale-sec: 1 time-scale-sec: 1
decimal-places: 1 decimal-places: 1
legend: legend:

View File

@ -8,7 +8,7 @@ import (
) )
const ( const (
RenderRate = 30 * time.Millisecond RenderRate = 50 * time.Millisecond // TODO not a constant, should be dynamically chosen based on min X scale (per each chart? should be tested). if it is 1 sec, it should be 100 ms, if 2 - 200 ms, if 3 - 300, 4 - 400, 5 - 500 and 500 is max. smth like that.
Title = "sampler" Title = "sampler"
) )

View File

@ -1,6 +1,14 @@
package data package data
import . "github.com/sqshq/termui"
type Consumer interface { type Consumer interface {
ConsumeValue(item Item, value string) ConsumeSample(sample Sample)
ConsumeError(item Item, err error) }
type Sample struct {
Label string
Color Color
Value string
Error error
} }

View File

@ -1,15 +1,15 @@
package data package data
import ( import (
ui "github.com/sqshq/termui" . "github.com/sqshq/termui"
"os/exec" "os/exec"
"strings" "strings"
) )
type Item struct { type Item struct {
Script string `yaml:"script"` Script string `yaml:"script"`
Label string `yaml:"label"` Label string `yaml:"label"`
Color ui.Color `yaml:"color"` Color Color `yaml:"color"`
} }
func (self *Item) nextValue() (value string, err error) { func (self *Item) nextValue() (value string, err error) {

View File

@ -30,9 +30,12 @@ func (self *Poller) poll() {
value, err := self.item.nextValue() value, err := self.item.nextValue()
if err != nil { sample := Sample{
self.consumer.ConsumeError(self.item, err) Value: value,
Error: err,
Color: self.item.Color,
Label: self.item.Label,
} }
self.consumer.ConsumeValue(self.item, value) self.consumer.ConsumeSample(sample)
} }

View File

@ -51,6 +51,6 @@ func (self *Handler) HandleEvents() {
func (self *Handler) handleMouseClick(x, y int) { func (self *Handler) handleMouseClick(x, y int) {
for _, chart := range self.Layout.GetComponents(widgets.TypeRunChart) { for _, chart := range self.Layout.GetComponents(widgets.TypeRunChart) {
runChart := chart.(*widgets.RunChart) runChart := chart.(*widgets.RunChart)
runChart.SelectValue(x, y) runChart.SelectPoint(x, y)
} }
} }

View File

@ -46,7 +46,7 @@ func (self *Layout) ChangeDimensions(width, height int) {
self.SetRect(0, 0, width, height) self.SetRect(0, 0, width, height)
} }
func (self *Layout) Draw(buf *Buffer) { func (self *Layout) Draw(buffer *Buffer) {
columnWidth := float64(self.GetRect().Dx()) / columnsCount columnWidth := float64(self.GetRect().Dx()) / columnsCount
rowHeight := float64(self.GetRect().Dy()) / rowsCount rowHeight := float64(self.GetRect().Dy()) / rowsCount
@ -59,6 +59,6 @@ func (self *Layout) Draw(buf *Buffer) {
y2 := y1 + float64(component.Size.Y)*rowHeight y2 := y1 + float64(component.Size.Y)*rowHeight
component.Drawable.SetRect(int(x1), int(y1), int(x2), int(y2)) component.Drawable.SetRect(int(x1), int(y1), int(x2), int(y2))
component.Drawable.Draw(buf) component.Drawable.Draw(buffer)
} }
} }

View File

@ -15,12 +15,13 @@ import (
) )
const ( const (
xAxisLabelsHeight = 1 chartHistoryReserve = 10
xAxisLabelsWidth = 8 xAxisLabelsHeight = 1
xAxisLabelsGap = 2 xAxisLabelsWidth = 8
yAxisLabelsHeight = 1 xAxisLabelsGap = 2
yAxisLabelsGap = 1 yAxisLabelsHeight = 1
xAxisLegendWidth = 15 yAxisLabelsGap = 1
xAxisLegendWidth = 20
) )
type RunChart struct { type RunChart struct {
@ -28,18 +29,20 @@ type RunChart struct {
lines []TimeLine lines []TimeLine
grid ChartGrid grid ChartGrid
precision int precision int
selection time.Time selection *time.Time
mutex *sync.Mutex mutex *sync.Mutex
} }
type TimePoint struct { type TimePoint struct {
Value float64 time time.Time
Time time.Time value float64
line *TimeLine
} }
type TimeLine struct { type TimeLine struct {
points []TimePoint points []TimePoint
item data.Item color Color
label string
} }
type ChartGrid struct { type ChartGrid struct {
@ -69,7 +72,7 @@ func NewRunChart(title string) *RunChart {
Block: block, Block: block,
lines: []TimeLine{}, lines: []TimeLine{},
mutex: &sync.Mutex{}, mutex: &sync.Mutex{},
precision: 2, // TODO config precision: 2, // TODO move to config
} }
} }
@ -89,74 +92,122 @@ func (self *RunChart) newChartGrid() ChartGrid {
} }
} }
func (self *RunChart) Draw(buf *Buffer) { func (self *RunChart) Draw(buffer *Buffer) {
self.mutex.Lock() self.mutex.Lock()
self.Block.Draw(buf) self.Block.Draw(buffer)
self.grid = self.newChartGrid() self.grid = self.newChartGrid()
self.renderAxes(buf)
drawArea := image.Rect( drawArea := image.Rect(
self.Inner.Min.X+self.grid.minTimeWidth+1, self.Inner.Min.Y, self.Inner.Min.X+self.grid.minTimeWidth+1, self.Inner.Min.Y,
self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1, self.Inner.Max.X, self.Inner.Max.Y-xAxisLabelsHeight-1,
) )
self.renderItems(buf, drawArea) selectedPoints := self.getSelectedTimePoints()
self.renderLegend(buf, drawArea)
self.renderAxes(buffer)
self.renderItems(buffer, drawArea)
self.renderSelection(buffer, drawArea, selectedPoints)
self.renderLegend(buffer, drawArea, selectedPoints)
self.mutex.Unlock() self.mutex.Unlock()
} }
func (self *RunChart) ConsumeValue(item data.Item, value string) { func (self *RunChart) ConsumeSample(sample data.Sample) {
float, err := strconv.ParseFloat(sample.Value, 64)
float, err := strconv.ParseFloat(value, 64)
if err != nil { if err != nil {
log.Printf("Expected float number, but got %v", value) // TODO visual notification log.Printf("Expected float number, but got %v", sample.Value) // TODO visual notification + check sample.Error
} }
timePoint := TimePoint{Value: float, Time: time.Now()}
self.mutex.Lock() self.mutex.Lock()
itemExists := false
lineIndex := -1
for i, line := range self.lines { for i, line := range self.lines {
if line.item.Label == item.Label { if line.label == sample.Label {
line.points = append(line.points, timePoint) lineIndex = i
self.lines[i] = line
itemExists = true
} }
} }
if !itemExists { if lineIndex == -1 {
item := &TimeLine{ line := &TimeLine{
points: []TimePoint{timePoint}, points: []TimePoint{},
item: item, color: sample.Color,
label: sample.Label,
} }
self.lines = append(self.lines, *item) self.lines = append(self.lines, *line)
lineIndex = len(self.lines) - 1
} }
line := self.lines[lineIndex]
timePoint := TimePoint{value: float, time: time.Now(), line: &line}
line.points = append(line.points, timePoint)
self.lines[lineIndex] = line
self.trimOutOfRangeValues() self.trimOutOfRangeValues()
self.mutex.Unlock() self.mutex.Unlock()
} }
func (self *RunChart) ConsumeError(item data.Item, err error) { func (self *RunChart) SelectPoint(x int, y int) {
// TODO visual notification
point := image.Point{X: x, Y: y}
if !point.In(self.Rectangle) {
self.selection = nil
return
}
timeDeltaToPaddingRelation := (self.grid.maxTimeWidth - x) / self.grid.paddingWidth
timeDeltaWithGridMaxTime := timeDeltaToPaddingRelation * int(self.grid.paddingDuration.Nanoseconds())
selection := self.grid.timeExtremum.max.Add(-time.Duration(timeDeltaWithGridMaxTime) * time.Nanosecond)
self.selection = &selection
} }
func (self *RunChart) SelectValue(x int, y int) { func (self *RunChart) getSelectedTimePoints() []TimePoint {
// TODO instead of that, find actual time for the given X
// + make sure that Y is within the given chart selected := []TimePoint{}
// once ensured, set "selected time" into the chart structure
// self.selection = image.Point{X: x, Y: y} if self.selection == nil {
return selected
}
for _, line := range self.lines {
if len(line.points) == 0 {
continue
}
closest := line.points[0]
for _, point := range line.points {
diffWithClosest := math.Abs(float64(self.selection.UnixNano() - closest.time.UnixNano()))
diffWithCurrent := math.Abs(float64(self.selection.UnixNano() - point.time.UnixNano()))
if diffWithClosest > diffWithCurrent {
closest = point
}
}
selected = append(selected, closest)
}
return selected
} }
func (self *RunChart) trimOutOfRangeValues() { func (self *RunChart) trimOutOfRangeValues() {
minRangeTime := self.grid.timeExtremum.min.Add(-self.grid.paddingDuration * 10) historyReserve := self.grid.paddingDuration * time.Duration(self.grid.linesCount) * chartHistoryReserve
minRangeTime := self.grid.timeExtremum.min.Add(-historyReserve)
for i, item := range self.lines { for i, item := range self.lines {
lastOutOfRangeValueIndex := -1 lastOutOfRangeValueIndex := -1
for j, point := range item.points { for j, point := range item.points {
if point.Time.Before(minRangeTime) { if point.time.Before(minRangeTime) {
lastOutOfRangeValueIndex = j lastOutOfRangeValueIndex = j
} }
} }
@ -178,9 +229,9 @@ func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) {
xToPoint := make(map[int]image.Point) xToPoint := make(map[int]image.Point)
pointsOrder := make([]int, 0) pointsOrder := make([]int, 0)
for _, point := range line.points { for _, timePoint := range line.points {
timeDeltaWithGridMaxTime := self.grid.timeExtremum.max.Sub(point.Time).Nanoseconds() timeDeltaWithGridMaxTime := self.grid.timeExtremum.max.Sub(timePoint.time).Nanoseconds()
timeDeltaToPaddingRelation := float64(timeDeltaWithGridMaxTime) / float64(self.grid.paddingDuration.Nanoseconds()) timeDeltaToPaddingRelation := float64(timeDeltaWithGridMaxTime) / float64(self.grid.paddingDuration.Nanoseconds())
x := self.grid.maxTimeWidth - (int(float64(self.grid.paddingWidth) * timeDeltaToPaddingRelation)) x := self.grid.maxTimeWidth - (int(float64(self.grid.paddingWidth) * timeDeltaToPaddingRelation))
@ -189,7 +240,7 @@ func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) {
y = (drawArea.Dy() - 2) / 2 y = (drawArea.Dy() - 2) / 2
} else { } else {
valuePerY := (self.grid.valueExtremum.max - self.grid.valueExtremum.min) / float64(drawArea.Dy()-2) valuePerY := (self.grid.valueExtremum.max - self.grid.valueExtremum.min) / float64(drawArea.Dy()-2)
y = int(float64(point.Value-self.grid.valueExtremum.min) / valuePerY) y = int(float64(timePoint.value-self.grid.valueExtremum.min) / valuePerY)
} }
point := image.Pt(x, drawArea.Max.Y-y-1) point := image.Pt(x, drawArea.Max.Y-y-1)
@ -220,19 +271,9 @@ func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) {
canvas.Line( canvas.Line(
braillePoint(previousPoint), braillePoint(previousPoint),
braillePoint(currentPoint), braillePoint(currentPoint),
line.item.Color, line.color,
) )
} }
//if point, exists := xToPoint[self.selection.X]; exists {
// buffer.SetCell(
// NewCell(DOT, NewStyle(line.item.Color)),
// point,
// )
// log.Printf("EXIST!")
//} else {
// //log.Printf("DOES NOT EXIST")
//}
} }
canvas.Draw(buffer) canvas.Draw(buffer)
@ -301,36 +342,86 @@ func (self *RunChart) renderAxes(buffer *Buffer) {
} }
} }
func (self *RunChart) renderLegend(buffer *Buffer, rectangle image.Rectangle) { func (self *RunChart) renderLegend(buffer *Buffer, rectangle image.Rectangle, selectedPoints []TimePoint) {
for i, line := range self.lines {
extremum := GetLineValueExtremum(line.points) for i, line := range self.lines {
buffer.SetString( buffer.SetString(
string(DOT), string(DOT),
NewStyle(line.item.Color), NewStyle(line.color),
image.Pt(self.Inner.Max.X-xAxisLegendWidth-2, self.Inner.Min.Y+1+i*5), image.Pt(self.Inner.Max.X-xAxisLegendWidth-2, self.Inner.Min.Y+1+i*5),
) )
buffer.SetString( buffer.SetString(
fmt.Sprintf("%s", line.item.Label), fmt.Sprintf("%s", line.label),
NewStyle(line.item.Color), NewStyle(line.color),
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+1+i*5), 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)), if len(selectedPoints) > 0 {
NewStyle(ColorWhite),
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+2+i*5), index := -1
)
buffer.SetString( for i, p := range selectedPoints {
fmt.Sprintf("max %s", formatValue(extremum.max, self.precision)), if p.line.label == line.label {
NewStyle(ColorWhite), index = i
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)), if index != -1 {
NewStyle(ColorWhite), buffer.SetString(
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+4+i*5), fmt.Sprintf("time: %v", selectedPoints[index].time.Format("15:04:05.000")),
) NewStyle(ColorWhite),
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+2+i*5),
)
buffer.SetString(
fmt.Sprintf("value: %s", formatValue(selectedPoints[index].value, self.precision)),
NewStyle(ColorWhite),
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+3+i*5),
)
}
} else {
extremum := GetLineValueExtremum(line.points)
buffer.SetString(
fmt.Sprintf("cur %s", formatValue(line.points[len(line.points)-1].value, self.precision)),
NewStyle(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)),
NewStyle(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)),
NewStyle(ColorWhite),
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+4+i*5),
)
}
}
}
func (self *RunChart) renderSelection(buffer *Buffer, drawArea image.Rectangle, selectedPoints []TimePoint) {
for _, timePoint := range selectedPoints {
timeDeltaWithGridMaxTime := self.grid.timeExtremum.max.Sub(timePoint.time).Nanoseconds()
timeDeltaToPaddingRelation := float64(timeDeltaWithGridMaxTime) / float64(self.grid.paddingDuration.Nanoseconds())
x := self.grid.maxTimeWidth - (int(float64(self.grid.paddingWidth) * timeDeltaToPaddingRelation))
var y int
if self.grid.valueExtremum.max-self.grid.valueExtremum.min == 0 {
y = (drawArea.Dy() - 2) / 2
} else {
valuePerY := (self.grid.valueExtremum.max - self.grid.valueExtremum.min) / float64(drawArea.Dy()-2)
y = int(float64(timePoint.value-self.grid.valueExtremum.min) / valuePerY)
}
point := image.Pt(x, drawArea.Max.Y-y-1)
if point.In(drawArea) {
buffer.SetCell(NewCell('▲', NewStyle(timePoint.line.color)), point)
}
} }
} }
@ -340,7 +431,7 @@ func (self *RunChart) getMaxValueLength() int {
for _, line := range self.lines { for _, line := range self.lines {
for _, point := range line.points { for _, point := range line.points {
l := len(formatValue(point.Value, self.precision)) l := len(formatValue(point.value, self.precision))
if l > maxValueLength { if l > maxValueLength {
maxValueLength = l maxValueLength = l
} }
@ -351,7 +442,7 @@ func (self *RunChart) getMaxValueLength() int {
} }
func formatValue(value float64, precision int) string { func formatValue(value float64, precision int) string {
format := " %." + strconv.Itoa(precision) + "f" format := "%." + strconv.Itoa(precision) + "f"
return fmt.Sprintf(format, value) return fmt.Sprintf(format, value)
} }
@ -365,11 +456,11 @@ func GetChartValueExtremum(items []TimeLine) ValueExtremum {
for _, item := range items { for _, item := range items {
for _, point := range item.points { for _, point := range item.points {
if point.Value > max { if point.value > max {
max = point.Value max = point.value
} }
if point.Value < min { if point.value < min {
min = point.Value min = point.value
} }
} }
} }
@ -386,11 +477,11 @@ func GetLineValueExtremum(points []TimePoint) ValueExtremum {
var max, min = -math.MaxFloat64, math.MaxFloat64 var max, min = -math.MaxFloat64, math.MaxFloat64
for _, point := range points { for _, point := range points {
if point.Value > max { if point.value > max {
max = point.Value max = point.value
} }
if point.Value < min { if point.value < min {
min = point.Value min = point.value
} }
} }