Example #1
0
File: canvas.go Project: zzn01/plot
// NewFormattedCanvas creates a new vg.CanvasWriterTo with the specified
// image format.
//
// Supported formats are:
//
//  eps, jpg|jpeg, pdf, png, svg, and tif|tiff.
func NewFormattedCanvas(w, h vg.Length, format string) (vg.CanvasWriterTo, error) {
	var c vg.CanvasWriterTo
	switch format {
	case "eps":
		c = vgeps.New(w, h)

	case "jpg", "jpeg":
		c = vgimg.JpegCanvas{Canvas: vgimg.New(w, h)}

	case "pdf":
		c = vgpdf.New(w, h)

	case "png":
		c = vgimg.PngCanvas{Canvas: vgimg.New(w, h)}

	case "svg":
		c = vgsvg.New(w, h)

	case "tif", "tiff":
		c = vgimg.TiffCanvas{Canvas: vgimg.New(w, h)}

	default:
		return nil, fmt.Errorf("unsupported format: %q", format)
	}
	return c, nil
}
Example #2
0
// WriterTo returns an io.WriterTo that will write the plot as
// the specified image format.
//
// Supported formats are:
//
//  eps, jpg|jpeg, pdf, png, svg, and tif|tiff.
func (p *Plot) WriterTo(w, h vg.Length, format string) (io.WriterTo, error) {
	var c interface {
		vg.CanvasSizer
		io.WriterTo
	}
	switch format {
	case "eps":
		c = vgeps.New(w, h)

	case "jpg", "jpeg":
		c = vgimg.JpegCanvas{Canvas: vgimg.New(w, h)}

	case "pdf":
		c = vgpdf.New(w, h)

	case "png":
		c = vgimg.PngCanvas{Canvas: vgimg.New(w, h)}

	case "svg":
		c = vgsvg.New(w, h)

	case "tif", "tiff":
		c = vgimg.TiffCanvas{Canvas: vgimg.New(w, h)}

	default:
		return nil, fmt.Errorf("unsupported format: %q", format)
	}
	p.Draw(draw.New(c))

	return c, nil
}
Example #3
0
func PlotGraph(title string, timestamps []int64, w io.Writer) error {
	p, err := plot.New()
	if err != nil {
		return err
	}

	p.Title.Text = title
	p.X.Label.Text = "Time"
	p.Y.Label.Text = "Number of stars"
	p.Y.Min = 0

	points := make(plotter.XYs, len(timestamps))
	for i, timestamp := range timestamps {
		points[i].X = float64(timestamp)
		points[i].Y = float64(i + 1)
	}
	plotutil.AddLinePoints(p, "Stars", points)

	c := vgimg.New(4*vg.Inch, 4*vg.Inch)
	cpng := vgimg.PngCanvas{c}

	p.Draw(draw.New(cpng))

	if _, err := cpng.WriteTo(w); err != nil {
		return err
	}
	return nil
}
Example #4
0
func TestIssue179(t *testing.T) {
	scatter, err := plotter.NewScatter(plotter.XYs{{1, 1}, {0, 1}, {0, 0}})
	if err != nil {
		log.Fatal(err)
	}
	p, err := plot.New()
	if err != nil {
		log.Fatal(err)
	}
	p.Add(scatter)
	p.HideAxes()

	c := vgimg.JpegCanvas{Canvas: vgimg.New(5.08*vg.Centimeter, 5.08*vg.Centimeter)}
	p.Draw(draw.New(c))
	b := bytes.NewBuffer([]byte{})
	if _, err = c.WriteTo(b); err != nil {
		t.Error(err)
	}

	f, err := os.Open(filepath.Join("testdata", "issue179.jpg"))
	if err != nil {
		t.Error(err)
	}
	defer f.Close()

	want, err := ioutil.ReadAll(f)
	if err != nil {
		t.Error(err)
	}
	if !bytes.Equal(b.Bytes(), want) {
		t.Error("Image mismatch")
	}
}
Example #5
0
func TestTextGrobs(t *testing.T) {
	// Output
	file, err := os.Create("text.png")
	if err != nil {
		t.Fatalf("%", err)
	}

	pngCanvas := vgimg.PngCanvas{Canvas: vgimg.New(10*vg.Inch, 8*vg.Inch)}
	vg.Initialize(pngCanvas)

	allVP := Viewport{
		X0:     0,
		Y0:     0,
		Width:  10 * vg.Inch,
		Height: 8 * vg.Inch,
		Canvas: pngCanvas,
	}
	bg := GrobRect{xmin: 0, ymin: 0, xmax: 1, ymax: 1, fill: BuiltinColors["gray60"]}
	bg.Draw(allVP)

	gridVP := allVP.Sub(0.1, 0.1, 0.35, 0.35)
	drawTextGrid(gridVP, 0)
	gridVP = allVP.Sub(0.55, 0.1, 0.35, 0.35)
	drawTextGrid(gridVP, 45./180*math.Pi)
	gridVP = allVP.Sub(0.1, 0.55, 0.35, 0.35)
	drawTextGrid(gridVP, 135./180*math.Pi)
	gridVP = allVP.Sub(0.55, 0.55, 0.35, 0.35)
	drawTextGrid(gridVP, 90./180*math.Pi)

	pngCanvas.WriteTo(file)
	file.Close()
}
Example #6
0
File: plot.go Project: vdobler/plot
// WritePNG renders plot to a png file of size width x height.
func (p *Plot) WritePNG(filename string, width, height vg.Length) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	canvas := vgimg.PngCanvas{Canvas: vgimg.New(width, height)}
	canvas.Translate(-width/2, -height/2)
	p.DumpTo(canvas, width, height)
	canvas.WriteTo(file)
	return nil
}
Example #7
0
func TestIndividualSteps(t *testing.T) {
	aes := AesMapping{
		"x": "Height",
		"y": "Weight",
	}
	plot, err := NewPlot(measurement, aes)
	if err != nil {
		t.Fatalf("Unxpected error: %s", err)
	}

	//
	// Add layers to plot
	//

	rawData := Layer{
		Name: "Raw Data",
		Stat: nil, // identity
		Geom: GeomPoint{
			Style: AesMapping{
				"color": "red",
				"shape": "diamond",
			}}}
	plot.Layers = append(plot.Layers, &rawData)

	linReg := Layer{
		Name: "Linear regression",
		Stat: &StatLinReq{},
		Geom: GeomABLine{
			Style: AesMapping{
				"color":    "green",
				"linetype": "dashed",
			},
		},
		// StatLinReq produces intercept/slope suitable for GeomABLine
		GeomMapping: nil,
	}
	plot.Layers = append(plot.Layers, &linReg)

	ageLabel := Layer{
		Name: "Age Label",
		DataMapping: AesMapping{
			"value": "Age",
		},
		Stat: &StatLabel{Format: "%.0f years"},
		Geom: GeomText{
			Style: AesMapping{
				"color":  "blue",
				"angle":  "0.7", // "45°",  // TODO: parsing ° fails
				"family": "Helvetica",
				"size":   "10", // TODO: should come from DefaultTheme
			},
		},
		GeomMapping: nil,
	}
	plot.Layers = append(plot.Layers, &ageLabel)

	histogram := Layer{
		Name:        "Histogram",
		DataMapping: AesMapping{"y": ""}, // clear mapping of y to Height
		Stat:        &StatBin{Drop: true},
		StatMapping: AesMapping{
			"y": "count",
		},
		Geom: GeomBar{
			Style: AesMapping{
				"fill": "gray50",
			},
		},
	}
	plot.Layers = append(plot.Layers, &histogram)

	// Set up the one panel.
	plot.CreatePanels()
	if len(plot.Panels) != 1 {
		t.Fatalf("Got %d panel rows, expected 1.", len(plot.Panels))
	}
	if len(plot.Panels[0]) != 1 {
		t.Fatalf("Got %d panel cols, expected 1.", len(plot.Panels[0]))
	}
	panel := plot.Panels[0][0]

	// 2. PrepareData
	panel.PrepareData()
	if fields := panel.Layers[0].Data.FieldNames(); !same(fields, []string{"x", "y"}) {
		t.Errorf("Layer 0 DF has fields %v", fields)
	}
	if fields := panel.Layers[1].Data.FieldNames(); !same(fields, []string{"x", "y"}) {
		t.Errorf("Layer 1 DF has fields %v", fields)
	}
	if sx, ok := panel.Scales["x"]; !ok {
		t.Errorf("Missing x scale")
	} else {
		if sx.Discrete || sx.Transform != &IdentityScale || sx.Aesthetic != "x" {
			t.Errorf("Scale x = %+v", sx)
		}
	}
	if sy, ok := panel.Scales["y"]; !ok {
		t.Errorf("Missing y scale")
	} else {
		if sy.Discrete || sy.Transform != &IdentityScale || sy.Aesthetic != "y" {
			t.Errorf("Scale y = %+v", sy)
		}
	}

	// 3. ComputeStatistics
	panel.ComputeStatistics()

	// No statistic on layer 0: data field is unchanges
	if fields := panel.Layers[0].Data.FieldNames(); !same(fields, []string{"x", "y"}) {
		t.Errorf("Layer 0 DF has fields %v", fields)
	}
	// StatLinReq produces intercept and slope
	if fields := panel.Layers[1].Data.FieldNames(); !same(fields, []string{"intercept", "slope", "interceptErr", "slopeErr"}) {
		t.Errorf("Layer 1 DF has fields %v", fields)
	}
	data := panel.Layers[1].Data
	if data.N != 1 {
		t.Errorf("Got %d data in lin req df.", panel.Layers[1].Data.N)
	}
	t.Logf("Intercept = %.2f   Slope = %.2f",
		data.Columns["intercept"].Data[0],
		data.Columns["slope"].Data[0])
	// StatLabels produces labels
	if fields := panel.Layers[2].Data.FieldNames(); !same(fields, []string{"x", "y", "text"}) {
		t.Errorf("Layer 2 %q has fields %v", panel.Layers[3].Name, fields)
	}
	data = panel.Layers[2].Data
	if data.N != 20 {
		t.Errorf("Got %d data in label df.", panel.Layers[2].Data.N)
	}
	// StatBin produces bins
	if fields := panel.Layers[3].Data.FieldNames(); !same(fields, []string{"x", "count", "ncount", "density", "ndensity"}) {
		t.Errorf("Layer 3 %q has fields %v", panel.Layers[3].Name, fields)
	}
	data = panel.Layers[3].Data
	if data.N != 11 {
		t.Errorf("Got %d data in binned df.", panel.Layers[3].Data.N)
	}

	// 4. Wireing
	panel.WireStatToGeom()

	for a, s := range panel.Scales {
		fmt.Printf("====== Scale %s %q ========\n", a, s.Name)
		fmt.Printf("%s\n", s.String())
	}

	// 5. Test Construct ConstructGeoms. This shouldn't change much as
	// GeomABLine doesn't reparametrize and we don't do position adjustments.
	panel.ConstructGeoms()

	// 6. FinalizeScales
	panel.FinalizeScales()
	// Only x and y are set up
	if sx, ok := panel.Scales["x"]; !ok {
		t.Errorf("Missing x scale")
	} else {
		if sx.Pos == nil {
			t.Errorf("Missing Pos for x scale.")
		}
		if sx.DomainMin > 1.62 || sx.DomainMax < 1.95 {
			t.Errorf("Bad training: %f %f", sx.DomainMin, sx.DomainMax)
		}
	}
	fmt.Printf("%s\n", panel.Scales["x"])
	fmt.Printf("%s\n", panel.Scales["y"])

	// 7. Render Geoms to Grobs using scales (Step7).
	panel.RenderGeoms()
	fmt.Println("Layer 0, raw data")
	for _, grob := range panel.Layers[0].Grobs {
		fmt.Println("  ", grob.String())
	}
	fmt.Println("Layer 1, linear regression")
	for _, grob := range panel.Layers[1].Grobs {
		fmt.Println("  ", grob.String())
	}
	fmt.Println("Layer 2, labels")
	for _, grob := range panel.Layers[2].Grobs {
		fmt.Println("  ", grob.String())
	}
	fmt.Println("Layer 3, histogram")
	for _, grob := range panel.Layers[3].Grobs {
		fmt.Println("  ", grob.String())
	}

	// Output
	pngCanvas := vgimg.PngCanvas{Canvas: vgimg.New(800, 600)}
	pngCanvas.Translate(-400, -300)
	vp := Viewport{
		X0:     50,
		Y0:     50,
		Width:  700,
		Height: 500,
		Canvas: pngCanvas,
	}
	file, err := os.Create("example.png")
	if err != nil {
		t.Fatalf("%", err)
	}

	panel.Draw(vp, true, true)
	if false {
		fmt.Println("Layer 0, raw data")
		for _, grob := range panel.Layers[0].Grobs {
			grob.Draw(vp)
		}
		fmt.Println("Layer 1, linear regression")
		for _, grob := range panel.Layers[1].Grobs {
			grob.Draw(vp)
		}
		fmt.Println("Layer 2, labels")
		for _, grob := range panel.Layers[2].Grobs {
			grob.Draw(vp)
		}
		fmt.Println("Layer 3, histogram")
		for _, grob := range panel.Layers[3].Grobs {
			grob.Draw(vp)
		}
	}

	pngCanvas.WriteTo(file)
	file.Close()

}
Example #8
0
func drawPng(b *bytes.Buffer, p *plot.Plot, width, height float64) {
	w, h := vg.Inches(width), vg.Inches(height)
	c := vgimg.PngCanvas{Canvas: vgimg.New(w, h)}
	p.Draw(plot.MakeDrawArea(c))
	c.WriteTo(b)
}
Example #9
0
func (c *client) run() {
	var err error
	dir, err := ioutil.TempDir("", "snfusion-web-")
	if err != nil {
		log.Printf("error creating temporary directory: %v\n", err)
		return
	}

	defer func() {
		c.srv.unregister <- c
		c.ws.Close()
		c.srv = nil
		os.RemoveAll(dir)
	}()

	type params struct {
		ID         int     `json:"id"`
		NumIters   int     `json:"num_iters"`
		NumCarbons float64 `json:"num_carbons"`
		Seed       int64   `json:"seed"`
	}

	type genReply struct {
		ID     int        `json:"id"`
		Stage  string     `json:"stage"`
		Err    error      `json:"err"`
		Msg    string     `json:"msg"`
		Engine sim.Engine `json:"engine"`
	}

	type plotReply struct {
		ID    int    `json:"id"`
		Stage string `json:"stage"`
		Err   error  `json:"err"`
		SVG   string `json:"svg"`
	}

	type zipReply struct {
		ID    int    `json:"id"`
		Stage string `json:"stage"`
		Err   error  `json:"err"`
		Href  string `json:"href"`
	}

	for {
		param := params{
			NumIters:   100000,
			NumCarbons: 60,
			Seed:       1234,
		}

		log.Printf("waiting for simulation parameters...\n")
		err = websocket.JSON.Receive(c.ws, &param)
		if err != nil {
			log.Printf("error rcv: %v\n", err)
			return
		}
		id := param.ID

		msgbuf := new(bytes.Buffer)
		msg := log.New(msgbuf, "snfusion-sim: ", 0)
		engine := sim.Engine{
			NumIters:   param.NumIters,
			NumCarbons: param.NumCarbons,
			Seed:       param.Seed,
		}
		engine.SetLogger(msg)

		log.Printf("processing... %#v\n", engine)
		csvbuf := new(bytes.Buffer)
		errc := make(chan error)
		ticker := time.NewTicker(1 * time.Second)
		go func() {
			errc <- engine.Run(csvbuf)
			ticker.Stop()
		}()

		err = <-errc
		if err != nil {
			log.Printf("error: %v\n", err)
			_ = websocket.JSON.Send(c.ws, genReply{
				ID: id, Err: err, Engine: engine, Stage: "gen-done", Msg: msgbuf.String(),
			})
			return
		}

		err = websocket.JSON.Send(c.ws, genReply{
			ID: id, Err: err, Engine: engine, Stage: "gen-done", Msg: msgbuf.String(),
		})
		if err != nil {
			log.Printf("error sending data: %v\n", err)
			return
		}

		csvdata := make([]byte, len(csvbuf.Bytes()))
		copy(csvdata, csvbuf.Bytes())

		log.Printf("running post-processing...\n")
		r := csv.NewReader(csvbuf)
		r.Comma = ';'
		r.Comment = '#'

		table := make([]plotter.XYs, len(engine.Population))
		for i := range table {
			table[i] = make(plotter.XYs, engine.NumIters+1)
		}

		for ix := 0; ix < engine.NumIters+1; ix++ {
			var text []string
			text, err = r.Read()
			if err != nil {
				break
			}
			for i := range engine.Population {
				table[i][ix].X = float64(ix)
				table[i][ix].Y = float64(atoi(text[i]))
			}
		}
		if err == io.EOF {
			err = nil
		}
		if err != nil {
			log.Printf("error reading data: %v\n", err)
			return
		}

		p, err := plot.New()
		if err != nil {
			panic(err)
		}

		p.Title.Text = fmt.Sprintf(
			"Time evolution of nuclei C%v-O%v (seed=%d)",
			engine.NumCarbons,
			100-engine.NumCarbons,
			engine.Seed,
		)
		p.X.Label.Text = "Iteration number"
		p.Y.Label.Text = "Atomic mass of nuclei"

		for i, n := range engine.Population {

			line, err := plotter.NewLine(table[i])
			if err != nil {
				log.Fatalf(
					"error adding data points for nucleus %v: %v\n",
					n, err,
				)
			}
			line.LineStyle.Color = col(n)
			line.LineStyle.Width = vg.Points(1)
			p.Add(line)
			p.Legend.Add(label(n), line)
		}

		p.Add(plotter.NewGrid())
		p.Legend.Top = true
		p.Legend.XOffs = -1 * vg.Centimeter

		figX := 25 * vg.Centimeter
		figY := figX / vg.Length(math.Phi)

		// Create a Canvas for writing SVG images.
		canvas := vgsvg.New(figX, figY)

		// Draw to the Canvas.
		p.Draw(draw.New(canvas))

		outsvg := new(bytes.Buffer)
		_, err = canvas.WriteTo(outsvg)
		if err != nil {
			log.Printf("error svg: %v\n", err)
			return
		}

		err = websocket.JSON.Send(c.ws, plotReply{
			ID: id, Err: err, SVG: outsvg.String(), Stage: "plot-done",
		})
		if err != nil {
			log.Printf("error sending data: %v\n", err)
			return
		}

		pngcanvas := vgimg.PngCanvas{Canvas: vgimg.New(figX, figY)}
		p.Draw(draw.New(pngcanvas))
		outpng := new(bytes.Buffer)
		_, err = pngcanvas.WriteTo(outpng)
		if err != nil {
			log.Printf("error png: %v\n", err)
			return
		}

		href := filepath.Join(dir, fmt.Sprintf("output-%d.zip", id))
		zipf, err := os.Create(href)
		if err != nil {
			log.Printf("error creating zip file: %v\n", err)
		}
		defer zipf.Close()

		zipw := zip.NewWriter(zipf)
		defer zipw.Close()

		for _, file := range []struct {
			Name string
			Body []byte
		}{
			{"output.csv", csvdata},
			{"output.png", outpng.Bytes()},
		} {
			ff, err := zipw.Create(file.Name)
			if err != nil {
				log.Printf("error creating zip content %v: %v\n", file.Name, err)
				return
			}
			_, err = ff.Write(file.Body)
			if err != nil {
				log.Printf("error writing zip content %v: %v\n", file.Name, err)
				return
			}
		}
		err = zipw.Close()
		if err != nil {
			log.Printf("error closing zip-writer: %v\n", err)
			return
		}
		err = zipf.Close()
		if err != nil {
			log.Printf("error closing zip-file: %v\n", err)
			return
		}

		err = websocket.JSON.Send(c.ws, zipReply{
			ID: id, Err: err, Href: href, Stage: "zip-done",
		})
		if err != nil {
			log.Printf("error sending zip: %v\n", err)
			return
		}
		log.Printf("saved report under %v\n", href)
	}

}
Example #10
0
func TestGraphicGrobs(t *testing.T) {
	// Output
	file, err := os.Create("grobs.png")
	if err != nil {
		t.Fatalf("%", err)
	}

	pngCanvas := vgimg.PngCanvas{Canvas: vgimg.New(10*vg.Inch, 8*vg.Inch)}
	vg.Initialize(pngCanvas)
	pngCanvas.Translate(-5*vg.Inch, -4*vg.Inch)
	dot := func(x, y float64) {
		pngCanvas.Push()
		pngCanvas.SetColor(BuiltinColors["red"])
		pngCanvas.SetLineWidth(5)
		var p vg.Path
		xr := vg.Length(x) * vg.Inch
		yr := vg.Length(y) * vg.Inch
		p.Move(xr-5, yr-5)
		p.Line(xr-5, yr+5)
		p.Line(xr+5, yr+5)
		p.Line(xr+5, yr-5)
		p.Close()
		pngCanvas.Fill(p)
		pngCanvas.Pop()
	}

	dot(0, 0)
	dot(5, 0)
	dot(0, 4)

	allVP := Viewport{
		X0:     0,
		Y0:     0,
		Width:  10 * vg.Inch,
		Height: 8 * vg.Inch,
		Canvas: pngCanvas,
	}
	innerVP := allVP.Sub(0.05, 0.05, 0.9, 0.9)
	bg := GrobRect{xmin: 0, ymin: 0, xmax: 1, ymax: 1, fill: BuiltinColors["gray80"]}
	bg.Draw(innerVP)

	cols := []string{"red", "green", "blue", "cyan", "magenta", "yellow",
		"white", "gray", "black"}

	// Draw points in all shapes, three sizes and all builtin colors.
	points := []Grob{}
	x, y := 0.1, 0.1
	for shape := DotPoint; shape <= StarPoint; shape++ {
		for size := 2; size < 7; size += 2 {
			y = 0.05
			for _, col := range cols {
				g := GrobPoint{
					x:     x,
					y:     y,
					size:  float64(size),
					shape: shape,
					color: BuiltinColors[col],
				}
				points = append(points, g)
				y += 0.035
			}
			x += 0.021
		}
	}
	x, y = 0.02, 0.05
	for _, col := range cols {
		g := GrobText{
			x:     x,
			y:     y,
			text:  col,
			size:  10,
			color: BuiltinColors[col],
			vjust: 0.5,
			hjust: 0,
		}
		points = append(points, g)
		y += 0.035
	}
	x, y = 0.121, 0.36
	for shape := DotPoint; shape <= StarPoint; shape++ {
		dy := float64(shape%2) * 0.015
		g := GrobText{
			x:     x,
			y:     y + dy,
			text:  shape.String(),
			size:  10,
			color: BuiltinColors["black"],
			vjust: 0.5,
			hjust: 0.5,
		}
		points = append(points, g)
		x += 3 * 0.021
	}
	for _, grob := range points {
		grob.Draw(innerVP)
	}

	// Draw lines with different styles and widths.
	lines := []Grob{}
	x, y = 0.1, 0.45
	for lt := SolidLine; lt <= TwodashLine; lt++ {
		x = 0.1
		for size := 1; size < 8; size += 2 {
			g := GrobLine{
				x0:       x,
				y0:       y,
				x1:       x + 0.18,
				y1:       y,
				size:     float64(size),
				linetype: lt,
				color:    BuiltinColors["black"],
			}
			lines = append(lines, g)
			x += 0.22
		}
		y += 0.04
	}
	for _, grob := range lines {
		grob.Draw(innerVP)
	}

	// Draw rectangles
	rectVP := innerVP.Sub(0.1, 0.7, 0.4, 0.3)
	rect := []Grob{}
	bgr := GrobRect{xmin: 0, ymin: 0, xmax: 1, ymax: 1, fill: BuiltinColors["gray40"]}
	bgr.Draw(rectVP)
	x, y = 0.0, 0.0
	w, h := 0.5, 0.5
	for _, col := range cols {
		g := GrobRect{
			xmin: x,
			ymin: y,
			xmax: x + w,
			ymax: y + h,
			fill: BuiltinColors[col],
		}
		rect = append(rect, g)
		x += w
		y += h
		w /= 2
		h /= 2
	}
	for _, grob := range rect {
		grob.Draw(rectVP)
	}

	// Draw path
	pathVP := innerVP.Sub(0.55, 0.7, 0.4, 0.3)
	bgp := GrobRect{xmin: 0, ymin: 0, xmax: 1, ymax: 1, fill: BuiltinColors["gray"]}
	bgp.Draw(pathVP)
	sin := make([]struct{ x, y float64 }, 50)
	for i := range sin {
		k := float64(i) / float64(len(sin)-1)
		x = k * 2 * math.Pi
		println(float64(i)/float64(len(sin)), x, math.Sin(x))
		y = 0.4 * math.Sin(x)
		sin[i].x = k
		sin[i].y = 0.55 + y
	}
	g := GrobPath{
		points:   sin,
		size:     4,
		linetype: SolidLine,
		color:    BuiltinColors["white"],
	}
	g.Draw(pathVP)
	cos := make([]struct{ x, y float64 }, 25)
	for i := range cos {
		k := float64(i) / float64(len(cos)-1)
		x = k * 4 * math.Pi
		y = 0.3 * math.Cos(x)
		cos[i].x = k
		cos[i].y = 0.45 + y
	}
	g = GrobPath{
		points:   cos,
		size:     2,
		linetype: DottedLine,
		color:    BuiltinColors["green"],
	}
	g.Draw(pathVP)

	pngCanvas.WriteTo(file)
	file.Close()
}