コード例 #1
0
ファイル: plot.go プロジェクト: vdobler/plot
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
}
コード例 #2
0
ファイル: grob.go プロジェクト: vdobler/plot
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()

}
コード例 #3
0
ファイル: vgpdf.go プロジェクト: zzn01/plot
// 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
	}
}
コード例 #4
0
ファイル: plot.go プロジェクト: vdobler/plot
// 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
}
コード例 #5
0
ファイル: canvas.go プロジェクト: nolenroyalty/bangarang
// 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
}
コード例 #6
0
ファイル: grob.go プロジェクト: vdobler/plot
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
}
コード例 #7
0
ファイル: grob.go プロジェクト: vdobler/plot
// 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
}
コード例 #8
0
ファイル: grob.go プロジェクト: vdobler/plot
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()
}
コード例 #9
0
ファイル: grob.go プロジェクト: vdobler/plot
// 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
}
コード例 #10
0
ファイル: grob.go プロジェクト: vdobler/plot
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
}
コード例 #11
0
ファイル: labels.go プロジェクト: nolenroyalty/bangarang
// 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
}
コード例 #12
0
ファイル: nyanpass.go プロジェクト: Rompei/nyanpass-graph2
// 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
}
コード例 #13
0
ファイル: canvas.go プロジェクト: nolenroyalty/bangarang
// 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)
	}
}
コード例 #14
0
ファイル: vgpdf.go プロジェクト: zzn01/plot
// 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))
}
コード例 #15
0
ファイル: grob.go プロジェクト: vdobler/plot
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
}
コード例 #16
0
ファイル: vgimg.go プロジェクト: zzn01/plot
// 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
}
コード例 #17
0
ファイル: plot.go プロジェクト: zzn01/plot
// 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},
		},
	}
}
コード例 #18
0
ファイル: plot.go プロジェクト: zzn01/plot
// 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},
		},
	}
}
コード例 #19
0
ファイル: graph.go プロジェクト: Theodus/graph
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)
}
コード例 #20
0
ファイル: axis.go プロジェクト: nolenroyalty/bangarang
// 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
}
コード例 #21
0
ファイル: axis.go プロジェクト: nolenroyalty/bangarang
// 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
}
コード例 #22
0
ファイル: graph.go プロジェクト: yichengq/issue-analyzer
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)
	}
}
コード例 #23
0
ファイル: misc.go プロジェクト: tiare-lava/worker-sizeup
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")
}
コード例 #24
0
ファイル: legend.go プロジェクト: nolenroyalty/bangarang
// 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
	}
}
コード例 #25
0
ファイル: canvas.go プロジェクト: zzn01/plot
// 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},
		},
	}
}
コード例 #26
0
ファイル: contour_test.go プロジェクト: zzn01/plot
func unity(f float64) vg.Length { return vg.Length(f) }
コード例 #27
0
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)
		}
	}
}
コード例 #28
0
ファイル: canvas.go プロジェクト: nolenroyalty/bangarang
// 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
}
コード例 #29
0
ファイル: canvas.go プロジェクト: nolenroyalty/bangarang
// 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
}
コード例 #30
0
ファイル: canvas.go プロジェクト: nolenroyalty/bangarang
// 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)