can rotate text + add y axis names

This commit is contained in:
Will Charczuk 2016-08-06 21:59:46 -07:00
parent 3607d732d9
commit 718678b421
34 changed files with 102 additions and 20 deletions

View file

@ -32,7 +32,11 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
} }
graph := chart.Chart{ graph := chart.Chart{
Width: 1920,
Height: 1080,
YAxis: chart.YAxis{ YAxis: chart.YAxis{
Name: "Random Values",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(), Style: chart.StyleShow(),
Range: &chart.ContinuousRange{ Range: &chart.ContinuousRange{
Min: 25, Min: 25,
@ -51,8 +55,10 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
}, },
} }
res.Header().Set("Content-Type", "image/png") graph.Elements = []chart.Renderable{chart.Legend(&graph)}
graph.Render(chart.PNG, res)
res.Header().Set("Content-Type", "image/svg+xml")
graph.Render(chart.SVG, res)
} }
func main() { func main() {

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View file

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -161,10 +161,10 @@ func (tr *Matrix) Translate(tx, ty float64) {
tr[5] = ty*tr[3] + tx*tr[1] + tr[5] tr[5] = ty*tr[3] + tx*tr[1] + tr[5]
} }
// Rotate adds a rotation to the matrix. angle is in radian // Rotate adds a rotation to the matrix.
func (tr *Matrix) Rotate(angle float64) { func (tr *Matrix) Rotate(radians float64) {
c := math.Cos(angle) c := math.Cos(radians)
s := math.Sin(angle) s := math.Sin(radians)
t0 := c*tr[0] + s*tr[2] t0 := c*tr[0] + s*tr[2]
t1 := s*tr[3] + c*tr[1] t1 := s*tr[3] + c*tr[1]
t2 := c*tr[2] - s*tr[0] t2 := c*tr[2] - s*tr[0]

View file

@ -22,15 +22,11 @@ func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) {
} else { } else {
path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy) path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
} }
} else { } else if !on0 {
if on0 {
// No-op.
} else {
midX := (q0X + qX) / 2 midX := (q0X + qX) / 2
midY := (q0Y + qY) / 2 midY := (q0Y + qY) / 2
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy) path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
} }
}
q0X, q0Y, on0 = qX, qY, on q0X, q0Y, on0 = qX, qY, on
} }
// Close the curve. // Close the curve.

View file

@ -28,6 +28,8 @@ type rasterRenderer struct {
i *image.RGBA i *image.RGBA
gc *drawing.RasterGraphicContext gc *drawing.RasterGraphicContext
rotateRadians float64
s Style s Style
} }
@ -139,10 +141,11 @@ func (rr *rasterRenderer) SetFontColor(c drawing.Color) {
// Text implements the interface method. // Text implements the interface method.
func (rr *rasterRenderer) Text(body string, x, y int) { func (rr *rasterRenderer) Text(body string, x, y int) {
xf, yf := rr.getCoords(x, y)
rr.gc.SetFont(rr.s.Font) rr.gc.SetFont(rr.s.Font)
rr.gc.SetFontSize(rr.s.FontSize) rr.gc.SetFontSize(rr.s.FontSize)
rr.gc.SetFillColor(rr.s.FontColor) rr.gc.SetFillColor(rr.s.FontColor)
rr.gc.CreateStringPath(body, float64(x), float64(y)) rr.gc.CreateStringPath(body, float64(xf), float64(yf))
rr.gc.Fill() rr.gc.Fill()
} }
@ -182,6 +185,29 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
} }
} }
// SetTextRotation sets a text rotation.
func (rr *rasterRenderer) SetTextRotation(radians float64) {
rr.rotateRadians = radians
}
func (rr *rasterRenderer) getCoords(x, y int) (xf, yf int) {
if rr.rotateRadians == 0 {
xf = x
yf = y
return
}
rr.gc.Translate(float64(x), float64(y))
rr.gc.Rotate(rr.rotateRadians)
return
}
// ClearTextRotation clears text rotation.
func (rr *rasterRenderer) ClearTextRotation() {
rr.gc.SetMatrixTransform(drawing.NewIdentityMatrix())
rr.rotateRadians = 0
}
// Save implements the interface method. // Save implements the interface method.
func (rr *rasterRenderer) Save(w io.Writer) error { func (rr *rasterRenderer) Save(w io.Writer) error {
return png.Encode(w, rr.i) return png.Encode(w, rr.i)

View file

@ -72,6 +72,12 @@ type Renderer interface {
// MeasureText measures text. // MeasureText measures text.
MeasureText(body string) Box MeasureText(body string) Box
// SetTextRotatation sets a rotation for drawing elements.
SetTextRotation(radians float64)
// ClearTextRotation clears rotation.
ClearTextRotation()
// Save writes the image to the given writer. // Save writes the image to the given writer.
Save(w io.Writer) error Save(w io.Writer) error
} }

View file

@ -32,6 +32,7 @@ type vectorRenderer struct {
b *bytes.Buffer b *bytes.Buffer
c *canvas c *canvas
s *Style s *Style
r float64
p []string p []string
fc *font.Drawer fc *font.Drawer
} }
@ -171,6 +172,16 @@ func (vr *vectorRenderer) MeasureText(body string) (box Box) {
return return
} }
// SetTextRotation sets the text rotation.
func (vr *vectorRenderer) SetTextRotation(radians float64) {
vr.c.r = radians
}
// ClearTextRotation clears the text rotation.
func (vr *vectorRenderer) ClearTextRotation() {
vr.c.r = 0
}
// Save saves the renderer's contents to a writer. // Save saves the renderer's contents to a writer.
func (vr *vectorRenderer) Save(w io.Writer) error { func (vr *vectorRenderer) Save(w io.Writer) error {
vr.c.End() vr.c.End()
@ -187,6 +198,7 @@ func newCanvas(w io.Writer) *canvas {
type canvas struct { type canvas struct {
w io.Writer w io.Writer
dpi float64 dpi float64
r float64
width int width int
height int height int
} }
@ -206,7 +218,12 @@ func (c *canvas) Path(d string, style Style) {
} }
func (c *canvas) Text(x, y int, body string, style Style) { func (c *canvas) Text(x, y int, body string, style Style) {
if c.r == 0 {
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body))) c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body)))
} else {
transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, Math.RadiansToDegrees(c.r), x, y)
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s"%s>%s</text>`, x, y, c.styleAsSVG(style), transform, body)))
}
} }
func (c *canvas) Circle(x, y, r int, style Style) { func (c *canvas) Circle(x, y, r int, style Style) {

View file

@ -9,6 +9,8 @@ import (
// There can be (2) y-axes; a primary and secondary. // There can be (2) y-axes; a primary and secondary.
type YAxis struct { type YAxis struct {
Name string Name string
NameStyle Style
Style Style Style Style
Zero GridLine Zero GridLine
@ -30,6 +32,11 @@ func (ya YAxis) GetName() string {
return ya.Name return ya.Name
} }
// GetNameStyle returns the name style.
func (ya YAxis) GetNameStyle() Style {
return ya.NameStyle
}
// GetStyle returns the style. // GetStyle returns the style.
func (ya YAxis) GetStyle() Style { func (ya YAxis) GetStyle() Style {
return ya.Style return ya.Style
@ -73,6 +80,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
} }
var minx, maxx, miny, maxy = math.MaxInt32, 0, math.MaxInt32, 0 var minx, maxx, miny, maxy = math.MaxInt32, 0, math.MaxInt32, 0
var maxTextHeight int
for _, t := range ticks { for _, t := range ticks {
v := t.Value v := t.Value
ly := canvasBox.Bottom - ra.Translate(v) ly := canvasBox.Bottom - ra.Translate(v)
@ -83,6 +91,10 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
finalTextX = tx - tb.Width() finalTextX = tx - tb.Width()
} }
if tb.Height() > maxTextHeight {
maxTextHeight = tb.Height()
}
if ya.AxisType == YAxisPrimary { if ya.AxisType == YAxisPrimary {
minx = canvasBox.Right minx = canvasBox.Right
maxx = Math.MaxInt(maxx, tx+tb.Width()) maxx = Math.MaxInt(maxx, tx+tb.Width())
@ -94,6 +106,10 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
maxy = Math.MaxInt(maxy, ly+tb.Height()>>1) maxy = Math.MaxInt(maxy, ly+tb.Height()>>1)
} }
if ya.NameStyle.Show && len(ya.Name) > 0 {
maxx += (DefaultYAxisMargin + maxTextHeight)
}
return Box{ return Box{
Top: miny, Top: miny,
Left: minx, Left: minx,
@ -124,12 +140,17 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
r.LineTo(lx, canvasBox.Top) r.LineTo(lx, canvasBox.Top)
r.Stroke() r.Stroke()
var maxTextWidth int
for _, t := range ticks { for _, t := range ticks {
v := t.Value v := t.Value
ly := canvasBox.Bottom - ra.Translate(v) ly := canvasBox.Bottom - ra.Translate(v)
tb := r.MeasureText(t.Label) tb := r.MeasureText(t.Label)
if tb.Width() > maxTextWidth {
maxTextWidth = tb.Width()
}
finalTextX := tx finalTextX := tx
finalTextY := ly + tb.Height()>>1 finalTextY := ly + tb.Height()>>1
if ya.AxisType == YAxisSecondary { if ya.AxisType == YAxisSecondary {
@ -147,8 +168,18 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
r.Stroke() r.Stroke()
} }
if ya.Zero.Style.Show { nameStyle := ya.NameStyle.InheritFrom(defaults)
ya.Zero.Render(r, canvasBox, ra, Style{}) if ya.NameStyle.Show && len(ya.Name) > 0 {
nameStyle.GetTextOptions().WriteToRenderer(r)
r.SetTextRotation(Math.DegreesToRadians(90))
tb := r.MeasureText(ya.Name)
tx := canvasBox.Right + int(sw) + DefaultYAxisMargin + maxTextWidth + DefaultYAxisMargin
ty := canvasBox.Bottom - (canvasBox.Height()>>1 + tb.Width()>>1)
r.Text(ya.Name, tx, ty)
r.ClearTextRotation()
} }
if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show { if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show {