...

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

Documentation: gonum.org/v1/plot/plotter

     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 plotter
     6  
     7  import (
     8  	"errors"
     9  	"image/color"
    10  	"math"
    11  	"sort"
    12  
    13  	"gonum.org/v1/plot"
    14  	"gonum.org/v1/plot/vg"
    15  	"gonum.org/v1/plot/vg/draw"
    16  )
    17  
    18  // fiveStatPlot contains the shared fields for quartile
    19  // and box-whisker plots.
    20  type fiveStatPlot struct {
    21  	// Values is a copy of the values of the values used to
    22  	// create this box plot.
    23  	Values
    24  
    25  	// Location is the location of the box along its axis.
    26  	Location float64
    27  
    28  	// Median is the median value of the data.
    29  	Median float64
    30  
    31  	// Quartile1 and Quartile3 are the first and
    32  	// third quartiles of the data respectively.
    33  	Quartile1, Quartile3 float64
    34  
    35  	// AdjLow and AdjHigh are the `adjacent' values
    36  	// on the low and high ends of the data.  The
    37  	// adjacent values are the points to which the
    38  	// whiskers are drawn.
    39  	AdjLow, AdjHigh float64
    40  
    41  	// Min and Max are the extreme values of the data.
    42  	Min, Max float64
    43  
    44  	// Outside are the indices of Vs for the outside points.
    45  	Outside []int
    46  }
    47  
    48  // BoxPlot implements the Plotter interface, drawing
    49  // a boxplot to represent the distribution of values.
    50  type BoxPlot struct {
    51  	fiveStatPlot
    52  
    53  	// Offset is added to the x location of each box.
    54  	// When the Offset is zero, the boxes are drawn
    55  	// centered at their x location.
    56  	Offset vg.Length
    57  
    58  	// Width is the width used to draw the box.
    59  	Width vg.Length
    60  
    61  	// CapWidth is the width of the cap used to top
    62  	// off a whisker.
    63  	CapWidth vg.Length
    64  
    65  	// GlyphStyle is the style of the outside point glyphs.
    66  	GlyphStyle draw.GlyphStyle
    67  
    68  	// FillColor is the color used to fill the box.
    69  	// The default is no fill.
    70  	FillColor color.Color
    71  
    72  	// BoxStyle is the line style for the box.
    73  	BoxStyle draw.LineStyle
    74  
    75  	// MedianStyle is the line style for the median line.
    76  	MedianStyle draw.LineStyle
    77  
    78  	// WhiskerStyle is the line style used to draw the
    79  	// whiskers.
    80  	WhiskerStyle draw.LineStyle
    81  
    82  	// Horizontal dictates whether the BoxPlot should be in the vertical
    83  	// (default) or horizontal direction.
    84  	Horizontal bool
    85  }
    86  
    87  // NewBoxPlot returns a new BoxPlot that represents
    88  // the distribution of the given values.  The style of
    89  // the box plot is that used for Tukey's schematic
    90  // plots in “Exploratory Data Analysis.”
    91  //
    92  // An error is returned if the boxplot is created with
    93  // no values.
    94  //
    95  // The fence values are 1.5x the interquartile before
    96  // the first quartile and after the third quartile.  Any
    97  // value that is outside of the fences are drawn as
    98  // Outside points.  The adjacent values (to which the
    99  // whiskers stretch) are the minimum and maximum
   100  // values that are not outside the fences.
   101  func NewBoxPlot(w vg.Length, loc float64, values Valuer) (*BoxPlot, error) {
   102  	if w < 0 {
   103  		return nil, errors.New("plotter: negative boxplot width")
   104  	}
   105  
   106  	b := new(BoxPlot)
   107  	var err error
   108  	if b.fiveStatPlot, err = newFiveStat(w, loc, values); err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	b.Width = w
   113  	b.CapWidth = 3 * w / 4
   114  
   115  	b.GlyphStyle = DefaultGlyphStyle
   116  	b.BoxStyle = DefaultLineStyle
   117  	b.MedianStyle = DefaultLineStyle
   118  	b.WhiskerStyle = draw.LineStyle{
   119  		Width:  vg.Points(0.5),
   120  		Dashes: []vg.Length{vg.Points(4), vg.Points(2)},
   121  	}
   122  
   123  	if len(b.Values) == 0 {
   124  		b.Width = 0
   125  		b.GlyphStyle.Radius = 0
   126  		b.BoxStyle.Width = 0
   127  		b.MedianStyle.Width = 0
   128  		b.WhiskerStyle.Width = 0
   129  	}
   130  
   131  	return b, nil
   132  }
   133  
   134  func newFiveStat(w vg.Length, loc float64, values Valuer) (fiveStatPlot, error) {
   135  	var b fiveStatPlot
   136  	b.Location = loc
   137  
   138  	var err error
   139  	if b.Values, err = CopyValues(values); err != nil {
   140  		return fiveStatPlot{}, err
   141  	}
   142  
   143  	sorted := make(Values, len(b.Values))
   144  	copy(sorted, b.Values)
   145  	sort.Float64s(sorted)
   146  
   147  	if len(sorted) == 1 {
   148  		b.Median = sorted[0]
   149  		b.Quartile1 = sorted[0]
   150  		b.Quartile3 = sorted[0]
   151  	} else {
   152  		b.Median = median(sorted)
   153  		b.Quartile1 = median(sorted[:len(sorted)/2])
   154  		b.Quartile3 = median(sorted[len(sorted)/2:])
   155  	}
   156  	b.Min = sorted[0]
   157  	b.Max = sorted[len(sorted)-1]
   158  
   159  	low := b.Quartile1 - 1.5*(b.Quartile3-b.Quartile1)
   160  	high := b.Quartile3 + 1.5*(b.Quartile3-b.Quartile1)
   161  	b.AdjLow = math.Inf(1)
   162  	b.AdjHigh = math.Inf(-1)
   163  	for i, v := range b.Values {
   164  		if v > high || v < low {
   165  			b.Outside = append(b.Outside, i)
   166  			continue
   167  		}
   168  		if v < b.AdjLow {
   169  			b.AdjLow = v
   170  		}
   171  		if v > b.AdjHigh {
   172  			b.AdjHigh = v
   173  		}
   174  	}
   175  
   176  	return b, nil
   177  }
   178  
   179  // median returns the median value from a
   180  // sorted Values.
   181  func median(vs Values) float64 {
   182  	if len(vs) == 1 {
   183  		return vs[0]
   184  	}
   185  	med := vs[len(vs)/2]
   186  	if len(vs)%2 == 0 {
   187  		med += vs[len(vs)/2-1]
   188  		med /= 2
   189  	}
   190  	return med
   191  }
   192  
   193  // Plot draws the BoxPlot on Canvas c and Plot plt.
   194  func (b *BoxPlot) Plot(c draw.Canvas, plt *plot.Plot) {
   195  	if b.Horizontal {
   196  		b := &horizBoxPlot{b}
   197  		b.Plot(c, plt)
   198  		return
   199  	}
   200  
   201  	trX, trY := plt.Transforms(&c)
   202  	x := trX(b.Location)
   203  	if !c.ContainsX(x) {
   204  		return
   205  	}
   206  	x += b.Offset
   207  
   208  	med := trY(b.Median)
   209  	q1 := trY(b.Quartile1)
   210  	q3 := trY(b.Quartile3)
   211  	aLow := trY(b.AdjLow)
   212  	aHigh := trY(b.AdjHigh)
   213  
   214  	pts := []vg.Point{
   215  		{X: x - b.Width/2, Y: q1},
   216  		{X: x - b.Width/2, Y: q3},
   217  		{X: x + b.Width/2, Y: q3},
   218  		{X: x + b.Width/2, Y: q1},
   219  		{X: x - b.Width/2 - b.BoxStyle.Width/2, Y: q1},
   220  	}
   221  	box := c.ClipLinesY(pts)
   222  	if b.FillColor != nil {
   223  		c.FillPolygon(b.FillColor, c.ClipPolygonY(pts))
   224  	}
   225  	c.StrokeLines(b.BoxStyle, box...)
   226  
   227  	medLine := c.ClipLinesY([]vg.Point{
   228  		{X: x - b.Width/2, Y: med},
   229  		{X: x + b.Width/2, Y: med},
   230  	})
   231  	c.StrokeLines(b.MedianStyle, medLine...)
   232  
   233  	cap := b.CapWidth / 2
   234  	whisks := c.ClipLinesY(
   235  		[]vg.Point{{X: x, Y: q3}, {X: x, Y: aHigh}},
   236  		[]vg.Point{{X: x - cap, Y: aHigh}, {X: x + cap, Y: aHigh}},
   237  		[]vg.Point{{X: x, Y: q1}, {X: x, Y: aLow}},
   238  		[]vg.Point{{X: x - cap, Y: aLow}, {X: x + cap, Y: aLow}},
   239  	)
   240  	c.StrokeLines(b.WhiskerStyle, whisks...)
   241  
   242  	for _, out := range b.Outside {
   243  		y := trY(b.Value(out))
   244  		if c.ContainsY(y) {
   245  			c.DrawGlyphNoClip(b.GlyphStyle, vg.Point{X: x, Y: y})
   246  		}
   247  	}
   248  }
   249  
   250  // DataRange returns the minimum and maximum x
   251  // and y values, implementing the plot.DataRanger
   252  // interface.
   253  func (b *BoxPlot) DataRange() (float64, float64, float64, float64) {
   254  	if b.Horizontal {
   255  		b := &horizBoxPlot{b}
   256  		return b.DataRange()
   257  	}
   258  	return b.Location, b.Location, b.Min, b.Max
   259  }
   260  
   261  // GlyphBoxes returns a slice of GlyphBoxes for the
   262  // points and for the median line of the boxplot,
   263  // implementing the plot.GlyphBoxer interface
   264  func (b *BoxPlot) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
   265  	if b.Horizontal {
   266  		b := &horizBoxPlot{b}
   267  		return b.GlyphBoxes(plt)
   268  	}
   269  
   270  	bs := make([]plot.GlyphBox, len(b.Outside)+1)
   271  	for i, out := range b.Outside {
   272  		bs[i].X = plt.X.Norm(b.Location)
   273  		bs[i].Y = plt.Y.Norm(b.Value(out))
   274  		bs[i].Rectangle = b.GlyphStyle.Rectangle()
   275  	}
   276  	bs[len(bs)-1].X = plt.X.Norm(b.Location)
   277  	bs[len(bs)-1].Y = plt.Y.Norm(b.Median)
   278  	bs[len(bs)-1].Rectangle = vg.Rectangle{
   279  		Min: vg.Point{X: b.Offset - (b.Width/2 + b.BoxStyle.Width/2)},
   280  		Max: vg.Point{X: b.Offset + (b.Width/2 + b.BoxStyle.Width/2)},
   281  	}
   282  	return bs
   283  }
   284  
   285  // OutsideLabels returns a *Labels that will plot
   286  // a label for each of the outside points.  The
   287  // labels are assumed to correspond to the
   288  // points used to create the box plot.
   289  func (b *BoxPlot) OutsideLabels(labels Labeller) (*Labels, error) {
   290  	if b.Horizontal {
   291  		b := &horizBoxPlot{b}
   292  		return b.OutsideLabels(labels)
   293  	}
   294  
   295  	strs := make([]string, len(b.Outside))
   296  	for i, out := range b.Outside {
   297  		strs[i] = labels.Label(out)
   298  	}
   299  	o := boxPlotOutsideLabels{b, strs}
   300  	ls, err := NewLabels(o)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	off := 0.5 * b.GlyphStyle.Radius
   305  	ls.Offset = ls.Offset.Add(vg.Point{X: off, Y: off})
   306  	return ls, nil
   307  }
   308  
   309  type boxPlotOutsideLabels struct {
   310  	box    *BoxPlot
   311  	labels []string
   312  }
   313  
   314  func (o boxPlotOutsideLabels) Len() int {
   315  	return len(o.box.Outside)
   316  }
   317  
   318  func (o boxPlotOutsideLabels) XY(i int) (float64, float64) {
   319  	return o.box.Location, o.box.Value(o.box.Outside[i])
   320  }
   321  
   322  func (o boxPlotOutsideLabels) Label(i int) string {
   323  	return o.labels[i]
   324  }
   325  
   326  // horizBoxPlot is like a regular BoxPlot, however,
   327  // it draws horizontally instead of Vertically.
   328  // TODO: Merge code for horizontal and vertical box plots as has been done for
   329  // bar charts.
   330  type horizBoxPlot struct{ *BoxPlot }
   331  
   332  func (b horizBoxPlot) Plot(c draw.Canvas, plt *plot.Plot) {
   333  	trX, trY := plt.Transforms(&c)
   334  	y := trY(b.Location)
   335  	if !c.ContainsY(y) {
   336  		return
   337  	}
   338  	y += b.Offset
   339  
   340  	med := trX(b.Median)
   341  	q1 := trX(b.Quartile1)
   342  	q3 := trX(b.Quartile3)
   343  	aLow := trX(b.AdjLow)
   344  	aHigh := trX(b.AdjHigh)
   345  
   346  	pts := []vg.Point{
   347  		{X: q1, Y: y - b.Width/2},
   348  		{X: q3, Y: y - b.Width/2},
   349  		{X: q3, Y: y + b.Width/2},
   350  		{X: q1, Y: y + b.Width/2},
   351  		{X: q1, Y: y - b.Width/2 - b.BoxStyle.Width/2},
   352  	}
   353  	box := c.ClipLinesX(pts)
   354  	if b.FillColor != nil {
   355  		c.FillPolygon(b.FillColor, c.ClipPolygonX(pts))
   356  	}
   357  	c.StrokeLines(b.BoxStyle, box...)
   358  
   359  	medLine := c.ClipLinesX([]vg.Point{
   360  		{X: med, Y: y - b.Width/2},
   361  		{X: med, Y: y + b.Width/2},
   362  	})
   363  	c.StrokeLines(b.MedianStyle, medLine...)
   364  
   365  	cap := b.CapWidth / 2
   366  	whisks := c.ClipLinesX(
   367  		[]vg.Point{{X: q3, Y: y}, {X: aHigh, Y: y}},
   368  		[]vg.Point{{X: aHigh, Y: y - cap}, {X: aHigh, Y: y + cap}},
   369  		[]vg.Point{{X: q1, Y: y}, {X: aLow, Y: y}},
   370  		[]vg.Point{{X: aLow, Y: y - cap}, {X: aLow, Y: y + cap}},
   371  	)
   372  	c.StrokeLines(b.WhiskerStyle, whisks...)
   373  
   374  	for _, out := range b.Outside {
   375  		x := trX(b.Value(out))
   376  		if c.ContainsX(x) {
   377  			c.DrawGlyphNoClip(b.GlyphStyle, vg.Point{X: x, Y: y})
   378  		}
   379  	}
   380  }
   381  
   382  // DataRange returns the minimum and maximum x
   383  // and y values, implementing the plot.DataRanger
   384  // interface.
   385  func (b horizBoxPlot) DataRange() (float64, float64, float64, float64) {
   386  	return b.Min, b.Max, b.Location, b.Location
   387  }
   388  
   389  // GlyphBoxes returns a slice of GlyphBoxes for the
   390  // points and for the median line of the boxplot,
   391  // implementing the plot.GlyphBoxer interface
   392  func (b horizBoxPlot) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
   393  	bs := make([]plot.GlyphBox, len(b.Outside)+1)
   394  	for i, out := range b.Outside {
   395  		bs[i].X = plt.X.Norm(b.Value(out))
   396  		bs[i].Y = plt.Y.Norm(b.Location)
   397  		bs[i].Rectangle = b.GlyphStyle.Rectangle()
   398  	}
   399  	bs[len(bs)-1].X = plt.X.Norm(b.Median)
   400  	bs[len(bs)-1].Y = plt.Y.Norm(b.Location)
   401  	bs[len(bs)-1].Rectangle = vg.Rectangle{
   402  		Min: vg.Point{Y: b.Offset - (b.Width/2 + b.BoxStyle.Width/2)},
   403  		Max: vg.Point{Y: b.Offset + (b.Width/2 + b.BoxStyle.Width/2)},
   404  	}
   405  	return bs
   406  }
   407  
   408  // OutsideLabels returns a *Labels that will plot
   409  // a label for each of the outside points.  The
   410  // labels are assumed to correspond to the
   411  // points used to create the box plot.
   412  func (b *horizBoxPlot) OutsideLabels(labels Labeller) (*Labels, error) {
   413  	strs := make([]string, len(b.Outside))
   414  	for i, out := range b.Outside {
   415  		strs[i] = labels.Label(out)
   416  	}
   417  	o := horizBoxPlotOutsideLabels{
   418  		boxPlotOutsideLabels{b.BoxPlot, strs},
   419  	}
   420  	ls, err := NewLabels(o)
   421  	if err != nil {
   422  		return nil, err
   423  	}
   424  	off := 0.5 * b.GlyphStyle.Radius
   425  	ls.Offset = ls.Offset.Add(vg.Point{X: off, Y: off})
   426  	return ls, nil
   427  }
   428  
   429  type horizBoxPlotOutsideLabels struct {
   430  	boxPlotOutsideLabels
   431  }
   432  
   433  func (o horizBoxPlotOutsideLabels) XY(i int) (float64, float64) {
   434  	return o.box.Value(o.box.Outside[i]), o.box.Location
   435  }
   436  
   437  // ValueLabels implements both the Valuer
   438  // and Labeller interfaces.
   439  type ValueLabels []struct {
   440  	Value float64
   441  	Label string
   442  }
   443  
   444  // Len returns the number of items.
   445  func (vs ValueLabels) Len() int {
   446  	return len(vs)
   447  }
   448  
   449  // Value returns the value of item i.
   450  func (vs ValueLabels) Value(i int) float64 {
   451  	return vs[i].Value
   452  }
   453  
   454  // Label returns the label of item i.
   455  func (vs ValueLabels) Label(i int) string {
   456  	return vs[i].Label
   457  }
   458  

View as plain text