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/03/08 06:57:33 UTC

[skywalking-cli] branch feature/thermodynamic updated (fadc3a4 -> fa4233b)

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a change to branch feature/thermodynamic
in repository https://gitbox.apache.org/repos/asf/skywalking-cli.git.


 discard fadc3a4  [Feature] Support visualization of heat map and enhance Display logic
    omit 09852ce  [Feature] Support top N entities and thermodynamic metrics
     add c38c595  [Feature] Support top N entities and thermodynamic metrics (#33)
     new fa4233b  [Feature] Support visualization of heat map and enhance Display logic

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (fadc3a4)
            \
             N -- N -- N   refs/heads/feature/thermodynamic (fa4233b)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:


[skywalking-cli] 01/01: [Feature] Support visualization of heat map and enhance Display logic

Posted by ke...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch feature/thermodynamic
in repository https://gitbox.apache.org/repos/asf/skywalking-cli.git

commit fa4233b23065ef7e429203bfec35538f232c19b6
Author: kezhenxu94 <ke...@163.com>
AuthorDate: Sun Mar 8 14:54:30 2020 +0800

    [Feature] Support visualization of heat map and enhance Display logic
    
    ### Motivation
    
    Visualize heat map and enhance Display logic
    
    ### Modification
    
    - Add HeatMap widget to visualize thermodynamic metrics.
    
    - Refactor display logic.
    
    ### Result
    
    - Thermodynamic metrics now can be visualized.
    
    - Display function takes more extra information to determine more details
    
    - Closes https://github.com/apache/skywalking/issues/4461
---
 README.md                                          |   8 +-
 commands/endpoint/list.go                          |   4 +-
 commands/instance/list.go                          |   4 +-
 commands/instance/search.go                        |   4 +-
 commands/metrics/aggregation/topn.go               |   4 +-
 commands/metrics/linear/linear-metrics.go          |   4 +-
 commands/metrics/linear/multiple-linear-metrics.go |   4 +-
 commands/metrics/single/single-metrics.go          |   4 +-
 commands/metrics/thermodynamic/thermodynamic.go    |   8 +-
 commands/service/list.go                           |   4 +-
 display/display.go                                 |  12 ++-
 .../{json/json.go => displayable/displayable.go}   |  19 ++--
 display/graph/graph.go                             |  22 ++--
 display/graph/heatmap/heatmap.go                   | 114 +++++++++++++++++++++
 display/json/json.go                               |  13 +--
 display/json/json_test.go                          |   4 +-
 display/table/table.go                             |   6 +-
 display/table/table_test.go                        |   4 +-
 display/yaml/yaml.go                               |  13 +--
 display/yaml/yaml_test.go                          |   4 +-
 dist/LICENSE                                       |   2 +
 dist/licenses/LICENSE-go-runewidth                 |  21 ++++
 dist/licenses/LICENSE-termui                       |  21 ++++
 go.mod                                             |   4 +-
 go.sum                                             |   7 ++
 graphql/metrics/metrics.go                         |   2 +-
 lib/heatmap.go                                     | 114 +++++++++++++++++++++
 display/json/json.go => util/math.go               |  18 +---
 28 files changed, 381 insertions(+), 67 deletions(-)

diff --git a/README.md b/README.md
index 9fba39b..7e748b7 100644
--- a/README.md
+++ b/README.md
@@ -461,13 +461,17 @@ $ ./bin/swctl metrics top 5 --name endpoint_sla --service-id 3
 
 <details>
 
-<summary>Query the overall heatmap</summary>
+<summary>Query the overall heat map</summary>
 
 ```shell
-$  ./bin/swctl metrics thermodynamic --name all_heatmap
+$ ./bin/swctl metrics thermodynamic --name all_heatmap
 {"nodes":[[0,0,238],[0,1,1],[0,2,39],[0,3,31],[0,4,12],[0,5,13],[0,6,4],[0,7,3],[0,8,3],[0,9,0],[0,10,48],[0,11,3],[0,12,49],[0,13,54],[0,14,11],[0,15,9],[0,16,2],[0,17,4],[0,18,0],[0,19,1],[0,20,186],[1,0,264],[1,1,3],[1,2,51],[1,3,38],[1,4,16],[1,5,14],[1,6,3],[1,7,2],[1,8,1],[1,9,2],[1,10,51],[1,11,1],[1,12,41],[1,13,56],[1,14,16],[1,15,15],[1,16,7],[1,17,7],[1,18,3],[1,19,1],[1,20,174],[2,0,231],[2,1,3],[2,2,42],[2,3,41],[2,4,18],[2,5,4],[2,6,2],[2,7,1],[2,8,2],[2,9,0],[2,10,54],[2,1 [...]
 ```
 
+```shell
+$ ./bin/swctl --display=graph metrics thermodynamic --name all_heatmap 
+```
+
 </details>
 
 <details>
diff --git a/commands/endpoint/list.go b/commands/endpoint/list.go
index 034c6a5..3c6767b 100644
--- a/commands/endpoint/list.go
+++ b/commands/endpoint/list.go
@@ -20,6 +20,8 @@ package endpoint
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metadata"
 
 	"github.com/apache/skywalking-cli/display"
@@ -56,6 +58,6 @@ var ListCommand = cli.Command{
 
 		endpoints := metadata.SearchEndpoints(ctx, serviceID, keyword, limit)
 
-		return display.Display(ctx, endpoints)
+		return display.Display(ctx, &displayable.Displayable{Data: endpoints})
 	},
 }
diff --git a/commands/instance/list.go b/commands/instance/list.go
index eda72c1..1cfcd88 100644
--- a/commands/instance/list.go
+++ b/commands/instance/list.go
@@ -20,6 +20,8 @@ package instance
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metadata"
 
 	"github.com/apache/skywalking-cli/commands/flags"
@@ -51,6 +53,6 @@ var ListCommand = cli.Command{
 			Step:  step.(*model.StepEnumValue).Selected,
 		})
 
-		return display.Display(ctx, instances)
+		return display.Display(ctx, &displayable.Displayable{Data: instances})
 	},
 }
diff --git a/commands/instance/search.go b/commands/instance/search.go
index b97adae..548606f 100644
--- a/commands/instance/search.go
+++ b/commands/instance/search.go
@@ -20,6 +20,8 @@ package instance
 import (
 	"regexp"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metadata"
 
 	"github.com/urfave/cli"
@@ -62,6 +64,6 @@ var SearchCommand = cli.Command{
 				}
 			}
 		}
-		return display.Display(ctx, result)
+		return display.Display(ctx, &displayable.Displayable{Data: result})
 	},
 }
diff --git a/commands/metrics/aggregation/topn.go b/commands/metrics/aggregation/topn.go
index 77876f6..ce6c63d 100644
--- a/commands/metrics/aggregation/topn.go
+++ b/commands/metrics/aggregation/topn.go
@@ -22,6 +22,8 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/commands/interceptor"
 
 	"github.com/urfave/cli"
@@ -107,6 +109,6 @@ var TopN = cli.Command{
 			metricsValues = aggregation.ServiceTopN(ctx, name, topN, duration, order)
 		}
 
-		return display.Display(ctx, metricsValues)
+		return display.Display(ctx, &displayable.Displayable{Data: metricsValues})
 	},
 }
diff --git a/commands/metrics/linear/linear-metrics.go b/commands/metrics/linear/linear-metrics.go
index d435063..c1aa19b 100644
--- a/commands/metrics/linear/linear-metrics.go
+++ b/commands/metrics/linear/linear-metrics.go
@@ -20,6 +20,8 @@ package linear
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metrics"
 	"github.com/apache/skywalking-cli/graphql/utils"
 
@@ -75,6 +77,6 @@ var Single = cli.Command{
 			ID:   id,
 		}, duration)
 
-		return display.Display(ctx, utils.MetricsToMap(duration, metricsValues))
+		return display.Display(ctx, &displayable.Displayable{Data: utils.MetricsToMap(duration, metricsValues)})
 	},
 }
diff --git a/commands/metrics/linear/multiple-linear-metrics.go b/commands/metrics/linear/multiple-linear-metrics.go
index 31db251..fae3580 100644
--- a/commands/metrics/linear/multiple-linear-metrics.go
+++ b/commands/metrics/linear/multiple-linear-metrics.go
@@ -20,6 +20,8 @@ package linear
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metrics"
 	"github.com/apache/skywalking-cli/graphql/utils"
 
@@ -88,6 +90,6 @@ var Multiple = cli.Command{
 			reshaped[index] = utils.MetricsToMap(duration, value)
 		}
 
-		return display.Display(ctx, reshaped)
+		return display.Display(ctx, &displayable.Displayable{Data: reshaped})
 	},
 }
diff --git a/commands/metrics/single/single-metrics.go b/commands/metrics/single/single-metrics.go
index 69405ea..1f710c0 100644
--- a/commands/metrics/single/single-metrics.go
+++ b/commands/metrics/single/single-metrics.go
@@ -20,6 +20,8 @@ package single
 import (
 	"strings"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metrics"
 
 	"github.com/urfave/cli"
@@ -75,6 +77,6 @@ var Command = cli.Command{
 			Step:  step.(*model.StepEnumValue).Selected,
 		})
 
-		return display.Display(ctx, metricsValues.Values)
+		return display.Display(ctx, &displayable.Displayable{Data: metricsValues.Values})
 	},
 }
diff --git a/commands/metrics/thermodynamic/thermodynamic.go b/commands/metrics/thermodynamic/thermodynamic.go
index 5eefb3a..05c4ba4 100644
--- a/commands/metrics/thermodynamic/thermodynamic.go
+++ b/commands/metrics/thermodynamic/thermodynamic.go
@@ -20,6 +20,8 @@ package thermodynamic
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/commands/flags"
 	"github.com/apache/skywalking-cli/commands/interceptor"
 	"github.com/apache/skywalking-cli/commands/model"
@@ -62,6 +64,10 @@ var Command = cli.Command{
 			Name: metricsName,
 		}, duration)
 
-		return display.Display(ctx, metricsValues)
+		return display.Display(ctx, &displayable.Displayable{
+			Data:     metricsValues,
+			Duration: duration,
+			Title:    metricsName,
+		})
 	},
 }
diff --git a/commands/service/list.go b/commands/service/list.go
index 2604a6f..14e2830 100644
--- a/commands/service/list.go
+++ b/commands/service/list.go
@@ -20,6 +20,8 @@ package service
 import (
 	"github.com/urfave/cli"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metadata"
 
 	"github.com/apache/skywalking-cli/commands/flags"
@@ -58,6 +60,6 @@ var ListCommand = cli.Command{
 			services = []schema.Service{service}
 		}
 
-		return display.Display(ctx, services)
+		return display.Display(ctx, &displayable.Displayable{Data: services})
 	},
 }
diff --git a/display/display.go b/display/display.go
index e620673..af3a598 100644
--- a/display/display.go
+++ b/display/display.go
@@ -21,6 +21,8 @@ import (
 	"fmt"
 	"strings"
 
+	d "github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/display/graph"
 
 	"github.com/urfave/cli"
@@ -38,18 +40,18 @@ const (
 )
 
 // Display the object in the style specified in flag --display
-func Display(ctx *cli.Context, object interface{}) error {
+func Display(ctx *cli.Context, displayable *d.Displayable) error {
 	displayStyle := ctx.GlobalString("display")
 
 	switch strings.ToLower(displayStyle) {
 	case JSON:
-		return json.Display(object)
+		return json.Display(displayable)
 	case YAML:
-		return yaml.Display(object)
+		return yaml.Display(displayable)
 	case TABLE:
-		return table.Display(object)
+		return table.Display(displayable)
 	case GRAPH:
-		return graph.Display(object)
+		return graph.Display(displayable)
 	default:
 		return fmt.Errorf("unsupported display style: %s", displayStyle)
 	}
diff --git a/display/json/json.go b/display/displayable/displayable.go
similarity index 79%
copy from display/json/json.go
copy to display/displayable/displayable.go
index 25aaccf..12ca7d2 100644
--- a/display/json/json.go
+++ b/display/displayable/displayable.go
@@ -15,19 +15,12 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package json
+package displayable
 
-import (
-	"encoding/json"
-	"fmt"
-)
+import "github.com/apache/skywalking-cli/graphql/schema"
 
-func Display(object interface{}) error {
-	if bytes, e := json.Marshal(object); e == nil {
-		fmt.Printf("%v\n", string(bytes))
-	} else {
-		return e
-	}
-
-	return nil
+type Displayable struct {
+	Data     interface{}
+	Duration schema.Duration
+	Title    string
 }
diff --git a/display/graph/graph.go b/display/graph/graph.go
index c27f835..e8fc072 100644
--- a/display/graph/graph.go
+++ b/display/graph/graph.go
@@ -21,21 +21,31 @@ import (
 	"fmt"
 	"reflect"
 
+	"github.com/apache/skywalking-cli/display/graph/heatmap"
+	"github.com/apache/skywalking-cli/graphql/schema"
+
+	d "github.com/apache/skywalking-cli/display/displayable"
 	"github.com/apache/skywalking-cli/display/graph/linear"
 )
 
-func Display(object interface{}) error {
-	if reflect.TypeOf(object) == reflect.TypeOf(map[string]float64{}) {
-		kvs := []map[string]float64{object.(map[string]float64)}
+func Display(displayable *d.Displayable) error {
+	data := displayable.Data
+
+	if reflect.TypeOf(data) == reflect.TypeOf(schema.Thermodynamic{}) {
+		return heatmap.Display(displayable)
+	}
+
+	if reflect.TypeOf(data) == reflect.TypeOf(map[string]float64{}) {
+		kvs := []map[string]float64{data.(map[string]float64)}
 
 		return linear.Display(kvs)
 	}
 
-	if reflect.TypeOf(object) == reflect.TypeOf([]map[string]float64{}) {
-		kvs := object.([]map[string]float64)
+	if reflect.TypeOf(data) == reflect.TypeOf([]map[string]float64{}) {
+		kvs := data.([]map[string]float64)
 
 		return linear.Display(kvs)
 	}
 
-	return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(object))
+	return fmt.Errorf("type of %T is not supported to be displayed as ascii graph", reflect.TypeOf(data))
 }
diff --git a/display/graph/heatmap/heatmap.go b/display/graph/heatmap/heatmap.go
new file mode 100644
index 0000000..0ca653a
--- /dev/null
+++ b/display/graph/heatmap/heatmap.go
@@ -0,0 +1,114 @@
+// 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 heatmap
+
+import (
+	"fmt"
+	"math"
+	"time"
+
+	"github.com/apache/skywalking-cli/graphql/utils"
+	"github.com/apache/skywalking-cli/util"
+
+	ui "github.com/gizak/termui/v3"
+
+	d "github.com/apache/skywalking-cli/display/displayable"
+	"github.com/apache/skywalking-cli/graphql/schema"
+	"github.com/apache/skywalking-cli/lib"
+)
+
+func Display(displayable *d.Displayable) error {
+	data := displayable.Data.(schema.Thermodynamic)
+
+	nodes := data.Nodes
+	duration := displayable.Duration
+
+	rows, cols, min, max := statistics(nodes)
+
+	if err := ui.Init(); err != nil {
+		return err
+	}
+	defer ui.Close()
+
+	termW, _ := ui.TerminalDimensions()
+
+	hm := lib.NewHeatMap()
+	hm.Title = fmt.Sprintf(" %s ", displayable.Title)
+	hm.XLabels = make([]string, rows)
+	hm.YLabels = make([]string, cols)
+	for i := 0; i < rows; i++ {
+		step := utils.StepDuration[duration.Step]
+		format := utils.StepFormats[duration.Step]
+		startTime, err := time.Parse(format, duration.Start)
+
+		if err != nil {
+			return err
+		}
+
+		hm.XLabels[i] = startTime.Add(time.Duration(i) * step).Format("15:04")
+	}
+	for i := 0; i < cols; i++ {
+		hm.YLabels[i] = fmt.Sprintf("%4d", i*data.AxisYStep)
+	}
+
+	hm.Data = make([][]float64, rows)
+	hm.CellColors = make([][]ui.Color, rows)
+	hm.NumStyles = make([][]ui.Style, rows)
+	for row := 0; row < rows; row++ {
+		hm.Data[row] = make([]float64, cols)
+		hm.CellColors[row] = make([]ui.Color, cols)
+		hm.NumStyles[row] = make([]ui.Style, cols)
+	}
+
+	scale := max - min
+	for _, node := range nodes {
+		color := ui.Color(255 - (float64(*node[2])/scale)*23)
+		hm.Data[*node[0]][*node[1]] = float64(*node[2])
+		hm.CellColors[*node[0]][*node[1]] = color
+		hm.NumStyles[*node[0]][*node[1]] = ui.Style{Fg: ui.ColorMagenta}
+	}
+
+	hm.Formatter = nil
+	hm.XLabelStyles = []ui.Style{{Fg: ui.ColorWhite}}
+	hm.CellGap = 0
+	hm.CellWidth = int(float64(termW) / float64(rows))
+	realWidth := (hm.CellWidth+hm.CellGap)*(rows+1) - hm.CellGap + 5
+	hm.SetRect(int(float64(termW-realWidth)/2), 2, realWidth, cols+5)
+
+	ui.Render(hm)
+
+	events := ui.PollEvents()
+	for e := <-events; e.ID != "q" && e.ID != "<C-c>"; e = <-events {
+	}
+	return nil
+}
+
+func statistics(nodes [][]*int) (rows, cols int, min, max float64) {
+	min = math.MaxFloat64
+
+	for _, node := range nodes {
+		rows = util.MaxInt(rows, *node[0])
+		cols = util.MaxInt(cols, *node[1])
+		max = math.Max(max, float64(*node[2]))
+		min = math.Min(min, float64(*node[2]))
+	}
+
+	rows++
+	cols++
+	return
+}
diff --git a/display/json/json.go b/display/json/json.go
index 25aaccf..12a506f 100644
--- a/display/json/json.go
+++ b/display/json/json.go
@@ -20,14 +20,15 @@ package json
 import (
 	"encoding/json"
 	"fmt"
+
+	d "github.com/apache/skywalking-cli/display/displayable"
 )
 
-func Display(object interface{}) error {
-	if bytes, e := json.Marshal(object); e == nil {
-		fmt.Printf("%v\n", string(bytes))
-	} else {
+func Display(displayable *d.Displayable) error {
+	bytes, e := json.Marshal(displayable.Data)
+	if e != nil {
 		return e
 	}
-
-	return nil
+	_, e = fmt.Printf("%v\n", string(bytes))
+	return e
 }
diff --git a/display/json/json_test.go b/display/json/json_test.go
index 8e30107..9d8ead4 100644
--- a/display/json/json_test.go
+++ b/display/json/json_test.go
@@ -20,6 +20,8 @@ package json
 import (
 	"testing"
 
+	d "github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -36,7 +38,7 @@ func TestJsonDisplay(t *testing.T) {
 }
 
 func display(t *testing.T, result []schema.Service) {
-	if err := Display(result); err != nil {
+	if err := Display(&d.Displayable{Data: result}); err != nil {
 		t.Error(err)
 	}
 }
diff --git a/display/table/table.go b/display/table/table.go
index ce2a5ea..8e2227d 100644
--- a/display/table/table.go
+++ b/display/table/table.go
@@ -21,15 +21,17 @@ import (
 	"encoding/json"
 	"os"
 
+	d "github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/logger"
 
 	"github.com/olekukonko/tablewriter"
 )
 
-func Display(object interface{}) error {
+func Display(displayable *d.Displayable) error {
 	var stringMapArrays []map[string]string
 
-	bytes, _ := json.Marshal(object)
+	bytes, _ := json.Marshal(displayable.Data)
 	_ = json.Unmarshal(bytes, &stringMapArrays)
 
 	if len(stringMapArrays) < 1 {
diff --git a/display/table/table_test.go b/display/table/table_test.go
index fd24460..ec2b265 100644
--- a/display/table/table_test.go
+++ b/display/table/table_test.go
@@ -20,6 +20,8 @@ package table
 import (
 	"testing"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -36,7 +38,7 @@ func TestTableDisplay(t *testing.T) {
 }
 
 func display(t *testing.T, result []schema.Service) {
-	if err := Display(result); err != nil {
+	if err := Display(&displayable.Displayable{Data: result}); err != nil {
 		t.Error(err)
 	}
 }
diff --git a/display/yaml/yaml.go b/display/yaml/yaml.go
index bb8cb79..3f3c98b 100644
--- a/display/yaml/yaml.go
+++ b/display/yaml/yaml.go
@@ -20,15 +20,16 @@ package yaml
 import (
 	"fmt"
 
+	d "github.com/apache/skywalking-cli/display/displayable"
+
 	"gopkg.in/yaml.v2"
 )
 
-func Display(object interface{}) error {
-	if bytes, e := yaml.Marshal(object); e == nil {
-		fmt.Printf("%v", string(bytes))
-	} else {
+func Display(displayable *d.Displayable) error {
+	bytes, e := yaml.Marshal(displayable.Data)
+	if e != nil {
 		return e
 	}
-
-	return nil
+	_, e = fmt.Printf("%v", string(bytes))
+	return e
 }
diff --git a/display/yaml/yaml_test.go b/display/yaml/yaml_test.go
index 81a772b..aaada3a 100644
--- a/display/yaml/yaml_test.go
+++ b/display/yaml/yaml_test.go
@@ -20,6 +20,8 @@ package yaml
 import (
 	"testing"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -36,7 +38,7 @@ func TestYamlDisplay(t *testing.T) {
 }
 
 func display(t *testing.T, result []schema.Service) {
-	if err := Display(result); err != nil {
+	if err := Display(&displayable.Displayable{Data: result}); err != nil {
 		t.Error(err)
 	}
 }
diff --git a/dist/LICENSE b/dist/LICENSE
index e5b6732..4dfc4c4 100644
--- a/dist/LICENSE
+++ b/dist/LICENSE
@@ -222,6 +222,8 @@ The text of each license is also included at licenses/LICENSE-[project].txt.
 	sirupsen (logrus) 1.4.2: https://github.com/sirupsen/logrus MIT
 	urfave (cli) 1.22.1: https://github.com/urfave/cli MIT
 	nsf (termbox-go) 0.0.0-20190817171036-93860e161317: https://github.com/nsf/termbox-go MIT
+	gizak (termui) v3: https://github.com/gizak/termui MIT
+	mattn (go-runewidth) v3: https://github.com/mattn/go-runewidth MIT
 
 ========================================================================
 BSD licenses
diff --git a/dist/licenses/LICENSE-go-runewidth b/dist/licenses/LICENSE-go-runewidth
new file mode 100644
index 0000000..91b5cef
--- /dev/null
+++ b/dist/licenses/LICENSE-go-runewidth
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Yasuhiro Matsumoto
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/dist/licenses/LICENSE-termui b/dist/licenses/LICENSE-termui
new file mode 100644
index 0000000..b8beeb7
--- /dev/null
+++ b/dist/licenses/LICENSE-termui
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Zack Guo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/go.mod b/go.mod
index f28ffcc..7586d33 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,11 @@ go 1.13
 
 require (
 	github.com/99designs/gqlgen v0.11.1 // indirect
+	github.com/gizak/termui/v3 v3.1.0
 	github.com/machinebox/graphql v0.2.2
+	github.com/mattn/go-runewidth v0.0.4
 	github.com/mum4k/termdash v0.10.0
-	github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 // indirect
+	github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317
 	github.com/olekukonko/tablewriter v0.0.2
 	github.com/sirupsen/logrus v1.4.2
 	github.com/urfave/cli v1.22.1
diff --git a/go.sum b/go.sum
index cd718b5..932479d 100644
--- a/go.sum
+++ b/go.sum
@@ -12,6 +12,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
+github.com/gizak/termui v3.1.0+incompatible h1:N3CFm+j087lanTxPpHOmQs0uS3s5I9TxoAFy6DqPqv8=
+github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
+github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
 github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
 github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
@@ -34,12 +37,16 @@ github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIG
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
+github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mum4k/termdash v0.10.0 h1:uqM6ePiMf+smecb1tJJeON36o1hREeCfOmLFG0iz4a0=
 github.com/mum4k/termdash v0.10.0/go.mod h1:l3tO+lJi9LZqXRq7cu7h5/8rDIK3AzelSuq2v/KncxI=
+github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
 github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317 h1:hhGN4SFXgXo61Q4Sjj/X9sBjyeSa2kdpaOzCO+8EVQw=
 github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
 github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
diff --git a/graphql/metrics/metrics.go b/graphql/metrics/metrics.go
index 68cf36d..ef71e32 100644
--- a/graphql/metrics/metrics.go
+++ b/graphql/metrics/metrics.go
@@ -85,7 +85,7 @@ func Thermodynamic(ctx *cli.Context, condition schema.MetricCondition, duration
 	request := graphql.NewRequest(`
 		query ($metric: MetricCondition!, $duration: Duration!) {
 			metrics: getThermodynamic(metric: $metric, duration: $duration) {
-				nodes responseTimeStep: axisYStep
+				nodes axisYStep
 			}
 		}
 	`)
diff --git a/lib/heatmap.go b/lib/heatmap.go
new file mode 100644
index 0000000..bf24b86
--- /dev/null
+++ b/lib/heatmap.go
@@ -0,0 +1,114 @@
+// 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 lib
+
+import (
+	"fmt"
+	im "image"
+
+	ui "github.com/gizak/termui/v3"
+	rw "github.com/mattn/go-runewidth"
+)
+
+type HeatMap struct {
+	ui.Block
+	XLabelStyles []ui.Style
+	CellColors   [][]ui.Color
+	NumStyles    [][]ui.Style
+	Formatter    func(float64) string
+	Data         [][]float64
+	XLabels      []string
+	YLabels      []string
+	CellWidth    int
+	CellGap      int
+}
+
+func NewHeatMap() *HeatMap {
+	return &HeatMap{
+		Block:        *ui.NewBlock(),
+		CellColors:   [][]ui.Color{ui.StandardColors, ui.StandardColors},
+		NumStyles:    [][]ui.Style{ui.StandardStyles, ui.StandardStyles},
+		Formatter:    func(n float64) string { return fmt.Sprint(n) },
+		XLabelStyles: ui.StandardStyles,
+		CellGap:      1,
+		CellWidth:    3,
+	}
+}
+
+func (hm *HeatMap) Draw(buffer *ui.Buffer) {
+	hm.Block.Draw(buffer)
+
+	cellX := hm.Inner.Min.X
+
+	for i, column := range hm.Data {
+		cellY := 0
+		for j, datum := range column {
+			buffer.SetString(
+				hm.YLabels[j],
+				ui.StyleClear,
+				im.Pt(hm.Inner.Min.X, (hm.Inner.Max.Y-2)-cellY),
+			)
+			for x := cellX + 5; x < ui.MinInt(cellX+hm.CellWidth, hm.Inner.Max.X)+5; x++ {
+				for y := (hm.Inner.Max.Y - 2) - cellY; y > (hm.Inner.Max.Y-2)-cellY-1; y-- {
+					cell := ui.NewCell(' ', ui.NewStyle(ui.ColorClear, color(hm.CellColors, i, j)))
+					buffer.SetCell(cell, im.Pt(x, y))
+				}
+			}
+
+			if hm.Formatter != nil {
+				hm.drawNumber(buffer, datum, i, j, cellX+5, cellY)
+			}
+
+			cellY++
+		}
+
+		if i < len(hm.XLabels) {
+			hm.drawLabel(buffer, cellX+5, i)
+		}
+
+		cellX += hm.CellWidth + hm.CellGap
+	}
+}
+
+func (hm *HeatMap) drawLabel(buffer *ui.Buffer, cellX, i int) {
+	labelX := cellX + ui.MaxInt(int(float64(hm.CellWidth)/2)-int(float64(rw.StringWidth(hm.XLabels[i]))/2), 0)
+	buffer.SetString(
+		ui.TrimString(hm.XLabels[i], hm.CellWidth),
+		ui.SelectStyle(hm.XLabelStyles, i),
+		im.Pt(labelX, hm.Inner.Max.Y-1),
+	)
+}
+
+func (hm *HeatMap) drawNumber(buffer *ui.Buffer, datum float64, i, j, cellX, cellY int) {
+	x := cellX + int(float64(hm.CellWidth)/2) - 1
+	numberStyle := style(hm.NumStyles, i, j)
+	cellColor := color(hm.CellColors, i, j)
+	buffer.SetString(
+		hm.Formatter(datum),
+		ui.NewStyle(numberStyle.Fg, cellColor, numberStyle.Modifier),
+		im.Pt(x, (hm.Inner.Max.Y-2)-cellY),
+	)
+}
+
+func color(colors [][]ui.Color, i, j int) ui.Color {
+	return colors[i%len(colors)][j%len(colors)]
+}
+
+func style(styles [][]ui.Style, i, j int) ui.Style {
+	return styles[i%len(styles)][j%len(styles)]
+}
diff --git a/display/json/json.go b/util/math.go
similarity index 79%
copy from display/json/json.go
copy to util/math.go
index 25aaccf..ddbe2a8 100644
--- a/display/json/json.go
+++ b/util/math.go
@@ -15,19 +15,11 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package json
+package util
 
-import (
-	"encoding/json"
-	"fmt"
-)
-
-func Display(object interface{}) error {
-	if bytes, e := json.Marshal(object); e == nil {
-		fmt.Printf("%v\n", string(bytes))
-	} else {
-		return e
+func MaxInt(a, b int) int {
+	if a > b {
+		return a
 	}
-
-	return nil
+	return b
 }