linechart.go 7.8 KB

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