diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 1b75aa0..80ea608 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -14,17 +14,18 @@ func drawChart(res http.ResponseWriter, req *http.Request) { FillColor: chart.ColorLightGray, }, Values: []chart.PieChartValue{ - {Value: 0.3, Label: "Blue"}, + {Value: 0.2, Label: "Blue"}, {Value: 0.2, Label: "Green"}, {Value: 0.2, Label: "Gray"}, {Value: 0.1, Label: "Orange"}, + {Value: 0.1, Label: "HEANG"}, {Value: 0.1, Label: "??"}, {Value: 0.1, Label: "!!"}, }, } - res.Header().Set("Content-Type", "image/png") - err := pie.Render(chart.PNG, res) + res.Header().Set("Content-Type", "image/svg+xml") + err := pie.Render(chart.SVG, res) if err != nil { fmt.Printf("Error rendering pie chart: %v\n", err) } diff --git a/pie_chart.go b/pie_chart.go index f84f95c..a9dfd4a 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -3,7 +3,6 @@ package chart import ( "errors" "io" - "math" "github.com/golang/freetype/truetype" ) @@ -142,14 +141,15 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []PieChartValue) cx, cy := canvasBox.Center() diameter := MinInt(canvasBox.Width(), canvasBox.Height()) radius := float64(diameter >> 1) - radius2 := (radius * 2.0) / 3.0 + labelRadius := (radius * 2.0) / 3.0 + // draw the pie slices var rads, delta, delta2, total float64 var lx, ly int for index, v := range values { v.Style.InheritFrom(pc.stylePieChartValue(index)).PersistToRenderer(r) - r.MoveTo(cx, cy) + r.MoveTo(cx, cy) rads = PercentToRadians(total) delta = PercentToRadians(v.Value) @@ -161,13 +161,14 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []PieChartValue) total = total + v.Value } + // draw the labels total = 0 for index, v := range values { v.Style.InheritFrom(pc.stylePieChartValue(index)).PersistToRenderer(r) if len(v.Label) > 0 { - delta2 = RadianAdd(PercentToRadians(total+(v.Value/2.0)), _pi2) - lx = cx + int(radius2*math.Sin(delta2)) - ly = cy - int(radius2*math.Cos(delta2)) + delta2 = PercentToRadians(total + (v.Value / 2.0)) + delta2 = RadianAdd(delta2, _pi2) + lx, ly = CirclePoint(cx, cy, labelRadius, delta2) tb := r.MeasureText(v.Label) lx = lx - (tb.Width() >> 1) diff --git a/util.go b/util.go index 8b1fcf2..6039142 100644 --- a/util.go +++ b/util.go @@ -191,18 +191,30 @@ func PercentDifference(v1, v2 float64) float64 { return (v2 - v1) / v1 } -// DegreesToRadians returns degrees as radians. -func DegreesToRadians(degrees float64) float64 { - return degrees * (math.Pi / 180.0) -} - const ( + _pi = math.Pi _2pi = 2 * math.Pi _3pi4 = (3 * math.Pi) / 4.0 + _4pi3 = (4 * math.Pi) / 3.0 + _3pi2 = (3 * math.Pi) / 2.0 + _5pi4 = (5 * math.Pi) / 4.0 + _7pi4 = (7 * math.Pi) / 4.0 _pi2 = math.Pi / 2.0 _pi4 = math.Pi / 4.0 + _d2r = (math.Pi / 180.0) + _r2d = (180.0 / math.Pi) ) +// DegreesToRadians returns degrees as radians. +func DegreesToRadians(degrees float64) float64 { + return degrees * _d2r +} + +// RadiansToDegrees translates a radian value to a degree value. +func RadiansToDegrees(value float64) float64 { + return math.Mod(value, _2pi) * _r2d +} + // PercentToRadians converts a normalized value (0,1) to radians. func PercentToRadians(pct float64) float64 { return DegreesToRadians(360.0 * pct) @@ -214,7 +226,31 @@ func RadianAdd(base, delta float64) float64 { if value > _2pi { return math.Mod(value, _2pi) } else if value < 0 { - return _2pi + value + return math.Mod(_2pi+value, _2pi) } return value } + +// DegreesAdd adds a delta to a base in radians. +func DegreesAdd(baseDegrees, deltaDegrees float64) float64 { + value := baseDegrees + deltaDegrees + if value > _2pi { + return math.Mod(value, 360.0) + } else if value < 0 { + return math.Mod(360.0+value, 360.0) + } + return value +} + +// DegreesToCompass returns the degree value in compass / clock orientation. +func DegreesToCompass(deg float64) float64 { + return DegreesAdd(deg, -90.0) +} + +// CirclePoint returns the absolute position of a circle diameter point given +// by the radius and the angle. +func CirclePoint(cx, cy int, radius, angleRadians float64) (x, y int) { + x = cx + int(radius*math.Sin(angleRadians)) + y = cy - int(radius*math.Cos(angleRadians)) + return +} diff --git a/util_test.go b/util_test.go index aff684e..c50ffa7 100644 --- a/util_test.go +++ b/util_test.go @@ -100,3 +100,60 @@ func TestPercentDifference(t *testing.T) { assert.Equal(0.5, PercentDifference(1.0, 1.5)) assert.Equal(-0.5, PercentDifference(2.0, 1.0)) } + +var ( + _degreesToRadians = map[float64]float64{ + 0: 0, // !_2pi b/c no irrational nums in floats. + 45: _pi4, + 90: _pi2, + 135: _3pi4, + 180: _pi, + 225: _5pi4, + 270: _3pi2, + 315: _7pi4, + } + + _compassToRadians = map[float64]float64{ + 0: _pi2, + 45: _pi4, + 90: 0, // !_2pi b/c no irrational nums in floats. + 135: _7pi4, + 180: _3pi2, + 225: _5pi4, + 270: _pi, + 315: _3pi4, + } +) + +func TestDegreesToRadians(t *testing.T) { + assert := assert.New(t) + + for d, r := range _degreesToRadians { + assert.Equal(r, DegreesToRadians(d)) + } +} + +func TestPercentToRadians(t *testing.T) { + assert := assert.New(t) + + for d, r := range _degreesToRadians { + assert.Equal(r, PercentToRadians(d/360.0)) + } +} + +func TestRadiansToDegrees(t *testing.T) { + assert := assert.New(t) + + for d, r := range _degreesToRadians { + assert.Equal(d, RadiansToDegrees(r)) + } +} + +func TestRadianAdd(t *testing.T) { + assert := assert.New(t) + + assert.Equal(_pi, RadianAdd(_pi2, _pi2)) + assert.Equal(_3pi2, RadianAdd(_pi2, _pi)) + assert.Equal(_pi, RadianAdd(_pi, _2pi)) + assert.Equal(_pi, RadianAdd(_pi, -_2pi)) +} diff --git a/vector_renderer.go b/vector_renderer.go index 35e8c5e..1f030be 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "math" "strings" "golang.org/x/image/font" @@ -82,7 +83,24 @@ func (vr *vectorRenderer) QuadCurveTo(cx, cy, x, y int) { } func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) { - vr.p = append(vr.p, fmt.Sprintf("A %d %d %0.2f 1 1 %d %d", int(rx), int(ry), delta, cx, cy)) + startAngle = RadianAdd(startAngle, _pi2) + endAngle := RadianAdd(startAngle, delta) + + startx := cx + int(rx*math.Sin(startAngle)) + starty := cy - int(ry*math.Cos(startAngle)) + + if len(vr.p) > 0 { + vr.p = append(vr.p, fmt.Sprintf("L %d %d", startx, starty)) + } else { + vr.p = append(vr.p, fmt.Sprintf("M %d %d", startx, starty)) + } + + endx := cx + int(rx*math.Sin(endAngle)) + endy := cy - int(ry*math.Cos(endAngle)) + + dd := RadiansToDegrees(delta) + + vr.p = append(vr.p, fmt.Sprintf("A %d %d %0.2f 0 1 %d %d", int(rx), int(ry), dd, endx, endy)) } // Close closes a shape.