diff --git a/axis.go b/axis.go index cbac5ac4..7f69f658 100644 --- a/axis.go +++ b/axis.go @@ -122,9 +122,9 @@ func makeAxis() (Axis, error) { return a, nil } -// sanitizeRange ensures that the range of the +// SanitizeRange ensures that the range of the // axis makes sense. -func (a *Axis) sanitizeRange() { +func (a *Axis) SanitizeRange() { if math.IsInf(a.Min, 0) { a.Min = 0 } @@ -177,14 +177,14 @@ func (a *Axis) drawTicks() bool { return a.Tick.Width > 0 && a.Tick.Length > 0 } -// A horizontalAxis draws horizontally across the bottom +// A HorizontalAxis draws horizontally across the bottom // of a plot. -type horizontalAxis struct { +type HorizontalAxis struct { Axis } -// size returns the height of the axis. -func (a *horizontalAxis) size() (h vg.Length) { +// Size returns the height of the axis. +func (a *HorizontalAxis) Size() (h vg.Length) { if a.Label.Text != "" { h -= a.Label.Font.Extents().Descent h += a.Label.Height(a.Label.Text) @@ -200,8 +200,8 @@ func (a *horizontalAxis) size() (h vg.Length) { return } -// draw draws the axis along the lower edge of a draw.Canvas. -func (a *horizontalAxis) draw(c draw.Canvas) { +// Draw draws the axis along the lower edge of a draw.Canvas. +func (a *HorizontalAxis) Draw(c draw.Canvas) { y := c.Min.Y if a.Label.Text != "" { y -= a.Label.Font.Extents().Descent @@ -241,7 +241,7 @@ func (a *horizontalAxis) draw(c draw.Canvas) { } // GlyphBoxes returns the GlyphBoxes for the tick labels. -func (a *horizontalAxis) GlyphBoxes(*Plot) (boxes []GlyphBox) { +func (a *HorizontalAxis) GlyphBoxes(*Plot) (boxes []GlyphBox) { for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max) { if t.IsMinor() { continue @@ -259,13 +259,13 @@ func (a *horizontalAxis) GlyphBoxes(*Plot) (boxes []GlyphBox) { return } -// A verticalAxis is drawn vertically up the left side of a plot. -type verticalAxis struct { +// A VerticalAxis is drawn vertically up the left side of a plot. +type VerticalAxis struct { Axis } -// size returns the width of the axis. -func (a *verticalAxis) size() (w vg.Length) { +// Size returns the width of the axis. +func (a *VerticalAxis) Size() (w vg.Length) { if a.Label.Text != "" { w -= a.Label.Font.Extents().Descent w += a.Label.Height(a.Label.Text) @@ -284,8 +284,8 @@ func (a *verticalAxis) size() (w vg.Length) { return } -// draw draws the axis along the left side of a draw.Canvas. -func (a *verticalAxis) draw(c draw.Canvas) { +// Draw draws the axis along the left side of a draw.Canvas. +func (a *VerticalAxis) Draw(c draw.Canvas) { x := c.Min.X if a.Label.Text != "" { x += a.Label.Height(a.Label.Text) @@ -327,7 +327,7 @@ func (a *verticalAxis) draw(c draw.Canvas) { } // GlyphBoxes returns the GlyphBoxes for the tick labels -func (a *verticalAxis) GlyphBoxes(*Plot) (boxes []GlyphBox) { +func (a *VerticalAxis) GlyphBoxes(*Plot) (boxes []GlyphBox) { for _, t := range a.Tick.Marker.Ticks(a.Min, a.Max) { if t.IsMinor() { continue diff --git a/export_test.go b/export_test.go deleted file mode 100644 index 5c4a95f2..00000000 --- a/export_test.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright ©2015 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 plot - -import ( - "github.com/gonum/plot/vg/draw" -) - -// Draw exports the Legend draw method for testing. -func (l *Legend) Draw(c draw.Canvas) { l.draw(c) } diff --git a/gob/gob.go b/gob/gob.go index 60f851f7..4e0df06f 100644 --- a/gob/gob.go +++ b/gob/gob.go @@ -40,4 +40,6 @@ func init() { gob.Register(plotter.XYZs{}) gob.Register(plotter.XYValues{}) + // plot.Drawer + gob.Register(plot.DefaultPlotStyle{}) } diff --git a/legend.go b/legend.go index 8c7abae0..9d5ad2ff 100644 --- a/legend.go +++ b/legend.go @@ -80,8 +80,8 @@ func makeLegend() (Legend, error) { }, nil } -// draw draws the legend to the given draw.Canvas. -func (l *Legend) draw(c draw.Canvas) { +// Draw draws the legend to the given draw.Canvas. +func (l *Legend) Draw(c draw.Canvas) { iconx := c.Min.X textx := iconx + l.ThumbnailWidth + l.TextStyle.Width(" ") xalign := 0.0 diff --git a/plot.go b/plot.go index 19e52b85..ead566d3 100644 --- a/plot.go +++ b/plot.go @@ -59,6 +59,9 @@ type Plot struct { // Legend is the plot's legend. Legend Legend + // Style draws the plot with style + Style PlotDrawer + // plotters are drawn by calling their Plot method // after the axes are drawn. plotters []Plotter @@ -74,6 +77,11 @@ type Plotter interface { Plot(draw.Canvas, *Plot) } +// PlotDrawer draws a Plot on a draw.Canvas +type PlotDrawer interface { + DrawPlot(p *Plot, c draw.Canvas) +} + // DataRanger wraps the DataRange method. type DataRanger interface { // DataRange returns the range of X and Y values. @@ -104,6 +112,7 @@ func New() (*Plot, error) { X: x, Y: y, Legend: legend, + Style: DefaultPlotStyle{}, } p.Title.TextStyle = draw.TextStyle{ Color: color.Black, @@ -112,6 +121,11 @@ func New() (*Plot, error) { return p, nil } +// Plotters returns the list of plotters attached to this Plot. +func (p *Plot) Plotters() []Plotter { + return p.plotters +} + // Add adds a Plotters to the plot. // // If the plotters implements DataRanger then the @@ -143,32 +157,7 @@ func (p *Plot) Add(ps ...Plotter) { // taken into account when padding the plot so that // none of their glyphs are clipped. func (p *Plot) Draw(c draw.Canvas) { - if p.BackgroundColor != nil { - c.SetColor(p.BackgroundColor) - c.Fill(c.Rectangle.Path()) - } - if p.Title.Text != "" { - c.FillText(p.Title.TextStyle, vg.Point{c.Center().X, c.Max.Y}, -0.5, -1, p.Title.Text) - c.Max.Y -= p.Title.Height(p.Title.Text) - p.Title.Font.Extents().Descent - c.Max.Y -= p.Title.Padding - } - - p.X.sanitizeRange() - x := horizontalAxis{p.X} - p.Y.sanitizeRange() - y := verticalAxis{p.Y} - - ywidth := y.size() - x.draw(padX(p, draw.Crop(c, ywidth, 0, 0, 0))) - xheight := x.size() - y.draw(padY(p, draw.Crop(c, 0, 0, xheight, 0))) - - dataC := padY(p, padX(p, draw.Crop(c, ywidth, 0, xheight, 0))) - for _, data := range p.plotters { - data.Plot(dataC, p) - } - - p.Legend.draw(draw.Crop(draw.Crop(c, ywidth, 0, 0, 0), 0, 0, xheight, 0)) + p.Style.DrawPlot(p, c) } // DataCanvas returns a new draw.Canvas that @@ -179,11 +168,11 @@ func (p *Plot) DataCanvas(da draw.Canvas) draw.Canvas { da.Max.Y -= p.Title.Height(p.Title.Text) - p.Title.Font.Extents().Descent da.Max.Y -= p.Title.Padding } - p.X.sanitizeRange() - x := horizontalAxis{p.X} - p.Y.sanitizeRange() - y := verticalAxis{p.Y} - return padY(p, padX(p, draw.Crop(da, y.size(), x.size(), 0, 0))) + p.X.SanitizeRange() + x := HorizontalAxis{p.X} + p.Y.SanitizeRange() + y := VerticalAxis{p.Y} + return PadY(p, PadX(p, draw.Crop(da, y.Size(), x.Size(), 0, 0))) } // DrawGlyphBoxes draws red outlines around the plot's @@ -197,12 +186,51 @@ func (p *Plot) DrawGlyphBoxes(c *draw.Canvas) { } } -// padX returns a draw.Canvas that is padded horizontally +// DefaultPlotStyle implements PlotDrawer +type DefaultPlotStyle struct{} + +// DrawPlot draws a plot to a draw.Canvas. +// +// Plotters are drawn in the order in which they were +// added to the plot. Plotters that implement the +// GlyphBoxer interface will have their GlyphBoxes +// taken into account when padding the plot so that +// none of their glyphs are clipped. +func (DefaultPlotStyle) DrawPlot(p *Plot, c draw.Canvas) { + if p.BackgroundColor != nil { + c.SetColor(p.BackgroundColor) + c.Fill(c.Rectangle.Path()) + } + if p.Title.Text != "" { + c.FillText(p.Title.TextStyle, vg.Point{c.Center().X, c.Max.Y}, -0.5, -1, p.Title.Text) + c.Max.Y -= p.Title.Height(p.Title.Text) - p.Title.Font.Extents().Descent + c.Max.Y -= p.Title.Padding + } + + p.X.SanitizeRange() + x := HorizontalAxis{p.X} + p.Y.SanitizeRange() + y := VerticalAxis{p.Y} + + ywidth := y.Size() + x.Draw(PadX(p, draw.Crop(c, ywidth, 0, 0, 0))) + xheight := x.Size() + y.Draw(PadY(p, draw.Crop(c, 0, 0, xheight, 0))) + + dataC := PadY(p, PadX(p, draw.Crop(c, ywidth, 0, xheight, 0))) + for _, data := range p.plotters { + data.Plot(dataC, p) + } + + p.Legend.Draw(draw.Crop(draw.Crop(c, ywidth, 0, 0, 0), 0, 0, xheight, 0)) +} + +// PadX returns a draw.Canvas that is padded horizontally // so that glyphs will no be clipped. -func padX(p *Plot, c draw.Canvas) draw.Canvas { +func PadX(p *Plot, c draw.Canvas) draw.Canvas { glyphs := p.GlyphBoxes(p) l := leftMost(&c, glyphs) - xAxis := horizontalAxis{p.X} + xAxis := HorizontalAxis{p.X} glyphs = append(glyphs, xAxis.GlyphBoxes(p)...) r := rightMost(&c, glyphs) @@ -253,12 +281,12 @@ func leftMost(c *draw.Canvas, boxes []GlyphBox) GlyphBox { return l } -// padY returns a draw.Canvas that is padded vertically +// PadY returns a draw.Canvas that is padded vertically // so that glyphs will no be clipped. -func padY(p *Plot, c draw.Canvas) draw.Canvas { +func PadY(p *Plot, c draw.Canvas) draw.Canvas { glyphs := p.GlyphBoxes(p) b := bottomMost(&c, glyphs) - yAxis := verticalAxis{p.Y} + yAxis := VerticalAxis{p.Y} glyphs = append(glyphs, yAxis.GlyphBoxes(p)...) t := topMost(&c, glyphs) diff --git a/plotter/histogram_customstyle_test.go b/plotter/histogram_customstyle_test.go new file mode 100644 index 00000000..e66605ff --- /dev/null +++ b/plotter/histogram_customstyle_test.go @@ -0,0 +1,121 @@ +// Copyright ©2016 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 ( + "image/color" + "log" + "math" + "math/rand" + "testing" + + "github.com/gonum/plot" + "github.com/gonum/plot/vg" + "github.com/gonum/plot/vg/draw" +) + +// An example of making a histogram with a custom style. +func ExampleHistogramCustomStyle() { + // stdNorm returns the probability of drawing a + // value from a standard normal distribution. + stdNorm := func(x float64) float64 { + const sigma = 1.0 + const mu = 0.0 + const root2π = 2.50662827459517818309 + return 1.0 / (sigma * root2π) * math.Exp(-((x-mu)*(x-mu))/(2*sigma*sigma)) + } + + n := 10000 + vals := make(Values, n) + for i := 0; i < n; i++ { + vals[i] = rand.NormFloat64() + } + + p, err := plot.New() + if err != nil { + log.Panic(err) + } + p.Title.Text = "Histogram" + p.X.Label.Text = "X-axis" + p.Y.Label.Text = "Y-axis" + p.Style = CustomStyle{} + + h, err := NewHist(vals, 16) + if err != nil { + log.Panic(err) + } + h.Normalize(1) + p.Add(h) + + // The normal distribution function + norm := NewFunction(stdNorm) + norm.Color = color.RGBA{R: 255, A: 255} + norm.Width = vg.Points(2) + p.Add(norm) + + err = p.Save(200, 200, "testdata/histogram-custom-style.png") + if err != nil { + log.Panic(err) + } +} + +func TestHistogramCustomStyle(t *testing.T) { + checkPlot(ExampleHistogramCustomStyle, t, "histogram-custom-style.png") +} + +type CustomStyle struct{} + +func (CustomStyle) DrawPlot(p *plot.Plot, c draw.Canvas) { + if p.BackgroundColor != nil { + c.SetColor(p.BackgroundColor) + c.Fill(c.Rectangle.Path()) + } + + xpad := p.X.Padding + ypad := p.Y.Padding + defer func() { + p.X.Padding = xpad + p.Y.Padding = ypad + }() + + p.X.Padding = 0 + p.Y.Padding = 0 + + p.X.SanitizeRange() + p.Y.SanitizeRange() + xaxis := plot.HorizontalAxis{p.X} + yaxis := plot.VerticalAxis{p.Y} + + ywidth := yaxis.Size() + xheight := xaxis.Size() + + if p.Title.Text != "" { + cx := draw.Crop(c, ywidth, 0, 0, 0) + c.FillText(p.Title.TextStyle, vg.Point{cx.Center().X, c.Max.Y}, -0.5, -1, p.Title.Text) + c.Max.Y -= p.Title.Height(p.Title.Text) - p.Title.Font.Extents().Descent + c.Max.Y -= p.Title.Padding + } + + xc := plot.PadX(p, draw.Crop(c, ywidth-yaxis.Width-yaxis.Padding, 0, 0, 0)) + yc := plot.PadY(p, draw.Crop(c, 0, xheight-xaxis.Width-xaxis.Padding, xheight, 0)) + + xaxis.Draw(xc) + yaxis.Draw(yc) + xmin := xc.Min.X + xmax := xc.Max.X + ymin := yc.Min.Y + ymax := xc.Max.Y + xc.StrokeLine2(xaxis.LineStyle, xmin, ymax, xmax, ymax) + xc.StrokeLine2(xaxis.LineStyle, xmin, ymin, xmax, ymin) + yc.StrokeLine2(yaxis.LineStyle, xmin, ymin, xmin, ymax) + yc.StrokeLine2(yaxis.LineStyle, xmax, ymin, xmax, ymax) + + datac := plot.PadY(p, plot.PadX(p, draw.Crop(c, ywidth, 0, xheight, 0))) + for _, data := range p.Plotters() { + data.Plot(datac, p) + } + + p.Legend.Draw(draw.Crop(draw.Crop(c, ywidth, 0, 0, 0), 0, 0, xheight, 0)) +} diff --git a/plotter/testdata/histogram-custom-style_golden.png b/plotter/testdata/histogram-custom-style_golden.png new file mode 100644 index 00000000..8c2df6a6 Binary files /dev/null and b/plotter/testdata/histogram-custom-style_golden.png differ