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) lx := canvasBox.Left + xrange.Translate(a.XValue)
ly := canvasBox.Bottom - yrange.Translate(a.YValue) ly := canvasBox.Bottom - yrange.Translate(a.YValue)
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label) ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
box.Top = MinInt(box.Top, ab.Top) box.Top = Math.MinInt(box.Top, ab.Top)
box.Left = MinInt(box.Left, ab.Left) box.Left = Math.MinInt(box.Left, ab.Left)
box.Right = MaxInt(box.Right, ab.Right) box.Right = Math.MaxInt(box.Right, ab.Right)
box.Bottom = MaxInt(box.Bottom, ab.Bottom) box.Bottom = Math.MaxInt(box.Bottom, ab.Bottom)
} }
} }
return box return box

View file

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

20
box.go
View file

@ -66,12 +66,12 @@ func (b Box) GetBottom(defaults ...int) int {
// Width returns the width // Width returns the width
func (b Box) Width() int { func (b Box) Width() int {
return AbsInt(b.Right - b.Left) return Math.AbsInt(b.Right - b.Left)
} }
// Height returns the height // Height returns the height
func (b Box) Height() int { 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 // 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. // Grow grows a box based on another box.
func (b Box) Grow(other Box) Box { func (b Box) Grow(other Box) Box {
return Box{ return Box{
Top: MinInt(b.Top, other.Top), Top: Math.MinInt(b.Top, other.Top),
Left: MinInt(b.Left, other.Left), Left: Math.MinInt(b.Left, other.Left),
Right: MaxInt(b.Right, other.Right), Right: Math.MaxInt(b.Right, other.Right),
Bottom: MaxInt(b.Bottom, other.Bottom), 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 { func (b Box) Constrain(other Box) Box {
newBox := b.Clone() newBox := b.Clone()
newBox.Top = MaxInt(newBox.Top, other.Top) newBox.Top = Math.MaxInt(newBox.Top, other.Top)
newBox.Left = MaxInt(newBox.Left, other.Left) newBox.Left = Math.MaxInt(newBox.Left, other.Left)
newBox.Right = MinInt(newBox.Right, other.Right) newBox.Right = Math.MinInt(newBox.Right, other.Right)
newBox.Bottom = MinInt(newBox.Bottom, other.Bottom) newBox.Bottom = Math.MinInt(newBox.Bottom, other.Bottom)
return newBox return newBox
} }

View file

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

View file

@ -384,8 +384,8 @@ func TestChartRegressionBadRangesByUser(t *testing.T) {
}, },
Series: []Series{ Series: []Series{
ContinuousSeries{ ContinuousSeries{
XValues: Seq(1.0, 10.0), XValues: Sequence.Float64(1.0, 10.0),
YValues: Seq(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) assert := assert.New(t)
s1 := ContinuousSeries{ s1 := ContinuousSeries{
XValues: Seq(1.0, 10.0), XValues: Sequence.Float64(1.0, 10.0),
YValues: Seq(1.0, 10.0), YValues: Sequence.Float64(1.0, 10.0),
} }
s2 := ContinuousSeries{ s2 := ContinuousSeries{
XValues: Seq(11, 20.0), XValues: Sequence.Float64(11, 20.0),
YValues: Seq(10.0, 1.0), YValues: Sequence.Float64(10.0, 1.0),
} }
s3 := ContinuousSeries{ s3 := ContinuousSeries{
XValues: Seq(21, 30.0), XValues: Sequence.Float64(21, 30.0),
YValues: Seq(1.0, 10.0), YValues: Sequence.Float64(1.0, 10.0),
} }
cs := ConcatSeries([]Series{s1, s2, s3}) cs := ConcatSeries([]Series{s1, s2, s3})

View file

@ -10,7 +10,7 @@ func TestRangeTranslate(t *testing.T) {
assert := assert.New(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} 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 := ContinuousRange{Domain: 1000}
r.Min, r.Max = MinAndMax(values...) r.Min, r.Max = Math.MinAndMax(values...)
// delta = ~7.0 // delta = ~7.0
// value = ~5.0 // value = ~5.0

View file

@ -11,8 +11,8 @@ func TestContinuousSeries(t *testing.T) {
cs := ContinuousSeries{ cs := ContinuousSeries{
Name: "Test Series", Name: "Test Series",
XValues: Seq(1.0, 10.0), XValues: Sequence.Float64(1.0, 10.0),
YValues: Seq(1.0, 10.0), YValues: Sequence.Float64(1.0, 10.0),
} }
assert.Equal("Test Series", cs.GetName()) 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 is the default distance from the top of the chart to put the title.
DefaultTitleTop = 10 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 is the default distance from the right of the canvas to the y axis labels.
DefaultYAxisMargin = 10 DefaultYAxisMargin = 10
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels. // 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{} type draw struct{}
// LineSeries draws a line series with a renderer. // 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 { if vs.Len() == 0 {
return return
} }
@ -25,7 +25,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
var vx, vy float64 var vx, vy float64
var x, y int var x, y int
fill := s.GetFillColor() fill := style.GetFillColor()
if !fill.IsZero() { if !fill.IsZero() {
r.SetFillColor(fill) r.SetFillColor(fill)
r.MoveTo(x0, y0) r.MoveTo(x0, y0)
@ -41,9 +41,9 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
r.Fill() r.Fill()
} }
r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeColor(style.GetStrokeColor())
r.SetStrokeDashArray(s.GetStrokeDashArray()) r.SetStrokeDashArray(style.GetStrokeDashArray())
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth)) r.SetStrokeWidth(style.GetStrokeWidth())
r.MoveTo(x0, y0) r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ { 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. // 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 drawOffsetIndex := 0
if len(drawOffsetIndexes) > 0 { if len(drawOffsetIndexes) > 0 {
drawOffsetIndex = drawOffsetIndexes[0] drawOffsetIndex = drawOffsetIndexes[0]
} }
r.SetStrokeColor(s.GetStrokeColor()) style.WriteToRenderer(r)
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFillColor(s.GetFillColor())
cb := canvasBox.Bottom cb := canvasBox.Bottom
cl := canvasBox.Left 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. // 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 { if vs.Len() == 0 {
return return
} }
@ -137,30 +134,25 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
Left: x - (barWidth >> 1), Left: x - (barWidth >> 1),
Right: x + (barWidth >> 1), Right: x + (barWidth >> 1),
Bottom: cb - y, Bottom: cb - y,
}, s) }, style)
} }
} }
// MeasureAnnotation measures how big an annotation would be. // MeasureAnnotation measures how big an annotation would be.
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string) Box { func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
r.SetFillColor(s.GetFillColor(DefaultAnnotationFillColor)) style.WriteToRenderer(r)
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
textBox := r.MeasureText(label) textBox := r.MeasureText(label)
textWidth := textBox.Width() textWidth := textBox.Width()
textHeight := textBox.Height() textHeight := textBox.Height()
halfTextHeight := textHeight >> 1 halfTextHeight := textHeight >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top) pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left) pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := s.Padding.GetRight(DefaultAnnotationPadding.Right) pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := s.Padding.GetBottom(DefaultAnnotationPadding.Bottom) pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
strokeWidth := s.GetStrokeWidth() strokeWidth := style.GetStrokeWidth()
top := ly - (pt + halfTextHeight) top := ly - (pt + halfTextHeight)
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth) 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. // Annotation draws an anotation with a renderer.
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) { func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
r.SetFillColor(style.GetFillColor()) style.WriteToRenderer(r)
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))
textBox := r.MeasureText(label) textBox := r.MeasureText(label)
textWidth := textBox.Width() 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. // Box draws a box with a given style.
func (d draw) Box(r Renderer, b Box, s Style) { func (d draw) Box(r Renderer, b Box, s Style) {
r.SetFillColor(s.GetFillColor()) s.WriteToRenderer(r)
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.MoveTo(b.Left, b.Top) r.MoveTo(b.Left, b.Top)
r.LineTo(b.Right, 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. // DrawText draws text with a given style.
func (d draw) Text(r Renderer, text string, x, y int, s Style) { func (d draw) Text(r Renderer, text string, x, y int, style Style) {
r.SetFontColor(s.GetFontColor(DefaultTextColor)) style.GetTextOptions().WriteToRenderer(r)
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize())
r.Text(text, x, y) r.Text(text, x, y)
} }
// TextWithin draws the text within a given box. // 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 ( var (
emaXValues = Seq(1.0, 50.0) emaXValues = Sequence.Float64(1.0, 50.0)
emaYValues = []float64{ emaYValues = []float64{
1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5, 4, 3, 2,
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{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
XValues: chart.Seq(1.0, 100.0), XValues: chart.Sequence.Float64(1.0, 100.0),
YValues: chart.SeqRand(100.0, 256.0), YValues: chart.Sequence.Random(100.0, 256.0),
}, },
}, },
} }
@ -57,8 +57,8 @@ func drawChartDefault(res http.ResponseWriter, req *http.Request) {
}, },
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
XValues: chart.Seq(1.0, 100.0), XValues: chart.Sequence.Float64(1.0, 100.0),
YValues: chart.SeqRand(100.0, 256.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 //note we have to do this as a separate step because we need a reference to graph
graph.Elements = []chart.Renderable{ graph.Elements = []chart.Renderable{
chart.CreateLegend(&graph), chart.Legend(&graph),
} }
res.Header().Set("Content-Type", "image/png") res.Header().Set("Content-Type", "image/png")

View file

@ -15,8 +15,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
mainSeries := chart.ContinuousSeries{ mainSeries := chart.ContinuousSeries{
Name: "A test series", 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. 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.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 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. // 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{ mainSeries := chart.ContinuousSeries{
Name: "A test series", 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. 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.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 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. // note we create a SimpleMovingAverage series by assignin the inner series.

View file

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

View file

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

View file

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

View file

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

View file

@ -1,19 +1,23 @@
package chart package chart
import ( import (
"fmt"
"math" "math"
"math/rand"
"time" "time"
) )
// Float is an alias for float64 that provides a better .String() method. const (
type Float float64 _pi = math.Pi
_2pi = 2 * math.Pi
// String returns the string representation of a float. _3pi4 = (3 * math.Pi) / 4.0
func (f Float) String() string { _4pi3 = (4 * math.Pi) / 3.0
return fmt.Sprintf("%.2f", f) _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. // TimeToFloat64 returns a float64 representation of a time.
func TimeToFloat64(t time.Time) float64 { func TimeToFloat64(t time.Time) float64 {
@ -25,8 +29,15 @@ func Float64ToTime(tf float64) time.Time {
return time.Unix(0, int64(tf)) 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. // 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 { if len(values) == 0 {
return 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 // MinAndMaxOfTime returns the min and max of a given set of times
// in one pass. // 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 { if len(values) == 0 {
return return
} }
@ -64,19 +75,8 @@ func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
return 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. // 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) startingDeltaBound := math.Pow(10.0, 10.0)
for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 { for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 {
if delta > cursor { if delta > cursor {
@ -88,13 +88,13 @@ func GetRoundToForDelta(delta float64) float64 {
} }
// RoundUp rounds up to a given roundTo value. // 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) d1 := math.Ceil(value / roundTo)
return d1 * roundTo return d1 * roundTo
} }
// RoundDown rounds down to a given roundTo value. // 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) d1 := math.Floor(value / roundTo)
return d1 * 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. // 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 // 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. // 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 var total float64
for _, v := range values { for _, v := range values {
total += v total += v
} }
output := make([]float64, len(values)) output := make([]float64, len(values))
for x, v := range values { for x, v := range values {
output[x] = RoundDown(v/total, 0.0001) output[x] = m.RoundDown(v/total, 0.0001)
} }
return output return output
} }
// MinInt returns the minimum of a set of integers. // MinInt returns the minimum of a set of integers.
func MinInt(values ...int) int { func (m mathUtil) MinInt(values ...int) int {
min := math.MaxInt32 min := math.MaxInt32
for _, v := range values { for _, v := range values {
if v < min { if v < min {
@ -126,7 +126,7 @@ func MinInt(values ...int) int {
} }
// MaxInt returns the maximum of a set of integers. // MaxInt returns the maximum of a set of integers.
func MaxInt(values ...int) int { func (m mathUtil) MaxInt(values ...int) int {
max := math.MinInt32 max := math.MinInt32
for _, v := range values { for _, v := range values {
if v > max { if v > max {
@ -137,47 +137,15 @@ func MaxInt(values ...int) int {
} }
// AbsInt returns the absolute value of an integer. // AbsInt returns the absolute value of an integer.
func AbsInt(value int) int { func (m mathUtil) AbsInt(value int) int {
if value < 0 { if value < 0 {
return -value return -value
} }
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. // Sum sums a set of values.
func Sum(values ...float64) float64 { func (m mathUtil) Sum(values ...float64) float64 {
var total float64 var total float64
for _, v := range values { for _, v := range values {
total += v total += v
@ -186,7 +154,7 @@ func Sum(values ...float64) float64 {
} }
// SumInt sums a set of values. // SumInt sums a set of values.
func SumInt(values ...int) int { func (m mathUtil) SumInt(values ...int) int {
var total int var total int
for _, v := range values { for _, v := range values {
total += v total += v
@ -194,52 +162,32 @@ func SumInt(values ...int) int {
return total 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. // PercentDifference computes the percentage difference between two values.
// The formula is (v2-v1)/v1. // 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 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. // DegreesToRadians returns degrees as radians.
func DegreesToRadians(degrees float64) float64 { func (m mathUtil) DegreesToRadians(degrees float64) float64 {
return degrees * _d2r return degrees * _d2r
} }
// RadiansToDegrees translates a radian value to a degree value. // 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 return math.Mod(value, _2pi) * _r2d
} }
// PercentToRadians converts a normalized value (0,1) to radians. // PercentToRadians converts a normalized value (0,1) to radians.
func PercentToRadians(pct float64) float64 { func (m mathUtil) PercentToRadians(pct float64) float64 {
return DegreesToRadians(360.0 * pct) return m.DegreesToRadians(360.0 * pct)
} }
// RadianAdd adds a delta to a base in radians. // 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 value := base + delta
if value > _2pi { if value > _2pi {
return math.Mod(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. // 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 value := baseDegrees + deltaDegrees
if value > _2pi { if value > _2pi {
return math.Mod(value, 360.0) 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. // DegreesToCompass returns the degree value in compass / clock orientation.
func DegreesToCompass(deg float64) float64 { func (m mathUtil) DegreesToCompass(deg float64) float64 {
return DegreesAdd(deg, -90.0) return m.DegreesAdd(deg, -90.0)
} }
// CirclePoint returns the absolute position of a circle diameter point given // CirclePoint returns the absolute position of a circle diameter point given
// by the radius and the angle. // 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)) x = cx + int(radius*math.Sin(angleRadians))
y = cy - int(radius*math.Cos(angleRadians)) y = cy - int(radius*math.Cos(angleRadians))
return return

View file

@ -10,7 +10,7 @@ import (
func TestMinAndMax(t *testing.T) { func TestMinAndMax(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
values := []float64{1.0, 2.0, 3.0, 4.0} 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(1.0, min)
assert.Equal(4.0, max) assert.Equal(4.0, max)
} }
@ -18,7 +18,7 @@ func TestMinAndMax(t *testing.T) {
func TestMinAndMaxReversed(t *testing.T) { func TestMinAndMaxReversed(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
values := []float64{4.0, 2.0, 3.0, 1.0} 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(1.0, min)
assert.Equal(4.0, max) assert.Equal(4.0, max)
} }
@ -26,7 +26,7 @@ func TestMinAndMaxReversed(t *testing.T) {
func TestMinAndMaxEmpty(t *testing.T) { func TestMinAndMaxEmpty(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
values := []float64{} values := []float64{}
min, max := MinAndMax(values...) min, max := Math.MinAndMax(values...)
assert.Equal(0.0, min) assert.Equal(0.0, min)
assert.Equal(0.0, max) 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, -3),
time.Now().AddDate(0, 0, -4), time.Now().AddDate(0, 0, -4),
} }
min, max := MinAndMaxOfTime(values...) min, max := Math.MinAndMaxOfTime(values...)
assert.Equal(values[3], min) assert.Equal(values[3], min)
assert.Equal(values[0], max) 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, -3),
time.Now().AddDate(0, 0, -1), time.Now().AddDate(0, 0, -1),
} }
min, max := MinAndMaxOfTime(values...) min, max := Math.MinAndMaxOfTime(values...)
assert.Equal(values[0], min) assert.Equal(values[0], min)
assert.Equal(values[3], max) assert.Equal(values[3], max)
} }
@ -60,66 +60,45 @@ func TestMinAndMaxOfTimeReversed(t *testing.T) {
func TestMinAndMaxOfTimeEmpty(t *testing.T) { func TestMinAndMaxOfTimeEmpty(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
values := []time.Time{} values := []time.Time{}
min, max := MinAndMaxOfTime(values...) min, max := Math.MinAndMaxOfTime(values...)
assert.Equal(time.Time{}, min) assert.Equal(time.Time{}, min)
assert.Equal(time.Time{}, max) 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) { func TestGetRoundToForDelta(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
assert.Equal(100.0, GetRoundToForDelta(1001.00)) assert.Equal(100.0, Math.GetRoundToForDelta(1001.00))
assert.Equal(10.0, GetRoundToForDelta(101.00)) assert.Equal(10.0, Math.GetRoundToForDelta(101.00))
assert.Equal(1.0, GetRoundToForDelta(11.00)) assert.Equal(1.0, Math.GetRoundToForDelta(11.00))
} }
func TestRoundUp(t *testing.T) { func TestRoundUp(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
assert.Equal(0.5, RoundUp(0.49, 0.1)) assert.Equal(0.5, Math.RoundUp(0.49, 0.1))
assert.Equal(1.0, RoundUp(0.51, 1.0)) assert.Equal(1.0, Math.RoundUp(0.51, 1.0))
assert.Equal(0.4999, RoundUp(0.49988, 0.0001)) assert.Equal(0.4999, Math.RoundUp(0.49988, 0.0001))
} }
func TestRoundDown(t *testing.T) { func TestRoundDown(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
assert.Equal(0.5, RoundDown(0.51, 0.1)) assert.Equal(0.5, Math.RoundDown(0.51, 0.1))
assert.Equal(1.0, RoundDown(1.01, 1.0)) assert.Equal(1.0, Math.RoundDown(1.01, 1.0))
assert.Equal(0.5001, RoundDown(0.50011, 0.0001)) assert.Equal(0.5001, Math.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)
} }
func TestPercentDifference(t *testing.T) { func TestPercentDifference(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
assert.Equal(0.5, PercentDifference(1.0, 1.5)) assert.Equal(0.5, Math.PercentDifference(1.0, 1.5))
assert.Equal(-0.5, PercentDifference(2.0, 1.0)) assert.Equal(-0.5, Math.PercentDifference(2.0, 1.0))
} }
func TestNormalize(t *testing.T) { func TestNormalize(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
values := []float64{10, 9, 8, 7, 6} values := []float64{10, 9, 8, 7, 6}
normalized := Normalize(values...) normalized := Math.Normalize(values...)
assert.Len(normalized, 5) assert.Len(normalized, 5)
assert.Equal(0.25, normalized[0]) assert.Equal(0.25, normalized[0])
assert.Equal(0.1499, normalized[4]) assert.Equal(0.1499, normalized[4])
@ -153,7 +132,7 @@ func TestDegreesToRadians(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
for d, r := range _degreesToRadians { 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) assert := assert.New(t)
for d, r := range _degreesToRadians { 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) assert := assert.New(t)
for d, r := range _degreesToRadians { for d, r := range _degreesToRadians {
assert.Equal(d, RadiansToDegrees(r)) assert.Equal(d, Math.RadiansToDegrees(r))
} }
} }
func TestRadianAdd(t *testing.T) { func TestRadianAdd(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
assert.Equal(_pi, RadianAdd(_pi2, _pi2)) assert.Equal(_pi, Math.RadianAdd(_pi2, _pi2))
assert.Equal(_3pi2, RadianAdd(_pi2, _pi)) assert.Equal(_3pi2, Math.RadianAdd(_pi2, _pi))
assert.Equal(_pi, RadianAdd(_pi, _2pi)) assert.Equal(_pi, Math.RadianAdd(_pi, _2pi))
assert.Equal(_pi, 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) { func (pc PieChart) drawTitle(r Renderer) {
if len(pc.Title) > 0 && pc.TitleStyle.Show { if len(pc.Title) > 0 && pc.TitleStyle.Show {
r.SetFont(pc.TitleStyle.GetFont(pc.GetFont())) Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
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)
} }
} }
func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) { func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
cx, cy := canvasBox.Center() cx, cy := canvasBox.Center()
diameter := MinInt(canvasBox.Width(), canvasBox.Height()) diameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
radius := float64(diameter >> 1) radius := float64(diameter >> 1)
labelRadius := (radius * 2.0) / 3.0 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) v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
r.MoveTo(cx, cy) r.MoveTo(cx, cy)
rads = PercentToRadians(total) rads = Math.PercentToRadians(total)
delta = PercentToRadians(v.Value) delta = Math.PercentToRadians(v.Value)
r.ArcTo(cx, cy, radius, radius, rads, delta) 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 { for index, v := range values {
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r) v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
if len(v.Label) > 0 { if len(v.Label) > 0 {
delta2 = PercentToRadians(total + (v.Value / 2.0)) delta2 = Math.PercentToRadians(total + (v.Value / 2.0))
delta2 = RadianAdd(delta2, _pi2) delta2 = Math.RadianAdd(delta2, _pi2)
lx, ly = CirclePoint(cx, cy, labelRadius, delta2) lx, ly = Math.CirclePoint(cx, cy, labelRadius, delta2)
tb := r.MeasureText(v.Label) tb := r.MeasureText(v.Label)
lx = lx - (tb.Width() >> 1) lx = lx - (tb.Width() >> 1)
@ -180,7 +167,7 @@ func (pc PieChart) getDefaultCanvasBox() Box {
} }
func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box { func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height()) circleDiameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
square := Box{ square := Box{
Right: circleDiameter, Right: circleDiameter,
@ -226,7 +213,7 @@ func (pc PieChart) stylePieChartValue(index int) Style {
} }
func (pc PieChart) getScaledFontSize() float64 { func (pc PieChart) getScaledFontSize() float64 {
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight()) effectiveDimension := Math.MinInt(pc.GetWidth(), pc.GetHeight())
if effectiveDimension >= 2048 { if effectiveDimension >= 2048 {
return 48.0 return 48.0
} else if effectiveDimension >= 1024 { } 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. // Box returns the chart bounds as a box.
func (pc PieChart) Box() Box { func (pc PieChart) Box() Box {
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right) 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 { func (sma SMASeries) getAverage(index int) float64 {
period := sma.GetPeriod() period := sma.GetPeriod()
floor := MaxInt(0, index-period) floor := Math.MaxInt(0, index-period)
var accum float64 var accum float64
var count float64 var count float64
for x := index; x >= floor; x-- { for x := index; x >= floor; x-- {

View file

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

View file

@ -25,6 +25,7 @@ type Style struct {
TextHorizontalAlign textHorizontalAlign TextHorizontalAlign textHorizontalAlign
TextVerticalAlign textVerticalAlign TextVerticalAlign textVerticalAlign
TextWrap textWrap TextWrap textWrap
TextLineSpacing int
} }
// IsZero returns if the object is set or not. // IsZero returns if the object is set or not.
@ -222,6 +223,17 @@ func (s Style) GetTextWrap(defaults ...textWrap) textWrap {
return s.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. // WriteToRenderer passes the style's options to a renderer.
func (s Style) WriteToRenderer(r Renderer) { func (s Style) WriteToRenderer(r Renderer) {
r.SetStrokeColor(s.GetStrokeColor()) r.SetStrokeColor(s.GetStrokeColor())
@ -261,6 +273,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
final.TextHorizontalAlign = s.GetTextHorizontalAlign(defaults.TextHorizontalAlign) final.TextHorizontalAlign = s.GetTextHorizontalAlign(defaults.TextHorizontalAlign)
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign) final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
final.TextWrap = s.GetTextWrap(defaults.TextWrap) final.TextWrap = s.GetTextWrap(defaults.TextWrap)
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
return return
} }
@ -299,5 +312,6 @@ func (s Style) GetTextOptions() Style {
TextHorizontalAlign: s.TextHorizontalAlign, TextHorizontalAlign: s.TextHorizontalAlign,
TextVerticalAlign: s.TextVerticalAlign, TextVerticalAlign: s.TextVerticalAlign,
TextWrap: s.TextWrap, TextWrap: s.TextWrap,
TextLineSpacing: s.TextLineSpacing,
} }
} }

29
text.go
View file

@ -64,15 +64,12 @@ type TextStyle struct {
type text struct{} type text struct{}
func (t text) WrapFit(r Renderer, value string, width int, style Style, wrapOption textWrap) []string { func (t text) WrapFit(r Renderer, value string, width int, style Style) []string {
valueBox := r.MeasureText(value) switch style.TextWrap {
if valueBox.Width() > width { case TextWrapRune:
switch wrapOption { return t.WrapFitRune(r, value, width, style)
case TextWrapRune: case TextWrapWord:
return t.WrapFitRune(r, value, width, style) return t.WrapFitWord(r, value, width, style)
case TextWrapWord:
return t.WrapFitWord(r, value, width, style)
}
} }
return []string{value} return []string{value}
} }
@ -143,6 +140,20 @@ func (t text) Trim(value string) string {
return strings.Trim(value, " \t\n\r") 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 { func (t text) appendLast(lines []string, text string) []string {
if len(lines) == 0 { if len(lines) == 0 {
return []string{text} return []string{text}

View file

@ -25,8 +25,36 @@ func TestTextWrapWord(t *testing.T) {
lineBox := r.MeasureText(line) lineBox := r.MeasureText(line)
assert.True(lineBox.Width() < 100, line+": "+lineBox.String()) 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) output = Text.WrapFitWord(r, "foo", 100, basicTextStyle)
assert.Len(output, 1) assert.Len(output, 1)
assert.Equal("foo", output[0]) 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. // ValuesNormalized returns normalized values.
func (vs Values) ValuesNormalized() []float64 { func (vs Values) ValuesNormalized() []float64 {
return Normalize(vs.Values()...) return Math.Normalize(vs.Values()...)
} }
// Normalize returns the values normalized. // Normalize returns the values normalized.
func (vs Values) Normalize() []Value { func (vs Values) Normalize() []Value {
output := make([]Value, len(vs)) output := make([]Value, len(vs))
total := Sum(vs.Values()...) total := Math.Sum(vs.Values()...)
for index, v := range vs { for index, v := range vs {
output[index] = Value{ output[index] = Value{
Style: v.Style, Style: v.Style,
Label: v.Label, Label: v.Label,
Value: RoundDown(v.Value/total, 0.0001), Value: Math.RoundDown(v.Value/total, 0.0001),
} }
} }
return output 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) { func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
startAngle = RadianAdd(startAngle, _pi2) startAngle = Math.RadianAdd(startAngle, _pi2)
endAngle := RadianAdd(startAngle, delta) endAngle := Math.RadianAdd(startAngle, delta)
startx := cx + int(rx*math.Sin(startAngle)) startx := cx + int(rx*math.Sin(startAngle))
starty := cy - int(ry*math.Cos(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)) endx := cx + int(rx*math.Sin(endAngle))
endy := cy - int(ry*math.Cos(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)) 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 tx := canvasBox.Left + lx
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height() ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
top = MinInt(top, canvasBox.Bottom) top = Math.MinInt(top, canvasBox.Bottom)
left = MinInt(left, tx-(tb.Width()>>1)) left = Math.MinInt(left, tx-(tb.Width()>>1))
right = MaxInt(right, tx+(tb.Width()>>1)) right = Math.MaxInt(right, tx+(tb.Width()>>1))
bottom = MaxInt(bottom, ty) bottom = Math.MaxInt(bottom, ty)
} }
return Box{ 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 { if ya.AxisType == YAxisPrimary {
minx = canvasBox.Right minx = canvasBox.Right
maxx = MaxInt(maxx, tx+tb.Width()) maxx = Math.MaxInt(maxx, tx+tb.Width())
} else if ya.AxisType == YAxisSecondary { } else if ya.AxisType == YAxisSecondary {
minx = MinInt(minx, finalTextX) minx = Math.MinInt(minx, finalTextX)
maxx = MaxInt(maxx, tx) maxx = Math.MaxInt(maxx, tx)
} }
miny = MinInt(miny, ly-tb.Height()>>1) miny = Math.MinInt(miny, ly-tb.Height()>>1)
maxy = MaxInt(maxy, ly+tb.Height()>>1) maxy = Math.MaxInt(maxy, ly+tb.Height()>>1)
} }
return Box{ return Box{