diff options
author | Sergey Grebenshchikov <sgreben@gmail.com> | 2018-03-28 17:27:59 +0200 |
---|---|---|
committer | Sergey Grebenshchikov <sgreben@gmail.com> | 2018-03-28 17:46:08 +0200 |
commit | c76ad10b1dc63beeee3cf4fa8f4f4f4b02222289 (patch) | |
tree | e5735d201e3ab6267a75dec79ed5d53b0bcdf635 /pkg | |
parent | 7e3d9839123783e5c367522953e494789ce3618d (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.go | 42 | ||||
-rw-r--r-- | pkg/draw/buffer.go | 108 | ||||
-rw-r--r-- | pkg/draw/canvas.go | 23 | ||||
-rw-r--r-- | pkg/draw/full.go | 11 | ||||
-rw-r--r-- | pkg/draw/line.go (renamed from pkg/jp/primitives/line.go) | 7 | ||||
-rw-r--r-- | pkg/draw/quarter.go | 37 | ||||
-rw-r--r-- | pkg/draw/runes.go | 12 | ||||
-rw-r--r-- | pkg/draw/runes_windows.go | 11 | ||||
-rw-r--r-- | pkg/jp/barchart.go | 124 | ||||
-rw-r--r-- | pkg/jp/linechart.go | 205 | ||||
-rw-r--r-- | pkg/jp/primitives/buffer.go | 9 | ||||
-rw-r--r-- | pkg/jp/primitives/format.go | 8 | ||||
-rw-r--r-- | pkg/jp/primitives/runes.go | 18 | ||||
-rw-r--r-- | pkg/plot/barchart.go | 87 | ||||
-rw-r--r-- | pkg/plot/datatable.go (renamed from pkg/jp/datatable.go) | 2 | ||||
-rw-r--r-- | pkg/plot/format.go | 15 | ||||
-rw-r--r-- | pkg/plot/linechart.go | 107 | ||||
-rw-r--r-- | pkg/plot/scatterchart.go | 63 | ||||
-rw-r--r-- | pkg/terminal/terminal_nosysioctl.go | 2 |
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 } |