1
2
3
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
24 DefaultFont = font.Font{
25 Typeface: "Liberation",
26 Variant: "Serif",
27 }
28
29
30 DefaultTextHandler text.Handler
31 )
32
33
34 type Plot struct {
35 Title struct {
36
37
38
39 Text string
40
41
42
43
44 Padding vg.Length
45
46
47 TextStyle text.Style
48 }
49
50
51
52 BackgroundColor color.Color
53
54
55
56 X, Y Axis
57
58
59 Legend Legend
60
61
62
63
64 TextHandler text.Handler
65
66
67
68 plotters []Plotter
69 }
70
71
72
73
74
75
76 type Plotter interface {
77
78 Plot(draw.Canvas, *Plot)
79 }
80
81
82 type DataRanger interface {
83
84 DataRange() (xmin, xmax, ymin, ymax float64)
85 }
86
87
88 type orientation byte
89
90 const (
91 horizontal orientation = iota
92 vertical
93 )
94
95
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
116
117
118
119
120
121
122
123
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
139
140
141
142
143
144
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
180
181
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
196
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
256
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
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
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
312
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
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
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
368
369
370
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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399 type GlyphBoxer interface {
400 GlyphBoxes(*Plot) []GlyphBox
401 }
402
403
404
405
406
407
408
409
410 type GlyphBox struct {
411
412 X, Y float64
413
414
415
416 vg.Rectangle
417 }
418
419
420
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
441
442
443
444
445
446
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
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
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
474 func (p *Plot) HideAxes() {
475 p.HideX()
476 p.HideY()
477 }
478
479
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
493
494
495
496
497
498
499
500
501
502
503
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
514
515
516
517
518
519
520
521
522
523
524
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