1// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
2// Use of this source code is governed by a MIT license that can
3// be found in the LICENSE file.
4
5package termui
6
7import (
8	"fmt"
9	"math"
10)
11
12// only 16 possible combinations, why bother
13var braillePatterns = map[[2]int]rune{
14	[2]int{0, 0}: '⣀',
15	[2]int{0, 1}: '⡠',
16	[2]int{0, 2}: '⡐',
17	[2]int{0, 3}: '⡈',
18
19	[2]int{1, 0}: '⢄',
20	[2]int{1, 1}: '⠤',
21	[2]int{1, 2}: '⠔',
22	[2]int{1, 3}: '⠌',
23
24	[2]int{2, 0}: '⢂',
25	[2]int{2, 1}: '⠢',
26	[2]int{2, 2}: '⠒',
27	[2]int{2, 3}: '⠊',
28
29	[2]int{3, 0}: '⢁',
30	[2]int{3, 1}: '⠡',
31	[2]int{3, 2}: '⠑',
32	[2]int{3, 3}: '⠉',
33}
34
35var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
36var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
37
38// LineChart has two modes: braille(default) and dot. Using braille gives 2x capacity as dot mode,
39// because one braille char can represent two data points.
40/*
41  lc := termui.NewLineChart()
42  lc.BorderLabel = "braille-mode Line Chart"
43  lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
44  lc.Width = 50
45  lc.Height = 12
46  lc.AxesColor = termui.ColorWhite
47  lc.LineColor = termui.ColorGreen | termui.AttrBold
48  // termui.Render(lc)...
49*/
50type LineChart struct {
51	Block
52	Data          []float64
53	DataLabels    []string // if unset, the data indices will be used
54	Mode          string   // braille | dot
55	DotStyle      rune
56	LineColor     Attribute
57	scale         float64 // data span per cell on y-axis
58	AxesColor     Attribute
59	drawingX      int
60	drawingY      int
61	axisYHeight   int
62	axisXWidth    int
63	axisYLabelGap int
64	axisXLabelGap int
65	topValue      float64
66	bottomValue   float64
67	labelX        [][]rune
68	labelY        [][]rune
69	labelYSpace   int
70	maxY          float64
71	minY          float64
72	autoLabels    bool
73}
74
75// NewLineChart returns a new LineChart with current theme.
76func NewLineChart() *LineChart {
77	lc := &LineChart{Block: *NewBlock()}
78	lc.AxesColor = ThemeAttr("linechart.axes.fg")
79	lc.LineColor = ThemeAttr("linechart.line.fg")
80	lc.Mode = "braille"
81	lc.DotStyle = '•'
82	lc.axisXLabelGap = 2
83	lc.axisYLabelGap = 1
84	lc.bottomValue = math.Inf(1)
85	lc.topValue = math.Inf(-1)
86	return lc
87}
88
89// one cell contains two data points
90// so the capacity is 2x as dot-mode
91func (lc *LineChart) renderBraille() Buffer {
92	buf := NewBuffer()
93
94	// return: b -> which cell should the point be in
95	//         m -> in the cell, divided into 4 equal height levels, which subcell?
96	getPos := func(d float64) (b, m int) {
97		cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5)
98		b = cnt4 / 4
99		m = cnt4 % 4
100		return
101	}
102	// plot points
103	for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
104		b0, m0 := getPos(lc.Data[2*i])
105		b1, m1 := getPos(lc.Data[2*i+1])
106
107		if b0 == b1 {
108			c := Cell{
109				Ch: braillePatterns[[2]int{m0, m1}],
110				Bg: lc.Bg,
111				Fg: lc.LineColor,
112			}
113			y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
114			x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
115			buf.Set(x, y, c)
116		} else {
117			c0 := Cell{Ch: lSingleBraille[m0],
118				Fg: lc.LineColor,
119				Bg: lc.Bg}
120			x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
121			y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
122			buf.Set(x0, y0, c0)
123
124			c1 := Cell{Ch: rSingleBraille[m1],
125				Fg: lc.LineColor,
126				Bg: lc.Bg}
127			x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
128			y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
129			buf.Set(x1, y1, c1)
130		}
131
132	}
133	return buf
134}
135
136func (lc *LineChart) renderDot() Buffer {
137	buf := NewBuffer()
138	lasty := -1 // previous y val
139	for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
140		c := Cell{
141			Ch: lc.DotStyle,
142			Fg: lc.LineColor,
143			Bg: lc.Bg,
144		}
145		x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
146		y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
147
148		if lasty != -1 && lasty != y {
149			u := 1 // direction
150			if lasty > y {
151				u = -1 // put dot below
152			}
153			for fy := lasty + u; fy != y; fy += u { // fy: filling point's y val
154				dx := -1 // lastx := x-1 = x+dx
155				if u*(fy-lasty) >= u*(y-lasty)/2 {
156					dx = 0 // cancel the horizontal backspace when getting close to (x,y)
157				}
158				buf.Set(x+dx, fy, c)
159			}
160		}
161		lasty = y
162
163		buf.Set(x, y, c)
164	}
165
166	return buf
167}
168
169func (lc *LineChart) calcLabelX() {
170	lc.labelX = [][]rune{}
171
172	for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ {
173		if lc.Mode == "dot" {
174			if l >= len(lc.DataLabels) {
175				break
176			}
177
178			s := str2runes(lc.DataLabels[l])
179			w := strWidth(lc.DataLabels[l])
180			if l+w <= lc.axisXWidth {
181				lc.labelX = append(lc.labelX, s)
182			}
183			l += w + lc.axisXLabelGap
184		} else { // braille
185			if 2*l >= len(lc.DataLabels) {
186				break
187			}
188
189			s := str2runes(lc.DataLabels[2*l])
190			w := strWidth(lc.DataLabels[2*l])
191			if l+w <= lc.axisXWidth {
192				lc.labelX = append(lc.labelX, s)
193			}
194			l += w + lc.axisXLabelGap
195
196		}
197	}
198}
199
200func shortenFloatVal(x float64) string {
201	s := fmt.Sprintf("%.2f", x)
202	//if len(s)-3 > 3 {
203	//s = fmt.Sprintf("%.2e", x)
204	//}
205
206	//if x < 0 {
207	//s = fmt.Sprintf("%.2f", x)
208	//}
209	return s
210}
211
212func (lc *LineChart) calcLabelY() {
213	span := lc.topValue - lc.bottomValue
214	lc.scale = span / float64(lc.axisYHeight)
215
216	n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
217	lc.labelY = make([][]rune, n)
218	maxLen := 0
219	for i := 0; i < n; i++ {
220		s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n)))
221		if len(s) > maxLen {
222			maxLen = len(s)
223		}
224		lc.labelY[i] = s
225	}
226
227	lc.labelYSpace = maxLen
228}
229
230func (lc *LineChart) calcLayout() {
231	// set datalabels if it is not provided
232	if (lc.DataLabels == nil || len(lc.DataLabels) == 0) || lc.autoLabels {
233		lc.autoLabels = true
234		lc.DataLabels = make([]string, len(lc.Data))
235		for i := range lc.Data {
236			lc.DataLabels[i] = fmt.Sprint(i)
237		}
238	}
239
240	// lazy increase, to avoid y shaking frequently
241	// update bound Y when drawing is gonna overflow
242	lc.minY = lc.Data[0]
243	lc.maxY = lc.Data[0]
244
245	lc.bottomValue = lc.minY
246	lc.topValue = lc.maxY
247
248	// valid visible range
249	vrange := lc.innerArea.Dx()
250	if lc.Mode == "braille" {
251		vrange = 2 * lc.innerArea.Dx()
252	}
253	if vrange > len(lc.Data) {
254		vrange = len(lc.Data)
255	}
256
257	for _, v := range lc.Data[:vrange] {
258		if v > lc.maxY {
259			lc.maxY = v
260		}
261		if v < lc.minY {
262			lc.minY = v
263		}
264	}
265
266	span := lc.maxY - lc.minY
267
268	if lc.minY < lc.bottomValue {
269		lc.bottomValue = lc.minY - 0.2*span
270	}
271
272	if lc.maxY > lc.topValue {
273		lc.topValue = lc.maxY + 0.2*span
274	}
275
276	lc.axisYHeight = lc.innerArea.Dy() - 2
277	lc.calcLabelY()
278
279	lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
280	lc.calcLabelX()
281
282	lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
283	lc.drawingY = lc.innerArea.Min.Y
284}
285
286func (lc *LineChart) plotAxes() Buffer {
287	buf := NewBuffer()
288
289	origY := lc.innerArea.Min.Y + lc.innerArea.Dy() - 2
290	origX := lc.innerArea.Min.X + lc.labelYSpace
291
292	buf.Set(origX, origY, Cell{Ch: ORIGIN, Fg: lc.AxesColor, Bg: lc.Bg})
293
294	for x := origX + 1; x < origX+lc.axisXWidth; x++ {
295		buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
296	}
297
298	for dy := 1; dy <= lc.axisYHeight; dy++ {
299		buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
300	}
301
302	// x label
303	oft := 0
304	for _, rs := range lc.labelX {
305		if oft+len(rs) > lc.axisXWidth {
306			break
307		}
308		for j, r := range rs {
309			c := Cell{
310				Ch: r,
311				Fg: lc.AxesColor,
312				Bg: lc.Bg,
313			}
314			x := origX + oft + j
315			y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 1
316			buf.Set(x, y, c)
317		}
318		oft += len(rs) + lc.axisXLabelGap
319	}
320
321	// y labels
322	for i, rs := range lc.labelY {
323		for j, r := range rs {
324			buf.Set(
325				lc.innerArea.Min.X+j,
326				origY-i*(lc.axisYLabelGap+1),
327				Cell{Ch: r, Fg: lc.AxesColor, Bg: lc.Bg})
328		}
329	}
330
331	return buf
332}
333
334// Buffer implements Bufferer interface.
335func (lc *LineChart) Buffer() Buffer {
336	buf := lc.Block.Buffer()
337
338	if lc.Data == nil || len(lc.Data) == 0 {
339		return buf
340	}
341	lc.calcLayout()
342	buf.Merge(lc.plotAxes())
343
344	if lc.Mode == "dot" {
345		buf.Merge(lc.renderDot())
346	} else {
347		buf.Merge(lc.renderBraille())
348	}
349
350	return buf
351}
352