From 65a0895143e986530ba0b36b8e5983f5f7e7ed8c Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Fri, 28 Apr 2017 16:07:36 -0700 Subject: [PATCH] api cleaup --- _examples/linear_regression/main.go | 2 +- _examples/poly_regression/main.go | 2 +- _examples/simple_moving_average/main.go | 2 +- bollinger_band_series.go | 70 ++---- bollinger_band_series_test.go | 19 +- chart.go | 8 +- chart_test.go | 18 +- concat_series.go | 6 +- concat_series_test.go | 12 +- continuous_series.go | 8 +- continuous_series_test.go | 18 +- draw.go | 22 +- ema_series.go | 16 +- ema_series_test.go | 8 +- generate.go | 190 +++++++++++++++ sequence_test.go => generate_test.go | 12 +- histogram_series.go | 16 +- histogram_series_test.go | 8 +- last_value_annotation_series.go | 8 +- linear_regression_series.go | 24 +- linear_regression_series_test.go | 24 +- macd_series.go | 34 +-- macd_series_test.go | 4 +- market_hours_range.go | 10 +- math_util.go => math.go | 0 math_util_test.go => math_test.go | 0 min_max_series.go | 20 +- polynomial_regression_series.go | 16 +- polynomial_regression_test.go | 2 +- ring_buffer.go | 252 -------------------- sequence.go | 232 +++++------------- sma_series.go | 16 +- sma_series_test.go | 34 +-- stacked_bar_chart.go | 2 +- time_series.go | 8 +- time_series_test.go | 2 +- value_buffer.go | 229 ++++++++++++++++++ ring_buffer_test.go => value_buffer_test.go | 52 ++-- value_provider.go | 40 ++-- 39 files changed, 747 insertions(+), 699 deletions(-) create mode 100644 generate.go rename sequence_test.go => generate_test.go (87%) rename math_util.go => math.go (100%) rename math_util_test.go => math_test.go (100%) delete mode 100644 ring_buffer.go create mode 100644 value_buffer.go rename ring_buffer_test.go => value_buffer_test.go (75%) diff --git a/_examples/linear_regression/main.go b/_examples/linear_regression/main.go index c397ca9..966b04a 100644 --- a/_examples/linear_regression/main.go +++ b/_examples/linear_regression/main.go @@ -10,7 +10,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { /* In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument. - InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted. + InnerSeries only needs to implement `ValuesProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted. */ mainSeries := chart.ContinuousSeries{ diff --git a/_examples/poly_regression/main.go b/_examples/poly_regression/main.go index 4ed25bf..e6250ac 100644 --- a/_examples/poly_regression/main.go +++ b/_examples/poly_regression/main.go @@ -10,7 +10,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { /* In this example we add a new type of series, a `PolynomialRegressionSeries` that takes another series as a required argument. - InnerSeries only needs to implement `ValueProvider`, so really you could chain `PolynomialRegressionSeries` together if you wanted. + InnerSeries only needs to implement `ValuesProvider`, so really you could chain `PolynomialRegressionSeries` together if you wanted. */ mainSeries := chart.ContinuousSeries{ diff --git a/_examples/simple_moving_average/main.go b/_examples/simple_moving_average/main.go index 216599c..0334b46 100644 --- a/_examples/simple_moving_average/main.go +++ b/_examples/simple_moving_average/main.go @@ -10,7 +10,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { /* In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument. - InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted. + InnerSeries only needs to implement `ValuesProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted. */ mainSeries := chart.ContinuousSeries{ diff --git a/bollinger_band_series.go b/bollinger_band_series.go index 902b541..185f8b4 100644 --- a/bollinger_band_series.go +++ b/bollinger_band_series.go @@ -1,9 +1,6 @@ package chart -import ( - "fmt" - "math" -) +import "fmt" // BollingerBandsSeries draws bollinger bands for an inner series. // Bollinger bands are defined by two lines, one at SMA+k*stddev, one at SMA-k*stdev. @@ -14,9 +11,9 @@ type BollingerBandsSeries struct { Period int K float64 - InnerSeries ValueProvider + InnerSeries ValuesProvider - valueBuffer *RingBuffer + valueBuffer *ValueBuffer } // GetName returns the name of the time series. @@ -42,7 +39,9 @@ func (bbs BollingerBandsSeries) GetPeriod() int { return bbs.Period } -// GetK returns the K value. +// GetK returns the K value, or the number of standard deviations above and below +// to band the simple moving average with. +// Typical K value is 2.0. func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 { if bbs.K == 0 { if len(defaults) > 0 { @@ -54,35 +53,35 @@ func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 { } // Len returns the number of elements in the series. -func (bbs *BollingerBandsSeries) Len() int { +func (bbs BollingerBandsSeries) Len() int { return bbs.InnerSeries.Len() } -// GetBoundedValue gets the bounded value for the series. -func (bbs *BollingerBandsSeries) GetBoundedValue(index int) (x, y1, y2 float64) { +// GetBoundedValues gets the bounded value for the series. +func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) { if bbs.InnerSeries == nil { return } if bbs.valueBuffer == nil || index == 0 { - bbs.valueBuffer = NewRingBufferWithCapacity(bbs.GetPeriod()) + bbs.valueBuffer = NewValueBufferWithCapacity(bbs.GetPeriod()) } if bbs.valueBuffer.Len() >= bbs.GetPeriod() { bbs.valueBuffer.Dequeue() } - px, py := bbs.InnerSeries.GetValue(index) + px, py := bbs.InnerSeries.GetValues(index) bbs.valueBuffer.Enqueue(py) x = px - ay := bbs.getAverage(bbs.valueBuffer) - std := bbs.getStdDev(bbs.valueBuffer) + ay := Sequence{bbs.valueBuffer}.Average() + std := Sequence{bbs.valueBuffer}.StdDev() y1 = ay + (bbs.GetK() * std) y2 = ay - (bbs.GetK() * std) return } -// GetBoundedLastValue returns the last bounded value for the series. -func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) { +// GetBoundedLastValues returns the last bounded value for the series. +func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) { if bbs.InnerSeries == nil { return } @@ -93,15 +92,15 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) { startAt = 0 } - vb := NewRingBufferWithCapacity(period) + vb := NewValueBufferWithCapacity(period) for index := startAt; index < seriesLength; index++ { - xn, yn := bbs.InnerSeries.GetValue(index) + xn, yn := bbs.InnerSeries.GetValues(index) vb.Enqueue(yn) x = xn } - ay := bbs.getAverage(vb) - std := bbs.getStdDev(vb) + ay := Sequence{vb}.Average() + std := Sequence{vb}.StdDev() y1 = ay + (bbs.GetK() * std) y2 = ay - (bbs.GetK() * std) @@ -120,37 +119,6 @@ func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrang Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod()) } -func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 { - var accum float64 - valueBuffer.Each(func(v interface{}) { - if typed, isTyped := v.(float64); isTyped { - accum += typed - } - }) - return accum / float64(valueBuffer.Len()) -} - -func (bbs BollingerBandsSeries) getVariance(valueBuffer *RingBuffer) float64 { - if valueBuffer.Len() == 0 { - return 0 - } - - var variance float64 - m := bbs.getAverage(valueBuffer) - - valueBuffer.Each(func(v interface{}) { - if n, isTyped := v.(float64); isTyped { - variance += (float64(n) - m) * (float64(n) - m) - } - }) - - return variance / float64(valueBuffer.Len()) -} - -func (bbs BollingerBandsSeries) getStdDev(valueBuffer *RingBuffer) float64 { - return math.Pow(bbs.getVariance(valueBuffer), 0.5) -} - // Validate validates the series. func (bbs BollingerBandsSeries) Validate() error { if bbs.InnerSeries == nil { diff --git a/bollinger_band_series_test.go b/bollinger_band_series_test.go index 28d5564..fbd2c35 100644 --- a/bollinger_band_series_test.go +++ b/bollinger_band_series_test.go @@ -1,6 +1,7 @@ package chart import ( + "fmt" "math" "testing" @@ -10,9 +11,9 @@ import ( func TestBollingerBandSeries(t *testing.T) { assert := assert.New(t) - s1 := mockValueProvider{ - X: Sequence.Float64(1.0, 100.0), - Y: Sequence.Random(100, 1024), + s1 := mockValuesProvider{ + X: Generate.Float64(1.0, 100.0), + Y: Generate.Random(100, 1024), } bbs := &BollingerBandsSeries{ @@ -24,27 +25,27 @@ func TestBollingerBandSeries(t *testing.T) { y2values := make([]float64, 100) for x := 0; x < 100; x++ { - xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValue(x) + xvalues[x], y1values[x], y2values[x] = bbs.GetBoundedValues(x) } for x := bbs.GetPeriod(); x < 100; x++ { - assert.True(y1values[x] > y2values[x]) + assert.True(y1values[x] > y2values[x], fmt.Sprintf("%v vs. %v", y1values[x], y2values[x])) } } func TestBollingerBandLastValue(t *testing.T) { assert := assert.New(t) - s1 := mockValueProvider{ - X: Sequence.Float64(1.0, 100.0), - Y: Sequence.Float64(1.0, 100.0), + s1 := mockValuesProvider{ + X: Generate.Float64(1.0, 100.0), + Y: Generate.Float64(1.0, 100.0), } bbs := &BollingerBandsSeries{ InnerSeries: s1, } - x, y1, y2 := bbs.GetBoundedLastValue() + x, y1, y2 := bbs.GetBoundedLastValues() assert.Equal(100.0, x) assert.Equal(101, math.Floor(y1)) assert.Equal(83, math.Floor(y2)) diff --git a/chart.go b/chart.go index 70e5a57..15a9e14 100644 --- a/chart.go +++ b/chart.go @@ -177,10 +177,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { for _, s := range c.Series { if s.GetStyle().IsZero() || s.GetStyle().Show { seriesAxis := s.GetYAxis() - if bvp, isBoundedValueProvider := s.(BoundedValueProvider); isBoundedValueProvider { + if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider { seriesLength := bvp.Len() for index := 0; index < seriesLength; index++ { - vx, vy1, vy2 := bvp.GetBoundedValue(index) + vx, vy1, vy2 := bvp.GetBoundedValues(index) minx = math.Min(minx, vx) maxx = math.Max(maxx, vx) @@ -198,10 +198,10 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { seriesMappedToSecondaryAxis = true } } - } else if vp, isValueProvider := s.(ValueProvider); isValueProvider { + } else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider { seriesLength := vp.Len() for index := 0; index < seriesLength; index++ { - vx, vy := vp.GetValue(index) + vx, vy := vp.GetValues(index) minx = math.Min(minx, vx) maxx = math.Max(maxx, vx) diff --git a/chart_test.go b/chart_test.go index 4dead70..f29387f 100644 --- a/chart_test.go +++ b/chart_test.go @@ -391,8 +391,8 @@ func TestChartRegressionBadRangesByUser(t *testing.T) { }, Series: []Series{ ContinuousSeries{ - XValues: Sequence.Float64(1.0, 10.0), - YValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(1.0, 10.0), + YValues: Generate.Float64(1.0, 10.0), }, }, } @@ -407,8 +407,8 @@ func TestChartValidatesSeries(t *testing.T) { c := Chart{ Series: []Series{ ContinuousSeries{ - XValues: Sequence.Float64(1.0, 10.0), - YValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(1.0, 10.0), + YValues: Generate.Float64(1.0, 10.0), }, }, } @@ -418,7 +418,7 @@ func TestChartValidatesSeries(t *testing.T) { c = Chart{ Series: []Series{ ContinuousSeries{ - XValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(1.0, 10.0), }, }, } @@ -504,8 +504,8 @@ func TestChartE2ELine(t *testing.T) { }, Series: []Series{ ContinuousSeries{ - XValues: Sequence.Float64(0, 4, 1), - YValues: Sequence.Float64(0, 4, 1), + XValues: Generate.Float64(0, 4, 1), + YValues: Generate.Float64(0, 4, 1), }, }, } @@ -549,8 +549,8 @@ func TestChartE2ELineWithFill(t *testing.T) { StrokeColor: drawing.ColorBlue, FillColor: drawing.ColorRed, }, - XValues: Sequence.Float64(0, 4, 1), - YValues: Sequence.Float64(0, 4, 1), + XValues: Generate.Float64(0, 4, 1), + YValues: Generate.Float64(0, 4, 1), }, }, } diff --git a/concat_series.go b/concat_series.go index 46d8dda..edec7e5 100644 --- a/concat_series.go +++ b/concat_series.go @@ -7,7 +7,7 @@ type ConcatSeries []Series func (cs ConcatSeries) Len() int { total := 0 for _, s := range cs { - if typed, isValueProvider := s.(ValueProvider); isValueProvider { + if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider { total += typed.Len() } } @@ -19,10 +19,10 @@ func (cs ConcatSeries) Len() int { func (cs ConcatSeries) GetValue(index int) (x, y float64) { cursor := 0 for _, s := range cs { - if typed, isValueProvider := s.(ValueProvider); isValueProvider { + if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider { len := typed.Len() if index < cursor+len { - x, y = typed.GetValue(index - cursor) //FENCEPOSTS. + x, y = typed.GetValues(index - cursor) //FENCEPOSTS. return } cursor += typed.Len() diff --git a/concat_series_test.go b/concat_series_test.go index f72eb23..e0d763a 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: Sequence.Float64(1.0, 10.0), - YValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(1.0, 10.0), + YValues: Generate.Float64(1.0, 10.0), } s2 := ContinuousSeries{ - XValues: Sequence.Float64(11, 20.0), - YValues: Sequence.Float64(10.0, 1.0), + XValues: Generate.Float64(11, 20.0), + YValues: Generate.Float64(10.0, 1.0), } s3 := ContinuousSeries{ - XValues: Sequence.Float64(21, 30.0), - YValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(21, 30.0), + YValues: Generate.Float64(1.0, 10.0), } cs := ConcatSeries([]Series{s1, s2, s3}) diff --git a/continuous_series.go b/continuous_series.go index d6252f3..bca80de 100644 --- a/continuous_series.go +++ b/continuous_series.go @@ -31,13 +31,13 @@ func (cs ContinuousSeries) Len() int { return len(cs.XValues) } -// GetValue gets a value at a given index. -func (cs ContinuousSeries) GetValue(index int) (float64, float64) { +// GetValues gets the x,y values at a given index. +func (cs ContinuousSeries) GetValues(index int) (float64, float64) { return cs.XValues[index], cs.YValues[index] } -// GetLastValue gets the last value. -func (cs ContinuousSeries) GetLastValue() (float64, float64) { +// GetLastValues gets the last x,y values. +func (cs ContinuousSeries) GetLastValues() (float64, float64) { return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1] } diff --git a/continuous_series_test.go b/continuous_series_test.go index c5d950c..dca5afc 100644 --- a/continuous_series_test.go +++ b/continuous_series_test.go @@ -12,21 +12,21 @@ func TestContinuousSeries(t *testing.T) { cs := ContinuousSeries{ Name: "Test Series", - XValues: Sequence.Float64(1.0, 10.0), - YValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(1.0, 10.0), + YValues: Generate.Float64(1.0, 10.0), } assert.Equal("Test Series", cs.GetName()) assert.Equal(10, cs.Len()) - x0, y0 := cs.GetValue(0) + x0, y0 := cs.GetValues(0) assert.Equal(1.0, x0) assert.Equal(1.0, y0) - xn, yn := cs.GetValue(9) + xn, yn := cs.GetValues(9) assert.Equal(10.0, xn) assert.Equal(10.0, yn) - xn, yn = cs.GetLastValue() + xn, yn = cs.GetLastValues() assert.Equal(10.0, xn) assert.Equal(10.0, yn) } @@ -53,20 +53,20 @@ func TestContinuousSeriesValidate(t *testing.T) { cs := ContinuousSeries{ Name: "Test Series", - XValues: Sequence.Float64(1.0, 10.0), - YValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(1.0, 10.0), + YValues: Generate.Float64(1.0, 10.0), } assert.Nil(cs.Validate()) cs = ContinuousSeries{ Name: "Test Series", - XValues: Sequence.Float64(1.0, 10.0), + XValues: Generate.Float64(1.0, 10.0), } assert.NotNil(cs.Validate()) cs = ContinuousSeries{ Name: "Test Series", - YValues: Sequence.Float64(1.0, 10.0), + YValues: Generate.Float64(1.0, 10.0), } assert.NotNil(cs.Validate()) } diff --git a/draw.go b/draw.go index 3185086..5335930 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, style Style, vs ValueProvider) { +func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) { if vs.Len() == 0 { return } @@ -18,7 +18,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style cb := canvasBox.Bottom cl := canvasBox.Left - v0x, v0y := vs.GetValue(0) + v0x, v0y := vs.GetValues(0) x0 := cl + xrange.Translate(v0x) y0 := cb - yrange.Translate(v0y) @@ -31,7 +31,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style style.GetFillOptions().WriteDrawingOptionsToRenderer(r) r.MoveTo(x0, y0) for i := 1; i < vs.Len(); i++ { - vx, vy = vs.GetValue(i) + vx, vy = vs.GetValues(i) x = cl + xrange.Translate(vx) y = cb - yrange.Translate(vy) r.LineTo(x, y) @@ -47,7 +47,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style r.MoveTo(x0, y0) for i := 1; i < vs.Len(); i++ { - vx, vy = vs.GetValue(i) + vx, vy = vs.GetValues(i) x = cl + xrange.Translate(vx) y = cb - yrange.Translate(vy) r.LineTo(x, y) @@ -60,7 +60,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style style.GetDotOptions().WriteDrawingOptionsToRenderer(r) for i := 0; i < vs.Len(); i++ { - vx, vy = vs.GetValue(i) + vx, vy = vs.GetValues(i) x = cl + xrange.Translate(vx) y = cb - yrange.Translate(vy) @@ -82,8 +82,8 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style } } -// BoundedSeries draws a series that implements BoundedValueProvider. -func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) { +// BoundedSeries draws a series that implements BoundedValuesProvider. +func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValuesProvider, drawOffsetIndexes ...int) { drawOffsetIndex := 0 if len(drawOffsetIndexes) > 0 { drawOffsetIndex = drawOffsetIndexes[0] @@ -92,7 +92,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty cb := canvasBox.Bottom cl := canvasBox.Left - v0x, v0y1, v0y2 := bbs.GetBoundedValue(0) + v0x, v0y1, v0y2 := bbs.GetBoundedValues(0) x0 := cl + xrange.Translate(v0x) y0 := cb - yrange.Translate(v0y1) @@ -107,7 +107,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty style.GetFillAndStrokeOptions().WriteToRenderer(r) r.MoveTo(x0, y0) for i := 1; i < bbs.Len(); i++ { - vx, vy1, vy2 = bbs.GetBoundedValue(i) + vx, vy1, vy2 = bbs.GetBoundedValues(i) xvalues[i] = vx y2values[i] = vy2 @@ -133,7 +133,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, sty } // HistogramSeries draws a value provider as boxes from 0. -func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider, barWidths ...int) { +func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) { if vs.Len() == 0 { return } @@ -150,7 +150,7 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s //foreach datapoint, draw a box. for index := 0; index < seriesLength; index++ { - vx, vy := vs.GetValue(index) + vx, vy := vs.GetValues(index) y0 := yrange.Translate(0) x := cl + xrange.Translate(vx) y := yrange.Translate(vy) diff --git a/ema_series.go b/ema_series.go index 44406aa..ceaec39 100644 --- a/ema_series.go +++ b/ema_series.go @@ -14,7 +14,7 @@ type EMASeries struct { YAxis YAxisType Period int - InnerSeries ValueProvider + InnerSeries ValuesProvider cache []float64 } @@ -52,23 +52,23 @@ func (ema EMASeries) GetSigma() float64 { return 2.0 / (float64(ema.GetPeriod()) + 1) } -// GetValue gets a value at a given index. -func (ema *EMASeries) GetValue(index int) (x, y float64) { +// GetValues gets a value at a given index. +func (ema *EMASeries) GetValues(index int) (x, y float64) { if ema.InnerSeries == nil { return } if len(ema.cache) == 0 { ema.ensureCachedValues() } - vx, _ := ema.InnerSeries.GetValue(index) + vx, _ := ema.InnerSeries.GetValues(index) x = vx y = ema.cache[index] return } -// GetLastValue computes the last moving average value but walking back window size samples, +// GetLastValues computes the last moving average value but walking back window size samples, // and recomputing the last moving average chunk. -func (ema *EMASeries) GetLastValue() (x, y float64) { +func (ema *EMASeries) GetLastValues() (x, y float64) { if ema.InnerSeries == nil { return } @@ -76,7 +76,7 @@ func (ema *EMASeries) GetLastValue() (x, y float64) { ema.ensureCachedValues() } lastIndex := ema.InnerSeries.Len() - 1 - x, _ = ema.InnerSeries.GetValue(lastIndex) + x, _ = ema.InnerSeries.GetValues(lastIndex) y = ema.cache[lastIndex] return } @@ -86,7 +86,7 @@ func (ema *EMASeries) ensureCachedValues() { ema.cache = make([]float64, seriesLength) sigma := ema.GetSigma() for x := 0; x < seriesLength; x++ { - _, y := ema.InnerSeries.GetValue(x) + _, y := ema.InnerSeries.GetValues(x) if x == 0 { ema.cache[x] = y continue diff --git a/ema_series_test.go b/ema_series_test.go index ad74d72..b0050b4 100644 --- a/ema_series_test.go +++ b/ema_series_test.go @@ -7,7 +7,7 @@ import ( ) var ( - emaXValues = Sequence.Float64(1.0, 50.0) + emaXValues = Generate.Float64(1.0, 50.0) emaYValues = []float64{ 1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5, 4, 3, 2, @@ -75,7 +75,7 @@ var ( func TestEMASeries(t *testing.T) { assert := assert.New(t) - mockSeries := mockValueProvider{ + mockSeries := mockValuesProvider{ emaXValues, emaYValues, } @@ -91,7 +91,7 @@ func TestEMASeries(t *testing.T) { var yvalues []float64 for x := 0; x < ema.Len(); x++ { - _, y := ema.GetValue(x) + _, y := ema.GetValues(x) yvalues = append(yvalues, y) } @@ -99,7 +99,7 @@ func TestEMASeries(t *testing.T) { assert.InDelta(yv, emaExpected[index], emaDelta) } - lvx, lvy := ema.GetLastValue() + lvx, lvy := ema.GetLastValues() assert.Equal(50.0, lvx) assert.InDelta(lvy, emaExpected[49], emaDelta) } diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..887fb9d --- /dev/null +++ b/generate.go @@ -0,0 +1,190 @@ +package chart + +import ( + "math/rand" + "time" +) + +var ( + // Generate contains some sequence generation utilities. + // These utilities can be useful for generating test data. + Generate = &generate{ + rnd: rand.New(rand.NewSource(time.Now().Unix())), + } +) + +type generate struct { + rnd *rand.Rand +} + +// Float64 produces an array of floats from [start,end] by optional steps. +func (g generate) 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 (g generate) Random(samples int, scale float64) []float64 { + values := make([]float64, samples) + + for x := 0; x < samples; x++ { + values[x] = g.rnd.Float64() * scale + } + + return values +} + +// Random generates a fixed length sequence of random values with a given average, above and below that average by (-scale, scale) +func (g generate) RandomWithAverage(samples int, average, scale float64) []float64 { + values := make([]float64, samples) + + for x := 0; x < samples; x++ { + jitter := scale - (g.rnd.Float64() * (2 * scale)) + values[x] = average + jitter + } + + return values +} + +// Days generates a sequence of timestamps by day, from -days to today. +func (g generate) 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 +} + +func (g generate) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketOpen, from) + toClose := Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + todayOpen := Date.On(marketOpen, cursor) + todayClose := Date.On(marketClose, cursor) + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + + if (cursor.Equal(todayOpen) || cursor.After(todayOpen)) && (cursor.Equal(todayClose) || cursor.Before(todayClose)) && isValidTradingDay { + times = append(times, cursor) + } + if cursor.After(todayClose) { + cursor = Date.NextMarketOpen(cursor, marketOpen, isHoliday) + } else { + cursor = Date.NextHour(cursor) + } + } + return times +} + +func (g generate) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketOpen, from) + toClose := Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + + if isValidTradingDay { + todayOpen := Date.On(marketOpen, cursor) + todayNoon := Date.NoonOn(cursor) + today2pm := Date.On(Date.Time(14, 0, 0, 0, cursor.Location()), cursor) + todayClose := Date.On(marketClose, cursor) + times = append(times, todayOpen, todayNoon, today2pm, todayClose) + } + + cursor = Date.NextDay(cursor) + } + return times +} + +func (g generate) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketOpen, from) + toClose := Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + if isValidTradingDay { + todayClose := Date.On(marketClose, cursor) + times = append(times, todayClose) + } + + cursor = Date.NextDay(cursor) + } + return times +} + +func (g generate) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketOpen, from) + toClose := Date.On(marketClose, to) + for cursor.Before(toClose) || cursor.Equal(toClose) { + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + if isValidTradingDay { + todayClose := Date.On(marketClose, cursor) + times = append(times, todayClose) + } + + cursor = cursor.AddDate(0, 0, 2) + } + return times +} + +func (g generate) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { + var times []time.Time + cursor := Date.On(marketClose, from) + toClose := Date.On(marketClose, to) + + for cursor.Equal(toClose) || cursor.Before(toClose) { + isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) + if isValidTradingDay { + times = append(times, cursor) + } + cursor = Date.NextDayOfWeek(cursor, time.Monday) + } + return times +} + +func (g generate) Hours(start time.Time, totalHours int) []time.Time { + times := make([]time.Time, totalHours) + + last := start + for i := 0; i < totalHours; i++ { + times[i] = last + last = last.Add(time.Hour) + } + + return times +} + +// HoursFilled adds zero values for the data bounded by the start and end of the xdata array. +func (g generate) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { + start := Date.Start(xdata) + end := Date.End(xdata) + + totalHours := Math.AbsInt(Date.DiffHours(start, end)) + + finalTimes := g.Hours(start, totalHours+1) + finalValues := make([]float64, totalHours+1) + + var hoursFromStart int + for i, xd := range xdata { + hoursFromStart = Date.DiffHours(start, xd) + finalValues[hoursFromStart] = ydata[i] + } + + return finalTimes, finalValues +} diff --git a/sequence_test.go b/generate_test.go similarity index 87% rename from sequence_test.go rename to generate_test.go index 32109e0..b062cd3 100644 --- a/sequence_test.go +++ b/generate_test.go @@ -10,10 +10,10 @@ import ( func TestSequenceFloat64(t *testing.T) { assert := assert.New(t) - asc := Sequence.Float64(1.0, 10.0) + asc := Generate.Float64(1.0, 10.0) assert.Len(asc, 10) - desc := Sequence.Float64(10.0, 1.0) + desc := Generate.Float64(10.0, 1.0) assert.Len(desc, 10) } @@ -21,7 +21,7 @@ func TestSequenceMarketHours(t *testing.T) { assert := assert.New(t) today := time.Date(2016, 07, 01, 12, 0, 0, 0, Date.Eastern()) - mh := Sequence.MarketHours(today, today, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday) + mh := Generate.MarketHours(today, today, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday) assert.Len(mh, 8) assert.Equal(Date.Eastern(), mh[0].Location()) } @@ -29,7 +29,7 @@ func TestSequenceMarketHours(t *testing.T) { func TestSequenceMarketQuarters(t *testing.T) { assert := assert.New(t) today := time.Date(2016, 07, 01, 12, 0, 0, 0, Date.Eastern()) - mh := Sequence.MarketHourQuarters(today, today, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday) + mh := Generate.MarketHourQuarters(today, today, NYSEOpen(), NYSEClose(), Date.IsNYSEHoliday) assert.Len(mh, 4) assert.Equal(9, mh[0].Hour()) assert.Equal(30, mh[0].Minute()) @@ -48,7 +48,7 @@ func TestSequenceHours(t *testing.T) { assert := assert.New(t) today := time.Date(2016, 07, 01, 12, 0, 0, 0, time.UTC) - seq := Sequence.Hours(today, 24) + seq := Generate.Hours(today, 24) end := Date.End(seq) assert.Len(seq, 24) @@ -81,7 +81,7 @@ func TestSequenceHoursFill(t *testing.T) { 0.6, } - filledTimes, filledValues := Sequence.HoursFill(xdata, ydata) + filledTimes, filledValues := Generate.HoursFilled(xdata, ydata) assert.Len(filledTimes, Date.DiffHours(Date.Start(xdata), Date.End(xdata))+1) assert.Equal(len(filledValues), len(filledTimes)) diff --git a/histogram_series.go b/histogram_series.go index 153837b..a21571d 100644 --- a/histogram_series.go +++ b/histogram_series.go @@ -9,7 +9,7 @@ type HistogramSeries struct { Name string Style Style YAxis YAxisType - InnerSeries ValueProvider + InnerSeries ValuesProvider } // GetName implements Series.GetName. @@ -27,19 +27,19 @@ func (hs HistogramSeries) GetYAxis() YAxisType { return hs.YAxis } -// Len implements BoundedValueProvider.Len. +// Len implements BoundedValuesProvider.Len. func (hs HistogramSeries) Len() int { return hs.InnerSeries.Len() } -// GetValue implements ValueProvider.GetValue. -func (hs HistogramSeries) GetValue(index int) (x, y float64) { - return hs.InnerSeries.GetValue(index) +// GetValues implements ValuesProvider.GetValues. +func (hs HistogramSeries) GetValues(index int) (x, y float64) { + return hs.InnerSeries.GetValues(index) } -// GetBoundedValue implements BoundedValueProvider.GetBoundedValue -func (hs HistogramSeries) GetBoundedValue(index int) (x, y1, y2 float64) { - vx, vy := hs.InnerSeries.GetValue(index) +// GetBoundedValues implements BoundedValuesProvider.GetBoundedValue +func (hs HistogramSeries) GetBoundedValues(index int) (x, y1, y2 float64) { + vx, vy := hs.InnerSeries.GetValues(index) x = vx diff --git a/histogram_series_test.go b/histogram_series_test.go index 3e51833..cfc9d00 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: Sequence.Float64(1.0, 20.0), - YValues: Sequence.Float64(10.0, -10.0), + XValues: Generate.Float64(1.0, 20.0), + YValues: Generate.Float64(10.0, -10.0), } hs := HistogramSeries{ @@ -20,8 +20,8 @@ func TestHistogramSeries(t *testing.T) { } for x := 0; x < hs.Len(); x++ { - csx, csy := cs.GetValue(0) - hsx, hsy1, hsy2 := hs.GetBoundedValue(0) + csx, csy := cs.GetValues(0) + hsx, hsy1, hsy2 := hs.GetBoundedValues(0) assert.Equal(csx, hsx) assert.True(hsy1 > 0) assert.True(hsy2 <= 0) diff --git a/last_value_annotation_series.go b/last_value_annotation_series.go index 9006378..f3d4b46 100644 --- a/last_value_annotation_series.go +++ b/last_value_annotation_series.go @@ -3,7 +3,7 @@ package chart import "fmt" // LastValueAnnotation returns an annotation series of just the last value of a value provider. -func LastValueAnnotation(innerSeries ValueProvider, vfs ...ValueFormatter) AnnotationSeries { +func LastValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries { var vf ValueFormatter if len(vfs) > 0 { vf = vfs[0] @@ -14,11 +14,11 @@ func LastValueAnnotation(innerSeries ValueProvider, vfs ...ValueFormatter) Annot } var lastValue Value2 - if typed, isTyped := innerSeries.(LastValueProvider); isTyped { - lastValue.XValue, lastValue.YValue = typed.GetLastValue() + if typed, isTyped := innerSeries.(LastValuesProvider); isTyped { + lastValue.XValue, lastValue.YValue = typed.GetLastValues() lastValue.Label = vf(lastValue.YValue) } else { - lastValue.XValue, lastValue.YValue = innerSeries.GetValue(innerSeries.Len() - 1) + lastValue.XValue, lastValue.YValue = innerSeries.GetValues(innerSeries.Len() - 1) lastValue.Label = vf(lastValue.YValue) } diff --git a/linear_regression_series.go b/linear_regression_series.go index f40e13c..a3c5049 100644 --- a/linear_regression_series.go +++ b/linear_regression_series.go @@ -11,7 +11,7 @@ type LinearRegressionSeries struct { Limit int Offset int - InnerSeries ValueProvider + InnerSeries ValuesProvider m float64 b float64 @@ -62,8 +62,8 @@ func (lrs LinearRegressionSeries) GetOffset() int { return lrs.Offset } -// GetValue gets a value at a given index. -func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) { +// GetValues gets a value at a given index. +func (lrs *LinearRegressionSeries) GetValues(index int) (x, y float64) { if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { return } @@ -72,13 +72,13 @@ func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) { } offset := lrs.GetOffset() effectiveIndex := Math.MinInt(index+offset, lrs.InnerSeries.Len()) - x, y = lrs.InnerSeries.GetValue(effectiveIndex) + x, y = lrs.InnerSeries.GetValues(effectiveIndex) y = (lrs.m * lrs.normalize(x)) + lrs.b return } -// GetLastValue computes the last linear regression value. -func (lrs *LinearRegressionSeries) GetLastValue() (x, y float64) { +// GetLastValues computes the last linear regression value. +func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) { if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 { return } @@ -86,7 +86,7 @@ func (lrs *LinearRegressionSeries) GetLastValue() (x, y float64) { lrs.computeCoefficients() } endIndex := lrs.GetEndIndex() - x, y = lrs.InnerSeries.GetValue(endIndex) + x, y = lrs.InnerSeries.GetValues(endIndex) y = (lrs.m * lrs.normalize(x)) + lrs.b return } @@ -102,18 +102,18 @@ func (lrs *LinearRegressionSeries) computeCoefficients() { p := float64(endIndex - startIndex) - xvalues := NewRingBufferWithCapacity(lrs.Len()) + xvalues := NewValueBufferWithCapacity(lrs.Len()) for index := startIndex; index < endIndex; index++ { - x, _ := lrs.InnerSeries.GetValue(index) + x, _ := lrs.InnerSeries.GetValues(index) xvalues.Enqueue(x) } - lrs.avgx = xvalues.Average() - lrs.stddevx = xvalues.StdDev() + lrs.avgx = Sequence{xvalues}.Average() + lrs.stddevx = Sequence{xvalues}.StdDev() var sumx, sumy, sumxx, sumxy float64 for index := startIndex; index < endIndex; index++ { - x, y := lrs.InnerSeries.GetValue(index) + x, y := lrs.InnerSeries.GetValues(index) x = lrs.normalize(x) diff --git a/linear_regression_series_test.go b/linear_regression_series_test.go index b8c39e1..1063082 100644 --- a/linear_regression_series_test.go +++ b/linear_regression_series_test.go @@ -11,19 +11,19 @@ func TestLinearRegressionSeries(t *testing.T) { mainSeries := ContinuousSeries{ Name: "A test series", - XValues: Sequence.Float64(1.0, 100.0), - YValues: Sequence.Float64(1.0, 100.0), + XValues: Generate.Float64(1.0, 100.0), + YValues: Generate.Float64(1.0, 100.0), } linRegSeries := &LinearRegressionSeries{ InnerSeries: mainSeries, } - lrx0, lry0 := linRegSeries.GetValue(0) + lrx0, lry0 := linRegSeries.GetValues(0) assert.InDelta(1.0, lrx0, 0.0000001) assert.InDelta(1.0, lry0, 0.0000001) - lrxn, lryn := linRegSeries.GetLastValue() + lrxn, lryn := linRegSeries.GetLastValues() assert.InDelta(100.0, lrxn, 0.0000001) assert.InDelta(100.0, lryn, 0.0000001) } @@ -33,19 +33,19 @@ func TestLinearRegressionSeriesDesc(t *testing.T) { mainSeries := ContinuousSeries{ Name: "A test series", - XValues: Sequence.Float64(100.0, 1.0), - YValues: Sequence.Float64(100.0, 1.0), + XValues: Generate.Float64(100.0, 1.0), + YValues: Generate.Float64(100.0, 1.0), } linRegSeries := &LinearRegressionSeries{ InnerSeries: mainSeries, } - lrx0, lry0 := linRegSeries.GetValue(0) + lrx0, lry0 := linRegSeries.GetValues(0) assert.InDelta(100.0, lrx0, 0.0000001) assert.InDelta(100.0, lry0, 0.0000001) - lrxn, lryn := linRegSeries.GetLastValue() + lrxn, lryn := linRegSeries.GetLastValues() assert.InDelta(1.0, lrxn, 0.0000001) assert.InDelta(1.0, lryn, 0.0000001) } @@ -55,8 +55,8 @@ func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) { mainSeries := ContinuousSeries{ Name: "A test series", - XValues: Sequence.Float64(100.0, 1.0), - YValues: Sequence.Float64(100.0, 1.0), + XValues: Generate.Float64(100.0, 1.0), + YValues: Generate.Float64(100.0, 1.0), } linRegSeries := &LinearRegressionSeries{ @@ -67,11 +67,11 @@ func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) { assert.Equal(10, linRegSeries.Len()) - lrx0, lry0 := linRegSeries.GetValue(0) + lrx0, lry0 := linRegSeries.GetValues(0) assert.InDelta(90.0, lrx0, 0.0000001) assert.InDelta(90.0, lry0, 0.0000001) - lrxn, lryn := linRegSeries.GetLastValue() + lrxn, lryn := linRegSeries.GetLastValues() assert.InDelta(80.0, lrxn, 0.0000001) assert.InDelta(80.0, lryn, 0.0000001) } diff --git a/macd_series.go b/macd_series.go index af51d9a..6d04011 100644 --- a/macd_series.go +++ b/macd_series.go @@ -17,7 +17,7 @@ type MACDSeries struct { Name string Style Style YAxis YAxisType - InnerSeries ValueProvider + InnerSeries ValuesProvider PrimaryPeriod int SecondaryPeriod int @@ -89,8 +89,8 @@ func (macd MACDSeries) Len() int { return macd.InnerSeries.Len() } -// GetValue gets a value at a given index. For MACD it is the signal value. -func (macd *MACDSeries) GetValue(index int) (x float64, y float64) { +// GetValues gets a value at a given index. For MACD it is the signal value. +func (macd *MACDSeries) GetValues(index int) (x float64, y float64) { if macd.InnerSeries == nil { return } @@ -99,10 +99,10 @@ func (macd *MACDSeries) GetValue(index int) (x float64, y float64) { macd.ensureChildSeries() } - _, lv := macd.macdl.GetValue(index) - _, sv := macd.signal.GetValue(index) + _, lv := macd.macdl.GetValues(index) + _, sv := macd.signal.GetValues(index) - x, _ = macd.InnerSeries.GetValue(index) + x, _ = macd.InnerSeries.GetValues(index) y = lv - sv return @@ -130,7 +130,7 @@ type MACDSignalSeries struct { Name string Style Style YAxis YAxisType - InnerSeries ValueProvider + InnerSeries ValuesProvider PrimaryPeriod int SecondaryPeriod int @@ -191,8 +191,8 @@ func (macds *MACDSignalSeries) Len() int { return macds.InnerSeries.Len() } -// GetValue gets a value at a given index. For MACD it is the signal value. -func (macds *MACDSignalSeries) GetValue(index int) (x float64, y float64) { +// GetValues gets a value at a given index. For MACD it is the signal value. +func (macds *MACDSignalSeries) GetValues(index int) (x float64, y float64) { if macds.InnerSeries == nil { return } @@ -200,8 +200,8 @@ func (macds *MACDSignalSeries) GetValue(index int) (x float64, y float64) { if macds.signal == nil { macds.ensureSignal() } - x, _ = macds.InnerSeries.GetValue(index) - _, y = macds.signal.GetValue(index) + x, _ = macds.InnerSeries.GetValues(index) + _, y = macds.signal.GetValues(index) return } @@ -229,7 +229,7 @@ type MACDLineSeries struct { Name string Style Style YAxis YAxisType - InnerSeries ValueProvider + InnerSeries ValuesProvider PrimaryPeriod int SecondaryPeriod int @@ -300,8 +300,8 @@ func (macdl *MACDLineSeries) Len() int { return macdl.InnerSeries.Len() } -// GetValue gets a value at a given index. For MACD it is the signal value. -func (macdl *MACDLineSeries) GetValue(index int) (x float64, y float64) { +// GetValues gets a value at a given index. For MACD it is the signal value. +func (macdl *MACDLineSeries) GetValues(index int) (x float64, y float64) { if macdl.InnerSeries == nil { return } @@ -309,10 +309,10 @@ func (macdl *MACDLineSeries) GetValue(index int) (x float64, y float64) { macdl.ensureEMASeries() } - x, _ = macdl.InnerSeries.GetValue(index) + x, _ = macdl.InnerSeries.GetValues(index) - _, emav1 := macdl.ema1.GetValue(index) - _, emav2 := macdl.ema2.GetValue(index) + _, emav1 := macdl.ema1.GetValues(index) + _, emav2 := macdl.ema2.GetValues(index) y = emav2 - emav1 return diff --git a/macd_series_test.go b/macd_series_test.go index f6f6a31..842eb4c 100644 --- a/macd_series_test.go +++ b/macd_series_test.go @@ -65,7 +65,7 @@ var ( func TestMACDSeries(t *testing.T) { assert := assert.New(t) - mockSeries := mockValueProvider{ + mockSeries := mockValuesProvider{ emaXValues, emaYValues, } @@ -77,7 +77,7 @@ func TestMACDSeries(t *testing.T) { var yvalues []float64 for x := 0; x < mas.Len(); x++ { - _, y := mas.GetValue(x) + _, y := mas.GetValues(x) yvalues = append(yvalues, y) } diff --git a/market_hours_range.go b/market_hours_range.go index 875094a..59d4563 100644 --- a/market_hours_range.go +++ b/market_hours_range.go @@ -112,31 +112,31 @@ func (mhr MarketHoursRange) GetMarketClose() time.Time { // GetTicks returns the ticks for the range. // This is to override the default continous ticks that would be generated for the range. func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick { - times := Sequence.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times := Generate.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth := mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = Sequence.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = Generate.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = Sequence.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = Generate.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = Sequence.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = Generate.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) } - times = Sequence.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) + times = Generate.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider()) timesWidth = mhr.measureTimes(r, defaults, vf, times) if timesWidth <= mhr.Domain { return mhr.makeTicks(vf, times) diff --git a/math_util.go b/math.go similarity index 100% rename from math_util.go rename to math.go diff --git a/math_util_test.go b/math_test.go similarity index 100% rename from math_util_test.go rename to math_test.go diff --git a/min_max_series.go b/min_max_series.go index db153aa..18d330a 100644 --- a/min_max_series.go +++ b/min_max_series.go @@ -10,7 +10,7 @@ type MinSeries struct { Name string Style Style YAxis YAxisType - InnerSeries ValueProvider + InnerSeries ValuesProvider minValue *float64 } @@ -35,10 +35,10 @@ func (ms MinSeries) Len() int { return ms.InnerSeries.Len() } -// GetValue gets a value at a given index. -func (ms *MinSeries) GetValue(index int) (x, y float64) { +// GetValues gets a value at a given index. +func (ms *MinSeries) GetValues(index int) (x, y float64) { ms.ensureMinValue() - x, _ = ms.InnerSeries.GetValue(index) + x, _ = ms.InnerSeries.GetValues(index) y = *ms.minValue return } @@ -54,7 +54,7 @@ func (ms *MinSeries) ensureMinValue() { minValue := math.MaxFloat64 var y float64 for x := 0; x < ms.InnerSeries.Len(); x++ { - _, y = ms.InnerSeries.GetValue(x) + _, y = ms.InnerSeries.GetValues(x) if y < minValue { minValue = y } @@ -76,7 +76,7 @@ type MaxSeries struct { Name string Style Style YAxis YAxisType - InnerSeries ValueProvider + InnerSeries ValuesProvider maxValue *float64 } @@ -101,10 +101,10 @@ func (ms MaxSeries) Len() int { return ms.InnerSeries.Len() } -// GetValue gets a value at a given index. -func (ms *MaxSeries) GetValue(index int) (x, y float64) { +// GetValues gets a value at a given index. +func (ms *MaxSeries) GetValues(index int) (x, y float64) { ms.ensureMaxValue() - x, _ = ms.InnerSeries.GetValue(index) + x, _ = ms.InnerSeries.GetValues(index) y = *ms.maxValue return } @@ -120,7 +120,7 @@ func (ms *MaxSeries) ensureMaxValue() { maxValue := -math.MaxFloat64 var y float64 for x := 0; x < ms.InnerSeries.Len(); x++ { - _, y = ms.InnerSeries.GetValue(x) + _, y = ms.InnerSeries.GetValues(x) if y > maxValue { maxValue = y } diff --git a/polynomial_regression_series.go b/polynomial_regression_series.go index c5a4a08..e9dd1bd 100644 --- a/polynomial_regression_series.go +++ b/polynomial_regression_series.go @@ -17,7 +17,7 @@ type PolynomialRegressionSeries struct { Limit int Offset int Degree int - InnerSeries ValueProvider + InnerSeries ValuesProvider coeffs []float64 } @@ -79,8 +79,8 @@ func (prs *PolynomialRegressionSeries) Validate() error { return nil } -// GetValue returns the series value for a given index. -func (prs *PolynomialRegressionSeries) GetValue(index int) (x, y float64) { +// GetValues returns the series value for a given index. +func (prs *PolynomialRegressionSeries) GetValues(index int) (x, y float64) { if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 { return } @@ -95,13 +95,13 @@ func (prs *PolynomialRegressionSeries) GetValue(index int) (x, y float64) { offset := prs.GetOffset() effectiveIndex := Math.MinInt(index+offset, prs.InnerSeries.Len()) - x, y = prs.InnerSeries.GetValue(effectiveIndex) + x, y = prs.InnerSeries.GetValues(effectiveIndex) y = prs.apply(x) return } -// GetLastValue computes the last poly regression value. -func (prs *PolynomialRegressionSeries) GetLastValue() (x, y float64) { +// GetLastValues computes the last poly regression value. +func (prs *PolynomialRegressionSeries) GetLastValues() (x, y float64) { if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 { return } @@ -113,7 +113,7 @@ func (prs *PolynomialRegressionSeries) GetLastValue() (x, y float64) { prs.coeffs = coeffs } endIndex := prs.GetEndIndex() - x, y = prs.InnerSeries.GetValue(endIndex) + x, y = prs.InnerSeries.GetValues(endIndex) y = prs.apply(x) return } @@ -138,7 +138,7 @@ func (prs *PolynomialRegressionSeries) values() (xvalues, yvalues []float64) { yvalues = make([]float64, endIndex-startIndex) for index := startIndex; index < endIndex; index++ { - x, y := prs.InnerSeries.GetValue(index) + x, y := prs.InnerSeries.GetValues(index) xvalues[index-startIndex] = x yvalues[index-startIndex] = y } diff --git a/polynomial_regression_test.go b/polynomial_regression_test.go index cde86d0..beabf37 100644 --- a/polynomial_regression_test.go +++ b/polynomial_regression_test.go @@ -29,7 +29,7 @@ func TestPolynomialRegression(t *testing.T) { } for i := 0; i < 100; i++ { - _, y := poly.GetValue(i) + _, y := poly.GetValues(i) assert.InDelta(float64(i*i), y, matrix.DefaultEpsilon) } } diff --git a/ring_buffer.go b/ring_buffer.go deleted file mode 100644 index 6fb4288..0000000 --- a/ring_buffer.go +++ /dev/null @@ -1,252 +0,0 @@ -package chart - -import ( - "fmt" - "math" - "strings" -) - -const ( - ringBufferMinimumGrow = 4 - ringBufferShrinkThreshold = 32 - ringBufferGrowFactor = 200 - ringBufferDefaultCapacity = 4 -) - -var ( - emptyArray = make([]interface{}, 0) -) - -// NewRingBuffer creates a new, empty, RingBuffer. -func NewRingBuffer() *RingBuffer { - return &RingBuffer{ - array: make([]interface{}, ringBufferDefaultCapacity), - head: 0, - tail: 0, - size: 0, - } -} - -// NewRingBufferWithCapacity creates a new RingBuffer pre-allocated with the given capacity. -func NewRingBufferWithCapacity(capacity int) *RingBuffer { - return &RingBuffer{ - array: make([]interface{}, capacity), - head: 0, - tail: 0, - size: 0, - } -} - -// NewRingBufferFromSlice createsa ring buffer out of a slice. -func NewRingBufferFromSlice(values []interface{}) *RingBuffer { - return &RingBuffer{ - array: values, - head: 0, - tail: len(values) - 1, - size: len(values), - } -} - -// RingBuffer is a fifo buffer that is backed by a pre-allocated array, instead of allocating -// a whole new node object for each element (which saves GC churn). -// Enqueue can be O(n), Dequeue can be O(1). -type RingBuffer struct { - array []interface{} - head int - tail int - size int -} - -// Len returns the length of the ring buffer (as it is currently populated). -// Actual memory footprint may be different. -func (rb *RingBuffer) Len() int { - return rb.size -} - -// TotalLen returns the total size of the ring bufffer, including empty elements. -func (rb *RingBuffer) TotalLen() int { - return len(rb.array) -} - -// Clear removes all objects from the RingBuffer. -func (rb *RingBuffer) Clear() { - if rb.head < rb.tail { - arrayClear(rb.array, rb.head, rb.size) - } else { - arrayClear(rb.array, rb.head, len(rb.array)-rb.head) - arrayClear(rb.array, 0, rb.tail) - } - - rb.head = 0 - rb.tail = 0 - rb.size = 0 -} - -// Enqueue adds an element to the "back" of the RingBuffer. -func (rb *RingBuffer) Enqueue(object interface{}) { - if rb.size == len(rb.array) { - newCapacity := int(len(rb.array) * int(ringBufferGrowFactor/100)) - if newCapacity < (len(rb.array) + ringBufferMinimumGrow) { - newCapacity = len(rb.array) + ringBufferMinimumGrow - } - rb.setCapacity(newCapacity) - } - - rb.array[rb.tail] = object - rb.tail = (rb.tail + 1) % len(rb.array) - rb.size++ -} - -// Dequeue removes the first element from the RingBuffer. -func (rb *RingBuffer) Dequeue() interface{} { - if rb.size == 0 { - return nil - } - - removed := rb.array[rb.head] - rb.head = (rb.head + 1) % len(rb.array) - rb.size-- - return removed -} - -// Peek returns but does not remove the first element. -func (rb *RingBuffer) Peek() interface{} { - if rb.size == 0 { - return nil - } - return rb.array[rb.head] -} - -// PeekBack returns but does not remove the last element. -func (rb *RingBuffer) PeekBack() interface{} { - if rb.size == 0 { - return nil - } - if rb.tail == 0 { - return rb.array[len(rb.array)-1] - } - return rb.array[rb.tail-1] -} - -func (rb *RingBuffer) setCapacity(capacity int) { - newArray := make([]interface{}, capacity) - if rb.size > 0 { - if rb.head < rb.tail { - arrayCopy(rb.array, rb.head, newArray, 0, rb.size) - } else { - arrayCopy(rb.array, rb.head, newArray, 0, len(rb.array)-rb.head) - arrayCopy(rb.array, 0, newArray, len(rb.array)-rb.head, rb.tail) - } - } - rb.array = newArray - rb.head = 0 - if rb.size == capacity { - rb.tail = 0 - } else { - rb.tail = rb.size - } -} - -// TrimExcess resizes the buffer to better fit the contents. -func (rb *RingBuffer) TrimExcess() { - threshold := float64(len(rb.array)) * 0.9 - if rb.size < int(threshold) { - rb.setCapacity(rb.size) - } -} - -// AsSlice returns the ring buffer, in order, as a slice. -func (rb *RingBuffer) AsSlice() []interface{} { - newArray := make([]interface{}, rb.size) - - if rb.size == 0 { - return newArray - } - - if rb.head < rb.tail { - arrayCopy(rb.array, rb.head, newArray, 0, rb.size) - } else { - arrayCopy(rb.array, rb.head, newArray, 0, len(rb.array)-rb.head) - arrayCopy(rb.array, 0, newArray, len(rb.array)-rb.head, rb.tail) - } - - return newArray -} - -// Each calls the consumer for each element in the buffer. -func (rb *RingBuffer) Each(consumer func(value interface{})) { - if rb.size == 0 { - return - } - - if rb.head < rb.tail { - for cursor := rb.head; cursor < rb.tail; cursor++ { - consumer(rb.array[cursor]) - } - } else { - for cursor := rb.head; cursor < len(rb.array); cursor++ { - consumer(rb.array[cursor]) - } - for cursor := 0; cursor < rb.tail; cursor++ { - consumer(rb.array[cursor]) - } - } -} - -func (rb *RingBuffer) String() string { - var values []string - for _, elem := range rb.AsSlice() { - values = append(values, fmt.Sprintf("%v", elem)) - } - return strings.Join(values, " <= ") -} - -// Average returns the float average of the values in the buffer. -func (rb *RingBuffer) Average() float64 { - var accum float64 - rb.Each(func(v interface{}) { - if typed, isTyped := v.(float64); isTyped { - accum += typed - } - }) - return accum / float64(rb.Len()) -} - -// Variance computes the variance of the buffer. -func (rb *RingBuffer) Variance() float64 { - if rb.Len() == 0 { - return 0 - } - - var variance float64 - m := rb.Average() - - rb.Each(func(v interface{}) { - if n, isTyped := v.(float64); isTyped { - variance += (float64(n) - m) * (float64(n) - m) - } - }) - - return variance / float64(rb.Len()) -} - -// StdDev returns the standard deviation. -func (rb *RingBuffer) StdDev() float64 { - return math.Pow(rb.Variance(), 0.5) -} - -func arrayClear(source []interface{}, index, length int) { - for x := 0; x < length; x++ { - absoluteIndex := x + index - source[absoluteIndex] = nil - } -} - -func arrayCopy(source []interface{}, sourceIndex int, destination []interface{}, destinationIndex, length int) { - for x := 0; x < length; x++ { - from := sourceIndex + x - to := destinationIndex + x - - destination[to] = source[from] - } -} diff --git a/sequence.go b/sequence.go index ab4125b..5d6d839 100644 --- a/sequence.go +++ b/sequence.go @@ -1,190 +1,82 @@ package chart -import ( - "math/rand" - "time" -) +import "math" -var ( - // Sequence contains some sequence utilities. - // These utilities can be useful for generating test data. - Sequence = &sequence{ - rnd: rand.New(rand.NewSource(time.Now().Unix())), - } -) - -type sequence struct { - rnd *rand.Rand +// SequenceProvider is a provider for values for a sequence. +type SequenceProvider interface { + Len() int + GetValue(int) float64 } -// 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 +// Sequence is a utility wrapper for sequence providers. +type Sequence struct { + SequenceProvider } -// Random generates a fixed length sequence of random values between (0, scale). -func (s sequence) Random(samples int, scale float64) []float64 { - values := make([]float64, samples) - - for x := 0; x < samples; x++ { - values[x] = s.rnd.Float64() * scale +// Each applies the `mapfn` to all values in the value provider. +func (s Sequence) Each(mapfn func(int, float64)) { + for i := 0; i < s.Len(); i++ { + mapfn(i, s.GetValue(i)) } - - return values } -// Random generates a fixed length sequence of random values with a given average, above and below that average by (-scale, scale) -func (s sequence) RandomWithAverage(samples int, average, scale float64) []float64 { - values := make([]float64, samples) - - for x := 0; x < samples; x++ { - jitter := scale - (s.rnd.Float64() * (2 * scale)) - values[x] = average + jitter +// Map applies the `mapfn` to all values in the value provider, +// returning a new sequence. +func (s Sequence) Map(mapfn func(i int, v float64) float64) Sequence { + output := make([]float64, s.Len()) + for i := 0; i < s.Len(); i++ { + mapfn(i, s.GetValue(i)) } - - return values + return Sequence{Array(output)} } -// 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 -} - -func (s sequence) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { - var times []time.Time - cursor := Date.On(marketOpen, from) - toClose := Date.On(marketClose, to) - for cursor.Before(toClose) || cursor.Equal(toClose) { - todayOpen := Date.On(marketOpen, cursor) - todayClose := Date.On(marketClose, cursor) - isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) - - if (cursor.Equal(todayOpen) || cursor.After(todayOpen)) && (cursor.Equal(todayClose) || cursor.Before(todayClose)) && isValidTradingDay { - times = append(times, cursor) - } - if cursor.After(todayClose) { - cursor = Date.NextMarketOpen(cursor, marketOpen, isHoliday) - } else { - cursor = Date.NextHour(cursor) - } - } - return times -} - -func (s sequence) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { - var times []time.Time - cursor := Date.On(marketOpen, from) - toClose := Date.On(marketClose, to) - for cursor.Before(toClose) || cursor.Equal(toClose) { - - isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) - - if isValidTradingDay { - todayOpen := Date.On(marketOpen, cursor) - todayNoon := Date.NoonOn(cursor) - today2pm := Date.On(Date.Time(14, 0, 0, 0, cursor.Location()), cursor) - todayClose := Date.On(marketClose, cursor) - times = append(times, todayOpen, todayNoon, today2pm, todayClose) - } - - cursor = Date.NextDay(cursor) - } - return times -} - -func (s sequence) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { - var times []time.Time - cursor := Date.On(marketOpen, from) - toClose := Date.On(marketClose, to) - for cursor.Before(toClose) || cursor.Equal(toClose) { - isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) - if isValidTradingDay { - todayClose := Date.On(marketClose, cursor) - times = append(times, todayClose) - } - - cursor = Date.NextDay(cursor) - } - return times -} - -func (s sequence) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { - var times []time.Time - cursor := Date.On(marketOpen, from) - toClose := Date.On(marketClose, to) - for cursor.Before(toClose) || cursor.Equal(toClose) { - isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) - if isValidTradingDay { - todayClose := Date.On(marketClose, cursor) - times = append(times, todayClose) - } - - cursor = cursor.AddDate(0, 0, 2) - } - return times -} - -func (s sequence) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday HolidayProvider) []time.Time { - var times []time.Time - cursor := Date.On(marketClose, from) - toClose := Date.On(marketClose, to) - - for cursor.Equal(toClose) || cursor.Before(toClose) { - isValidTradingDay := !isHoliday(cursor) && Date.IsWeekDay(cursor.Weekday()) - if isValidTradingDay { - times = append(times, cursor) - } - cursor = Date.NextDayOfWeek(cursor, time.Monday) - } - return times -} - -func (s sequence) Hours(start time.Time, totalHours int) []time.Time { - times := make([]time.Time, totalHours) - - last := start - for i := 0; i < totalHours; i++ { - times[i] = last - last = last.Add(time.Hour) +// Average returns the float average of the values in the buffer. +func (s Sequence) Average() float64 { + if s.Len() == 0 { + return 0 } - return times + var accum float64 + for i := 0; i < s.Len(); i++ { + accum += s.GetValue(i) + } + return accum / float64(s.Len()) } -// HoursFill adds zero values for the data bounded by the start and end of the xdata array. -func (s sequence) HoursFill(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { - start := Date.Start(xdata) - end := Date.End(xdata) - - totalHours := Math.AbsInt(Date.DiffHours(start, end)) - - finalTimes := s.Hours(start, totalHours+1) - finalValues := make([]float64, totalHours+1) - - var hoursFromStart int - for i, xd := range xdata { - hoursFromStart = Date.DiffHours(start, xd) - finalValues[hoursFromStart] = ydata[i] +// Variance computes the variance of the buffer. +func (s Sequence) Variance() float64 { + if s.Len() == 0 { + return 0 } - return finalTimes, finalValues + m := s.Average() + var variance, v float64 + for i := 0; i < s.Len(); i++ { + v = s.GetValue(i) + variance += (v - m) * (v - m) + } + + return variance / float64(s.Len()) +} + +// StdDev returns the standard deviation. +func (s Sequence) StdDev() float64 { + if s.Len() == 0 { + return 0 + } + + return math.Pow(s.Variance(), 0.5) +} + +// Array is a wrapper for an array of floats that implements `ValuesProvider`. +type Array []float64 + +// Len returns the value provider length. +func (a Array) Len() int { + return len(a) +} + +// GetValue returns the value at a given index. +func (a Array) GetValue(index int) float64 { + return a[index] } diff --git a/sma_series.go b/sma_series.go index f68c60d..73f563c 100644 --- a/sma_series.go +++ b/sma_series.go @@ -14,7 +14,7 @@ type SMASeries struct { YAxis YAxisType Period int - InnerSeries ValueProvider + InnerSeries ValuesProvider } // GetName returns the name of the time series. @@ -48,25 +48,25 @@ func (sma SMASeries) GetPeriod(defaults ...int) int { return sma.Period } -// GetValue gets a value at a given index. -func (sma SMASeries) GetValue(index int) (x, y float64) { +// GetValues gets a value at a given index. +func (sma SMASeries) GetValues(index int) (x, y float64) { if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 { return } - px, _ := sma.InnerSeries.GetValue(index) + px, _ := sma.InnerSeries.GetValues(index) x = px y = sma.getAverage(index) return } -// GetLastValue computes the last moving average value but walking back window size samples, +// GetLastValues computes the last moving average value but walking back window size samples, // and recomputing the last moving average chunk. -func (sma SMASeries) GetLastValue() (x, y float64) { +func (sma SMASeries) GetLastValues() (x, y float64) { if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 { return } seriesLen := sma.InnerSeries.Len() - px, _ := sma.InnerSeries.GetValue(seriesLen - 1) + px, _ := sma.InnerSeries.GetValues(seriesLen - 1) x = px y = sma.getAverage(seriesLen - 1) return @@ -78,7 +78,7 @@ func (sma SMASeries) getAverage(index int) float64 { var accum float64 var count float64 for x := index; x >= floor; x-- { - _, vy := sma.InnerSeries.GetValue(x) + _, vy := sma.InnerSeries.GetValues(x) accum += vy count += 1.0 } diff --git a/sma_series_test.go b/sma_series_test.go index 7a715cf..c2f7d81 100644 --- a/sma_series_test.go +++ b/sma_series_test.go @@ -6,16 +6,16 @@ import ( "github.com/blendlabs/go-assert" ) -type mockValueProvider struct { +type mockValuesProvider struct { X []float64 Y []float64 } -func (m mockValueProvider) Len() int { +func (m mockValuesProvider) Len() int { return Math.MinInt(len(m.X), len(m.Y)) } -func (m mockValueProvider) GetValue(index int) (x, y float64) { +func (m mockValuesProvider) GetValues(index int) (x, y float64) { if index < 0 { panic("negative index at GetValue()") } @@ -30,9 +30,9 @@ func (m mockValueProvider) GetValue(index int) (x, y float64) { func TestSMASeriesGetValue(t *testing.T) { assert := assert.New(t) - mockSeries := mockValueProvider{ - Sequence.Float64(1.0, 10.0), - Sequence.Float64(10, 1.0), + mockSeries := mockValuesProvider{ + Generate.Float64(1.0, 10.0), + Generate.Float64(10, 1.0), } assert.Equal(10, mockSeries.Len()) @@ -43,7 +43,7 @@ func TestSMASeriesGetValue(t *testing.T) { var yvalues []float64 for x := 0; x < mas.Len(); x++ { - _, y := mas.GetValue(x) + _, y := mas.GetValues(x) yvalues = append(yvalues, y) } @@ -61,9 +61,9 @@ func TestSMASeriesGetValue(t *testing.T) { func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) { assert := assert.New(t) - mockSeries := mockValueProvider{ - Sequence.Float64(1.0, 10.0), - Sequence.Float64(10, 1.0), + mockSeries := mockValuesProvider{ + Generate.Float64(1.0, 10.0), + Generate.Float64(10, 1.0), } assert.Equal(10, mockSeries.Len()) @@ -74,11 +74,11 @@ func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) { var yvalues []float64 for x := 0; x < mas.Len(); x++ { - _, y := mas.GetValue(x) + _, y := mas.GetValues(x) yvalues = append(yvalues, y) } - lx, ly := mas.GetLastValue() + lx, ly := mas.GetLastValues() assert.Equal(10.0, lx) assert.Equal(5.5, ly) assert.Equal(yvalues[len(yvalues)-1], ly) @@ -87,9 +87,9 @@ func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) { func TestSMASeriesGetLastValue(t *testing.T) { assert := assert.New(t) - mockSeries := mockValueProvider{ - Sequence.Float64(1.0, 100.0), - Sequence.Float64(100, 1.0), + mockSeries := mockValuesProvider{ + Generate.Float64(1.0, 100.0), + Generate.Float64(100, 1.0), } assert.Equal(100, mockSeries.Len()) @@ -100,11 +100,11 @@ func TestSMASeriesGetLastValue(t *testing.T) { var yvalues []float64 for x := 0; x < mas.Len(); x++ { - _, y := mas.GetValue(x) + _, y := mas.GetValues(x) yvalues = append(yvalues, y) } - lx, ly := mas.GetLastValue() + lx, ly := mas.GetLastValues() assert.Equal(100.0, lx) assert.Equal(6, ly) assert.Equal(yvalues[len(yvalues)-1], ly) diff --git a/stacked_bar_chart.go b/stacked_bar_chart.go index 16c4919..6b85d81 100644 --- a/stacked_bar_chart.go +++ b/stacked_bar_chart.go @@ -200,7 +200,7 @@ func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom) r.Stroke() - ticks := Sequence.Float64(1.0, 0.0, 0.2) + ticks := Generate.Float64(1.0, 0.0, 0.2) for _, t := range ticks { axisStyle.GetStrokeOptions().WriteToRenderer(r) ty := canvasBox.Bottom - int(t*float64(canvasBox.Height())) diff --git a/time_series.go b/time_series.go index edade97..7707dd1 100644 --- a/time_series.go +++ b/time_series.go @@ -31,15 +31,15 @@ func (ts TimeSeries) Len() int { return len(ts.XValues) } -// GetValue gets a value at a given index. -func (ts TimeSeries) GetValue(index int) (x, y float64) { +// GetValues gets a value at a given index. +func (ts TimeSeries) GetValues(index int) (x, y float64) { x = Time.ToFloat64(ts.XValues[index]) y = ts.YValues[index] return } -// GetLastValue gets the last value. -func (ts TimeSeries) GetLastValue() (x, y float64) { +// GetLastValues gets the last value. +func (ts TimeSeries) GetLastValues() (x, y float64) { x = Time.ToFloat64(ts.XValues[len(ts.XValues)-1]) y = ts.YValues[len(ts.YValues)-1] return diff --git a/time_series_test.go b/time_series_test.go index 717477b..ebf14e2 100644 --- a/time_series_test.go +++ b/time_series_test.go @@ -24,7 +24,7 @@ func TestTimeSeriesGetValue(t *testing.T) { }, } - x0, y0 := ts.GetValue(0) + x0, y0 := ts.GetValues(0) assert.NotZero(x0) assert.Equal(1.0, y0) } diff --git a/value_buffer.go b/value_buffer.go new file mode 100644 index 0000000..0685940 --- /dev/null +++ b/value_buffer.go @@ -0,0 +1,229 @@ +package chart + +import ( + "fmt" + "strings" +) + +const ( + valueBufferMinimumGrow = 4 + valueBufferShrinkThreshold = 32 + valueBufferGrowFactor = 200 + valueBufferDefaultCapacity = 4 +) + +var ( + emptyArray = make([]float64, 0) +) + +// NewValueBuffer creates a new value buffer with an optional set of values. +func NewValueBuffer(values ...float64) *ValueBuffer { + var tail int + array := make([]float64, Math.MaxInt(len(values), valueBufferDefaultCapacity)) + if len(values) > 0 { + copy(array, values) + tail = len(values) + } + return &ValueBuffer{ + array: array, + head: 0, + tail: tail, + size: len(values), + } +} + +// NewValueBufferWithCapacity creates a new ValueBuffer pre-allocated with the given capacity. +func NewValueBufferWithCapacity(capacity int) *ValueBuffer { + return &ValueBuffer{ + array: make([]float64, capacity), + head: 0, + tail: 0, + size: 0, + } +} + +// ValueBuffer is a fifo buffer that is backed by a pre-allocated array, instead of allocating +// a whole new node object for each element (which saves GC churn). +// Enqueue can be O(n), Dequeue can be O(1). +type ValueBuffer struct { + array []float64 + head int + tail int + size int +} + +// Len returns the length of the ValueBuffer (as it is currently populated). +// Actual memory footprint may be different. +func (vb *ValueBuffer) Len() int { + return vb.size +} + +// GetValue implements sequence provider. +func (vb *ValueBuffer) GetValue(index int) float64 { + effectiveIndex := (vb.head + index) % len(vb.array) + return vb.array[effectiveIndex] +} + +// Capacity returns the total size of the ValueBuffer, including empty elements. +func (vb *ValueBuffer) Capacity() int { + return len(vb.array) +} + +// SetCapacity sets the capacity of the ValueBuffer. +func (vb *ValueBuffer) SetCapacity(capacity int) { + newArray := make([]float64, capacity) + if vb.size > 0 { + if vb.head < vb.tail { + arrayCopy(vb.array, vb.head, newArray, 0, vb.size) + } else { + arrayCopy(vb.array, vb.head, newArray, 0, len(vb.array)-vb.head) + arrayCopy(vb.array, 0, newArray, len(vb.array)-vb.head, vb.tail) + } + } + vb.array = newArray + vb.head = 0 + if vb.size == capacity { + vb.tail = 0 + } else { + vb.tail = vb.size + } +} + +// Clear removes all objects from the ValueBuffer. +func (vb *ValueBuffer) Clear() { + if vb.head < vb.tail { + arrayClear(vb.array, vb.head, vb.size) + } else { + arrayClear(vb.array, vb.head, len(vb.array)-vb.head) + arrayClear(vb.array, 0, vb.tail) + } + + vb.head = 0 + vb.tail = 0 + vb.size = 0 +} + +// Enqueue adds an element to the "back" of the ValueBuffer. +func (vb *ValueBuffer) Enqueue(value float64) { + if vb.size == len(vb.array) { + newCapacity := int(len(vb.array) * int(valueBufferGrowFactor/100)) + if newCapacity < (len(vb.array) + valueBufferMinimumGrow) { + newCapacity = len(vb.array) + valueBufferMinimumGrow + } + vb.SetCapacity(newCapacity) + } + + vb.array[vb.tail] = value + vb.tail = (vb.tail + 1) % len(vb.array) + vb.size++ +} + +// Dequeue removes the first element from the RingBuffer. +func (vb *ValueBuffer) Dequeue() float64 { + if vb.size == 0 { + return 0 + } + + removed := vb.array[vb.head] + vb.head = (vb.head + 1) % len(vb.array) + vb.size-- + return removed +} + +// Peek returns but does not remove the first element. +func (vb *ValueBuffer) Peek() float64 { + if vb.size == 0 { + return 0 + } + return vb.array[vb.head] +} + +// PeekBack returns but does not remove the last element. +func (vb *ValueBuffer) PeekBack() float64 { + if vb.size == 0 { + return 0 + } + if vb.tail == 0 { + return vb.array[len(vb.array)-1] + } + return vb.array[vb.tail-1] +} + +// TrimExcess resizes the buffer to better fit the contents. +func (vb *ValueBuffer) TrimExcess() { + threshold := float64(len(vb.array)) * 0.9 + if vb.size < int(threshold) { + vb.SetCapacity(vb.size) + } +} + +// Array returns the ring buffer, in order, as an array. +func (vb *ValueBuffer) Array() Array { + newArray := make([]float64, vb.size) + + if vb.size == 0 { + return newArray + } + + if vb.head < vb.tail { + arrayCopy(vb.array, vb.head, newArray, 0, vb.size) + } else { + arrayCopy(vb.array, vb.head, newArray, 0, len(vb.array)-vb.head) + arrayCopy(vb.array, 0, newArray, len(vb.array)-vb.head, vb.tail) + } + + return Array(newArray) +} + +// Each calls the consumer for each element in the buffer. +func (vb *ValueBuffer) Each(mapfn func(int, float64)) { + if vb.size == 0 { + return + } + + var index int + if vb.head < vb.tail { + for cursor := vb.head; cursor < vb.tail; cursor++ { + mapfn(index, vb.array[cursor]) + index++ + } + } else { + for cursor := vb.head; cursor < len(vb.array); cursor++ { + mapfn(index, vb.array[cursor]) + index++ + } + for cursor := 0; cursor < vb.tail; cursor++ { + mapfn(index, vb.array[cursor]) + index++ + } + } +} + +// String returns a string representation for value buffers. +func (vb *ValueBuffer) String() string { + var values []string + for _, elem := range vb.Array() { + values = append(values, fmt.Sprintf("%v", elem)) + } + return strings.Join(values, " <= ") +} + +// -------------------------------------------------------------------------------- +// Util methods +// -------------------------------------------------------------------------------- + +func arrayClear(source []float64, index, length int) { + for x := 0; x < length; x++ { + absoluteIndex := x + index + source[absoluteIndex] = 0 + } +} + +func arrayCopy(source []float64, sourceIndex int, destination []float64, destinationIndex, length int) { + for x := 0; x < length; x++ { + from := sourceIndex + x + to := destinationIndex + x + + destination[to] = source[from] + } +} diff --git a/ring_buffer_test.go b/value_buffer_test.go similarity index 75% rename from ring_buffer_test.go rename to value_buffer_test.go index cc35074..1b31bb3 100644 --- a/ring_buffer_test.go +++ b/value_buffer_test.go @@ -6,10 +6,10 @@ import ( "github.com/blendlabs/go-assert" ) -func TestRingBuffer(t *testing.T) { +func TestValueBuffer(t *testing.T) { assert := assert.New(t) - buffer := NewRingBuffer() + buffer := NewValueBuffer() buffer.Enqueue(1) assert.Equal(1, buffer.Len()) @@ -96,14 +96,14 @@ func TestRingBuffer(t *testing.T) { value = buffer.Dequeue() assert.Equal(8, value) assert.Equal(0, buffer.Len()) - assert.Nil(buffer.Peek()) - assert.Nil(buffer.PeekBack()) + assert.Zero(buffer.Peek()) + assert.Zero(buffer.PeekBack()) } func TestRingBufferClear(t *testing.T) { assert := assert.New(t) - buffer := NewRingBuffer() + buffer := NewValueBuffer() buffer.Enqueue(1) buffer.Enqueue(1) buffer.Enqueue(1) @@ -117,21 +117,21 @@ func TestRingBufferClear(t *testing.T) { buffer.Clear() assert.Equal(0, buffer.Len()) - assert.Nil(buffer.Peek()) - assert.Nil(buffer.PeekBack()) + assert.Zero(buffer.Peek()) + assert.Zero(buffer.PeekBack()) } func TestRingBufferAsSlice(t *testing.T) { assert := assert.New(t) - buffer := NewRingBuffer() + buffer := NewValueBuffer() buffer.Enqueue(1) buffer.Enqueue(2) buffer.Enqueue(3) buffer.Enqueue(4) buffer.Enqueue(5) - contents := buffer.AsSlice() + contents := buffer.Array() assert.Len(contents, 5) assert.Equal(1, contents[0]) assert.Equal(2, contents[1]) @@ -143,20 +143,40 @@ func TestRingBufferAsSlice(t *testing.T) { func TestRingBufferEach(t *testing.T) { assert := assert.New(t) - buffer := NewRingBuffer() + buffer := NewValueBuffer() for x := 1; x < 17; x++ { - buffer.Enqueue(x) + buffer.Enqueue(float64(x)) } called := 0 - buffer.Each(func(v interface{}) { - if typed, isTyped := v.(int); isTyped { - if typed == (called + 1) { - called++ - } + buffer.Each(func(_ int, v float64) { + if v == float64(called+1) { + called++ } }) assert.Equal(16, called) } + +func TestNewValueBuffer(t *testing.T) { + assert := assert.New(t) + + empty := NewValueBuffer() + assert.NotNil(empty) + assert.Zero(empty.Len()) + assert.Equal(valueBufferDefaultCapacity, empty.Capacity()) + assert.Zero(empty.Peek()) + assert.Zero(empty.PeekBack()) +} + +func TestNewValueBufferWithValues(t *testing.T) { + assert := assert.New(t) + + values := NewValueBuffer(1, 2, 3, 4) + assert.NotNil(values) + assert.Equal(4, values.Len()) + assert.Equal(valueBufferDefaultCapacity, values.Capacity()) + assert.Equal(1, values.Peek()) + assert.Equal(4, values.PeekBack()) +} diff --git a/value_provider.go b/value_provider.go index f6e7824..e93c30d 100644 --- a/value_provider.go +++ b/value_provider.go @@ -2,38 +2,38 @@ package chart import "github.com/wcharczuk/go-chart/drawing" -// ValueProvider is a type that produces values. -type ValueProvider interface { +// ValuesProvider is a type that produces values. +type ValuesProvider interface { Len() int - GetValue(index int) (float64, float64) + GetValues(index int) (float64, float64) } -// BoundedValueProvider allows series to return a range. -type BoundedValueProvider interface { +// BoundedValuesProvider allows series to return a range. +type BoundedValuesProvider interface { Len() int - GetBoundedValue(index int) (x, y1, y2 float64) + GetBoundedValues(index int) (x, y1, y2 float64) } -// LastValueProvider is a special type of value provider that can return it's (potentially computed) last value. -type LastValueProvider interface { - GetLastValue() (x, y float64) +// LastValuesProvider is a special type of value provider that can return it's (potentially computed) last value. +type LastValuesProvider interface { + GetLastValues() (x, y float64) } -// BoundedLastValueProvider is a special type of value provider that can return it's (potentially computed) bounded last value. -type BoundedLastValueProvider interface { - GetBoundedLastValue() (x, y1, y2 float64) +// BoundedLastValuesProvider is a special type of value provider that can return it's (potentially computed) bounded last value. +type BoundedLastValuesProvider interface { + GetBoundedLastValues() (x, y1, y2 float64) } -// FullValueProvider is an interface that combines `ValueProvider` and `LastValueProvider` -type FullValueProvider interface { - ValueProvider - LastValueProvider +// FullValuesProvider is an interface that combines `ValuesProvider` and `LastValuesProvider` +type FullValuesProvider interface { + ValuesProvider + LastValuesProvider } -// FullBoundedValueProvider is an interface that combines `BoundedValueProvider` and `BoundedLastValueProvider` -type FullBoundedValueProvider interface { - BoundedValueProvider - BoundedLastValueProvider +// FullBoundedValuesProvider is an interface that combines `BoundedValuesProvider` and `BoundedLastValuesProvider` +type FullBoundedValuesProvider interface { + BoundedValuesProvider + BoundedLastValuesProvider } // SizeProvider is a provider for integer size.