805 lines
21 KiB
Go
805 lines
21 KiB
Go
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
|
|
}
|