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
+ })
+}