diff options
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/jp/bar.go | 39 | ||||
-rw-r--r-- | cmd/jp/canvas_other.go | 10 | ||||
-rw-r--r-- | cmd/jp/canvas_windows.go | 10 | ||||
-rw-r--r-- | cmd/jp/csv.go | 29 | ||||
-rw-r--r-- | cmd/jp/flag.go | 6 | ||||
-rw-r--r-- | cmd/jp/hist.go | 50 | ||||
-rw-r--r-- | cmd/jp/line.go | 45 | ||||
-rw-r--r-- | cmd/jp/main.go | 117 | ||||
-rw-r--r-- | cmd/jp/scatter.go | 52 | ||||
-rw-r--r-- | cmd/jp/split.go | 6 |
10 files changed, 293 insertions, 71 deletions
diff --git a/cmd/jp/bar.go b/cmd/jp/bar.go index e1a622b..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/jp" - "github.com/sgreben/jp/pkg/jp/primitives" + "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, box primitives.Box) string { - groups, y := barPlotData(xvv, yvv) - chart := jp.NewBarChart(box.Width, box.Height) - data := new(jp.DataTable) +func barPlot(xv, yv []reflect.Value, c draw.Canvas) string { + groups, y := barPlotData(xv, yv) + chart := plot.NewBarChart(c) + 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/canvas_other.go b/cmd/jp/canvas_other.go new file mode 100644 index 0000000..053cef7 --- /dev/null +++ b/cmd/jp/canvas_other.go @@ -0,0 +1,10 @@ +// +build !windows + +package main + +var autoCanvas = map[string]string{ + plotTypeBar: canvasTypeQuarter, + plotTypeLine: canvasTypeQuarter, + plotTypeScatter: canvasTypeBraille, + plotTypeHist: canvasTypeQuarter, +} diff --git a/cmd/jp/canvas_windows.go b/cmd/jp/canvas_windows.go new file mode 100644 index 0000000..de22e5b --- /dev/null +++ b/cmd/jp/canvas_windows.go @@ -0,0 +1,10 @@ +// +build windows + +package main + +var autoCanvas = map[string]string{ + plotTypeBar: canvasTypeFull, + plotTypeLine: canvasTypeFull, + plotTypeScatter: canvasTypeFull, + plotTypeHist: canvasTypeFull, +} diff --git a/cmd/jp/csv.go b/cmd/jp/csv.go new file mode 100644 index 0000000..08ce65c --- /dev/null +++ b/cmd/jp/csv.go @@ -0,0 +1,29 @@ +package main + +import "strconv" + +func parseCell(cell string) interface{} { + f, err := strconv.ParseFloat(cell, 64) + if err == nil { + return f + } + b, err := strconv.ParseBool(cell) + if err == nil { + if b { + return 1 + } + return 0 + } + return cell +} + +func parseRows(rows [][]string) (out [][]interface{}) { + out = make([][]interface{}, len(rows)) + for i, row := range rows { + out[i] = make([]interface{}, len(row)) + for j, cell := range row { + out[i][j] = parseCell(cell) + } + } + return +} diff --git a/cmd/jp/flag.go b/cmd/jp/flag.go index 533b35d..eeec98c 100644 --- a/cmd/jp/flag.go +++ b/cmd/jp/flag.go @@ -6,8 +6,8 @@ import ( ) type enumVar struct { - Choices []string // The acceptable choices the user may pass to the flag - Value string // the current value of the flag + Choices []string + Value string } // Set implements the flag.Value interface. @@ -18,7 +18,7 @@ func (so *enumVar) Set(v string) error { return nil } } - return fmt.Errorf("invalid choice; must be one of %s", strings.Join(so.Choices, ",")) + return fmt.Errorf("must be one of [%s]", strings.Join(so.Choices, " ")) } func (so *enumVar) String() string { 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 b47b38c..8054f87 100644 --- a/cmd/jp/line.go +++ b/cmd/jp/line.go @@ -1,46 +1,48 @@ package main import ( + "log" "reflect" - "github.com/sgreben/jp/pkg/jp" - "github.com/sgreben/jp/pkg/jp/primitives" + "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, box primitives.Box) string { - x, y := linePlotData(xvv, yvv) - chart := jp.NewLineChart(box.Width, box.Height) - data := new(jp.DataTable) +func linePlot(xv, yv []reflect.Value, c draw.Canvas) string { + x, y := linePlotData(xv, yv) + chart := plot.NewLineChart(c) + 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)) @@ -51,6 +53,5 @@ func linePlot(xvv, yvv [][]reflect.Value, box primitives.Box) string { for i := 0; i < n; i++ { data.AddRow(x[i%len(x)], y[i%len(y)]) } - chart.Symbol = "█" return chart.Draw(data) } diff --git a/cmd/jp/main.go b/cmd/jp/main.go index c603f68..173ac27 100644 --- a/cmd/jp/main.go +++ b/cmd/jp/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/csv" "encoding/json" "flag" "fmt" @@ -8,24 +9,41 @@ import ( "os" "reflect" - "github.com/sgreben/jp/pkg/jp/primitives" + "github.com/sgreben/jp/pkg/draw" "github.com/sgreben/jp/pkg/terminal" "github.com/sgreben/jp/pkg/jsonpath" ) type configuration struct { - Box primitives.Box - X string - Y string - XY string - PlotType enumVar + Box draw.Box + X string + Y string + XY string + PlotType enumVar + CanvasType enumVar + InputType enumVar + HistBins uint } -const plotTypeLine = "line" -const plotTypeBar = "bar" -const plotTypeScatter = "scatter" -const plotTypeHist = "hist" +const ( + plotTypeLine = "line" + plotTypeBar = "bar" + plotTypeScatter = "scatter" + plotTypeHist = "hist" +) + +const ( + canvasTypeFull = "full" + canvasTypeQuarter = "quarter" + canvasTypeBraille = "braille" + canvasTypeAuto = "auto" +) + +const ( + inputTypeCSV = "csv" + inputTypeJSON = "json" +) var config = configuration{ PlotType: enumVar{ @@ -33,23 +51,47 @@ var config = configuration{ Choices: []string{ plotTypeLine, plotTypeBar, + plotTypeScatter, + plotTypeHist, + }, + }, + CanvasType: enumVar{ + Value: canvasTypeAuto, + Choices: []string{ + canvasTypeFull, + canvasTypeQuarter, + canvasTypeBraille, + canvasTypeAuto, + }, + }, + InputType: enumVar{ + Value: inputTypeJSON, + Choices: []string{ + inputTypeJSON, + inputTypeCSV, }, }, } -var xPattern *jsonpath.JSONPath -var yPattern *jsonpath.JSONPath -var xyPattern *jsonpath.JSONPath +var ( + xPattern *jsonpath.JSONPath + yPattern *jsonpath.JSONPath + xyPattern *jsonpath.JSONPath +) func init() { flag.Var(&config.PlotType, "type", fmt.Sprintf("Plot type. One of %v", config.PlotType.Choices)) + flag.Var(&config.CanvasType, "canvas", fmt.Sprintf("Canvas type. One of %v", config.CanvasType.Choices)) + flag.Var(&config.InputType, "input", fmt.Sprintf("Input type. One of %v", config.InputType.Choices)) flag.StringVar(&config.X, "x", "", "x values (JSONPath expression)") flag.StringVar(&config.Y, "y", "", "y values (JSONPath expression)") 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") @@ -75,6 +117,9 @@ func init() { if config.Box.Height == 0 { config.Box.Height = terminal.Height() - 1 } + if config.CanvasType.Value == canvasTypeAuto { + config.CanvasType.Value = autoCanvas[config.PlotType.Value] + } } func match(in interface{}, p *jsonpath.JSONPath) [][]reflect.Value { @@ -87,24 +132,48 @@ func match(in interface{}, p *jsonpath.JSONPath) [][]reflect.Value { func main() { var in interface{} - dec := json.NewDecoder(os.Stdin) - err := dec.Decode(&in) - if err != nil { - log.Println(err) + switch config.InputType.Value { + case inputTypeJSON: + dec := json.NewDecoder(os.Stdin) + err := dec.Decode(&in) + if err != nil { + log.Println(err) + } + case inputTypeCSV: + r := csv.NewReader(os.Stdin) + rows, err := r.ReadAll() + if err != nil { + log.Println(err) + } + in = parseRows(rows) } - fmt.Println() - 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 + switch config.CanvasType.Value { + case canvasTypeBraille: + p = &draw.Braille{Buffer: buffer} + case canvasTypeQuarter: + p = &draw.Quarter{Buffer: buffer} + case canvasTypeFull: + p = &draw.Full{Buffer: buffer} + } + p.Clear() + c := draw.Canvas{Pixels: p} switch config.PlotType.Value { case plotTypeLine: - fmt.Print(linePlot(x, y, config.Box)) + fmt.Println(linePlot(x, y, c)) + case plotTypeScatter: + fmt.Println(scatterPlot(x, y, c)) case plotTypeBar: - fmt.Print(barPlot(x, y, config.Box)) + 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 new file mode 100644 index 0000000..2fdf0e6 --- /dev/null +++ b/cmd/jp/scatter.go @@ -0,0 +1,52 @@ +package main + +import ( + "reflect" + + "github.com/sgreben/jp/pkg/data" + "github.com/sgreben/jp/pkg/draw" + "github.com/sgreben/jp/pkg/plot" +) + +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 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(xv, yv []reflect.Value, c draw.Canvas) string { + x, y := scatterPlotData(xv, yv) + chart := plot.NewScatterChart(c) + data := new(data.Table) + data.AddColumn("x") + data.AddColumn("y") + n := len(x) + if len(y) > n { + n = len(y) + } + // If no valid xs are given, use the indices as x values. + if len(x) == 0 { + x = make([]float64, len(y)) + for i := 0; i < len(y); i++ { + x[i] = float64(i) + } + } + for i := 0; i < n; i++ { + data.AddRow(x[i%len(x)], y[i%len(y)]) + } + return chart.Draw(data) +} 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 } |