├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── common.go ├── common_test.go ├── core.go ├── core_test.go ├── function.go ├── function_test.go ├── glot.go ├── glot_test.go ├── pointgroup.go └── pointgroup_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | 3 | language: go 4 | 5 | go: 6 | - 1.6.x 7 | - 1.7.x 8 | - 1.8.x 9 | - 1.9.x 10 | - master 11 | matrix: 12 | fast_finish: true 13 | allow_failures: 14 | - go: master 15 | 16 | env: 17 | global: 18 | - DISPLAY: ":99.0" 19 | 20 | # Get coverage tools, and start the virtual framebuffer. 21 | before_install: 22 | - sudo apt-get -qq update 23 | - sudo apt-get install -y gnuplot-x11 24 | 25 | install: 26 | - export PATH=$HOME/gopath/bin:$PATH 27 | - go get -v github.com/axw/gocov/gocov 28 | - go install github.com/axw/gocov/gocov 29 | 30 | # Get deps, build, test, and ensure the code is gofmt'ed. 31 | # If we are building as gonum, then we have access to the coveralls api key, so we can run coverage as well. 32 | script: 33 | - go get -d -t -v ./... 34 | - go build -v ./... 35 | - go test -v ./... 36 | - gocov test | gocov report 37 | - test -z "$(gofmt -d .)" 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of conduct 2 | Welcomes any kind of contribution, please follow the [how to write GO code](https://golang.org/doc/code.html) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contribute 2 | Welcomes any kind of contribution, please follow the next steps: 3 | 4 | - Fork the project on github.com. 5 | - Create a new branch. 6 | - Commit changes to the new branch. 7 | - Send a pull request. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Arafat Da Khan 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 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.` 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Arafatk/glot.svg?branch=master)](https://travis-ci.org/Arafatk/glot) [![GoDoc](https://godoc.org/github.com/arafat/glot?status.svg)](https://godoc.org/github.com/Arafatk/glot) [![Join the chat at https://gitter.im/tensorflowrb/Lobby](https://badges.gitter.im/tensorflowrb/Lobby.svg)](https://gitter.im/glot-dev/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link) ![License](https://img.shields.io/badge/License-MIT-blue.svg) 2 | 3 | # Glot 4 | `glot` is a plotting library for Golang built on top of [gnuplot](http://www.gnuplot.info/). `glot` currently supports styles like lines, points, bars, steps, histogram, circle, and many others. We are continuously making efforts to add more features. 5 | 6 | ## Documentation 7 | Documentation is available at [godoc](https://godoc.org/github.com/Arafatk/glot). 8 | 9 | ## Requirements 10 | - gnu plot 11 | - build gnu plot from [source](https://sourceforge.net/projects/gnuplot/files/gnuplot/) 12 | - linux users 13 | - ```sudo apt-get update``` 14 | - ```sudo apt-get install gnuplot-x11``` 15 | - mac users 16 | - install homebrew 17 | - ```brew cask install xquartz``` (for x-11) 18 | - ```brew install gnuplot --with-x11``` 19 | 20 | ## Installation 21 | ```go get github.com/Arafatk/glot``` 22 | 23 | ## Usage and Examples 24 | We have a blog post explaining our vision and covering some basic usage of the `glot` library. Check it out [here](https://medium.com/@Arafat./introducing-glot-the-plotting-library-for-golang-3133399948a1). 25 | 26 | ## Examples 27 | ![](https://raw.githubusercontent.com/Arafatk/plot/master/Screenshot%20-%20Saturday%2014%20October%202017%20-%2004-51-13%20%20IST.png) 28 | 29 | ## Contributing 30 | We really encourage developers coming in, finding a bug or requesting a new feature. Want to tell us about the feature you just implemented, just raise a pull request and we'll be happy to go through it. Please read the CONTRIBUTING and CODE_OF_CONDUCT file. 31 | 32 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // SetTitle sets the title for the plot 8 | // 9 | // Usage 10 | // dimensions := 3 11 | // persist := false 12 | // debug := false 13 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 14 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 15 | // plot.SetTitle("Test Results") 16 | func (plot *Plot) SetTitle(title string) error { 17 | return plot.Cmd(fmt.Sprintf("set title \"%s\" ", title)) 18 | } 19 | 20 | // SetXLabel changes the label for the x-axis 21 | // 22 | // Usage 23 | // dimensions := 3 24 | // persist := false 25 | // debug := false 26 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 27 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 28 | // plot.SetTitle("Test Results") 29 | // plot.SetXLabel("X-Axis") 30 | func (plot *Plot) SetXLabel(label string) error { 31 | return plot.Cmd(fmt.Sprintf("set xlabel '%s'", label)) 32 | } 33 | 34 | // SetYLabel changes the label for the y-axis 35 | // 36 | // Usage 37 | // dimensions := 3 38 | // persist := false 39 | // debug := false 40 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 41 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 42 | // plot.SetTitle("Test Results") 43 | // plot.SetYLabel("Y-Axis") 44 | func (plot *Plot) SetYLabel(label string) error { 45 | return plot.Cmd(fmt.Sprintf("set ylabel '%s'", label)) 46 | } 47 | 48 | // SetZLabel changes the label for the z-axis 49 | // 50 | // Usage 51 | // dimensions := 3 52 | // persist := false 53 | // debug := false 54 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 55 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 56 | // plot.SetTitle("Test Results") 57 | // plot.SetZLabel("Z-Axis") 58 | func (plot *Plot) SetZLabel(label string) error { 59 | return plot.Cmd(fmt.Sprintf("set zlabel '%s'", label)) 60 | } 61 | 62 | // SetLabels Functions helps to set labels for x, y, z axis simultaneously 63 | // 64 | // Usage 65 | // dimensions := 3 66 | // persist := false 67 | // debug := false 68 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 69 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 70 | // plot.SetTitle("Test Results") 71 | // plot.SetLabels("X-axis","Y-Axis","Z-Axis") 72 | func (plot *Plot) SetLabels(labels ...string) error { 73 | ndims := len(labels) 74 | if ndims > 3 || ndims <= 0 { 75 | return &gnuplotError{fmt.Sprintf("invalid number of dims '%v'", ndims)} 76 | } 77 | var err error 78 | 79 | for i, label := range labels { 80 | switch i { 81 | case 0: 82 | ierr := plot.SetXLabel(label) 83 | if ierr != nil { 84 | err = ierr 85 | return err 86 | } 87 | case 1: 88 | ierr := plot.SetYLabel(label) 89 | if ierr != nil { 90 | err = ierr 91 | return err 92 | } 93 | case 2: 94 | ierr := plot.SetZLabel(label) 95 | if ierr != nil { 96 | err = ierr 97 | return err 98 | } 99 | } 100 | } 101 | return nil 102 | } 103 | 104 | // SetXrange changes the label for the x-axis 105 | // 106 | // Usage 107 | // dimensions := 3 108 | // persist := false 109 | // debug := false 110 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 111 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 112 | // plot.SetTitle("Test Results") 113 | // plot.SetXrange(-2,2) 114 | func (plot *Plot) SetXrange(start int, end int) error { 115 | return plot.Cmd(fmt.Sprintf("set xrange [%d:%d]", start, end)) 116 | } 117 | 118 | // SetLogscale changes the label for the x-axis 119 | // 120 | // Usage 121 | // dimensions := 3 122 | // persist := false 123 | // debug := false 124 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 125 | // plot.SetYrange(-2, 18) 126 | // plot.AddPointGroup("rates", "circle", [][]float64{{2, 4, 8, 16, 32}, {4, 7, 4, 10, 3}}) 127 | // plot.SetLogscale("x", 2) 128 | func (plot *Plot) SetLogscale(axis string, base int) error { 129 | return plot.Cmd(fmt.Sprintf("set logscale %s %d", axis, base)) 130 | } 131 | 132 | // SetYrange changes the label for the y-axis 133 | // 134 | // Usage 135 | // dimensions := 3 136 | // persist := false 137 | // debug := false 138 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 139 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 140 | // plot.SetTitle("Test Results") 141 | // plot.SetYrange(-2,2) 142 | func (plot *Plot) SetYrange(start int, end int) error { 143 | return plot.Cmd(fmt.Sprintf("set yrange [%d:%d]", start, end)) 144 | } 145 | 146 | // SetZrange changes the label for the z-axis 147 | // 148 | // Usage 149 | // dimensions := 3 150 | // persist := false 151 | // debug := false 152 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 153 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 154 | // plot.SetTitle("Test Results") 155 | // plot.SetZrange(-2,2) 156 | func (plot *Plot) SetZrange(start int, end int) error { 157 | return plot.Cmd(fmt.Sprintf("set zrange [%d:%d]", start, end)) 158 | } 159 | 160 | // SavePlot function is used to save the plot at this point. 161 | // The plot is dynamic and additional pointgroups can be added and removed and different versions 162 | // of the same plot can be saved. 163 | // 164 | // Usage 165 | // dimensions := 3 166 | // persist := false 167 | // debug := false 168 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 169 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 170 | // plot.SetTitle("Test Results") 171 | // plot.SetZrange(-2,2) 172 | // plot.SavePlot("1.jpeg") 173 | func (plot *Plot) SavePlot(filename string) (err error) { 174 | if plot.nplots == 0 { 175 | return &gnuplotError{fmt.Sprintf("This plot has 0 curves and therefore its a redundant plot and it can't be printed.")} 176 | } 177 | outputFormat := "set terminal " + plot.format 178 | plot.CheckedCmd(outputFormat) 179 | outputFileCommand := "set output" + "'" + filename + "'" 180 | plot.CheckedCmd(outputFileCommand) 181 | plot.CheckedCmd("replot ") 182 | return nil 183 | } 184 | 185 | // SetFormat function is used to save the plot at this point. 186 | // The plot is dynamic and additional pointgroups can be added and removed and different versions 187 | // of the same plot can be saved. 188 | // 189 | // Usage 190 | // dimensions := 3 191 | // persist := false 192 | // debug := false 193 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 194 | // plot.AddPointGroup("Sample 1", "lines", []float64{2, 3, 4, 1}) 195 | // plot.SetTitle("Test Results") 196 | // plot.SetFormat("pdf") 197 | // plot.SavePlot("1.pdf") 198 | // NOTE: png is default format for saving files. 199 | func (plot *Plot) SetFormat(newformat string) error { 200 | allowed := []string{ 201 | "png", "pdf"} 202 | for _, s := range allowed { 203 | if newformat == s { 204 | plot.format = newformat 205 | return nil 206 | } 207 | } 208 | fmt.Printf("** Format '%v' not in allowed list %v\n", newformat, allowed) 209 | fmt.Printf("** default to 'png'\n") 210 | err := &gnuplotError{fmt.Sprintf("invalid format '%s'", newformat)} 211 | return err 212 | } 213 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import "testing" 4 | 5 | func TestSetLabels(t *testing.T) { 6 | dimensions := 3 7 | persist := false 8 | debug := false 9 | plot, _ := NewPlot(dimensions, persist, debug) 10 | err := plot.SetLabels() 11 | if err == nil { 12 | t.Error("SetLabels raises error when empty string is passed") 13 | } 14 | } 15 | 16 | func TestSetFormat(t *testing.T) { 17 | dimensions := 3 18 | persist := false 19 | debug := false 20 | plot, _ := NewPlot(dimensions, persist, debug) 21 | err := plot.SetFormat("tls") 22 | if err == nil { 23 | t.Error("SetLabels raises error when non-supported format is passed as an argument.") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | ) 10 | 11 | var gGnuplotCmd string 12 | var gGnuplotPrefix = "go-gnuplot-" 13 | 14 | const defaultStyle = "points" // The default style for a curve 15 | const plotCommand = "replot" // The default style for a curve 16 | 17 | func min(a, b int) int { 18 | if a < b { 19 | return a 20 | } 21 | return b 22 | } 23 | 24 | // Function to intialize the package and check for GNU plot installation 25 | // This raises an error if GNU plot is not installed 26 | func init() { 27 | var err error 28 | 29 | gnuplotExecutableName := "gnuplot" 30 | 31 | if runtime.GOOS == "windows" { 32 | gnuplotExecutableName = "gnuplot.exe" 33 | } 34 | 35 | gGnuplotCmd, err = exec.LookPath(gnuplotExecutableName) 36 | if err != nil { 37 | fmt.Errorf("** could not find path to 'gnuplot':\n%v\n", err) 38 | fmt.Errorf("** set custom path to 'gnuplot' ") 39 | } 40 | } 41 | 42 | type gnuplotError struct { 43 | err string 44 | } 45 | 46 | func (e *gnuplotError) Error() string { 47 | return e.err 48 | } 49 | 50 | // plotterProcess is the type for handling gnu commands. 51 | type plotterProcess struct { 52 | handle *exec.Cmd 53 | stdin io.WriteCloser 54 | } 55 | 56 | // newPlotterProc function makes the plotterProcess struct 57 | func newPlotterProc(persist bool) (*plotterProcess, error) { 58 | procArgs := []string{} 59 | if persist { 60 | procArgs = append(procArgs, "-persist") 61 | } 62 | cmd := exec.Command(gGnuplotCmd, procArgs...) 63 | stdin, err := cmd.StdinPipe() 64 | if err != nil { 65 | return nil, err 66 | } 67 | return &plotterProcess{handle: cmd, stdin: stdin}, cmd.Start() 68 | } 69 | 70 | // Cmd sends a command to the gnuplot subprocess and returns an error 71 | // if something bad happened in the gnuplot process. 72 | // ex: 73 | // 74 | // fname := "foo.dat" 75 | // err := p.Cmd("plot %s", fname) 76 | // if err != nil { 77 | // panic(err) 78 | // } 79 | func (plot *Plot) Cmd(format string, a ...interface{}) error { 80 | cmd := fmt.Sprintf(format, a...) + "\n" 81 | n, err := io.WriteString(plot.proc.stdin, cmd) 82 | if plot.debug { 83 | //buf := new(bytes.Buffer) 84 | //io.Copy(buf, plot.proc.handle.Stdout) 85 | fmt.Printf("cmd> %v", cmd) 86 | fmt.Printf("res> %v\n", n) 87 | } 88 | return err 89 | } 90 | 91 | // CheckedCmd is a convenience wrapper around Cmd: it will error if the 92 | // error returned by Cmd isn't nil. 93 | // ex: 94 | // 95 | // fname := "foo.dat" 96 | // p.CheckedCmd("plot %s", fname) 97 | func (plot *Plot) CheckedCmd(format string, a ...interface{}) { 98 | err := plot.Cmd(format, a...) 99 | if err != nil { 100 | _ = fmt.Errorf("** err: %v\n", err) 101 | } 102 | } 103 | 104 | // A map between os files and file names 105 | type tmpfilesDb map[string]*os.File 106 | 107 | // Close makes sure all resources used by the gnuplot subprocess are reclaimed. 108 | // This method is typically called when the Plotter instance is not needed 109 | // anymore. That's usually done via a defer statement: 110 | // 111 | // p, err := gnuplot.NewPlotter(...) 112 | // if err != nil { /* handle error */ } 113 | // defer p.Close() 114 | func (plot *Plot) Close() (err error) { 115 | if plot.proc != nil && plot.proc.handle != nil { 116 | plot.proc.stdin.Close() 117 | err = plot.proc.handle.Wait() 118 | } 119 | plot.ResetPlot() 120 | return err 121 | } 122 | 123 | func (plot *Plot) cleanplot() (err error) { 124 | plot.tmpfiles = make(tmpfilesDb) 125 | plot.nplots = 0 126 | return err 127 | } 128 | 129 | // ResetPlot is used to reset the whole plot. 130 | // This removes all the PointGroup's from the plot and makes it new. 131 | // Usage 132 | // 133 | // plot.ResetPlot() 134 | func (plot *Plot) ResetPlot() (err error) { 135 | plot.cleanplot() 136 | plot.PointGroup = make(map[string]*PointGroup) // Adding a mapping between a curve name and a curve 137 | return err 138 | } 139 | 140 | func SetCustomPathToGNUPlot(path string) { 141 | gGnuplotCmd = path 142 | } 143 | -------------------------------------------------------------------------------- /core_test.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import "testing" 4 | 5 | func TestMin(t *testing.T) { 6 | var v int 7 | v = min(1, 2) 8 | if v != 1 { 9 | t.Error("Expected 1, got ", v) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /function.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import "fmt" 4 | 5 | // Func2d is a 2-d function which can be plotted with gnuplot 6 | type Func2d func(x float64) float64 7 | 8 | // Func3d is a 3-d function which can be plotted with gnuplot 9 | type Func3d func(x float64, y float64) float64 10 | 11 | // AddFunc2d is used to make a 2-d plot of the format y = Function(x) 12 | // 13 | // Usage 14 | // dimensions := 2 15 | // persist := false 16 | // debug := false 17 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 18 | // fct := func(x float64) float64 { return (math.Exp(x)) } 19 | // groupName := "Exponential Curve" 20 | // style := "lines" 21 | // pointsX := []float64{1, 2, 3, 4, 5} 22 | // plot.AddFunc2d(groupName, style, pointsX, fct) 23 | // plot.SavePlot("1.png") 24 | // Variable definitions 25 | // dimensions :=> refers to the dimensions of the plot. 26 | // debug :=> can be used by developers to check the actual commands sent to gnu plot. 27 | // persist :=> used to make the gnu plot window stay open. 28 | // groupName :=> Name of the curve 29 | // style :=> Style of the curve 30 | // pointsX :=> The x Value of the points to be plotted. y = func(x) is plotted on the curve. 31 | // style :=> Style of the curve 32 | // NOTE: Currently only float64 type is supported for this function 33 | func (plot *Plot) AddFunc2d(name string, style string, x []float64, fct Func2d) error { 34 | y := make([]float64, len(x)) 35 | for index := range x { 36 | y[index] = fct(x[index]) 37 | } 38 | combined := [][]float64{} 39 | combined = append(combined, x) 40 | combined = append(combined, y) 41 | plot.AddPointGroup(name, style, combined) 42 | return nil 43 | } 44 | 45 | // AddFunc3d is used to make a 3-d plot of the format z = Function(x,y) 46 | // 47 | // Usage 48 | // dimensions := 3 49 | // persist := false 50 | // debug := false 51 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 52 | // fct := func(x, y float64) float64 { return x - y } 53 | // groupName := "Stright Line" 54 | // style := "lines" 55 | // pointsY := []float64{1, 2, 3, 4, 5} 56 | // pointsX := []float64{1, 2, 3, 4, 5} 57 | // plot.AddFunc3d(groupName, style, pointsX, pointsY, fct) 58 | // plot.SetXrange(0, 5) 59 | // plot.SetYrange(0, 5) 60 | // plot.SetZrange(0, 5) 61 | // plot.SavePlot("1.png") 62 | // Variable definitions 63 | // dimensions :=> refers to the dimensions of the plot. 64 | // debug :=> can be used by developers to check the actual commands sent to gnu plot. 65 | // persist :=> used to make the gnu plot window stay open. 66 | // groupName :=> Name of the curve 67 | // style :=> Style of the curve 68 | // pointsX :=> The x Value of the points to be plotted. y = func(x) is plotted on the curve. 69 | // NOTE: Currently only float64 type is supported for this function 70 | func (plot *Plot) AddFunc3d(name string, style string, x []float64, y []float64, fct Func3d) error { 71 | if len(x) != len(y) { 72 | return &gnuplotError{fmt.Sprintf("The length of the x-axis array and y-axis array are not same.")} 73 | } 74 | z := make([]float64, len(x)) 75 | for index := range x { 76 | z[index] = fct(x[index], y[index]) 77 | } 78 | combined := [][]float64{} 79 | combined = append(combined, x) 80 | combined = append(combined, y) 81 | combined = append(combined, z) 82 | plot.AddPointGroup(name, style, combined) 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /function_test.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAddFunc3d(t *testing.T) { 8 | dimensions := 3 9 | persist := false 10 | debug := false 11 | plot, _ := NewPlot(dimensions, persist, debug) 12 | fct := func(x, y float64) float64 { return x - y } 13 | groupName := "Stright Line" 14 | style := "lines" 15 | pointsY := []float64{1, 2, 3} 16 | pointsX := []float64{1, 2, 3, 4, 5} 17 | err := plot.AddFunc3d(groupName, style, pointsX, pointsY, fct) 18 | if err == nil { 19 | t.Error("TestAddFunc3d raises error when the size of X and Y arrays are not equal.") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /glot.go: -------------------------------------------------------------------------------- 1 | // Glot is a library for having simplified 1,2,3 Dimensional points/line plots 2 | // It's built on top of Gnu plot and offers the ability to use Raw Gnu plot commands 3 | // directly from golang. 4 | // See the gnuplot documentation page for the exact semantics of the gnuplot 5 | // commands. 6 | // http://www.gnuplot.info/ 7 | 8 | package glot 9 | 10 | import ( 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | ) 15 | 16 | // Plot is the basic type representing a plot. 17 | // Every plot has a set of Pointgroups that are simultaneously plotted 18 | // on a 2/3 D plane given the plot type. 19 | // The Plot dimensions must be specified at the time of construction 20 | // and can't be changed later. All the Pointgroups added to a plot must 21 | // have same dimensions as the dimension specified at the 22 | // the time of plot construction. 23 | // The Pointgroups can be dynamically added and removed from a plot 24 | // And style changes can also be made dynamically. 25 | type Plot struct { 26 | proc *plotterProcess 27 | debug bool 28 | plotcmd string 29 | nplots int // number of currently active plots 30 | tmpfiles tmpfilesDb // A temporary file used for saving data 31 | dimensions int // dimensions of the plot 32 | PointGroup map[string]*PointGroup // A map between Curve name and curve type. This maps a name to a given curve in a plot. Only one curve with a given name exists in a plot. 33 | format string // The saving format of the plot. This could be PDF, PNG, JPEG and so on. 34 | style string // style of the plot 35 | title string // The title of the plot. 36 | } 37 | 38 | // NewPlot Function makes a new plot with the specified dimensions. 39 | // 40 | // Usage 41 | // dimensions := 3 42 | // persist := false 43 | // debug := false 44 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 45 | // Variable definitions 46 | // dimensions :=> refers to the dimensions of the plot. 47 | // debug :=> can be used by developers to check the actual commands sent to gnu plot. 48 | // persist :=> used to make the gnu plot window stay open. 49 | func NewPlot(dimensions int, persist, debug bool) (*Plot, error) { 50 | p := &Plot{proc: nil, debug: debug, plotcmd: "plot", 51 | nplots: 0, dimensions: dimensions, style: "points", format: "png"} 52 | p.PointGroup = make(map[string]*PointGroup) // Adding a mapping between a curve name and a curve 53 | p.tmpfiles = make(tmpfilesDb) 54 | proc, err := newPlotterProc(persist) 55 | if err != nil { 56 | return nil, err 57 | } 58 | // Only 1,2,3 Dimensional plots are supported 59 | if dimensions > 3 || dimensions < 1 { 60 | return nil, &gnuplotError{fmt.Sprintf("invalid number of dims '%v'", dimensions)} 61 | } 62 | p.proc = proc 63 | return p, nil 64 | } 65 | 66 | func (plot *Plot) plotX(PointGroup *PointGroup) error { 67 | f, err := ioutil.TempFile(os.TempDir(), gGnuplotPrefix) 68 | if err != nil { 69 | return err 70 | } 71 | fname := f.Name() 72 | plot.tmpfiles[fname] = f 73 | for _, d := range PointGroup.castedData.([]float64) { 74 | f.WriteString(fmt.Sprintf("%v\n", d)) 75 | } 76 | f.Close() 77 | cmd := plot.plotcmd 78 | if plot.nplots > 0 { 79 | cmd = plotCommand 80 | } 81 | if PointGroup.style == "" { 82 | PointGroup.style = defaultStyle 83 | } 84 | var line string 85 | if PointGroup.name == "" { 86 | line = fmt.Sprintf("%s \"%s\" with %s", cmd, fname, PointGroup.style) 87 | } else { 88 | line = fmt.Sprintf("%s \"%s\" title \"%s\" with %s", 89 | cmd, fname, PointGroup.name, PointGroup.style) 90 | } 91 | plot.nplots++ 92 | return plot.Cmd(line) 93 | } 94 | 95 | func (plot *Plot) plotXY(PointGroup *PointGroup) error { 96 | x := PointGroup.castedData.([][]float64)[0] 97 | y := PointGroup.castedData.([][]float64)[1] 98 | npoints := min(len(x), len(y)) 99 | 100 | f, err := ioutil.TempFile(os.TempDir(), gGnuplotPrefix) 101 | if err != nil { 102 | return err 103 | } 104 | fname := f.Name() 105 | plot.tmpfiles[fname] = f 106 | 107 | for i := 0; i < npoints; i++ { 108 | f.WriteString(fmt.Sprintf("%v %v\n", x[i], y[i])) 109 | } 110 | 111 | f.Close() 112 | cmd := plot.plotcmd 113 | if plot.nplots > 0 { 114 | cmd = plotCommand 115 | } 116 | 117 | if PointGroup.style == "" { 118 | PointGroup.style = "points" 119 | } 120 | var line string 121 | if PointGroup.name == "" { 122 | line = fmt.Sprintf("%s \"%s\" with %s", cmd, fname, PointGroup.style) 123 | } else { 124 | line = fmt.Sprintf("%s \"%s\" title \"%s\" with %s", 125 | cmd, fname, PointGroup.name, PointGroup.style) 126 | } 127 | plot.nplots++ 128 | return plot.Cmd(line) 129 | } 130 | 131 | func (plot *Plot) plotXYZ(points *PointGroup) error { 132 | x := points.castedData.([][]float64)[0] 133 | y := points.castedData.([][]float64)[1] 134 | z := points.castedData.([][]float64)[2] 135 | npoints := min(len(x), len(y)) 136 | npoints = min(npoints, len(z)) 137 | f, err := ioutil.TempFile(os.TempDir(), gGnuplotPrefix) 138 | if err != nil { 139 | return err 140 | } 141 | fname := f.Name() 142 | plot.tmpfiles[fname] = f 143 | 144 | for i := 0; i < npoints; i++ { 145 | f.WriteString(fmt.Sprintf("%v %v %v\n", x[i], y[i], z[i])) 146 | } 147 | 148 | f.Close() 149 | cmd := "splot" // Force 3D plot 150 | if plot.nplots > 0 { 151 | cmd = plotCommand 152 | } 153 | 154 | var line string 155 | if points.name == "" { 156 | line = fmt.Sprintf("%s \"%s\" with %s", cmd, fname, points.style) 157 | } else { 158 | line = fmt.Sprintf("%s \"%s\" title \"%s\" with %s", 159 | cmd, fname, points.name, points.style) 160 | } 161 | plot.nplots++ 162 | return plot.Cmd(line) 163 | } 164 | -------------------------------------------------------------------------------- /glot_test.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import "testing" 4 | 5 | func TestNewPlot(t *testing.T) { 6 | persist := false 7 | debug := true 8 | _, err := NewPlot(0, persist, debug) 9 | if err == nil { 10 | t.Error("Expected error when making a 0 dimensional plot.") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pointgroup.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // A PointGroup refers to a set of points that need to plotted. 8 | // It could either be a set of points or a function of co-ordinates. 9 | // For Example z = Function(x,y)(3 Dimensional) or y = Function(x) (2-Dimensional) 10 | type PointGroup struct { 11 | name string // Name of the curve 12 | dimensions int // dimensions of the curve 13 | style string // current plotting style 14 | data interface{} // Data inside the curve in any integer/float format 15 | castedData interface{} // The data inside the curve typecasted to float64 16 | set bool // 17 | } 18 | 19 | // AddPointGroup function adds a group of points to a plot. 20 | // 21 | // Usage 22 | // dimensions := 2 23 | // persist := false 24 | // debug := false 25 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 26 | // plot.AddPointGroup("Sample1", "points", []int32{51, 8, 4, 11}) 27 | // plot.AddPointGroup("Sample2", "points", []int32{1, 2, 4, 11}) 28 | // plot.SavePlot("1.png") 29 | func (plot *Plot) AddPointGroup(name string, style string, data interface{}) (err error) { 30 | _, exists := plot.PointGroup[name] 31 | if exists { 32 | return &gnuplotError{fmt.Sprintf("A PointGroup with the name %s already exists, please use another name of the curve or remove this curve before using another one with the same name.", name)} 33 | } 34 | 35 | curve := &PointGroup{name: name, dimensions: plot.dimensions, data: data, set: true} 36 | allowed := []string{ 37 | "lines", "points", "linepoints", 38 | "impulses", "dots", "bar", 39 | "steps", "fill solid", "histogram", "circle", 40 | "errorbars", "boxerrorbars", 41 | "boxes", "lp"} 42 | curve.style = defaultStyle 43 | discovered := 0 44 | for _, s := range allowed { 45 | if s == style { 46 | curve.style = style 47 | err = nil 48 | discovered = 1 49 | } 50 | } 51 | switch data.(type) { 52 | case [][]float64: 53 | if plot.dimensions != len(data.([][]float64)) { 54 | return &gnuplotError{fmt.Sprintf("The dimensions of this PointGroup are not compatible with the dimensions of the plot.\nIf you want to make a 2-d curve you must specify a 2-d plot.")} 55 | } 56 | curve.castedData = data.([][]float64) 57 | if plot.dimensions == 2 { 58 | plot.plotXY(curve) 59 | } else { 60 | plot.plotXYZ(curve) 61 | } 62 | plot.PointGroup[name] = curve 63 | 64 | case [][]float32: 65 | if plot.dimensions != len(data.([][]float32)) { 66 | return &gnuplotError{fmt.Sprintf("The dimensions of this PointGroup are not compatible with the dimensions of the plot.\nIf you want to make a 2-d curve you must specify a 2-d plot.")} 67 | } 68 | originalSlice := data.([][]float32) 69 | typeCasteSlice := make([][]float64, len(originalSlice)) 70 | for i := 0; i < len(originalSlice); i++ { 71 | typeCasteSlice[i] = make([]float64, len(originalSlice[i])) 72 | for j := 0; j < len(originalSlice[i]); j++ { 73 | typeCasteSlice[i][j] = float64(originalSlice[i][j]) 74 | } 75 | } 76 | curve.castedData = typeCasteSlice 77 | if plot.dimensions == 2 { 78 | plot.plotXY(curve) 79 | } else { 80 | plot.plotXYZ(curve) 81 | } 82 | plot.PointGroup[name] = curve 83 | 84 | case [][]int: 85 | if plot.dimensions != len(data.([][]int)) { 86 | return &gnuplotError{fmt.Sprintf("The dimensions of this PointGroup are not compatible with the dimensions of the plot.\nIf you want to make a 2-d curve you must specify a 2-d plot.")} 87 | } 88 | originalSlice := data.([][]int) 89 | if len(originalSlice) != 2 { 90 | return &gnuplotError{fmt.Sprintf("this is not a 2d matrix")} 91 | } 92 | typeCasteSlice := make([][]float64, len(originalSlice)) 93 | for i := 0; i < len(originalSlice); i++ { 94 | typeCasteSlice[i] = make([]float64, len(originalSlice[i])) 95 | for j := 0; j < len(originalSlice[i]); j++ { 96 | typeCasteSlice[i][j] = float64(originalSlice[i][j]) 97 | } 98 | } 99 | curve.castedData = typeCasteSlice 100 | if plot.dimensions == 2 { 101 | plot.plotXY(curve) 102 | } else { 103 | plot.plotXYZ(curve) 104 | } 105 | plot.PointGroup[name] = curve 106 | 107 | case [][]int8: 108 | if plot.dimensions != len(data.([][]int8)) { 109 | return &gnuplotError{fmt.Sprintf("The dimensions of this PointGroup are not compatible with the dimensions of the plot.\nIf you want to make a 2-d curve you must specify a 2-d plot.")} 110 | } 111 | originalSlice := data.([][]int8) 112 | if len(originalSlice) != 2 { 113 | return &gnuplotError{fmt.Sprintf("this is not a 2d matrix")} 114 | } 115 | typeCasteSlice := make([][]float64, len(originalSlice)) 116 | for i := 0; i < len(originalSlice); i++ { 117 | typeCasteSlice[i] = make([]float64, len(originalSlice[i])) 118 | for j := 0; j < len(originalSlice[i]); j++ { 119 | typeCasteSlice[i][j] = float64(originalSlice[i][j]) 120 | } 121 | } 122 | curve.castedData = typeCasteSlice 123 | 124 | if plot.dimensions == 2 { 125 | plot.plotXY(curve) 126 | } else { 127 | plot.plotXYZ(curve) 128 | } 129 | plot.PointGroup[name] = curve 130 | 131 | case [][]int16: 132 | if plot.dimensions != len(data.([][]int16)) { 133 | return &gnuplotError{fmt.Sprintf("The dimensions of this PointGroup are not compatible with the dimensions of the plot.\nIf you want to make a 2-d curve you must specify a 2-d plot.")} 134 | } 135 | originalSlice := data.([][]int16) 136 | if len(originalSlice) != 2 { 137 | return &gnuplotError{fmt.Sprintf("this is not a 2d matrix")} 138 | } 139 | typeCasteSlice := make([][]float64, len(originalSlice)) 140 | for i := 0; i < len(originalSlice); i++ { 141 | typeCasteSlice[i] = make([]float64, len(originalSlice[i])) 142 | for j := 0; j < len(originalSlice[i]); j++ { 143 | typeCasteSlice[i][j] = float64(originalSlice[i][j]) 144 | } 145 | } 146 | curve.castedData = typeCasteSlice 147 | 148 | if plot.dimensions == 2 { 149 | plot.plotXY(curve) 150 | } else { 151 | plot.plotXYZ(curve) 152 | } 153 | plot.PointGroup[name] = curve 154 | 155 | case [][]int32: 156 | if plot.dimensions != len(data.([][]int32)) { 157 | return &gnuplotError{fmt.Sprintf("The dimensions of this PointGroup are not compatible with the dimensions of the plot.\nIf you want to make a 2-d curve you must specify a 2-d plot.")} 158 | } 159 | originalSlice := data.([][]int32) 160 | if len(originalSlice) != 2 { 161 | return &gnuplotError{fmt.Sprintf("this is not a 2d matrix")} 162 | } 163 | typeCasteSlice := make([][]float64, len(originalSlice)) 164 | for i := 0; i < len(originalSlice); i++ { 165 | typeCasteSlice[i] = make([]float64, len(originalSlice[i])) 166 | for j := 0; j < len(originalSlice[i]); j++ { 167 | typeCasteSlice[i][j] = float64(originalSlice[i][j]) 168 | } 169 | } 170 | curve.castedData = typeCasteSlice 171 | 172 | if plot.dimensions == 2 { 173 | plot.plotXY(curve) 174 | } else { 175 | plot.plotXYZ(curve) 176 | } 177 | plot.PointGroup[name] = curve 178 | 179 | case [][]int64: 180 | if plot.dimensions != len(data.([][]int64)) { 181 | return &gnuplotError{fmt.Sprintf("The dimensions of this PointGroup are not compatible with the dimensions of the plot.\nIf you want to make a 2-d curve you must specify a 2-d plot.")} 182 | } 183 | originalSlice := data.([][]int64) 184 | if len(originalSlice) != 2 { 185 | return &gnuplotError{fmt.Sprintf("this is not a 2d matrix")} 186 | } 187 | typeCasteSlice := make([][]float64, len(originalSlice)) 188 | for i := 0; i < len(originalSlice); i++ { 189 | typeCasteSlice[i] = make([]float64, len(originalSlice[i])) 190 | for j := 0; j < len(originalSlice[i]); j++ { 191 | typeCasteSlice[i][j] = float64(originalSlice[i][j]) 192 | } 193 | } 194 | curve.castedData = typeCasteSlice 195 | 196 | if plot.dimensions == 2 { 197 | plot.plotXY(curve) 198 | } else { 199 | plot.plotXYZ(curve) 200 | } 201 | plot.PointGroup[name] = curve 202 | 203 | case []float64: 204 | curve.castedData = data.([]float64) 205 | plot.plotX(curve) 206 | plot.PointGroup[name] = curve 207 | case []float32: 208 | originalSlice := data.([]float32) 209 | typeCasteSlice := make([]float64, len(originalSlice)) 210 | for i := 0; i < len(originalSlice); i++ { 211 | typeCasteSlice[i] = float64(originalSlice[i]) 212 | } 213 | curve.castedData = typeCasteSlice 214 | plot.plotX(curve) 215 | plot.PointGroup[name] = curve 216 | case []int: 217 | originalSlice := data.([]int) 218 | typeCasteSlice := make([]float64, len(originalSlice)) 219 | for i := 0; i < len(originalSlice); i++ { 220 | typeCasteSlice[i] = float64(originalSlice[i]) 221 | } 222 | curve.castedData = typeCasteSlice 223 | plot.plotX(curve) 224 | plot.PointGroup[name] = curve 225 | case []int8: 226 | originalSlice := data.([]int8) 227 | typeCasteSlice := make([]float64, len(originalSlice)) 228 | for i := 0; i < len(originalSlice); i++ { 229 | typeCasteSlice[i] = float64(originalSlice[i]) 230 | } 231 | curve.castedData = typeCasteSlice 232 | plot.plotX(curve) 233 | plot.PointGroup[name] = curve 234 | case []int16: 235 | originalSlice := data.([]int16) 236 | typeCasteSlice := make([]float64, len(originalSlice)) 237 | for i := 0; i < len(originalSlice); i++ { 238 | typeCasteSlice[i] = float64(originalSlice[i]) 239 | } 240 | curve.castedData = typeCasteSlice 241 | plot.plotX(curve) 242 | plot.PointGroup[name] = curve 243 | case []int32: 244 | originalSlice := data.([]int32) 245 | typeCasteSlice := make([]float64, len(originalSlice)) 246 | for i := 0; i < len(originalSlice); i++ { 247 | typeCasteSlice[i] = float64(originalSlice[i]) 248 | } 249 | curve.castedData = typeCasteSlice 250 | plot.plotX(curve) 251 | plot.PointGroup[name] = curve 252 | case []int64: 253 | originalSlice := data.([]int64) 254 | typeCasteSlice := make([]float64, len(originalSlice)) 255 | for i := 0; i < len(originalSlice); i++ { 256 | typeCasteSlice[i] = float64(originalSlice[i]) 257 | } 258 | curve.castedData = typeCasteSlice 259 | plot.plotX(curve) 260 | plot.PointGroup[name] = curve 261 | default: 262 | return &gnuplotError{fmt.Sprintf("invalid number of dims ")} 263 | 264 | } 265 | if discovered == 0 { 266 | fmt.Printf("** style '%v' not in allowed list %v\n", style, allowed) 267 | fmt.Printf("** default to 'points'\n") 268 | err = &gnuplotError{fmt.Sprintf("invalid style '%s'", style)} 269 | } 270 | return err 271 | } 272 | 273 | // RemovePointGroup helps to remove a particular point group from the plot. 274 | // This way you can remove a pointgroup if it's un-necessary. 275 | // 276 | // Usage 277 | // dimensions := 3 278 | // persist := false 279 | // debug := false 280 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 281 | // plot.AddPointGroup("Sample1", "points", []int32{51, 8, 4, 11}) 282 | // plot.AddPointGroup("Sample2", "points", []int32{1, 2, 4, 11}) 283 | // plot.RemovePointGroup("Sample1") 284 | func (plot *Plot) RemovePointGroup(name string) { 285 | delete(plot.PointGroup, name) 286 | plot.cleanplot() 287 | for _, pointGroup := range plot.PointGroup { 288 | plot.plotX(pointGroup) 289 | } 290 | } 291 | 292 | // ResetPointGroupStyle helps to reset the style of a particular point group in a plot. 293 | // Using both AddPointGroup and RemovePointGroup you can add or remove point groups. 294 | // And dynamically change the plots. 295 | // 296 | // Usage 297 | // dimensions := 2 298 | // persist := false 299 | // debug := false 300 | // plot, _ := glot.NewPlot(dimensions, persist, debug) 301 | // plot.AddPointGroup("Sample1", "points", []int32{51, 8, 4, 11}) 302 | // plot.ResetPointGroupStyle("Sample1", "points") 303 | func (plot *Plot) ResetPointGroupStyle(name string, style string) (err error) { 304 | pointGroup, exists := plot.PointGroup[name] 305 | if !exists { 306 | return &gnuplotError{fmt.Sprintf("A curve with name %s does not exist.", name)} 307 | } 308 | plot.RemovePointGroup(name) 309 | pointGroup.style = style 310 | plot.plotX(pointGroup) 311 | return err 312 | } 313 | -------------------------------------------------------------------------------- /pointgroup_test.go: -------------------------------------------------------------------------------- 1 | package glot 2 | 3 | import "testing" 4 | 5 | func TestResetPointGroupStyle(t *testing.T) { 6 | dimensions := 2 7 | persist := false 8 | debug := false 9 | plot, _ := NewPlot(dimensions, persist, debug) 10 | plot.AddPointGroup("Sample1", "points", []int32{51, 8, 4, 11}) 11 | err := plot.ResetPointGroupStyle("Sam", "lines") 12 | if err == nil { 13 | t.Error("The specified pointgroup to be reset does not exist") 14 | } 15 | } 16 | --------------------------------------------------------------------------------