// Approximate a circular arc using multiple // cubic Bézier curves, one for each π/2 segment. // // This is from: // http://hansmuller-flex.blogspot.com/2011/04/approximating-circular-arc-with-cubic.html func arc(p *pdf.Path, comp vg.PathComp) { x0 := comp.X + comp.Radius*vg.Length(math.Cos(comp.Start)) y0 := comp.Y + comp.Radius*vg.Length(math.Sin(comp.Start)) p.Line(pdfPoint(x0, y0)) a1 := comp.Start end := a1 + comp.Angle sign := 1.0 if end < a1 { sign = -1.0 } left := math.Abs(comp.Angle) // Square root of the machine epsilon for IEEE 64-bit floating // point values. This is the equality threshold recommended // in Numerical Recipes, if I recall correctly—it's small enough. const epsilon = 1.4901161193847656e-08 for left > epsilon { a2 := a1 + sign*math.Min(math.Pi/2, left) partialArc(p, comp.X, comp.Y, comp.Radius, a1, a2) left -= math.Abs(a2 - a1) a1 = a2 } }
// Height returns the height of the text when using // the given font. func (sty TextStyle) Height(txt string) vg.Length { nl := textNLines(txt) if nl == 0 { return vg.Length(0) } e := sty.Font.Extents() return e.Height*vg.Length(nl-1) + e.Ascent }
// GlyphBoxes returns a slice of GlyphBoxes, // one for each of the labels, implementing the // plot.GlyphBoxer interface. func (l *Labels) GlyphBoxes(p *plot.Plot) []plot.GlyphBox { bs := make([]plot.GlyphBox, len(l.Labels)) for i, label := range l.Labels { bs[i].X = p.X.Norm(l.XYs[i].X) bs[i].Y = p.Y.Norm(l.XYs[i].Y) w := l.Width(label) h := l.Height(label) bs[i].Rect.Min.X = w*vg.Length(l.XAlign) + l.XOffset bs[i].Rect.Min.Y = h*vg.Length(l.YAlign) + l.YOffset bs[i].Rect.Size.X = w bs[i].Rect.Size.Y = h } return bs }
// draw draws the legend to the given DrawArea. func (l *Legend) draw(da DrawArea) { iconx := da.Min.X textx := iconx + l.ThumbnailWidth + l.TextStyle.Width(" ") xalign := 0.0 if !l.Left { iconx = da.Max().X - l.ThumbnailWidth textx = iconx - l.TextStyle.Width(" ") xalign = -1 } textx += l.XOffs iconx += l.XOffs enth := l.entryHeight() y := da.Max().Y - enth if !l.Top { y = da.Min.Y + (enth+l.Padding)*(vg.Length(len(l.entries))-1) } y += l.YOffs icon := &DrawArea{ Canvas: da.Canvas, Rect: Rect{Min: Point{iconx, y}, Size: Point{l.ThumbnailWidth, enth}}, } for _, e := range l.entries { for _, t := range e.thumbs { t.Thumbnail(icon) } yoffs := (enth - l.TextStyle.Height(e.text)) / 2 da.FillText(l.TextStyle, textx, icon.Min.Y+yoffs, xalign, 0, e.text) icon.Min.Y -= enth + l.Padding } }
// FillText fills lines of text in the draw area. // The text is offset by its width times xalign and // its height times yalign. x and y give the bottom // left corner of the text befor e it is offset. func (da *DrawArea) FillText(sty TextStyle, x, y vg.Length, xalign, yalign float64, txt string) { txt = strings.TrimRight(txt, "\n") if len(txt) == 0 { return } da.SetColor(sty.Color) ht := sty.Height(txt) y += ht*vg.Length(yalign) - sty.Font.Extents().Ascent nl := textNLines(txt) for i, line := range strings.Split(txt, "\n") { xoffs := vg.Length(xalign) * sty.Font.Width(line) n := vg.Length(nl - i) da.FillString(sty.Font, x+xoffs, y+n*sty.Font.Size, line) } }
// radius returns the radius of a bubble by linear interpolation. func (bs *Bubbles) radius(z float64) vg.Length { rng := bs.MaxRadius - bs.MinRadius if bs.MaxZ == bs.MinZ { return rng/2 + bs.MinRadius } d := (z - bs.MinZ) / (bs.MaxZ - bs.MinZ) return vg.Length(d)*rng + bs.MinRadius }
// Approximate a circular arc of fewer than π/2 // radians with cubic Bézier curve. func partialArc(p *pdf.Path, x, y, r vg.Length, a1, a2 float64) { a := (a2 - a1) / 2 x4 := r * vg.Length(math.Cos(a)) y4 := r * vg.Length(math.Sin(a)) x1 := x4 y1 := -y4 const k = 0.5522847498 // some magic constant f := k * vg.Length(math.Tan(a)) x2 := x1 + f*y4 y2 := y1 + f*x4 x3 := x2 y3 := -y2 // Rotate and translate points into position. ar := a + a1 sinar := vg.Length(math.Sin(ar)) cosar := vg.Length(math.Cos(ar)) x2r := x2*cosar - y2*sinar + x y2r := x2*sinar + y2*cosar + y x3r := x3*cosar - y3*sinar + x y3r := x3*sinar + y3*cosar + y x4 = r*vg.Length(math.Cos(a2)) + x y4 = r*vg.Length(math.Sin(a2)) + y p.Curve(pdfPoint(x2r, y2r), pdfPoint(x3r, y3r), pdfPoint(x4, y4)) }
// padY returns a DrawArea that is padded vertically // so that glyphs will no be clipped. func padY(p *Plot, da DrawArea) DrawArea { glyphs := p.GlyphBoxes(p) b := bottomMost(&da, glyphs) yAxis := verticalAxis{p.Y} glyphs = append(glyphs, yAxis.GlyphBoxes(p)...) t := topMost(&da, glyphs) miny := da.Min.Y - b.Min.Y maxy := da.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 DrawArea{ Canvas: vg.Canvas(da), Rect: Rect{ Min: Point{Y: n, X: da.Min.X}, Size: Point{Y: m - n, X: da.Size.X}, }, } }
// padX returns a DrawArea that is padded horizontally // so that glyphs will no be clipped. func padX(p *Plot, da DrawArea) DrawArea { glyphs := p.GlyphBoxes(p) l := leftMost(&da, glyphs) xAxis := horizontalAxis{p.X} glyphs = append(glyphs, xAxis.GlyphBoxes(p)...) r := rightMost(&da, glyphs) minx := da.Min.X - l.Min.X maxx := da.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 DrawArea{ Canvas: vg.Canvas(da), Rect: Rect{ Min: Point{X: n, Y: da.Min.Y}, Size: Point{X: m - n, Y: da.Size.Y}, }, } }
// tickLabelWidth returns the width of the widest tick mark label. func tickLabelWidth(sty TextStyle, ticks []Tick) vg.Length { maxWidth := vg.Length(0) for _, t := range ticks { if t.IsMinor() { continue } w := sty.Width(t.Label) if w > maxWidth { maxWidth = w } } return maxWidth }
// tickLabelHeight returns height of the tick mark labels. func tickLabelHeight(sty TextStyle, ticks []Tick) vg.Length { maxHeight := vg.Length(0) for _, t := range ticks { if t.IsMinor() { continue } h := sty.Height(t.Label) if h > maxHeight { maxHeight = h } } return maxHeight }
// Y returns the value of x, given in the unit range, // in the drawing coordinates of this draw area. // A value of 0, for example, will return the minimum // y value of the draw area and a value of 1 will // return the maximum. func (da *DrawArea) Y(y float64) vg.Length { return vg.Length(y)*(da.Max().Y-da.Min.Y) + da.Min.Y }
// X returns the value of x, given in the unit range, // in the drawing coordinates of this draw area. // A value of 0, for example, will return the minimum // x value of the draw area and a value of 1 will // return the maximum. func (da *DrawArea) X(x float64) vg.Length { return vg.Length(x)*(da.Max().X-da.Min.X) + da.Min.X }
// RingGlyph is a glyph that draws the outline of a circle. type RingGlyph struct{} // DrawGlyph implements the Glyph interface. func (RingGlyph) DrawGlyph(da *DrawArea, sty GlyphStyle, pt Point) { da.SetLineStyle(LineStyle{Color: sty.Color, Width: vg.Points(0.5)}) var p vg.Path p.Move(pt.X+sty.Radius, pt.Y) p.Arc(pt.X, pt.Y, sty.Radius, 0, 2*math.Pi) p.Close() da.Stroke(p) } const ( cosπover4 = vg.Length(.707106781202420) sinπover6 = vg.Length(.500000000025921) cosπover6 = vg.Length(.866025403769473) ) // SquareGlyph is a glyph that draws the outline of a square. type SquareGlyph struct{} // DrawGlyph implements the Glyph interface. func (SquareGlyph) DrawGlyph(da *DrawArea, sty GlyphStyle, pt Point) { da.SetLineStyle(LineStyle{Color: sty.Color, Width: vg.Points(0.5)}) x := (sty.Radius-sty.Radius*cosπover4)/2 + sty.Radius*cosπover4 var p vg.Path p.Move(pt.X-x, pt.Y-x) p.Line(pt.X+x, pt.Y-x) p.Line(pt.X+x, pt.Y+x)
func TestBubblesRadius(t *testing.T) { b := &Bubbles{ MinRadius: vg.Length(0), MaxRadius: vg.Length(1), } tests := []struct { minz, maxz, z float64 r vg.Length }{ {0, 0, 0, vg.Length(0.5)}, {1, 1, 1, vg.Length(0.5)}, {0, 1, 0, vg.Length(0)}, {0, 1, 1, vg.Length(1)}, {0, 1, 0.5, vg.Length(0.5)}, {0, 2, 1, vg.Length(0.5)}, {0, 4, 0, vg.Length(0)}, {0, 4, 1, vg.Length(0.25)}, {0, 4, 2, vg.Length(0.5)}, {0, 4, 3, vg.Length(0.75)}, {0, 4, 4, vg.Length(1)}, } for _, test := range tests { b.MinZ, b.MaxZ = test.minz, test.maxz if r := b.radius(test.z); r != test.r { t.Errorf("Got incorrect radius (%g) on %v", r, test) } } }