1
2
3
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
19
20 type fiveStatPlot struct {
21
22
23 Values
24
25
26 Location float64
27
28
29 Median float64
30
31
32
33 Quartile1, Quartile3 float64
34
35
36
37
38
39 AdjLow, AdjHigh float64
40
41
42 Min, Max float64
43
44
45 Outside []int
46 }
47
48
49
50 type BoxPlot struct {
51 fiveStatPlot
52
53
54
55
56 Offset vg.Length
57
58
59 Width vg.Length
60
61
62
63 CapWidth vg.Length
64
65
66 GlyphStyle draw.GlyphStyle
67
68
69
70 FillColor color.Color
71
72
73 BoxStyle draw.LineStyle
74
75
76 MedianStyle draw.LineStyle
77
78
79
80 WhiskerStyle draw.LineStyle
81
82
83
84 Horizontal bool
85 }
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
180
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
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
251
252
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
262
263
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
286
287
288
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
327
328
329
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
383
384
385 func (b horizBoxPlot) DataRange() (float64, float64, float64, float64) {
386 return b.Min, b.Max, b.Location, b.Location
387 }
388
389
390
391
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
409
410
411
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
438
439 type ValueLabels []struct {
440 Value float64
441 Label string
442 }
443
444
445 func (vs ValueLabels) Len() int {
446 return len(vs)
447 }
448
449
450 func (vs ValueLabels) Value(i int) float64 {
451 return vs[i].Value
452 }
453
454
455 func (vs ValueLabels) Label(i int) string {
456 return vs[i].Label
457 }
458
View as plain text