diff --git a/_examples/text_rotation/main.go b/_examples/text_rotation/main.go new file mode 100644 index 0000000..b30c41f --- /dev/null +++ b/_examples/text_rotation/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + /* + In this example we set a rotation on the style for the custom ticks from the `custom_ticks` example. + */ + + graph := chart.Chart{ + YAxis: chart.YAxis{ + Style: chart.Style{ + Show: true, + TextRotationDegrees: 45.0, + }, + Range: &chart.ContinuousRange{ + Min: 0.0, + Max: 4.0, + }, + Ticks: []chart.Tick{ + {0.0, "0.00"}, + {2.0, "2.00"}, + {4.0, "4.00"}, + {6.0, "6.00"}, + {8.0, "Eight"}, + {10.0, "Ten"}, + }, + }, + Series: []chart.Series{ + chart.ContinuousSeries{ + XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + } + + res.Header().Set("Content-Type", "image/png") + graph.Render(chart.PNG, res) +} + +func main() { + http.HandleFunc("/", drawChart) + http.ListenAndServe(":8080", nil) +} diff --git a/box.go b/box.go index 0705749..68b71a8 100644 --- a/box.go +++ b/box.go @@ -219,3 +219,17 @@ func (b Box) OuterConstrain(bounds, other Box) Box { } return newBox } + +// Rotate rotates a box's corners by a given radian rotation angle. +func (b Box) Rotate(radians float64) Box { + cx, cy := b.Center() + + ltx, lty := Math.RotateCoordinate(cx, cy, b.Left, b.Top, radians) + rbx, rby := Math.RotateCoordinate(cx, cy, b.Right, b.Bottom, radians) + return Box{ + Top: lty, + Left: ltx, + Right: rbx, + Bottom: rby, + } +} diff --git a/box_test.go b/box_test.go index ba3b1ed..5f4df40 100644 --- a/box_test.go +++ b/box_test.go @@ -143,3 +143,19 @@ func TestBoxShift(t *testing.T) { assert.Equal(11, shifted.Right) assert.Equal(12, shifted.Bottom) } + +func TestBoxRotate(t *testing.T) { + assert := assert.New(t) + + b := Box{ + Top: 5, + Left: 5, + Right: 20, + Bottom: 10, + } + rotated := b.Rotate(Math.DegreesToRadians(45)) + assert.Equal(1, rotated.Top) + assert.Equal(4, rotated.Left) + assert.Equal(11, rotated.Right) + assert.Equal(15, rotated.Bottom) +} diff --git a/math.go b/math.go index 77180e8..bc4456c 100644 --- a/math.go +++ b/math.go @@ -214,9 +214,18 @@ func (m mathUtil) DegreesToCompass(deg float64) float64 { } // CirclePoint returns the absolute position of a circle diameter point given -// by the radius and the angle. -func (m mathUtil) CirclePoint(cx, cy int, radius, angleRadians float64) (x, y int) { - x = cx + int(radius*math.Sin(angleRadians)) - y = cy - int(radius*math.Cos(angleRadians)) +// by the radius and the theta. +func (m mathUtil) CirclePoint(cx, cy int, radius, thetaRadians float64) (x, y int) { + x = cx + int(radius*math.Sin(thetaRadians)) + y = cy - int(radius*math.Cos(thetaRadians)) + return +} + +func (m mathUtil) RotateCoordinate(cx, cy, x, y int, thetaRadians float64) (rx, ry int) { + tempX, tempY := float64(x-cx), float64(y-cy) + rotatedX := int(math.Ceil(tempX*math.Cos(thetaRadians) - tempY*math.Sin(thetaRadians))) + rotatedY := int(math.Ceil(tempX*math.Sin(thetaRadians) + tempY*math.Cos(thetaRadians))) + rx = rotatedX + cy + ry = rotatedY + cy return } diff --git a/math_test.go b/math_test.go index a4c4006..88cf860 100644 --- a/math_test.go +++ b/math_test.go @@ -160,3 +160,14 @@ func TestRadianAdd(t *testing.T) { assert.Equal(_pi, Math.RadianAdd(_pi, _2pi)) assert.Equal(_pi, Math.RadianAdd(_pi, -_2pi)) } + +func TestRotateCoordinate(t *testing.T) { + assert := assert.New(t) + + cx, cy := 10, 10 + x, y := 5, 5 + + rx, ry := Math.RotateCoordinate(cx, cy, x, y, Math.DegreesToRadians(45)) + assert.Equal(10, rx) + assert.Equal(3, ry) +} diff --git a/raster_renderer.go b/raster_renderer.go index 3b9fa4d..cc32ebb 100644 --- a/raster_renderer.go +++ b/raster_renderer.go @@ -177,12 +177,17 @@ func (rr *rasterRenderer) MeasureText(body string) Box { t = 0 } - return Box{ + textBox := Box{ Top: int(math.Ceil(t)), Left: int(math.Ceil(l)), Right: int(math.Ceil(r)), Bottom: int(math.Ceil(b)), } + if rr.rotateRadians == 0 { + return textBox + } + + return textBox.Rotate(rr.rotateRadians) } // SetTextRotation sets a text rotation. diff --git a/style.go b/style.go index 9563066..df4794f 100644 --- a/style.go +++ b/style.go @@ -33,6 +33,7 @@ type Style struct { TextVerticalAlign TextVerticalAlign TextWrap TextWrap TextLineSpacing int + TextRotationDegrees float64 } // IsZero returns if the object is set or not. @@ -241,6 +242,16 @@ func (s Style) GetTextLineSpacing(defaults ...int) int { return s.TextLineSpacing } +// GetTextRotationDegrees returns the text rotation in degrees. +func (s Style) GetTextRotationDegrees(defaults ...float64) float64 { + if s.TextRotationDegrees == 0 { + if len(defaults) > 0 { + return defaults[0] + } + } + return s.TextRotationDegrees +} + // WriteToRenderer passes the style's options to a renderer. func (s Style) WriteToRenderer(r Renderer) { r.SetStrokeColor(s.GetStrokeColor()) @@ -250,6 +261,12 @@ func (s Style) WriteToRenderer(r Renderer) { r.SetFont(s.GetFont()) r.SetFontColor(s.GetFontColor()) r.SetFontSize(s.GetFontSize()) + + if s.GetTextRotationDegrees() == 0 { + r.ClearTextRotation() + } else { + r.SetTextRotation(Math.DegreesToRadians(s.GetTextRotationDegrees())) + } } // WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer. @@ -281,6 +298,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) { final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign) final.TextWrap = s.GetTextWrap(defaults.TextWrap) final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing) + final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees) return } @@ -320,5 +338,6 @@ func (s Style) GetTextOptions() Style { TextVerticalAlign: s.TextVerticalAlign, TextWrap: s.TextWrap, TextLineSpacing: s.TextLineSpacing, + TextRotationDegrees: s.TextRotationDegrees, } } diff --git a/yaxis.go b/yaxis.go index fe7ab44..d2b6a72 100644 --- a/yaxis.go +++ b/yaxis.go @@ -20,6 +20,7 @@ type YAxis struct { ValueFormatter ValueFormatter Range Range + TickStyle Style Ticks []Tick GridLines []GridLine @@ -42,6 +43,11 @@ func (ya YAxis) GetStyle() Style { return ya.Style } +// GetTickStyle returns the tick style. +func (ya YAxis) GetTickStyle() Style { + return ya.TickStyle +} + // GetTicks returns the ticks for a series. // The coalesce priority is: // - User Supplied Ticks (i.e. Ticks array on the axis itself). @@ -68,8 +74,6 @@ func (ya YAxis) GetGridLines(ticks []Tick) []GridLine { // Measure returns the bounds of the axis. func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box { - ya.Style.InheritFrom(defaults).WriteToRenderer(r) - sort.Sort(Ticks(ticks)) var tx int @@ -79,9 +83,12 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic tx = canvasBox.Left - DefaultYAxisMargin } + ya.TickStyle.InheritFrom(ya.Style.InheritFrom(defaults)).WriteToRenderer(r) + var minx, maxx, miny, maxy = math.MaxInt32, 0, math.MaxInt32, 0 var maxTextHeight int for _, t := range ticks { + v := t.Value ly := canvasBox.Bottom - ra.Translate(v) @@ -142,9 +149,11 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick var maxTextWidth int for _, t := range ticks { + v := t.Value ly := canvasBox.Bottom - ra.Translate(v) + ya.TickStyle.InheritFrom(ya.Style.InheritFrom(defaults)).WriteToRenderer(r) tb := r.MeasureText(t.Label) if tb.Width() > maxTextWidth { @@ -158,6 +167,7 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick } r.Text(t.Label, finalTextX, finalTextY) + r.ClearTextRotation() r.MoveTo(lx, ly) if ya.AxisType == YAxisPrimary { @@ -168,12 +178,9 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick r.Stroke() } - nameStyle := ya.NameStyle.InheritFrom(defaults) + nameStyle := ya.NameStyle.InheritFrom(defaults.InheritFrom(Style{TextRotationDegrees: 90})) if ya.NameStyle.Show && len(ya.Name) > 0 { nameStyle.GetTextOptions().WriteToRenderer(r) - - r.SetTextRotation(Math.DegreesToRadians(90)) - tb := r.MeasureText(ya.Name) var tx int @@ -186,7 +193,6 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick ty := canvasBox.Bottom - (canvasBox.Height()>>1 + tb.Width()>>1) r.Text(ya.Name, tx, ty) - r.ClearTextRotation() } if ya.Zero.Style.Show {