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/08/13 15:02:07 UTC

[skywalking-cli] branch master updated: Rewrite heatmap widget and add it into the `dashboard global` command (#55)

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 36c5958  Rewrite heatmap widget and add it into the `dashboard global` command (#55)
36c5958 is described below

commit 36c59588ca485a2517b679321ceeee863839372b
Author: Hoshea Jiang <fg...@gmail.com>
AuthorDate: Thu Aug 13 23:01:58 2020 +0800

    Rewrite heatmap widget and add it into the `dashboard global` command (#55)
---
 README.md                         |  16 ++-
 display/graph/dashboard/global.go |  20 +++
 display/graph/gauge/gauge.go      |   2 +-
 go.mod                            |   5 +-
 go.sum                            |  24 ++++
 graphql/utils/adapter.go          |  27 ++++
 lib/heatmap/axes/axes.go          | 118 ++++++++++++++++
 lib/heatmap/axes/label.go         | 125 +++++++++++++++++
 lib/heatmap/heatmap.go            | 280 ++++++++++++++++++++++++++++++++++++++
 lib/heatmap/options.go            |  73 ++++++++++
 10 files changed, 686 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index c83deff..3655fa9 100644
--- a/README.md
+++ b/README.md
@@ -265,6 +265,8 @@ Ascii Graph, like coloring in terminal, so please use `json`  or `yaml` instead.
 
 ### `dashboard`
 
+#### `dashboard global-metrics`
+
 <details>
 
 <summary>dashboard global-metrics [--template=template]</summary>
@@ -273,12 +275,24 @@ Ascii Graph, like coloring in terminal, so please use `json`  or `yaml` instead.
 
 | argument | description | default |
 | :--- | :--- | :--- |
-| `--template` | the template file to customize how to display information | `templates/Dashboard.Global.json` |
+| `--template` | The template file to customize how to display information | `templates/Dashboard.Global.json` |
 
 You can imitate the content of [the default template file](example/Dashboard.Global.json) to customize the dashboard.
 
 </details>
 
+#### `dashboard global`
+
+<details>
+
+<summary>dashboard global [--template=template]</summary>
+
+`dashboard global` displays global metrics, global response latency and global heat map in the form of a dashboard.
+
+| argument | description | default |
+| :--- | :--- | :--- |
+| `--template` | The template file to customize how to display information | `templates/Dashboard.Global.json` |
+
 </details>
 
 ### `checkHealth`
diff --git a/display/graph/dashboard/global.go b/display/graph/dashboard/global.go
index c71f9af..9e9f05e 100644
--- a/display/graph/dashboard/global.go
+++ b/display/graph/dashboard/global.go
@@ -23,6 +23,9 @@ import (
 	"math"
 	"strings"
 
+	"github.com/apache/skywalking-cli/graphql/utils"
+	"github.com/apache/skywalking-cli/lib/heatmap"
+
 	"github.com/mattn/go-runewidth"
 	"github.com/mum4k/termdash"
 	"github.com/mum4k/termdash/container/grid"
@@ -68,6 +71,7 @@ var strToLayoutType = map[string]layoutType{
 type widgets struct {
 	gauges  []*gauge.MetricColumn
 	linears []*linechart.LineChart
+	heatmap *heatmap.HeatMap
 
 	// buttons are used to change the layout.
 	buttons []*button.Button
@@ -144,6 +148,11 @@ func gridLayout(w *widgets, lt layoutType) ([]container.Option, error) {
 				grid.RowHeightPerc(percentage, e...),
 			)
 		}
+
+	case layoutHeatMap:
+		rows = append(rows,
+			grid.RowHeightPerc(99-buttonRowHeight, grid.Widget(w.heatmap)),
+		)
 	}
 
 	builder := grid.New()
@@ -180,9 +189,20 @@ func newWidgets(data *dashboard.GlobalData, template *dashboard.GlobalTemplate)
 		linears = append(linears, l)
 	}
 
+	// Create a heat map.
+	hp, err := heatmap.NewHeatMap()
+	if err != nil {
+		return nil, 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
 }
 
diff --git a/display/graph/gauge/gauge.go b/display/graph/gauge/gauge.go
index 5a0dde9..e143578 100644
--- a/display/graph/gauge/gauge.go
+++ b/display/graph/gauge/gauge.go
@@ -141,7 +141,7 @@ func MetricColumnsElement(columns []*MetricColumn) []grid.Element {
 		// Number of gauge in a column, each gauge represents a service or endpoint
 		// The number should be less than or equal to MaxGaugeNum
 		gaugeNum := int(math.Min(MaxGaugeNum, float64(len(columns[i].gauges))))
-		gaugeHeight := int(math.Floor(float64(100-TitleHeight) / float64(gaugeNum)))
+		gaugeHeight := int(math.Floor(float64(99-TitleHeight) / float64(gaugeNum)))
 
 		for j := 0; j < gaugeNum; j++ {
 			column = append(column, grid.RowHeightPerc(gaugeHeight, grid.Widget(columns[i].gauges[j])))
diff --git a/go.mod b/go.mod
index f1bd056..6eb95eb 100644
--- a/go.mod
+++ b/go.mod
@@ -7,8 +7,9 @@ require (
 	github.com/gizak/termui/v3 v3.1.0
 	github.com/gobuffalo/packr/v2 v2.8.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/matryer/is v1.4.0 // indirect
+	github.com/mattn/go-runewidth v0.0.9
+	github.com/mum4k/termdash v0.12.1
 	github.com/olekukonko/tablewriter v0.0.2
 	github.com/sirupsen/logrus v1.6.0
 	github.com/urfave/cli v1.22.1
diff --git a/go.sum b/go.sum
index 50425e5..0cf4386 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,7 @@ github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2b
 github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
 github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
@@ -25,11 +26,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
 github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
+github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
 github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
@@ -47,6 +51,7 @@ github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONC
 github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@@ -79,10 +84,15 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
 github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
+github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
 github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
 github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -92,6 +102,8 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY
 github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
 github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
 github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
 github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -100,6 +112,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
 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/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
@@ -109,9 +123,13 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz
 github.com/mitchellh/mapstructure v1.1.2/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/mum4k/termdash v0.12.1 h1:g3WAT602WIYII6Szhn2KsGoT4PffJZQoA4aAFxEllKc=
+github.com/mum4k/termdash v0.12.1/go.mod h1:haerPCSO0U8pehROAecmuOHDF+2UXw2KaCTxdWooDFE=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
 github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
+github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be h1:yzmWtPyxEUIKdZg4RcPq64MfS8NA6A5fNOJgYhpR9EQ=
+github.com/nsf/termbox-go v0.0.0-20200204031403-4d2b513ad8be/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
 github.com/olekukonko/tablewriter v0.0.2 h1:sq53g+DWf0J6/ceFUHpQ0nAEb6WgM++fq16MZ91cS6o=
@@ -123,6 +141,7 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
@@ -134,6 +153,7 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.5.2 h1:qLvObTrvO/XRCqmkKxUlOBc48bI3efyDuAZe25QiF0w=
 github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
@@ -161,6 +181,7 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
@@ -202,6 +223,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -212,6 +234,7 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
@@ -241,6 +264,7 @@ google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRn
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
diff --git a/graphql/utils/adapter.go b/graphql/utils/adapter.go
index baec147..cabf7cb 100644
--- a/graphql/utils/adapter.go
+++ b/graphql/utils/adapter.go
@@ -43,3 +43,30 @@ func MetricsToMap(duration schema.Duration, intValues schema.IntValues) map[stri
 
 	return values
 }
+
+// HeatMapToMap converts a HeatMap into a map that uses time as key.
+func HeatMapToMap(hp *schema.HeatMap) map[string][]int64 {
+	ret := make(map[string][]int64)
+	for _, col := range hp.Values {
+		// col.id is a string represents date, like "202007292131",
+		// extracts its time part as key.
+		t := col.ID[8:10] + ":" + col.ID[10:12]
+
+		// Reverse the array.
+		for i, j := 0, len(col.Values)-1; i < j; i, j = i+1, j-1 {
+			col.Values[i], col.Values[j] = col.Values[j], col.Values[i]
+		}
+
+		ret[t] = col.Values
+	}
+	return ret
+}
+
+// BucketsToStrings extracts strings from buckets as a chart's labels.
+func BucketsToStrings(buckets []*schema.Bucket) []string {
+	var ret []string
+	for _, b := range buckets {
+		ret = append(ret, b.Min)
+	}
+	return ret
+}
diff --git a/lib/heatmap/axes/axes.go b/lib/heatmap/axes/axes.go
new file mode 100644
index 0000000..5354f7c
--- /dev/null
+++ b/lib/heatmap/axes/axes.go
@@ -0,0 +1,118 @@
+// 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 axes
+
+import (
+	"image"
+
+	"github.com/mum4k/termdash/private/runewidth"
+)
+
+const AxisWidth = 1
+
+// YDetails contain information about the Y axis
+// that will be drawn onto the canvas.
+type YDetails struct {
+	// Width in character cells of the Y axis and its character labels.
+	Width int
+
+	// Start is the point where the Y axis starts.
+	// The Y coordinate of Start is less than the Y coordinate of End.
+	Start image.Point
+
+	// End is the point where the Y axis ends.
+	End image.Point
+
+	// Labels are the labels for values on the Y axis in an increasing order.
+	Labels []*Label
+}
+
+// RequiredWidth calculates the minimum width required
+// in order to draw the Y axis and its labels.
+func RequiredWidth(max string) int {
+	return runewidth.StringWidth(max) + AxisWidth
+}
+
+// NewYDetails retrieves details about the Y axis required
+// to draw it on a canvas of the provided area.
+func NewYDetails(stringLabels []string) (*YDetails, error) {
+	graphHeight := len(stringLabels)
+
+	// See how the labels would look like on the entire maxWidth.
+	maxLabelWidth := LongestString(stringLabels)
+	labels, err := yLabels(graphHeight, maxLabelWidth, stringLabels)
+	if err != nil {
+		return nil, err
+	}
+
+	width := maxLabelWidth + 1
+
+	return &YDetails{
+		Width:  width,
+		Start:  image.Point{X: width - 1, Y: 0},
+		End:    image.Point{X: width - 1, Y: graphHeight},
+		Labels: labels,
+	}, nil
+}
+
+// LongestString returns the length of the longest string in the string array.
+func LongestString(strings []string) int {
+	var widest int
+	for _, s := range strings {
+		if l := runewidth.StringWidth(s); l > widest {
+			widest = l
+		}
+	}
+	return widest
+}
+
+// XDetails contain information about the X axis
+// that will be drawn onto the canvas.
+type XDetails struct {
+	// Start is the point where the X axis starts.
+	// Both coordinates of Start are less than End.
+	Start image.Point
+	// End is the point where the X axis ends.
+	End image.Point
+
+	// Labels are the labels for values on the X axis in an increasing order.
+	Labels []*Label
+}
+
+// NewXDetails retrieves details about the X axis required to draw it on a canvas
+// of the provided area. The yStart is the point where the Y axis starts.
+// The numPoints is the number of points in the largest series that will be
+// plotted.
+// customLabels are the desired labels for the X axis, these are preferred if
+// provided.
+func NewXDetails(cvsAr image.Rectangle, yEnd image.Point, stringLabels []string, cellWidth int) (*XDetails, error) {
+	// The space between the start of the axis and the end of the canvas.
+	// graphWidth := cvsAr.Dx() - yEnd.X - 1
+	graphWidth := len(stringLabels) * cellWidth
+
+	labels, err := xLabels(yEnd, graphWidth, stringLabels, cellWidth)
+	if err != nil {
+		return nil, err
+	}
+
+	return &XDetails{
+		Start:  image.Point{yEnd.X, yEnd.Y - 1},
+		End:    image.Point{yEnd.X + graphWidth, yEnd.Y - 1},
+		Labels: labels,
+	}, nil
+}
diff --git a/lib/heatmap/axes/label.go b/lib/heatmap/axes/label.go
new file mode 100644
index 0000000..e797dae
--- /dev/null
+++ b/lib/heatmap/axes/label.go
@@ -0,0 +1,125 @@
+// 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 axes
+
+// label.go contains code that calculates the positions of labels on the axes.
+
+import (
+	"fmt"
+	"image"
+
+	"github.com/mum4k/termdash/align"
+	"github.com/mum4k/termdash/private/alignfor"
+)
+
+// Label is one text label on an axis.
+type Label struct {
+	// Label content.
+	Text string
+
+	// Position of the label within the canvas.
+	Pos image.Point
+}
+
+// yLabels returns labels that should be placed next to the Y axis.
+// The labelWidth is the width of the area from the left-most side of the
+// canvas until the Y axis (not including the Y axis). This is the area where
+// the labels will be placed and aligned.
+// Labels are returned with Y coordinates in ascending order.
+// Y coordinates grow down.
+func yLabels(graphHeight, labelWidth int, stringLabels []string) ([]*Label, error) {
+	if min := 2; graphHeight < min {
+		return nil, fmt.Errorf("cannot place labels on a canvas with height %d, minimum is %d", graphHeight, min)
+	}
+	if min := 0; labelWidth < min {
+		return nil, fmt.Errorf("cannot place labels in label area width %d, minimum is %d", labelWidth, min)
+	}
+
+	var labels []*Label
+	for row, l := range stringLabels {
+		label, err := rowLabel(row, l, labelWidth)
+		if err != nil {
+			return nil, err
+		}
+
+		labels = append(labels, label)
+	}
+
+	return labels, nil
+}
+
+// rowLabel returns one label for the specified row.
+// The row is the Y coordinate of the row, Y coordinates grow down.
+func rowLabel(row int, label string, labelWidth int) (*Label, error) {
+	// The area available for the label
+	ar := image.Rect(0, row, labelWidth, row+1)
+
+	pos, err := alignfor.Text(ar, label, align.HorizontalRight, align.VerticalMiddle)
+	if err != nil {
+		return nil, fmt.Errorf("unable to align the label value: %v", err)
+	}
+
+	return &Label{
+		Text: label,
+		Pos:  pos,
+	}, nil
+}
+
+// xLabels returns labels that should be placed under the X axis.
+// Labels are returned with X coordinates in ascending order.
+// X coordinates grow right.
+func xLabels(yEnd image.Point, graphWidth int, stringLabels []string, cellWidth int) ([]*Label, error) {
+	var ret []*Label
+
+	length, index := paddedLabelLength(graphWidth, LongestString(stringLabels), cellWidth)
+
+	for x := yEnd.X + 1; x <= graphWidth && index < len(stringLabels); x += length {
+		ar := image.Rect(x, yEnd.Y, x+length, yEnd.Y+1)
+		pos, err := alignfor.Text(ar, stringLabels[index], align.HorizontalCenter, align.VerticalMiddle)
+		if err != nil {
+			return nil, fmt.Errorf("unable to align the label value: %v", err)
+		}
+
+		l := &Label{
+			Text: stringLabels[index],
+			Pos:  pos,
+		}
+		index += length / cellWidth
+		ret = append(ret, l)
+	}
+
+	return ret, nil
+}
+
+// paddedLabelLength calculates the length of the padded label and
+// the column index corresponding to the label.
+// For example, the longest label's length is 5, like '12:34', and the cell's width is 3.
+// So in order to better display, every three cells will display a label,
+// the label belongs to the middle column of the three columns,
+// and the padded length is 3*3, which is 9.
+func paddedLabelLength(graphWidth, longest, cellWidth int) (l, index int) {
+	l, index = 0, 0
+	for i := longest/cellWidth + 1; i < graphWidth/cellWidth; i++ {
+		if (i*cellWidth-longest)%2 == 0 {
+			l = i * cellWidth
+			index = i / 2
+			break
+		}
+	}
+	return
+}
diff --git a/lib/heatmap/heatmap.go b/lib/heatmap/heatmap.go
new file mode 100644
index 0000000..3826ddb
--- /dev/null
+++ b/lib/heatmap/heatmap.go
@@ -0,0 +1,280 @@
+// 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 (
+	"errors"
+	"fmt"
+	"image"
+	"math"
+	"sort"
+	"sync"
+
+	"github.com/apache/skywalking-cli/lib/heatmap/axes"
+
+	"github.com/mum4k/termdash/cell"
+	"github.com/mum4k/termdash/private/area"
+	"github.com/mum4k/termdash/private/canvas"
+	"github.com/mum4k/termdash/private/draw"
+	"github.com/mum4k/termdash/terminal/terminalapi"
+	"github.com/mum4k/termdash/widgetapi"
+)
+
+// columnValues represent values stored in a column.
+type columnValues struct {
+	// values are the values in a column.
+	values []int64
+	// Min is the smallest value in the column, zero if values is empty.
+	Min int64
+	// Max is the largest value in the column, zero if values is empty.
+	Max int64
+}
+
+// newColumnValues returns a new columnValues instance.
+func newColumnValues(values []int64) *columnValues {
+	// Copy to avoid external modifications.
+	v := make([]int64, len(values))
+	copy(v, values)
+
+	min, max := minMax(values)
+
+	return &columnValues{
+		values: v,
+		Min:    min,
+		Max:    max,
+	}
+}
+
+// HeatMap draws heatmap charts.
+// Implements widgetapi.Widget. This object is thread-safe.
+type HeatMap struct {
+	columns map[string]*columnValues
+
+	// XLabels are the labels on the X axis in an increasing order.
+	XLabels []string
+	// YLabels are the labels on the Y axis in an increasing order.
+	YLabels []string
+
+	// MinValue and MaxValue are the Min and Max values in the columns.
+	MinValue, MaxValue int64
+
+	// opts are the provided options.
+	opts *options
+
+	// mu protects the HeatMap widget.
+	mu sync.RWMutex
+}
+
+// NewHeatMap returns a new HeatMap widget.
+func NewHeatMap(opts ...Option) (*HeatMap, error) {
+	opt := newOptions(opts...)
+	if err := opt.validate(); err != nil {
+		return nil, err
+	}
+	return &HeatMap{
+		columns: map[string]*columnValues{},
+		opts:    opt,
+	}, nil
+}
+
+// SetColumns sets the HeatMap's values, min and max values.
+func (hp *HeatMap) SetColumns(values map[string][]int64) {
+	hp.mu.Lock()
+	defer hp.mu.Unlock()
+
+	var minMaxValues []int64
+
+	// The iteration order of map is uncertain, so the keys must be sorted explicitly.
+	var names []string
+	for name := range values {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+
+	for _, name := range names {
+		cv := newColumnValues(values[name])
+		hp.columns[name] = cv
+		hp.XLabels = append(hp.XLabels, name)
+
+		minMaxValues = append(minMaxValues, cv.Min)
+		minMaxValues = append(minMaxValues, cv.Max)
+	}
+
+	hp.MinValue, hp.MaxValue = minMax(minMaxValues)
+}
+
+// SetYLabels sets HeatMap's Y-Labels.
+func (hp *HeatMap) SetYLabels(labels []string) {
+	hp.mu.Lock()
+	defer hp.mu.Unlock()
+
+	hp.YLabels = append(hp.YLabels, labels...)
+
+	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]
+	}
+}
+
+// axesDetails determines the details about the X and Y axes.
+func (hp *HeatMap) axesDetails(cvs *canvas.Canvas) (*axes.XDetails, *axes.YDetails, error) {
+	yd, err := axes.NewYDetails(hp.YLabels)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	xd, err := axes.NewXDetails(cvs.Area(), yd.End, hp.XLabels, hp.opts.cellWidth)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return xd, yd, nil
+}
+
+// Draw draws the values as HeatMap.
+// Implements widgetapi.Widget.Draw.
+func (hp *HeatMap) Draw(cvs *canvas.Canvas, meta *widgetapi.Meta) error {
+	hp.mu.Lock()
+	defer hp.mu.Unlock()
+
+	// Check if the canvas has enough area to draw HeatMap.
+	needAr, err := area.FromSize(hp.minSize())
+	if err != nil {
+		return err
+	}
+	if !needAr.In(cvs.Area()) {
+		return draw.ResizeNeeded(cvs)
+	}
+
+	xd, yd, err := hp.axesDetails(cvs)
+	if err != nil {
+		return err
+	}
+
+	err = hp.drawColumns(cvs, xd, yd)
+	if err != nil {
+		return err
+	}
+
+	return hp.drawAxes(cvs, xd, yd)
+}
+
+// drawColumns draws the graph representing the stored series.
+// Returns XDetails that might be adjusted to not start at zero value if some
+// of the series didn't fit the graphs and XAxisUnscaled was provided.
+// If the series has NaN values they will be ignored and not draw on the graph.
+func (hp *HeatMap) drawColumns(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) error {
+	for i, xl := range hp.XLabels {
+		cv := hp.columns[xl]
+
+		for j := 0; j < len(cv.values); j++ {
+			v := cv.values[j]
+
+			startX := xd.Start.X + 1 + i*hp.opts.cellWidth
+			startY := yd.Labels[j].Pos.Y
+
+			endX := startX + hp.opts.cellWidth
+			endY := startY + 1
+
+			rect := image.Rect(startX, startY, endX, endY)
+			color := hp.getBlockColor(v)
+
+			if err := cvs.SetAreaCells(rect, ' ', cell.BgColor(color)); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// drawAxes draws the X,Y axes and their labels.
+func (hp *HeatMap) drawAxes(cvs *canvas.Canvas, xd *axes.XDetails, yd *axes.YDetails) error {
+	for _, l := range yd.Labels {
+		if err := draw.Text(cvs, l.Text, l.Pos,
+			draw.TextMaxX(yd.Start.X),
+			draw.TextOverrunMode(draw.OverrunModeThreeDot),
+			draw.TextCellOpts(hp.opts.yLabelCellOpts...),
+		); err != nil {
+			return fmt.Errorf("failed to draw the Y labels: %v", err)
+		}
+	}
+
+	for _, l := range xd.Labels {
+		if err := draw.Text(cvs, l.Text, l.Pos, draw.TextCellOpts(hp.opts.xLabelCellOpts...)); err != nil {
+			return fmt.Errorf("failed to draw the X horizontal labels: %v", err)
+		}
+	}
+	return nil
+}
+
+// minSize determines the minimum required size to draw HeatMap.
+func (hp *HeatMap) minSize() image.Point {
+	// At the very least we need:
+	// - n cells width for the Y axis and its labels.
+	// - m cells width for the graph.
+	reqWidth := axes.LongestString(hp.YLabels) + axes.AxisWidth + hp.opts.cellWidth*len(hp.columns)
+
+	// For the height:
+	// - 1 cells height for labels on the X axis.
+	// - n cell height for the graph.
+	reqHeight := 1 + len(hp.YLabels)
+
+	return image.Point{X: reqWidth, Y: reqHeight}
+}
+
+// Keyboard input isn't supported on the SparkLine widget.
+func (*HeatMap) Keyboard(k *terminalapi.Keyboard) error {
+	return errors.New("the HeatMap widget doesn't support keyboard events")
+}
+
+// Mouse input isn't supported on the SparkLine widget.
+func (*HeatMap) Mouse(m *terminalapi.Mouse) error {
+	return errors.New("the HeatMap widget doesn't support mouse events")
+}
+
+// Options implements widgetapi.Widget.Options.
+func (hp *HeatMap) Options() widgetapi.Options {
+	hp.mu.Lock()
+	defer hp.mu.Unlock()
+	return widgetapi.Options{}
+}
+
+// getBlockColor returns the color of the block according to the value.
+// The larger the value, the darker the color.
+func (hp *HeatMap) getBlockColor(value int64) cell.Color {
+	const colorNum = 23
+	scale := float64(hp.MaxValue - hp.MinValue)
+	fv := float64(value)
+
+	// Refer to https://jonasjacek.github.io/colors/.
+	// The color range is in Xterm color [232, 255].
+	rgb := int(255 - (fv / scale * colorNum))
+	return cell.ColorNumber(rgb)
+}
+
+// minMax returns the min and max values in given integer array.
+func minMax(values []int64) (min, max int64) {
+	min = math.MaxInt64
+	max = math.MinInt64
+
+	for _, v := range values {
+		min = int64(math.Min(float64(min), float64(v)))
+		max = int64(math.Max(float64(max), float64(v)))
+	}
+	return
+}
diff --git a/lib/heatmap/options.go b/lib/heatmap/options.go
new file mode 100644
index 0000000..81bbd6e
--- /dev/null
+++ b/lib/heatmap/options.go
@@ -0,0 +1,73 @@
+// 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 (
+	"github.com/mum4k/termdash/cell"
+)
+
+// Option is used to provide options.
+type Option interface {
+	// set sets the provided option.
+	set(*options)
+}
+
+// options stores the provided options.
+type options struct {
+	cellWidth      int
+	xLabelCellOpts []cell.Option
+	yLabelCellOpts []cell.Option
+}
+
+// validate validates the provided options.
+func (o *options) validate() error {
+	return nil
+}
+
+// newOptions returns a new options instance.
+func newOptions(opts ...Option) *options {
+	opt := &options{
+		cellWidth: 3,
+	}
+	for _, o := range opts {
+		o.set(opt)
+	}
+	return opt
+}
+
+// option implements Option.
+type option func(*options)
+
+// set implements Option.set.
+func (o option) set(opts *options) {
+	o(opts)
+}
+
+// XLabelCellOpts set the cell options for the labels on the X axis.
+func XLabelCellOpts(co ...cell.Option) Option {
+	return option(func(opts *options) {
+		opts.xLabelCellOpts = co
+	})
+}
+
+// YLabelCellOpts set the cell options for the labels on the Y axis.
+func YLabelCellOpts(co ...cell.Option) Option {
+	return option(func(opts *options) {
+		opts.yLabelCellOpts = co
+	})
+}