can rotate text + add y axis names
|
@ -32,8 +32,12 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
graph := chart.Chart{
|
graph := chart.Chart{
|
||||||
|
Width: 1920,
|
||||||
|
Height: 1080,
|
||||||
YAxis: chart.YAxis{
|
YAxis: chart.YAxis{
|
||||||
Style: chart.StyleShow(),
|
Name: "Random Values",
|
||||||
|
NameStyle: chart.StyleShow(),
|
||||||
|
Style: chart.StyleShow(),
|
||||||
Range: &chart.ContinuousRange{
|
Range: &chart.ContinuousRange{
|
||||||
Min: 25,
|
Min: 25,
|
||||||
Max: 175,
|
Max: 175,
|
||||||
|
@ -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() {
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
@ -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]
|
||||||
|
|
|
@ -22,14 +22,10 @@ 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 {
|
midX := (q0X + qX) / 2
|
||||||
// No-op.
|
midY := (q0Y + qY) / 2
|
||||||
} else {
|
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
|
||||||
midX := (q0X + qX) / 2
|
|
||||||
midY := (q0Y + qY) / 2
|
|
||||||
path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
q0X, q0Y, on0 = qX, qY, on
|
q0X, q0Y, on0 = qX, qY, on
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body)))
|
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)))
|
||||||
|
} 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) {
|
||||||
|
|
37
yaxis.go
|
@ -8,7 +8,9 @@ import (
|
||||||
// YAxis is a veritcal rule of the range.
|
// YAxis is a veritcal rule of the range.
|
||||||
// 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 {
|
||||||
|
|