big api overhauls.

This commit is contained in:
Will Charczuk 2016-07-29 18:24:25 -07:00
parent d84d6790c0
commit cbc0002d2a
33 changed files with 356 additions and 297 deletions

View File

@ -51,10 +51,10 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
lx := canvasBox.Left + xrange.Translate(a.XValue)
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
box.Top = MinInt(box.Top, ab.Top)
box.Left = MinInt(box.Left, ab.Left)
box.Right = MaxInt(box.Right, ab.Right)
box.Bottom = MaxInt(box.Bottom, ab.Bottom)
box.Top = Math.MinInt(box.Top, ab.Top)
box.Left = Math.MinInt(box.Left, ab.Left)
box.Right = Math.MaxInt(box.Right, ab.Right)
box.Bottom = Math.MaxInt(box.Bottom, ab.Bottom)
}
}
return box

View File

@ -11,8 +11,8 @@ func TestBollingerBandSeries(t *testing.T) {
assert := assert.New(t)
s1 := mockValueProvider{
X: Seq(1.0, 100.0),
Y: SeqRand(100, 1024),
X: Sequence.Float64(1.0, 100.0),
Y: Sequence.Random(100, 1024),
}
bbs := &BollingerBandsSeries{
@ -36,8 +36,8 @@ func TestBollingerBandLastValue(t *testing.T) {
assert := assert.New(t)
s1 := mockValueProvider{
X: Seq(1.0, 100.0),
Y: Seq(1.0, 100.0),
X: Sequence.Float64(1.0, 100.0),
Y: Sequence.Float64(1.0, 100.0),
}
bbs := &BollingerBandsSeries{

20
box.go
View File

@ -66,12 +66,12 @@ func (b Box) GetBottom(defaults ...int) int {
// Width returns the width
func (b Box) Width() int {
return AbsInt(b.Right - b.Left)
return Math.AbsInt(b.Right - b.Left)
}
// Height returns the height
func (b Box) Height() int {
return AbsInt(b.Bottom - b.Top)
return Math.AbsInt(b.Bottom - b.Top)
}
// Center returns the center of the box
@ -122,10 +122,10 @@ func (b Box) Equals(other Box) bool {
// Grow grows a box based on another box.
func (b Box) Grow(other Box) Box {
return Box{
Top: MinInt(b.Top, other.Top),
Left: MinInt(b.Left, other.Left),
Right: MaxInt(b.Right, other.Right),
Bottom: MaxInt(b.Bottom, other.Bottom),
Top: Math.MinInt(b.Top, other.Top),
Left: Math.MinInt(b.Left, other.Left),
Right: Math.MaxInt(b.Right, other.Right),
Bottom: Math.MaxInt(b.Bottom, other.Bottom),
}
}
@ -186,10 +186,10 @@ func (b Box) Fit(other Box) Box {
func (b Box) Constrain(other Box) Box {
newBox := b.Clone()
newBox.Top = MaxInt(newBox.Top, other.Top)
newBox.Left = MaxInt(newBox.Left, other.Left)
newBox.Right = MinInt(newBox.Right, other.Right)
newBox.Bottom = MinInt(newBox.Bottom, other.Bottom)
newBox.Top = Math.MaxInt(newBox.Top, other.Top)
newBox.Left = Math.MaxInt(newBox.Left, other.Left)
newBox.Right = Math.MinInt(newBox.Right, other.Right)
newBox.Bottom = Math.MinInt(newBox.Bottom, other.Bottom)
return newBox
}

View File

@ -230,8 +230,8 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
yrange.SetMax(maxy)
delta := yrange.GetDelta()
roundTo := GetRoundToForDelta(delta)
rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
roundTo := Math.GetRoundToForDelta(delta)
rmin, rmax := Math.RoundDown(yrange.GetMin(), roundTo), Math.RoundUp(yrange.GetMax(), roundTo)
yrange.SetMin(rmin)
yrange.SetMax(rmax)
}
@ -249,8 +249,8 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
yrangeAlt.SetMax(maxya)
delta := yrangeAlt.GetDelta()
roundTo := GetRoundToForDelta(delta)
rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
roundTo := Math.GetRoundToForDelta(delta)
rmin, rmax := Math.RoundDown(yrangeAlt.GetMin(), roundTo), Math.RoundUp(yrangeAlt.GetMax(), roundTo)
yrangeAlt.SetMin(rmin)
yrangeAlt.SetMax(rmax)
}

View File

@ -384,8 +384,8 @@ func TestChartRegressionBadRangesByUser(t *testing.T) {
},
Series: []Series{
ContinuousSeries{
XValues: Seq(1.0, 10.0),
YValues: Seq(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
},
},
}

View File

@ -10,18 +10,18 @@ func TestConcatSeries(t *testing.T) {
assert := assert.New(t)
s1 := ContinuousSeries{
XValues: Seq(1.0, 10.0),
YValues: Seq(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
}
s2 := ContinuousSeries{
XValues: Seq(11, 20.0),
YValues: Seq(10.0, 1.0),
XValues: Sequence.Float64(11, 20.0),
YValues: Sequence.Float64(10.0, 1.0),
}
s3 := ContinuousSeries{
XValues: Seq(21, 30.0),
YValues: Seq(1.0, 10.0),
XValues: Sequence.Float64(21, 30.0),
YValues: Sequence.Float64(1.0, 10.0),
}
cs := ConcatSeries([]Series{s1, s2, s3})

View File

@ -10,7 +10,7 @@ func TestRangeTranslate(t *testing.T) {
assert := assert.New(t)
values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
r := ContinuousRange{Domain: 1000}
r.Min, r.Max = MinAndMax(values...)
r.Min, r.Max = Math.MinAndMax(values...)
// delta = ~7.0
// value = ~5.0

View File

@ -11,8 +11,8 @@ func TestContinuousSeries(t *testing.T) {
cs := ContinuousSeries{
Name: "Test Series",
XValues: Seq(1.0, 10.0),
YValues: Seq(1.0, 10.0),
XValues: Sequence.Float64(1.0, 10.0),
YValues: Sequence.Float64(1.0, 10.0),
}
assert.Equal("Test Series", cs.GetName())

View File

@ -33,6 +33,9 @@ const (
// DefaultTitleTop is the default distance from the top of the chart to put the title.
DefaultTitleTop = 10
// DefaultLineSpacing is the default vertical distance between lines of text.
DefaultLineSpacing = 5
// DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels.
DefaultYAxisMargin = 10
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.

94
draw.go
View File

@ -10,7 +10,7 @@ var (
type draw struct{}
// LineSeries draws a line series with a renderer.
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider) {
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider) {
if vs.Len() == 0 {
return
}
@ -25,7 +25,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
var vx, vy float64
var x, y int
fill := s.GetFillColor()
fill := style.GetFillColor()
if !fill.IsZero() {
r.SetFillColor(fill)
r.MoveTo(x0, y0)
@ -41,9 +41,9 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
r.Fill()
}
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
r.SetStrokeColor(style.GetStrokeColor())
r.SetStrokeDashArray(style.GetStrokeDashArray())
r.SetStrokeWidth(style.GetStrokeWidth())
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
@ -56,16 +56,13 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
}
// BoundedSeries draws a series that implements BoundedValueProvider.
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
drawOffsetIndex := 0
if len(drawOffsetIndexes) > 0 {
drawOffsetIndex = drawOffsetIndexes[0]
}
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFillColor(s.GetFillColor())
style.WriteToRenderer(r)
cb := canvasBox.Bottom
cl := canvasBox.Left
@ -110,7 +107,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s S
}
// HistogramSeries draws a value provider as boxes from 0.
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider, barWidths ...int) {
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider, barWidths ...int) {
if vs.Len() == 0 {
return
}
@ -137,30 +134,25 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
Left: x - (barWidth >> 1),
Right: x + (barWidth >> 1),
Bottom: cb - y,
}, s)
}, style)
}
}
// MeasureAnnotation measures how big an annotation would be.
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string) Box {
r.SetFillColor(s.GetFillColor(DefaultAnnotationFillColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
style.WriteToRenderer(r)
textBox := r.MeasureText(label)
textWidth := textBox.Width()
textHeight := textBox.Height()
halfTextHeight := textHeight >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := s.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := s.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
strokeWidth := s.GetStrokeWidth()
strokeWidth := style.GetStrokeWidth()
top := ly - (pt + halfTextHeight)
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
@ -176,14 +168,7 @@ func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int,
// Annotation draws an anotation with a renderer.
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
r.SetFillColor(style.GetFillColor())
r.SetStrokeColor(style.GetStrokeColor())
r.SetStrokeWidth(style.GetStrokeWidth())
r.SetStrokeDashArray(style.GetStrokeDashArray())
r.SetFont(style.GetFont())
r.SetFontColor(style.GetFontColor(DefaultTextColor))
r.SetFontSize(style.GetFontSize(DefaultAnnotationFontSize))
style.WriteToRenderer(r)
textBox := r.MeasureText(label)
textWidth := textBox.Width()
@ -223,10 +208,7 @@ func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, lab
// Box draws a box with a given style.
func (d draw) Box(r Renderer, b Box, s Style) {
r.SetFillColor(s.GetFillColor())
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
r.SetStrokeDashArray(s.GetStrokeDashArray())
s.WriteToRenderer(r)
r.MoveTo(b.Left, b.Top)
r.LineTo(b.Right, b.Top)
@ -237,17 +219,41 @@ func (d draw) Box(r Renderer, b Box, s Style) {
}
// DrawText draws text with a given style.
func (d draw) Text(r Renderer, text string, x, y int, s Style) {
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize())
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
style.GetTextOptions().WriteToRenderer(r)
r.Text(text, x, y)
}
// TextWithin draws the text within a given box.
func (d draw) TextWithin(r Renderer, text string, box Box, s Style) {
func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
lines := Text.WrapFit(r, text, box.Width(), style)
linesBox := Text.MeasureLines(r, lines, style)
style.GetTextOptions().WriteToRenderer(r)
y := box.Top
switch style.GetTextVerticalAlign() {
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
y = y - linesBox.Height()
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
y = (y - linesBox.Height()) >> 1
}
var tx, ty int
for _, line := range lines {
lineBox := r.MeasureText(line)
switch style.GetTextHorizontalAlign() {
case TextHorizontalAlignCenter:
tx = box.Left + ((lineBox.Width() - box.Left) >> 1)
case TextHorizontalAlignRight:
tx = box.Right - lineBox.Width()
default:
tx = box.Left
}
ty = y + lineBox.Height()
d.Text(r, line, tx, ty, style)
y += lineBox.Height() + style.GetTextLineSpacing()
}
}

View File

@ -7,7 +7,7 @@ import (
)
var (
emaXValues = Seq(1.0, 50.0)
emaXValues = Sequence.Float64(1.0, 50.0)
emaYValues = []float64{
1, 2, 3, 4, 5, 4, 3, 2,
1, 2, 3, 4, 5, 4, 3, 2,

View File

@ -30,8 +30,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: chart.Seq(1.0, 100.0),
YValues: chart.SeqRand(100.0, 256.0),
XValues: chart.Sequence.Float64(1.0, 100.0),
YValues: chart.Sequence.Random(100.0, 256.0),
},
},
}
@ -57,8 +57,8 @@ func drawChartDefault(res http.ResponseWriter, req *http.Request) {
},
Series: []chart.Series{
chart.ContinuousSeries{
XValues: chart.Seq(1.0, 100.0),
YValues: chart.SeqRand(100.0, 256.0),
XValues: chart.Sequence.Float64(1.0, 100.0),
YValues: chart.Sequence.Random(100.0, 256.0),
},
},
}

View File

@ -38,7 +38,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
//note we have to do this as a separate step because we need a reference to graph
graph.Elements = []chart.Renderable{
chart.CreateLegend(&graph),
chart.Legend(&graph),
}
res.Header().Set("Content-Type", "image/png")

View File

@ -15,8 +15,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
mainSeries := chart.ContinuousSeries{
Name: "A test series",
XValues: chart.Seq(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
}
// note we create a LinearRegressionSeries series by assignin the inner series.

View File

@ -15,8 +15,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
mainSeries := chart.ContinuousSeries{
Name: "A test series",
XValues: chart.Seq(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
}
// note we create a SimpleMovingAverage series by assignin the inner series.

View File

@ -11,8 +11,8 @@ func TestHistogramSeries(t *testing.T) {
cs := ContinuousSeries{
Name: "Test Series",
XValues: Seq(1.0, 20.0),
YValues: Seq(10.0, -10.0),
XValues: Sequence.Float64(1.0, 20.0),
YValues: Sequence.Float64(10.0, -10.0),
}
hs := HistogramSeries{

View File

@ -68,7 +68,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
}
legendContent.Bottom += tb.Height()
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
legendContent.Right = MaxInt(legendContent.Right, right)
legendContent.Right = Math.MaxInt(legendContent.Right, right)
labelCount++
}
}

View File

@ -34,7 +34,7 @@ func (lrs LinearRegressionSeries) GetYAxis() YAxisType {
// Len returns the number of elements in the series.
func (lrs LinearRegressionSeries) Len() int {
return MinInt(lrs.GetWindow(), lrs.InnerSeries.Len()-lrs.GetOffset())
return Math.MinInt(lrs.GetWindow(), lrs.InnerSeries.Len()-lrs.GetOffset())
}
// GetWindow returns the window size.
@ -47,7 +47,7 @@ func (lrs LinearRegressionSeries) GetWindow() int {
// GetEndIndex returns the effective window end.
func (lrs LinearRegressionSeries) GetEndIndex() int {
return MinInt(lrs.GetOffset()+(lrs.Len()), (lrs.InnerSeries.Len() - 1))
return Math.MinInt(lrs.GetOffset()+(lrs.Len()), (lrs.InnerSeries.Len() - 1))
}
// GetOffset returns the data offset.
@ -67,7 +67,7 @@ func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) {
lrs.computeCoefficients()
}
offset := lrs.GetOffset()
effectiveIndex := MinInt(index+offset, lrs.InnerSeries.Len())
effectiveIndex := Math.MinInt(index+offset, lrs.InnerSeries.Len())
x, y = lrs.InnerSeries.GetValue(effectiveIndex)
y = (lrs.m * lrs.normalize(x)) + lrs.b
return

View File

@ -11,8 +11,8 @@ func TestLinearRegressionSeries(t *testing.T) {
mainSeries := ContinuousSeries{
Name: "A test series",
XValues: Seq(1.0, 100.0),
YValues: Seq(1.0, 100.0),
XValues: Sequence.Float64(1.0, 100.0),
YValues: Sequence.Float64(1.0, 100.0),
}
linRegSeries := &LinearRegressionSeries{
@ -33,8 +33,8 @@ func TestLinearRegressionSeriesDesc(t *testing.T) {
mainSeries := ContinuousSeries{
Name: "A test series",
XValues: Seq(100.0, 1.0),
YValues: Seq(100.0, 1.0),
XValues: Sequence.Float64(100.0, 1.0),
YValues: Sequence.Float64(100.0, 1.0),
}
linRegSeries := &LinearRegressionSeries{
@ -55,8 +55,8 @@ func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) {
mainSeries := ContinuousSeries{
Name: "A test series",
XValues: Seq(100.0, 1.0),
YValues: Seq(100.0, 1.0),
XValues: Sequence.Float64(100.0, 1.0),
YValues: Sequence.Float64(100.0, 1.0),
}
linRegSeries := &LinearRegressionSeries{

View File

@ -1,19 +1,23 @@
package chart
import (
"fmt"
"math"
"math/rand"
"time"
)
// Float is an alias for float64 that provides a better .String() method.
type Float float64
// String returns the string representation of a float.
func (f Float) String() string {
return fmt.Sprintf("%.2f", f)
}
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)
)
// TimeToFloat64 returns a float64 representation of a time.
func TimeToFloat64(t time.Time) float64 {
@ -25,8 +29,15 @@ func Float64ToTime(tf float64) time.Time {
return time.Unix(0, int64(tf))
}
var (
// Math contains helper methods for common math operations.
Math = &mathUtil{}
)
type mathUtil struct{}
// MinAndMax returns both the min and max in one pass.
func MinAndMax(values ...float64) (min float64, max float64) {
func (m mathUtil) MinAndMax(values ...float64) (min float64, max float64) {
if len(values) == 0 {
return
}
@ -45,7 +56,7 @@ func MinAndMax(values ...float64) (min float64, max float64) {
// MinAndMaxOfTime returns the min and max of a given set of times
// in one pass.
func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
func (m mathUtil) MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
if len(values) == 0 {
return
}
@ -64,19 +75,8 @@ func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
return
}
// Slices generates N slices that span the total.
// The resulting array will be intermediate indexes until total.
func Slices(count int, total float64) []float64 {
var values []float64
sliceWidth := float64(total) / float64(count)
for cursor := 0.0; cursor < total; cursor += sliceWidth {
values = append(values, cursor)
}
return values
}
// GetRoundToForDelta returns a `roundTo` value for a given delta.
func GetRoundToForDelta(delta float64) float64 {
func (m mathUtil) GetRoundToForDelta(delta float64) float64 {
startingDeltaBound := math.Pow(10.0, 10.0)
for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 {
if delta > cursor {
@ -88,13 +88,13 @@ func GetRoundToForDelta(delta float64) float64 {
}
// RoundUp rounds up to a given roundTo value.
func RoundUp(value, roundTo float64) float64 {
func (m mathUtil) RoundUp(value, roundTo float64) float64 {
d1 := math.Ceil(value / roundTo)
return d1 * roundTo
}
// RoundDown rounds down to a given roundTo value.
func RoundDown(value, roundTo float64) float64 {
func (m mathUtil) RoundDown(value, roundTo float64) float64 {
d1 := math.Floor(value / roundTo)
return d1 * roundTo
}
@ -102,20 +102,20 @@ func RoundDown(value, roundTo float64) float64 {
// Normalize returns a set of numbers on the interval [0,1] for a given set of inputs.
// An example: 4,3,2,1 => 0.4, 0.3, 0.2, 0.1
// Caveat; the total may be < 1.0; there are going to be issues with irrational numbers etc.
func Normalize(values ...float64) []float64 {
func (m mathUtil) Normalize(values ...float64) []float64 {
var total float64
for _, v := range values {
total += v
}
output := make([]float64, len(values))
for x, v := range values {
output[x] = RoundDown(v/total, 0.0001)
output[x] = m.RoundDown(v/total, 0.0001)
}
return output
}
// MinInt returns the minimum of a set of integers.
func MinInt(values ...int) int {
func (m mathUtil) MinInt(values ...int) int {
min := math.MaxInt32
for _, v := range values {
if v < min {
@ -126,7 +126,7 @@ func MinInt(values ...int) int {
}
// MaxInt returns the maximum of a set of integers.
func MaxInt(values ...int) int {
func (m mathUtil) MaxInt(values ...int) int {
max := math.MinInt32
for _, v := range values {
if v > max {
@ -137,47 +137,15 @@ func MaxInt(values ...int) int {
}
// AbsInt returns the absolute value of an integer.
func AbsInt(value int) int {
func (m mathUtil) AbsInt(value int) int {
if value < 0 {
return -value
}
return value
}
// Seq produces an array of floats from [start,end] by optional steps.
func Seq(start, end float64, steps ...float64) []float64 {
var values []float64
step := 1.0
if len(steps) > 0 {
step = steps[0]
}
if start < end {
for x := start; x <= end; x += step {
values = append(values, x)
}
} else {
for x := start; x >= end; x = x - step {
values = append(values, x)
}
}
return values
}
// SeqRand generates a random sequence.
func SeqRand(samples int, scale float64) []float64 {
rnd := rand.New(rand.NewSource(time.Now().Unix()))
values := make([]float64, samples)
for x := 0; x < samples; x++ {
values[x] = rnd.Float64() * scale
}
return values
}
// Sum sums a set of values.
func Sum(values ...float64) float64 {
func (m mathUtil) Sum(values ...float64) float64 {
var total float64
for _, v := range values {
total += v
@ -186,7 +154,7 @@ func Sum(values ...float64) float64 {
}
// SumInt sums a set of values.
func SumInt(values ...int) int {
func (m mathUtil) SumInt(values ...int) int {
var total int
for _, v := range values {
total += v
@ -194,52 +162,32 @@ func SumInt(values ...int) int {
return total
}
// SeqDays generates a sequence of timestamps by day, from -days to today.
func SeqDays(days int) []time.Time {
var values []time.Time
for day := days; day >= 0; day-- {
values = append(values, time.Now().AddDate(0, 0, -day))
}
return values
}
// PercentDifference computes the percentage difference between two values.
// The formula is (v2-v1)/v1.
func PercentDifference(v1, v2 float64) float64 {
func (m mathUtil) PercentDifference(v1, v2 float64) float64 {
if v1 == 0 {
return 0
}
return (v2 - v1) / v1
}
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 {
func (m mathUtil) DegreesToRadians(degrees float64) float64 {
return degrees * _d2r
}
// RadiansToDegrees translates a radian value to a degree value.
func RadiansToDegrees(value float64) float64 {
func (m mathUtil) 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)
func (m mathUtil) PercentToRadians(pct float64) float64 {
return m.DegreesToRadians(360.0 * pct)
}
// RadianAdd adds a delta to a base in radians.
func RadianAdd(base, delta float64) float64 {
func (m mathUtil) RadianAdd(base, delta float64) float64 {
value := base + delta
if value > _2pi {
return math.Mod(value, _2pi)
@ -250,7 +198,7 @@ func RadianAdd(base, delta float64) float64 {
}
// DegreesAdd adds a delta to a base in radians.
func DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
func (m mathUtil) DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
value := baseDegrees + deltaDegrees
if value > _2pi {
return math.Mod(value, 360.0)
@ -261,13 +209,13 @@ func DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
}
// DegreesToCompass returns the degree value in compass / clock orientation.
func DegreesToCompass(deg float64) float64 {
return DegreesAdd(deg, -90.0)
func (m mathUtil) DegreesToCompass(deg float64) float64 {
return m.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) {
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))
return

View File

@ -10,7 +10,7 @@ import (
func TestMinAndMax(t *testing.T) {
assert := assert.New(t)
values := []float64{1.0, 2.0, 3.0, 4.0}
min, max := MinAndMax(values...)
min, max := Math.MinAndMax(values...)
assert.Equal(1.0, min)
assert.Equal(4.0, max)
}
@ -18,7 +18,7 @@ func TestMinAndMax(t *testing.T) {
func TestMinAndMaxReversed(t *testing.T) {
assert := assert.New(t)
values := []float64{4.0, 2.0, 3.0, 1.0}
min, max := MinAndMax(values...)
min, max := Math.MinAndMax(values...)
assert.Equal(1.0, min)
assert.Equal(4.0, max)
}
@ -26,7 +26,7 @@ func TestMinAndMaxReversed(t *testing.T) {
func TestMinAndMaxEmpty(t *testing.T) {
assert := assert.New(t)
values := []float64{}
min, max := MinAndMax(values...)
min, max := Math.MinAndMax(values...)
assert.Equal(0.0, min)
assert.Equal(0.0, max)
}
@ -39,7 +39,7 @@ func TestMinAndMaxOfTime(t *testing.T) {
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -4),
}
min, max := MinAndMaxOfTime(values...)
min, max := Math.MinAndMaxOfTime(values...)
assert.Equal(values[3], min)
assert.Equal(values[0], max)
}
@ -52,7 +52,7 @@ func TestMinAndMaxOfTimeReversed(t *testing.T) {
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -1),
}
min, max := MinAndMaxOfTime(values...)
min, max := Math.MinAndMaxOfTime(values...)
assert.Equal(values[0], min)
assert.Equal(values[3], max)
}
@ -60,66 +60,45 @@ func TestMinAndMaxOfTimeReversed(t *testing.T) {
func TestMinAndMaxOfTimeEmpty(t *testing.T) {
assert := assert.New(t)
values := []time.Time{}
min, max := MinAndMaxOfTime(values...)
min, max := Math.MinAndMaxOfTime(values...)
assert.Equal(time.Time{}, min)
assert.Equal(time.Time{}, max)
}
func TestSlices(t *testing.T) {
assert := assert.New(t)
s := Slices(10, 100)
assert.Len(s, 10)
assert.Equal(0, s[0])
assert.Equal(10, s[1])
assert.Equal(20, s[2])
assert.Equal(90, s[9])
}
func TestGetRoundToForDelta(t *testing.T) {
assert := assert.New(t)
assert.Equal(100.0, GetRoundToForDelta(1001.00))
assert.Equal(10.0, GetRoundToForDelta(101.00))
assert.Equal(1.0, GetRoundToForDelta(11.00))
assert.Equal(100.0, Math.GetRoundToForDelta(1001.00))
assert.Equal(10.0, Math.GetRoundToForDelta(101.00))
assert.Equal(1.0, Math.GetRoundToForDelta(11.00))
}
func TestRoundUp(t *testing.T) {
assert := assert.New(t)
assert.Equal(0.5, RoundUp(0.49, 0.1))
assert.Equal(1.0, RoundUp(0.51, 1.0))
assert.Equal(0.4999, RoundUp(0.49988, 0.0001))
assert.Equal(0.5, Math.RoundUp(0.49, 0.1))
assert.Equal(1.0, Math.RoundUp(0.51, 1.0))
assert.Equal(0.4999, Math.RoundUp(0.49988, 0.0001))
}
func TestRoundDown(t *testing.T) {
assert := assert.New(t)
assert.Equal(0.5, RoundDown(0.51, 0.1))
assert.Equal(1.0, RoundDown(1.01, 1.0))
assert.Equal(0.5001, RoundDown(0.50011, 0.0001))
}
func TestSeq(t *testing.T) {
assert := assert.New(t)
asc := Seq(1.0, 10.0)
assert.Len(asc, 10)
desc := Seq(10.0, 1.0)
assert.Len(desc, 10)
assert.Equal(0.5, Math.RoundDown(0.51, 0.1))
assert.Equal(1.0, Math.RoundDown(1.01, 1.0))
assert.Equal(0.5001, Math.RoundDown(0.50011, 0.0001))
}
func TestPercentDifference(t *testing.T) {
assert := assert.New(t)
assert.Equal(0.5, PercentDifference(1.0, 1.5))
assert.Equal(-0.5, PercentDifference(2.0, 1.0))
assert.Equal(0.5, Math.PercentDifference(1.0, 1.5))
assert.Equal(-0.5, Math.PercentDifference(2.0, 1.0))
}
func TestNormalize(t *testing.T) {
assert := assert.New(t)
values := []float64{10, 9, 8, 7, 6}
normalized := Normalize(values...)
normalized := Math.Normalize(values...)
assert.Len(normalized, 5)
assert.Equal(0.25, normalized[0])
assert.Equal(0.1499, normalized[4])
@ -153,7 +132,7 @@ func TestDegreesToRadians(t *testing.T) {
assert := assert.New(t)
for d, r := range _degreesToRadians {
assert.Equal(r, DegreesToRadians(d))
assert.Equal(r, Math.DegreesToRadians(d))
}
}
@ -161,7 +140,7 @@ func TestPercentToRadians(t *testing.T) {
assert := assert.New(t)
for d, r := range _degreesToRadians {
assert.Equal(r, PercentToRadians(d/360.0))
assert.Equal(r, Math.PercentToRadians(d/360.0))
}
}
@ -169,15 +148,15 @@ func TestRadiansToDegrees(t *testing.T) {
assert := assert.New(t)
for d, r := range _degreesToRadians {
assert.Equal(d, RadiansToDegrees(r))
assert.Equal(d, Math.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))
assert.Equal(_pi, Math.RadianAdd(_pi2, _pi2))
assert.Equal(_3pi2, Math.RadianAdd(_pi2, _pi))
assert.Equal(_pi, Math.RadianAdd(_pi, _2pi))
assert.Equal(_pi, Math.RadianAdd(_pi, -_2pi))
}

View File

@ -111,26 +111,13 @@ func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
func (pc PieChart) drawTitle(r Renderer) {
if len(pc.Title) > 0 && pc.TitleStyle.Show {
r.SetFont(pc.TitleStyle.GetFont(pc.GetFont()))
r.SetFontColor(pc.TitleStyle.GetFontColor(DefaultTextColor))
titleFontSize := pc.TitleStyle.GetFontSize(DefaultTitleFontSize)
r.SetFontSize(titleFontSize)
textBox := r.MeasureText(pc.Title)
textWidth := textBox.Width()
textHeight := textBox.Height()
titleX := (pc.GetWidth() >> 1) - (textWidth >> 1)
titleY := pc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
r.Text(pc.Title, titleX, titleY)
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
}
}
func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
cx, cy := canvasBox.Center()
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
diameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
radius := float64(diameter >> 1)
labelRadius := (radius * 2.0) / 3.0
@ -141,8 +128,8 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
r.MoveTo(cx, cy)
rads = PercentToRadians(total)
delta = PercentToRadians(v.Value)
rads = Math.PercentToRadians(total)
delta = Math.PercentToRadians(v.Value)
r.ArcTo(cx, cy, radius, radius, rads, delta)
@ -157,9 +144,9 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
for index, v := range values {
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
if len(v.Label) > 0 {
delta2 = PercentToRadians(total + (v.Value / 2.0))
delta2 = RadianAdd(delta2, _pi2)
lx, ly = CirclePoint(cx, cy, labelRadius, delta2)
delta2 = Math.PercentToRadians(total + (v.Value / 2.0))
delta2 = Math.RadianAdd(delta2, _pi2)
lx, ly = Math.CirclePoint(cx, cy, labelRadius, delta2)
tb := r.MeasureText(v.Label)
lx = lx - (tb.Width() >> 1)
@ -180,7 +167,7 @@ func (pc PieChart) getDefaultCanvasBox() Box {
}
func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
circleDiameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
square := Box{
Right: circleDiameter,
@ -226,7 +213,7 @@ func (pc PieChart) stylePieChartValue(index int) Style {
}
func (pc PieChart) getScaledFontSize() float64 {
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
effectiveDimension := Math.MinInt(pc.GetWidth(), pc.GetHeight())
if effectiveDimension >= 2048 {
return 48.0
} else if effectiveDimension >= 1024 {
@ -253,6 +240,17 @@ func (pc PieChart) styleDefaultsElements() Style {
}
}
func (pc PieChart) styleDefaultsTitle() Style {
return pc.TitleStyle.InheritFrom(Style{
FontColor: DefaultTextColor,
Font: pc.GetFont(),
FontSize: 24.0,
TextHorizontalAlign: TextHorizontalAlignCenter,
TextVerticalAlign: TextVerticalAlignTop,
TextWrap: TextWrapNone,
})
}
// Box returns the chart bounds as a box.
func (pc PieChart) Box() Box {
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)

55
sequence.go Normal file
View File

@ -0,0 +1,55 @@
package chart
import (
"math/rand"
"time"
)
var (
// Sequence contains some sequence utilities.
// These utilities can be useful for generating test data.
Sequence = &sequence{}
)
type sequence struct{}
// Float64 produces an array of floats from [start,end] by optional steps.
func (s sequence) Float64(start, end float64, steps ...float64) []float64 {
var values []float64
step := 1.0
if len(steps) > 0 {
step = steps[0]
}
if start < end {
for x := start; x <= end; x += step {
values = append(values, x)
}
} else {
for x := start; x >= end; x = x - step {
values = append(values, x)
}
}
return values
}
// Random generates a fixed length sequence of random values between (0, scale).
func (s sequence) Random(samples int, scale float64) []float64 {
rnd := rand.New(rand.NewSource(time.Now().Unix()))
values := make([]float64, samples)
for x := 0; x < samples; x++ {
values[x] = rnd.Float64() * scale
}
return values
}
// Days generates a sequence of timestamps by day, from -days to today.
func (s sequence) Days(days int) []time.Time {
var values []time.Time
for day := days; day >= 0; day-- {
values = append(values, time.Now().AddDate(0, 0, -day))
}
return values
}

17
sequence_test.go Normal file
View File

@ -0,0 +1,17 @@
package chart
import (
"testing"
assert "github.com/blendlabs/go-assert"
)
func TestSequenceFloat64(t *testing.T) {
assert := assert.New(t)
asc := Sequence.Float64(1.0, 10.0)
assert.Len(asc, 10)
desc := Sequence.Float64(10.0, 1.0)
assert.Len(desc, 10)
}

View File

@ -72,7 +72,7 @@ func (sma SMASeries) GetLastValue() (x, y float64) {
func (sma SMASeries) getAverage(index int) float64 {
period := sma.GetPeriod()
floor := MaxInt(0, index-period)
floor := Math.MaxInt(0, index-period)
var accum float64
var count float64
for x := index; x >= floor; x-- {

View File

@ -12,14 +12,14 @@ type mockValueProvider struct {
}
func (m mockValueProvider) Len() int {
return MinInt(len(m.X), len(m.Y))
return Math.MinInt(len(m.X), len(m.Y))
}
func (m mockValueProvider) GetValue(index int) (x, y float64) {
if index < 0 {
panic("negative index at GetValue()")
}
if index > MinInt(len(m.X), len(m.Y)) {
if index > Math.MinInt(len(m.X), len(m.Y)) {
panic("index is outside the length of m.X or m.Y")
}
x = m.X[index]
@ -31,8 +31,8 @@ func TestSMASeriesGetValue(t *testing.T) {
assert := assert.New(t)
mockSeries := mockValueProvider{
Seq(1.0, 10.0),
Seq(10, 1.0),
Sequence.Float64(1.0, 10.0),
Sequence.Float64(10, 1.0),
}
assert.Equal(10, mockSeries.Len())
@ -62,8 +62,8 @@ func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) {
assert := assert.New(t)
mockSeries := mockValueProvider{
Seq(1.0, 10.0),
Seq(10, 1.0),
Sequence.Float64(1.0, 10.0),
Sequence.Float64(10, 1.0),
}
assert.Equal(10, mockSeries.Len())
@ -88,8 +88,8 @@ func TestSMASeriesGetLastValue(t *testing.T) {
assert := assert.New(t)
mockSeries := mockValueProvider{
Seq(1.0, 100.0),
Seq(100, 1.0),
Sequence.Float64(1.0, 100.0),
Sequence.Float64(100, 1.0),
}
assert.Equal(100, mockSeries.Len())

View File

@ -25,6 +25,7 @@ type Style struct {
TextHorizontalAlign textHorizontalAlign
TextVerticalAlign textVerticalAlign
TextWrap textWrap
TextLineSpacing int
}
// IsZero returns if the object is set or not.
@ -222,6 +223,17 @@ func (s Style) GetTextWrap(defaults ...textWrap) textWrap {
return s.TextWrap
}
// GetTextLineSpacing returns the spacing in pixels between lines of text (vertically).
func (s Style) GetTextLineSpacing(defaults ...int) int {
if s.TextLineSpacing == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultLineSpacing
}
return s.TextLineSpacing
}
// WriteToRenderer passes the style's options to a renderer.
func (s Style) WriteToRenderer(r Renderer) {
r.SetStrokeColor(s.GetStrokeColor())
@ -261,6 +273,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
final.TextHorizontalAlign = s.GetTextHorizontalAlign(defaults.TextHorizontalAlign)
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
return
}
@ -299,5 +312,6 @@ func (s Style) GetTextOptions() Style {
TextHorizontalAlign: s.TextHorizontalAlign,
TextVerticalAlign: s.TextVerticalAlign,
TextWrap: s.TextWrap,
TextLineSpacing: s.TextLineSpacing,
}
}

21
text.go
View File

@ -64,16 +64,13 @@ type TextStyle struct {
type text struct{}
func (t text) WrapFit(r Renderer, value string, width int, style Style, wrapOption textWrap) []string {
valueBox := r.MeasureText(value)
if valueBox.Width() > width {
switch wrapOption {
func (t text) WrapFit(r Renderer, value string, width int, style Style) []string {
switch style.TextWrap {
case TextWrapRune:
return t.WrapFitRune(r, value, width, style)
case TextWrapWord:
return t.WrapFitWord(r, value, width, style)
}
}
return []string{value}
}
@ -143,6 +140,20 @@ func (t text) Trim(value string) string {
return strings.Trim(value, " \t\n\r")
}
func (t text) MeasureLines(r Renderer, lines []string, style Style) Box {
style.WriteTextOptionsToRenderer(r)
var output Box
for index, line := range lines {
lineBox := r.MeasureText(line)
output.Right = Math.MaxInt(lineBox.Right, output.Right)
output.Bottom += lineBox.Height()
if index < len(lines)-1 {
output.Bottom += +style.GetTextLineSpacing()
}
}
return output
}
func (t text) appendLast(lines []string, text string) []string {
if len(lines) == 0 {
return []string{text}

View File

@ -25,8 +25,36 @@ func TestTextWrapWord(t *testing.T) {
lineBox := r.MeasureText(line)
assert.True(lineBox.Width() < 100, line+": "+lineBox.String())
}
assert.Equal("this is", output[0])
assert.Equal("a test", output[1])
assert.Equal("string", output[2])
output = Text.WrapFitWord(r, "foo", 100, basicTextStyle)
assert.Len(output, 1)
assert.Equal("foo", output[0])
// test that it handles newlines.
output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring", 100, basicTextStyle)
assert.Len(output, 5)
// test that it handles newlines and long lines.
output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring that is very long", 100, basicTextStyle)
assert.Len(output, 8)
}
func TestTextWrapRune(t *testing.T) {
assert := assert.New(t)
r, err := PNG(1024, 1024)
assert.Nil(err)
f, err := GetDefaultFont()
assert.Nil(err)
basicTextStyle := Style{Font: f, FontSize: 24}
output := Text.WrapFitRune(r, "this is a test string", 150, basicTextStyle)
assert.NotEmpty(output)
assert.Len(output, 2)
assert.Equal("this is a t", output[0])
assert.Equal("est string", output[1])
}

View File

@ -21,18 +21,18 @@ func (vs Values) Values() []float64 {
// ValuesNormalized returns normalized values.
func (vs Values) ValuesNormalized() []float64 {
return Normalize(vs.Values()...)
return Math.Normalize(vs.Values()...)
}
// Normalize returns the values normalized.
func (vs Values) Normalize() []Value {
output := make([]Value, len(vs))
total := Sum(vs.Values()...)
total := Math.Sum(vs.Values()...)
for index, v := range vs {
output[index] = Value{
Style: v.Style,
Label: v.Label,
Value: RoundDown(v.Value/total, 0.0001),
Value: Math.RoundDown(v.Value/total, 0.0001),
}
}
return output

View File

@ -83,8 +83,8 @@ func (vr *vectorRenderer) QuadCurveTo(cx, cy, x, y int) {
}
func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
startAngle = RadianAdd(startAngle, _pi2)
endAngle := RadianAdd(startAngle, delta)
startAngle = Math.RadianAdd(startAngle, _pi2)
endAngle := Math.RadianAdd(startAngle, delta)
startx := cx + int(rx*math.Sin(startAngle))
starty := cy - int(ry*math.Cos(startAngle))
@ -98,7 +98,7 @@ func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
endx := cx + int(rx*math.Sin(endAngle))
endy := cy - int(ry*math.Cos(endAngle))
dd := RadiansToDegrees(delta)
dd := Math.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))
}

View File

@ -66,10 +66,10 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
tx := canvasBox.Left + lx
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
top = MinInt(top, canvasBox.Bottom)
left = MinInt(left, tx-(tb.Width()>>1))
right = MaxInt(right, tx+(tb.Width()>>1))
bottom = MaxInt(bottom, ty)
top = Math.MinInt(top, canvasBox.Bottom)
left = Math.MinInt(left, tx-(tb.Width()>>1))
right = Math.MaxInt(right, tx+(tb.Width()>>1))
bottom = Math.MaxInt(bottom, ty)
}
return Box{

View File

@ -85,13 +85,13 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
if ya.AxisType == YAxisPrimary {
minx = canvasBox.Right
maxx = MaxInt(maxx, tx+tb.Width())
maxx = Math.MaxInt(maxx, tx+tb.Width())
} else if ya.AxisType == YAxisSecondary {
minx = MinInt(minx, finalTextX)
maxx = MaxInt(maxx, tx)
minx = Math.MinInt(minx, finalTextX)
maxx = Math.MaxInt(maxx, tx)
}
miny = MinInt(miny, ly-tb.Height()>>1)
maxy = MaxInt(maxy, ly+tb.Height()>>1)
miny = Math.MinInt(miny, ly-tb.Height()>>1)
maxy = Math.MaxInt(maxy, ly+tb.Height()>>1)
}
return Box{