SVG!
This commit is contained in:
parent
3d68b7e51d
commit
f843d124d6
7 changed files with 205 additions and 14 deletions
10
chart.go
10
chart.go
|
@ -191,7 +191,7 @@ func (c Chart) initRanges(canvasBox Box) (xrange Range, yrange Range) {
|
||||||
func (c Chart) drawBackground(r Renderer) {
|
func (c Chart) drawBackground(r Renderer) {
|
||||||
r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor))
|
r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor))
|
||||||
r.SetStrokeColor(c.Background.GetStrokeColor(DefaultBackgroundStrokeColor))
|
r.SetStrokeColor(c.Background.GetStrokeColor(DefaultBackgroundStrokeColor))
|
||||||
r.SetLineWidth(c.Background.GetStrokeWidth(DefaultStrokeWidth))
|
r.SetStrokeWidth(c.Background.GetStrokeWidth(DefaultStrokeWidth))
|
||||||
r.MoveTo(0, 0)
|
r.MoveTo(0, 0)
|
||||||
r.LineTo(c.Width, 0)
|
r.LineTo(c.Width, 0)
|
||||||
r.LineTo(c.Width, c.Height)
|
r.LineTo(c.Width, c.Height)
|
||||||
|
@ -204,7 +204,7 @@ func (c Chart) drawBackground(r Renderer) {
|
||||||
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
|
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor))
|
r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor))
|
||||||
r.SetStrokeColor(c.Canvas.GetStrokeColor(DefaultCanvasStrokColor))
|
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.MoveTo(canvasBox.Left, canvasBox.Top)
|
||||||
r.LineTo(canvasBox.Right, canvasBox.Top)
|
r.LineTo(canvasBox.Right, canvasBox.Top)
|
||||||
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
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) {
|
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange Range) {
|
||||||
if c.Axes.Show {
|
if c.Axes.Show {
|
||||||
r.SetStrokeColor(c.Axes.GetStrokeColor(DefaultAxisColor))
|
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.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
r.LineTo(canvasBox.Right, canvasBox.Top)
|
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) {
|
func (c Chart) drawSeries(r Renderer, canvasBox Box, index int, s Series, xrange, yrange Range) {
|
||||||
r.SetStrokeColor(s.GetStyle().GetStrokeColor(GetDefaultSeriesStrokeColor(index)))
|
r.SetStrokeColor(s.GetStyle().GetStrokeColor(GetDefaultSeriesStrokeColor(index)))
|
||||||
r.SetLineWidth(s.GetStyle().GetStrokeWidth(DefaultStrokeWidth))
|
r.SetStrokeWidth(s.GetStyle().GetStrokeWidth(DefaultStrokeWidth))
|
||||||
|
|
||||||
if s.Len() == 0 {
|
if s.Len() == 0 {
|
||||||
return
|
return
|
||||||
|
@ -366,7 +366,7 @@ func (c Chart) drawFinalValueLabel(r Renderer, canvasBox Box, index int, s Serie
|
||||||
//draw the shape...
|
//draw the shape...
|
||||||
r.SetFillColor(c.FinalValueLabel.GetFillColor(DefaultFinalLabelBackgroundColor))
|
r.SetFillColor(c.FinalValueLabel.GetFillColor(DefaultFinalLabelBackgroundColor))
|
||||||
r.SetStrokeColor(c.FinalValueLabel.GetStrokeColor(s.GetStyle().GetStrokeColor(GetDefaultSeriesStrokeColor(index))))
|
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.MoveTo(cx, ly)
|
||||||
r.LineTo(ltlx, ltly)
|
r.LineTo(ltlx, ltly)
|
||||||
r.LineTo(ltrx, ltry)
|
r.LineTo(ltrx, ltry)
|
||||||
|
|
|
@ -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 {
|
type rasterRenderer struct {
|
||||||
i *image.RGBA
|
i *image.RGBA
|
||||||
gc *drawing.GraphicContext
|
gc *drawing.GraphicContext
|
||||||
|
@ -43,7 +43,7 @@ func (rr *rasterRenderer) SetFillColor(c color.RGBA) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLineWidth implements the interface method.
|
// SetLineWidth implements the interface method.
|
||||||
func (rr *rasterRenderer) SetLineWidth(width float64) {
|
func (rr *rasterRenderer) SetStrokeWidth(width float64) {
|
||||||
rr.gc.SetLineWidth(width)
|
rr.gc.SetLineWidth(width)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ func (rr *rasterRenderer) Text(body string, x, y int) {
|
||||||
rr.gc.Fill()
|
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 {
|
func (rr *rasterRenderer) MeasureText(body string) int {
|
||||||
if rr.fc == nil && rr.font != nil {
|
if rr.fc == nil && rr.font != nil {
|
||||||
rr.fc = &font.Drawer{
|
rr.fc = &font.Drawer{
|
||||||
|
|
|
@ -18,8 +18,8 @@ type Renderer interface {
|
||||||
// SetFillColor sets the current fill color.
|
// SetFillColor sets the current fill color.
|
||||||
SetFillColor(color.RGBA)
|
SetFillColor(color.RGBA)
|
||||||
|
|
||||||
// SetLineWidth sets the stroke line width.
|
// SetStrokeWidth sets the stroke width.
|
||||||
SetLineWidth(width float64)
|
SetStrokeWidth(width float64)
|
||||||
|
|
||||||
// MoveTo moves the cursor to a given point.
|
// MoveTo moves the cursor to a given point.
|
||||||
MoveTo(x, y int)
|
MoveTo(x, y int)
|
||||||
|
|
40
style.go
40
style.go
|
@ -1,6 +1,10 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
import "image/color"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// Style is a simple style set.
|
// Style is a simple style set.
|
||||||
type Style struct {
|
type Style struct {
|
||||||
|
@ -72,3 +76,37 @@ func (s Style) GetFontColor(defaults ...color.RGBA) color.RGBA {
|
||||||
}
|
}
|
||||||
return s.FontColor
|
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}, ";")
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ func main() {
|
||||||
app.SetName("Chart Test Server")
|
app.SetName("Chart Test Server")
|
||||||
app.SetLogger(web.NewStandardOutputLogger())
|
app.SetLogger(web.NewStandardOutputLogger())
|
||||||
app.GET("/", func(rc *web.RequestContext) web.ControllerResult {
|
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()
|
now := time.Now()
|
||||||
c := chart.Chart{
|
c := chart.Chart{
|
||||||
Title: "A Test Chart",
|
Title: "A Test Chart",
|
||||||
|
@ -49,7 +49,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer := bytes.NewBuffer([]byte{})
|
buffer := bytes.NewBuffer([]byte{})
|
||||||
err := c.Render(chart.PNG, buffer)
|
err := c.Render(chart.SVG, buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rc.API().InternalError(err)
|
return rc.API().InternalError(err)
|
||||||
}
|
}
|
||||||
|
|
3
util.go
3
util.go
|
@ -13,7 +13,8 @@ func ColorIsZero(c color.RGBA) bool {
|
||||||
|
|
||||||
// ColorAsString returns if a color.RGBA is unset or not.
|
// ColorAsString returns if a color.RGBA is unset or not.
|
||||||
func ColorAsString(c color.RGBA) string {
|
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.
|
// MinAndMax returns both the min and max in one pass.
|
||||||
|
|
|
@ -1 +1,153 @@
|
||||||
package chart
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue