1
2
3
4
5 package plot
6
7 import (
8 "image/color"
9 "math"
10 "strconv"
11 "time"
12
13 "gonum.org/v1/plot/font"
14 "gonum.org/v1/plot/text"
15 "gonum.org/v1/plot/vg"
16 "gonum.org/v1/plot/vg/draw"
17 )
18
19
20 type Ticker interface {
21
22 Ticks(min, max float64) []Tick
23 }
24
25
26
27 type Normalizer interface {
28
29
30 Normalize(min, max, x float64) float64
31 }
32
33
34
35 type Axis struct {
36
37
38 Min, Max float64
39
40 Label struct {
41
42 Text string
43
44
45 Padding vg.Length
46
47
48
49
50
51 TextStyle text.Style
52
53
54
55
56
57
58 Position float64
59 }
60
61
62 draw.LineStyle
63
64
65
66
67 Padding vg.Length
68
69 Tick struct {
70
71 Label text.Style
72
73
74 draw.LineStyle
75
76
77
78
79 Length vg.Length
80
81
82
83
84 Marker Ticker
85 }
86
87
88
89
90 Scale Normalizer
91
92
93
94 AutoRescale bool
95 }
96
97
98
99
100
101 func makeAxis(o orientation) Axis {
102
103 a := Axis{
104 Min: math.Inf(+1),
105 Max: math.Inf(-1),
106 LineStyle: draw.LineStyle{
107 Color: color.Black,
108 Width: vg.Points(0.5),
109 },
110 Padding: vg.Points(5),
111 Scale: LinearScale{},
112 }
113 a.Label.TextStyle = text.Style{
114 Color: color.Black,
115 Font: font.From(DefaultFont, 12),
116 XAlign: draw.XCenter,
117 YAlign: draw.YBottom,
118 Handler: DefaultTextHandler,
119 }
120 a.Label.Position = draw.PosCenter
121
122 var (
123 xalign draw.XAlignment
124 yalign draw.YAlignment
125 )
126 switch o {
127 case vertical:
128 xalign = draw.XRight
129 yalign = draw.YCenter
130 case horizontal:
131 xalign = draw.XCenter
132 yalign = draw.YTop
133 }
134
135 a.Tick.Label = text.Style{
136 Color: color.Black,
137 Font: font.From(DefaultFont, 10),
138 XAlign: xalign,
139 YAlign: yalign,
140 Handler: DefaultTextHandler,
141 }
142 a.Tick.LineStyle = draw.LineStyle{
143 Color: color.Black,
144 Width: vg.Points(0.5),
145 }
146 a.Tick.Length = vg.Points(8)
147 a.Tick.Marker = DefaultTicks{}
148
149 return a
150 }
151
152
153
154 func (a *Axis) sanitizeRange() {
155 if math.IsInf(a.Min, 0) {
156 a.Min = 0
157 }
158 if math.IsInf(a.Max, 0) {
159 a.Max = 0
160 }
161 if a.Min > a.Max {
162 a.Min, a.Max = a.Max, a.Min
163 }
164 if a.Min == a.Max {
165 a.Min--
166 a.Max++
167 }
168
169 if a.AutoRescale {
170 marks := a.Tick.Marker.Ticks(a.Min, a.Max)
171 for _, t := range marks {
172 a.Min = math.Min(a.Min, t.Value)
173 a.Max = math.Max(a.Max, t.Value)
174 }
175 }
176 }
177
178
179
180 type LinearScale struct{}
181
182 var _ Normalizer = LinearScale{}
183
184
185 func (LinearScale) Normalize(min, max, x float64) float64 {
186 return (x - min) / (max - min)
187 }
188
189
190
191 type LogScale struct{}
192
193 var _ Normalizer = LogScale{}
194
195
196
197 func (LogScale) Normalize(min, max, x float64) float64 {
198 if min <= 0 || max <= 0 || x <= 0 {
199 panic("Values must be greater than 0 for a log scale.")
200 }
201 logMin := math.Log(min)
202 return (math.Log(x) - logMin) / (math.Log(max) - logMin)
203 }
204
205
206
207 type InvertedScale struct{ Normalizer }
208
209 var _ Normalizer = InvertedScale{}
210
211
212 func (is InvertedScale) Normalize(min, max, x float64) float64 {
213 return is.Normalizer.Normalize(max, min, x)
214 }
215
216
217
218
219
220 func (a Axis) Norm(x float64) float64 {
221 return a.Scale.Normalize(a.Min, a.Max, x)
222 }
223
224
225 func (a Axis) drawTicks() bool {
226 return a.Tick.Width > 0 && a.Tick.Length > 0
227 }
228
229
230
231 type horizontalAxis struct {
232 Axis
233 }
234
235
236 func (a horizontalAxis) size() (h vg.Length) {
237 if a.Label.Text != "" {
238 h += a.Label.TextStyle.FontExtents().Descent
239 h += a.Label.TextStyle.Height(a.Label.Text)
240 h += a.Label.Padding
241 }
242
243 marks := a.Tick.Marker.Ticks(a.Min, a.Max)
244 if len(marks) > 0 {
245 if a.drawTicks() {
246 h += a.Tick.Length
247 }
248 h += tickLabelHeight(a.Tick.Label, marks)
249 }
250 h += a.Width / 2
251 h += a.Padding
252
253 return h
254 }
255
256
257 func (a horizontalAxis) draw(c draw.Canvas) {
258 var (
259 x vg.Length
260 y = c.Min.Y
261 )
262 switch a.Label.Position {
263 case draw.PosCenter:
264 x = c.Center().X
265 case draw.PosRight:
266 x = c.Max.X
267 x -= a.Label.TextStyle.Width(a.Label.Text) / 2
268 }
269 if a.Label.Text != "" {
270 descent := a.Label.TextStyle.FontExtents().Descent
271 c.FillText(a.Label.TextStyle, vg.Point{X: x, Y: y + descent}, a.Label.Text)
272 y += a.Label.TextStyle.Height(a.Label.Text)
273 y += a.Label.Padding
274 }
275
276 marks := a.Tick.Marker.Ticks(a.Min, a.Max)
277 ticklabelheight := tickLabelHeight(a.Tick.Label, marks)
278 descent := a.Tick.Label.FontExtents().Descent
279 for _, t := range marks {
280 x := c.X(a.Norm(t.Value))
281 if !c.ContainsX(x) || t.IsMinor() {
282 continue
283 }
284 c.FillText(a.Tick.Label, vg.Point{X: x, Y: y + ticklabelheight + descent}, t.Label)
285 }
286
287 if len(marks) > 0 {
288 y += ticklabelheight
289 } else {
290 y += a.Width / 2
291 }
292
293 if len(marks) > 0 && a.drawTicks() {
294 len := a.Tick.Length
295 for _, t := range marks {
296 x := c.X(a.Norm(t.Value))
297 if !c.ContainsX(x) {
298 continue
299 }
300 start := t.lengthOffset(len)
301 c.StrokeLine2(a.Tick.LineStyle, x, y+start, x, y+len)
302 }
303 y += len
304 }
305
306 c.StrokeLine2(a.LineStyle, c.Min.X, y, c.Max.X, y)
307 }
308
309
310 func (a horizontalAxis) GlyphBoxes(p *Plot) []GlyphBox {
311 var (
312 boxes []GlyphBox
313 yoff font.Length
314 )
315
316 if a.Label.Text != "" {
317 x := a.Norm(p.X.Max)
318 switch a.Label.Position {
319 case draw.PosCenter:
320 x = a.Norm(0.5 * (p.X.Max + p.X.Min))
321 case draw.PosRight:
322 x -= a.Norm(0.5 * a.Label.TextStyle.Width(a.Label.Text).Points())
323 }
324 descent := a.Label.TextStyle.FontExtents().Descent
325 boxes = append(boxes, GlyphBox{
326 X: x,
327 Rectangle: a.Label.TextStyle.Rectangle(a.Label.Text).Add(vg.Point{Y: yoff + descent}),
328 })
329 yoff += a.Label.TextStyle.Height(a.Label.Text)
330 yoff += a.Label.Padding
331 }
332
333 var (
334 marks = a.Tick.Marker.Ticks(a.Min, a.Max)
335 height = tickLabelHeight(a.Tick.Label, marks)
336 descent = a.Tick.Label.FontExtents().Descent
337 )
338 for _, t := range marks {
339 if t.IsMinor() {
340 continue
341 }
342 box := GlyphBox{
343 X: a.Norm(t.Value),
344 Rectangle: a.Tick.Label.Rectangle(t.Label).Add(vg.Point{Y: yoff + height + descent}),
345 }
346 boxes = append(boxes, box)
347 }
348 return boxes
349 }
350
351
352 type verticalAxis struct {
353 Axis
354 }
355
356
357 func (a verticalAxis) size() (w vg.Length) {
358 if a.Label.Text != "" {
359 w += a.Label.TextStyle.FontExtents().Descent
360 w += a.Label.TextStyle.Height(a.Label.Text)
361 w += a.Label.Padding
362 }
363
364 marks := a.Tick.Marker.Ticks(a.Min, a.Max)
365 if len(marks) > 0 {
366 if lwidth := tickLabelWidth(a.Tick.Label, marks); lwidth > 0 {
367 w += lwidth
368 w += a.Label.TextStyle.Width(" ")
369 }
370 if a.drawTicks() {
371 w += a.Tick.Length
372 }
373 }
374 w += a.Width / 2
375 w += a.Padding
376
377 return w
378 }
379
380
381 func (a verticalAxis) draw(c draw.Canvas) {
382 var (
383 x = c.Min.X
384 y vg.Length
385 )
386 if a.Label.Text != "" {
387 sty := a.Label.TextStyle
388 sty.Rotation += math.Pi / 2
389 x += a.Label.TextStyle.Height(a.Label.Text)
390 switch a.Label.Position {
391 case draw.PosCenter:
392 y = c.Center().Y
393 case draw.PosTop:
394 y = c.Max.Y
395 y -= a.Label.TextStyle.Width(a.Label.Text) / 2
396 }
397 descent := a.Label.TextStyle.FontExtents().Descent
398 c.FillText(sty, vg.Point{X: x - descent, Y: y}, a.Label.Text)
399 x += descent
400 x += a.Label.Padding
401 }
402 marks := a.Tick.Marker.Ticks(a.Min, a.Max)
403 if w := tickLabelWidth(a.Tick.Label, marks); len(marks) > 0 && w > 0 {
404 x += w
405 }
406
407 major := false
408 descent := a.Tick.Label.FontExtents().Descent
409 for _, t := range marks {
410 y := c.Y(a.Norm(t.Value))
411 if !c.ContainsY(y) || t.IsMinor() {
412 continue
413 }
414 c.FillText(a.Tick.Label, vg.Point{X: x, Y: y + descent}, t.Label)
415 major = true
416 }
417 if major {
418 x += a.Tick.Label.Width(" ")
419 }
420 if a.drawTicks() && len(marks) > 0 {
421 len := a.Tick.Length
422 for _, t := range marks {
423 y := c.Y(a.Norm(t.Value))
424 if !c.ContainsY(y) {
425 continue
426 }
427 start := t.lengthOffset(len)
428 c.StrokeLine2(a.Tick.LineStyle, x+start, y, x+len, y)
429 }
430 x += len
431 }
432
433 c.StrokeLine2(a.LineStyle, x, c.Min.Y, x, c.Max.Y)
434 }
435
436
437 func (a verticalAxis) GlyphBoxes(p *Plot) []GlyphBox {
438 var (
439 boxes []GlyphBox
440 xoff font.Length
441 )
442
443 if a.Label.Text != "" {
444 yoff := a.Norm(p.Y.Max)
445 switch a.Label.Position {
446 case draw.PosCenter:
447 yoff = a.Norm(0.5 * (p.Y.Max + p.Y.Min))
448 case draw.PosTop:
449 yoff -= a.Norm(0.5 * a.Label.TextStyle.Width(a.Label.Text).Points())
450 }
451
452 sty := a.Label.TextStyle
453 sty.Rotation += math.Pi / 2
454
455 xoff += a.Label.TextStyle.Height(a.Label.Text)
456 descent := a.Label.TextStyle.FontExtents().Descent
457 boxes = append(boxes, GlyphBox{
458 Y: yoff,
459 Rectangle: sty.Rectangle(a.Label.Text).Add(vg.Point{X: xoff - descent}),
460 })
461 xoff += descent
462 xoff += a.Label.Padding
463 }
464
465 marks := a.Tick.Marker.Ticks(a.Min, a.Max)
466 if w := tickLabelWidth(a.Tick.Label, marks); len(marks) != 0 && w > 0 {
467 xoff += w
468 }
469
470 var (
471 ext = a.Tick.Label.FontExtents()
472 desc = ext.Height - ext.Ascent
473 )
474 for _, t := range marks {
475 if t.IsMinor() {
476 continue
477 }
478 box := GlyphBox{
479 Y: a.Norm(t.Value),
480 Rectangle: a.Tick.Label.Rectangle(t.Label).Add(vg.Point{X: xoff, Y: desc}),
481 }
482 boxes = append(boxes, box)
483 }
484 return boxes
485 }
486
487
488
489 type DefaultTicks struct{}
490
491 var _ Ticker = DefaultTicks{}
492
493
494 func (DefaultTicks) Ticks(min, max float64) []Tick {
495 if max <= min {
496 panic("illegal range")
497 }
498
499 const suggestedTicks = 3
500
501 labels, step, q, mag := talbotLinHanrahan(min, max, suggestedTicks, withinData, nil, nil, nil)
502 majorDelta := step * math.Pow10(mag)
503 if q == 0 {
504
505
506 majorDelta = labels[1] - labels[0]
507 }
508
509
510
511 fc := byte('f')
512 var off int
513 if mag < -1 || 6 < mag {
514 off = 1
515 fc = 'g'
516 }
517 if math.Trunc(q) != q {
518 off += 2
519 }
520 prec := minInt(6, maxInt(off, -mag))
521 ticks := make([]Tick, len(labels))
522 for i, v := range labels {
523 ticks[i] = Tick{Value: v, Label: strconv.FormatFloat(v, fc, prec, 64)}
524 }
525
526 var minorDelta float64
527
528 switch step {
529 case 1, 2.5:
530 minorDelta = majorDelta / 5
531 case 2, 3, 4, 5:
532 minorDelta = majorDelta / step
533 default:
534 if majorDelta/2 < dlamchP {
535 return ticks
536 }
537 minorDelta = majorDelta / 2
538 }
539
540
541
542 var i float64
543 for labels[0]+(i-1)*minorDelta > min {
544 i--
545 }
546
547
548
549 for {
550 val := labels[0] + i*minorDelta
551 if val > max {
552 break
553 }
554 found := false
555 for _, t := range ticks {
556 if math.Abs(t.Value-val) < minorDelta/2 {
557 found = true
558 }
559 }
560 if !found {
561 ticks = append(ticks, Tick{Value: val})
562 }
563 i++
564 }
565
566 return ticks
567 }
568
569 func minInt(a, b int) int {
570 if a < b {
571 return a
572 }
573 return b
574 }
575
576 func maxInt(a, b int) int {
577 if a > b {
578 return a
579 }
580 return b
581 }
582
583
584
585 type LogTicks struct {
586
587
588 Prec int
589 }
590
591 var _ Ticker = LogTicks{}
592
593
594 func (t LogTicks) Ticks(min, max float64) []Tick {
595 if min <= 0 || max <= 0 {
596 panic("Values must be greater than 0 for a log scale.")
597 }
598
599 val := math.Pow10(int(math.Log10(min)))
600 max = math.Pow10(int(math.Ceil(math.Log10(max))))
601 var ticks []Tick
602 for val < max {
603 for i := 1; i < 10; i++ {
604 if i == 1 {
605 ticks = append(ticks, Tick{Value: val, Label: formatFloatTick(val, t.Prec)})
606 }
607 ticks = append(ticks, Tick{Value: val * float64(i)})
608 }
609 val *= 10
610 }
611 ticks = append(ticks, Tick{Value: val, Label: formatFloatTick(val, t.Prec)})
612
613 return ticks
614 }
615
616
617
618 type ConstantTicks []Tick
619
620 var _ Ticker = ConstantTicks{}
621
622
623 func (ts ConstantTicks) Ticks(float64, float64) []Tick {
624 return ts
625 }
626
627
628 func UnixTimeIn(loc *time.Location) func(t float64) time.Time {
629 return func(t float64) time.Time {
630 return time.Unix(int64(t), 0).In(loc)
631 }
632 }
633
634
635 var UTCUnixTime = UnixTimeIn(time.UTC)
636
637
638 type TimeTicks struct {
639
640
641 Ticker Ticker
642
643
644
645 Format string
646
647
648
649 Time func(t float64) time.Time
650 }
651
652 var _ Ticker = TimeTicks{}
653
654
655 func (t TimeTicks) Ticks(min, max float64) []Tick {
656 if t.Ticker == nil {
657 t.Ticker = DefaultTicks{}
658 }
659 if t.Format == "" {
660 t.Format = time.RFC3339
661 }
662 if t.Time == nil {
663 t.Time = UTCUnixTime
664 }
665
666 ticks := t.Ticker.Ticks(min, max)
667 for i := range ticks {
668 tick := &ticks[i]
669 if tick.Label == "" {
670 continue
671 }
672 tick.Label = t.Time(tick.Value).Format(t.Format)
673 }
674 return ticks
675 }
676
677
678 type Tick struct {
679
680 Value float64
681
682
683
684
685 Label string
686 }
687
688
689 func (t Tick) IsMinor() bool {
690 return t.Label == ""
691 }
692
693
694
695
696
697 func (t Tick) lengthOffset(len vg.Length) vg.Length {
698 if t.IsMinor() {
699 return len / 2
700 }
701 return 0
702 }
703
704
705 func tickLabelHeight(sty text.Style, ticks []Tick) vg.Length {
706 maxHeight := vg.Length(0)
707 for _, t := range ticks {
708 if t.IsMinor() {
709 continue
710 }
711 r := sty.Rectangle(t.Label)
712 h := r.Max.Y - r.Min.Y
713 if h > maxHeight {
714 maxHeight = h
715 }
716 }
717 return maxHeight
718 }
719
720
721 func tickLabelWidth(sty text.Style, ticks []Tick) vg.Length {
722 maxWidth := vg.Length(0)
723 for _, t := range ticks {
724 if t.IsMinor() {
725 continue
726 }
727 r := sty.Rectangle(t.Label)
728 w := r.Max.X - r.Min.X
729 if w > maxWidth {
730 maxWidth = w
731 }
732 }
733 return maxWidth
734 }
735
736
737
738 func formatFloatTick(v float64, prec int) string {
739 return strconv.FormatFloat(v, 'g', prec, 64)
740 }
741
742
743
744 type TickerFunc func(min, max float64) []Tick
745
746 var _ Ticker = TickerFunc(nil)
747
748
749 func (f TickerFunc) Ticks(min, max float64) []Tick {
750 return f(min, max)
751 }
752
View as plain text