blob: 0186b4db29bc77b99a23f3101bc95a563c463479 [file] [log] [blame]
// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package linear
import (
"context"
"fmt"
"math"
"sort"
"strings"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/container"
"github.com/mum4k/termdash/container/grid"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/terminal/termbox"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgets/linechart"
"github.com/urfave/cli/v2"
)
const RootID = "root"
const defaultSeriesLabel = "linear"
func NewLineChart(inputs map[string]float64) (lineChart *linechart.LineChart, err error) {
if lineChart, err = linechart.New(linechart.YAxisAdaptive()); err != nil {
return
}
if err = SetLineChartSeries(lineChart, inputs); err != nil {
return
}
return lineChart, err
}
func SetLineChartSeries(lc *linechart.LineChart, inputs map[string]float64) error {
xLabels, yValues := processInputs(inputs)
return lc.Series(defaultSeriesLabel, yValues, linechart.SeriesXLabels(xLabels))
}
// processInputs converts inputs into xLabels and yValues for line charts.
func processInputs(inputs map[string]float64) (xLabels map[int]string, yValues []float64) {
index := 0
xLabels = map[int]string{}
yValues = make([]float64, len(inputs))
// The iteration order of map is uncertain, so the keys must be sorted explicitly.
var names []string
for name := range inputs {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
xLabels[index] = name
yValues[index] = inputs[name]
index++
}
return
}
// LineChartElements is the part that separated from layout,
// which can be reused by global dashboard.
func LineChartElements(lineCharts map[string]*linechart.LineChart) [][]grid.Element {
cols := maxSqrt(len(lineCharts))
rows := make([][]grid.Element, int(math.Ceil(float64(len(lineCharts))/float64(cols))))
var charts []*linechart.LineChart
var titles []string
for t := range lineCharts {
titles = append(titles, t)
}
sort.Strings(titles)
for _, title := range titles {
charts = append(charts, lineCharts[title])
}
for r := 0; r < len(rows); r++ {
var row []grid.Element
for c := 0; c < cols && r*cols+c < len(lineCharts); c++ {
percentage := int(math.Floor(float64(100) / float64(cols)))
if r == len(rows)-1 {
percentage = int(math.Floor(float64(100) / float64(len(lineCharts)-r*cols)))
}
title := titles[r*cols+c]
chart := charts[r*cols+c]
row = append(row, grid.ColWidthPerc(
int(math.Min(99, float64(percentage))),
grid.Widget(
chart,
container.Border(linestyle.Light),
container.BorderTitleAlignCenter(),
container.BorderTitle(title),
),
))
}
rows[r] = row
}
return rows
}
func layout(rows [][]grid.Element) ([]container.Option, error) {
builder := grid.New()
for _, row := range rows {
percentage := int(math.Min(99, float64(100/len(rows))))
builder.Add(grid.RowHeightPerc(percentage, row...))
}
return builder.Build()
}
func Display(cliCtx *cli.Context, inputs map[string]map[string]float64) error {
t, err := termbox.New()
if err != nil {
return err
}
defer t.Close()
c, err := container.New(
t,
container.ID(RootID),
)
if err != nil {
return err
}
elements := make(map[string]*linechart.LineChart)
for title, input := range inputs {
w, e := NewLineChart(input)
if e != nil {
return e
}
elements[title] = w
}
gridOpts, err := layout(LineChartElements(elements))
if err != nil {
return err
}
err = c.Update(RootID, append(
gridOpts,
container.Border(linestyle.Light),
container.BorderTitle(fmt.Sprintf("[%s]-PRESS Q TO QUIT", cliCtx.String("name"))),
container.BorderTitleAlignLeft())...,
)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
quitter := func(keyboard *terminalapi.Keyboard) {
if strings.EqualFold(keyboard.Key.String(), "q") {
cancel()
}
}
err = termdash.Run(ctx, t, c, termdash.KeyboardSubscriber(quitter))
return err
}
func maxSqrt(num int) int {
return int(math.Ceil(math.Sqrt(float64(num))))
}