// Copyright ©2019 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package plotter import ( "math" "gonum.org/v1/plot" "gonum.org/v1/plot/vg" "gonum.org/v1/plot/vg/draw" ) // FieldXY describes a two dimensional vector field where the // X and Y coordinates are arranged on a rectangular grid. type FieldXY interface { // Dims returns the dimensions of the grid. Dims() (c, r int) // Vector returns the value of a vector field at (c, r). // It will panic if c or r are out of bounds for the field. Vector(c, r int) XY // X returns the coordinate for the column at the index c. // It will panic if c is out of bounds for the grid. X(c int) float64 // Y returns the coordinate for the row at the index r. // It will panic if r is out of bounds for the grid. Y(r int) float64 } // Field implements the Plotter interface, drawing // a vector field of the values in the FieldXY field. type Field struct { FieldXY FieldXY // DrawGlyph is the user hook to draw a field // vector glyph. The function should draw a unit // vector to (1, 0) on the vg.Canvas, c with the // sty LineStyle. The Field plotter will rotate // and scale the unit vector appropriately. // If the magnitude of v is zero, no scaling or // rotation is performed. // // The direction and magnitude of v can be used // to determine properties of the glyph drawing // but should not be used to determine size or // directions of the glyph. // // If DrawGlyph is nil, a simple arrow will be // drawn. DrawGlyph func(c vg.Canvas, sty draw.LineStyle, v XY) // LineStyle is the style of the line used to // render vectors when DrawGlyph is nil. // Otherwise it is passed to DrawGlyph. LineStyle draw.LineStyle // max define the dynamic range of the field. max float64 } // NewField creates a new vector field plotter. func NewField(f FieldXY) *Field { max := math.Inf(-1) c, r := f.Dims() for i := 0; i < c; i++ { for j := 0; j < r; j++ { v := f.Vector(i, j) d := math.Hypot(v.X, v.Y) if math.IsNaN(d) { continue } max = math.Max(max, d) } } return &Field{ FieldXY: f, LineStyle: DefaultLineStyle, max: max, } } // Plot implements the Plot method of the plot.Plotter interface. func (f *Field) Plot(c draw.Canvas, plt *plot.Plot) { c.Push() defer c.Pop() c.SetLineStyle(f.LineStyle) trX, trY := plt.Transforms(&c) cols, rows := f.FieldXY.Dims() for i := 0; i < cols; i++ { var right, left float64 switch i { case 0: if cols == 1 { right = 0.5 } else { right = (f.FieldXY.X(1) - f.FieldXY.X(0)) / 2 } left = -right case cols - 1: right = (f.FieldXY.X(cols-1) - f.FieldXY.X(cols-2)) / 2 left = -right default: right = (f.FieldXY.X(i+1) - f.FieldXY.X(i)) / 2 left = -(f.FieldXY.X(i) - f.FieldXY.X(i-1)) / 2 } for j := 0; j < rows; j++ { var up, down float64 switch j { case 0: if rows == 1 { up = 0.5 } else { up = (f.FieldXY.Y(1) - f.FieldXY.Y(0)) / 2 } down = -up case rows - 1: up = (f.FieldXY.Y(rows-1) - f.FieldXY.Y(rows-2)) / 2 down = -up default: up = (f.FieldXY.Y(j+1) - f.FieldXY.Y(j)) / 2 down = -(f.FieldXY.Y(j) - f.FieldXY.Y(j-1)) / 2 } x, y := trX(f.FieldXY.X(i)+left), trY(f.FieldXY.Y(j)+down) dx, dy := trX(f.FieldXY.X(i)+right), trY(f.FieldXY.Y(j)+up) if !c.Contains(vg.Point{X: x, Y: y}) || !c.Contains(vg.Point{X: dx, Y: dy}) { continue } c.Push() c.Translate(vg.Point{X: (x + dx) / 2, Y: (y + dy) / 2}) v := f.FieldXY.Vector(i, j) s := math.Hypot(v.X, v.Y) / (2 * f.max) // Do not scale when the vector is zero, otherwise the // user cannot render special-case glyphs for that case. if s != 0 { c.Rotate(math.Atan2(v.Y, v.X)) c.Scale(s*float64(dx-x), s*float64(dy-y)) } v.X /= f.max v.Y /= f.max if f.DrawGlyph == nil { drawVector(c, v) } else { f.DrawGlyph(c, f.LineStyle, v) } c.Pop() } } } func drawVector(c vg.Canvas, v XY) { if math.Hypot(v.X, v.Y) == 0 { return } // TODO(kortschak): Improve this arrow. var pa vg.Path pa.Move(vg.Point{}) pa.Line(vg.Point{X: 1, Y: 0}) pa.Close() c.Stroke(pa) } // DataRange implements the DataRange method // of the plot.DataRanger interface. func (f *Field) DataRange() (xmin, xmax, ymin, ymax float64) { c, r := f.FieldXY.Dims() switch c { case 1: // Make a unit length when there is no neighbour. xmax = f.FieldXY.X(0) + 0.5 xmin = f.FieldXY.X(0) - 0.5 default: xmax = f.FieldXY.X(c-1) + (f.FieldXY.X(c-1)-f.FieldXY.X(c-2))/2 xmin = f.FieldXY.X(0) - (f.FieldXY.X(1)-f.FieldXY.X(0))/2 } switch r { case 1: // Make a unit length when there is no neighbour. ymax = f.FieldXY.Y(0) + 0.5 ymin = f.FieldXY.Y(0) - 0.5 default: ymax = f.FieldXY.Y(r-1) + (f.FieldXY.Y(r-1)-f.FieldXY.Y(r-2))/2 ymin = f.FieldXY.Y(0) - (f.FieldXY.Y(1)-f.FieldXY.Y(0))/2 } return xmin, xmax, ymin, ymax } // GlyphBoxes implements the GlyphBoxes method // of the plot.GlyphBoxer interface. func (f *Field) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox { c, r := f.FieldXY.Dims() b := make([]plot.GlyphBox, 0, r*c) for i := 0; i < c; i++ { for j := 0; j < r; j++ { b = append(b, plot.GlyphBox{ X: plt.X.Norm(f.FieldXY.X(i)), Y: plt.Y.Norm(f.FieldXY.Y(j)), Rectangle: vg.Rectangle{ Min: vg.Point{X: -5, Y: -5}, Max: vg.Point{X: +5, Y: +5}, }, }) } } return b }