Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/gonumplot #272

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions engine/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,16 @@ m = {{.Data.Configs | .SelectArray "mselect" "Uniform"}} {{.TextBox "margs" "(1,



{{.Data.Div "gnuplot"}}
{{.Data.Div "plot"}}

<p title="{{$.Data.Doc "TableAutoSave"}}">
TableAutosave: {{.TextBox "tableAutoSave" "0" }} s
</p>
Plot of "table.txt", provided table is being autosaved and gnuplot installed.<br>
<b>plot "table.txt" using {{.TextBox "usingx" "1"}} : {{.TextBox "usingy" "2"}} with lines </b><br/>
<p class=ErrorBox>{{.Span "plotErr" ""}}</p>
{{.Img "plot" "/plot/"}}
<p>
Plot columns {{.TextBox "usingx" "0" "size=1"}} and {{.TextBox "usingy" "1" "size=1"}} of "table.txt".
</p>
<p class=ErrorBox>{{.Span "plotErr" ""}}</p>
{{.Img "plot" "/plot/"}}

</div>

Expand Down
201 changes: 145 additions & 56 deletions engine/plot.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,99 +3,188 @@ package engine
import (
"bytes"
"errors"
"fmt"
"github.com/mumax/3/httpfs"
"image"
"image/color"
"image/png"
"io/ioutil"
"io"
"net/http"
"os/exec"
"sync/atomic"
"strconv"
"strings"
"sync"
"time"

"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
)

var nPlots int32 // counts number of active gnuplot processes
const MAX_GNUPLOTS = 5 // maximum allowed number of gnuplot processes
const DefaultCacheLifetime = 1 * time.Second

func (g *guistate) servePlot(w http.ResponseWriter, r *http.Request) {
var guiplot *TablePlot

// TablePlot is a WriterTo which writes a (cached) plot image of table data.
// The internally cached image will be updated when the column indices have been changed,
// or when the cache lifetime is exceeded.
// If another GO routine is updating the image, the cached image will be written.
type TablePlot struct {
lock sync.Mutex
updating bool

out := []byte{}
table *DataTable
xcol, ycol int

// handle error and return wheter err != nil.
handle := func(err error) bool {
if err != nil {
w.Write(emptyIMG())
g.Set("plotErr", err.Error()+string(out))
return true
} else {
return false
}
cache struct {
img []byte // cached output
err error // cached error
expirytime time.Time // expiration time of the cache
lifetime time.Duration // maximum lifetime of the cache
}
}

// limit max processes
atomic.AddInt32(&nPlots, 1)
defer atomic.AddInt32(&nPlots, -1)
if atomic.LoadInt32(&nPlots) > MAX_GNUPLOTS {
handle(errors.New("too many gnuplot processes"))
return
func NewPlot(table *DataTable) (p *TablePlot) {
p = &TablePlot{table: table, xcol: 0, ycol: 1}
p.cache.lifetime = DefaultCacheLifetime
return
}

func (p *TablePlot) SelectDataColumns(xcolidx, ycolidx int) {
p.lock.Lock()
defer p.lock.Unlock()
if xcolidx != p.xcol || ycolidx != p.ycol {
p.xcol, p.ycol = xcolidx, ycolidx
p.cache.expirytime = time.Time{} // this will trigger an update at the next write
}
}

a := g.StringValue("usingx")
b := g.StringValue("usingy")
func (p *TablePlot) WriteTo(w io.Writer) (int64, error) {
p.update()

cmd := "gnuplot"
args := []string{"-e", fmt.Sprintf(`set format x "%%g"; set key off; set format y "%%g"; set term svg size 480,320 font 'Arial,10'; plot "-" u %v:%v w li; set output;exit;`, a, b)}
excmd := exec.Command(cmd, args...)
p.lock.Lock()
defer p.lock.Unlock()

stdin, err := excmd.StdinPipe()
if handle(err) {
if p.cache.err != nil {
return 0, p.cache.err
}
nBytes, err := w.Write(p.cache.img)
return int64(nBytes), err
}

// Updates the cached image if the cache is expired
// Does nothing if the image is already being updated by another GO process
func (p *TablePlot) update() {
p.lock.Lock()
xcol, ycol := p.xcol, p.ycol
needupdate := !p.updating && time.Now().After(p.cache.expirytime)
p.updating = p.updating || needupdate
p.lock.Unlock()

if !needupdate {
return
}

stdout, err := excmd.StdoutPipe()
if handle(err) {
// create plot without the TablePlot being locked!
img, err := CreatePlot(p.table, xcol, ycol)

p.lock.Lock()
p.cache.img, p.cache.err = img, err
p.updating = false
if p.xcol == xcol && p.ycol == ycol {
p.cache.expirytime = time.Now().Add(p.cache.lifetime)
} else { // column indices have been changed during the update
p.cache.expirytime = time.Time{}
}
p.lock.Unlock()
}

// Returns a png image plot of table data
func CreatePlot(table *DataTable, xcol, ycol int) (img []byte, err error) {
if table == nil {
err = errors.New("DataTable pointer is nil")
return
}

data, err := httpfs.Read(fmt.Sprintf(`%vtable.txt`, OD()))
if handle(err) {
data, err := table.Read()
if err != nil {
return
}

err = excmd.Start()
if handle(err) {
header := table.Header()

if !(xcol >= 0 && xcol < len(header) && ycol >= 0 && ycol < len(header)) {
err = errors.New("Invalid column index")
return
}
defer excmd.Wait()

_, err = stdin.Write(data)
if handle(err) {
pl, err := plot.New()
if err != nil {
return
}
err = stdin.Close()
if handle(err) {

pl.X.Label.Text = header[xcol].Name()
if unit := header[xcol].Unit(); unit != "" {
pl.X.Label.Text += " (" + unit + ")"
}
pl.Y.Label.Text = header[ycol].Name()
if unit := header[ycol].Unit(); unit != "" {
pl.Y.Label.Text += " (" + unit + ")"
}

pl.X.Label.Font.SetName("Helvetica")
pl.Y.Label.Font.SetName("Helvetica")
pl.X.Label.Padding = 0.2 * vg.Inch
pl.Y.Label.Padding = 0.2 * vg.Inch

points := make(plotter.XYs, len(data))
for i := 0; i < len(data); i++ {
points[i].X = data[i][xcol]
points[i].Y = data[i][ycol]
}

scatter, err := plotter.NewScatter(points)
if err != nil {
return
}
scatter.Color = color.RGBA{R: 255, G: 0, B: 0, A: 255}
scatter.Shape = draw.CircleGlyph{}
scatter.Radius = 1
pl.Add(scatter)

out, err = ioutil.ReadAll(stdout)
if handle(err) {
wr, err := pl.WriterTo(8*vg.Inch, 4*vg.Inch, "png")
if err != nil {
return
}

w.Header().Set("Content-Type", "image/svg+xml")
w.Write(out)
g.Set("plotErr", "")
return
buf := bytes.NewBuffer(nil)
_, err = wr.WriteTo(buf)

if err != nil {
return nil, err
} else {
return buf.Bytes(), nil
}
}

var empty_img []byte
func (g *guistate) servePlot(w http.ResponseWriter, r *http.Request) {
if guiplot == nil {
guiplot = NewPlot(&Table)
}

xcol, errx := strconv.Atoi(strings.TrimSpace(g.StringValue("usingx")))
ycol, erry := strconv.Atoi(strings.TrimSpace(g.StringValue("usingy")))
if errx != nil || erry != nil {
guiplot.SelectDataColumns(-1, -1) // set explicitly invalid column indices
} else {
guiplot.SelectDataColumns(xcol, ycol)
}

w.Header().Set("Content-Type", "image/png")
_, err := guiplot.WriteTo(w)

// empty image to show if there's no plot...
func emptyIMG() []byte {
if empty_img == nil {
o := bytes.NewBuffer(nil)
png.Encode(o, image.NewNRGBA(image.Rect(0, 0, 4, 4)))
empty_img = o.Bytes()
if err != nil {
png.Encode(w, image.NewNRGBA(image.Rect(0, 0, 4, 4)))
g.Set("plotErr", "Plot Error: "+err.Error())
} else {
g.Set("plotErr", "")
}
return empty_img
}
Loading