pkg/display/graph/linear/linear.go (137 lines of code) (raw):

// 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/apache/skywalking-cli/pkg/display/displayable" "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]*displayable.MetricValue) (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]*displayable.MetricValue) 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]*displayable.MetricValue) (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].Value 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]*displayable.MetricValue) 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)))) }