...

Source file src/gonum.org/v1/plot/plot.go

Documentation: gonum.org/v1/plot

     1  // Copyright ©2015 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package plot
     6  
     7  import (
     8  	"image/color"
     9  	"io"
    10  	"math"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"gonum.org/v1/plot/font"
    16  	"gonum.org/v1/plot/font/liberation"
    17  	"gonum.org/v1/plot/text"
    18  	"gonum.org/v1/plot/vg"
    19  	"gonum.org/v1/plot/vg/draw"
    20  )
    21  
    22  var (
    23  	// DefaultFont is the name of the default font for plot text.
    24  	DefaultFont = font.Font{
    25  		Typeface: "Liberation",
    26  		Variant:  "Serif",
    27  	}
    28  
    29  	// DefaultTextHandler is the default text handler used for text processing.
    30  	DefaultTextHandler text.Handler
    31  )
    32  
    33  // Plot is the basic type representing a plot.
    34  type Plot struct {
    35  	Title struct {
    36  		// Text is the text of the plot title.  If
    37  		// Text is the empty string then the plot
    38  		// will not have a title.
    39  		Text string
    40  
    41  		// Padding is the amount of padding
    42  		// between the bottom of the title and
    43  		// the top of the plot.
    44  		Padding vg.Length
    45  
    46  		// TextStyle specifies how the plot title text should be displayed.
    47  		TextStyle text.Style
    48  	}
    49  
    50  	// BackgroundColor is the background color of the plot.
    51  	// The default is White.
    52  	BackgroundColor color.Color
    53  
    54  	// X and Y are the horizontal and vertical axes
    55  	// of the plot respectively.
    56  	X, Y Axis
    57  
    58  	// Legend is the plot's legend.
    59  	Legend Legend
    60  
    61  	// TextHandler parses and formats text according to a given
    62  	// dialect (Markdown, LaTeX, plain, ...)
    63  	// The default is a plain text handler.
    64  	TextHandler text.Handler
    65  
    66  	// plotters are drawn by calling their Plot method
    67  	// after the axes are drawn.
    68  	plotters []Plotter
    69  }
    70  
    71  // Plotter is an interface that wraps the Plot method.
    72  // Some standard implementations of Plotter can be
    73  // found in the gonum.org/v1/plot/plotter
    74  // package, documented here:
    75  // https://godoc.org/gonum.org/v1/plot/plotter
    76  type Plotter interface {
    77  	// Plot draws the data to a draw.Canvas.
    78  	Plot(draw.Canvas, *Plot)
    79  }
    80  
    81  // DataRanger wraps the DataRange method.
    82  type DataRanger interface {
    83  	// DataRange returns the range of X and Y values.
    84  	DataRange() (xmin, xmax, ymin, ymax float64)
    85  }
    86  
    87  // orientation describes whether an axis is horizontal or vertical.
    88  type orientation byte
    89  
    90  const (
    91  	horizontal orientation = iota
    92  	vertical
    93  )
    94  
    95  // New returns a new plot with some reasonable default settings.
    96  func New() *Plot {
    97  	hdlr := DefaultTextHandler
    98  	p := &Plot{
    99  		BackgroundColor: color.White,
   100  		X:               makeAxis(horizontal),
   101  		Y:               makeAxis(vertical),
   102  		Legend:          newLegend(hdlr),
   103  		TextHandler:     hdlr,
   104  	}
   105  	p.Title.TextStyle = text.Style{
   106  		Color:   color.Black,
   107  		Font:    font.From(DefaultFont, 12),
   108  		XAlign:  draw.XCenter,
   109  		YAlign:  draw.YTop,
   110  		Handler: hdlr,
   111  	}
   112  	return p
   113  }
   114  
   115  // Add adds a Plotters to the plot.
   116  //
   117  // If the plotters implements DataRanger then the
   118  // minimum and maximum values of the X and Y
   119  // axes are changed if necessary to fit the range of
   120  // the data.
   121  //
   122  // When drawing the plot, Plotters are drawn in the
   123  // order in which they were added to the plot.
   124  func (p *Plot) Add(ps ...Plotter) {
   125  	for _, d := range ps {
   126  		if x, ok := d.(DataRanger); ok {
   127  			xmin, xmax, ymin, ymax := x.DataRange()
   128  			p.X.Min = math.Min(p.X.Min, xmin)
   129  			p.X.Max = math.Max(p.X.Max, xmax)
   130  			p.Y.Min = math.Min(p.Y.Min, ymin)
   131  			p.Y.Max = math.Max(p.Y.Max, ymax)
   132  		}
   133  	}
   134  
   135  	p.plotters = append(p.plotters, ps...)
   136  }
   137  
   138  // Draw draws a plot to a draw.Canvas.
   139  //
   140  // Plotters are drawn in the order in which they were
   141  // added to the plot.  Plotters that  implement the
   142  // GlyphBoxer interface will have their GlyphBoxes
   143  // taken into account when padding the plot so that
   144  // none of their glyphs are clipped.
   145  func (p *Plot) Draw(c draw.Canvas) {
   146  	if p.BackgroundColor != nil {
   147  		c.SetColor(p.BackgroundColor)
   148  		c.Fill(c.Rectangle.Path())
   149  	}
   150  
   151  	if p.Title.Text != "" {
   152  		descent := p.Title.TextStyle.FontExtents().Descent
   153  		c.FillText(p.Title.TextStyle, vg.Point{X: c.Center().X, Y: c.Max.Y + descent}, p.Title.Text)
   154  
   155  		rect := p.Title.TextStyle.Rectangle(p.Title.Text)
   156  		c.Max.Y -= rect.Size().Y
   157  		c.Max.Y -= p.Title.Padding
   158  	}
   159  
   160  	p.X.sanitizeRange()
   161  	x := horizontalAxis{p.X}
   162  	p.Y.sanitizeRange()
   163  	y := verticalAxis{p.Y}
   164  
   165  	ywidth := y.size()
   166  
   167  	xheight := x.size()
   168  	x.draw(padX(p, draw.Crop(c, ywidth, 0, 0, 0)))
   169  	y.draw(padY(p, draw.Crop(c, 0, 0, xheight, 0)))
   170  
   171  	dataC := padY(p, padX(p, draw.Crop(c, ywidth, 0, xheight, 0)))
   172  	for _, data := range p.plotters {
   173  		data.Plot(dataC, p)
   174  	}
   175  
   176  	p.Legend.Draw(draw.Crop(c, ywidth, 0, xheight, 0))
   177  }
   178  
   179  // DataCanvas returns a new draw.Canvas that
   180  // is the subset of the given draw area into which
   181  // the plot data will be drawn.
   182  func (p *Plot) DataCanvas(da draw.Canvas) draw.Canvas {
   183  	if p.Title.Text != "" {
   184  		rect := p.Title.TextStyle.Rectangle(p.Title.Text)
   185  		da.Max.Y -= rect.Size().Y
   186  		da.Max.Y -= p.Title.Padding
   187  	}
   188  	p.X.sanitizeRange()
   189  	x := horizontalAxis{p.X}
   190  	p.Y.sanitizeRange()
   191  	y := verticalAxis{p.Y}
   192  	return padY(p, padX(p, draw.Crop(da, y.size(), 0, x.size(), 0)))
   193  }
   194  
   195  // DrawGlyphBoxes draws red outlines around the plot's
   196  // GlyphBoxes.  This is intended for debugging.
   197  func (p *Plot) DrawGlyphBoxes(c draw.Canvas) {
   198  	dac := p.DataCanvas(c)
   199  	sty := draw.LineStyle{
   200  		Color: color.RGBA{R: 255, A: 255},
   201  		Width: vg.Points(0.5),
   202  	}
   203  
   204  	drawBox := func(c draw.Canvas, b GlyphBox) {
   205  		x := c.X(b.X) + b.Rectangle.Min.X
   206  		y := c.Y(b.Y) + b.Rectangle.Min.Y
   207  		c.StrokeLines(sty, []vg.Point{
   208  			{X: x, Y: y},
   209  			{X: x + b.Rectangle.Size().X, Y: y},
   210  			{X: x + b.Rectangle.Size().X, Y: y + b.Rectangle.Size().Y},
   211  			{X: x, Y: y + b.Rectangle.Size().Y},
   212  			{X: x, Y: y},
   213  		})
   214  	}
   215  
   216  	var title vg.Length
   217  	if p.Title.Text != "" {
   218  		rect := p.Title.TextStyle.Rectangle(p.Title.Text)
   219  		title += rect.Size().Y
   220  		title += p.Title.Padding
   221  		box := GlyphBox{
   222  			Rectangle: rect.Add(vg.Point{
   223  				X: c.Center().X,
   224  				Y: c.Max.Y,
   225  			}),
   226  		}
   227  		drawBox(c, box)
   228  	}
   229  
   230  	for _, b := range p.GlyphBoxes(p) {
   231  		drawBox(dac, b)
   232  	}
   233  
   234  	p.X.sanitizeRange()
   235  	p.Y.sanitizeRange()
   236  
   237  	x := horizontalAxis{p.X}
   238  	y := verticalAxis{p.Y}
   239  
   240  	ywidth := y.size()
   241  	xheight := x.size()
   242  
   243  	cx := padX(p, draw.Crop(c, ywidth, 0, 0, 0))
   244  	for _, b := range x.GlyphBoxes(p) {
   245  		drawBox(cx, b)
   246  	}
   247  
   248  	cy := padY(p, draw.Crop(c, 0, 0, xheight, 0))
   249  	cy.Max.Y -= title
   250  	for _, b := range y.GlyphBoxes(p) {
   251  		drawBox(cy, b)
   252  	}
   253  }
   254  
   255  // padX returns a draw.Canvas that is padded horizontally
   256  // so that glyphs will no be clipped.
   257  func padX(p *Plot, c draw.Canvas) draw.Canvas {
   258  	glyphs := p.GlyphBoxes(p)
   259  	l := leftMost(&c, glyphs)
   260  	xAxis := horizontalAxis{p.X}
   261  	glyphs = append(glyphs, xAxis.GlyphBoxes(p)...)
   262  	r := rightMost(&c, glyphs)
   263  
   264  	minx := c.Min.X - l.Min.X
   265  	maxx := c.Max.X - (r.Min.X + r.Size().X)
   266  	lx := vg.Length(l.X)
   267  	rx := vg.Length(r.X)
   268  	n := (lx*maxx - rx*minx) / (lx - rx)
   269  	m := ((lx-1)*maxx - rx*minx + minx) / (lx - rx)
   270  	return draw.Canvas{
   271  		Canvas: vg.Canvas(c),
   272  		Rectangle: vg.Rectangle{
   273  			Min: vg.Point{X: n, Y: c.Min.Y},
   274  			Max: vg.Point{X: m, Y: c.Max.Y},
   275  		},
   276  	}
   277  }
   278  
   279  // rightMost returns the right-most GlyphBox.
   280  func rightMost(c *draw.Canvas, boxes []GlyphBox) GlyphBox {
   281  	maxx := c.Max.X
   282  	r := GlyphBox{X: 1}
   283  	for _, b := range boxes {
   284  		if b.Size().X <= 0 {
   285  			continue
   286  		}
   287  		if x := c.X(b.X) + b.Min.X + b.Size().X; x > maxx && b.X <= 1 {
   288  			maxx = x
   289  			r = b
   290  		}
   291  	}
   292  	return r
   293  }
   294  
   295  // leftMost returns the left-most GlyphBox.
   296  func leftMost(c *draw.Canvas, boxes []GlyphBox) GlyphBox {
   297  	minx := c.Min.X
   298  	l := GlyphBox{}
   299  	for _, b := range boxes {
   300  		if b.Size().X <= 0 {
   301  			continue
   302  		}
   303  		if x := c.X(b.X) + b.Min.X; x < minx && b.X >= 0 {
   304  			minx = x
   305  			l = b
   306  		}
   307  	}
   308  	return l
   309  }
   310  
   311  // padY returns a draw.Canvas that is padded vertically
   312  // so that glyphs will no be clipped.
   313  func padY(p *Plot, c draw.Canvas) draw.Canvas {
   314  	glyphs := p.GlyphBoxes(p)
   315  	b := bottomMost(&c, glyphs)
   316  	yAxis := verticalAxis{p.Y}
   317  	glyphs = append(glyphs, yAxis.GlyphBoxes(p)...)
   318  	t := topMost(&c, glyphs)
   319  
   320  	miny := c.Min.Y - b.Min.Y
   321  	maxy := c.Max.Y - (t.Min.Y + t.Size().Y)
   322  	by := vg.Length(b.Y)
   323  	ty := vg.Length(t.Y)
   324  	n := (by*maxy - ty*miny) / (by - ty)
   325  	m := ((by-1)*maxy - ty*miny + miny) / (by - ty)
   326  	return draw.Canvas{
   327  		Canvas: vg.Canvas(c),
   328  		Rectangle: vg.Rectangle{
   329  			Min: vg.Point{Y: n, X: c.Min.X},
   330  			Max: vg.Point{Y: m, X: c.Max.X},
   331  		},
   332  	}
   333  }
   334  
   335  // topMost returns the top-most GlyphBox.
   336  func topMost(c *draw.Canvas, boxes []GlyphBox) GlyphBox {
   337  	maxy := c.Max.Y
   338  	t := GlyphBox{Y: 1}
   339  	for _, b := range boxes {
   340  		if b.Size().Y <= 0 {
   341  			continue
   342  		}
   343  		if y := c.Y(b.Y) + b.Min.Y + b.Size().Y; y > maxy && b.Y <= 1 {
   344  			maxy = y
   345  			t = b
   346  		}
   347  	}
   348  	return t
   349  }
   350  
   351  // bottomMost returns the bottom-most GlyphBox.
   352  func bottomMost(c *draw.Canvas, boxes []GlyphBox) GlyphBox {
   353  	miny := c.Min.Y
   354  	l := GlyphBox{}
   355  	for _, b := range boxes {
   356  		if b.Size().Y <= 0 {
   357  			continue
   358  		}
   359  		if y := c.Y(b.Y) + b.Min.Y; y < miny && b.Y >= 0 {
   360  			miny = y
   361  			l = b
   362  		}
   363  	}
   364  	return l
   365  }
   366  
   367  // Transforms returns functions to transfrom
   368  // from the x and y data coordinate system to
   369  // the draw coordinate system of the given
   370  // draw area.
   371  func (p *Plot) Transforms(c *draw.Canvas) (x, y func(float64) vg.Length) {
   372  	x = func(x float64) vg.Length { return c.X(p.X.Norm(x)) }
   373  	y = func(y float64) vg.Length { return c.Y(p.Y.Norm(y)) }
   374  	return
   375  }
   376  
   377  // GlyphBoxer wraps the GlyphBoxes method.
   378  // It should be implemented by things that meet
   379  // the Plotter interface that draw glyphs so that
   380  // their glyphs are not clipped if drawn near the
   381  // edge of the draw.Canvas.
   382  //
   383  // When computing padding, the plot ignores
   384  // GlyphBoxes as follows:
   385  // If the Size.X > 0 and the X value is not in range
   386  // of the X axis then the box is ignored.
   387  // If Size.Y > 0 and the Y value is not in range of
   388  // the Y axis then the box is ignored.
   389  //
   390  // Also, GlyphBoxes with Size.X <= 0 are ignored
   391  // when computing horizontal padding and
   392  // GlyphBoxes with Size.Y <= 0 are ignored when
   393  // computing vertical padding.  This is useful
   394  // for things like box plots and bar charts where
   395  // the boxes and bars are considered to be glyphs
   396  // in the X direction (and thus need padding), but
   397  // may be clipped in the Y direction (and do not
   398  // need padding).
   399  type GlyphBoxer interface {
   400  	GlyphBoxes(*Plot) []GlyphBox
   401  }
   402  
   403  // A GlyphBox describes the location of a glyph
   404  // and the offset/size of its bounding box.
   405  //
   406  // If the Rectangle.Size().X is non-positive (<= 0) then
   407  // the GlyphBox is ignored when computing the
   408  // horizontal padding, and likewise with
   409  // Rectangle.Size().Y and the vertical padding.
   410  type GlyphBox struct {
   411  	// The glyph location in normalized coordinates.
   412  	X, Y float64
   413  
   414  	// Rectangle is the offset of the glyph's minimum drawing
   415  	// point relative to the glyph location and its size.
   416  	vg.Rectangle
   417  }
   418  
   419  // GlyphBoxes returns the GlyphBoxes for all plot
   420  // data that meet the GlyphBoxer interface.
   421  func (p *Plot) GlyphBoxes(*Plot) (boxes []GlyphBox) {
   422  	for _, d := range p.plotters {
   423  		gb, ok := d.(GlyphBoxer)
   424  		if !ok {
   425  			continue
   426  		}
   427  		for _, b := range gb.GlyphBoxes(p) {
   428  			if b.Size().X > 0 && (b.X < 0 || b.X > 1) {
   429  				continue
   430  			}
   431  			if b.Size().Y > 0 && (b.Y < 0 || b.Y > 1) {
   432  				continue
   433  			}
   434  			boxes = append(boxes, b)
   435  		}
   436  	}
   437  	return
   438  }
   439  
   440  // NominalX configures the plot to have a nominal X
   441  // axis—an X axis with names instead of numbers.  The
   442  // X location corresponding to each name are the integers,
   443  // e.g., the x value 0 is centered above the first name and
   444  // 1 is above the second name, etc.  Labels for x values
   445  // that do not end up in range of the X axis will not have
   446  // tick marks.
   447  func (p *Plot) NominalX(names ...string) {
   448  	p.X.Tick.Width = 0
   449  	p.X.Tick.Length = 0
   450  	p.X.Width = 0
   451  	p.Y.Padding = p.X.Tick.Label.Width(names[0]) / 2
   452  	ticks := make([]Tick, len(names))
   453  	for i, name := range names {
   454  		ticks[i] = Tick{float64(i), name}
   455  	}
   456  	p.X.Tick.Marker = ConstantTicks(ticks)
   457  }
   458  
   459  // HideX configures the X axis so that it will not be drawn.
   460  func (p *Plot) HideX() {
   461  	p.X.Tick.Length = 0
   462  	p.X.Width = 0
   463  	p.X.Tick.Marker = ConstantTicks([]Tick{})
   464  }
   465  
   466  // HideY configures the Y axis so that it will not be drawn.
   467  func (p *Plot) HideY() {
   468  	p.Y.Tick.Length = 0
   469  	p.Y.Width = 0
   470  	p.Y.Tick.Marker = ConstantTicks([]Tick{})
   471  }
   472  
   473  // HideAxes hides the X and Y axes.
   474  func (p *Plot) HideAxes() {
   475  	p.HideX()
   476  	p.HideY()
   477  }
   478  
   479  // NominalY is like NominalX, but for the Y axis.
   480  func (p *Plot) NominalY(names ...string) {
   481  	p.Y.Tick.Width = 0
   482  	p.Y.Tick.Length = 0
   483  	p.Y.Width = 0
   484  	p.X.Padding = p.Y.Tick.Label.Height(names[0]) / 2
   485  	ticks := make([]Tick, len(names))
   486  	for i, name := range names {
   487  		ticks[i] = Tick{float64(i), name}
   488  	}
   489  	p.Y.Tick.Marker = ConstantTicks(ticks)
   490  }
   491  
   492  // WriterTo returns an io.WriterTo that will write the plot as
   493  // the specified image format.
   494  //
   495  // Supported formats are:
   496  //
   497  //   - .eps
   498  //   - .jpg|.jpeg
   499  //   - .pdf
   500  //   - .png
   501  //   - .svg
   502  //   - .tex
   503  //   - .tif|.tiff
   504  func (p *Plot) WriterTo(w, h vg.Length, format string) (io.WriterTo, error) {
   505  	c, err := draw.NewFormattedCanvas(w, h, format)
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  	p.Draw(draw.New(c))
   510  	return c, nil
   511  }
   512  
   513  // Save saves the plot to an image file.  The file format is determined
   514  // by the extension.
   515  //
   516  // Supported extensions are:
   517  //
   518  //   - .eps
   519  //   - .jpg|.jpeg
   520  //   - .pdf
   521  //   - .png
   522  //   - .svg
   523  //   - .tex
   524  //   - .tif|.tiff
   525  func (p *Plot) Save(w, h vg.Length, file string) (err error) {
   526  	f, err := os.Create(file)
   527  	if err != nil {
   528  		return err
   529  	}
   530  	defer func() {
   531  		e := f.Close()
   532  		if err == nil {
   533  			err = e
   534  		}
   535  	}()
   536  
   537  	format := strings.ToLower(filepath.Ext(file))
   538  	if len(format) != 0 {
   539  		format = format[1:]
   540  	}
   541  	c, err := p.WriterTo(w, h, format)
   542  	if err != nil {
   543  		return err
   544  	}
   545  
   546  	_, err = c.WriteTo(f)
   547  	return err
   548  }
   549  
   550  func init() {
   551  	font.DefaultCache.Add(liberation.Collection())
   552  	DefaultTextHandler = text.Plain{
   553  		Fonts: font.DefaultCache,
   554  	}
   555  }
   556  

View as plain text