From f843d124d6bf188c324c9aa788a2c526c052bcc9 Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Thu, 7 Jul 2016 22:18:53 -0700 Subject: [PATCH] SVG! --- chart.go | 10 +-- raster_renderer.go | 6 +- renderer.go | 4 +- style.go | 40 +++++++++++- testserver/main.go | 4 +- util.go | 3 +- vector_renderer.go | 152 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 205 insertions(+), 14 deletions(-) diff --git a/chart.go b/chart.go index 3297026..2f380f1 100644 --- a/chart.go +++ b/chart.go @@ -191,7 +191,7 @@ func (c Chart) initRanges(canvasBox Box) (xrange Range, yrange Range) { func (c Chart) drawBackground(r Renderer) { r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor)) r.SetStrokeColor(c.Background.GetStrokeColor(DefaultBackgroundStrokeColor)) - r.SetLineWidth(c.Background.GetStrokeWidth(DefaultStrokeWidth)) + r.SetStrokeWidth(c.Background.GetStrokeWidth(DefaultStrokeWidth)) r.MoveTo(0, 0) r.LineTo(c.Width, 0) r.LineTo(c.Width, c.Height) @@ -204,7 +204,7 @@ func (c Chart) drawBackground(r Renderer) { func (c Chart) drawCanvas(r Renderer, canvasBox Box) { r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor)) r.SetStrokeColor(c.Canvas.GetStrokeColor(DefaultCanvasStrokColor)) - r.SetLineWidth(c.Canvas.GetStrokeWidth(DefaultStrokeWidth)) + r.SetStrokeWidth(c.Canvas.GetStrokeWidth(DefaultStrokeWidth)) r.MoveTo(canvasBox.Left, canvasBox.Top) r.LineTo(canvasBox.Right, canvasBox.Top) r.LineTo(canvasBox.Right, canvasBox.Bottom) @@ -217,7 +217,7 @@ func (c Chart) drawCanvas(r Renderer, canvasBox Box) { func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange Range) { if c.Axes.Show { r.SetStrokeColor(c.Axes.GetStrokeColor(DefaultAxisColor)) - r.SetLineWidth(c.Axes.GetStrokeWidth(DefaultStrokeWidth)) + r.SetStrokeWidth(c.Axes.GetStrokeWidth(DefaultStrokeWidth)) r.MoveTo(canvasBox.Left, canvasBox.Bottom) r.LineTo(canvasBox.Right, canvasBox.Bottom) r.LineTo(canvasBox.Right, canvasBox.Top) @@ -295,7 +295,7 @@ func (c Chart) drawXAxisLabels(r Renderer, canvasBox Box, xrange Range) { func (c Chart) drawSeries(r Renderer, canvasBox Box, index int, s Series, xrange, yrange Range) { r.SetStrokeColor(s.GetStyle().GetStrokeColor(GetDefaultSeriesStrokeColor(index))) - r.SetLineWidth(s.GetStyle().GetStrokeWidth(DefaultStrokeWidth)) + r.SetStrokeWidth(s.GetStyle().GetStrokeWidth(DefaultStrokeWidth)) if s.Len() == 0 { return @@ -366,7 +366,7 @@ func (c Chart) drawFinalValueLabel(r Renderer, canvasBox Box, index int, s Serie //draw the shape... r.SetFillColor(c.FinalValueLabel.GetFillColor(DefaultFinalLabelBackgroundColor)) r.SetStrokeColor(c.FinalValueLabel.GetStrokeColor(s.GetStyle().GetStrokeColor(GetDefaultSeriesStrokeColor(index)))) - r.SetLineWidth(c.FinalValueLabel.GetStrokeWidth(DefaultAxisLineWidth)) + r.SetStrokeWidth(c.FinalValueLabel.GetStrokeWidth(DefaultAxisLineWidth)) r.MoveTo(cx, ly) r.LineTo(ltlx, ltly) r.LineTo(ltrx, ltry) diff --git a/raster_renderer.go b/raster_renderer.go index 152b4d0..331c0a7 100644 --- a/raster_renderer.go +++ b/raster_renderer.go @@ -21,7 +21,7 @@ func PNG(width, height int) Renderer { } } -// RasterRenderer renders chart commands to a bitmap. +// rasterRenderer renders chart commands to a bitmap. type rasterRenderer struct { i *image.RGBA gc *drawing.GraphicContext @@ -43,7 +43,7 @@ func (rr *rasterRenderer) SetFillColor(c color.RGBA) { } // SetLineWidth implements the interface method. -func (rr *rasterRenderer) SetLineWidth(width float64) { +func (rr *rasterRenderer) SetStrokeWidth(width float64) { rr.gc.SetLineWidth(width) } @@ -115,7 +115,7 @@ func (rr *rasterRenderer) Text(body string, x, y int) { rr.gc.Fill() } -// MeasureText implements the interface method. +// MeasureText uses the truetype font drawer to measure the width of text. func (rr *rasterRenderer) MeasureText(body string) int { if rr.fc == nil && rr.font != nil { rr.fc = &font.Drawer{ diff --git a/renderer.go b/renderer.go index 84371cb..48473af 100644 --- a/renderer.go +++ b/renderer.go @@ -18,8 +18,8 @@ type Renderer interface { // SetFillColor sets the current fill color. SetFillColor(color.RGBA) - // SetLineWidth sets the stroke line width. - SetLineWidth(width float64) + // SetStrokeWidth sets the stroke width. + SetStrokeWidth(width float64) // MoveTo moves the cursor to a given point. MoveTo(x, y int) diff --git a/style.go b/style.go index 2fd8dcd..cfdbd6f 100644 --- a/style.go +++ b/style.go @@ -1,6 +1,10 @@ package chart -import "image/color" +import ( + "fmt" + "image/color" + "strings" +) // Style is a simple style set. type Style struct { @@ -72,3 +76,37 @@ func (s Style) GetFontColor(defaults ...color.RGBA) color.RGBA { } return s.FontColor } + +// SVG returns the style as a svg style string. +func (s Style) SVG() string { + sw := s.StrokeWidth + sc := s.StrokeColor + fc := s.FillColor + fs := s.FontSize + fnc := s.FontColor + + strokeWidthText := "stroke-width:0" + if sw != 0 { + strokeWidthText = "stroke-width:" + fmt.Sprintf("%d", int(sw)) + } + + strokeText := "stroke:none" + if !ColorIsZero(sc) { + strokeText = "stroke:" + ColorAsString(sc) + } + + fillText := "fill:none" + if !ColorIsZero(fc) { + fillText = "fill:" + ColorAsString(fc) + } + + fontSizeText := "" + if fs != 0 { + fontSizeText = "font-size:" + fmt.Sprintf("%.1f", fs) + } + + if !ColorIsZero(fnc) { + fillText = "fill:" + ColorAsString(fnc) + } + return strings.Join([]string{strokeWidthText, strokeText, fillText, fontSizeText}, ";") +} diff --git a/testserver/main.go b/testserver/main.go index 5e79bd8..2d407bb 100644 --- a/testserver/main.go +++ b/testserver/main.go @@ -14,7 +14,7 @@ func main() { app.SetName("Chart Test Server") app.SetLogger(web.NewStandardOutputLogger()) app.GET("/", func(rc *web.RequestContext) web.ControllerResult { - rc.Response.Header().Set("Content-Type", "image/png") + rc.Response.Header().Set("Content-Type", "image/svg+xml") now := time.Now() c := chart.Chart{ Title: "A Test Chart", @@ -49,7 +49,7 @@ func main() { } buffer := bytes.NewBuffer([]byte{}) - err := c.Render(chart.PNG, buffer) + err := c.Render(chart.SVG, buffer) if err != nil { return rc.API().InternalError(err) } diff --git a/util.go b/util.go index bda14b6..dae980d 100644 --- a/util.go +++ b/util.go @@ -13,7 +13,8 @@ func ColorIsZero(c color.RGBA) bool { // ColorAsString returns if a color.RGBA is unset or not. func ColorAsString(c color.RGBA) string { - return fmt.Sprintf("RGBA(%v,%v,%v,%v)", c.R, c.G, c.G, c.A) + a := float64(c.A) / float64(255) + return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, a) } // MinAndMax returns both the min and max in one pass. diff --git a/vector_renderer.go b/vector_renderer.go index 58a733f..be3ed8f 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -1 +1,153 @@ package chart + +import ( + "bytes" + "fmt" + "image/color" + "io" + "strings" + + "golang.org/x/image/font" + + "github.com/ajstarks/svgo" + "github.com/golang/freetype/truetype" +) + +// SVG returns a new png/raster renderer. +func SVG(width, height int) Renderer { + buffer := bytes.NewBuffer([]byte{}) + canvas := svg.New(buffer) + canvas.Start(width, height) + return &vectorRenderer{ + b: buffer, + c: canvas, + s: &Style{}, + p: []string{}, + } +} + +// vectorRenderer renders chart commands to a bitmap. +type vectorRenderer struct { + b *bytes.Buffer + c *svg.SVG + s *Style + f *truetype.Font + p []string + fc *font.Drawer +} + +func (vr *vectorRenderer) SetStrokeColor(c color.RGBA) { + vr.s.StrokeColor = c +} + +// SetFillColor implements the interface method. +func (vr *vectorRenderer) SetFillColor(c color.RGBA) { + vr.s.FillColor = c +} + +// SetLineWidth implements the interface method. +func (vr *vectorRenderer) SetStrokeWidth(width float64) { + vr.s.StrokeWidth = width +} + +// MoveTo implements the interface method. +func (vr *vectorRenderer) MoveTo(x, y int) { + vr.p = append(vr.p, fmt.Sprintf("M %d %d", x, y)) +} + +// LineTo implements the interface method. +func (vr *vectorRenderer) LineTo(x, y int) { + vr.p = append(vr.p, fmt.Sprintf("L %d %d", x, y)) +} + +func (vr *vectorRenderer) Close() { + vr.p = append(vr.p, fmt.Sprintf("Z")) +} + +// Stroke draws the path with no fill. +func (vr *vectorRenderer) Stroke() { + vr.s.FillColor = color.RGBA{} + vr.s.FontColor = color.RGBA{} + vr.drawPath() +} + +// Fill draws the path with no stroke. +func (vr *vectorRenderer) Fill() { + vr.s.StrokeColor = color.RGBA{} + vr.s.StrokeWidth = 0 + vr.s.FontColor = color.RGBA{} + vr.drawPath() +} + +// FillStroke draws the path with both fill and stroke. +func (vr *vectorRenderer) FillStroke() { + vr.s.FontColor = color.RGBA{} + vr.drawPath() +} + +func (vr *vectorRenderer) drawPath() { + vr.c.Path(strings.Join(vr.p, "\n"), vr.s.SVG()) + vr.p = []string{} +} + +// Circle implements the interface method. +func (vr *vectorRenderer) Circle(radius float64, x, y int) { + vr.c.Circle(x, y, int(radius), vr.s.SVG()) +} + +// SetFont implements the interface method. +func (vr *vectorRenderer) SetFont(f *truetype.Font) { + vr.f = f +} + +// SetFontColor implements the interface method. +func (vr *vectorRenderer) SetFontColor(c color.RGBA) { + vr.s.FontColor = c +} + +// SetFontSize implements the interface method. +func (vr *vectorRenderer) SetFontSize(size float64) { + vr.s.FontSize = size +} + +func (vr *vectorRenderer) svgFontFace() string { + family := "sans-serif" + if vr.f != nil { + name := vr.f.Name(truetype.NameIDFontFamily) + if len(name) != 0 { + family = fmt.Sprintf(`'%s',%s`, name, family) + } + } + return fmt.Sprintf("font-family:%s", family) +} + +// Text draws a text blob. +func (vr *vectorRenderer) Text(body string, x, y int) { + vr.s.FillColor = color.RGBA{} + vr.s.StrokeColor = color.RGBA{} + vr.s.StrokeWidth = 0 + vr.c.Text(x, y, body, vr.s.SVG()+";"+vr.svgFontFace()) +} + +// MeasureText uses the truetype font drawer to measure the width of text. +func (vr *vectorRenderer) MeasureText(body string) int { + if vr.fc == nil && vr.f != nil { + vr.fc = &font.Drawer{ + Face: truetype.NewFace(vr.f, &truetype.Options{ + DPI: DefaultDPI, + Size: vr.s.FontSize, + }), + } + } + if vr.fc != nil { + dimensions := vr.fc.MeasureString(body) + return dimensions.Floor() + } + return 0 +} + +func (vr *vectorRenderer) Save(w io.Writer) error { + vr.c.End() + _, err := w.Write(vr.b.Bytes()) + return err +}