func (plot *Plot) RenderGuides() { maxWidth := vg.Length(0) yCum := vg.Length(0) ySep := vg.Length(5) // TODO; make configurable guides := GrobGroup{x0: 0, y0: 0} for aes, scale := range plot.Scales { if aes == "x" || aes == "y" { // X and y axes are draw on a per-panel base. continue } fmt.Printf("%s\n", scale.String()) grobs, width, height := scale.Render() if width > maxWidth { maxWidth = width } gg := grobs.(GrobGroup) gg.y0 = float64(yCum) guides.elements = append(guides.elements, gg) yCum += height + ySep } plot.Grobs["Guides"] = guides plot.renderInfo["Guides.Width"] = maxWidth }
func (text GrobText) Draw(vp Viewport) { vp.Canvas.Push() vp.Canvas.SetColor(text.color) x, y := vp.X(text.x), vp.Y(text.y) font := text.Font() ww, hh := text.BoundingBox() // w := font.Width(text.text) h := font.Extents().Ascent dx := ww * vg.Length(text.hjust) dy := hh * vg.Length(text.vjust) if text.angle <= math.Pi/2 { dx -= h * vg.Length(math.Sin(text.angle)) } else if text.angle <= math.Pi { dx -= ww dy += h * vg.Length(math.Cos(text.angle)) } else { panic("Implement me....") } vp.Canvas.Translate(x-dx, y-dy) vp.Canvas.Rotate(text.angle) vp.Canvas.FillString(font, 0, 0, text.text) // fmt.Printf("Printed %s %.1f %.1f\n", text.String(), ww, hh) vp.Canvas.Pop() }
// 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 } }
// ticsExtents computes the width of the y-tics and the height of the x-tics // needed to display the tics. func (plot *Plot) ticsExtents() (ywidth, xheight vg.Length) { label := MergeStyles(plot.Theme.TicLabel, DefaultTheme.TicLabel) size := String2Float(label["size"], 4, 36) angle := String2Float(label["angle"], 0, 2*math.Pi) // TODO: Should be different for x and y. sep := vg.Length(String2Float(label["sep"], 0, 100)) tic := MergeStyles(plot.Theme.Tic, DefaultTheme.Tic) length := vg.Length(String2Float(tic["length"], 0, 100)) // Look for longest y label. for r := range plot.Panels { sy := plot.Panels[r][0].Scales["y"] for _, label := range sy.Labels { w, _ := GrobText{text: label, size: size, angle: angle}.BoundingBox() if w > ywidth { ywidth = w } } } ywidth += length + sep // Look for highest x label. for c := range plot.Panels[0] { sx := plot.Panels[0][c].Scales["x"] for _, label := range sx.Labels { _, h := GrobText{text: label, size: size, angle: angle}.BoundingBox() if h > xheight { xheight = h } } } xheight += length + sep return ywidth, xheight }
// 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 }
func (vp Viewport) Y(y float64) vg.Length { ans := vp.Y0 if !vp.Direct { ans += vg.Length(y) * vp.Height } else { ans += vg.Length(y) } // fmt.Printf("Y( %.3f ) = %.1fin\n", y, ans.Inches()) return ans }
// X and Y turn natural grob coordinates [0,1] to canvas lengths. func (vp Viewport) X(x float64) vg.Length { ans := vp.X0 if !vp.Direct { ans += vg.Length(x) * vp.Width } else { ans += vg.Length(x) } // fmt.Printf("X( %.3f ) = %.1fin\n", x, ans.Inches()) return ans }
func (group GrobGroup) Draw(vp Viewport) { // x0, y0 := vp.X(group.x0), vp.Y(group.y0) // fmt.Printf("Guides translate %.1f %.1f\n", x0,y0) vp.Canvas.Push() vp.Canvas.Translate(vg.Length(group.x0), vg.Length(group.y0)) for _, g := range group.elements { g.Draw(vp) } vp.Canvas.Pop() }
// SubViewport returns the area described by x0,y0,width,height in // natural grob coordinates [0,1] as a viewport. func (vp Viewport) Sub(x0, y0, width, height float64) Viewport { sub := Viewport{ X0: vp.X0 + vg.Length(x0)*vp.Width, Y0: vp.Y0 + vg.Length(y0)*vp.Height, Width: vg.Length(width) * vp.Width, Height: vg.Length(height) * vp.Height, Canvas: vp.Canvas, } return sub }
func (text GrobText) BoundingBox() (vg.Length, vg.Length) { font := text.Font() // Compute width ww and height hh of the rotateted bounding box. w := font.Width(text.text) h := font.Extents().Ascent s := math.Sin(text.angle) z := vg.Length(math.Sqrt(1 - s*s)) ww := w*z + h*vg.Length(s) hh := w*vg.Length(s) + h*z return ww, hh }
// 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 }
// CreateImage creates graph of nyanpass func (n *Nyanpass) CreateImage(fileName string) error { if n.Counts == nil { return errors.New("Count is not defined.") } p, err := plot.New() if err != nil { return err } bar, err := plotter.NewBarChart(n.Counts, vg.Points(30)) if err != nil { return err } bar.LineStyle.Width = vg.Length(0) bar.Color = plotutil.Color(2) p.Add(bar) p.Title.Text = "Nyanpass Graph" p.X.Label.Text = "Days" p.Y.Label.Text = "Nyanpass count" p.NominalX(n.labels...) p.Y.Tick.Marker = RelabelTicks{} if err := p.Save(6*vg.Inch, 6*vg.Inch, fileName); err != nil { return err } n.imagePath = fileName return nil }
// 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)) }
func (text GrobText) Font() vg.Font { fname := text.font if fname == "" { fname = "Courier-Bold" } font, err := vg.MakeFont(fname, vg.Length(text.size)) if err != nil { panic(err.Error()) } return font }
// 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 }
// 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}, }, } }
// 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}, }, } }
func (g *Graph) Save(filename string, w, h int) error { g.Plot.Add(plotter.NewGrid()) for _, c := range g.Curves { lpLine, lpPoints, err := plotter.NewLinePoints(c.Points) if err != nil { return err } if len(c.RGB) != 3 { return errors.New("bad RGB") } color := color.RGBA{R: c.RGB[0], G: c.RGB[1], B: c.RGB[2], A: 255} lpLine.LineStyle.Color = color lpPoints.Color = color g.Plot.Add(lpLine, lpPoints) if c.Name != "" { g.Plot.Legend.Add(c.Name, lpLine, lpPoints) } } return g.Plot.Save(vg.Length(w), vg.Length(h), filename) }
// 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 }
// 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 }
func drawTopReleaseDownloads(ctx *context, per *period, filename string) { var rs releases ctx.WalkReleases(func(r github.RepositoryRelease) { var cnt int if r.CreatedAt.Before(per.start) || r.CreatedAt.After(per.end) { return } for _, a := range r.Assets { cnt += *a.DownloadCount } rs = append(rs, release{name: *r.TagName, download: cnt}) }) sort.Sort(rs) var names []string var downloads []int num := 10 if num > len(rs) { num = len(rs) } for i := 0; i < num; i++ { names = append(names, rs[i].name) downloads = append(downloads, rs[i].download) } p, err := plot.New() if err != nil { panic(err) } p.Title.Text = "Release Downloads" p.Y.Label.Text = "Download Count" if len(names) > 0 { p.NominalX(names...) bars, err := plotter.NewBarChart(ints(downloads), vg.Points(20)) if err != nil { panic(err) } bars.LineStyle.Width = vg.Length(0) p.Add(bars) } // Save the plot to a PNG file. if err := p.Save(defaultWidth, defaultHeight, filename); err != nil { panic(err) } }
func plotTableSizes(sess *r.Session) { sizes := []float64{} for _, t := range tables { sizes = append(sizes, float64(averageDocumentSize(sess, t))) } p, _ := plot.New() p.Title.Text = "Average document sizes" p.Y.Label.Text = "Size in bytes" w := vg.Points(20) bars, _ := plotter.NewBarChart(plotter.Values(sizes), w) bars.Color = plotutil.Color(0) bars.LineStyle.Width = vg.Length(0) p.Add(bars) p.NominalX(tables...) p.Save(6*vg.Inch, 6*vg.Inch, "avg_doc_sizes.png") }
// 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 } }
// At returns the subcanvas within c that corresponds to the // tile at column x, row y. func (ts Tiles) At(c Canvas, x, y int) Canvas { tileH := (c.Max.Y - c.Min.Y - ts.PadTop - ts.PadBottom - vg.Length(ts.Rows-1)*ts.PadY) / vg.Length(ts.Rows) tileW := (c.Max.X - c.Min.X - ts.PadLeft - ts.PadRight - vg.Length(ts.Cols-1)*ts.PadX) / vg.Length(ts.Cols) ymax := c.Max.Y - ts.PadTop - vg.Length(y)*(ts.PadY+tileH) ymin := ymax - tileH xmin := c.Min.X + ts.PadLeft + vg.Length(x)*(ts.PadX+tileW) xmax := xmin + tileW return Canvas{ Canvas: vg.Canvas(c), Rectangle: Rectangle{ Min: Point{X: xmin, Y: ymin}, Max: Point{X: xmax, Y: ymax}, }, } }
func unity(f float64) vg.Length { return vg.Length(f) }
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) } } }
// 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 }
// 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 }
// 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)