summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSergey Grebenshchikov <sgreben@gmail.com>2018-03-29 00:53:05 +0200
committerSergey Grebenshchikov <sgreben@gmail.com>2018-03-30 01:00:44 +0200
commitef658b5e65bfef1c1ee62125ec6f0c33a27eb7e7 (patch)
tree88b265cb06e76b54c6f467b65e944457bd7c308d
parent3791de72267fc1b561e7f22595c9e90353f2f351 (diff)
Add histogram plot type, display bar chart labels on the right instead of overlapping.
-rw-r--r--Makefile2
-rw-r--r--README.md12
-rw-r--r--cmd/jp/bar.go33
-rw-r--r--cmd/jp/hist.go50
-rw-r--r--cmd/jp/line.go37
-rw-r--r--cmd/jp/main.go13
-rw-r--r--cmd/jp/scatter.go34
-rw-r--r--cmd/jp/split.go6
-rw-r--r--pkg/data/histogram.go108
-rw-r--r--pkg/data/table.go14
-rw-r--r--pkg/plot/barchart.go49
-rw-r--r--pkg/plot/datatable.go14
-rw-r--r--pkg/plot/format.go5
-rw-r--r--pkg/plot/linechart.go11
-rw-r--r--pkg/plot/scatterchart.go7
15 files changed, 296 insertions, 99 deletions
diff --git a/Makefile b/Makefile
index 27c9814..6df4d2c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION = 1.1.2
+VERSION = 1.1.3
APP := jp
PACKAGES := $(shell go list -f {{.Dir}} ./...)
diff --git a/README.md b/README.md
index 9cc8738..1db486d 100644
--- a/README.md
+++ b/README.md
@@ -34,16 +34,16 @@ Or [download the binary](https://github.com/sgreben/jp/releases/latest) from the
```bash
# Linux
-curl -LO https://github.com/sgreben/jp/releases/download/1.1.2/jp_1.1.2_linux_x86_64.zip
-unzip jp_1.1.2_linux_x86_64.zip
+curl -LO https://github.com/sgreben/jp/releases/download/1.1.3/jp_1.1.3_linux_x86_64.zip
+unzip jp_1.1.3_linux_x86_64.zip
# OS X
-curl -LO https://github.com/sgreben/jp/releases/download/1.1.2/jp_1.1.2_osx_x86_64.zip
-unzip jp_1.1.2_osx_x86_64.zip
+curl -LO https://github.com/sgreben/jp/releases/download/1.1.3/jp_1.1.3_osx_x86_64.zip
+unzip jp_1.1.3_osx_x86_64.zip
# Windows
-curl -LO https://github.com/sgreben/jp/releases/download/1.1.2/jp_1.1.2_windows_x86_64.zip
-unzip jp_1.1.2_windows_x86_64.zip
+curl -LO https://github.com/sgreben/jp/releases/download/1.1.3/jp_1.1.3_windows_x86_64.zip
+unzip jp_1.1.3_windows_x86_64.zip
```
## Use it
diff --git a/cmd/jp/bar.go b/cmd/jp/bar.go
index 15dd196..cfc39c2 100644
--- a/cmd/jp/bar.go
+++ b/cmd/jp/bar.go
@@ -2,37 +2,38 @@ package main
import (
"fmt"
+ "log"
"reflect"
+ "github.com/sgreben/jp/pkg/data"
"github.com/sgreben/jp/pkg/draw"
"github.com/sgreben/jp/pkg/plot"
)
-func barPlotData(xvv, yvv [][]reflect.Value) (x []string, y []float64) {
- for _, xv := range xvv {
- for i := range xv {
- if xv[i].IsValid() && xv[i].CanInterface() {
- x = append(x, fmt.Sprint(xv[i].Interface()))
- }
+func barPlotData(xv, yv []reflect.Value) (x []string, y []float64) {
+ for i := range xv {
+ if xv[i].IsValid() && xv[i].CanInterface() {
+ x = append(x, fmt.Sprint(xv[i].Interface()))
}
}
- for _, yv := range yvv {
- for i := range yv {
- if yv[i].IsValid() && yv[i].CanInterface() {
- yvi, ok := yv[i].Interface().(float64)
- if ok {
- y = append(y, yvi)
- }
+ for i := range yv {
+ if yv[i].IsValid() && yv[i].CanInterface() {
+ yvi, ok := yv[i].Interface().(float64)
+ if ok {
+ y = append(y, yvi)
}
}
}
return
}
-func barPlot(xvv, yvv [][]reflect.Value, c draw.Canvas) string {
- groups, y := barPlotData(xvv, yvv)
+func barPlot(xv, yv []reflect.Value, c draw.Canvas) string {
+ groups, y := barPlotData(xv, yv)
chart := plot.NewBarChart(c)
- data := new(plot.DataTable)
+ data := new(data.Table)
+ if len(y) == 0 {
+ log.Fatal("no valid y values given")
+ }
if len(groups) != len(y) {
for i := range y {
data.AddColumn(fmt.Sprint(i))
diff --git a/cmd/jp/hist.go b/cmd/jp/hist.go
new file mode 100644
index 0000000..77c04e6
--- /dev/null
+++ b/cmd/jp/hist.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+ "log"
+ "reflect"
+
+ "github.com/sgreben/jp/pkg/data"
+ "github.com/sgreben/jp/pkg/draw"
+ "github.com/sgreben/jp/pkg/plot"
+)
+
+func histogramData(xv []reflect.Value, nbins uint) (groups []string, counts []float64) {
+ var x []float64
+ for i := range xv {
+ if xv[i].IsValid() && xv[i].CanInterface() {
+ xvi, ok := xv[i].Interface().(float64)
+ if ok {
+ x = append(x, xvi)
+ }
+ }
+ }
+ if len(x) == 0 {
+ log.Fatal("no valid x values given")
+ }
+ bins := data.NewBins(x)
+ bins.Number = int(nbins)
+ if nbins == 0 {
+ bins.ChooseSturges()
+ }
+ hist := data.Histogram(x, bins)
+ groups = make([]string, len(hist))
+ counts = make([]float64, len(hist))
+ for i, b := range hist {
+ groups[i] = b.String()
+ counts[i] = float64(b.Count)
+ }
+ return
+}
+
+func histogram(xv []reflect.Value, c draw.Canvas, nbins uint) string {
+ groups, counts := histogramData(xv, nbins)
+ chart := plot.NewBarChart(c)
+ chart.BarPaddingX = 0
+ data := new(data.Table)
+ for _, g := range groups {
+ data.AddColumn(g)
+ }
+ data.AddRow(counts...)
+ return chart.Draw(data)
+}
diff --git a/cmd/jp/line.go b/cmd/jp/line.go
index caca49f..8054f87 100644
--- a/cmd/jp/line.go
+++ b/cmd/jp/line.go
@@ -1,47 +1,48 @@
package main
import (
+ "log"
"reflect"
+ "github.com/sgreben/jp/pkg/data"
"github.com/sgreben/jp/pkg/draw"
"github.com/sgreben/jp/pkg/plot"
)
-func linePlotData(xvv, yvv [][]reflect.Value) (x, y []float64) {
- for _, xv := range xvv {
- for i := range xv {
- if xv[i].IsValid() && xv[i].CanInterface() {
- xvi, ok := xv[i].Interface().(float64)
- if ok {
- x = append(x, xvi)
- }
+func linePlotData(xv, yv []reflect.Value) (x, y []float64) {
+ for i := range xv {
+ if xv[i].IsValid() && xv[i].CanInterface() {
+ xvi, ok := xv[i].Interface().(float64)
+ if ok {
+ x = append(x, xvi)
}
}
}
- for _, yv := range yvv {
- for i := range yv {
- if yv[i].IsValid() && yv[i].CanInterface() {
- yvi, ok := yv[i].Interface().(float64)
- if ok {
- y = append(y, yvi)
- }
+ for i := range yv {
+ if yv[i].IsValid() && yv[i].CanInterface() {
+ yvi, ok := yv[i].Interface().(float64)
+ if ok {
+ y = append(y, yvi)
}
}
}
return
}
-func linePlot(xvv, yvv [][]reflect.Value, c draw.Canvas) string {
- x, y := linePlotData(xvv, yvv)
+func linePlot(xv, yv []reflect.Value, c draw.Canvas) string {
+ x, y := linePlotData(xv, yv)
chart := plot.NewLineChart(c)
- data := new(plot.DataTable)
+ data := new(data.Table)
data.AddColumn("x")
data.AddColumn("y")
n := len(x)
if len(y) > n {
n = len(y)
}
+ if len(y) == 0 {
+ log.Fatal("no valid y values given")
+ }
// If no valid xs are given, use the indices as x values.
if len(x) == 0 {
x = make([]float64, len(y))
diff --git a/cmd/jp/main.go b/cmd/jp/main.go
index 62f3db0..173ac27 100644
--- a/cmd/jp/main.go
+++ b/cmd/jp/main.go
@@ -23,6 +23,7 @@ type configuration struct {
PlotType enumVar
CanvasType enumVar
InputType enumVar
+ HistBins uint
}
const (
@@ -51,6 +52,7 @@ var config = configuration{
plotTypeLine,
plotTypeBar,
plotTypeScatter,
+ plotTypeHist,
},
},
CanvasType: enumVar{
@@ -86,8 +88,10 @@ func init() {
flag.StringVar(&config.XY, "xy", "", "x,y value pairs (JSONPath expression). Overrides -x and -y if given.")
flag.IntVar(&config.Box.Width, "width", 0, "Plot width (default 0 (auto))")
flag.IntVar(&config.Box.Height, "height", 0, "Plot height (default 0 (auto))")
+ flag.UintVar(&config.HistBins, "bins", 0, "Number of histogram bins (default 0 (auto))")
flag.Parse()
log.SetOutput(os.Stderr)
+ log.SetFlags(log.LstdFlags | log.Lshortfile)
var err error
xPattern = jsonpath.New("x")
@@ -143,12 +147,12 @@ func main() {
}
in = parseRows(rows)
}
- var x, y [][]reflect.Value
+ var x, y []reflect.Value
if xyPattern != nil {
x, y = split(match(in, xyPattern))
} else {
- x = match(in, xPattern)
- y = match(in, yPattern)
+ x = flatten(match(in, xPattern))
+ y = flatten(match(in, yPattern))
}
buffer := draw.NewBuffer(config.Box)
var p draw.Pixels
@@ -162,7 +166,6 @@ func main() {
}
p.Clear()
c := draw.Canvas{Pixels: p}
- fmt.Println()
switch config.PlotType.Value {
case plotTypeLine:
fmt.Println(linePlot(x, y, c))
@@ -170,5 +173,7 @@ func main() {
fmt.Println(scatterPlot(x, y, c))
case plotTypeBar:
fmt.Println(barPlot(x, y, c))
+ case plotTypeHist:
+ fmt.Println(histogram(x, c, config.HistBins))
}
}
diff --git a/cmd/jp/scatter.go b/cmd/jp/scatter.go
index 9df8b4a..2fdf0e6 100644
--- a/cmd/jp/scatter.go
+++ b/cmd/jp/scatter.go
@@ -3,39 +3,35 @@ package main
import (
"reflect"
+ "github.com/sgreben/jp/pkg/data"
"github.com/sgreben/jp/pkg/draw"
-
"github.com/sgreben/jp/pkg/plot"
)
-func scatterPlotData(xvv, yvv [][]reflect.Value) (x, y []float64) {
- for _, xv := range xvv {
- for i := range xv {
- if xv[i].IsValid() && xv[i].CanInterface() {
- xvi, ok := xv[i].Interface().(float64)
- if ok {
- x = append(x, xvi)
- }
+func scatterPlotData(xv, yv []reflect.Value) (x, y []float64) {
+ for i := range xv {
+ if xv[i].IsValid() && xv[i].CanInterface() {
+ xvi, ok := xv[i].Interface().(float64)
+ if ok {
+ x = append(x, xvi)
}
}
}
- for _, yv := range yvv {
- for i := range yv {
- if yv[i].IsValid() && yv[i].CanInterface() {
- yvi, ok := yv[i].Interface().(float64)
- if ok {
- y = append(y, yvi)
- }
+ for i := range yv {
+ if yv[i].IsValid() && yv[i].CanInterface() {
+ yvi, ok := yv[i].Interface().(float64)
+ if ok {
+ y = append(y, yvi)
}
}
}
return
}
-func scatterPlot(xvv, yvv [][]reflect.Value, c draw.Canvas) string {
- x, y := scatterPlotData(xvv, yvv)
+func scatterPlot(xv, yv []reflect.Value, c draw.Canvas) string {
+ x, y := scatterPlotData(xv, yv)
chart := plot.NewScatterChart(c)
- data := new(plot.DataTable)
+ data := new(data.Table)
data.AddColumn("x")
data.AddColumn("y")
n := len(x)
diff --git a/cmd/jp/split.go b/cmd/jp/split.go
index 4f36825..efe59c9 100644
--- a/cmd/jp/split.go
+++ b/cmd/jp/split.go
@@ -11,10 +11,10 @@ func flatten(in [][]reflect.Value) (out []reflect.Value) {
return
}
-func split(in [][]reflect.Value) (x, y [][]reflect.Value) {
+func split(in [][]reflect.Value) (x, y []reflect.Value) {
flat := flatten(in)
n := len(flat)
- x = [][]reflect.Value{flat[:n/2]}
- y = [][]reflect.Value{flat[n/2:]}
+ x = flat[:n/2]
+ y = flat[n/2:]
return
}
diff --git a/pkg/data/histogram.go b/pkg/data/histogram.go
new file mode 100644
index 0000000..06e5d95
--- /dev/null
+++ b/pkg/data/histogram.go
@@ -0,0 +1,108 @@
+package data
+
+import (
+ "fmt"
+ "math"
+)
+
+import "strconv"
+
+const maxDigits = 6
+
+func ff(x float64) string {
+ minExact := strconv.FormatFloat(x, 'g', -1, 64)
+ fixed := strconv.FormatFloat(x, 'g', maxDigits, 64)
+ if len(minExact) < len(fixed) {
+ return minExact
+ }
+ return fixed
+}
+
+type Bin struct {
+ LeftInclusive float64
+ Right float64
+ RightInclusive bool
+ Count uint64
+}
+
+func (b *Bin) String() string {
+ if b.RightInclusive {
+ return fmt.Sprintf("[%s,%s]", ff(b.LeftInclusive), ff(b.Right))
+ }
+ return fmt.Sprintf("[%s,%s)", ff(b.LeftInclusive), ff(b.Right))
+}
+
+type Bins struct {
+ Number int
+ min, max float64
+ numPoints int
+}
+
+func (b *Bins) ChooseSqrt() {
+ b.Number = int(math.Sqrt(float64(b.numPoints)))
+}
+
+func (b *Bins) ChooseSturges() {
+ b.Number = int(math.Ceil(math.Log2(float64(b.numPoints))) + 1)
+}
+
+func (b *Bins) ChooseRice() {
+ b.Number = int(math.Ceil(2 * math.Pow(float64(b.numPoints), 1.0/3.0)))
+}
+
+func NewBins(points []float64) *Bins {
+ bins := new(Bins)
+ bins.numPoints = len(points)
+ bins.Number = 5
+ bins.min = math.Inf(1)
+ bins.max = math.Inf(-1)
+ for _, x := range points {
+ bins.min = math.Min(bins.min, x)
+ bins.max = math.Max(bins.max, x)
+ }
+ return bins
+}
+
+func (b *Bins) left(i int) float64 {
+ return (b.max - b.min) / float64(b.Number) * float64(i)
+}
+
+func (b *Bins) right(i int) float64 {
+ return b.left(i + 1)
+}
+
+func (b *Bins) All() (out []Bin) {
+ if b.max == b.min {
+ b.Number = 1
+ }
+ for i := 0; i < b.Number; i++ {
+ out = append(out, Bin{
+ LeftInclusive: b.left(i),
+ Right: b.right(i),
+ })
+ }
+ out[b.Number-1].RightInclusive = true
+ return
+}
+
+func (b *Bins) Point(x float64) int {
+ if b.max == b.min {
+ return 0
+ }
+ i := int((x - b.min) / (b.max - b.min) * float64(b.Number))
+ if i >= b.Number {
+ i--
+ }
+ return i
+}
+
+func Histogram(points []float64, bins *Bins) (out []Bin) {
+ out = bins.All()
+ for _, b := range out {
+ b.Count = 0
+ }
+ for _, x := range points {
+ out[bins.Point(x)].Count++
+ }
+ return
+}
diff --git a/pkg/data/table.go b/pkg/data/table.go
new file mode 100644
index 0000000..764ee23
--- /dev/null
+++ b/pkg/data/table.go
@@ -0,0 +1,14 @@
+package data
+
+type Table struct {
+ Columns []string
+ Rows [][]float64
+}
+
+func (d *Table) AddColumn(name string) {
+ d.Columns = append(d.Columns, name)
+}
+
+func (d *Table) AddRow(elms ...float64) {
+ d.Rows = append(d.Rows, elms)
+}
diff --git a/pkg/plot/barchart.go b/pkg/plot/barchart.go
index 6bcacd9..04a36be 100644
--- a/pkg/plot/barchart.go
+++ b/pkg/plot/barchart.go
@@ -4,6 +4,7 @@ import (
"bytes"
"math"
+ "github.com/sgreben/jp/pkg/data"
"github.com/sgreben/jp/pkg/draw"
)
@@ -22,10 +23,10 @@ func NewBarChart(canvas draw.Canvas) *BarChart {
}
// Draw implements Chart
-func (c *BarChart) Draw(data *DataTable) string {
+func (c *BarChart) Draw(table *data.Table) string {
minY := math.Inf(1)
maxY := math.Inf(-1)
- for _, row := range data.Rows {
+ for _, row := range table.Rows {
for _, y := range row {
if y < minY {
minY = y
@@ -35,12 +36,31 @@ func (c *BarChart) Draw(data *DataTable) string {
}
}
}
- paddingX := 4
+ paddingX := 2
paddingY := 3
chartHeight := c.Size().Height - paddingY*c.RuneSize().Height
chartWidth := c.Size().Width - 2*paddingX*c.RuneSize().Width
+
+ labelsBelowBars := true
+ labelsRight := false
+ maxLabelLength := 0
+ totalLabelLength := 0
+ for _, group := range table.Columns {
+ totalLabelLength += len(group)
+ if len(group) > maxLabelLength {
+ maxLabelLength = len(group)
+ }
+ }
+ if totalLabelLength*c.RuneSize().Width > chartWidth {
+ labelsBelowBars = false
+ if len(table.Columns)*c.RuneSize().Height <= chartHeight {
+ labelsRight = true
+ chartWidth -= 3 + maxLabelLength*c.RuneSize().Width
+ }
+ }
+
scaleY := float64(chartHeight) / maxY
- barPaddedWidth := chartWidth / len(data.Columns)
+ barPaddedWidth := chartWidth / len(table.Columns)
barWidth := barPaddedWidth - (c.BarPaddingX * c.RuneSize().Width)
if barPaddedWidth < c.RuneSize().Width {
barPaddedWidth = c.RuneSize().Width
@@ -51,11 +71,11 @@ func (c *BarChart) Draw(data *DataTable) string {
scaleY = float64(chartHeight) / maxY
- for i, group := range data.Columns {
+ for i, group := range table.Columns {
barLeft := paddingX*c.RuneSize().Width + barPaddedWidth*i
barRight := barLeft + barWidth
- y := data.Rows[0][i]
+ y := table.Rows[0][i]
barHeight := y * scaleY
barBottom := (paddingY - 1) * c.RuneSize().Height
barTop := barBottom + int(barHeight)
@@ -66,20 +86,29 @@ func (c *BarChart) Draw(data *DataTable) string {
}
}
- // Group label
barMiddle := int(math.Floor(float64(barLeft+barRight) / float64(2*c.RuneSize().Width)))
- c.GetBuffer().WriteCenter(0, barMiddle, []rune(group))
+
+ // Group label
+ if labelsBelowBars {
+ c.GetBuffer().WriteCenter(0, barMiddle, []rune(group))
+ } else {
+ c.GetBuffer().WriteCenter(0, barMiddle, []rune(Fi(i)))
+ }
// 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, Ff(y))
}
+ if labelsRight {
+ for i, group := range table.Columns {
+ c.GetBuffer().WriteRight(c.GetBuffer().Height-i, paddingX+1+chartWidth/c.RuneSize().Width, []rune(Fi(i)))
+ c.GetBuffer().WriteRight(c.GetBuffer().Height-i, paddingX+4+chartWidth/c.RuneSize().Width, []rune(group))
+ }
+ }
b := bytes.NewBuffer(nil)
c.GetBuffer().Render(b)
diff --git a/pkg/plot/datatable.go b/pkg/plot/datatable.go
deleted file mode 100644
index 15cbff4..0000000
--- a/pkg/plot/datatable.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package plot
-
-type DataTable struct {
- Columns []string
- Rows [][]float64
-}
-
-func (d *DataTable) AddColumn(name string) {
- d.Columns = append(d.Columns, name)
-}
-
-func (d *DataTable) AddRow(elms ...float64) {
- d.Rows = append(d.Rows, elms)
-}
diff --git a/pkg/plot/format.go b/pkg/plot/format.go
index f1175dd..03d19f0 100644
--- a/pkg/plot/format.go
+++ b/pkg/plot/format.go
@@ -13,3 +13,8 @@ func Ff(x float64) []rune {
}
return []rune(fixed)
}
+
+// Fi formats an int
+func Fi(x int) []rune {
+ return []rune(strconv.FormatInt(int64(x), 10))
+}
diff --git a/pkg/plot/linechart.go b/pkg/plot/linechart.go
index 25468b9..cfddd0e 100644
--- a/pkg/plot/linechart.go
+++ b/pkg/plot/linechart.go
@@ -4,6 +4,7 @@ import (
"bytes"
"math"
+ "github.com/sgreben/jp/pkg/data"
"github.com/sgreben/jp/pkg/draw"
)
@@ -29,11 +30,11 @@ func (c *LineChart) drawAxes(paddingX, paddingY int, minX, maxX, minY, maxY floa
}
// Draw implements Chart
-func (c *LineChart) Draw(data *DataTable) string {
+func (c *LineChart) Draw(table *data.Table) string {
var scaleY, scaleX float64
var prevX, prevY int
- minX, maxX, minY, maxY := minMax(data)
+ minX, maxX, minY, maxY := minMax(table)
minLabelWidth := len(Ff(minY))
maxLabelWidth := len(Ff(maxY))
@@ -48,7 +49,7 @@ func (c *LineChart) Draw(data *DataTable) string {
scaleY = float64(chartHeight) / (maxY - minY)
first := true
- for _, point := range data.Rows {
+ for _, point := range table.Rows {
if len(point) < 2 {
continue
}
@@ -83,11 +84,11 @@ func roundUpToPercentOfRange(x, d float64) float64 {
return math.Ceil((x*105)/d) * d / 100
}
-func minMax(data *DataTable) (minX, maxX, minY, maxY float64) {
+func minMax(table *data.Table) (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 {
+ for _, r := range table.Rows {
if len(r) < 2 {
continue
}
diff --git a/pkg/plot/scatterchart.go b/pkg/plot/scatterchart.go
index 038b814..3ea7187 100644
--- a/pkg/plot/scatterchart.go
+++ b/pkg/plot/scatterchart.go
@@ -3,6 +3,7 @@ package plot
import (
"bytes"
+ "github.com/sgreben/jp/pkg/data"
"github.com/sgreben/jp/pkg/draw"
)
@@ -28,10 +29,10 @@ func (c *ScatterChart) drawAxes(paddingX, paddingY int, minX, maxX, minY, maxY f
}
// Draw implements Chart
-func (c *ScatterChart) Draw(data *DataTable) string {
+func (c *ScatterChart) Draw(table *data.Table) string {
var scaleY, scaleX float64
- minX, maxX, minY, maxY := minMax(data)
+ minX, maxX, minY, maxY := minMax(table)
minLabelWidth := len(Ff(minY))
maxLabelWidth := len(Ff(maxY))
@@ -45,7 +46,7 @@ func (c *ScatterChart) Draw(data *DataTable) string {
scaleX = float64(chartWidth) / (maxX - minX)
scaleY = float64(chartHeight) / (maxY - minY)
- for _, point := range data.Rows {
+ for _, point := range table.Rows {
if len(point) < 2 {
continue
}