diff --git a/CHANGELOG.md b/CHANGELOG.md index 86785f3..e3026e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ Feel free to search/open an issue if something is missing or confusing from the changelog, since many things have been in flux. +## 2019/01/24 + +Breaking changes: + +- Change LineChart to Plot + - Added ScatterPlot mode which plots points instead of lines between points + ## 2019/01/23 Non breaking changes: diff --git a/README.md b/README.md index c3b1b93..fcd4563 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,10 @@ If you upgrade and notice something is missing or don't like a change, revert th - [BarChart](./_examples/barchart.go) - [Canvas](./_examples/canvas.go) - [Gauge](./_examples/gauge.go) -- [LineChart](./_examples/linechart.go) - [List](./_examples/list.go) - [Paragraph](./_examples/paragraph.go) - [PieChart](./_examples/piechart.go) +- [Plot](./_examples/plot.go) - [Sparkline](./_examples/sparkline.go) - [StackedBarChart](./_examples/stacked_barchart.go) - [Table](./_examples/table.go) diff --git a/_examples/linechart.go b/_examples/linechart.go deleted file mode 100644 index 9c4a2de..0000000 --- a/_examples/linechart.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2017 Zack Guo . All rights reserved. -// Use of this source code is governed by a MIT license that can -// be found in the LICENSE file. - -// +build ignore - -package main - -import ( - "log" - "math" - - ui "github.com/gizak/termui" - "github.com/gizak/termui/widgets" -) - -func main() { - if err := ui.Init(); err != nil { - log.Fatalf("failed to initialize termui: %v", err) - } - defer ui.Close() - - sinData := func() [][]float64 { - n := 220 - data := make([][]float64, 2) - data[0] = make([]float64, n) - data[1] = make([]float64, n) - for i := 0; i < n; i++ { - data[0][i] = 1 + math.Sin(float64(i)/5) - data[1][i] = 1 + math.Cos(float64(i)/5) - } - return data - }() - - lc0 := widgets.NewLineChart() - lc0.Title = "braille-mode Line Chart" - lc0.Data = sinData - lc0.SetRect(0, 0, 50, 15) - lc0.AxesColor = ui.ColorWhite - lc0.LineColors[0] = ui.ColorGreen - - lc1 := widgets.NewLineChart() - lc1.Title = "custom Line Chart" - lc1.LineType = widgets.DotLine - lc1.Data = [][]float64{[]float64{1, 2, 3, 4, 5}} - lc1.SetRect(50, 0, 75, 10) - lc1.DotChar = '+' - lc1.AxesColor = ui.ColorWhite - lc1.LineColors[0] = ui.ColorYellow - lc1.DrawDirection = widgets.DrawLeft - - lc2 := widgets.NewLineChart() - lc2.Title = "dot-mode Line Chart" - lc2.LineType = widgets.DotLine - lc2.Data = make([][]float64, 2) - lc2.Data[0] = sinData[0][4:] - lc2.Data[1] = sinData[1][4:] - lc2.SetRect(0, 15, 50, 30) - lc2.AxesColor = ui.ColorWhite - lc2.LineColors[0] = ui.ColorCyan - - ui.Render(lc0, lc1, lc2) - - uiEvents := ui.PollEvents() - for { - e := <-uiEvents - switch e.ID { - case "q", "": - return - } - } -} diff --git a/_examples/plot.go b/_examples/plot.go new file mode 100644 index 0000000..59cfd9c --- /dev/null +++ b/_examples/plot.go @@ -0,0 +1,84 @@ +// Copyright 2017 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import ( + "log" + "math" + + ui "github.com/gizak/termui" + "github.com/gizak/termui/widgets" +) + +func main() { + if err := ui.Init(); err != nil { + log.Fatalf("failed to initialize termui: %v", err) + } + defer ui.Close() + + sinData := func() [][]float64 { + n := 220 + data := make([][]float64, 2) + data[0] = make([]float64, n) + data[1] = make([]float64, n) + for i := 0; i < n; i++ { + data[0][i] = 1 + math.Sin(float64(i)/5) + data[1][i] = 1 + math.Cos(float64(i)/5) + } + return data + }() + + p0 := widgets.NewPlot() + p0.Title = "braille-mode Line Chart" + p0.Data = sinData + p0.SetRect(0, 0, 50, 15) + p0.AxesColor = ui.ColorWhite + p0.LineColors[0] = ui.ColorGreen + + p1 := widgets.NewPlot() + p1.Title = "custom Line Chart" + p1.Marker = widgets.MarkerDot + p1.Data = [][]float64{[]float64{1, 2, 3, 4, 5}} + p1.SetRect(50, 0, 75, 10) + p1.DotRune = '+' + p1.AxesColor = ui.ColorWhite + p1.LineColors[0] = ui.ColorYellow + p1.DrawDirection = widgets.DrawLeft + + p2 := widgets.NewPlot() + p2.Title = "dot-mode Scatter Plot" + p2.Marker = widgets.MarkerDot + p2.Data = make([][]float64, 2) + p2.Data[0] = []float64{1, 2, 3, 4, 5} + p2.Data[1] = sinData[1][4:] + p2.SetRect(0, 15, 50, 30) + p2.AxesColor = ui.ColorWhite + p2.LineColors[0] = ui.ColorCyan + p2.Type = widgets.ScatterPlot + + p3 := widgets.NewPlot() + p3.Title = "dot-mode Scatter Plot" + p3.Data = make([][]float64, 2) + p3.Data[0] = []float64{1, 2, 3, 4, 5} + p3.Data[1] = sinData[1][4:] + p3.SetRect(45, 15, 80, 30) + p3.AxesColor = ui.ColorWhite + p3.LineColors[0] = ui.ColorCyan + p3.Marker = widgets.MarkerBraille + p3.Type = widgets.ScatterPlot + + ui.Render(p0, p1, p2, p3) + + uiEvents := ui.PollEvents() + for { + e := <-uiEvents + switch e.ID { + case "q", "": + return + } + } +} diff --git a/canvas.go b/canvas.go index 94733f9..7a2e230 100644 --- a/canvas.go +++ b/canvas.go @@ -54,6 +54,14 @@ func (self *Canvas) Line(p0, p1 image.Point, color Color) { } } +func (self *Canvas) Point(p image.Point, color Color) { + point := image.Pt(p.X/2, p.Y/4) + self.CellMap[point] = Cell{ + self.CellMap[point].Rune | BRAILLE[p.X%4][p.Y%2], + NewStyle(color), + } +} + func (self *Canvas) Draw(buf *Buffer) { for point, cell := range self.CellMap { if point.In(self.Rectangle) { diff --git a/theme.go b/theme.go index 7b019c6..ef2ad7a 100644 --- a/theme.go +++ b/theme.go @@ -31,7 +31,7 @@ type RootTheme struct { BarChart BarChartTheme Gauge GaugeTheme - LineChart LineChartTheme + Plot PlotTheme List ListTheme Paragraph ParagraphTheme PieChart PieChartTheme @@ -57,7 +57,7 @@ type GaugeTheme struct { Label Style } -type LineChartTheme struct { +type PlotTheme struct { Lines []Color Axes Color } @@ -136,7 +136,7 @@ var Theme = RootTheme{ Line: ColorWhite, }, - LineChart: LineChartTheme{ + Plot: PlotTheme{ Lines: StandardColors, Axes: ColorWhite, }, diff --git a/widgets/linechart.go b/widgets/plot.go similarity index 51% rename from widgets/linechart.go rename to widgets/plot.go index ad16a93..ff5697e 100644 --- a/widgets/linechart.go +++ b/widgets/plot.go @@ -11,20 +11,25 @@ import ( . "github.com/gizak/termui" ) -// LineChart has two modes: braille(default) and dot. +// Plot has two modes: line(default) and scatter. +// Plot also has two marker types: braille(default) and dot. // A single braille character is a 2x4 grid of dots, so using braille // gives 2x X resolution and 4x Y resolution over dot mode. -type LineChart struct { +type Plot struct { Block - Data [][]float64 - DataLabels []string + + Data [][]float64 + DataLabels []string + MaxVal float64 + + LineColors []Color + AxesColor Color // TODO + ShowAxes bool + + Marker PlotMarker + DotRune rune + Type PlotType HorizontalScale int - LineType LineType - DotChar rune - LineColors []Color - AxesColor Color // TODO - MaxVal float64 - ShowAxes bool DrawDirection DrawDirection // TODO } @@ -35,11 +40,18 @@ const ( yAxisLabelsGap = 1 ) -type LineType int +type PlotType uint const ( - BrailleLine LineType = iota - DotLine + LineChart PlotType = iota + ScatterPlot +) + +type PlotMarker uint + +const ( + MarkerBraille PlotMarker = iota + MarkerDot ) type DrawDirection uint @@ -49,60 +61,93 @@ const ( DrawRight ) -func NewLineChart() *LineChart { - return &LineChart{ +func NewPlot() *Plot { + return &Plot{ Block: *NewBlock(), - LineColors: Theme.LineChart.Lines, - AxesColor: Theme.LineChart.Axes, - LineType: BrailleLine, - DotChar: DOT, + LineColors: Theme.Plot.Lines, + AxesColor: Theme.Plot.Axes, + Marker: MarkerBraille, + DotRune: DOT, Data: [][]float64{}, HorizontalScale: 1, DrawDirection: DrawRight, ShowAxes: true, + Type: LineChart, } } -func (self *LineChart) renderBraille(buf *Buffer, drawArea image.Rectangle, maxVal float64) { +func (self *Plot) renderBraille(buf *Buffer, drawArea image.Rectangle, maxVal float64) { canvas := NewCanvas() canvas.Rectangle = drawArea - for i, line := range self.Data { - previousHeight := int((line[1] / maxVal) * float64(drawArea.Dy()-1)) - for j, val := range line[1:] { - height := int((val / maxVal) * float64(drawArea.Dy()-1)) - canvas.Line( - image.Pt( - (drawArea.Min.X+(j*self.HorizontalScale))*2, - (drawArea.Max.Y-previousHeight-1)*4, - ), - image.Pt( - (drawArea.Min.X+((j+1)*self.HorizontalScale))*2, - (drawArea.Max.Y-height-1)*4, - ), - SelectColor(self.LineColors, i), - ) - previousHeight = height + switch self.Type { + case ScatterPlot: + for i, line := range self.Data { + for j, val := range line { + height := int((val / maxVal) * float64(drawArea.Dy()-1)) + canvas.Point( + image.Pt( + (drawArea.Min.X+(j*self.HorizontalScale))*2, + (drawArea.Max.Y-height-1)*4, + ), + SelectColor(self.LineColors, i), + ) + } + } + case LineChart: + for i, line := range self.Data { + previousHeight := int((line[1] / maxVal) * float64(drawArea.Dy()-1)) + for j, val := range line[1:] { + height := int((val / maxVal) * float64(drawArea.Dy()-1)) + canvas.Line( + image.Pt( + (drawArea.Min.X+(j*self.HorizontalScale))*2, + (drawArea.Max.Y-previousHeight-1)*4, + ), + image.Pt( + (drawArea.Min.X+((j+1)*self.HorizontalScale))*2, + (drawArea.Max.Y-height-1)*4, + ), + SelectColor(self.LineColors, i), + ) + previousHeight = height + } } } canvas.Draw(buf) } -func (self *LineChart) renderDot(buf *Buffer, drawArea image.Rectangle, maxVal float64) { - for i, line := range self.Data { - for j := 0; j < len(line) && j*self.HorizontalScale < drawArea.Dx(); j++ { - val := line[j] - height := int((val / maxVal) * float64(drawArea.Dy()-1)) - buf.SetCell( - NewCell(self.DotChar, NewStyle(SelectColor(self.LineColors, i))), - image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height), - ) +func (self *Plot) renderDot(buf *Buffer, drawArea image.Rectangle, maxVal float64) { + switch self.Type { + case ScatterPlot: + for i, line := range self.Data { + for j, val := range line { + height := int((val / maxVal) * float64(drawArea.Dy()-1)) + point := image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height) + if point.In(drawArea) { + buf.SetCell( + NewCell(self.DotRune, NewStyle(SelectColor(self.LineColors, i))), + point, + ) + } + } + } + case LineChart: + for i, line := range self.Data { + for j := 0; j < len(line) && j*self.HorizontalScale < drawArea.Dx(); j++ { + val := line[j] + height := int((val / maxVal) * float64(drawArea.Dy()-1)) + buf.SetCell( + NewCell(self.DotRune, NewStyle(SelectColor(self.LineColors, i))), + image.Pt(drawArea.Min.X+(j*self.HorizontalScale), drawArea.Max.Y-1-height), + ) + } } } } -func (self *LineChart) plotAxes(buf *Buffer, maxVal float64) { +func (self *Plot) plotAxes(buf *Buffer, maxVal float64) { // draw origin cell buf.SetCell( NewCell(BOTTOM_LEFT, NewStyle(ColorWhite)), @@ -153,7 +198,7 @@ func (self *LineChart) plotAxes(buf *Buffer, maxVal float64) { } } -func (self *LineChart) Draw(buf *Buffer) { +func (self *Plot) Draw(buf *Buffer) { self.Block.Draw(buf) maxVal := self.MaxVal @@ -173,9 +218,10 @@ func (self *LineChart) Draw(buf *Buffer) { ) } - if self.LineType == BrailleLine { + switch self.Marker { + case MarkerBraille: self.renderBraille(buf, drawArea, maxVal) - } else { + case MarkerDot: self.renderDot(buf, drawArea, maxVal) } }