summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorSergey Grebenshchikov <sgreben@gmail.com>2018-03-28 17:27:59 +0200
committerSergey Grebenshchikov <sgreben@gmail.com>2018-03-28 17:46:08 +0200
commitc76ad10b1dc63beeee3cf4fa8f4f4f4b02222289 (patch)
treee5735d201e3ab6267a75dec79ed5d53b0bcdf635 /pkg
parent7e3d9839123783e5c367522953e494789ce3618d (diff)
Add braille & quarter-block canvas. Add scatter plot type. Re-write linechart + barchart.
Diffstat (limited to 'pkg')
-rw-r--r--pkg/draw/box.go (renamed from pkg/jp/primitives/box.go)2
-rw-r--r--pkg/draw/braille.go42
-rw-r--r--pkg/draw/buffer.go108
-rw-r--r--pkg/draw/canvas.go23
-rw-r--r--pkg/draw/full.go11
-rw-r--r--pkg/draw/line.go (renamed from pkg/jp/primitives/line.go)7
-rw-r--r--pkg/draw/quarter.go37
-rw-r--r--pkg/draw/runes.go12
-rw-r--r--pkg/draw/runes_windows.go11
-rw-r--r--pkg/jp/barchart.go124
-rw-r--r--pkg/jp/linechart.go205
-rw-r--r--pkg/jp/primitives/buffer.go9
-rw-r--r--pkg/jp/primitives/format.go8
-rw-r--r--pkg/jp/primitives/runes.go18
-rw-r--r--pkg/plot/barchart.go87
-rw-r--r--pkg/plot/datatable.go (renamed from pkg/jp/datatable.go)2
-rw-r--r--pkg/plot/format.go15
-rw-r--r--pkg/plot/linechart.go107
-rw-r--r--pkg/plot/scatterchart.go63
-rw-r--r--pkg/terminal/terminal_nosysioctl.go2
20 files changed, 522 insertions, 371 deletions
diff --git a/pkg/jp/primitives/box.go b/pkg/draw/box.go
index 38474c6..769e2fd 100644
--- a/pkg/jp/primitives/box.go
+++ b/pkg/draw/box.go
@@ -1,4 +1,4 @@
-package primitives
+package draw
type Box struct {
Width int
diff --git a/pkg/draw/braille.go b/pkg/draw/braille.go
new file mode 100644
index 0000000..33b238e
--- /dev/null
+++ b/pkg/draw/braille.go
@@ -0,0 +1,42 @@
+package draw
+
+var _ Pixels = &Braille{}
+
+const (
+ brailleScaleX = 2
+ brailleScaleY = 4
+ brailleEmpty = rune(0x2800)
+)
+
+type Braille struct{ *Buffer }
+
+func (b *Braille) Size() Box {
+ return Box{
+ Width: b.Width * brailleScaleX,
+ Height: b.Height * brailleScaleY,
+ }
+}
+
+func (b *Braille) Clear() { b.Fill(brailleEmpty) }
+
+func (b *Braille) Set(y, x int) {
+ ry := y / brailleScaleY
+ rx := x / brailleScaleX
+ b.SetOr(ry, rx, braillePoint(y, x))
+}
+
+func braillePoint(y, x int) rune {
+ var cy, cx int
+ if y >= 0 {
+ cy = y % 4
+ } else {
+ cy = 3 + ((y + 1) % 4)
+ }
+ if x >= 0 {
+ cx = x % 2
+ } else {
+ cx = 1 + ((x + 1) % 2)
+ }
+ pixelMap := [4][2]rune{{1, 8}, {2, 16}, {4, 32}, {64, 128}}
+ return pixelMap[3-cy][cx]
+}
diff --git a/pkg/draw/buffer.go b/pkg/draw/buffer.go
new file mode 100644
index 0000000..9e18f58
--- /dev/null
+++ b/pkg/draw/buffer.go
@@ -0,0 +1,108 @@
+package draw
+
+import (
+ "io"
+)
+
+type Buffer struct {
+ Runes [][]rune
+ Box
+}
+
+func (b *Buffer) GetBuffer() *Buffer { return b }
+
+func NewBuffer(size Box) *Buffer {
+ r := make([][]rune, size.Height)
+ for i := range r {
+ r[i] = make([]rune, size.Width)
+ }
+ return &Buffer{
+ Runes: r,
+ Box: size,
+ }
+}
+
+func (b *Buffer) Fill(r rune) {
+ for i := range b.Runes {
+ for j := range b.Runes[i] {
+ b.Runes[i][j] = r
+ }
+ }
+}
+
+func (b *Buffer) Get(y, x int) rune {
+ if y < 0 || y >= b.Height {
+ return 0
+ }
+ if x < 0 || x >= b.Width {
+ return 0
+ }
+ return b.Runes[y][x]
+}
+
+func (b *Buffer) Set(y, x int, r rune) {
+ if y < 0 || y >= b.Height {
+ return
+ }
+ if x < 0 || x >= b.Width {
+ return
+ }
+ b.Runes[y][x] = r
+}
+
+func (b *Buffer) SetRow(y, x1, x2 int, r rune) {
+ for i := x1; i < x2; i++ {
+ b.Set(y, i, r)
+ }
+}
+
+func (b *Buffer) SetColumn(y1, y2, x int, r rune) {
+ for i := y1; i < y2; i++ {
+ b.Set(i, x, r)
+ }
+}
+
+func (b *Buffer) SetOr(y, x int, r rune) {
+ if y < 0 || y >= b.Height {
+ return
+ }
+ if x < 0 || x >= b.Width {
+ return
+ }
+ b.Runes[y][x] |= r
+}
+
+func (b *Buffer) Write(y, x int, r []rune) {
+ if y < 0 || y >= b.Height {
+ return
+ }
+ for i := 0; i < len(r); i++ {
+ xi := x + i
+ if xi < 0 || xi >= b.Width {
+ continue
+ }
+ b.Runes[y][xi] = r[i]
+ }
+}
+
+func (b *Buffer) WriteLeft(y, x int, r []rune) {
+ b.Write(y, x-len(r), r)
+}
+
+func (b *Buffer) WriteRight(y, x int, r []rune) {
+ b.Write(y, x, r)
+}
+
+func (b *Buffer) WriteCenter(y, x int, r []rune) {
+ b.Write(y, x-len(r)/2, r)
+}
+
+func (b *Buffer) Render(w io.Writer) {
+ for i := range b.Runes {
+ row := b.Runes[b.Height-i-1]
+ w.Write([]byte(string(row)))
+ if i < b.Height-1 {
+ w.Write([]byte{'\n'})
+ }
+ }
+}
diff --git a/pkg/draw/canvas.go b/pkg/draw/canvas.go
new file mode 100644
index 0000000..0235c66
--- /dev/null
+++ b/pkg/draw/canvas.go
@@ -0,0 +1,23 @@
+package draw
+
+type Pixels interface {
+ GetBuffer() *Buffer
+ Clear()
+ Set(y, x int)
+ Size() Box
+}
+
+type Canvas struct{ Pixels }
+
+func (c *Canvas) RuneSize() Box {
+ pixelBox := c.Size()
+ runeBox := c.GetBuffer().Box
+ return Box{
+ Width: pixelBox.Width / runeBox.Width,
+ Height: pixelBox.Height / runeBox.Height,
+ }
+}
+
+func (c *Canvas) DrawLine(y0, x0, y1, x1 int) {
+ line(y0, x0, y1, x1, c.Set)
+}
diff --git a/pkg/draw/full.go b/pkg/draw/full.go
new file mode 100644
index 0000000..6f234b0
--- /dev/null
+++ b/pkg/draw/full.go
@@ -0,0 +1,11 @@
+package draw
+
+var _ Pixels = &Full{}
+
+type Full struct{ *Buffer }
+
+func (b *Full) Size() Box { return b.Box }
+
+func (b *Full) Set(y, x int) { b.Buffer.Set(y, x, '█') }
+
+func (b *Full) Clear() { b.Fill(' ') }
diff --git a/pkg/jp/primitives/line.go b/pkg/draw/line.go
index 4bcedca..c5a5ee5 100644
--- a/pkg/jp/primitives/line.go
+++ b/pkg/draw/line.go
@@ -1,4 +1,4 @@
-package primitives
+package draw
// Adapted from https://github.com/buger/goterm under the MIT License.
@@ -27,8 +27,7 @@ SOFTWARE.
*/
-// http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
-func DrawLine(x0, y0, x1, y1 int, plot func(int, int)) {
+func line(y0, x0, y1, x1 int, plot func(y, x int)) {
dx := x1 - x0
if dx < 0 {
dx = -dx
@@ -51,7 +50,7 @@ func DrawLine(x0, y0, x1, y1 int, plot func(int, int)) {
err := dx - dy
for {
- plot(x0, y0)
+ plot(y0, x0)
if x0 == x1 && y0 == y1 {
break
}
diff --git a/pkg/draw/quarter.go b/pkg/draw/quarter.go
new file mode 100644
index 0000000..b7748f4
--- /dev/null
+++ b/pkg/draw/quarter.go
@@ -0,0 +1,37 @@
+package draw
+
+var _ Pixels = &Quarter{}
+
+const (
+ quarterScaleX = 2
+ quarterScaleY = 2
+)
+
+type Quarter struct{ *Buffer }
+
+func (b *Quarter) Size() Box {
+ return Box{
+ Width: b.Width * quarterScaleX,
+ Height: b.Height * quarterScaleY,
+ }
+}
+
+func (b *Quarter) Set(y, x int) {
+ ry, cy := y/2, 1-y%2
+ rx, cx := x/2, x%2
+ i := index(b.Get(ry, rx))
+ b.Buffer.Set(ry, rx, runes[i|(1<<uint(cx+2*cy))])
+}
+
+func (b *Quarter) Clear() { b.Fill(runes[0]) }
+
+var runes = []rune(" ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█")
+
+func index(r rune) int {
+ for i := range runes {
+ if runes[i] == r {
+ return i
+ }
+ }
+ return 0
+}
diff --git a/pkg/draw/runes.go b/pkg/draw/runes.go
new file mode 100644
index 0000000..834681e
--- /dev/null
+++ b/pkg/draw/runes.go
@@ -0,0 +1,12 @@
+// +build !windows
+
+package draw
+
+// Box drawing characters
+var (
+ HorizontalLine = '─'
+ VerticalLine = '│'
+ CornerBottomLeft = '└'
+ PointSymbolDefault = '•'
+ Cross = '╳'
+)
diff --git a/pkg/draw/runes_windows.go b/pkg/draw/runes_windows.go
new file mode 100644
index 0000000..6132acb
--- /dev/null
+++ b/pkg/draw/runes_windows.go
@@ -0,0 +1,11 @@
+// +build windows
+
+package draw
+
+var (
+ HorizontalLine = '-'
+ VerticalLine = '|'
+ CornerBottomLeft = '+'
+ PointSymbolDefault = '*'
+ Cross = 'X'
+)
diff --git a/pkg/jp/barchart.go b/pkg/jp/barchart.go
deleted file mode 100644
index c561f98..0000000
--- a/pkg/jp/barchart.go
+++ /dev/null
@@ -1,124 +0,0 @@
-package jp
-
-import (
- "math"
- "strings"
-
- "github.com/sgreben/jp/pkg/jp/primitives"
-)
-
-// BarChart is a bar chart
-type BarChart struct {
- Buffer []string
-
- data *DataTable
-
- Width int
- Height int
- Symbol string
- BarPaddingX int
-
- chartHeight int
- chartWidth int
-
- paddingX int
- paddingY int
-}
-
-// NewBarChart returns a new bar chart
-func NewBarChart(width, height int) *BarChart {
- chart := new(BarChart)
- chart.Width = width
- chart.Height = height
- chart.Buffer = primitives.Buffer(width * height)
- chart.Symbol = primitives.FullBlock4
- chart.paddingY = 3
- chart.BarPaddingX = 1
- return chart
-}
-
-func (c *BarChart) writeText(text string, x, y int) {
- coord := y*c.Width + x
-
- for idx, char := range strings.Split(text, "") {
- if coord+idx >= 0 && coord+idx < len(c.Buffer) {
- c.Buffer[coord+idx] = char
- }
- }
-}
-
-// Draw implements Chart
-func (c *BarChart) Draw(data *DataTable) (out string) {
-
- c.data = data
-
- minY := math.Inf(1)
- maxY := math.Inf(-1)
- for _, row := range data.Rows {
- for _, y := range row {
- if y < minY {
- minY = y
- }
- if y > maxY {
- maxY = y
- }
- }
- }
-
- c.paddingX = 1
- c.chartHeight = c.Height - c.paddingY
- c.chartWidth = c.Width - c.paddingX - 1
- scaleY := float64(c.chartHeight) / maxY
- barPaddedWidth := c.chartWidth / len(data.Columns)
- barWidth := barPaddedWidth - c.BarPaddingX
- if barPaddedWidth < 1 {
- barPaddedWidth = 1
- }
- if barWidth < 1 {
- barWidth = 1
- }
-
- scaleY = float64(c.chartHeight) / maxY
-
- for i, group := range data.Columns {
- barLeft := c.paddingX + barPaddedWidth*i
- barRight := barLeft + barWidth
- y := data.Rows[0][i]
- barHeight := y * scaleY
- barTop := c.paddingY - 1 + int(barHeight)
- for x := barLeft; x < barRight; x++ {
- for y := c.paddingY - 1; y < barTop; y++ {
- c.set(x, y, c.Symbol)
- }
- if barHeight < 1 && y > 0 {
- for x := barLeft; x < barRight; x++ {
- c.set(x, barTop, "▁")
- }
- }
- }
-
- // Group label
- barMiddle := (barLeft + barRight) / 2
- c.writeText(group, barMiddle-(len(group)/2), 0)
-
- // Count label
- countLabelY := barTop
- if barHeight < 1 {
- countLabelY = barTop + 1
- }
- c.writeText(primitives.Ff(y), barMiddle-(len(primitives.Ff(y))/2), countLabelY)
- }
-
- for row := c.Height - 1; row >= 0; row-- {
- out += strings.Join(c.Buffer[row*c.Width:(row+1)*c.Width], "") + "\n"
- }
-
- return
-}
-
-func (c *BarChart) set(x, y int, s string) {
- coord := y*c.Width + x
- if coord > 0 && coord < len(c.Buffer) {
- c.Buffer[coord] = s
- }
-}
diff --git a/pkg/jp/linechart.go b/pkg/jp/linechart.go
deleted file mode 100644
index 1849a1e..0000000
--- a/pkg/jp/linechart.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package jp
-
-import (
- "math"
- "strings"
-
- "github.com/sgreben/jp/pkg/jp/primitives"
-)
-
-// Adapted from https://github.com/buger/goterm under the MIT License.
-
-/*
-MIT License
-
-Copyright (c) 2016 Leonid Bugaev
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-*/
-
-// LineChart is a line chart
-type LineChart struct {
- Buffer []string
- Width int
- Height int
- Symbol string
- chartHeight int
- chartWidth int
- paddingX int
- paddingY int
-
- data *DataTable
-}
-
-// NewLineChart returns a new line chart
-func NewLineChart(width, height int) *LineChart {
- chart := new(LineChart)
- chart.Width = width
- chart.Height = height
- chart.Buffer = primitives.Buffer(width * height)
- chart.Symbol = primitives.PointSymbolDefault
- chart.paddingY = 2
- return chart
-}
-
-func (c *LineChart) drawAxes(maxX, minX, maxY, minY float64, index int) {
- c.drawLine(c.paddingX-1, 1, c.Width-1, 1, primitives.HorizontalLine)
- c.drawLine(c.paddingX-1, 1, c.paddingX-1, c.Height-1, primitives.VerticalLine)
- c.set(c.paddingX-1, c.paddingY-1, primitives.CornerBottomLeft)
-
- left := 0 // c.Width - c.paddingX + 1
- c.writeText(primitives.Ff(minY), left, 1)
-
- c.writeText(primitives.Ff(maxY), left, c.Height-1)
-
- c.writeText(primitives.Ff(minX), c.paddingX, 0)
-
- xCol := c.data.Columns[0]
- c.writeText(c.data.Columns[0], c.Width/2-len(xCol)/2, 1)
-
- if len(c.data.Columns) < 3 {
- col := c.data.Columns[index]
-
- for idx, char := range strings.Split(col, "") {
- startFrom := c.Height/2 + len(col)/2 - idx
-
- c.writeText(char, c.paddingX-1, startFrom)
- }
- }
-
- c.writeText(primitives.Ff(maxX), c.Width-len(primitives.Ff(maxX)), 0)
-
-}
-
-func (c *LineChart) writeText(text string, x, y int) {
- coord := y*c.Width + x
-
- for idx, char := range strings.Split(text, "") {
- c.Buffer[coord+idx] = char
- }
-}
-
-// Draw implements Chart
-func (c *LineChart) Draw(data *DataTable) (out string) {
- var scaleY, scaleX float64
-
- c.data = data
-
- charts := len(data.Columns) - 1
-
- prevPoint := [2]int{-1, -1}
-
- maxX, minX, maxY, minY := getBoundaryValues(data, -1)
-
- c.paddingX = int(math.Max(float64(len(primitives.Ff(minY))), float64(len(primitives.Ff(maxY))))) + 1
-
- c.chartHeight = c.Height - c.paddingY
- c.chartWidth = c.Width - c.paddingX - 1
-
- scaleX = float64(c.chartWidth) / (maxX - minX)
-
- scaleY = float64(c.chartHeight) / (maxY - minY)
-
- for i := 1; i < charts+1; i++ {
- symbol := c.Symbol
-
- chartData := getChartData(data, i)
-
- for _, point := range chartData {
- x := int((point[0]-minX)*scaleX) + c.paddingX
- y := int((point[1])*scaleY) + c.paddingY
- y = int((point[1]-minY)*scaleY) + c.paddingY
-
- if prevPoint[0] == -1 {
- prevPoint[0] = x
- prevPoint[1] = y
- }
-
- if prevPoint[0] <= x {
- c.drawLine(prevPoint[0], prevPoint[1], x, y, symbol)
- }
-
- prevPoint[0] = x
- prevPoint[1] = y
- }
-
- c.drawAxes(maxX, minX, maxY, minY, i)
- }
-
- for row := c.Height - 1; row >= 0; row-- {
- out += strings.Join(c.Buffer[row*c.Width:(row+1)*c.Width], "") + "\n"
- }
-
- return
-}
-
-func (c *LineChart) set(x, y int, s string) {
- coord := y*c.Width + x
- if coord > 0 && coord < len(c.Buffer) {
- c.Buffer[coord] = s
- }
-}
-
-func (c *LineChart) drawLine(x0, y0, x1, y1 int, symbol string) {
- primitives.DrawLine(x0, y0, x1, y1, func(x, y int) { c.set(x, y, symbol) })
-}
-
-func getBoundaryValues(data *DataTable, index int) (maxX, minX, maxY, minY float64) {
- maxX = math.Inf(-1)
- minX = math.Inf(1)
- maxY = math.Inf(-1)
- minY = math.Inf(1)
-
- for _, r := range data.Rows {
- maxX = math.Max(maxX, r[0])
- minX = math.Min(minX, r[0])
-
- for idx, c := range r {
- if idx > 0 {
- if index == -1 || index == idx {
- maxY = math.Max(maxY, c)
- minY = math.Min(minY, c)
- }
- }
- }
- }
-
- if maxY > 0 {
- maxY = maxY * 1.1
- } else {
- maxY = maxY * 0.9
- }
-
- if minY > 0 {
- minY = minY * 0.9
- } else {
- minY = minY * 1.1
- }
-
- return
-}
-
-func getChartData(data *DataTable, index int) (out [][]float64) {
- for _, r := range data.Rows {
- out = append(out, []float64{r[0], r[index]})
- }
-
- return
-}
diff --git a/pkg/jp/primitives/buffer.go b/pkg/jp/primitives/buffer.go
deleted file mode 100644
index 4102322..0000000
--- a/pkg/jp/primitives/buffer.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package primitives
-
-func Buffer(size int) (out []string) {
- out = make([]string, size)
- for i := range out {
- out[i] = " "
- }
- return
-}
diff --git a/pkg/jp/primitives/format.go b/pkg/jp/primitives/format.go
deleted file mode 100644
index 26df0e3..0000000
--- a/pkg/jp/primitives/format.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package primitives
-
-import "fmt"
-
-// Format float
-func Ff(num interface{}) string {
- return fmt.Sprintf("%.1f", num)
-}
diff --git a/pkg/jp/primitives/runes.go b/pkg/jp/primitives/runes.go
deleted file mode 100644
index 03c0845..0000000
--- a/pkg/jp/primitives/runes.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package primitives
-
-// Block elements
-const (
- FullBlock1 = "░"
- FullBlock2 = "▒"
- FullBlock3 = "▓"
- FullBlock4 = "█"
-)
-
-// Box drawing characters
-var (
- HorizontalLine = "─"
- VerticalLine = "│"
- CornerBottomLeft = "└"
- PointSymbolDefault = "•"
- Cross = "╳"
-)
diff --git a/pkg/plot/barchart.go b/pkg/plot/barchart.go
new file mode 100644
index 0000000..cb6c95c
--- /dev/null
+++ b/pkg/plot/barchart.go
@@ -0,0 +1,87 @@
+package plot
+
+import (
+ "bytes"
+ "math"
+
+ "github.com/sgreben/jp/pkg/draw"
+)
+
+// BarChart is a bar chart
+type BarChart struct {
+ draw.Canvas
+ BarPaddingX int
+}
+
+// NewBarChart returns a new bar chart
+func NewBarChart(canvas draw.Canvas) *BarChart {
+ chart := new(BarChart)
+ chart.Canvas = canvas
+ chart.BarPaddingX = 1
+ return chart
+}
+
+// Draw implements Chart
+func (c *BarChart) Draw(data *DataTable) string {
+ minY := math.Inf(1)
+ maxY := math.Inf(-1)
+ for _, row := range data.Rows {
+ for _, y := range row {
+ if y < minY {
+ minY = y
+ }
+ if y > maxY {
+ maxY = y
+ }
+ }
+ }
+ paddingX := 4
+ paddingY := 3
+ chartHeight := c.Size().Height - paddingY*c.RuneSize().Height
+ chartWidth := c.Size().Width - 2*paddingX*c.RuneSize().Width
+ scaleY := float64(chartHeight) / maxY
+ barPaddedWidth := chartWidth / len(data.Columns)
+ barWidth := barPaddedWidth - (c.BarPaddingX * c.RuneSize().Width)
+ if barPaddedWidth < c.RuneSize().Width {
+ barPaddedWidth = c.RuneSize().Width
+ }
+ if barWidth < c.RuneSize().Width {
+ barWidth = c.RuneSize().Width
+ }
+
+ scaleY = float64(chartHeight) / maxY
+
+ for i, group := range data.Columns {
+ barLeft := paddingX*c.RuneSize().Width + barPaddedWidth*i
+ barRight := barLeft + barWidth
+
+ y := data.Rows[0][i]
+ barHeight := y * scaleY
+ barBottom := (paddingY - 1) * c.RuneSize().Height
+ barTop := barBottom + int(barHeight)
+
+ for x := barLeft; x < barRight; x++ {
+ for y := barBottom; y < barTop; y++ {
+ c.Set(y, x)
+ }
+ }
+
+ // Group label
+ barMiddle := (barLeft + barRight) / 2
+ c.GetBuffer().WriteCenter(0, barMiddle/c.RuneSize().Width, []rune(group))
+
+ // Count label
+ countLabelY := int(math.Ceil(float64(barTop)/float64(c.RuneSize().Height))) * c.RuneSize().Height
+
+ if countLabelY <= barBottom && y > 0 {
+ c.GetBuffer().SetRow(barTop/c.RuneSize().Height, barLeft/c.RuneSize().Width, barRight/c.RuneSize().Width, '▁')
+ countLabelY = 3 * c.RuneSize().Height
+ }
+
+ c.GetBuffer().WriteCenter(countLabelY/c.RuneSize().Height, barMiddle/c.RuneSize().Width, Ff(y))
+ }
+
+ b := bytes.NewBuffer(nil)
+ c.GetBuffer().Render(b)
+ return b.String()
+}
diff --git a/pkg/jp/datatable.go b/pkg/plot/datatable.go
index f1a682e..15cbff4 100644
--- a/pkg/jp/datatable.go
+++ b/pkg/plot/datatable.go
@@ -1,4 +1,4 @@
-package jp
+package plot
type DataTable struct {
Columns []string
diff --git a/pkg/plot/format.go b/pkg/plot/format.go
new file mode 100644
index 0000000..f1175dd
--- /dev/null
+++ b/pkg/plot/format.go
@@ -0,0 +1,15 @@
+package plot
+
+import "strconv"
+
+const maxDigits = 7
+
+// Ff formats a float
+func Ff(x float64) []rune {
+ minExact := strconv.FormatFloat(x, 'g', -1, 64)
+ fixed := strconv.FormatFloat(x, 'g', maxDigits, 64)
+ if len(minExact) < len(fixed) {
+ return []rune(minExact)
+ }
+ return []rune(fixed)
+}
diff --git a/pkg/plot/linechart.go b/pkg/plot/linechart.go
new file mode 100644
index 0000000..f3c47f1
--- /dev/null
+++ b/pkg/plot/linechart.go
@@ -0,0 +1,107 @@
+package plot
+
+import (
+ "bytes"
+ "math"
+
+ "github.com/sgreben/jp/pkg/draw"
+)
+
+// LineChart is a line chart
+type LineChart struct{ draw.Canvas }
+
+// NewLineChart returns a new line chart
+func NewLineChart(canvas draw.Canvas) *LineChart { return &LineChart{canvas} }
+
+func (c *LineChart) drawAxes(paddingX, paddingY int, minX, maxX, minY, maxY float64) {
+ buffer := c.GetBuffer()
+ // X axis
+ buffer.SetRow(1, paddingX, buffer.Width, draw.HorizontalLine)
+ // Y axis
+ buffer.SetColumn(1, buffer.Height, paddingX, draw.VerticalLine)
+ // Corner
+ buffer.Set(1, paddingX, draw.CornerBottomLeft)
+ // Labels
+ buffer.WriteRight(1, 1, Ff(minY))
+ buffer.WriteLeft(buffer.Height-1, paddingX, Ff(maxY))
+ buffer.WriteRight(0, paddingX, Ff(minX))
+ buffer.WriteLeft(0, buffer.Width, Ff(maxX))
+}
+
+// Draw implements Chart
+func (c *LineChart) Draw(data *DataTable) string {
+ var scaleY, scaleX float64
+ var prevX, prevY int
+
+ minX, maxX, minY, maxY := minMax(data)
+ minLabelWidth := len(Ff(minY))
+ maxLabelWidth := len(Ff(maxY))
+
+ paddingX := minLabelWidth + 1
+ paddingY := 2
+ if minLabelWidth < maxLabelWidth {
+ paddingX = maxLabelWidth + 1
+ }
+ chartWidth := c.Size().Width - (paddingX+1)*c.RuneSize().Width
+ chartHeight := c.Size().Height - paddingY*c.RuneSize().Height
+ scaleX = float64(chartWidth) / (maxX - minX)
+ scaleY = float64(chartHeight) / (maxY - minY)
+
+ first := true
+ for _, point := range data.Rows {
+ if len(point) < 2 {
+ continue
+ }
+ x := int((point[0]-minX)*scaleX + float64((paddingX+1)*c.RuneSize().Width))
+ y := int((point[1]-minY)*scaleY + float64(paddingY*c.RuneSize().Height))
+
+ if first {
+ first = false
+ prevX = x
+ prevY = y
+ }
+
+ if prevX <= x {
+ c.DrawLine(prevY, prevX, y, x)
+ }
+
+ prevX = x
+ prevY = y
+ }
+ c.drawAxes(paddingX, paddingY, minX, maxX, minY, maxY)
+
+ b := bytes.NewBuffer(nil)
+ c.GetBuffer().Render(b)
+ return b.String()
+}
+
+func roundDownToPercentOfRange(x, d float64) float64 {
+ return math.Floor((x*(100-math.Copysign(5, x)))/d) * d / 100
+}
+
+func roundUpToPercentOfRange(x, d float64) float64 {
+ return math.Ceil((x*105)/d) * d / 100
+}
+
+func minMax(data *DataTable) (minX, maxX, minY, maxY float64) {
+ minX, minY = math.Inf(1), math.Inf(1)
+ maxX, maxY = math.Inf(-1), math.Inf(-1)
+
+ for _, r := range data.Rows {
+ if len(r) < 2 {
+ continue
+ }
+ maxX = math.Max(maxX, r[0])
+ minX = math.Min(minX, r[0])
+ maxY = math.Max(maxY, r[1])
+ minY = math.Min(minY, r[1])
+ }
+
+ xRange := maxX - minX
+ yRange := maxY - minY
+ minX = roundDownToPercentOfRange(minX, xRange)
+ minY = roundDownToPercentOfRange(minY, yRange)
+ maxX = roundUpToPercentOfRange(maxX, xRange)
+ maxY = roundUpToPercentOfRange(maxY, yRange)
+ return
+}
diff --git a/pkg/plot/scatterchart.go b/pkg/plot/scatterchart.go
new file mode 100644
index 0000000..85de1f3
--- /dev/null
+++ b/pkg/plot/scatterchart.go
@@ -0,0 +1,63 @@
+package plot
+
+import (
+ "bytes"
+
+ "github.com/sgreben/jp/pkg/draw"
+)
+
+// ScatterChart is a scatter plot
+type ScatterChart struct{ draw.Canvas }
+
+// NewScatterChart returns a new line chart
+func NewScatterChart(canvas draw.Canvas) *ScatterChart { return &ScatterChart{canvas} }
+
+func (c *ScatterChart) drawAxes(paddingX, paddingY int, minX, maxX, minY, maxY float64) {
+ buffer := c.GetBuffer()
+ // X axis
+ buffer.SetRow(1, paddingX, buffer.Width, draw.HorizontalLine)
+ // Y axis
+ buffer.SetColumn(1, buffer.Height, paddingX, draw.VerticalLine)
+ // Corner
+ buffer.Set(1, paddingX, draw.CornerBottomLeft)
+ // Labels
+ buffer.WriteRight(1, 1, Ff(minY))
+ buffer.WriteLeft(buffer.Height-1, paddingX, Ff(maxY))
+ buffer.WriteRight(0, paddingX, Ff(minX))
+ buffer.WriteLeft(0, buffer.Width, Ff(maxX))
+}
+
+// Draw implements Chart
+func (c *ScatterChart) Draw(data *DataTable) string {
+ var scaleY, scaleX float64
+
+ minX, maxX, minY, maxY := minMax(data)
+ minLabelWidth := len(Ff(minY))
+ maxLabelWidth := len(Ff(maxY))
+
+ paddingX := minLabelWidth + 1
+ paddingY := 2
+ if minLabelWidth < maxLabelWidth {
+ paddingX = maxLabelWidth + 1
+ }
+ chartWidth := c.Size().Width - (paddingX+1)*c.RuneSize().Width
+ chartHeight := c.Size().Height - paddingY*c.RuneSize().Height
+ scaleX = float64(chartWidth) / (maxX - minX)
+ scaleY = float64(chartHeight) / (maxY - minY)
+
+ for _, point := range data.Rows {
+ if len(point) < 2 {
+ continue
+ }
+ x := int((point[0]-minX)*scaleX + float64((paddingX+1)*c.RuneSize().Width))
+ y := int((point[1]-minY)*scaleY + float64(paddingY*c.RuneSize().Height))
+
+ c.Set(y, x)
+
+ }
+ c.drawAxes(paddingX, paddingY, minX, maxX, minY, maxY)
+
+ b := bytes.NewBuffer(nil)
+ c.GetBuffer().Render(b)
+ return b.String()
+}
diff --git a/pkg/terminal/terminal_nosysioctl.go b/pkg/terminal/terminal_nosysioctl.go
index 0190499..2ec899d 100644
--- a/pkg/terminal/terminal_nosysioctl.go
+++ b/pkg/terminal/terminal_nosysioctl.go
@@ -3,5 +3,5 @@
package terminal
func getWinsize() (int, int, error) {
- return 80, 24, nil
+ return 79, 24, nil
}