[Feature] Support visualization of heat map and enhance Display logic (#34)

### 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
diff --git a/README.md b/README.md
index 9fba39b..7e748b7 100644
--- a/README.md
+++ b/README.md
@@ -461,13 +461,17 @@
 
 <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,11,4],[2,12,55],[2,13,48],[2,14,14],[2,15,4],[2,16,3],[2,17,2],[2,18,4],[2,19,4],[2,20,187],[3,0,231],[3,1,3],[3,2,55],[3,3,38],[3,4,18],[3,5,9],[3,6,1],[3,7,1],[3,8,1],[3,9,1],[3,10,56],[3,11,6],[3,12,38],[3,13,50],[3,14,16],[3,15,12],[3,16,4],[3,17,4],[3,18,2],[3,19,2],[3,20,183],[4,0,238],[4,1,2],[4,2,47],[4,3,49],[4,4,11],[4,5,7],[4,6,0],[4,7,0],[4,8,2],[4,9,2],[4,10,55],[4,11,3],[4,12,41],[4,13,47],[4,14,12],[4,15,7],[4,16,3],[4,17,2],[4,18,10],[4,19,0],[4,20,190],[5,0,238],[5,1,3],[5,2,42],[5,3,28],[5,4,18],[5,5,4],[5,6,2],[5,7,4],[5,8,4],[5,9,1],[5,10,54],[5,11,2],[5,12,65],[5,13,56],[5,14,17],[5,15,9],[5,16,2],[5,17,3],[5,18,0],[5,19,2],[5,20,179],[6,0,218],[6,1,1],[6,2,34],[6,3,37],[6,4,10],[6,5,5],[6,6,1],[6,7,1],[6,8,0],[6,9,3],[6,10,49],[6,11,7],[6,12,47],[6,13,43],[6,14,19],[6,15,15],[6,16,1],[6,17,4],[6,18,2],[6,19,3],[6,20,183],[7,0,242],[7,1,0],[7,2,41],[7,3,34],[7,4,21],[7,5,4],[7,6,3],[7,7,4],[7,8,1],[7,9,0],[7,10,71],[7,11,4],[7,12,47],[7,13,50],[7,14,19],[7,15,8],[7,16,6],[7,17,3],[7,18,2],[7,19,4],[7,20,174],[8,0,220],[8,1,3],[8,2,40],[8,3,36],[8,4,6],[8,5,8],[8,6,1],[8,7,5],[8,8,0],[8,9,1],[8,10,61],[8,11,2],[8,12,43],[8,13,50],[8,14,17],[8,15,11],[8,16,4],[8,17,5],[8,18,1],[8,19,1],[8,20,183],[9,0,239],[9,1,1],[9,2,48],[9,3,37],[9,4,8],[9,5,12],[9,6,2],[9,7,0],[9,8,0],[9,9,0],[9,10,74],[9,11,1],[9,12,58],[9,13,53],[9,14,17],[9,15,13],[9,16,5],[9,17,2],[9,18,2],[9,19,0],[9,20,178],[10,0,249],[10,1,2],[10,2,40],[10,3,49],[10,4,12],[10,5,8],[10,6,0],[10,7,1],[10,8,0],[10,9,0],[10,10,58],[10,11,1],[10,12,54],[10,13,47],[10,14,21],[10,15,12],[10,16,6],[10,17,4],[10,18,3],[10,19,2],[10,20,165],[11,0,240],[11,1,1],[11,2,50],[11,3,47],[11,4,10],[11,5,2],[11,6,1],[11,7,1],[11,8,2],[11,9,1],[11,10,52],[11,11,4],[11,12,41],[11,13,51],[11,14,17],[11,15,6],[11,16,1],[11,17,6],[11,18,1],[11,19,0],[11,20,199],[12,0,240],[12,1,3],[12,2,40],[12,3,41],[12,4,17],[12,5,10],[12,6,5],[12,7,2],[12,8,2],[12,9,0],[12,10,86],[12,11,1],[12,12,56],[12,13,49],[12,14,16],[12,15,7],[12,16,4],[12,17,8],[12,18,4],[12,19,3],[12,20,157],[13,0,234],[13,1,1],[13,2,53],[13,3,38],[13,4,12],[13,5,4],[13,6,0],[13,7,2],[13,8,0],[13,9,0],[13,10,59],[13,11,2],[13,12,53],[13,13,48],[13,14,18],[13,15,8],[13,16,3],[13,17,8],[13,18,1],[13,19,1],[13,20,187],[14,0,269],[14,1,0],[14,2,66],[14,3,47],[14,4,17],[14,5,4],[14,6,1],[14,7,0],[14,8,0],[14,9,0],[14,10,55],[14,11,1],[14,12,53],[14,13,48],[14,14,18],[14,15,8],[14,16,3],[14,17,3],[14,18,4],[14,19,0],[14,20,179],[15,0,254],[15,1,0],[15,2,57],[15,3,45],[15,4,8],[15,5,9],[15,6,9],[15,7,4],[15,8,3],[15,9,0],[15,10,68],[15,11,1],[15,12,52],[15,13,51],[15,14,19],[15,15,7],[15,16,4],[15,17,0],[15,18,0],[15,19,1],[15,20,177],[16,0,257],[16,1,1],[16,2,65],[16,3,50],[16,4,16],[16,5,3],[16,6,1],[16,7,0],[16,8,0],[16,9,0],[16,10,61],[16,11,3],[16,12,63],[16,13,59],[16,14,14],[16,15,9],[16,16,5],[16,17,2],[16,18,0],[16,19,0],[16,20,174],[17,0,243],[17,1,1],[17,2,63],[17,3,44],[17,4,5],[17,5,3],[17,6,0],[17,7,3],[17,8,0],[17,9,0],[17,10,66],[17,11,4],[17,12,56],[17,13,38],[17,14,11],[17,15,10],[17,16,4],[17,17,2],[17,18,3],[17,19,0],[17,20,181],[18,0,236],[18,1,3],[18,2,38],[18,3,49],[18,4,16],[18,5,5],[18,6,3],[18,7,3],[18,8,1],[18,9,0],[18,10,41],[18,11,4],[18,12,59],[18,13,49],[18,14,13],[18,15,9],[18,16,4],[18,17,1],[18,18,2],[18,19,0],[18,20,192],[19,0,238],[19,1,2],[19,2,49],[19,3,37],[19,4,15],[19,5,2],[19,6,1],[19,7,1],[19,8,3],[19,9,0],[19,10,60],[19,11,3],[19,12,58],[19,13,53],[19,14,17],[19,15,4],[19,16,2],[19,17,2],[19,18,2],[19,19,0],[19,20,185],[20,0,242],[20,1,0],[20,2,55],[20,3,36],[20,4,10],[20,5,6],[20,6,1],[20,7,1],[20,8,1],[20,9,0],[20,10,57],[20,11,4],[20,12,46],[20,13,58],[20,14,15],[20,15,11],[20,16,3],[20,17,2],[20,18,7],[20,19,0],[20,20,188],[21,0,231],[21,1,3],[21,2,50],[21,3,43],[21,4,13],[21,5,1],[21,6,0],[21,7,1],[21,8,0],[21,9,0],[21,10,57],[21,11,3],[21,12,51],[21,13,36],[21,14,15],[21,15,8],[21,16,7],[21,17,2],[21,18,3],[21,19,1],[21,20,188],[22,0,241],[22,1,2],[22,2,60],[22,3,42],[22,4,11],[22,5,8],[22,6,0],[22,7,0],[22,8,0],[22,9,0],[22,10,56],[22,11,4],[22,12,57],[22,13,46],[22,14,20],[22,15,8],[22,16,6],[22,17,1],[22,18,1],[22,19,0],[22,20,191],[23,0,240],[23,1,0],[23,2,46],[23,3,44],[23,4,20],[23,5,3],[23,6,3],[23,7,4],[23,8,1],[23,9,1],[23,10,62],[23,11,4],[23,12,64],[23,13,44],[23,14,15],[23,15,3],[23,16,4],[23,17,2],[23,18,3],[23,19,1],[23,20,181],[24,0,255],[24,1,0],[24,2,61],[24,3,41],[24,4,17],[24,5,7],[24,6,0],[24,7,1],[24,8,0],[24,9,0],[24,10,60],[24,11,3],[24,12,62],[24,13,49],[24,14,17],[24,15,10],[24,16,3],[24,17,2],[24,18,3],[24,19,2],[24,20,177],[25,0,244],[25,1,1],[25,2,56],[25,3,35],[25,4,12],[25,5,12],[25,6,2],[25,7,1],[25,8,0],[25,9,0],[25,10,66],[25,11,3],[25,12,53],[25,13,55],[25,14,20],[25,15,13],[25,16,3],[25,17,1],[25,18,3],[25,19,2],[25,20,173],[26,0,234],[26,1,1],[26,2,45],[26,3,34],[26,4,9],[26,5,6],[26,6,0],[26,7,3],[26,8,0],[26,9,1],[26,10,54],[26,11,6],[26,12,59],[26,13,48],[26,14,20],[26,15,10],[26,16,1],[26,17,2],[26,18,2],[26,19,0],[26,20,182],[27,0,228],[27,1,1],[27,2,46],[27,3,35],[27,4,5],[27,5,7],[27,6,2],[27,7,3],[27,8,2],[27,9,3],[27,10,61],[27,11,2],[27,12,61],[27,13,43],[27,14,15],[27,15,7],[27,16,3],[27,17,1],[27,18,3],[27,19,1],[27,20,187],[28,0,248],[28,1,4],[28,2,60],[28,3,45],[28,4,11],[28,5,9],[28,6,5],[28,7,1],[28,8,1],[28,9,1],[28,10,58],[28,11,2],[28,12,53],[28,13,38],[28,14,20],[28,15,10],[28,16,4],[28,17,6],[28,18,1],[28,19,2],[28,20,178],[29,0,241],[29,1,2],[29,2,46],[29,3,28],[29,4,16],[29,5,8],[29,6,4],[29,7,2],[29,8,1],[29,9,0],[29,10,66],[29,11,3],[29,12,51],[29,13,51],[29,14,28],[29,15,9],[29,16,3],[29,17,4],[29,18,3],[29,19,4],[29,20,153],[30,0,151],[30,1,1],[30,2,26],[30,3,26],[30,4,8],[30,5,4],[30,6,2],[30,7,2],[30,8,3],[30,9,1],[30,10,32],[30,11,3],[30,12,33],[30,13,25],[30,14,10],[30,15,3],[30,16,1],[30,17,3],[30,18,2],[30,19,0],[30,20,82]],"axisYStep":0}
 ```
 
+```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 @@
 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 @@
 
 		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 @@
 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 @@
 			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 @@
 import (
 	"regexp"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metadata"
 
 	"github.com/urfave/cli"
@@ -62,6 +64,6 @@
 				}
 			}
 		}
-		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 @@
 	"strconv"
 	"strings"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/commands/interceptor"
 
 	"github.com/urfave/cli"
@@ -107,6 +109,6 @@
 			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 @@
 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 @@
 			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 @@
 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 @@
 			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 @@
 import (
 	"strings"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/metrics"
 
 	"github.com/urfave/cli"
@@ -75,6 +77,6 @@
 			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 @@
 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 @@
 			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 @@
 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 @@
 			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 @@
 	"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 @@
 )
 
 // 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/displayable/displayable.go b/display/displayable/displayable.go
new file mode 100644
index 0000000..12ca7d2
--- /dev/null
+++ b/display/displayable/displayable.go
@@ -0,0 +1,26 @@
+// 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 displayable
+
+import "github.com/apache/skywalking-cli/graphql/schema"
+
+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 @@
 	"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 @@
 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 @@
 import (
 	"testing"
 
+	d "github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -36,7 +38,7 @@
 }
 
 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 @@
 	"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 @@
 import (
 	"testing"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -36,7 +38,7 @@
 }
 
 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 @@
 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 @@
 import (
 	"testing"
 
+	"github.com/apache/skywalking-cli/display/displayable"
+
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
@@ -36,7 +38,7 @@
 }
 
 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 @@
 	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 @@
 
 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/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/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 @@
 	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/util/math.go b/util/math.go
new file mode 100644
index 0000000..ddbe2a8
--- /dev/null
+++ b/util/math.go
@@ -0,0 +1,25 @@
+// 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 util
+
+func MaxInt(a, b int) int {
+	if a > b {
+		return a
+	}
+	return b
+}