diff --git a/plotter/field.go b/plotter/field.go index 3e189e81..39d76c6a 100644 --- a/plotter/field.go +++ b/plotter/field.go @@ -127,9 +127,15 @@ func (f *Field) Plot(c draw.Canvas, plt *plot.Plot) { v := f.FieldXY.Vector(i, j) c.Rotate(math.Atan2(v.Y, v.X)) s := math.Hypot(v.X, v.Y) / (2 * f.max) - v.X *= s - v.Y *= s - c.Scale(s*float64(dx-x), s*float64(dy-y)) + v.X /= f.max + v.Y /= 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.Scale(s*float64(dx-x), s*float64(dy-y)) + } + if f.DrawGlyph == nil { drawVector(c, v) } else { diff --git a/plotter/field_test.go b/plotter/field_test.go index 7702840d..03517d09 100644 --- a/plotter/field_test.go +++ b/plotter/field_test.go @@ -5,13 +5,16 @@ package plotter_test import ( + "image/color" "image/png" "log" + "math" "os" "testing" "gonum.org/v1/plot" "gonum.org/v1/plot/cmpimg" + "gonum.org/v1/plot/palette/moreland" "gonum.org/v1/plot/plotter" "gonum.org/v1/plot/vg" "gonum.org/v1/plot/vg/draw" @@ -79,6 +82,78 @@ func TestField(t *testing.T) { cmpimg.CheckPlot(ExampleField, t, "field.png") } +func ExampleField_colors() { + f := plotter.NewField(field{ + r: 5, c: 9, + fn: func(x, y float64) plotter.XY { + return plotter.XY{ + X: -0.75*x + y, + Y: -0.75*y - x, + } + }, + }) + + pal := moreland.ExtendedBlackBody() + pal.SetMin(0) + pal.SetMax(1.1) // Use 1.1 to make highest magnitude vectors visible on white. + + // Provide a DrawGlyph function to render a custom + // vector instead of the default monochrome arrow. + f.DrawGlyph = func(c vg.Canvas, v plotter.XY) { + c.Push() + defer c.Pop() + mag := math.Hypot(v.X, v.Y) + var pa vg.Path + if mag == 0 { + // Draw a black dot for zero vectors. + c.SetColor(color.Black) + pa.Move(vg.Point{X: f.LineStyle.Width}) + pa.Arc(vg.Point{}, f.LineStyle.Width, 0, 2*math.Pi) + pa.Close() + c.Fill(pa) + return + } + // Choose a color from the palette for the magnitude. + col, err := pal.At(mag) + if err != nil { + panic(err) + } + c.SetColor(col) + pa.Move(vg.Point{}) + pa.Line(vg.Point{X: 1, Y: 0}) + pa.Close() + c.Stroke(pa) + } + + p, err := plot.New() + if err != nil { + log.Panic(err) + } + p.Title.Text = "Vortex" + + p.X.Tick.Marker = integerTicks{} + p.Y.Tick.Marker = integerTicks{} + + p.Add(f) + + img := vgimg.New(250, 175) + dc := draw.New(img) + + p.Draw(dc) + w, err := os.Create("testdata/color_field.png") + if err != nil { + log.Panic(err) + } + png := vgimg.PngCanvas{Canvas: img} + if _, err = png.WriteTo(w); err != nil { + log.Panic(err) + } +} + +func TestFieldColors(t *testing.T) { + cmpimg.CheckPlot(ExampleField_colors, t, "color_field.png") +} + func ExampleField_gophers() { file, err := os.Open("testdata/gopher_running.png") if err != nil { @@ -103,6 +178,11 @@ func ExampleField_gophers() { // Provide a DrawGlyph function to render a custom // vector glyph instead of the default arrow. f.DrawGlyph = func(c vg.Canvas, v plotter.XY) { + // The canvas is unscaled if the vector has a zero + // magnitude, so return in that case. + if math.Hypot(v.X, v.Y) == 0 { + return + } // Vector glyphs are scaled to half unit length by the // plotter, so scale the gopher to twice unit size so // it fits the cell, and center on the cell. @@ -135,13 +215,6 @@ func ExampleField_gophers() { } } -func max(a, b int) int { - if a > b { - return a - } - return b -} - func TestFieldGophers(t *testing.T) { cmpimg.CheckPlot(ExampleField_gophers, t, "gopher_field.png") } diff --git a/plotter/testdata/color_field_golden.png b/plotter/testdata/color_field_golden.png new file mode 100644 index 00000000..343c4ada Binary files /dev/null and b/plotter/testdata/color_field_golden.png differ