Fix a bug and make several improvements (#51)

diff --git a/assets/templates/Dashboard.Global.json b/assets/templates/Dashboard.Global.json
index d82c108..318bde8 100644
--- a/assets/templates/Dashboard.Global.json
+++ b/assets/templates/Dashboard.Global.json
@@ -1,6 +1,5 @@
 {
   "buttons": {
-    "texts": "Metrics, Response Latency",
     "colorNumber": 220,
     "height": 1
   },
@@ -56,7 +55,8 @@
         "normal": true
       }
     },
-    "labels": "0, 1, 2, 3, 4",
+    "labels": "P50, P75, P90, P95, P99",
+    "labelsIndex": "0, 1, 2, 3, 4",
     "title": "Global Response Latency",
     "unit": "percentile in ms"
   },
diff --git a/display/graph/dashboard/global.go b/display/graph/dashboard/global.go
index 445b541..c71f9af 100644
--- a/display/graph/dashboard/global.go
+++ b/display/graph/dashboard/global.go
@@ -19,6 +19,7 @@
 
 import (
 	"context"
+	"fmt"
 	"math"
 	"strings"
 
@@ -46,13 +47,23 @@
 type layoutType int
 
 const (
-	// layoutAll displays all the widgets.
-	layoutAll layoutType = iota
+	// layoutMetrics displays all the widgets.
+	layoutMetrics layoutType = iota
 
 	// layoutLineChart focuses onto the line chart.
 	layoutLineChart
+
+	// layoutHeatMap focuses onto the heat map.
+	layoutHeatMap
 )
 
+// strToLayoutType ensures the order of buttons is fixed.
+var strToLayoutType = map[string]layoutType{
+	"Metrics":         layoutMetrics,
+	"ResponseLatency": layoutLineChart,
+	"HeatMap":         layoutHeatMap,
+}
+
 // widgets holds the widgets used by the dashboard.
 type widgets struct {
 	gauges  []*gauge.MetricColumn
@@ -62,6 +73,9 @@
 	buttons []*button.Button
 }
 
+// linearTitles are titles of each line chart, load from the template file.
+var linearTitles []string
+
 // setLayout sets the specified layout.
 func setLayout(c *container.Container, w *widgets, lt layoutType) error {
 	gridOpts, err := gridLayout(w, lt)
@@ -75,17 +89,18 @@
 func newLayoutButtons(c *container.Container, w *widgets, template *dashboard.ButtonTemplate) ([]*button.Button, error) {
 	var buttons []*button.Button
 
-	buttonTexts := strings.Split(template.Texts, ",")
-
 	opts := []button.Option{
-		button.WidthFor(longestString(buttonTexts)),
+		button.WidthFor(longestString(template.Texts)),
 		button.FillColor(cell.ColorNumber(template.ColorNum)),
 		button.Height(template.Height),
 	}
 
-	for i, text := range buttonTexts {
+	for _, text := range template.Texts {
 		// declare a local variable lt to avoid closure.
-		lt := layoutType(i)
+		lt, ok := strToLayoutType[text]
+		if !ok {
+			return nil, fmt.Errorf("the %s is not supposed to be the button's text", text)
+		}
 
 		b, err := button.New(text, func() error {
 			return setLayout(c, w, lt)
@@ -115,13 +130,13 @@
 	}
 
 	switch lt {
-	case layoutAll:
+	case layoutMetrics:
 		rows = append(rows,
 			grid.RowHeightPerc(70, gauge.MetricColumnsElement(w.gauges)...),
 		)
 
 	case layoutLineChart:
-		lcElements := linear.LineChartElements(w.linears)
+		lcElements := linear.LineChartElements(w.linears, linearTitles)
 		percentage := int(math.Min(99, float64((100-buttonRowHeight)/len(lcElements))))
 
 		for _, e := range lcElements {
@@ -191,6 +206,7 @@
 	if err != nil {
 		return err
 	}
+	linearTitles = strings.Split(template.ResponseLatency.Labels, ", ")
 
 	w, err := newWidgets(data, template)
 	if err != nil {
@@ -202,7 +218,7 @@
 	}
 	w.buttons = lb
 
-	gridOpts, err := gridLayout(w, layoutAll)
+	gridOpts, err := gridLayout(w, layoutMetrics)
 	if err != nil {
 		return err
 	}
diff --git a/display/graph/linear/linear.go b/display/graph/linear/linear.go
index a437524..da219f4 100644
--- a/display/graph/linear/linear.go
+++ b/display/graph/linear/linear.go
@@ -66,7 +66,7 @@
 
 // LineChartElements is the part that separated from layout,
 // which can be reused by global dashboard.
-func LineChartElements(lineCharts []*linechart.LineChart) [][]grid.Element {
+func LineChartElements(lineCharts []*linechart.LineChart, titles []string) [][]grid.Element {
 	cols := maxSqrt(len(lineCharts))
 
 	rows := make([][]grid.Element, int(math.Ceil(float64(len(lineCharts))/float64(cols))))
@@ -78,13 +78,21 @@
 			if r == len(rows)-1 {
 				percentage = int(math.Floor(float64(100) / float64(len(lineCharts)-r*cols)))
 			}
+
+			var title string
+			if titles == nil {
+				title = fmt.Sprintf("#%v", r*cols+c)
+			} else {
+				title = titles[r*cols+c]
+			}
+
 			row = append(row, grid.ColWidthPerc(
 				int(math.Min(99, float64(percentage))),
 				grid.Widget(
 					lineCharts[r*cols+c],
 					container.Border(linestyle.Light),
 					container.BorderTitleAlignCenter(),
-					container.BorderTitle(fmt.Sprintf("#%v", r*cols+c)),
+					container.BorderTitle(title),
 				),
 			))
 		}
@@ -130,7 +138,7 @@
 		elements = append(elements, w)
 	}
 
-	gridOpts, err := layout(LineChartElements(elements))
+	gridOpts, err := layout(LineChartElements(elements, nil))
 	if err != nil {
 		return err
 	}
diff --git a/example/Dashboard.Global.json b/example/Dashboard.Global.json
index d82c108..318bde8 100644
--- a/example/Dashboard.Global.json
+++ b/example/Dashboard.Global.json
@@ -1,6 +1,5 @@
 {
   "buttons": {
-    "texts": "Metrics, Response Latency",
     "colorNumber": 220,
     "height": 1
   },
@@ -56,7 +55,8 @@
         "normal": true
       }
     },
-    "labels": "0, 1, 2, 3, 4",
+    "labels": "P50, P75, P90, P95, P99",
+    "labelsIndex": "0, 1, 2, 3, 4",
     "title": "Global Response Latency",
     "unit": "percentile in ms"
   },
diff --git a/graphql/dashboard/global.go b/graphql/dashboard/global.go
index 39b4202..eccc266 100644
--- a/graphql/dashboard/global.go
+++ b/graphql/dashboard/global.go
@@ -35,9 +35,9 @@
 )
 
 type ButtonTemplate struct {
-	Texts    string `json:"texts"`
-	ColorNum int    `json:"colorNumber"`
-	Height   int    `json:"height"`
+	Texts    []string `json:"texts"`
+	ColorNum int      `json:"colorNumber"`
+	Height   int      `json:"height"`
 }
 
 type MetricTemplate struct {
@@ -48,10 +48,11 @@
 }
 
 type ChartTemplate struct {
-	Condition schema.MetricsCondition `json:"condition"`
-	Title     string                  `json:"title"`
-	Unit      string                  `json:"unit"`
-	Labels    string                  `json:"labels"`
+	Condition   schema.MetricsCondition `json:"condition"`
+	Title       string                  `json:"title"`
+	Unit        string                  `json:"unit"`
+	Labels      string                  `json:"labels"`
+	LabelsIndex string                  `json:"labelsIndex"`
 }
 
 type GlobalTemplate struct {
@@ -72,13 +73,23 @@
 
 const DefaultTemplatePath = "templates/Dashboard.Global.json"
 
+// newGlobalTemplate create a new GlobalTemplate and set default values for buttons' template.
+func newGlobalTemplate() GlobalTemplate {
+	return GlobalTemplate{
+		Buttons: ButtonTemplate{
+			ColorNum: 220,
+			Height:   1,
+		},
+	}
+}
+
 // LoadTemplate reads UI template from file.
 func LoadTemplate(filename string) (*GlobalTemplate, error) {
 	if globalTemplate != nil {
 		return globalTemplate, nil
 	}
 
-	var t GlobalTemplate
+	t := newGlobalTemplate()
 	var byteValue []byte
 
 	if filename == DefaultTemplatePath {
@@ -100,10 +111,31 @@
 	if err := json.Unmarshal(byteValue, &t); err != nil {
 		return nil, err
 	}
+	t.Buttons.Texts = getButtonTexts(byteValue)
+
 	globalTemplate = &t
 	return globalTemplate, nil
 }
 
+// getButtonTexts get keys in the template file,
+// which will be set as texts of buttons in the dashboard.
+func getButtonTexts(byteValue []byte) []string {
+	var ret []string
+
+	c := make(map[string]json.RawMessage)
+	err := json.Unmarshal(byteValue, &c)
+	if err != nil {
+		return nil
+	}
+
+	for s := range c {
+		if s != "buttons" {
+			ret = append(ret, strings.Title(s))
+		}
+	}
+	return ret
+}
+
 func Metrics(ctx *cli.Context, duration schema.Duration) [][]*schema.SelectedRecord {
 	var ret [][]*schema.SelectedRecord
 
@@ -112,6 +144,11 @@
 		return nil
 	}
 
+	// Check if there is a template of metrics.
+	if template.Metrics == nil {
+		return nil
+	}
+
 	for _, m := range template.Metrics {
 		var response map[string][]*schema.SelectedRecord
 		request := graphql.NewRequest(assets.Read("graphqls/dashboard/SortMetrics.graphql"))
@@ -133,14 +170,19 @@
 		return nil
 	}
 
-	// labels in the template file is string type,
-	// need to convert to string array for graphql query.
-	labels := strings.Split(template.ResponseLatency.Labels, ",")
+	// Check if there is a template of response latency.
+	if template.ResponseLatency == (ChartTemplate{}) {
+		return nil
+	}
+
+	// LabelsIndex in the template file is string type, like "0, 1, 2",
+	// need use ", " to split into string array for graphql query.
+	labelsIndex := strings.Split(template.ResponseLatency.LabelsIndex, ", ")
 
 	request := graphql.NewRequest(assets.Read("graphqls/dashboard/LabeledMetricsValues.graphql"))
 	request.Var("duration", duration)
 	request.Var("condition", template.ResponseLatency.Condition)
-	request.Var("labels", labels)
+	request.Var("labels", labelsIndex)
 
 	client.ExecuteQueryOrFail(ctx, request, &response)
 
@@ -167,6 +209,11 @@
 		return schema.HeatMap{}
 	}
 
+	// Check if there is a template of heat map.
+	if template.HeatMap == (ChartTemplate{}) {
+		return schema.HeatMap{}
+	}
+
 	request := graphql.NewRequest(assets.Read("graphqls/dashboard/HeatMap.graphql"))
 	request.Var("duration", duration)
 	request.Var("condition", template.HeatMap.Condition)