You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2020/09/15 13:17:19 UTC
[skywalking-cli] branch master updated: Implement auto-refresh for
`dashboard` (#63)
This is an automated email from the ASF dual-hosted git repository.
kezhenxu94 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-cli.git
The following commit(s) were added to refs/heads/master by this push:
new 98f1a92 Implement auto-refresh for `dashboard` (#63)
98f1a92 is described below
commit 98f1a9270985769b0e5d224c2d7044f3ab707cfe
Author: Hoshea Jiang <fg...@gmail.com>
AuthorDate: Tue Sep 15 21:14:06 2020 +0800
Implement auto-refresh for `dashboard` (#63)
---
README.md | 1 +
commands/dashboard/global/global.go | 6 ++
commands/flags/duration.go | 4 +
commands/interceptor/duration.go | 24 +++--
commands/interceptor/duration_test.go | 2 +-
display/graph/dashboard/global.go | 190 +++++++++++++++++++++++++++-------
display/graph/gauge/gauge.go | 67 +++++++++---
display/graph/heatmap/heatmap.go | 13 ++-
display/graph/linear/linear.go | 31 ++++--
graphql/utils/constants.go | 13 +++
lib/heatmap/heatmap.go | 12 +++
11 files changed, 286 insertions(+), 77 deletions(-)
diff --git a/README.md b/README.md
index 4c634c8..c1d0dee 100644
--- a/README.md
+++ b/README.md
@@ -311,6 +311,7 @@ You can imitate the content of [the default template file](example/Dashboard.Glo
| argument | description | default |
| :--- | :--- | :--- |
| `--template` | The template file to customize how to display information | `templates/Dashboard.Global.json` |
+| `--refresh` | The interval of auto-refresh (s). When `start` and `end` are both present, auto-refresh is disabled. | `6` |
| `--start` | See [Common options](#common-options) | See [Common options](#common-options) |
| `--end` | See [Common options](#common-options) | See [Common options](#common-options) |
diff --git a/commands/dashboard/global/global.go b/commands/dashboard/global/global.go
index baa0226..577abde 100644
--- a/commands/dashboard/global/global.go
+++ b/commands/dashboard/global/global.go
@@ -45,6 +45,12 @@ var GlobalCommand = cli.Command{
Required: false,
Value: dashboard.DefaultTemplatePath,
},
+ cli.IntFlag{
+ Name: "refresh",
+ Usage: "the auto refreshing interval (s)",
+ Required: false,
+ Value: 6,
+ },
},
),
Before: interceptor.BeforeChain([]cli.BeforeFunc{
diff --git a/commands/flags/duration.go b/commands/flags/duration.go
index f026aea..152ba0e 100644
--- a/commands/flags/duration.go
+++ b/commands/flags/duration.go
@@ -45,4 +45,8 @@ var DurationFlags = []cli.Flag{
Selected: schema.StepMinute,
},
},
+ cli.StringFlag{
+ Name: "durationType",
+ Usage: "the type of duration",
+ },
}
diff --git a/commands/interceptor/duration.go b/commands/interceptor/duration.go
index c52ae69..6e382e0 100644
--- a/commands/interceptor/duration.go
+++ b/commands/interceptor/duration.go
@@ -29,7 +29,7 @@ import (
"github.com/apache/skywalking-cli/logger"
)
-func tryParseTime(unparsed string) (schema.Step, time.Time, error) {
+func TryParseTime(unparsed string) (schema.Step, time.Time, error) {
var possibleError error = nil
for step, layout := range utils.StepFormats {
t, err := time.Parse(layout, unparsed)
@@ -48,7 +48,7 @@ func DurationInterceptor(ctx *cli.Context) error {
end := ctx.String("end")
timezone := ctx.GlobalString("timezone")
- startTime, endTime, step := ParseDuration(start, end, timezone)
+ startTime, endTime, step, dt := ParseDuration(start, end, timezone)
if err := ctx.Set("start", startTime.Format(utils.StepFormats[step])); err != nil {
return err
@@ -56,6 +56,8 @@ func DurationInterceptor(ctx *cli.Context) error {
return err
} else if err := ctx.Set("step", step.String()); err != nil {
return err
+ } else if err := ctx.Set("durationType", dt.String()); err != nil {
+ return err
}
return nil
}
@@ -68,7 +70,7 @@ func DurationInterceptor(ctx *cli.Context) error {
// then: end := now + 30 units, where unit is the precision of `start`, (hours, minutes, etc.)
// if --start is absent, --end is given,
// then: start := end - 30 units, where unit is the precision of `end`, (hours, minutes, etc.)
-func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, step schema.Step) {
+func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, step schema.Step, dt utils.DurationType) {
logger.Log.Debugln("Start time:", start, "end time:", end, "timezone:", timezone)
now := time.Now()
@@ -84,7 +86,7 @@ func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, s
// both are absent
if start == "" && end == "" {
- return now.Add(-30 * time.Minute), now, schema.StepMinute
+ return now.Add(-30 * time.Minute), now, schema.StepMinute, utils.BothAbsent
}
var err error
@@ -93,24 +95,24 @@ func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, s
if len(start) > 0 && len(end) > 0 {
start, end = AlignPrecision(start, end)
- if _, startTime, err = tryParseTime(start); err != nil {
+ if _, startTime, err = TryParseTime(start); err != nil {
logger.Log.Fatalln("Unsupported time format:", start, err)
}
- if step, endTime, err = tryParseTime(end); err != nil {
+ if step, endTime, err = TryParseTime(end); err != nil {
logger.Log.Fatalln("Unsupported time format:", end, err)
}
- return startTime, endTime, step
+ return startTime, endTime, step, utils.BothPresent
} else if end == "" { // end is absent
- if step, startTime, err = tryParseTime(start); err != nil {
+ if step, startTime, err = TryParseTime(start); err != nil {
logger.Log.Fatalln("Unsupported time format:", start, err)
}
- return startTime, startTime.Add(30 * utils.StepDuration[step]), step
+ return startTime, startTime.Add(30 * utils.StepDuration[step]), step, utils.EndAbsent
} else { // start is absent
- if step, endTime, err = tryParseTime(end); err != nil {
+ if step, endTime, err = TryParseTime(end); err != nil {
logger.Log.Fatalln("Unsupported time format:", end, err)
}
- return endTime.Add(-30 * utils.StepDuration[step]), endTime, step
+ return endTime.Add(-30 * utils.StepDuration[step]), endTime, step, utils.StartAbsent
}
}
diff --git a/commands/interceptor/duration_test.go b/commands/interceptor/duration_test.go
index b4f8c90..10c8de5 100644
--- a/commands/interceptor/duration_test.go
+++ b/commands/interceptor/duration_test.go
@@ -82,7 +82,7 @@ func TestParseDuration(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- gotStartTime, gotEndTime, gotStep := ParseDuration(tt.args.start, tt.args.end, "")
+ gotStartTime, gotEndTime, gotStep, _ := ParseDuration(tt.args.start, tt.args.end, "")
current := gotStartTime.Truncate(time.Minute).Format(timeFormat)
spec := tt.wantedStartTime.Truncate(time.Minute).Format(timeFormat)
if !reflect.DeepEqual(current, spec) {
diff --git a/display/graph/dashboard/global.go b/display/graph/dashboard/global.go
index 1d4a2d6..d28f3a7 100644
--- a/display/graph/dashboard/global.go
+++ b/display/graph/dashboard/global.go
@@ -22,9 +22,12 @@ import (
"fmt"
"math"
"strings"
+ "time"
+ "github.com/apache/skywalking-cli/commands/interceptor"
+ "github.com/apache/skywalking-cli/graphql/schema"
"github.com/apache/skywalking-cli/graphql/utils"
- "github.com/apache/skywalking-cli/lib/heatmap"
+ lib "github.com/apache/skywalking-cli/lib/heatmap"
"github.com/mattn/go-runewidth"
"github.com/mum4k/termdash"
@@ -35,6 +38,7 @@ import (
"github.com/urfave/cli"
"github.com/apache/skywalking-cli/display/graph/gauge"
+ "github.com/apache/skywalking-cli/display/graph/heatmap"
"github.com/apache/skywalking-cli/display/graph/linear"
"github.com/apache/skywalking-cli/graphql/dashboard"
@@ -71,7 +75,7 @@ var strToLayoutType = map[string]layoutType{
type widgets struct {
gauges []*gauge.MetricColumn
linears []*linechart.LineChart
- heatmap *heatmap.HeatMap
+ heatmap *lib.HeatMap
// buttons are used to change the layout.
buttons []*button.Button
@@ -80,9 +84,20 @@ type widgets struct {
// linearTitles are titles of each line chart, load from the template file.
var linearTitles []string
+// template determines how the global dashboard is displayed.
+var template *dashboard.GlobalTemplate
+
+var allWidgets *widgets
+
+var initStartStr string
+var initEndStr string
+
+var curStartTime time.Time
+var curEndTime time.Time
+
// setLayout sets the specified layout.
-func setLayout(c *container.Container, w *widgets, lt layoutType) error {
- gridOpts, err := gridLayout(w, lt)
+func setLayout(c *container.Container, lt layoutType) error {
+ gridOpts, err := gridLayout(lt)
if err != nil {
return err
}
@@ -90,16 +105,16 @@ func setLayout(c *container.Container, w *widgets, lt layoutType) error {
}
// newLayoutButtons returns buttons that dynamically switch the layouts.
-func newLayoutButtons(c *container.Container, w *widgets, template *dashboard.ButtonTemplate) ([]*button.Button, error) {
+func newLayoutButtons(c *container.Container) ([]*button.Button, error) {
var buttons []*button.Button
opts := []button.Option{
- button.WidthFor(longestString(template.Texts)),
- button.FillColor(cell.ColorNumber(template.ColorNum)),
- button.Height(template.Height),
+ button.WidthFor(longestString(template.Buttons.Texts)),
+ button.FillColor(cell.ColorNumber(template.Buttons.ColorNum)),
+ button.Height(template.Buttons.Height),
}
- for _, text := range template.Texts {
+ for _, text := range template.Buttons.Texts {
// declare a local variable lt to avoid closure.
lt, ok := strToLayoutType[text]
if !ok {
@@ -107,7 +122,7 @@ func newLayoutButtons(c *container.Container, w *widgets, template *dashboard.Bu
}
b, err := button.New(text, func() error {
- return setLayout(c, w, lt)
+ return setLayout(c, lt)
}, opts...)
if err != nil {
return nil, err
@@ -119,13 +134,13 @@ func newLayoutButtons(c *container.Container, w *widgets, template *dashboard.Bu
}
// gridLayout prepares container options that represent the desired screen layout.
-func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
+func gridLayout(lt layoutType) ([]container.Option, error) {
const buttonRowHeight = 15
- buttonColWidthPerc := 100 / len(w.buttons)
+ buttonColWidthPerc := 100 / len(allWidgets.buttons)
var buttonCols []grid.Element
- for _, b := range w.buttons {
+ for _, b := range allWidgets.buttons {
buttonCols = append(buttonCols, grid.ColWidthPerc(buttonColWidthPerc, grid.Widget(b)))
}
@@ -136,11 +151,11 @@ func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
switch lt {
case layoutMetrics:
rows = append(rows,
- grid.RowHeightPerc(70, gauge.MetricColumnsElement(w.gauges)...),
+ grid.RowHeightPerc(70, gauge.MetricColumnsElement(allWidgets.gauges)...),
)
case layoutLineChart:
- lcElements := linear.LineChartElements(w.linears, linearTitles)
+ lcElements := linear.LineChartElements(allWidgets.linears, linearTitles)
percentage := int(math.Min(99, float64((100-buttonRowHeight)/len(lcElements))))
for _, e := range lcElements {
@@ -156,7 +171,7 @@ func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
grid.RowHeightPerc(
99-buttonRowHeight,
grid.ColWidthPerc((99-heatmapColWidth)/2), // Use two empty cols to center the heatmap.
- grid.ColWidthPerc(heatmapColWidth, grid.Widget(w.heatmap)),
+ grid.ColWidthPerc(heatmapColWidth, grid.Widget(allWidgets.heatmap)),
grid.ColWidthPerc((99-heatmapColWidth)/2),
),
)
@@ -174,7 +189,7 @@ func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
}
// newWidgets creates all widgets used by the dashboard.
-func newWidgets(data *dashboard.GlobalData, template *dashboard.GlobalTemplate) (*widgets, error) {
+func newWidgets(data *dashboard.GlobalData) error {
var columns []*gauge.MetricColumn
var linears []*linechart.LineChart
@@ -182,7 +197,7 @@ func newWidgets(data *dashboard.GlobalData, template *dashboard.GlobalTemplate)
for i, t := range template.Metrics {
col, err := gauge.NewMetricColumn(data.Metrics[i], &t)
if err != nil {
- return nil, err
+ return err
}
columns = append(columns, col)
}
@@ -191,26 +206,21 @@ func newWidgets(data *dashboard.GlobalData, template *dashboard.GlobalTemplate)
for _, input := range data.ResponseLatency {
l, err := linear.NewLineChart(input)
if err != nil {
- return nil, err
+ return err
}
linears = append(linears, l)
}
// Create a heat map.
- hp, err := heatmap.NewHeatMap()
+ hp, err := heatmap.NewHeatMapWidget(data.HeatMap)
if err != nil {
- return nil, err
+ return err
}
- hpColumns := utils.HeatMapToMap(&data.HeatMap)
- yLabels := utils.BucketsToStrings(data.HeatMap.Buckets)
- hp.SetColumns(hpColumns)
- hp.SetYLabels(yLabels)
-
- return &widgets{
- gauges: columns,
- linears: linears,
- heatmap: hp,
- }, nil
+
+ allWidgets.gauges = columns
+ allWidgets.linears = linears
+ allWidgets.heatmap = hp
+ return nil
}
func Display(ctx *cli.Context, data *dashboard.GlobalData) error {
@@ -229,23 +239,31 @@ func Display(ctx *cli.Context, data *dashboard.GlobalData) error {
return err
}
- template, err := dashboard.LoadTemplate(ctx.String("template"))
+ te, err := dashboard.LoadTemplate(ctx.String("template"))
if err != nil {
return err
}
+ template = te
linearTitles = strings.Split(template.ResponseLatency.Labels, ", ")
- w, err := newWidgets(data, template)
+ // Initialization
+ allWidgets = &widgets{
+ gauges: nil,
+ linears: nil,
+ heatmap: nil,
+ buttons: nil,
+ }
+ err = newWidgets(data)
if err != nil {
- panic(err)
+ return err
}
- lb, err := newLayoutButtons(c, w, &template.Buttons)
+ lb, err := newLayoutButtons(c)
if err != nil {
return err
}
- w.buttons = lb
+ allWidgets.buttons = lb
- gridOpts, err := gridLayout(w, layoutMetrics)
+ gridOpts, err := gridLayout(layoutMetrics)
if err != nil {
return err
}
@@ -261,7 +279,15 @@ func Display(ctx *cli.Context, data *dashboard.GlobalData) error {
}
}
- err = termdash.Run(con, t, c, termdash.KeyboardSubscriber(quitter))
+ refreshInterval := time.Duration(ctx.Int("refresh")) * time.Second
+ dt := utils.DurationType(ctx.String("durationType"))
+
+ // Only when users use the relative time, the duration will be adjusted to refresh.
+ if dt != utils.BothPresent {
+ go refresh(con, ctx, refreshInterval)
+ }
+
+ err = termdash.Run(con, t, c, termdash.KeyboardSubscriber(quitter), termdash.RedrawInterval(refreshInterval))
return err
}
@@ -277,3 +303,91 @@ func longestString(strs []string) (ret string) {
}
return
}
+
+// refresh updates the duration and query the new data to update all of widgets, once every delay.
+func refresh(con context.Context, ctx *cli.Context, interval time.Duration) {
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+
+ initStartStr = ctx.String("start")
+ initEndStr = ctx.String("end")
+
+ _, start, err := interceptor.TryParseTime(initStartStr)
+ if err != nil {
+ return
+ }
+ _, end, err := interceptor.TryParseTime(initEndStr)
+ if err != nil {
+ return
+ }
+
+ curStartTime = start
+ curEndTime = end
+
+ for {
+ select {
+ case <-ticker.C:
+ d, err := updateDuration(interval)
+ if err != nil {
+ continue
+ }
+
+ data := dashboard.Global(ctx, d)
+ if err := updateAllWidgets(data); err != nil {
+ continue
+ }
+ case <-con.Done():
+ return
+ }
+ }
+}
+
+// updateDuration will check if the duration changes after adding the interval.
+// If the duration doesn't change, an error will be returned, and the dashboard will not refresh.
+// Otherwise, a new duration will be returned, which is used to get the latest global data.
+func updateDuration(interval time.Duration) (schema.Duration, error) {
+ step, _, err := interceptor.TryParseTime(initStartStr)
+ if err != nil {
+ return schema.Duration{}, err
+ }
+
+ curStartTime = curStartTime.Add(interval)
+ curEndTime = curEndTime.Add(interval)
+
+ curStartStr := curStartTime.Format(utils.StepFormats[step])
+ curEndStr := curEndTime.Format(utils.StepFormats[step])
+
+ if curStartStr == initStartStr && curEndStr == initEndStr {
+ return schema.Duration{}, fmt.Errorf("the duration does not update")
+ }
+
+ initStartStr = curStartStr
+ initEndStr = curEndStr
+ return schema.Duration{
+ Start: curStartStr,
+ End: curEndStr,
+ Step: step,
+ }, nil
+}
+
+// updateAllWidgets will update all of widgets' data to be displayed.
+func updateAllWidgets(data *dashboard.GlobalData) error {
+ // Update gauges
+ for i, mcData := range data.Metrics {
+ if err := allWidgets.gauges[i].Update(mcData); err != nil {
+ return err
+ }
+ }
+
+ // Update line charts.
+ for i, inputs := range data.ResponseLatency {
+ if err := linear.SetLineChartSeries(allWidgets.linears[i], inputs); err != nil {
+ return err
+ }
+ }
+
+ // Update the heat map.
+ heatmap.SetData(allWidgets.heatmap, data.HeatMap)
+
+ return nil
+}
diff --git a/display/graph/gauge/gauge.go b/display/graph/gauge/gauge.go
index e143578..c7571ee 100644
--- a/display/graph/gauge/gauge.go
+++ b/display/graph/gauge/gauge.go
@@ -28,6 +28,7 @@ import (
"github.com/apache/skywalking-cli/graphql/dashboard"
"github.com/apache/skywalking-cli/graphql/schema"
+ "github.com/apache/skywalking-cli/util"
"github.com/mum4k/termdash"
"github.com/mum4k/termdash/cell"
@@ -43,13 +44,38 @@ import (
const RootID = "root"
type MetricColumn struct {
- title *text.Text
- gauges []*gauge.Gauge
+ title *text.Text
+ gauges []*gauge.Gauge
+ aggregationNum int
+}
+
+// Update updates the MetricColumn's `Absolute` and `BorderTitle`.
+func (mc *MetricColumn) Update(data []*schema.SelectedRecord) error {
+ for i, item := range data {
+ strValue := *(item.Value)
+ v, err := strconv.Atoi(strValue)
+ if err != nil {
+ return err
+ }
+
+ if mc.aggregationNum != 0 {
+ strValue = fmt.Sprintf("%.4f", float64(v)/float64(mc.aggregationNum))
+ }
+
+ maxValue, err := findMaxValue(data)
+ if err != nil {
+ return err
+ }
+
+ if err := mc.gauges[i].Absolute(v, maxValue, gauge.BorderTitle("["+strValue+"]")); err != nil {
+ return err
+ }
+ }
+ return nil
}
func NewMetricColumn(column []*schema.SelectedRecord, config *dashboard.MetricTemplate) (*MetricColumn, error) {
var ret MetricColumn
- var maxValue int
t, err := text.New()
if err != nil {
@@ -60,18 +86,8 @@ func NewMetricColumn(column []*schema.SelectedRecord, config *dashboard.MetricTe
}
ret.title = t
- if config.Condition.Order == schema.OrderDes {
- temp, err := strconv.Atoi(*(column[0].Value))
- if err != nil {
- return nil, err
- }
- maxValue = temp
- } else if config.Condition.Order == schema.OrderAsc {
- temp, err := strconv.Atoi(*(column[len(column)-1].Value))
- if err != nil {
- return nil, err
- }
- maxValue = temp
+ if len(column) == 0 {
+ return nil, fmt.Errorf("the metrics data is empty, please check the GraphQL backend")
}
for _, item := range column {
@@ -87,6 +103,7 @@ func NewMetricColumn(column []*schema.SelectedRecord, config *dashboard.MetricTe
return nil, convErr
}
strValue = fmt.Sprintf("%.4f", float64(v)/float64(aggregationNum))
+ ret.aggregationNum = aggregationNum
}
g, err := gauge.New(
@@ -101,6 +118,11 @@ func NewMetricColumn(column []*schema.SelectedRecord, config *dashboard.MetricTe
return nil, err
}
+ maxValue, err := findMaxValue(column)
+ if err != nil {
+ return nil, err
+ }
+
if err := g.Absolute(v, maxValue); err != nil {
return nil, err
}
@@ -222,3 +244,18 @@ func Display(ctx *cli.Context, metrics [][]*schema.SelectedRecord) error {
return err
}
+
+// findMaxValue finds the maximum value in the array of `schema.SelectedRecord`.
+func findMaxValue(column []*schema.SelectedRecord) (int, error) {
+ var ret int
+
+ for _, c := range column {
+ v, err := strconv.Atoi(*(c.Value))
+ if err != nil {
+ return ret, err
+ }
+ ret = util.MaxInt(ret, v)
+ }
+
+ return ret, nil
+}
diff --git a/display/graph/heatmap/heatmap.go b/display/graph/heatmap/heatmap.go
index 1a40d29..12f64d4 100644
--- a/display/graph/heatmap/heatmap.go
+++ b/display/graph/heatmap/heatmap.go
@@ -44,11 +44,20 @@ func NewHeatMapWidget(data schema.HeatMap) (hp *heatmap.HeatMap, err error) {
return hp, err
}
- hpColumns := utils.HeatMapToMap(&data)
- yLabels := utils.BucketsToStrings(data.Buckets)
+ SetData(hp, data)
+ return
+}
+
+func SetData(hp *heatmap.HeatMap, data schema.HeatMap) {
+ hpColumns, yLabels := processData(data)
hp.SetColumns(hpColumns)
hp.SetYLabels(yLabels)
+}
+// processData converts data into hpColumns and yValues for the heat map.
+func processData(data schema.HeatMap) (hpColumns map[string][]int64, yLabels []string) {
+ hpColumns = utils.HeatMapToMap(&data)
+ yLabels = utils.BucketsToStrings(data.Buckets)
return
}
diff --git a/display/graph/linear/linear.go b/display/graph/linear/linear.go
index da219f4..bf0bc5c 100644
--- a/display/graph/linear/linear.go
+++ b/display/graph/linear/linear.go
@@ -36,11 +36,29 @@ import (
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))
+ 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
@@ -54,14 +72,7 @@ func NewLineChart(inputs map[string]float64) (lineChart *linechart.LineChart, er
yValues[index] = inputs[name]
index++
}
-
- if lineChart, err = linechart.New(linechart.YAxisAdaptive()); err != nil {
- return
- }
-
- err = lineChart.Series("graph-linear", yValues, linechart.SeriesXLabels(xLabels))
-
- return lineChart, err
+ return
}
// LineChartElements is the part that separated from layout,
diff --git a/graphql/utils/constants.go b/graphql/utils/constants.go
index 5650c57..2f527e3 100644
--- a/graphql/utils/constants.go
+++ b/graphql/utils/constants.go
@@ -38,3 +38,16 @@ var StepDuration = map[schema.Step]time.Duration{
schema.StepHour: time.Hour,
schema.StepDay: time.Hour * 24,
}
+
+type DurationType string
+
+const (
+ BothAbsent DurationType = "BothAbsent"
+ BothPresent DurationType = "BothPresent"
+ StartAbsent DurationType = "StartAbsent"
+ EndAbsent DurationType = "EndAbsent"
+)
+
+func (dt DurationType) String() string {
+ return string(dt)
+}
diff --git a/lib/heatmap/heatmap.go b/lib/heatmap/heatmap.go
index 3826ddb..cdc466f 100644
--- a/lib/heatmap/heatmap.go
+++ b/lib/heatmap/heatmap.go
@@ -106,6 +106,12 @@ func (hp *HeatMap) SetColumns(values map[string][]int64) {
}
sort.Strings(names)
+ // Clear XLabels and columns.
+ if len(hp.XLabels) > 0 {
+ hp.XLabels = hp.XLabels[:0]
+ }
+ hp.columns = make(map[string]*columnValues)
+
for _, name := range names {
cv := newColumnValues(values[name])
hp.columns[name] = cv
@@ -123,8 +129,14 @@ func (hp *HeatMap) SetYLabels(labels []string) {
hp.mu.Lock()
defer hp.mu.Unlock()
+ // Clear YLabels.
+ if len(hp.YLabels) > 0 {
+ hp.YLabels = hp.YLabels[:0]
+ }
+
hp.YLabels = append(hp.YLabels, labels...)
+ // Reverse the array.
for i, j := 0, len(hp.YLabels)-1; i < j; i, j = i+1, j-1 {
hp.YLabels[i], hp.YLabels[j] = hp.YLabels[j], hp.YLabels[i]
}