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