add support for multiple series to linechart
This commit is contained in:
		
							parent
							
								
									08a5d3f67b
								
							
						
					
					
						commit
						e74935dded
					
				@ -20,7 +20,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/gizak/termui"
 | 
			
		||||
	"github.com/gizak/termui/extra"
 | 
			
		||||
	"github.com/gizak/termui/_extra"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const statFilePath = "/proc/stat"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										300
									
								
								linechart.go
									
									
									
									
									
								
							
							
						
						
									
										300
									
								
								linechart.go
									
									
									
									
									
								
							@ -7,6 +7,9 @@ package termui
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math"
 | 
			
		||||
	"os"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// only 16 possible combinations, why bother
 | 
			
		||||
@ -35,12 +38,43 @@ var braillePatterns = map[[2]int]rune{
 | 
			
		||||
var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'}
 | 
			
		||||
var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
 | 
			
		||||
 | 
			
		||||
// set this filename to have debug logging written here
 | 
			
		||||
var DebugFilename string
 | 
			
		||||
var debugFile *os.File
 | 
			
		||||
 | 
			
		||||
func debugLog(str string) {
 | 
			
		||||
	if DebugFilename == "" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var err error
 | 
			
		||||
	if debugFile == nil {
 | 
			
		||||
		debugFile, err = os.OpenFile(DebugFilename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stamp := time.Now().Format(time.StampMilli)
 | 
			
		||||
	_, err = fmt.Fprintln(debugFile, stamp, str)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Debug(a ...interface{}) {
 | 
			
		||||
	debugLog(fmt.Sprint(a))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Debugf(format string, a ...interface{}) {
 | 
			
		||||
	debugLog(fmt.Sprintf(format, a...))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LineChart has two modes: braille(default) and dot. Using braille gives 2x capicity as dot mode,
 | 
			
		||||
// because one braille char can represent two data points.
 | 
			
		||||
/*
 | 
			
		||||
  lc := termui.NewLineChart()
 | 
			
		||||
  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.Height = 12
 | 
			
		||||
  lc.AxesColor = termui.ColorWhite
 | 
			
		||||
@ -49,44 +83,52 @@ var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'}
 | 
			
		||||
*/
 | 
			
		||||
type LineChart struct {
 | 
			
		||||
	Block
 | 
			
		||||
	Data          []float64
 | 
			
		||||
	DataLabels    []string // if unset, the data indices will be used
 | 
			
		||||
	Mode          string   // braille | dot
 | 
			
		||||
	DotStyle      rune
 | 
			
		||||
	LineColor     Attribute
 | 
			
		||||
	scale         float64 // data span per cell on y-axis
 | 
			
		||||
	AxesColor     Attribute
 | 
			
		||||
	drawingX      int
 | 
			
		||||
	drawingY      int
 | 
			
		||||
	axisYHeight   int
 | 
			
		||||
	axisXWidth    int
 | 
			
		||||
	axisYLebelGap int
 | 
			
		||||
	axisXLebelGap int
 | 
			
		||||
	topValue      float64
 | 
			
		||||
	bottomValue   float64
 | 
			
		||||
	labelX        [][]rune
 | 
			
		||||
	labelY        [][]rune
 | 
			
		||||
	labelYSpace   int
 | 
			
		||||
	maxY          float64
 | 
			
		||||
	minY          float64
 | 
			
		||||
	Data             map[string][]float64
 | 
			
		||||
	DataLabels       []string // if unset, the data indices will be used
 | 
			
		||||
	Mode             string   // braille | dot
 | 
			
		||||
	DotStyle         rune
 | 
			
		||||
	LineColor        map[string]Attribute
 | 
			
		||||
	defaultLineColor Attribute
 | 
			
		||||
	scale            float64 // data span per cell on y-axis
 | 
			
		||||
	AxesColor        Attribute
 | 
			
		||||
	drawingX         int
 | 
			
		||||
	drawingY         int
 | 
			
		||||
	axisYHeight      int
 | 
			
		||||
	axisXWidth       int
 | 
			
		||||
	axisYLebelGap    int
 | 
			
		||||
	axisXLebelGap    int
 | 
			
		||||
	topValue         float64
 | 
			
		||||
	bottomValue      float64
 | 
			
		||||
	labelX           [][]rune
 | 
			
		||||
	labelY           [][]rune
 | 
			
		||||
	labelYSpace      int
 | 
			
		||||
	maxY             float64
 | 
			
		||||
	minY             float64
 | 
			
		||||
	YPadding         float64
 | 
			
		||||
	YFloor           float64
 | 
			
		||||
	YCeil            float64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewLineChart returns a new LineChart with current theme.
 | 
			
		||||
func NewLineChart() *LineChart {
 | 
			
		||||
	lc := &LineChart{Block: *NewBlock()}
 | 
			
		||||
	lc.AxesColor = ThemeAttr("linechart.axes.fg")
 | 
			
		||||
	lc.LineColor = ThemeAttr("linechart.line.fg")
 | 
			
		||||
	lc.defaultLineColor = ThemeAttr("linechart.line.fg")
 | 
			
		||||
	lc.Mode = "braille"
 | 
			
		||||
	lc.DotStyle = '•'
 | 
			
		||||
	lc.Data = make(map[string][]float64)
 | 
			
		||||
	lc.LineColor = make(map[string]Attribute)
 | 
			
		||||
	lc.axisXLebelGap = 2
 | 
			
		||||
	lc.axisYLebelGap = 1
 | 
			
		||||
	lc.bottomValue = math.Inf(1)
 | 
			
		||||
	lc.topValue = math.Inf(-1)
 | 
			
		||||
	lc.YPadding = 0.2
 | 
			
		||||
	lc.YFloor = math.Inf(-1)
 | 
			
		||||
	lc.YCeil = math.Inf(1)
 | 
			
		||||
	return lc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// one cell contains two data points
 | 
			
		||||
// so the capicity is 2x as dot-mode
 | 
			
		||||
// one cell contains two data points, so capicity is 2x dot mode
 | 
			
		||||
func (lc *LineChart) renderBraille() Buffer {
 | 
			
		||||
	buf := NewBuffer()
 | 
			
		||||
 | 
			
		||||
@ -98,51 +140,101 @@ func (lc *LineChart) renderBraille() Buffer {
 | 
			
		||||
		m = cnt4 % 4
 | 
			
		||||
		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
 | 
			
		||||
	for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ {
 | 
			
		||||
		b0, m0 := getPos(lc.Data[2*i])
 | 
			
		||||
		b1, m1 := getPos(lc.Data[2*i+1])
 | 
			
		||||
 | 
			
		||||
		if b0 == b1 {
 | 
			
		||||
			c := Cell{
 | 
			
		||||
				Ch: braillePatterns[[2]int{m0, m1}],
 | 
			
		||||
				Bg: lc.Bg,
 | 
			
		||||
				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)
 | 
			
		||||
	for _, seriesName := range seriesList {
 | 
			
		||||
		seriesData := lc.Data[seriesName]
 | 
			
		||||
		if len(seriesData) == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		thisLineColor, ok := lc.LineColor[seriesName]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			thisLineColor = lc.defaultLineColor
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lc *LineChart) renderDot() Buffer {
 | 
			
		||||
	buf := NewBuffer()
 | 
			
		||||
	for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ {
 | 
			
		||||
		c := Cell{
 | 
			
		||||
			Ch: lc.DotStyle,
 | 
			
		||||
			Fg: lc.LineColor,
 | 
			
		||||
			Bg: lc.Bg,
 | 
			
		||||
	for seriesName, seriesData := range lc.Data {
 | 
			
		||||
		thisLineColor, ok := lc.LineColor[seriesName]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			thisLineColor = lc.defaultLineColor
 | 
			
		||||
		}
 | 
			
		||||
		minCell := lc.innerArea.Min.X + lc.labelYSpace
 | 
			
		||||
		cellPos := lc.innerArea.Max.X - 1
 | 
			
		||||
		for dataPos := len(seriesData) - 1; dataPos >= 0 && cellPos > minCell; {
 | 
			
		||||
			Debug(seriesName, " ", dataPos, cellPos, seriesData[dataPos])
 | 
			
		||||
			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)
 | 
			
		||||
 | 
			
		||||
			cellPos--
 | 
			
		||||
			dataPos--
 | 
			
		||||
		}
 | 
			
		||||
		x := lc.innerArea.Min.X + lc.labelYSpace + 1 + i
 | 
			
		||||
		y := lc.innerArea.Min.Y + lc.innerArea.Dy() - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5)
 | 
			
		||||
		buf.Set(x, y, c)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return buf
 | 
			
		||||
@ -210,45 +302,56 @@ func (lc *LineChart) calcLabelY() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lc *LineChart) calcLayout() {
 | 
			
		||||
	// set datalabels if it is not provided
 | 
			
		||||
	if lc.DataLabels == nil || len(lc.DataLabels) == 0 {
 | 
			
		||||
		lc.DataLabels = make([]string, len(lc.Data))
 | 
			
		||||
		for i := range lc.Data {
 | 
			
		||||
			lc.DataLabels[i] = fmt.Sprint(i)
 | 
			
		||||
	for _, seriesData := range lc.Data {
 | 
			
		||||
		if seriesData == nil || len(seriesData) == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// lazy increase, to avoid y shaking frequently
 | 
			
		||||
	// update bound Y when drawing is gonna overflow
 | 
			
		||||
	lc.minY = lc.Data[0]
 | 
			
		||||
	lc.maxY = lc.Data[0]
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
		// set datalabels if not provided
 | 
			
		||||
		if lc.DataLabels == nil || len(lc.DataLabels) == 0 {
 | 
			
		||||
			lc.DataLabels = make([]string, len(seriesData))
 | 
			
		||||
			for i := range seriesData {
 | 
			
		||||
				lc.DataLabels[i] = fmt.Sprint(i)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if v < lc.minY {
 | 
			
		||||
			lc.minY = v
 | 
			
		||||
 | 
			
		||||
		// 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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	span := lc.maxY - lc.minY
 | 
			
		||||
		for _, v := range seriesData[:vrange] {
 | 
			
		||||
			if v > lc.maxY {
 | 
			
		||||
				lc.maxY = v
 | 
			
		||||
			}
 | 
			
		||||
			if v < lc.minY {
 | 
			
		||||
				lc.minY = v
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	if lc.minY < lc.bottomValue {
 | 
			
		||||
		lc.bottomValue = lc.minY - 0.2*span
 | 
			
		||||
	}
 | 
			
		||||
		span := lc.maxY - lc.minY
 | 
			
		||||
 | 
			
		||||
	if lc.maxY > lc.topValue {
 | 
			
		||||
		lc.topValue = lc.maxY + 0.2*span
 | 
			
		||||
		// 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
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lc.axisYHeight = lc.innerArea.Dy() - 2
 | 
			
		||||
@ -259,6 +362,8 @@ func (lc *LineChart) calcLayout() {
 | 
			
		||||
 | 
			
		||||
	lc.drawingX = lc.innerArea.Min.X + 1 + lc.labelYSpace
 | 
			
		||||
	lc.drawingY = lc.innerArea.Min.Y
 | 
			
		||||
 | 
			
		||||
	Debugf("calcLayout bottom=%f top=%f min=%f max=%f axisYHeight=%d", lc.bottomValue, lc.topValue, lc.minY, lc.maxY, lc.axisYHeight)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (lc *LineChart) plotAxes() Buffer {
 | 
			
		||||
@ -313,15 +418,24 @@ func (lc *LineChart) plotAxes() Buffer {
 | 
			
		||||
func (lc *LineChart) Buffer() 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 {
 | 
			
		||||
		Debug("lc render no data")
 | 
			
		||||
		return buf
 | 
			
		||||
	}
 | 
			
		||||
	lc.calcLayout()
 | 
			
		||||
	buf.Merge(lc.plotAxes())
 | 
			
		||||
 | 
			
		||||
	if lc.Mode == "dot" {
 | 
			
		||||
		Debug("lc render start dot")
 | 
			
		||||
		buf.Merge(lc.renderDot())
 | 
			
		||||
	} else {
 | 
			
		||||
		Debug("lc render start braille")
 | 
			
		||||
		buf.Merge(lc.renderBraille())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user