From 7bfb11c3e2c8dcc5f2acbb73d40e8a991ee2e661 Mon Sep 17 00:00:00 2001 From: Dorival Pedroso Date: Sun, 15 Oct 2017 18:48:24 +1000 Subject: [PATCH] Implement PlotXY widget to draw line charts --- examples/xygraph-simple.go | 13 + examples/xygraph.go | 93 +++++ plotargs.go | 185 +++++++++ plotxy.go | 804 +++++++++++++++++++++++++++++++++++++ util.go | 32 ++ 5 files changed, 1127 insertions(+) create mode 100644 examples/xygraph-simple.go create mode 100644 examples/xygraph.go create mode 100644 plotargs.go create mode 100644 plotxy.go diff --git a/examples/xygraph-simple.go b/examples/xygraph-simple.go new file mode 100644 index 0000000..db10c43 --- /dev/null +++ b/examples/xygraph-simple.go @@ -0,0 +1,13 @@ +package main + +import "github.com/fogleman/gg" + +func main() { + p := gg.NewPlotXY() + p.Title = "my graph" + c := p.AddCurve("data", []float64{1, 2, 3, 4}, []float64{1, 4, 9, 16}) + c.M = "o" + dc := gg.NewContext(400, 300) + p.Render(dc) + dc.SavePNG("/tmp/figure-simple.png") +} diff --git a/examples/xygraph.go b/examples/xygraph.go new file mode 100644 index 0000000..f640c1b --- /dev/null +++ b/examples/xygraph.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + + "github.com/fogleman/gg" +) + +func main() { + + // plot + p := gg.NewPlotXY() + p.LegNrow = 2 + p.LegAtBottom = false + + // x-values [-1, +1] + npts := 21 + x := make([]float64, npts) + for i := 0; i < npts; i++ { + x[i] = -1 + 2*float64(i)/float64(npts-1) + } + + // curve 1 + y1 := make([]float64, npts) + for i := 0; i < npts; i++ { + y1[i] = x[i] + } + c1 := p.AddCurve("y = x", x, y1) + c1.M = "o" + c1.C = "#000" + c1.Mec = "#f00" + + // curve 2 + y2 := make([]float64, npts) + for i := 0; i < npts; i++ { + y2[i] = 2 * x[i] + } + c2 := p.AddCurve("y = 2x", x, y2) + c2.M = "o" + c2.Void = true + + // curve 3 + y3 := make([]float64, npts) + for i := 0; i < npts; i++ { + y3[i] = x[i] * x[i] + } + c3 := p.AddCurve("y = x²", x, y3) + c3.M = "s" + + // curve 4 + y4 := make([]float64, npts) + for i := 0; i < npts; i++ { + y4[i] = 2 * x[i] * x[i] + } + c4 := p.AddCurve("y = 2x²", x, y4) + c4.M = "s" + c4.Void = true + + // curve 5 + y5 := make([]float64, npts) + for i := 0; i < npts; i++ { + y5[i] = -x[i] + } + c5 := p.AddCurve("y = -x", x, y5) + c5.M = "+" + + // curve 6 + y6 := make([]float64, npts) + for i := 0; i < npts; i++ { + y6[i] = -2 * x[i] + } + c6 := p.AddCurve("y = -2x", x, y6) + c6.M = "x" + + // curve 7 + y7 := make([]float64, npts) + c7 := p.AddCurve("gopher", y7, x) + c7.M = "img:examples/gopher30.png" + c7.Me = 5 + c7.Ls = "none" + + // render graph + height := 400 + if p.LegAtBottom { + height = 500 + } + dc := gg.NewContext(500, height) + p.Render(dc) + + // save + dc.SavePNG("/tmp/figure.png") + fmt.Printf("file written\n") +} diff --git a/plotargs.go b/plotargs.go new file mode 100644 index 0000000..936858d --- /dev/null +++ b/plotargs.go @@ -0,0 +1,185 @@ +package gg + +import ( + "image" + "strings" +) + +// PlotArgs holds the arguments for drawing line graphs (charts) +type PlotArgs struct { + + // curve name + L string // label + + // lines + C string // line: color + A float64 // line: alpha (0, 1]. A<1e-14 => A=1.0 + Ls string // line: style + Lw float64 // line: width + + // markers + M string // marker: type, e.g. "o", "s", "+", "x", "img:filename.png" + Ms int // marker: size + Me int // marker: mark-every + Mec string // marker: edge color + Mew float64 // marker: edge width + Void bool // marker: void marker (draw edge only) + + // internal + markerImg image.Image // marker image + markerName string // filename corresponding to loaded image +} + +// DrawMarker draws marker +func (o *PlotArgs) DrawMarker(dc *Context, x, y int) { + + // skip if marker type is empty + if o.M == "" { + return + } + + // markersize and half-markersize + s := o.markerSize() + h := s / 2 + + // draw marker + switch o.M { + + // circle + case "o": + if !o.Void { + o.Circle(dc, true, false, x, y, h) + } + o.Circle(dc, true, true, x, y, h) + + // square + case "s": + if !o.Void { + o.Rect(dc, true, false, x-h, y-h, s, s) + } + o.Rect(dc, true, true, x-h, y-h, s, s) + + // cross + case "+": + o.Line(dc, true, true, x-h, y, x+h, y) + o.Line(dc, true, true, x, y-h, x, y+h) + + // x + case "x": + o.Line(dc, true, true, x-h, y-h, x+h, y+h) + o.Line(dc, true, true, x-h, y+h, x+h, y-h) + + // use image as marker + default: + if o.checkMarkerImage() { + dc.DrawImageAnchored(o.markerImg, x, y, 0.5, 0.5) + } + } +} + +// Activate activates properties of lines/shapes +func (o *PlotArgs) Activate(dc *Context, marker, edge bool) { + + // color + clr := o.C + if marker && edge && o.Mec != "" { + clr = o.Mec + } + r, g, b, _ := parseHexColor(clr) + + // alpha + alpha := o.A + if alpha < 1e-14 || marker { + alpha = 1.0 + } + + // set color + dc.SetRGBA255(r, g, b, int(alpha*255)) +} + +// Circle draws Circle +func (o *PlotArgs) Circle(dc *Context, marker, edge bool, x, y, r int) { + o.Activate(dc, marker, edge) + dc.DrawCircle(float64(x), float64(y), float64(r)) + if edge { + dc.Stroke() + } else { + dc.Fill() + } +} + +// Rect draws rectangle +func (o *PlotArgs) Rect(dc *Context, marker, edge bool, x, y, w, h int) { + o.Activate(dc, marker, edge) + dc.DrawRectangle(float64(x), float64(y), float64(w), float64(h)) + if edge { + dc.Stroke() + } else { + dc.Fill() + } +} + +// Line draws Line +func (o *PlotArgs) Line(dc *Context, marker, edge bool, x1, y1, x2, y2 int) { + o.Activate(dc, marker, edge) + dc.DrawLine(float64(x1), float64(y1), float64(x2), float64(y2)) + if edge { + dc.Stroke() + } else { + dc.Fill() + } +} + +// checkMarkerImage loads marker image if not loaded already +func (o *PlotArgs) checkMarkerImage() (useImage bool) { + useImage = strings.HasPrefix(o.M, "img:") + if useImage { + fn := strings.TrimPrefix(o.M, "img:") + if o.markerImg == nil || o.markerName != fn { // load only if not loaded already + var err error + o.markerImg, err = LoadPNG(fn) + if err != nil { + panic(err) + } + o.markerName = fn + } + } + return +} + +// markerSize returns the size of marker +func (o *PlotArgs) markerSize() int { + if o.checkMarkerImage() { + return o.markerImg.Bounds().Dy() + } + if o.Ms == 0 { + return 8 // default value + } + return o.Ms +} + +// colors ///////////////////////////////////////////////////////////////////////////////////////// + +// GetColor returns a color from a default palette +// use palette < 0 for automatic color +func GetColor(i, palette int) string { + if palette < 0 || palette >= len(palettes) { + return "" + } + p := palettes[palette] + return p[i%len(p)] +} + +// palettes holds color palettes +var palettes = [][]string{ + {"#003fff", "#35b052", "#e8000b", "#8a2be2", "#ffc400", "#00d7ff"}, + {"blue", "green", "magenta", "orange", "red", "cyan", "black", "#de9700", "#89009d", "#7ad473", "#737ad4", "#d473ce", "#7e6322", "#462222", "#98ac9d", "#37a3e8", "yellow"}, + {"#4c72b0", "#55a868", "#c44e52", "#8172b2", "#ccb974", "#64b5cd"}, + {"#9b59b6", "#3498db", "#95a5a6", "#e74c3c", "#34495e", "#2ecc71"}, + {"#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33"}, + {"#7fc97f", "#beaed4", "#fdc086", "#ffff99", "#386cb0", "#f0027f", "#bf5b17"}, + {"#001c7f", "#017517", "#8c0900", "#7600a1", "#b8860b", "#006374"}, + {"#0072b2", "#009e73", "#d55e00", "#cc79a7", "#f0e442", "#56b4e9"}, + {"#4878cf", "#6acc65", "#d65f5f", "#b47cc7", "#c4ad66", "#77bedb"}, + {"#92c6ff", "#97f0aa", "#ff9f9a", "#d0bbff", "#fffea3", "#b0e0e6"}, +} diff --git a/plotxy.go b/plotxy.go new file mode 100644 index 0000000..7443b38 --- /dev/null +++ b/plotxy.go @@ -0,0 +1,804 @@ +package gg + +import ( + "fmt" + "math" + "strconv" + + "github.com/golang/freetype/truetype" + "golang.org/x/image/font/gofont/gomono" + "golang.org/x/image/font/gofont/goregular" +) + +// PlotXY draws line graphs for given X-Y values +// +// |←——————————————————— W ————————————————————→| +// |←——————————— ww ——————————→| +// |←—————— w ——————→| +// (0,0) +// ——————————— ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ——→ xScr +// ↑ ┃ | ' ┃ +// | ┃ (x0,y0) ' TR ┃ +// | ———————— ┃ ┌───────────────────────────┐ ——————— ┃ ——→ x +// | ↑ ┃ │ (p0,q0) ' DV │ ↑ ┃ +// | | ——— ┃ │ o─────────────────┐ │ | ┃ ——→ p +// | | ↑ ┃ │ │ │ │ | ┃ +// | ┃ —LR— │-DH-│ │-DH-│-RR-|-RL-┃ +// hh h ┃ │ │ │ │ | ┃ +// H ┃ │ ↑ yReal │ │ | ┃ +// | ↓ ┃ │ │ │ │ | ┃ +// | | ——— ┃ │ ●─────────────────o │ | ┃ ——→ xReal +// | ↓ ┃ │ ' DV (pf,qf) │ ↓ ┃ +// | ——————— ┃ ———— └───────────────────────────┘ ——————— ┃ +// | ┃ ' BR (xf,yf) ┃ +// ↓ ┃ ' BL ┃ +// ——————————— ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +// | | | (W,H) +// ↓ ↓ ↓ +// yScr y q +// +// W: figure width ww: plot area width w: x-y area width +// H: figure height hh: plot area height h: x-y area height +// +// LR : Left Ruler TR : Top Ruler xScr ,yScr : "screen" coordinates +// BR : Bottom Ruler RR : Right Ruler x ,y : plot area coordinates +// BL : Bottom Legend RL : Right Legend xReal,yReal: x-y (real data) coords +// DH : Delta Horizontal DV : Delta Vertical +// +type PlotXY struct { + + // title and labels + Title string // Title + Xlabel string // XLabel + Ylabel string // YLabel + + // options + EqualScale bool // equal scale factors + DrawGrid bool // draw grid + DrawBorders bool // draw borders + DeltaH int // DH increment + DeltaV int // DV increment + + // legend + LegendOn bool // legend is on + LegAtBottom bool // legend at bottom + LegLineLen int // length of legend line indicator + LegGap int // legend: gap between icons + LegTxtGap int // legend: gap between line and text + LegNrow int // legend: number of rows + + // ticks + NumTicksX int // number of x-ticks + NumTicksY int // number of y-ticks + TicksFormat string // format of ticks numbers + TicksNumDigits int // number of digits of ticks + TicksLength int // length of tick lines + + // styles + StyleFG *PlotArgs // style: foreground + StyleFR *PlotArgs // style: frame + StylePL *PlotArgs // style: plot area + StyleGD *PlotArgs // style: grid + StyleBR *PlotArgs // style: bottom ruler + StyleLR *PlotArgs // style: left ruler + StyleTR *PlotArgs // style: top ruler + StyleRR *PlotArgs // style: right ruler + + // font typefaces + FontTitle *truetype.Font // font for title text + FontTicks *truetype.Font // font for ticks text + FontLabel *truetype.Font // font for x-y labels + FontLegend *truetype.Font // font for legend text + + // font sizes + FsizeTitle int // font size of title text + FsizeTicks int // font size of ticks text + FsizeLabels int // font size of x-y labels + FsizeLegend int // font size of legend text + + // curves and ticks + dataX [][]float64 // all curves x-data + dataY [][]float64 // all curves y-data + curves []*PlotArgs // all curves properties + xticks []float64 // x-ticks + yticks []float64 // y-ticks + + // scale and positions + sfx float64 // x scale factor + sfy float64 // y scale factor + p0 int // x-origin of plotting area + q0 int // y-origin of plotting area + pf int // x-max of plotting area + qf int // y-max of plotting area + + // limits + xmin float64 // minimum x value (real coordinates) + ymin float64 // minimum y value (real coordinates) + xmax float64 // maximum x value (real coordinates) + ymax float64 // maximum y value (real coordinates) + xminFix float64 // fixed minimum x value (real coordinates) + yminFix float64 // fixed minimum y value (real coordinates) + xmaxFix float64 // fixed maximum x value (real coordinates) + ymaxFix float64 // fixed maximum y value (real coordinates) + xminFixOn bool // use or not xminFix + xmaxFixOn bool // use or not xmaxFix + yminFixOn bool // use or not yminFix + ymaxFixOn bool // use or not ymaxFix + + // constants + cteEps float64 // constant machine eps + cteSqEps float64 // constant sqrt(eps) + cteMin float64 // constant min float + cteMax float64 // constant max float +} + +// NewPlotXY creates a new PlotXY object +func NewPlotXY() (o *PlotXY) { + + // title and labels + o = new(PlotXY) + o.Title = "Plotting with PlotXY" + o.Xlabel = "x" + o.Ylabel = "y" + + // options + o.EqualScale = false + o.DrawGrid = true + o.DrawBorders = true + o.DeltaH = 8 + o.DeltaV = 8 + + // legend + o.LegendOn = true + o.LegAtBottom = true + o.LegLineLen = 30 + o.LegGap = 10 + o.LegTxtGap = 4 + o.LegNrow = 1 + + // ticks + o.NumTicksX = 10 + o.NumTicksY = 10 + o.TicksFormat = "%g" + o.TicksNumDigits = 7 + o.TicksLength = 6 + + // styles + o.StyleFG = &PlotArgs{C: "#000"} // foreground + o.StyleFR = &PlotArgs{C: "#fff"} // frame + o.StylePL = &PlotArgs{C: "#faf7ec"} // plot area + o.StyleGD = &PlotArgs{C: "#b7b7b7"} // grid + o.StyleBR = &PlotArgs{C: "#e0e0e0"} // bottom ruler + o.StyleLR = &PlotArgs{C: "#e0e0e0"} // left ruler + o.StyleTR = &PlotArgs{C: "#e0e0e0"} // top ruler + o.StyleRR = &PlotArgs{C: "#e0e0e0"} // right ruler + + // fonts + var err1, err2, err3, err4 error + o.FontTitle, err1 = truetype.Parse(goregular.TTF) + o.FontTicks, err2 = truetype.Parse(gomono.TTF) + o.FontLabel, err3 = truetype.Parse(gomono.TTF) + o.FontLegend, err4 = truetype.Parse(gomono.TTF) + if err1 != nil || err2 != nil || err3 != nil || err4 != nil { + panic("cannot load fonts") + } + + // font sizes + o.FsizeTitle = 16 + o.FsizeTicks = 12 + o.FsizeLabels = 14 + o.FsizeLegend = 12 + + // constants + o.cteEps = math.Nextafter(1.0, 2.0) - 1.0 + o.cteSqEps = math.Sqrt(o.cteEps) + o.cteMin = math.SmallestNonzeroFloat64 + o.cteMax = math.MaxFloat64 + return +} + +// AddCurve adds curve to graph and returns the new curve properties +func (o *PlotXY) AddCurve(name string, x, y []float64) (curve *PlotArgs) { + + // check + if len(x) != len(y) { + panic("lengths of x and y must be the same") + } + + // add (real) coordinates and curve properties + ncurves := len(o.curves) + curve = &PlotArgs{L: name, C: GetColor(ncurves, 0)} + o.dataX = append(o.dataX, x) + o.dataY = append(o.dataY, y) + o.curves = append(o.curves, curve) + + // find limits + if ncurves == 0 { // first curve + if len(x) > 1 { + o.xmin = x[0] + o.xmax = x[0] + o.ymin = y[0] + o.ymax = y[0] + } else { + o.xmin = 0.0 + o.xmax = 1.0 + o.ymin = 0.0 + o.ymax = 1.0 + } + } + for i := 1; i < len(x); i++ { + o.xmin = min(o.xmin, x[i]) + o.xmax = max(o.xmax, x[i]) + o.ymin = min(o.ymin, y[i]) + o.ymax = max(o.ymax, y[i]) + } + return +} + +// SetMinX sets minimum x value. Use fixed=true to prevent automatic updates +func (o *PlotXY) SetMinX(value float64, fixed bool) { + o.xminFix = value + o.xminFixOn = fixed +} + +// SetMinY sets minimum y value. Use fixed=true to prevent automatic updates +func (o *PlotXY) SetMinY(value float64, fixed bool) { + o.yminFix = value + o.yminFixOn = fixed +} + +// SetMaxX sets maximum x value. Use fixed=true to prevent automatic updates +func (o *PlotXY) SetMaxX(value float64, fixed bool) { + o.xmaxFix = value + o.xmaxFixOn = fixed +} + +// SetMaxY sets maximum y value. Use fixed=true to prevent automatic updates +func (o *PlotXY) SetMaxY(value float64, fixed bool) { + o.ymaxFix = value + o.ymaxFixOn = fixed +} + +// Render draws PlotXY +func (o *PlotXY) Render(dc *Context) { + + // check number of curves + ncurves := len(o.curves) + if ncurves < 1 { + return + } + + // x-y limits + if o.xminFixOn { + o.xmin = o.xminFix + } + if o.xmaxFixOn { + o.xmax = o.xmaxFix + } + if o.yminFixOn { + o.ymin = o.yminFix + } + if o.ymaxFixOn { + o.ymax = o.ymaxFix + } + if math.Abs(o.xmax-o.xmin) <= o.cteEps { + o.xmin = o.xmin - 1 + o.xmax = o.xmax + 1 + } + if math.Abs(o.ymax-o.ymin) <= o.cteEps { + o.ymin = o.ymin - 1 + o.ymax = o.ymax + 1 + } + + // ticks values + bnumtck := o.NumTicksX + lnumtck := o.NumTicksY + if math.Abs(o.xmax-o.xmin) <= o.cteEps { + bnumtck = 3 + } + if math.Abs(o.ymax-o.ymin) <= o.cteEps { + lnumtck = 3 + } + o.xticks = o.pretty(o.xmin, o.xmax, bnumtck) + o.yticks = o.pretty(o.ymin, o.ymax, lnumtck) + + // bottom: tick text height + txt := o.fmtNum(o.TicksFormat, o.TicksNumDigits, o.xticks[0]) + _, tickH := o.measureTxt(dc, o.FontTicks, o.FsizeTicks, txt) + + // bottom: x-label text height + _, xlblH := o.measureTxt(dc, o.FontLabel, o.FsizeLabels, o.Xlabel) + + // left: tick text width + tickW := 0 + for _, value := range o.yticks { + txt = o.fmtNum(o.TicksFormat, o.TicksNumDigits, value) + tw, _ := o.measureTxt(dc, o.FontTicks, o.FsizeTicks, txt) + tickW = imax(tickW, tw) + } + + // left: y-label text width + ylblW, _ := o.measureTxt(dc, o.FontLabel, o.FsizeLabels, o.Ylabel) + + // legend dimensions + legTxtW := 0 // legend txt width + legTxtH := 0 // legend txt height + legH := 0 // legend total height + if o.LegendOn { + o.setFont(dc, o.FontLegend, o.FsizeLegend, "") + for _, curve := range o.curves { + tw, th := dc.MeasureString(curve.L) + legTxtW = imax(legTxtW, int(tw)) + legTxtH = imax(legTxtH, int(th)) + legTxtH = imax(legTxtH, curve.markerSize()) + } + legH = (2 + legTxtH) * o.LegNrow + } + + // height of scales + xscaleH := o.TicksLength + tickH + xlblH + 2 // height of x-scale (ticks + label) + yscaleW := o.TicksLength + tickW + ylblW + 2 // width of y-scale (ticks + label) + + // auxiliary variables + LR := 0 // Left ruler thickness (screen coordinates) + RR := 6 // Right ruler thickness (screen coordinates) + BR := 0 // Bottom ruler thickness (screen coordinates) + TR := 6 // Top ruler thickness (screen coordinates) + RL := 0 // right legend thickness + BL := 0 // bottom legend thickness + BR = imax(BR, xscaleH) + LR = imax(LR, yscaleW) + if o.LegAtBottom { + BR += legH + } else { + RR = o.LegLineLen + o.LegTxtGap + o.LegGap + legTxtW + 2 // width of legend "icon" + } + + // height of title + if o.Title != "" { + _, th := o.measureTxt(dc, o.FontTitle, o.FsizeTitle, o.Title) + TR = th + 12 + } + + // derived variables + W := dc.Width() + H := dc.Height() + ww := imax(1, W-(LR+RR+RL)) + hh := imax(1, H-(TR+BR+BL)) + w := imax(1, ww-2*o.DeltaH) + h := imax(1, hh-2*o.DeltaV) + x0 := LR + y0 := TR + o.p0 = x0 + o.DeltaH + o.q0 = y0 + o.DeltaV + xf := x0 + ww + yf := y0 + hh + o.pf = o.p0 + w + o.qf = o.q0 + h + + // scaling factors + o.sfx = float64(w) / (o.xmax - o.xmin) + o.sfy = float64(h) / (o.ymax - o.ymin) + if o.sfx <= o.cteEps { + o.sfx = 1.0 + } + if o.sfy <= o.cteEps { + o.sfy = 1.0 + } + if o.EqualScale { + sf := o.sfx + if o.sfx > o.sfy { + sf = o.sfy + } + o.sfx = sf + o.sfy = sf + } + + // draw background of plot-area + o.StylePL.Rect(dc, false, false, x0, y0, ww, hh) + + // draw grid + if o.DrawGrid { + + // vertical lines + for i := 0; i < len(o.xticks); i++ { + x := o.xScr(o.xticks[i]) + if x >= x0 && x <= xf { + o.StyleGD.Line(dc, false, true, x, y0, x, yf) + } + } + + // horizontal lines + for i := 0; i < len(o.yticks); i++ { + y := o.yScr(o.yticks[i]) + if y >= y0 && y <= yf { + o.StyleGD.Line(dc, false, true, x0, y, xf, y) + } + } + } + + // draw curves + for k, curve := range o.curves { + + // draw markers + if curve.M != "" { + idx := 0 + for i := 0; i < len(o.dataX[k]); i++ { + if i >= idx { + curve.DrawMarker(dc, o.xScr(o.dataX[k][i]), o.yScr(o.dataY[k][i])) + idx += curve.Me + } + } + } + + // draw lines + if curve.Ls != "none" { + if len(o.dataX[k]) > 1 { + curve.Activate(dc, false, true) + dc.MoveTo(float64(o.xScr(o.dataX[k][0])), float64(o.yScr(o.dataY[k][0]))) + for i := 0; i < len(o.dataX[k]); i++ { + dc.LineTo(float64(o.xScr(o.dataX[k][i])), float64(o.yScr(o.dataY[k][i]))) + } + dc.Stroke() + } + } + } + + // compute legend data, where the legend "icon" dimensions are: + // + // |← legLineLen →|← labelLen →| + // [gap][ line | txt ] example: ——x——Curve1 + // + hei := 2 + legTxtH // icon height + lll := o.LegLineLen // length of legend line + hll := lll / 2 // half length of legend line + xl := o.LegGap // initial x-coord on icon line + yl := yf + xscaleH + hei/2 // initial y-coord on icon line + col := 0 // column number + ncol := ncurves / o.LegNrow // number of columns + if ncurves%o.LegNrow > 0 { + ncol++ + } + if !o.LegAtBottom { + xl = xf + o.LegGap + yl = TR + o.LegGap + ncol = 1 + } + + // bottom ruler + if BR > 1 { + + // clear background + o.StyleBR.Rect(dc, false, false, 0, yf, W, imax(1, H-hh-TR)) + + // draw ticks and text + for _, x := range o.xticks { + xi := o.xScr(x) + if xi >= x0 && xi <= xf { + o.StyleFG.Line(dc, false, true, xi, yf, xi, yf+o.TicksLength) + txt = o.fmtNum(o.TicksFormat, o.TicksNumDigits, x) + o.text(dc, o.FontTicks, o.FsizeTicks, "", txt, xi, yf+o.TicksLength, 0.5, 1.0) + } + } + + // x-label + if o.Xlabel != "" { + xmid := (o.xScr(o.xmin) + o.xScr(o.xmax)) / 2 + ymid := yf + o.TicksLength + tickH + o.text(dc, o.FontLabel, o.FsizeLabels, "", o.Xlabel, xmid, ymid, 0.5, 1.0) + } + + // legend @ bottom side + // + // |← LegHlen →| + // [gap][ line |txt][gap][line|txt] ... ← yl + // ↑ ↑ + // x x + // + if o.LegAtBottom && o.LegendOn { + for _, curve := range o.curves { + + // icon={line,marker} and label + if curve.M != "" { + curve.DrawMarker(dc, xl+hll, yl) + } + if curve.Ls != "none" { + curve.Line(dc, false, true, xl, yl, xl+lll, yl) + } + if curve.L != "" { + xt := xl + lll + o.LegTxtGap + o.text(dc, o.FontLegend, o.FsizeLegend, "", curve.L, xt, yl, 0.0, 0.3) + } + + // update column position + tw := legTxtW + if o.LegNrow < 2 { + txtw, _ := dc.MeasureString(curve.L) + tw = int(txtw) + } + xl += lll + o.LegTxtGap + tw + o.LegGap + + // update row position + if o.LegNrow > 1 { + if col == ncol-1 { + col = -1 + xl = o.LegGap + yl += hei + } + col++ + } + } + } + } + + // left ruler + if LR > 1 { + + // clear background + o.StyleLR.Rect(dc, false, false, 0, 0, LR, hh+TR) + + // draw ticks and text + for _, y := range o.yticks { + yi := o.yScr(y) + if yi >= y0 && yi <= yf { + o.StyleFG.Line(dc, false, true, x0-o.TicksLength, yi, x0, yi) + txt = o.fmtNum(o.TicksFormat, o.TicksNumDigits, y) + o.text(dc, o.FontTicks, o.FsizeTicks, "", txt, x0-o.TicksLength-1, yi, 1.0, 0.4) + } + } + + // y-label + if o.Ylabel != "" { + xmid := x0 - o.TicksLength - tickW + ymid := (o.yScr(o.ymin) + o.yScr(o.ymax)) / 2 + o.text(dc, o.FontLabel, o.FsizeLabels, "", o.Ylabel, xmid, ymid, 1.0, 0.3) + } + } + + // top ruler + if TR > 1 { + + // clear background + o.StyleTR.Rect(dc, false, false, LR, 0, imax(1, W-LR), TR) + + // draw title + if o.Title != "" { + o.text(dc, o.FontTitle, o.FsizeTitle, "", o.Title, W/2, TR/2, 0.5, 0.5) + } + } + + // right ruler + if RR > 1 { + + // clear background + o.StyleRR.Rect(dc, false, false, xf, y0, imax(1, W-ww-LR), hh) + + // legend @ right side + // + // |← LegHlen →| + // [gap][ line |txt] ← yl + // [gap][ line |txt] + // ↑ + // xl + // + if !o.LegAtBottom && o.LegendOn { + for _, curve := range o.curves { + + // icon={line,marker} and label + if curve.M != "" { + curve.DrawMarker(dc, xl+hll, yl) + } + if curve.Ls != "none" { + curve.Line(dc, false, true, xl, yl, xl+lll, yl) + } + if curve.L != "" { + xt := xl + lll + o.LegTxtGap + o.text(dc, o.FontLegend, o.FsizeLegend, "", curve.L, xt, yl, 0.0, 0.3) + } + + // update row position + yl += hei + } + } + } + + // frame + if o.DrawBorders { + dc.SetRGB(0, 0, 0) + dc.DrawRectangle(float64(x0), float64(y0), float64(ww), float64(hh)) + dc.DrawRectangle(0, 0, float64(W), float64(H)) + dc.Stroke() + } +} + +// auxiliary //////////////////////////////////////////////////////////////////////////////////// + +// xScr converts real x-coords to to screen coordinates +func (o *PlotXY) xScr(x float64) int { + return o.p0 + int(o.sfx*(x-o.xmin)) +} + +// yScr converts real y-coords to to screen coordinates +func (o *PlotXY) yScr(y float64) int { + return o.qf - int(o.sfy*(y-o.ymin)) +} + +// text draws text +func (o *PlotXY) text(dc *Context, f *truetype.Font, size int, clr, txt string, x, y int, ax, ay float64) { + o.setFont(dc, f, size, clr) + dc.DrawStringAnchored(txt, float64(x), float64(y), ax, ay) +} + +// setFont sets font +func (o *PlotXY) setFont(dc *Context, f *truetype.Font, size int, clr string) { + if f == nil { + var err error + f, err = truetype.Parse(goregular.TTF) + if err != nil { + panic(err) + } + } + face := truetype.NewFace(f, &truetype.Options{ + Size: float64(size), + }) + dc.SetFontFace(face) + dc.SetHexColor("") +} + +// fmtNum formats number +func (o *PlotXY) fmtNum(format string, ndigits int, x float64) (l string) { + val := o.truncate(ndigits, x) + l = fmt.Sprintf(format, val) + return +} + +// truncate returns a truncated float +func (o *PlotXY) truncate(ndigits int, x float64) (val float64) { + s := fmt.Sprintf("%."+fmt.Sprintf("%d", ndigits)+"f", x) + val, err := strconv.ParseFloat(s, 64) + if err != nil { + panic(err) + } + return +} + +// measureTxt returns string measures in screen units +func (o *PlotXY) measureTxt(dc *Context, font *truetype.Font, fsz int, txt string) (w, h int) { + o.setFont(dc, font, fsz, "") + tw, th := dc.MeasureString(txt) + return int(tw), int(th) +} + +// pretty format //////////////////////////////////////////////////////////////////////////////// + +// compute pretty scale numbers +func (o *PlotXY) pretty(Lo, Hi float64, nDiv int) (vals []float64) { + + // constants + roundingEps := o.cteSqEps + epsCorrection := 0.0 + shrinkSml := 0.75 + h := 1.5 + h5 := 0.5 + 1.5*h + + // local variables + minN := int(int(nDiv) / int(3)) + lo := Lo + hi := Hi + dx := hi - lo + cell := 1.0 // cell := "scale" here + ub := 0.0 // upper bound on cell/unit + isml := true // is small ? + + // check range + if !(dx == 0 && hi == 0) { // hi=lo=0 + + cell := math.Abs(hi) + ub := 1.0 + 1.5/(1.0+h5) + ndiv := 1 + + if math.Abs(lo) > math.Abs(hi) { + cell = math.Abs(lo) + } + if h5 >= 1.5*h+0.5 { + ub = 1.0 + 1.0/(1.0+h) + } + if nDiv > 1 { + ndiv = nDiv + } + isml = dx < cell*ub*float64(ndiv)*o.cteEps*3 // added times 3, as several calculations here + } + + // set cell + if isml { + if cell > 10 { + cell = 9 + cell/10 + } + cell *= shrinkSml + if minN > 1 { + cell /= float64(minN) + } + } else { + cell = dx + if nDiv > 1 { + cell /= float64(nDiv) + } + } + if cell < 20*o.cteMin { + cell = 20 * o.cteMin // very small range.. corrected + } else if cell*10 > o.cteMax { + cell = 0.1 * o.cteMax // very large range.. corrected + } + + // find base and unit + bas := math.Pow(10.0, math.Floor(math.Log10(cell))) // base <= cell < 10*base + unit := bas + ub = 2 * bas + if ub-cell < h*(cell-unit) { + unit = ub + ub = 5 * bas + if ub-cell < h5*(cell-unit) { + unit = ub + ub = 10 * bas + if ub-cell < h*(cell-unit) { + unit = ub + } + } + } + + // find number of + ns := math.Floor(lo/unit + roundingEps) + nu := math.Ceil(hi/unit - roundingEps) + if epsCorrection > 0 && (epsCorrection > 1 || !isml) { + if lo > 0 { + lo *= (1 - o.cteEps) + } else { + lo = -o.cteMin + } + if hi > 0 { + hi *= (1 + o.cteEps) + } else { + hi = +o.cteMin + } + } + for ns*unit > lo+roundingEps*unit { + ns -= 1.0 + } + for nu*unit < hi-roundingEps*unit { + nu += 1.0 + } + + // find number of divisions + ndiv := int(0.5 + nu - ns) + if ndiv < minN { + k := minN - ndiv + if ns >= 0.0 { + nu += float64(k / 2) + ns -= float64(k/2 + k%2) + } else { + ns -= float64(k / 2) + nu += float64(k/2 + k%2) + } + ndiv = minN + } + ndiv++ + + // ensure that result covers original range + if ns*unit < lo { + lo = ns * unit + } + if nu*unit > hi { + hi = nu * unit + } + + // fill array + vals = make([]float64, ndiv) + vals[0] = lo + for i := 1; i < ndiv; i++ { + vals[i] = vals[i-1] + unit + if math.Abs(vals[i]) < roundingEps { + vals[i] = 0.0 + } + } + return +} diff --git a/util.go b/util.go index a530fcb..3eaf25d 100644 --- a/util.go +++ b/util.go @@ -115,3 +115,35 @@ func LoadFontFace(path string, points float64) (font.Face, error) { }) return face, nil } + +// imin returns the minimum between two integers +func imin(a, b int) int { + if a < b { + return a + } + return b +} + +// imax returns the maximum between two integers +func imax(a, b int) int { + if a > b { + return a + } + return b +} + +// min returns the minimum between two float point numbers +func min(a, b float64) float64 { + if a < b { + return a + } + return b +} + +// max returns the maximum between two float point numbers +func max(a, b float64) float64 { + if a > b { + return a + } + return b +}