Merge pull request #81 from uber-archive/mjr-multi-series
add support for multiple data series in linechart
This commit is contained in:
commit
e4bc917824
@ -19,7 +19,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gizak/termui"
|
"github.com/gizak/termui"
|
||||||
"github.com/gizak/termui/extra"
|
"github.com/gizak/termui/_extra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const statFilePath = "/proc/stat"
|
const statFilePath = "/proc/stat"
|
||||||
|
305
linechart.go
305
linechart.go
@ -7,6 +7,7 @@ package termui
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
// only 16 possible combinations, why bother
|
// only 16 possible combinations, why bother
|
||||||
@ -35,12 +36,13 @@ var braillePatterns = map[[2]int]rune{
|
|||||||
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
|
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
|
||||||
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
||||||
|
|
||||||
// LineChart has two modes: braille(default) and dot. Using braille gives 2x capacity as dot mode,
|
// LineChart has two modes: braille(default) and dot.
|
||||||
// because one braille char can represent two data points.
|
// A single braille character is a 2x4 grid of dots, so Using braille
|
||||||
|
// gives 2x X resolution and 4x Y resolution over dot mode.
|
||||||
/*
|
/*
|
||||||
lc := termui.NewLineChart()
|
lc := termui.NewLineChart()
|
||||||
lc.BorderLabel = "braille-mode Line Chart"
|
lc.Border.Label = "braille-mode Line Chart"
|
||||||
lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
|
lc.Data["name'] = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0]
|
||||||
lc.Width = 50
|
lc.Width = 50
|
||||||
lc.Height = 12
|
lc.Height = 12
|
||||||
lc.AxesColor = termui.ColorWhite
|
lc.AxesColor = termui.ColorWhite
|
||||||
@ -49,45 +51,52 @@ var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
|
|||||||
*/
|
*/
|
||||||
type LineChart struct {
|
type LineChart struct {
|
||||||
Block
|
Block
|
||||||
Data []float64
|
Data map[string][]float64
|
||||||
DataLabels []string // if unset, the data indices will be used
|
DataLabels []string // if unset, the data indices will be used
|
||||||
Mode string // braille | dot
|
Mode string // braille | dot
|
||||||
DotStyle rune
|
DotStyle rune
|
||||||
LineColor Attribute
|
LineColor map[string]Attribute
|
||||||
scale float64 // data span per cell on y-axis
|
defaultLineColor Attribute
|
||||||
AxesColor Attribute
|
scale float64 // data span per cell on y-axis
|
||||||
drawingX int
|
AxesColor Attribute
|
||||||
drawingY int
|
drawingX int
|
||||||
axisYHeight int
|
drawingY int
|
||||||
axisXWidth int
|
axisYHeight int
|
||||||
axisYLabelGap int
|
axisXWidth int
|
||||||
axisXLabelGap int
|
axisYLabelGap int
|
||||||
topValue float64
|
axisXLabelGap int
|
||||||
bottomValue float64
|
topValue float64
|
||||||
labelX [][]rune
|
bottomValue float64
|
||||||
labelY [][]rune
|
labelX [][]rune
|
||||||
labelYSpace int
|
labelY [][]rune
|
||||||
maxY float64
|
labelYSpace int
|
||||||
minY float64
|
maxY float64
|
||||||
autoLabels bool
|
minY float64
|
||||||
|
YPadding float64
|
||||||
|
YFloor float64
|
||||||
|
YCeil float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLineChart returns a new LineChart with current theme.
|
// NewLineChart returns a new LineChart with current theme.
|
||||||
func NewLineChart() *LineChart {
|
func NewLineChart() *LineChart {
|
||||||
lc := &LineChart{Block: *NewBlock()}
|
lc := &LineChart{Block: *NewBlock()}
|
||||||
lc.AxesColor = ThemeAttr("linechart.axes.fg")
|
lc.AxesColor = ThemeAttr("linechart.axes.fg")
|
||||||
lc.LineColor = ThemeAttr("linechart.line.fg")
|
lc.defaultLineColor = ThemeAttr("linechart.line.fg")
|
||||||
lc.Mode = "braille"
|
lc.Mode = "braille"
|
||||||
lc.DotStyle = '•'
|
lc.DotStyle = '•'
|
||||||
|
lc.Data = make(map[string][]float64)
|
||||||
|
lc.LineColor = make(map[string]Attribute)
|
||||||
lc.axisXLabelGap = 2
|
lc.axisXLabelGap = 2
|
||||||
lc.axisYLabelGap = 1
|
lc.axisYLabelGap = 1
|
||||||
lc.bottomValue = math.Inf(1)
|
lc.bottomValue = math.Inf(1)
|
||||||
lc.topValue = math.Inf(-1)
|
lc.topValue = math.Inf(-1)
|
||||||
|
lc.YPadding = 0.2
|
||||||
|
lc.YFloor = math.Inf(-1)
|
||||||
|
lc.YCeil = math.Inf(1)
|
||||||
return lc
|
return lc
|
||||||
}
|
}
|
||||||
|
|
||||||
// one cell contains two data points
|
// one cell contains two data points, so capicity is 2x dot mode
|
||||||
// so the capacity is 2x as dot-mode
|
|
||||||
func (lc *LineChart) renderBraille() Buffer {
|
func (lc *LineChart) renderBraille() Buffer {
|
||||||
buf := NewBuffer()
|
buf := NewBuffer()
|
||||||
|
|
||||||
@ -99,68 +108,100 @@ func (lc *LineChart) renderBraille() Buffer {
|
|||||||
m = cnt4 % 4
|
m = cnt4 % 4
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort the series so that overlapping data will overlap the same way each time
|
||||||
|
seriesList := make([]string, len(lc.Data))
|
||||||
|
i := 0
|
||||||
|
for seriesName := range lc.Data {
|
||||||
|
seriesList[i] = seriesName
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
sort.Strings(seriesList)
|
||||||
|
|
||||||
// plot points
|
// plot points
|
||||||
for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
|
for _, seriesName := range seriesList {
|
||||||
b0, m0 := getPos(lc.Data[2*i])
|
seriesData := lc.Data[seriesName]
|
||||||
b1, m1 := getPos(lc.Data[2*i+1])
|
if len(seriesData) == 0 {
|
||||||
|
continue
|
||||||
if b0 == b1 {
|
}
|
||||||
c := Cell{
|
thisLineColor, ok := lc.LineColor[seriesName]
|
||||||
Ch: braillePatterns[[2]int{m0, m1}],
|
if !ok {
|
||||||
Bg: lc.Bg,
|
thisLineColor = lc.defaultLineColor
|
||||||
Fg: lc.LineColor,
|
|
||||||
}
|
|
||||||
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
|
||||||
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
} else {
|
|
||||||
c0 := Cell{Ch: lSingleBraille[m0],
|
|
||||||
Fg: lc.LineColor,
|
|
||||||
Bg: lc.Bg}
|
|
||||||
x0 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
|
||||||
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
|
||||||
buf.Set(x0, y0, c0)
|
|
||||||
|
|
||||||
c1 := Cell{Ch: rSingleBraille[m1],
|
|
||||||
Fg: lc.LineColor,
|
|
||||||
Bg: lc.Bg}
|
|
||||||
x1 := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
|
||||||
y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
|
|
||||||
buf.Set(x1, y1, c1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
minCell := lc.innerArea.Min.X + lc.labelYSpace
|
||||||
|
cellPos := lc.innerArea.Max.X - 1
|
||||||
|
for dataPos := len(seriesData) - 1; dataPos >= 0 && cellPos > minCell; {
|
||||||
|
b0, m0 := getPos(seriesData[dataPos])
|
||||||
|
var b1, m1 int
|
||||||
|
|
||||||
|
if dataPos > 0 {
|
||||||
|
b1, m1 = getPos(seriesData[dataPos-1])
|
||||||
|
|
||||||
|
if b0 == b1 {
|
||||||
|
c := Cell{
|
||||||
|
Ch: braillePatterns[[2]int{m1, m0}],
|
||||||
|
Bg: lc.Bg,
|
||||||
|
Fg: thisLineColor,
|
||||||
|
}
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
||||||
|
buf.Set(cellPos, y, c)
|
||||||
|
} else {
|
||||||
|
c0 := Cell{
|
||||||
|
Ch: rSingleBraille[m0],
|
||||||
|
Fg: thisLineColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
||||||
|
buf.Set(cellPos, y0, c0)
|
||||||
|
|
||||||
|
c1 := Cell{
|
||||||
|
Ch: lSingleBraille[m1],
|
||||||
|
Fg: thisLineColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
y1 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b1
|
||||||
|
buf.Set(cellPos, y1, c1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c0 := Cell{
|
||||||
|
Ch: rSingleBraille[m0],
|
||||||
|
Fg: thisLineColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
x0 := cellPos
|
||||||
|
y0 := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - b0
|
||||||
|
buf.Set(x0, y0, c0)
|
||||||
|
}
|
||||||
|
dataPos -= 2
|
||||||
|
cellPos--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lc *LineChart) renderDot() Buffer {
|
func (lc *LineChart) renderDot() Buffer {
|
||||||
buf := NewBuffer()
|
buf := NewBuffer()
|
||||||
lasty := -1 // previous y val
|
for seriesName, seriesData := range lc.Data {
|
||||||
for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
|
thisLineColor, ok := lc.LineColor[seriesName]
|
||||||
c := Cell{
|
if !ok {
|
||||||
Ch: lc.DotStyle,
|
thisLineColor = lc.defaultLineColor
|
||||||
Fg: lc.LineColor,
|
|
||||||
Bg: lc.Bg,
|
|
||||||
}
|
}
|
||||||
x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
|
minCell := lc.innerArea.Min.X + lc.labelYSpace
|
||||||
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
|
cellPos := lc.innerArea.Max.X - 1
|
||||||
|
for dataPos := len(seriesData) - 1; dataPos >= 0 && cellPos > minCell; {
|
||||||
|
c := Cell{
|
||||||
|
Ch: lc.DotStyle,
|
||||||
|
Fg: thisLineColor,
|
||||||
|
Bg: lc.Bg,
|
||||||
|
}
|
||||||
|
x := cellPos
|
||||||
|
y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((seriesData[dataPos]-lc.bottomValue)/lc.scale+0.5)
|
||||||
|
buf.Set(x, y, c)
|
||||||
|
|
||||||
if lasty != -1 && lasty != y {
|
cellPos--
|
||||||
u := 1 // direction
|
dataPos--
|
||||||
if lasty > y {
|
|
||||||
u = -1 // put dot below
|
|
||||||
}
|
|
||||||
for fy := lasty + u; fy != y; fy += u { // fy: filling point's y val
|
|
||||||
dx := -1 // lastx := x-1 = x+dx
|
|
||||||
if u*(fy-lasty) >= u*(y-lasty)/2 {
|
|
||||||
dx = 0 // cancel the horizontal backspace when getting close to (x,y)
|
|
||||||
}
|
|
||||||
buf.Set(x+dx, fy, c)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
lasty = y
|
|
||||||
|
|
||||||
buf.Set(x, y, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf
|
return buf
|
||||||
@ -211,7 +252,8 @@ func shortenFloatVal(x float64) string {
|
|||||||
|
|
||||||
func (lc *LineChart) calcLabelY() {
|
func (lc *LineChart) calcLabelY() {
|
||||||
span := lc.topValue - lc.bottomValue
|
span := lc.topValue - lc.bottomValue
|
||||||
lc.scale = span / float64(lc.axisYHeight)
|
// where does -2 come from? Without it, we might draw on the top border or past the block
|
||||||
|
lc.scale = span / float64(lc.axisYHeight-2)
|
||||||
|
|
||||||
n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
|
n := (1 + lc.axisYHeight) / (lc.axisYLabelGap + 1)
|
||||||
lc.labelY = make([][]rune, n)
|
lc.labelY = make([][]rune, n)
|
||||||
@ -228,52 +270,59 @@ func (lc *LineChart) calcLabelY() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (lc *LineChart) calcLayout() {
|
func (lc *LineChart) calcLayout() {
|
||||||
// set datalabels if it is not provided
|
for _, seriesData := range lc.Data {
|
||||||
if (lc.DataLabels == nil || len(lc.DataLabels) == 0) || lc.autoLabels {
|
if seriesData == nil || len(seriesData) == 0 {
|
||||||
lc.autoLabels = true
|
continue
|
||||||
lc.DataLabels = make([]string, len(lc.Data))
|
}
|
||||||
for i := range lc.Data {
|
// set datalabels if not provided
|
||||||
lc.DataLabels[i] = fmt.Sprint(i)
|
if lc.DataLabels == nil || len(lc.DataLabels) == 0 {
|
||||||
|
lc.DataLabels = make([]string, len(seriesData))
|
||||||
|
for i := range seriesData {
|
||||||
|
lc.DataLabels[i] = fmt.Sprint(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy increase, to avoid y shaking frequently
|
||||||
|
lc.minY = seriesData[0]
|
||||||
|
lc.maxY = seriesData[0]
|
||||||
|
|
||||||
|
// valid visible range
|
||||||
|
vrange := lc.innerArea.Dx()
|
||||||
|
if lc.Mode == "braille" {
|
||||||
|
vrange = 2 * lc.innerArea.Dx()
|
||||||
|
}
|
||||||
|
if vrange > len(seriesData) {
|
||||||
|
vrange = len(seriesData)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range seriesData[:vrange] {
|
||||||
|
if v > lc.maxY {
|
||||||
|
lc.maxY = v
|
||||||
|
}
|
||||||
|
if v < lc.minY {
|
||||||
|
lc.minY = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span := lc.maxY - lc.minY
|
||||||
|
|
||||||
|
// allow some padding unless we are beyond the flor/ceil
|
||||||
|
if lc.minY <= lc.bottomValue {
|
||||||
|
lc.bottomValue = lc.minY - lc.YPadding*span
|
||||||
|
if lc.bottomValue < lc.YFloor {
|
||||||
|
lc.bottomValue = lc.YFloor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lc.maxY >= lc.topValue {
|
||||||
|
lc.topValue = lc.maxY + lc.YPadding*span
|
||||||
|
if lc.topValue > lc.YCeil {
|
||||||
|
lc.topValue = lc.YCeil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lazy increase, to avoid y shaking frequently
|
lc.axisYHeight = lc.innerArea.Dy() - 1
|
||||||
// update bound Y when drawing is gonna overflow
|
|
||||||
lc.minY = lc.Data[0]
|
|
||||||
lc.maxY = lc.Data[0]
|
|
||||||
|
|
||||||
lc.bottomValue = lc.minY
|
|
||||||
lc.topValue = lc.maxY
|
|
||||||
|
|
||||||
// valid visible range
|
|
||||||
vrange := lc.innerArea.Dx()
|
|
||||||
if lc.Mode == "braille" {
|
|
||||||
vrange = 2 * lc.innerArea.Dx()
|
|
||||||
}
|
|
||||||
if vrange > len(lc.Data) {
|
|
||||||
vrange = len(lc.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range lc.Data[:vrange] {
|
|
||||||
if v > lc.maxY {
|
|
||||||
lc.maxY = v
|
|
||||||
}
|
|
||||||
if v < lc.minY {
|
|
||||||
lc.minY = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span := lc.maxY - lc.minY
|
|
||||||
|
|
||||||
if lc.minY < lc.bottomValue {
|
|
||||||
lc.bottomValue = lc.minY - 0.2*span
|
|
||||||
}
|
|
||||||
|
|
||||||
if lc.maxY > lc.topValue {
|
|
||||||
lc.topValue = lc.maxY + 0.2*span
|
|
||||||
}
|
|
||||||
|
|
||||||
lc.axisYHeight = lc.innerArea.Dy() - 2
|
|
||||||
lc.calcLabelY()
|
lc.calcLabelY()
|
||||||
|
|
||||||
lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
|
lc.axisXWidth = lc.innerArea.Dx() - 1 - lc.labelYSpace
|
||||||
@ -295,8 +344,8 @@ func (lc *LineChart) plotAxes() Buffer {
|
|||||||
buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
buf.Set(x, origY, Cell{Ch: HDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
}
|
}
|
||||||
|
|
||||||
for dy := 1; dy <= lc.axisYHeight; dy++ {
|
for y := origY - 1; y > origY-lc.axisYHeight; y-- {
|
||||||
buf.Set(origX, origY-dy, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
buf.Set(origX, y, Cell{Ch: VDASH, Fg: lc.AxesColor, Bg: lc.Bg})
|
||||||
}
|
}
|
||||||
|
|
||||||
// x label
|
// x label
|
||||||
@ -335,7 +384,13 @@ func (lc *LineChart) plotAxes() Buffer {
|
|||||||
func (lc *LineChart) Buffer() Buffer {
|
func (lc *LineChart) Buffer() Buffer {
|
||||||
buf := lc.Block.Buffer()
|
buf := lc.Block.Buffer()
|
||||||
|
|
||||||
if lc.Data == nil || len(lc.Data) == 0 {
|
seriesCount := 0
|
||||||
|
for _, data := range lc.Data {
|
||||||
|
if len(data) > 0 {
|
||||||
|
seriesCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seriesCount == 0 {
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
lc.calcLayout()
|
lc.calcLayout()
|
||||||
|
Loading…
Reference in New Issue
Block a user