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