// 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].Rectangle.Min.X = w*vg.Length(l.XAlign) + l.XOffset bs[i].Rectangle.Min.Y = h*vg.Length(l.YAlign) + l.YOffset bs[i].Rectangle.Max.X = w + w*vg.Length(l.XAlign) + l.XOffset bs[i].Rectangle.Max.Y = h + h*vg.Length(l.YAlign) + l.YOffset } return bs }
// 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 }
// 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 (c *Canvas) FillText(sty TextStyle, x, y vg.Length, xalign, yalign float64, txt string) { txt = strings.TrimRight(txt, "\n") if len(txt) == 0 { return } c.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) c.FillString(sty.Font, x+xoffs, y+n*sty.Font.Size, line) } }
// 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)) }
// NewWith returns a new image canvas created according to the specified // options. The currently accepted options are UseWH, // UseDPI, UseImage, and UseImageWithContext. // Each of the options specifies the size of the canvas (UseWH, UseImage), // the resolution of the canvas (UseDPI), or both (useImageWithContext). // If size or resolution are not specified, defaults are used. // It panics if size and resolution are overspecified (i.e., too many options are // passed). func NewWith(o ...option) *Canvas { c := new(Canvas) var g uint32 for _, opt := range o { f := opt(c) if g&f != 0 { panic("incompatible options") } g |= f } if c.dpi == 0 { c.dpi = DefaultDPI } if c.w == 0 { // h should also == 0. if c.img == nil { c.w = DefaultWidth c.h = DefaultHeight } else { w := float64(c.img.Bounds().Max.X - c.img.Bounds().Min.X) h := float64(c.img.Bounds().Max.Y - c.img.Bounds().Min.Y) c.w = vg.Length(w/float64(c.dpi)) * vg.Inch c.h = vg.Length(h/float64(c.dpi)) * vg.Inch } } if c.img == nil { w := c.w / vg.Inch * vg.Length(c.dpi) h := c.h / vg.Inch * vg.Length(c.dpi) c.img = draw.Image(image.NewRGBA(image.Rect(0, 0, int(w+0.5), int(h+0.5)))) } if c.gc == nil { h := float64(c.img.Bounds().Max.Y - c.img.Bounds().Min.Y) c.gc = draw2dimg.NewGraphicContext(c.img) c.gc.SetDPI(c.dpi) c.gc.Scale(1, -1) c.gc.Translate(0, -h) } draw.Draw(c.img, c.img.Bounds(), image.White, image.ZP, draw.Src) c.color = []color.Color{color.Black} vg.Initialize(c) return c }
// 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: draw.Rectangle{ Min: draw.Point{X: n, Y: c.Min.Y}, Max: draw.Point{X: m, Y: c.Max.Y}, }, } }
// 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: draw.Rectangle{ Min: draw.Point{Y: n, X: c.Min.X}, Max: draw.Point{Y: m, X: c.Max.X}, }, } }
// tickLabelHeight returns height of the tick mark labels. func tickLabelHeight(sty draw.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 }
// tickLabelWidth returns the width of the widest tick mark label. func tickLabelWidth(sty draw.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 }
// draw draws the legend to the given draw.Canvas. func (l *Legend) draw(c draw.Canvas) { iconx := c.Min.X textx := iconx + l.ThumbnailWidth + l.TextStyle.Width(" ") xalign := 0.0 if !l.Left { iconx = c.Max.X - l.ThumbnailWidth textx = iconx - l.TextStyle.Width(" ") xalign = -1 } textx += l.XOffs iconx += l.XOffs enth := l.entryHeight() y := c.Max.Y - enth if !l.Top { y = c.Min.Y + (enth+l.Padding)*(vg.Length(len(l.entries))-1) } y += l.YOffs icon := &draw.Canvas{ Canvas: c.Canvas, Rectangle: draw.Rectangle{ Min: draw.Point{iconx, y}, Max: draw.Point{iconx + l.ThumbnailWidth, y + enth}, }, } for _, e := range l.entries { for _, t := range e.thumbs { t.Thumbnail(icon) } yoffs := (enth - l.TextStyle.Height(e.text)) / 2 c.FillText(l.TextStyle, textx, icon.Min.Y+yoffs, xalign, 0, e.text) icon.Min.Y -= enth + l.Padding icon.Max.Y -= enth + l.Padding } }
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) } } }
// 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 (c *Canvas) Y(y float64) vg.Length { return vg.Length(y)*(c.Max.Y-c.Min.Y) + c.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 (c *Canvas) X(x float64) vg.Length { return vg.Length(x)*(c.Max.X-c.Min.X) + c.Min.X }
// RingGlyph is a glyph that draws the outline of a circle. type RingGlyph struct{} // DrawGlyph implements the Glyph interface. func (RingGlyph) DrawGlyph(c *Canvas, sty GlyphStyle, pt Point) { c.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() c.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(c *Canvas, sty GlyphStyle, pt Point) { c.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 unity(f float64) vg.Length { return vg.Length(f) }