runchart grid lines and labels calibration
This commit is contained in:
parent
41b1fa59a5
commit
e2b478657a
|
@ -22,9 +22,8 @@ run-charts:
|
||||||
y: 15
|
y: 15
|
||||||
- title: MONGO-COUNT
|
- title: MONGO-COUNT
|
||||||
data:
|
data:
|
||||||
- label: posts
|
- label: POSTS
|
||||||
script: mongo --quiet --host=localhost blog --eval "db.getCollection('post').find({}).size()" | grep 2
|
script: mongo --quiet --host=localhost blog --eval "db.getCollection('post').find({}).size()"
|
||||||
color: 3
|
|
||||||
position:
|
position:
|
||||||
x: 15
|
x: 15
|
||||||
y: 0
|
y: 0
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/sqshq/vcmd/console"
|
||||||
"github.com/sqshq/vcmd/data"
|
"github.com/sqshq/vcmd/data"
|
||||||
"github.com/sqshq/vcmd/settings"
|
|
||||||
. "github.com/sqshq/vcmd/widgets"
|
. "github.com/sqshq/vcmd/widgets"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Theme settings.Theme `yaml:"theme"`
|
Theme console.Theme `yaml:"theme"`
|
||||||
RunCharts []RunChartConfig `yaml:"run-charts"`
|
RunCharts []RunChartConfig `yaml:"run-charts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import "github.com/sqshq/vcmd/settings"
|
import (
|
||||||
|
"github.com/sqshq/vcmd/console"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultRefreshRateMs = 300
|
defaultRefreshRateMs = 300
|
||||||
defaultTimeScaleSec = 1
|
defaultTimeScaleSec = 1
|
||||||
defaultTheme = settings.ThemeDark
|
defaultTheme = console.ThemeDark
|
||||||
)
|
)
|
||||||
|
|
||||||
func (self *Config) setDefaultValues() {
|
func (self *Config) setDefaultValues() {
|
||||||
|
@ -31,7 +33,7 @@ func (config *Config) setDefaultLayout() {
|
||||||
|
|
||||||
func (config *Config) setDefaultColors() {
|
func (config *Config) setDefaultColors() {
|
||||||
|
|
||||||
palette := settings.GetPalette(config.Theme)
|
palette := console.GetPalette(config.Theme)
|
||||||
|
|
||||||
for i, chart := range config.RunCharts {
|
for i, chart := range config.RunCharts {
|
||||||
for j, item := range chart.Items {
|
for j, item := range chart.Items {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package console
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
ui "github.com/sqshq/termui"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RenderRate = 30 * time.Millisecond
|
||||||
|
Title = "vcmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Console struct{}
|
||||||
|
|
||||||
|
func (self *Console) Init() {
|
||||||
|
|
||||||
|
fmt.Printf("\033]0;%s\007", Title)
|
||||||
|
|
||||||
|
if err := ui.Init(); err != nil {
|
||||||
|
log.Fatalf("failed to initialize termui: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Console) Close() {
|
||||||
|
ui.Close()
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package settings
|
package console
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
|
@ -1,4 +1,4 @@
|
||||||
package settings
|
package event
|
||||||
|
|
||||||
type Event string
|
type Event string
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
ui "github.com/sqshq/termui"
|
||||||
|
"github.com/sqshq/vcmd/widgets"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
Layout *widgets.Layout
|
||||||
|
RenderEvents <-chan time.Time
|
||||||
|
ConsoleEvents <-chan ui.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Handler) HandleEvents() {
|
||||||
|
|
||||||
|
pause := false
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-self.RenderEvents:
|
||||||
|
if !pause {
|
||||||
|
ui.Render(self.Layout)
|
||||||
|
}
|
||||||
|
case e := <-self.ConsoleEvents:
|
||||||
|
switch e.ID {
|
||||||
|
case EventQuit, EventExit:
|
||||||
|
return
|
||||||
|
case EventPause:
|
||||||
|
pause = !pause
|
||||||
|
case EventResize:
|
||||||
|
payload := e.Payload.(ui.Resize)
|
||||||
|
self.Layout.ChangeDimensions(payload.Width, payload.Height)
|
||||||
|
case EventMouseClick:
|
||||||
|
payload := e.Payload.(ui.Mouse)
|
||||||
|
self.handleMouseClick(payload.X, payload.Y)
|
||||||
|
case EventKeyboardLeft:
|
||||||
|
// here we are going to move selection (special type of layout item)
|
||||||
|
//layout.GetItem("").Move(-1, 0)
|
||||||
|
case EventKeyboardRight:
|
||||||
|
//layout.GetItem(0).Move(1, 0)
|
||||||
|
case EventKeyboardDown:
|
||||||
|
//layout.GetItem(0).Move(0, 1)
|
||||||
|
case EventKeyboardUp:
|
||||||
|
//layout.GetItem(0).Move(0, -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Handler) handleMouseClick(x, y int) {
|
||||||
|
for _, chart := range self.Layout.GetComponents(widgets.TypeRunChart) {
|
||||||
|
runChart := chart.(*widgets.RunChart)
|
||||||
|
runChart.SelectValue(x, y)
|
||||||
|
}
|
||||||
|
}
|
68
main.go
68
main.go
|
@ -3,77 +3,37 @@ package main
|
||||||
import (
|
import (
|
||||||
ui "github.com/sqshq/termui"
|
ui "github.com/sqshq/termui"
|
||||||
"github.com/sqshq/vcmd/config"
|
"github.com/sqshq/vcmd/config"
|
||||||
|
"github.com/sqshq/vcmd/console"
|
||||||
"github.com/sqshq/vcmd/data"
|
"github.com/sqshq/vcmd/data"
|
||||||
"github.com/sqshq/vcmd/settings"
|
"github.com/sqshq/vcmd/event"
|
||||||
"github.com/sqshq/vcmd/widgets"
|
"github.com/sqshq/vcmd/widgets"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
print("\033]0;vcmd\007")
|
|
||||||
|
|
||||||
cfg := config.Load("/Users/sqshq/Go/src/github.com/sqshq/vcmd/config.yml")
|
cfg := config.Load("/Users/sqshq/Go/src/github.com/sqshq/vcmd/config.yml")
|
||||||
|
csl := console.Console{}
|
||||||
|
csl.Init()
|
||||||
|
defer csl.Close()
|
||||||
|
|
||||||
if err := ui.Init(); err != nil {
|
layout := widgets.NewLayout(ui.TerminalDimensions())
|
||||||
log.Fatalf("failed to initialize termui: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer ui.Close()
|
|
||||||
events := ui.PollEvents()
|
|
||||||
|
|
||||||
pollers := make([]data.Poller, 0)
|
|
||||||
lout := widgets.NewLayout(ui.TerminalDimensions())
|
|
||||||
|
|
||||||
for _, chartConfig := range cfg.RunCharts {
|
for _, chartConfig := range cfg.RunCharts {
|
||||||
|
|
||||||
chart := widgets.NewRunChart(chartConfig.Title)
|
chart := widgets.NewRunChart(chartConfig.Title)
|
||||||
lout.AddItem(chart, chartConfig.Position, chartConfig.Size)
|
layout.AddComponent(chart, chartConfig.Position, chartConfig.Size, widgets.TypeRunChart)
|
||||||
|
|
||||||
for _, item := range chartConfig.Items {
|
for _, item := range chartConfig.Items {
|
||||||
pollers = append(pollers,
|
data.NewPoller(chart, item, chartConfig.RefreshRateMs)
|
||||||
data.NewPoller(chart, item, chartConfig.RefreshRateMs))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(30 * time.Millisecond)
|
handler := event.Handler{
|
||||||
pause := false
|
Layout: layout,
|
||||||
|
RenderEvents: time.NewTicker(console.RenderRate).C,
|
||||||
|
ConsoleEvents: ui.PollEvents(),
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
handler.HandleEvents()
|
||||||
select {
|
|
||||||
case e := <-events:
|
|
||||||
switch e.ID {
|
|
||||||
case settings.EventQuit, settings.EventExit:
|
|
||||||
return
|
|
||||||
case settings.EventResize:
|
|
||||||
payload := e.Payload.(ui.Resize)
|
|
||||||
lout.ChangeDimensions(payload.Width, payload.Height)
|
|
||||||
case settings.EventMouseClick:
|
|
||||||
//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:
|
|
||||||
switch e.ID {
|
|
||||||
case settings.EventKeyboardLeft:
|
|
||||||
// here we are going to move selection (special type of layout item)
|
|
||||||
//lout.GetItem("").Move(-1, 0)
|
|
||||||
case settings.EventKeyboardRight:
|
|
||||||
//lout.GetItem(0).Move(1, 0)
|
|
||||||
case settings.EventKeyboardDown:
|
|
||||||
//lout.GetItem(0).Move(0, 1)
|
|
||||||
case settings.EventKeyboardUp:
|
|
||||||
//lout.GetItem(0).Move(0, -1)
|
|
||||||
case settings.EventPause:
|
|
||||||
pause = !pause
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-ticker.C:
|
|
||||||
if !pause {
|
|
||||||
ui.Render(lout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,15 @@ type Component struct {
|
||||||
Drawable Drawable
|
Drawable Drawable
|
||||||
Position Position
|
Position Position
|
||||||
Size Size
|
Size Size
|
||||||
|
Type ComponentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ComponentType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeRunChart ComponentType = "runchart"
|
||||||
|
)
|
||||||
|
|
||||||
type Position struct {
|
type Position struct {
|
||||||
X int `yaml:"x"`
|
X int `yaml:"x"`
|
||||||
Y int `yaml:"y"`
|
Y int `yaml:"y"`
|
||||||
|
|
|
@ -25,8 +25,21 @@ func NewLayout(width, height int) *Layout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Layout) AddItem(drawable Drawable, position Position, size Size) {
|
func (self *Layout) AddComponent(drawable Drawable, position Position, size Size, Type ComponentType) {
|
||||||
self.components = append(self.components, Component{drawable, position, size})
|
self.components = append(self.components, Component{drawable, position, size, Type})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Layout) GetComponents(Type ComponentType) []Drawable {
|
||||||
|
|
||||||
|
var components []Drawable
|
||||||
|
|
||||||
|
for _, component := range self.components {
|
||||||
|
if component.Type == Type {
|
||||||
|
components = append(components, component.Drawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Layout) ChangeDimensions(width, height int) {
|
func (self *Layout) ChangeDimensions(width, height int) {
|
||||||
|
|
|
@ -2,8 +2,8 @@ package widgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/sqshq/vcmd/console"
|
||||||
"github.com/sqshq/vcmd/data"
|
"github.com/sqshq/vcmd/data"
|
||||||
"github.com/sqshq/vcmd/settings"
|
|
||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
|
@ -18,7 +18,7 @@ const (
|
||||||
xAxisLabelsHeight = 1
|
xAxisLabelsHeight = 1
|
||||||
xAxisLabelsWidth = 8
|
xAxisLabelsWidth = 8
|
||||||
xAxisLabelsGap = 2
|
xAxisLabelsGap = 2
|
||||||
yAxisLabelsWidth = 5
|
yAxisLabelsHeight = 1
|
||||||
yAxisLabelsGap = 1
|
yAxisLabelsGap = 1
|
||||||
xAxisLegendWidth = 15
|
xAxisLegendWidth = 15
|
||||||
)
|
)
|
||||||
|
@ -27,6 +27,8 @@ type RunChart struct {
|
||||||
Block
|
Block
|
||||||
lines []TimeLine
|
lines []TimeLine
|
||||||
grid ChartGrid
|
grid ChartGrid
|
||||||
|
precision int
|
||||||
|
selection time.Time
|
||||||
mutex *sync.Mutex
|
mutex *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +47,7 @@ type ChartGrid struct {
|
||||||
paddingDuration time.Duration
|
paddingDuration time.Duration
|
||||||
paddingWidth int
|
paddingWidth int
|
||||||
maxTimeWidth int
|
maxTimeWidth int
|
||||||
|
minTimeWidth int
|
||||||
valueExtremum ValueExtremum
|
valueExtremum ValueExtremum
|
||||||
timeExtremum TimeExtremum
|
timeExtremum TimeExtremum
|
||||||
}
|
}
|
||||||
|
@ -66,19 +69,21 @@ func NewRunChart(title string) *RunChart {
|
||||||
Block: block,
|
Block: block,
|
||||||
lines: []TimeLine{},
|
lines: []TimeLine{},
|
||||||
mutex: &sync.Mutex{},
|
mutex: &sync.Mutex{},
|
||||||
|
precision: 2, // TODO config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *RunChart) newChartGrid() ChartGrid {
|
func (self *RunChart) newChartGrid() ChartGrid {
|
||||||
|
|
||||||
linesCount := (self.Inner.Max.X - self.Inner.Min.X) / (xAxisLabelsGap + xAxisLabelsWidth)
|
linesCount := (self.Inner.Max.X - self.Inner.Min.X - self.grid.minTimeWidth) / (xAxisLabelsGap + xAxisLabelsWidth)
|
||||||
paddingDuration := time.Duration(time.Second) // TODO support others and/or adjust automatically depending on refresh rate
|
paddingDuration := time.Duration(time.Second) // TODO support others and/or adjust automatically depending on refresh rate
|
||||||
|
|
||||||
return ChartGrid{
|
return ChartGrid{
|
||||||
linesCount: linesCount,
|
linesCount: linesCount,
|
||||||
paddingDuration: paddingDuration,
|
paddingDuration: paddingDuration,
|
||||||
paddingWidth: xAxisLabelsGap + xAxisLabelsWidth,
|
paddingWidth: xAxisLabelsGap + xAxisLabelsWidth,
|
||||||
maxTimeWidth: self.Inner.Max.X - xAxisLabelsWidth/2 - xAxisLabelsGap,
|
maxTimeWidth: self.Inner.Max.X,
|
||||||
|
minTimeWidth: self.getMaxValueLength(),
|
||||||
timeExtremum: GetTimeExtremum(linesCount, paddingDuration),
|
timeExtremum: GetTimeExtremum(linesCount, paddingDuration),
|
||||||
valueExtremum: GetChartValueExtremum(self.lines),
|
valueExtremum: GetChartValueExtremum(self.lines),
|
||||||
}
|
}
|
||||||
|
@ -92,7 +97,7 @@ func (self *RunChart) Draw(buf *Buffer) {
|
||||||
self.renderAxes(buf)
|
self.renderAxes(buf)
|
||||||
|
|
||||||
drawArea := image.Rect(
|
drawArea := image.Rect(
|
||||||
self.Inner.Min.X+yAxisLabelsWidth+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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,7 +110,7 @@ func (self *RunChart) ConsumeValue(item data.Item, value string) {
|
||||||
|
|
||||||
float, err := strconv.ParseFloat(value, 64)
|
float, err := strconv.ParseFloat(value, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Expected float number, but got %v", value) // TODO visual notification
|
log.Printf("Expected float number, but got %v", value) // TODO visual notification
|
||||||
}
|
}
|
||||||
|
|
||||||
timePoint := TimePoint{Value: float, Time: time.Now()}
|
timePoint := TimePoint{Value: float, Time: time.Now()}
|
||||||
|
@ -136,12 +141,22 @@ func (self *RunChart) ConsumeError(item data.Item, err error) {
|
||||||
// TODO visual notification
|
// TODO visual notification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *RunChart) SelectValue(x int, y int) {
|
||||||
|
// TODO instead of that, find actual time for the given X
|
||||||
|
// + make sure that Y is within the given chart
|
||||||
|
// once ensured, set "selected time" into the chart structure
|
||||||
|
// self.selection = image.Point{X: x, Y: y}
|
||||||
|
}
|
||||||
|
|
||||||
func (self *RunChart) trimOutOfRangeValues() {
|
func (self *RunChart) trimOutOfRangeValues() {
|
||||||
|
|
||||||
|
minRangeTime := self.grid.timeExtremum.min.Add(-self.grid.paddingDuration * 10)
|
||||||
|
|
||||||
for i, item := range self.lines {
|
for i, item := range self.lines {
|
||||||
lastOutOfRangeValueIndex := -1
|
lastOutOfRangeValueIndex := -1
|
||||||
|
|
||||||
for j, timePoint := range item.points {
|
for j, point := range item.points {
|
||||||
if !self.isTimePointInRange(timePoint) {
|
if point.Time.Before(minRangeTime) {
|
||||||
lastOutOfRangeValueIndex = j
|
lastOutOfRangeValueIndex = j
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,60 +168,6 @@ func (self *RunChart) trimOutOfRangeValues() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *RunChart) renderAxes(buffer *Buffer) {
|
|
||||||
// draw origin cell
|
|
||||||
buffer.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++ {
|
|
||||||
buffer.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.linesCount; x++ {
|
|
||||||
buffer.SetCell(
|
|
||||||
NewCell(VERTICAL_DASH, NewStyle(settings.ColorDarkGrey)),
|
|
||||||
image.Pt(self.grid.maxTimeWidth-x*self.grid.paddingWidth, y+self.Inner.Min.Y+1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw y axis line
|
|
||||||
for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
|
||||||
buffer.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.linesCount; i++ {
|
|
||||||
labelTime := self.grid.timeExtremum.max.Add(time.Duration(-i) * time.Second)
|
|
||||||
buffer.SetString(
|
|
||||||
labelTime.Format("15:04:05"),
|
|
||||||
NewStyle(ColorWhite),
|
|
||||||
image.Pt(self.grid.maxTimeWidth-xAxisLabelsWidth/2-i*(self.grid.paddingWidth), self.Inner.Max.Y-1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw y axis labels
|
|
||||||
verticalScale := self.grid.valueExtremum.max - self.grid.valueExtremum.min/float64(self.Inner.Dy()-xAxisLabelsHeight-1)
|
|
||||||
for i := 1; i*(yAxisLabelsGap+1) <= self.Inner.Dy()-1; i++ {
|
|
||||||
buffer.SetString(
|
|
||||||
fmt.Sprintf("%.3f", float64(i)*self.grid.valueExtremum.min*verticalScale*(yAxisLabelsGap+1)),
|
|
||||||
NewStyle(ColorWhite),
|
|
||||||
image.Pt(self.Inner.Min.X, self.Inner.Max.Y-(i*(yAxisLabelsGap+1))-2),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) {
|
func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) {
|
||||||
|
|
||||||
canvas := NewCanvas()
|
canvas := NewCanvas()
|
||||||
|
@ -219,22 +180,29 @@ func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) {
|
||||||
|
|
||||||
for _, point := range line.points {
|
for _, point := range line.points {
|
||||||
|
|
||||||
if !self.isTimePointInRange(point) {
|
timeDeltaWithGridMaxTime := self.grid.timeExtremum.max.Sub(point.Time).Nanoseconds()
|
||||||
continue
|
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(point.Value-self.grid.valueExtremum.min) / valuePerY)
|
||||||
}
|
}
|
||||||
|
|
||||||
timeDeltaWithGridMaxTime := self.grid.timeExtremum.max.Sub(point.Time)
|
point := image.Pt(x, drawArea.Max.Y-y-1)
|
||||||
deltaToPaddingRelation := float64(timeDeltaWithGridMaxTime.Nanoseconds()) / float64(self.grid.paddingDuration.Nanoseconds())
|
|
||||||
x := self.grid.maxTimeWidth - (int(float64(self.grid.paddingWidth) * deltaToPaddingRelation))
|
|
||||||
|
|
||||||
valuePerYDot := (self.grid.valueExtremum.max - self.grid.valueExtremum.min) / float64(drawArea.Dy()-1)
|
|
||||||
y := int(float64(point.Value-self.grid.valueExtremum.min) / valuePerYDot)
|
|
||||||
|
|
||||||
if _, exists := xToPoint[x]; exists {
|
if _, exists := xToPoint[x]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
xToPoint[x] = image.Pt(x, drawArea.Max.Y-y-1)
|
if !point.In(drawArea) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
xToPoint[x] = point
|
||||||
pointsOrder = append(pointsOrder, x)
|
pointsOrder = append(pointsOrder, x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,29 +217,97 @@ func (self *RunChart) renderItems(buffer *Buffer, drawArea image.Rectangle) {
|
||||||
previousPoint = xToPoint[pointsOrder[i-1]]
|
previousPoint = xToPoint[pointsOrder[i-1]]
|
||||||
}
|
}
|
||||||
|
|
||||||
//buffer.SetCell(
|
|
||||||
// NewCell(self.DotRune, NewStyle(SelectColor(self.LineColors, 0))),
|
|
||||||
// currentPoint,
|
|
||||||
//)
|
|
||||||
|
|
||||||
canvas.Line(
|
canvas.Line(
|
||||||
braillePoint(previousPoint),
|
braillePoint(previousPoint),
|
||||||
braillePoint(currentPoint),
|
braillePoint(currentPoint),
|
||||||
line.item.Color,
|
line.item.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *RunChart) renderAxes(buffer *Buffer) {
|
||||||
|
// draw origin cell
|
||||||
|
buffer.SetCell(
|
||||||
|
NewCell(BOTTOM_LEFT, NewStyle(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(
|
||||||
|
NewCell(HORIZONTAL_DASH, NewStyle(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(
|
||||||
|
NewCell(VERTICAL_DASH, NewStyle(console.ColorDarkGrey)),
|
||||||
|
image.Pt(self.grid.maxTimeWidth-x*self.grid.paddingWidth, y+self.Inner.Min.Y+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw y axis line
|
||||||
|
for i := 0; i < self.Inner.Dy()-xAxisLabelsHeight-1; i++ {
|
||||||
|
buffer.SetCell(
|
||||||
|
NewCell(VERTICAL_DASH, NewStyle(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.timeExtremum.max.Add(time.Duration(-i) * time.Second)
|
||||||
|
buffer.SetString(
|
||||||
|
labelTime.Format("15:04:05"),
|
||||||
|
NewStyle(ColorWhite),
|
||||||
|
image.Pt(self.grid.maxTimeWidth-xAxisLabelsWidth/2-i*(self.grid.paddingWidth), self.Inner.Max.Y-1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw y axis labels
|
||||||
|
if self.grid.valueExtremum.max != self.grid.valueExtremum.min {
|
||||||
|
labelsCount := (self.Inner.Dy() - xAxisLabelsHeight - 1) / (yAxisLabelsGap + yAxisLabelsHeight)
|
||||||
|
valuePerY := (self.grid.valueExtremum.max - self.grid.valueExtremum.min) / float64(self.Inner.Dy()-xAxisLabelsHeight-3)
|
||||||
|
for i := 0; i < int(labelsCount); i++ {
|
||||||
|
value := self.grid.valueExtremum.max - (valuePerY * float64(i) * (yAxisLabelsGap + yAxisLabelsHeight))
|
||||||
|
buffer.SetString(
|
||||||
|
formatValue(value, self.precision),
|
||||||
|
NewStyle(ColorWhite),
|
||||||
|
image.Pt(self.Inner.Min.X, 1+self.Inner.Min.Y+i*(yAxisLabelsGap+yAxisLabelsHeight)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer.SetString(
|
||||||
|
formatValue(self.grid.valueExtremum.max, self.precision),
|
||||||
|
NewStyle(ColorWhite),
|
||||||
|
image.Pt(self.Inner.Min.X, self.Inner.Dy()/2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (self *RunChart) renderLegend(buffer *Buffer, rectangle image.Rectangle) {
|
func (self *RunChart) renderLegend(buffer *Buffer, rectangle image.Rectangle) {
|
||||||
for i, line := range self.lines {
|
for i, line := range self.lines {
|
||||||
|
|
||||||
extremum := GetLineValueExtremum(line.points)
|
extremum := GetLineValueExtremum(line.points)
|
||||||
|
|
||||||
buffer.SetString(
|
buffer.SetString(
|
||||||
fmt.Sprintf("•"),
|
string(DOT),
|
||||||
NewStyle(line.item.Color),
|
NewStyle(line.item.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),
|
||||||
)
|
)
|
||||||
|
@ -281,25 +317,42 @@ func (self *RunChart) renderLegend(buffer *Buffer, rectangle image.Rectangle) {
|
||||||
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(
|
buffer.SetString(
|
||||||
fmt.Sprintf("max %.3f", extremum.max),
|
fmt.Sprintf("cur %s", formatValue(line.points[len(line.points)-1].Value, self.precision)),
|
||||||
NewStyle(ColorWhite),
|
NewStyle(ColorWhite),
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+2+i*5),
|
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+2+i*5),
|
||||||
)
|
)
|
||||||
buffer.SetString(
|
buffer.SetString(
|
||||||
fmt.Sprintf("min %.3f", extremum.min),
|
fmt.Sprintf("max %s", formatValue(extremum.max, self.precision)),
|
||||||
NewStyle(ColorWhite),
|
NewStyle(ColorWhite),
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+3+i*5),
|
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+3+i*5),
|
||||||
)
|
)
|
||||||
buffer.SetString(
|
buffer.SetString(
|
||||||
fmt.Sprintf("cur %.3f", line.points[len(line.points)-1].Value),
|
fmt.Sprintf("min %s", formatValue(extremum.min, self.precision)),
|
||||||
NewStyle(ColorWhite),
|
NewStyle(ColorWhite),
|
||||||
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+4+i*5),
|
image.Pt(self.Inner.Max.X-xAxisLegendWidth, self.Inner.Min.Y+4+i*5),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *RunChart) isTimePointInRange(point TimePoint) bool {
|
func (self *RunChart) getMaxValueLength() int {
|
||||||
return point.Time.After(self.grid.timeExtremum.min.Add(self.grid.paddingDuration))
|
|
||||||
|
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 formatValue(value float64, precision int) string {
|
||||||
|
format := " %." + strconv.Itoa(precision) + "f"
|
||||||
|
return fmt.Sprintf(format, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetChartValueExtremum(items []TimeLine) ValueExtremum {
|
func GetChartValueExtremum(items []TimeLine) ValueExtremum {
|
||||||
|
|
Loading…
Reference in New Issue