From cbc0002d2acc8fb4028e7785d5c8045a95111628 Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Fri, 29 Jul 2016 18:24:25 -0700 Subject: [PATCH] big api overhauls. --- annotation_series.go | 8 +- bollinger_band_series_test.go | 8 +- box.go | 20 ++-- chart.go | 8 +- chart_test.go | 4 +- concat_series_test.go | 12 +-- continuous_range_test.go | 2 +- continuous_series_test.go | 4 +- defaults.go | 3 + draw.go | 94 ++++++++-------- ema_series_test.go | 2 +- examples/custom_padding/main.go | 8 +- examples/legend/main.go | 2 +- examples/linear_regression/main.go | 4 +- examples/simple_moving_average/main.go | 4 +- histogram_series_test.go | 4 +- legend.go | 2 +- linear_regression_series.go | 6 +- linear_regression_series_test.go | 12 +-- util.go => math.go | 142 ++++++++----------------- util_test.go => math_test.go | 71 +++++-------- pie_chart.go | 42 ++++---- sequence.go | 55 ++++++++++ sequence_test.go | 17 +++ sma_series.go | 2 +- sma_series_test.go | 16 +-- style.go | 14 +++ text.go | 29 +++-- text_test.go | 28 +++++ value.go | 6 +- vector_renderer.go | 6 +- xaxis.go | 8 +- yaxis.go | 10 +- 33 files changed, 356 insertions(+), 297 deletions(-) rename util.go => math.go (59%) rename util_test.go => math_test.go (64%) create mode 100644 sequence.go create mode 100644 sequence_test.go diff --git a/annotation_series.go b/annotation_series.go index f622b8a..9b383c9 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -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 diff --git a/bollinger_band_series_test.go b/bollinger_band_series_test.go index f1a6693..28d5564 100644 --- a/bollinger_band_series_test.go +++ b/bollinger_band_series_test.go @@ -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{ diff --git a/box.go b/box.go index 5df34e0..0705749 100644 --- a/box.go +++ b/box.go @@ -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 } diff --git a/chart.go b/chart.go index 0a77b07..67c1061 100644 --- a/chart.go +++ b/chart.go @@ -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) } diff --git a/chart_test.go b/chart_test.go index eca6f98..e313fdf 100644 --- a/chart_test.go +++ b/chart_test.go @@ -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), }, }, } diff --git a/concat_series_test.go b/concat_series_test.go index f9f93cd..f72eb23 100644 --- a/concat_series_test.go +++ b/concat_series_test.go @@ -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}) diff --git a/continuous_range_test.go b/continuous_range_test.go index 4400366..114ecbe 100644 --- a/continuous_range_test.go +++ b/continuous_range_test.go @@ -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 diff --git a/continuous_series_test.go b/continuous_series_test.go index df2e3b8..171db37 100644 --- a/continuous_series_test.go +++ b/continuous_series_test.go @@ -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()) diff --git a/defaults.go b/defaults.go index a1fb01c..e1cdb69 100644 --- a/defaults.go +++ b/defaults.go @@ -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. diff --git a/draw.go b/draw.go index c669791..de61c1d 100644 --- a/draw.go +++ b/draw.go @@ -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() + } } diff --git a/ema_series_test.go b/ema_series_test.go index 42025da..ad74d72 100644 --- a/ema_series_test.go +++ b/ema_series_test.go @@ -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, diff --git a/examples/custom_padding/main.go b/examples/custom_padding/main.go index 8fff64e..199a8e7 100644 --- a/examples/custom_padding/main.go +++ b/examples/custom_padding/main.go @@ -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), }, }, } diff --git a/examples/legend/main.go b/examples/legend/main.go index edad885..41cff72 100644 --- a/examples/legend/main.go +++ b/examples/legend/main.go @@ -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") diff --git a/examples/linear_regression/main.go b/examples/linear_regression/main.go index 402a91a..c397ca9 100644 --- a/examples/linear_regression/main.go +++ b/examples/linear_regression/main.go @@ -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. diff --git a/examples/simple_moving_average/main.go b/examples/simple_moving_average/main.go index e5da665..216599c 100644 --- a/examples/simple_moving_average/main.go +++ b/examples/simple_moving_average/main.go @@ -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. diff --git a/histogram_series_test.go b/histogram_series_test.go index 80a2fec..3e51833 100644 --- a/histogram_series_test.go +++ b/histogram_series_test.go @@ -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{ diff --git a/legend.go b/legend.go index 82b5286..9425c9a 100644 --- a/legend.go +++ b/legend.go @@ -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++ } } diff --git a/linear_regression_series.go b/linear_regression_series.go index 9c9756f..a33a0b1 100644 --- a/linear_regression_series.go +++ b/linear_regression_series.go @@ -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 diff --git a/linear_regression_series_test.go b/linear_regression_series_test.go index 9ff890e..4a72669 100644 --- a/linear_regression_series_test.go +++ b/linear_regression_series_test.go @@ -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{ diff --git a/util.go b/math.go similarity index 59% rename from util.go rename to math.go index af3467f..77180e8 100644 --- a/util.go +++ b/math.go @@ -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 diff --git a/util_test.go b/math_test.go similarity index 64% rename from util_test.go rename to math_test.go index 8bf6b15..a4c4006 100644 --- a/util_test.go +++ b/math_test.go @@ -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)) } diff --git a/pie_chart.go b/pie_chart.go index 4f3ce4a..d67bac6 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -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) diff --git a/sequence.go b/sequence.go new file mode 100644 index 0000000..c04ec4d --- /dev/null +++ b/sequence.go @@ -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 +} diff --git a/sequence_test.go b/sequence_test.go new file mode 100644 index 0000000..91e4965 --- /dev/null +++ b/sequence_test.go @@ -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) +} diff --git a/sma_series.go b/sma_series.go index 9538d3b..a7e0034 100644 --- a/sma_series.go +++ b/sma_series.go @@ -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-- { diff --git a/sma_series_test.go b/sma_series_test.go index e2f5e4f..7a715cf 100644 --- a/sma_series_test.go +++ b/sma_series_test.go @@ -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()) diff --git a/style.go b/style.go index 1f506d5..78fca60 100644 --- a/style.go +++ b/style.go @@ -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, } } diff --git a/text.go b/text.go index 7011095..6e45c1c 100644 --- a/text.go +++ b/text.go @@ -64,15 +64,12 @@ 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 { - case TextWrapRune: - return t.WrapFitRune(r, value, width, style) - case TextWrapWord: - return t.WrapFitWord(r, value, width, style) - } +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} diff --git a/text_test.go b/text_test.go index b1f577b..78c0e9b 100644 --- a/text_test.go +++ b/text_test.go @@ -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]) } diff --git a/value.go b/value.go index be436b5..8a1da0d 100644 --- a/value.go +++ b/value.go @@ -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 diff --git a/vector_renderer.go b/vector_renderer.go index 3f3f359..7e50dc8 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -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)) } diff --git a/xaxis.go b/xaxis.go index 5a1ddd4..b229008 100644 --- a/xaxis.go +++ b/xaxis.go @@ -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{ diff --git a/yaxis.go b/yaxis.go index 7c1e26f..b9c16d3 100644 --- a/yaxis.go +++ b/yaxis.go @@ -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{