// 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 }
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() }
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() }