From 5f42a580a98589e4f0acf5658ce3f0e7bc59df3f Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Wed, 13 Feb 2019 18:55:13 -0800 Subject: [PATCH] mostly working --- _examples/custom_padding/main.go | 4 +- _examples/linear_regression/main.go | 2 +- _examples/min_max/main.go | 2 +- _examples/poly_regression/main.go | 2 +- _examples/request_timings/main.go | 145 +++++++++--------- _examples/scatter/main.go | 11 +- _examples/simple_moving_average/main.go | 7 +- _examples/stacked_bar/main.go | 2 +- _examples/stock_analysis/main.go | 2 +- _examples/text_rotation/main.go | 2 +- _examples/timeseries/main.go | 2 +- _examples/twoaxis/main.go | 5 +- _examples/twopoint/main.go | 2 +- annotation_series.go | 4 +- annotation_series_test.go | 2 - array.go | 24 +++ bar_chart.go | 14 +- bar_chart_test.go | 30 ++-- bollinger_band_series.go | 4 +- bollinger_band_series_test.go | 8 +- chart.go | 54 +++---- chart_test.go | 63 ++++---- cmd/chart/main.go | 132 ++++++----------- concat_series_test.go | 12 +- continuous_range.go | 3 + continuous_series.go | 8 +- continuous_series_test.go | 12 +- ema_series_test.go | 3 +- histogram_series_test.go | 5 +- legend.go | 6 +- linear_regression_series_test.go | 13 +- linear_sequence.go | 73 +++++++++ logger.go | 129 ++++++++++++++++ parse.go | 8 +- pie_chart.go | 2 +- random_sequence.go | 92 ++++++++++++ seq.go | 178 ++-------------------- seq_test.go | 136 +++++++++++++++++ sma_series_test.go | 13 +- stacked_bar_chart.go | 10 +- style.go | 27 ++-- times.go | 43 ++++++ timeutil.go | 45 ++++++ value_buffer.go | 4 +- value_buffer_test.go | 188 ++++++------------------ xaxis.go | 8 +- yaxis.go | 10 +- 47 files changed, 914 insertions(+), 637 deletions(-) create mode 100644 array.go create mode 100644 linear_sequence.go create mode 100644 logger.go create mode 100644 random_sequence.go create mode 100644 seq_test.go create mode 100644 times.go diff --git a/_examples/custom_padding/main.go b/_examples/custom_padding/main.go index 0de71d9..a705054 100644 --- a/_examples/custom_padding/main.go +++ b/_examples/custom_padding/main.go @@ -27,7 +27,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { }, Series: []chart.Series{ chart.ContinuousSeries{ - XValues: seq.Range(1.0, 100.0), + XValues: SeqRange(1.0, 100.0), YValues: seq.RandomValuesWithMax(100, 512), }, }, @@ -50,7 +50,7 @@ func drawChartDefault(res http.ResponseWriter, req *http.Request) { }, Series: []chart.Series{ chart.ContinuousSeries{ - XValues: seq.Range(1.0, 100.0), + XValues: SeqRange(1.0, 100.0), YValues: seq.RandomValuesWithMax(100, 512), }, }, diff --git a/_examples/linear_regression/main.go b/_examples/linear_regression/main.go index eb4cc60..9daf3a6 100644 --- a/_examples/linear_regression/main.go +++ b/_examples/linear_regression/main.go @@ -16,7 +16,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { mainSeries := chart.ContinuousSeries{ Name: "A test series", - XValues: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + XValues: SeqRange(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. YValues: seq.RandomValuesWithMax(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements. } diff --git a/_examples/min_max/main.go b/_examples/min_max/main.go index a0cb4b1..cd112a0 100644 --- a/_examples/min_max/main.go +++ b/_examples/min_max/main.go @@ -10,7 +10,7 @@ import ( func drawChart(res http.ResponseWriter, req *http.Request) { mainSeries := chart.ContinuousSeries{ Name: "A test series", - XValues: seq.Range(1.0, 100.0), + XValues: SeqRange(1.0, 100.0), YValues: seq.New(seq.NewRandom().WithLen(100).WithMax(150).WithMin(50)).Array(), } diff --git a/_examples/poly_regression/main.go b/_examples/poly_regression/main.go index 27a39a3..81a7a27 100644 --- a/_examples/poly_regression/main.go +++ b/_examples/poly_regression/main.go @@ -16,7 +16,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { mainSeries := chart.ContinuousSeries{ Name: "A test series", - XValues: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + XValues: SeqRange(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. YValues: seq.RandomValuesWithMax(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements. } diff --git a/_examples/request_timings/main.go b/_examples/request_timings/main.go index ccea8e4..b1a8929 100644 --- a/_examples/request_timings/main.go +++ b/_examples/request_timings/main.go @@ -4,7 +4,6 @@ import ( "fmt" "net/http" "strconv" - "strings" "time" chart "github.com/wcharczuk/go-chart" @@ -24,7 +23,7 @@ func readData() ([]time.Time, []float64) { var xvalues []time.Time var yvalues []float64 err := chart.ReadLines("requests.csv", func(line string) error { - parts := strings.Split(line, ",") + parts := chart.SplitCSV(line) year := parseInt(parts[0]) month := parseInt(parts[1]) day := parseInt(parts[2]) @@ -51,84 +50,84 @@ func releases() []chart.GridLine { } } -func drawChart(res http.ResponseWriter, req *http.Request) { - xvalues, yvalues := readData() - mainSeries := chart.TimeSeries{ - Name: "Prod Request Timings", - Style: chart.Style{ - Show: true, - StrokeColor: chart.ColorBlue, - FillColor: chart.ColorBlue.WithAlpha(100), - }, - XValues: xvalues, - YValues: yvalues, - } - - linreg := &chart.LinearRegressionSeries{ - Name: "Linear Regression", - Style: chart.Style{ - Show: true, - StrokeColor: chart.ColorAlternateBlue, - StrokeDashArray: []float64{5.0, 5.0}, - }, - InnerSeries: mainSeries, - } - - sma := &chart.SMASeries{ - Name: "SMA", - Style: chart.Style{ - Show: true, - StrokeColor: chart.ColorRed, - StrokeDashArray: []float64{5.0, 5.0}, - }, - InnerSeries: mainSeries, - } - - graph := chart.Chart{ - Width: 1280, - Height: 720, - Background: chart.Style{ - Padding: chart.Box{ - Top: 50, +func drawChart(log chart.Logger) http.HandlerFunc { + return func(res http.ResponseWriter, req *http.Request) { + xvalues, yvalues := readData() + mainSeries := chart.TimeSeries{ + Name: "Prod Request Timings", + Style: chart.Style{ + StrokeColor: chart.ColorBlue, + FillColor: chart.ColorBlue.WithAlpha(100), }, - }, - YAxis: chart.YAxis{ - Name: "Elapsed Millis", - NameStyle: chart.StyleShow(), - Style: chart.StyleShow(), - TickStyle: chart.Style{ - TextRotationDegrees: 45.0, + XValues: xvalues, + YValues: yvalues, + } + + linreg := &chart.LinearRegressionSeries{ + Name: "Linear Regression", + Style: chart.Style{ + StrokeColor: chart.ColorAlternateBlue, + StrokeDashArray: []float64{5.0, 5.0}, }, - ValueFormatter: func(v interface{}) string { - return fmt.Sprintf("%d ms", int(v.(float64))) + InnerSeries: mainSeries, + } + + sma := &chart.SMASeries{ + Name: "SMA", + Style: chart.Style{ + StrokeColor: chart.ColorRed, + StrokeDashArray: []float64{5.0, 5.0}, }, - }, - XAxis: chart.XAxis{ - Style: chart.StyleShow(), - ValueFormatter: chart.TimeHourValueFormatter, - GridMajorStyle: chart.Style{ - Show: true, - StrokeColor: chart.ColorAlternateGray, - StrokeWidth: 1.0, + InnerSeries: mainSeries, + } + + graph := chart.Chart{ + Log: log, + Width: 1280, + Height: 720, + Background: chart.Style{ + Padding: chart.Box{ + Top: 50, + }, }, - GridLines: releases(), - }, - Series: []chart.Series{ - mainSeries, - linreg, - chart.LastValueAnnotation(linreg), - sma, - chart.LastValueAnnotation(sma), - }, + YAxis: chart.YAxis{ + Name: "Elapsed Millis", + TickStyle: chart.Style{ + TextRotationDegrees: 45.0, + }, + ValueFormatter: func(v interface{}) string { + return fmt.Sprintf("%d ms", int(v.(float64))) + }, + }, + XAxis: chart.XAxis{ + ValueFormatter: chart.TimeHourValueFormatter, + GridMajorStyle: chart.Style{ + StrokeColor: chart.ColorAlternateGray, + StrokeWidth: 1.0, + }, + GridLines: releases(), + }, + Series: []chart.Series{ + mainSeries, + linreg, + chart.LastValueAnnotation(linreg), + sma, + chart.LastValueAnnotation(sma), + }, + } + + graph.Elements = []chart.Renderable{chart.LegendThin(&graph)} + + res.Header().Set("Content-Type", chart.ContentTypePNG) + if err := graph.Render(chart.PNG, res); err != nil { + log.Err(err) + } } - - graph.Elements = []chart.Renderable{chart.LegendThin(&graph)} - - res.Header().Set("Content-Type", chart.ContentTypePNG) - graph.Render(chart.PNG, res) } func main() { - http.HandleFunc("/", drawChart) + log := chart.NewLogger() + log.Infof("listening on :8080") + http.HandleFunc("/", drawChart(log)) http.ListenAndServe(":8080", nil) } diff --git a/_examples/scatter/main.go b/_examples/scatter/main.go index e20539a..99ba2e0 100644 --- a/_examples/scatter/main.go +++ b/_examples/scatter/main.go @@ -6,9 +6,8 @@ import ( _ "net/http/pprof" - "github.com/wcharczuk/go-chart" + chart "github.com/wcharczuk/go-chart" "github.com/wcharczuk/go-chart/drawing" - "github.com/wcharczuk/go-chart/seq" ) func drawChart(res http.ResponseWriter, req *http.Request) { @@ -26,8 +25,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) { DotWidth: 5, DotColorProvider: viridisByY, }, - XValues: seq.Range(0, 127), - YValues: seq.New(seq.NewRandom().WithLen(128).WithMax(1024)).Array(), + XValues: chart.SeqRange(0, 127), + YValues: chart.NewSeq(chart.NewSeqRandom().WithLen(128).WithMax(1024)).Values(), }, }, } @@ -51,8 +50,8 @@ func unit(res http.ResponseWriter, req *http.Request) { }, Series: []chart.Series{ chart.ContinuousSeries{ - XValues: seq.RangeWithStep(0, 4, 1), - YValues: seq.RangeWithStep(0, 4, 1), + XValues: chart.SeqRangeWithStep(0, 4, 1), + YValues: chart.SeqRangeWithStep(0, 4, 1), }, }, } diff --git a/_examples/simple_moving_average/main.go b/_examples/simple_moving_average/main.go index 3020b0a..485dacb 100644 --- a/_examples/simple_moving_average/main.go +++ b/_examples/simple_moving_average/main.go @@ -3,16 +3,15 @@ package main import ( "net/http" - "github.com/wcharczuk/go-chart" - "github.com/wcharczuk/go-chart/seq" + chart "github.com/wcharczuk/go-chart" ) func drawChart(res http.ResponseWriter, req *http.Request) { mainSeries := chart.ContinuousSeries{ Name: "A test series", - XValues: seq.Range(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. - YValues: seq.RandomValuesWithMax(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements. + XValues: chart.SeqRange(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + YValues: chart.SeqRandomValuesWithMax(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/_examples/stacked_bar/main.go b/_examples/stacked_bar/main.go index 3a6f22e..3f5015d 100644 --- a/_examples/stacked_bar/main.go +++ b/_examples/stacked_bar/main.go @@ -5,7 +5,7 @@ import ( "log" "net/http" - "github.com/wcharczuk/go-chart" + chart "github.com/wcharczuk/go-chart" ) func drawChart(res http.ResponseWriter, req *http.Request) { diff --git a/_examples/stock_analysis/main.go b/_examples/stock_analysis/main.go index 397e70f..9e28889 100644 --- a/_examples/stock_analysis/main.go +++ b/_examples/stock_analysis/main.go @@ -4,7 +4,7 @@ import ( "net/http" "time" - "github.com/wcharczuk/go-chart" + chart "github.com/wcharczuk/go-chart" "github.com/wcharczuk/go-chart/drawing" ) diff --git a/_examples/text_rotation/main.go b/_examples/text_rotation/main.go index 76bb2b0..4fa60ef 100644 --- a/_examples/text_rotation/main.go +++ b/_examples/text_rotation/main.go @@ -3,7 +3,7 @@ package main import ( "net/http" - "github.com/wcharczuk/go-chart" + chart "github.com/wcharczuk/go-chart" "github.com/wcharczuk/go-chart/drawing" ) diff --git a/_examples/timeseries/main.go b/_examples/timeseries/main.go index b77f554..c2e05ff 100644 --- a/_examples/timeseries/main.go +++ b/_examples/timeseries/main.go @@ -4,7 +4,7 @@ import ( "net/http" "time" - "github.com/wcharczuk/go-chart" + chart "github.com/wcharczuk/go-chart" ) func drawChart(res http.ResponseWriter, req *http.Request) { diff --git a/_examples/twoaxis/main.go b/_examples/twoaxis/main.go index 471cf85..a3ba063 100644 --- a/_examples/twoaxis/main.go +++ b/_examples/twoaxis/main.go @@ -4,8 +4,7 @@ import ( "fmt" "net/http" - "github.com/wcharczuk/go-chart" - util "github.com/wcharczuk/go-chart/util" + chart "github.com/wcharczuk/go-chart" ) func drawChart(res http.ResponseWriter, req *http.Request) { @@ -22,7 +21,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { TickPosition: chart.TickPositionBetweenTicks, ValueFormatter: func(v interface{}) string { typed := v.(float64) - typedDate := util.Time.FromFloat64(typed) + typedDate := chart.TimeFromFloat64(typed) return fmt.Sprintf("%d-%d\n%d", typedDate.Month(), typedDate.Day(), typedDate.Year()) }, }, diff --git a/_examples/twopoint/main.go b/_examples/twopoint/main.go index fc49641..9030f76 100644 --- a/_examples/twopoint/main.go +++ b/_examples/twopoint/main.go @@ -5,7 +5,7 @@ import ( "log" "os" - "github.com/wcharczuk/go-chart" + chart "github.com/wcharczuk/go-chart" ) func main() { diff --git a/annotation_series.go b/annotation_series.go index a251622..96e78f9 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -53,7 +53,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran Right: 0, Bottom: 0, } - if as.Style.IsZero() || as.Style.Show { + if !as.Style.Hidden { seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) for _, a := range as.Annotations { style := a.Style.InheritFrom(seriesStyle) @@ -71,7 +71,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran // Render draws the series. func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - if as.Style.IsZero() || as.Style.Show { + if !as.Style.Hidden { seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) for _, a := range as.Annotations { style := a.Style.InheritFrom(seriesStyle) diff --git a/annotation_series_test.go b/annotation_series_test.go index 64ab4db..f5cd2da 100644 --- a/annotation_series_test.go +++ b/annotation_series_test.go @@ -13,7 +13,6 @@ func TestAnnotationSeriesMeasure(t *testing.T) { assert := assert.New(t) as := AnnotationSeries{ - Style: StyleShow(), Annotations: []Value2{ {XValue: 1.0, YValue: 1.0, Label: "1.0"}, {XValue: 2.0, YValue: 2.0, Label: "2.0"}, @@ -63,7 +62,6 @@ func TestAnnotationSeriesRender(t *testing.T) { as := AnnotationSeries{ Style: Style{ - Show: true, FillColor: drawing.ColorWhite, StrokeColor: drawing.ColorBlack, }, diff --git a/array.go b/array.go new file mode 100644 index 0000000..71b3ee7 --- /dev/null +++ b/array.go @@ -0,0 +1,24 @@ +package chart + +var ( + _ Sequence = (*Array)(nil) +) + +// NewArray returns a new array from a given set of values. +// Array implements Sequence, which allows it to be used with the sequence helpers. +func NewArray(values ...float64) Array { + return Array(values) +} + +// 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/bar_chart.go b/bar_chart.go index e6b9936..d61f3db 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -224,7 +224,7 @@ func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) { } func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) { - if bc.XAxis.Show { + if !bc.XAxis.Hidden { axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes()) axisStyle.WriteToRenderer(r) @@ -263,7 +263,7 @@ func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) { } func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) { - if bc.YAxis.Style.Show { + if !bc.YAxis.Style.Hidden { axisStyle := bc.YAxis.Style.InheritFrom(bc.styleDefaultsAxes()) axisStyle.WriteToRenderer(r) @@ -294,7 +294,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) } func (bc BarChart) drawTitle(r Renderer) { - if len(bc.Title) > 0 && bc.TitleStyle.Show { + if len(bc.Title) > 0 && !bc.TitleStyle.Hidden { r.SetFont(bc.TitleStyle.GetFont(bc.GetFont())) r.SetFontColor(bc.TitleStyle.GetFontColor(bc.GetColorPalette().TextColor())) titleFontSize := bc.TitleStyle.GetFontSize(bc.getTitleFontSize()) @@ -325,7 +325,7 @@ func (bc BarChart) styleDefaultsCanvas() Style { } func (bc BarChart) hasAxes() bool { - return bc.YAxis.Style.Show + return !bc.YAxis.Style.Hidden } func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range { @@ -345,7 +345,7 @@ func (bc BarChart) getValueFormatters() ValueFormatter { } func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) { - if bc.YAxis.Style.Show { + if !bc.YAxis.Style.Hidden { yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf) } return @@ -391,7 +391,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range, _, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox) - if bc.XAxis.Show { + if !bc.XAxis.Hidden { xaxisHeight := DefaultVerticalTickHeight axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes()) @@ -423,7 +423,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range, axesOuterBox = axesOuterBox.Grow(xbox) } - if bc.YAxis.Style.Show { + if !bc.YAxis.Style.Hidden { axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks) axesOuterBox = axesOuterBox.Grow(axesBounds) } diff --git a/bar_chart_test.go b/bar_chart_test.go index 14dc231..8a25cae 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -12,13 +12,8 @@ func TestBarChartRender(t *testing.T) { assert := assert.New(t) bc := BarChart{ - Width: 1024, - Title: "Test Title", - TitleStyle: StyleShow(), - XAxis: StyleShow(), - YAxis: YAxis{ - Style: StyleShow(), - }, + Width: 1024, + Title: "Test Title", Bars: []Value{ {Value: 1.0, Label: "One"}, {Value: 2.0, Label: "Two"}, @@ -38,13 +33,8 @@ func TestBarChartRenderZero(t *testing.T) { assert := assert.New(t) bc := BarChart{ - Width: 1024, - Title: "Test Title", - TitleStyle: StyleShow(), - XAxis: StyleShow(), - YAxis: YAxis{ - Style: StyleShow(), - }, + Width: 1024, + Title: "Test Title", Bars: []Value{ {Value: 0.0, Label: "One"}, {Value: 0.0, Label: "Two"}, @@ -183,12 +173,11 @@ func TestBarChartHasAxes(t *testing.T) { assert := assert.New(t) bc := BarChart{} - assert.False(bc.hasAxes()) - bc.YAxis = YAxis{ - Style: StyleShow(), - } - assert.True(bc.hasAxes()) + bc.YAxis = YAxis{ + Style: Hidden(), + } + assert.False(bc.hasAxes()) } func TestBarChartGetDefaultCanvasBox(t *testing.T) { @@ -237,10 +226,11 @@ func TestBarChartGetAxesTicks(t *testing.T) { yr := bc.getRanges() yf := bc.getValueFormatters() + bc.YAxis.Style.Hidden = true ticks := bc.getAxesTicks(r, yr, yf) assert.Empty(ticks) - bc.YAxis.Style.Show = true + bc.YAxis.Style.Hidden = false ticks = bc.getAxesTicks(r, yr, yf) assert.Len(ticks, 2) } diff --git a/bollinger_band_series.go b/bollinger_band_series.go index 6451f13..728b232 100644 --- a/bollinger_band_series.go +++ b/bollinger_band_series.go @@ -79,8 +79,8 @@ func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) bbs.valueBuffer.Enqueue(py) x = px - ay := NewSeq(bbs.valueBuffer).Average() - std := NewSeq(bbs.valueBuffer).StdDev() + ay := Seq{bbs.valueBuffer}.Average() + std := Seq{bbs.valueBuffer}.StdDev() y1 = ay + (bbs.GetK() * std) y2 = ay - (bbs.GetK() * std) diff --git a/bollinger_band_series_test.go b/bollinger_band_series_test.go index 6afa71b..d679960 100644 --- a/bollinger_band_series_test.go +++ b/bollinger_band_series_test.go @@ -12,8 +12,8 @@ func TestBollingerBandSeries(t *testing.T) { assert := assert.New(t) s1 := mockValuesProvider{ - X: SeqRange(1.0, 100.0), - Y: SeqRandomValuesWithMax(100, 1024), + X: LinearRange(1.0, 100.0), + Y: RandomValuesWithMax(100, 1024), } bbs := &BollingerBandsSeries{ @@ -37,8 +37,8 @@ func TestBollingerBandLastValue(t *testing.T) { assert := assert.New(t) s1 := mockValuesProvider{ - X: SeqRange(1.0, 100.0), - Y: SeqRange(1.0, 100.0), + X: LinearRange(1.0, 100.0), + Y: LinearRange(1.0, 100.0), } bbs := &BollingerBandsSeries{ diff --git a/chart.go b/chart.go index 986318a..411a9c6 100644 --- a/chart.go +++ b/chart.go @@ -32,6 +32,8 @@ type Chart struct { Series []Series Elements []Renderable + + Log Logger } // GetDPI returns the dpi for the chart. @@ -74,8 +76,8 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error { if len(c.Series) == 0 { return errors.New("please provide at least one series") } - if visibleSeriesErr := c.checkHasVisibleSeries(); visibleSeriesErr != nil { - return visibleSeriesErr + if err := c.checkHasVisibleSeries(); err != nil { + return err } c.YAxisSecondary.AxisType = YAxisSecondary @@ -142,16 +144,14 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error { } func (c Chart) checkHasVisibleSeries() error { - hasVisibleSeries := false var style Style for _, s := range c.Series { style = s.GetStyle() - hasVisibleSeries = hasVisibleSeries || (style.IsZero() || style.Show) + if !style.Hidden { + return nil + } } - if !hasVisibleSeries { - return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true") - } - return nil + return fmt.Errorf("chart render; must have (1) visible series") } func (c Chart) validateSeries() error { @@ -175,7 +175,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { // note: a possible future optimization is to not scan the series values if // all axis are represented by either custom ticks or custom ranges. for _, s := range c.Series { - if s.GetStyle().IsZero() || s.GetStyle().Show { + if !s.GetStyle().Hidden { seriesAxis := s.GetYAxis() if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider { seriesLength := bvp.Len() @@ -262,8 +262,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { yrange.SetMin(miny) yrange.SetMax(maxy) - // only round if we're showing the axis - if c.YAxis.Style.Show { + if !c.YAxis.Style.Hidden { delta := yrange.GetDelta() roundTo := GetRoundToForDelta(delta) rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo) @@ -285,7 +284,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { yrangeAlt.SetMin(minya) yrangeAlt.SetMax(maxya) - if c.YAxisSecondary.Style.Show { + if !c.YAxisSecondary.Style.Hidden { delta := yrangeAlt.GetDelta() roundTo := GetRoundToForDelta(delta) rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo) @@ -298,6 +297,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { } func (c Chart) checkRanges(xr, yr, yra Range) error { + Debugf(c.Log, "checking xrange: %v", xr) xDelta := xr.GetDelta() if math.IsInf(xDelta, 0) { return errors.New("infinite x-range delta") @@ -309,6 +309,7 @@ func (c Chart) checkRanges(xr, yr, yra Range) error { return errors.New("zero x-range delta; there needs to be at least (2) values") } + Debugf(c.Log, "checking yrange: %v", yr) yDelta := yr.GetDelta() if math.IsInf(yDelta, 0) { return errors.New("infinite y-range delta") @@ -318,6 +319,7 @@ func (c Chart) checkRanges(xr, yr, yra Range) error { } if c.hasSecondarySeries() { + Debugf(c.Log, "checking secondary yrange: %v", yra) yraDelta := yra.GetDelta() if math.IsInf(yraDelta, 0) { return errors.New("infinite secondary y-range delta") @@ -360,17 +362,17 @@ func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) { } func (c Chart) hasAxes() bool { - return c.XAxis.Style.Show || c.YAxis.Style.Show || c.YAxisSecondary.Style.Show + return !c.XAxis.Style.Hidden || !c.YAxis.Style.Hidden || !c.YAxisSecondary.Style.Hidden } func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueFormatter) (xticks, yticks, yticksAlt []Tick) { - if c.XAxis.Style.Show { + if !c.XAxis.Style.Hidden { xticks = c.XAxis.GetTicks(r, xr, c.styleDefaultsAxes(), xf) } - if c.YAxis.Style.Show { + if !c.YAxis.Style.Hidden { yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf) } - if c.YAxisSecondary.Style.Show { + if !c.YAxisSecondary.Style.Hidden { yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa) } return @@ -378,15 +380,15 @@ func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueForm func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box { axesOuterBox := canvasBox.Clone() - if c.XAxis.Style.Show { + if !c.XAxis.Style.Hidden { axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks) axesOuterBox = axesOuterBox.Grow(axesBounds) } - if c.YAxis.Style.Show { + if !c.YAxis.Style.Hidden { axesBounds := c.YAxis.Measure(r, canvasBox, yr, c.styleDefaultsAxes(), yticks) axesOuterBox = axesOuterBox.Grow(axesBounds) } - if c.YAxisSecondary.Style.Show { + if !c.YAxisSecondary.Style.Hidden { axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, c.styleDefaultsAxes(), yticksAlt) axesOuterBox = axesOuterBox.Grow(axesBounds) } @@ -404,7 +406,7 @@ func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range, func (c Chart) hasAnnotationSeries() bool { for _, s := range c.Series { if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries { - if as.Style.IsZero() || as.Style.Show { + if !as.GetStyle().Hidden { return true } } @@ -425,7 +427,7 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, annotationSeriesBox := canvasBox.Clone() for seriesIndex, s := range c.Series { if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries { - if as.Style.IsZero() || as.Style.Show { + if !as.GetStyle().Hidden { style := c.styleDefaultsSeries(seriesIndex) var annotationBounds Box if as.YAxis == YAxisPrimary { @@ -462,19 +464,19 @@ func (c Chart) drawCanvas(r Renderer, canvasBox Box) { } func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) { - if c.XAxis.Style.Show { + if !c.XAxis.Style.Hidden { c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks) } - if c.YAxis.Style.Show { + if !c.YAxis.Style.Hidden { c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks) } - if c.YAxisSecondary.Style.Show { + if !c.YAxisSecondary.Style.Hidden { c.YAxisSecondary.Render(r, canvasBox, yrangeAlt, c.styleDefaultsAxes(), yticksAlt) } } func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) { - if s.GetStyle().IsZero() || s.GetStyle().Show { + if !s.GetStyle().Hidden { if s.GetYAxis() == YAxisPrimary { s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex)) } else if s.GetYAxis() == YAxisSecondary { @@ -484,7 +486,7 @@ func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt R } func (c Chart) drawTitle(r Renderer) { - if len(c.Title) > 0 && c.TitleStyle.Show { + if len(c.Title) > 0 && !c.TitleStyle.Hidden { r.SetFont(c.TitleStyle.GetFont(c.GetFont())) r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor())) titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize) diff --git a/chart_test.go b/chart_test.go index 1f60c38..8810374 100644 --- a/chart_test.go +++ b/chart_test.go @@ -274,25 +274,44 @@ func TestChartGetValueFormatters(t *testing.T) { func TestChartHasAxes(t *testing.T) { assert := assert.New(t) - assert.False(Chart{}.hasAxes()) + assert.True(Chart{}.hasAxes()) + assert.False(Chart{XAxis: XAxis{Style: Hidden()}, YAxis: YAxis{Style: Hidden()}, YAxisSecondary: YAxis{Style: Hidden()}}.hasAxes()) x := Chart{ XAxis: XAxis{ - Style: StyleShow(), + Style: Hidden(), + }, + YAxis: YAxis{ + Style: Shown(), + }, + YAxisSecondary: YAxis{ + Style: Hidden(), }, } assert.True(x.hasAxes()) y := Chart{ + XAxis: XAxis{ + Style: Shown(), + }, YAxis: YAxis{ - Style: StyleShow(), + Style: Hidden(), + }, + YAxisSecondary: YAxis{ + Style: Hidden(), }, } assert.True(y.hasAxes()) ya := Chart{ + XAxis: XAxis{ + Style: Hidden(), + }, + YAxis: YAxis{ + Style: Hidden(), + }, YAxisSecondary: YAxis{ - Style: StyleShow(), + Style: Shown(), }, } assert.True(ya.hasAxes()) @@ -306,15 +325,12 @@ func TestChartGetAxesTicks(t *testing.T) { c := Chart{ XAxis: XAxis{ - Style: StyleShow(), Range: &ContinuousRange{Min: 9.8, Max: 19.8}, }, YAxis: YAxis{ - Style: StyleShow(), Range: &ContinuousRange{Min: 9.9, Max: 19.9}, }, YAxisSecondary: YAxis{ - Style: StyleShow(), Range: &ContinuousRange{Min: 9.7, Max: 19.7}, }, } @@ -330,20 +346,15 @@ func TestChartSingleSeries(t *testing.T) { assert := assert.New(t) now := time.Now() c := Chart{ - Title: "Hello!", - TitleStyle: StyleShow(), - Width: 1024, - Height: 400, + Title: "Hello!", + Width: 1024, + Height: 400, YAxis: YAxis{ - Style: StyleShow(), Range: &ContinuousRange{ Min: 0.0, Max: 4.0, }, }, - XAxis: XAxis{ - Style: StyleShow(), - }, Series: []Series{ TimeSeries{ Name: "goog", @@ -386,8 +397,8 @@ func TestChartRegressionBadRangesByUser(t *testing.T) { }, Series: []Series{ ContinuousSeries{ - XValues: SeqRange(1.0, 10.0), - YValues: SeqRange(1.0, 10.0), + XValues: LinearRange(1.0, 10.0), + YValues: LinearRange(1.0, 10.0), }, }, } @@ -402,8 +413,8 @@ func TestChartValidatesSeries(t *testing.T) { c := Chart{ Series: []Series{ ContinuousSeries{ - XValues: SeqRange(1.0, 10.0), - YValues: SeqRange(1.0, 10.0), + XValues: LinearRange(1.0, 10.0), + YValues: LinearRange(1.0, 10.0), }, }, } @@ -413,7 +424,7 @@ func TestChartValidatesSeries(t *testing.T) { c = Chart{ Series: []Series{ ContinuousSeries{ - XValues: SeqRange(1.0, 10.0), + XValues: LinearRange(1.0, 10.0), }, }, } @@ -483,8 +494,8 @@ func TestChartE2ELine(t *testing.T) { }, Series: []Series{ ContinuousSeries{ - XValues: seq.RangeWithStep(0, 4, 1), - YValues: seq.RangeWithStep(0, 4, 1), + XValues: LinearRangeWithStep(0, 4, 1), + YValues: LinearRangeWithStep(0, 4, 1), }, }, } @@ -524,16 +535,18 @@ func TestChartE2ELineWithFill(t *testing.T) { Series: []Series{ ContinuousSeries{ Style: Style{ - Show: true, StrokeColor: drawing.ColorBlue, FillColor: drawing.ColorRed, }, - XValues: seq.RangeWithStep(0, 4, 1), - YValues: seq.RangeWithStep(0, 4, 1), + XValues: LinearRangeWithStep(0, 4, 1), + YValues: LinearRangeWithStep(0, 4, 1), }, }, } + assert.Equal(5, len(c.Series[0].(ContinuousSeries).XValues)) + assert.Equal(5, len(c.Series[0].(ContinuousSeries).YValues)) + var buffer = &bytes.Buffer{} err := c.Render(PNG, buffer) assert.Nil(err) diff --git a/cmd/chart/main.go b/cmd/chart/main.go index 9ad23d3..216062a 100644 --- a/cmd/chart/main.go +++ b/cmd/chart/main.go @@ -3,118 +3,78 @@ package main import ( "flag" "fmt" - "io" "io/ioutil" "os" - "time" + "strings" chart "github.com/wcharczuk/go-chart" ) var ( outputPath = flag.String("output", "", "The output file") + inputFormat = flag.String("format", "csv", "The input format, either 'csv' or 'tsv' (defaults to 'csv')") + inputPath = flag.String("f", "", "The input file") disableLinreg = flag.Bool("disable-linreg", false, "If we should omit linear regressions") disableLastValues = flag.Bool("disable-last-values", false, "If we should omit last values") ) -// NewLogger returns a new logger. -func NewLogger() *Logger { - return &Logger{ - TimeFormat: time.RFC3339Nano, - Stdout: os.Stdout, - Stderr: os.Stderr, - } -} +func main() { + flag.Parse() + log := chart.NewLogger() -// Logger is a basic logger. -type Logger struct { - TimeFormat string - Stdout io.Writer - Stderr io.Writer -} - -// Info writes an info message. -func (l *Logger) Info(arguments ...interface{}) { - l.Println(append([]interface{}{"[INFO]"}, arguments...)...) -} - -// Infof writes an info message. -func (l *Logger) Infof(format string, arguments ...interface{}) { - l.Println(append([]interface{}{"[INFO]"}, fmt.Sprintf(format, arguments...))...) -} - -// Debug writes an debug message. -func (l *Logger) Debug(arguments ...interface{}) { - l.Println(append([]interface{}{"[DEBUG]"}, arguments...)...) -} - -// Debugf writes an debug message. -func (l *Logger) Debugf(format string, arguments ...interface{}) { - l.Println(append([]interface{}{"[DEBUG]"}, fmt.Sprintf(format, arguments...))...) -} - -// Error writes an error message. -func (l *Logger) Error(arguments ...interface{}) { - l.Println(append([]interface{}{"[ERROR]"}, arguments...)...) -} - -// Errorf writes an error message. -func (l *Logger) Errorf(format string, arguments ...interface{}) { - l.Println(append([]interface{}{"[ERROR]"}, fmt.Sprintf(format, arguments...))...) -} - -// Err writes an error message. -func (l *Logger) Err(err error) { - if err != nil { - l.Println(append([]interface{}{"[ERROR]"}, err.Error())...) - } -} - -// FatalErr writes an error message and exits. -func (l *Logger) FatalErr(err error) { - if err != nil { - l.Println(append([]interface{}{"[FATAL]"}, err.Error())...) + var rawData []byte + var err error + if *inputPath != "" { + if *inputPath == "-" { + rawData, err = ioutil.ReadAll(os.Stdin) + if err != nil { + log.FatalErr(err) + } + } else { + rawData, err = ioutil.ReadFile(*inputPath) + if err != nil { + log.FatalErr(err) + } + } + } else if len(flag.Args()) > 0 { + rawData = []byte(flag.Args()[0]) + } else { + flag.Usage() os.Exit(1) } -} -// Println prints a new message. -func (l *Logger) Println(arguments ...interface{}) { - fmt.Fprintln(l.Stdout, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...) -} + var parts []string + switch *inputFormat { + case "csv": + parts = chart.SplitCSV(string(rawData)) + case "tsv": + parts = strings.Split(string(rawData), "\t") + default: + log.FatalErr(fmt.Errorf("invalid format; must be 'csv' or 'tsv'")) + } -// Errorln prints a new message. -func (l *Logger) Errorln(arguments ...interface{}) { - fmt.Fprintln(l.Stderr, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...) -} - -func main() { - log := NewLogger() - - rawData, err := ioutil.ReadAll(os.Stdin) + yvalues, err := chart.ParseFloats(parts...) if err != nil { log.FatalErr(err) } - csvParts := chart.SplitCSV(string(rawData)) - - yvalues, err := chart.ParseFloats(csvParts...) - + var series []chart.Series mainSeries := chart.ContinuousSeries{ - Name: "A test series", - XValues: chart.SeqRange(0, float64(len(csvParts))), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements. + Name: "Values", + XValues: chart.LinearRange(1, float64(len(yvalues))), YValues: yvalues, } + series = append(series, mainSeries) - linRegSeries := &chart.LinearRegressionSeries{ - InnerSeries: mainSeries, + if !*disableLinreg { + linRegSeries := &chart.LinearRegressionSeries{ + InnerSeries: mainSeries, + } + series = append(series, linRegSeries) } graph := chart.Chart{ - Series: []chart.Series{ - mainSeries, - linRegSeries, - }, + Series: series, } var output *os.File @@ -130,8 +90,10 @@ func main() { } } - log.Info("rendering chart to", output.Name()) if err := graph.Render(chart.PNG, output); err != nil { log.FatalErr(err) } + + fmt.Fprintln(os.Stdout, output.Name()) + os.Exit(0) } diff --git a/concat_series_test.go b/concat_series_test.go index 2052bd5..fb6a55f 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: SeqRange(1.0, 10.0), - YValues: SeqRange(1.0, 10.0), + XValues: LinearRange(1.0, 10.0), + YValues: LinearRange(1.0, 10.0), } s2 := ContinuousSeries{ - XValues: SeqRange(11, 20.0), - YValues: SeqRange(10.0, 1.0), + XValues: LinearRange(11, 20.0), + YValues: LinearRange(10.0, 1.0), } s3 := ContinuousSeries{ - XValues: SeqRange(21, 30.0), - YValues: SeqRange(1.0, 10.0), + XValues: LinearRange(21, 30.0), + YValues: LinearRange(1.0, 10.0), } cs := ConcatSeries([]Series{s1, s2, s3}) diff --git a/continuous_range.go b/continuous_range.go index 99fa939..517b727 100644 --- a/continuous_range.go +++ b/continuous_range.go @@ -62,6 +62,9 @@ func (r *ContinuousRange) SetDomain(domain int) { // String returns a simple string for the ContinuousRange. func (r ContinuousRange) String() string { + if r.GetDelta() == 0 { + return "ContinuousRange [empty]" + } return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) } diff --git a/continuous_series.go b/continuous_series.go index 7e5ed2a..73c7ab7 100644 --- a/continuous_series.go +++ b/continuous_series.go @@ -82,11 +82,15 @@ func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Rang // Validate validates the series. func (cs ContinuousSeries) Validate() error { if len(cs.XValues) == 0 { - return fmt.Errorf("continuous series must have xvalues set") + return fmt.Errorf("continuous series; must have xvalues set") } if len(cs.YValues) == 0 { - return fmt.Errorf("continuous series must have yvalues set") + return fmt.Errorf("continuous series; must have yvalues set") + } + + if len(cs.XValues) != len(cs.YValues) { + return fmt.Errorf("continuous series; must have same length xvalues as yvalues") } return nil } diff --git a/continuous_series_test.go b/continuous_series_test.go index 2ae3928..17b6612 100644 --- a/continuous_series_test.go +++ b/continuous_series_test.go @@ -12,8 +12,8 @@ func TestContinuousSeries(t *testing.T) { cs := ContinuousSeries{ Name: "Test Series", - XValues: SeqRange(1.0, 10.0), - YValues: SeqRange(1.0, 10.0), + XValues: LinearRange(1.0, 10.0), + YValues: LinearRange(1.0, 10.0), } assert.Equal("Test Series", cs.GetName()) @@ -53,20 +53,20 @@ func TestContinuousSeriesValidate(t *testing.T) { cs := ContinuousSeries{ Name: "Test Series", - XValues: SeqRange(1.0, 10.0), - YValues: SeqRange(1.0, 10.0), + XValues: LinearRange(1.0, 10.0), + YValues: LinearRange(1.0, 10.0), } assert.Nil(cs.Validate()) cs = ContinuousSeries{ Name: "Test Series", - XValues: SeqRange(1.0, 10.0), + XValues: LinearRange(1.0, 10.0), } assert.NotNil(cs.Validate()) cs = ContinuousSeries{ Name: "Test Series", - YValues: SeqRange(1.0, 10.0), + YValues: LinearRange(1.0, 10.0), } assert.NotNil(cs.Validate()) } diff --git a/ema_series_test.go b/ema_series_test.go index 2e11336..90f0eae 100644 --- a/ema_series_test.go +++ b/ema_series_test.go @@ -4,11 +4,10 @@ import ( "testing" "github.com/blend/go-sdk/assert" - "github.com/wcharczuk/go-chart/seq" ) var ( - emaXValues = seq.Range(1.0, 50.0) + emaXValues = LinearRange(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/histogram_series_test.go b/histogram_series_test.go index 1f47bad..bd4f2d0 100644 --- a/histogram_series_test.go +++ b/histogram_series_test.go @@ -4,7 +4,6 @@ import ( "testing" assert "github.com/blend/go-sdk/assert" - "github.com/wcharczuk/go-chart/seq" ) func TestHistogramSeries(t *testing.T) { @@ -12,8 +11,8 @@ func TestHistogramSeries(t *testing.T) { cs := ContinuousSeries{ Name: "Test Series", - XValues: seq.Range(1.0, 20.0), - YValues: seq.Range(10.0, -10.0), + XValues: LinearRange(1.0, 20.0), + YValues: LinearRange(10.0, -10.0), } hs := HistogramSeries{ diff --git a/legend.go b/legend.go index 171c869..9d16ced 100644 --- a/legend.go +++ b/legend.go @@ -35,7 +35,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable { var labels []string var lines []Style for index, s := range c.Series { - if s.GetStyle().IsZero() || s.GetStyle().Show { + if !s.GetStyle().Hidden { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) @@ -149,7 +149,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable { var labels []string var lines []Style for index, s := range c.Series { - if s.GetStyle().IsZero() || s.GetStyle().Show { + if !s.GetStyle().Hidden { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) @@ -247,7 +247,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable { var labels []string var lines []Style for index, s := range c.Series { - if s.GetStyle().IsZero() || s.GetStyle().Show { + if !s.GetStyle().Hidden { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) diff --git a/linear_regression_series_test.go b/linear_regression_series_test.go index 335a636..0d04718 100644 --- a/linear_regression_series_test.go +++ b/linear_regression_series_test.go @@ -4,7 +4,6 @@ import ( "testing" assert "github.com/blend/go-sdk/assert" - "github.com/wcharczuk/go-chart/seq" ) func TestLinearRegressionSeries(t *testing.T) { @@ -12,8 +11,8 @@ func TestLinearRegressionSeries(t *testing.T) { mainSeries := ContinuousSeries{ Name: "A test series", - XValues: seq.Range(1.0, 100.0), - YValues: seq.Range(1.0, 100.0), + XValues: LinearRange(1.0, 100.0), + YValues: LinearRange(1.0, 100.0), } linRegSeries := &LinearRegressionSeries{ @@ -34,8 +33,8 @@ func TestLinearRegressionSeriesDesc(t *testing.T) { mainSeries := ContinuousSeries{ Name: "A test series", - XValues: seq.Range(100.0, 1.0), - YValues: seq.Range(100.0, 1.0), + XValues: LinearRange(100.0, 1.0), + YValues: LinearRange(100.0, 1.0), } linRegSeries := &LinearRegressionSeries{ @@ -56,8 +55,8 @@ func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) { mainSeries := ContinuousSeries{ Name: "A test series", - XValues: seq.Range(100.0, 1.0), - YValues: seq.Range(100.0, 1.0), + XValues: LinearRange(100.0, 1.0), + YValues: LinearRange(100.0, 1.0), } linRegSeries := &LinearRegressionSeries{ diff --git a/linear_sequence.go b/linear_sequence.go new file mode 100644 index 0000000..dda761b --- /dev/null +++ b/linear_sequence.go @@ -0,0 +1,73 @@ +package chart + +// LinearRange returns an array of values representing the range from start to end, incremented by 1.0. +func LinearRange(start, end float64) []float64 { + return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(1.0)}.Values() +} + +// LinearRangeWithStep returns the array values of a linear seq with a given start, end and optional step. +func LinearRangeWithStep(start, end, step float64) []float64 { + return Seq{NewLinearSequence().WithStart(start).WithEnd(end).WithStep(step)}.Values() +} + +// NewLinearSequence returns a new linear generator. +func NewLinearSequence() *LinearSeq { + return &LinearSeq{step: 1.0} +} + +// LinearSeq is a stepwise generator. +type LinearSeq struct { + start float64 + end float64 + step float64 +} + +// Start returns the start value. +func (lg LinearSeq) Start() float64 { + return lg.start +} + +// End returns the end value. +func (lg LinearSeq) End() float64 { + return lg.end +} + +// Step returns the step value. +func (lg LinearSeq) Step() float64 { + return lg.step +} + +// Len returns the number of elements in the seq. +func (lg LinearSeq) Len() int { + if lg.start < lg.end { + return int((lg.end-lg.start)/lg.step) + 1 + } + return int((lg.start-lg.end)/lg.step) + 1 +} + +// GetValue returns the value at a given index. +func (lg LinearSeq) GetValue(index int) float64 { + fi := float64(index) + if lg.start < lg.end { + return lg.start + (fi * lg.step) + } + return lg.start - (fi * lg.step) +} + +// WithStart sets the start and returns the linear generator. +func (lg *LinearSeq) WithStart(start float64) *LinearSeq { + lg.start = start + return lg +} + +// WithEnd sets the end and returns the linear generator. +func (lg *LinearSeq) WithEnd(end float64) *LinearSeq { + lg.end = end + return lg +} + +// WithStep sets the step and returns the linear generator. +func (lg *LinearSeq) WithStep(step float64) *LinearSeq { + lg.step = step + return lg +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..a2fa397 --- /dev/null +++ b/logger.go @@ -0,0 +1,129 @@ +package chart + +import ( + "fmt" + "io" + "os" + "time" +) + +var ( + _ Logger = (*StdoutLogger)(nil) +) + +// NewLogger returns a new logger. +func NewLogger() Logger { + return &StdoutLogger{ + TimeFormat: time.RFC3339Nano, + Stdout: os.Stdout, + Stderr: os.Stderr, + } +} + +// Logger is a type that implements the logging interface. +type Logger interface { + Info(...interface{}) + Infof(string, ...interface{}) + Debug(...interface{}) + Debugf(string, ...interface{}) + Err(error) + FatalErr(error) + Error(...interface{}) + Errorf(string, ...interface{}) + Println(...interface{}) + Errorln(...interface{}) +} + +// Info logs an info message if the logger is set. +func Info(log Logger, arguments ...interface{}) { + if log == nil { + return + } + log.Info(arguments...) +} + +// Infof logs an info message if the logger is set. +func Infof(log Logger, format string, arguments ...interface{}) { + if log == nil { + return + } + log.Infof(format, arguments...) +} + +// Debug logs an debug message if the logger is set. +func Debug(log Logger, arguments ...interface{}) { + if log == nil { + return + } + log.Debug(arguments...) +} + +// Debugf logs an debug message if the logger is set. +func Debugf(log Logger, format string, arguments ...interface{}) { + if log == nil { + return + } + log.Debugf(format, arguments...) +} + +// StdoutLogger is a basic logger. +type StdoutLogger struct { + TimeFormat string + Stdout io.Writer + Stderr io.Writer +} + +// Info writes an info message. +func (l *StdoutLogger) Info(arguments ...interface{}) { + l.Println(append([]interface{}{"[INFO]"}, arguments...)...) +} + +// Infof writes an info message. +func (l *StdoutLogger) Infof(format string, arguments ...interface{}) { + l.Println(append([]interface{}{"[INFO]"}, fmt.Sprintf(format, arguments...))...) +} + +// Debug writes an debug message. +func (l *StdoutLogger) Debug(arguments ...interface{}) { + l.Println(append([]interface{}{"[DEBUG]"}, arguments...)...) +} + +// Debugf writes an debug message. +func (l *StdoutLogger) Debugf(format string, arguments ...interface{}) { + l.Println(append([]interface{}{"[DEBUG]"}, fmt.Sprintf(format, arguments...))...) +} + +// Error writes an error message. +func (l *StdoutLogger) Error(arguments ...interface{}) { + l.Println(append([]interface{}{"[ERROR]"}, arguments...)...) +} + +// Errorf writes an error message. +func (l *StdoutLogger) Errorf(format string, arguments ...interface{}) { + l.Println(append([]interface{}{"[ERROR]"}, fmt.Sprintf(format, arguments...))...) +} + +// Err writes an error message. +func (l *StdoutLogger) Err(err error) { + if err != nil { + l.Println(append([]interface{}{"[ERROR]"}, err.Error())...) + } +} + +// FatalErr writes an error message and exits. +func (l *StdoutLogger) FatalErr(err error) { + if err != nil { + l.Println(append([]interface{}{"[FATAL]"}, err.Error())...) + os.Exit(1) + } +} + +// Println prints a new message. +func (l *StdoutLogger) Println(arguments ...interface{}) { + fmt.Fprintln(l.Stdout, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...) +} + +// Errorln prints a new message. +func (l *StdoutLogger) Errorln(arguments ...interface{}) { + fmt.Fprintln(l.Stderr, append([]interface{}{time.Now().UTC().Format(l.TimeFormat)}, arguments...)...) +} diff --git a/parse.go b/parse.go index 2d0f836..f38f6cc 100644 --- a/parse.go +++ b/parse.go @@ -2,6 +2,7 @@ package chart import ( "strconv" + "strings" "time" "github.com/blend/go-sdk/exception" @@ -12,8 +13,13 @@ func ParseFloats(values ...string) ([]float64, error) { var output []float64 var parsedValue float64 var err error + var cleaned string for _, value := range values { - if parsedValue, err = strconv.ParseFloat(value, 64); err != nil { + cleaned = strings.TrimSpace(strings.Replace(value, ",", "", -1)) + if cleaned == "" { + continue + } + if parsedValue, err = strconv.ParseFloat(cleaned, 64); err != nil { return nil, exception.New(err) } output = append(output, parsedValue) diff --git a/pie_chart.go b/pie_chart.go index 426ed50..49b551f 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -116,7 +116,7 @@ func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) { } func (pc PieChart) drawTitle(r Renderer) { - if len(pc.Title) > 0 && pc.TitleStyle.Show { + if len(pc.Title) > 0 && !pc.TitleStyle.Hidden { Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle()) } } diff --git a/random_sequence.go b/random_sequence.go new file mode 100644 index 0000000..45c9971 --- /dev/null +++ b/random_sequence.go @@ -0,0 +1,92 @@ +package chart + +import ( + "math" + "math/rand" + "time" +) + +var ( + _ Sequence = (*RandomSeq)(nil) +) + +// RandomValues returns an array of random values. +func RandomValues(count int) []float64 { + return Seq{NewRandomSequence().WithLen(count)}.Values() +} + +// RandomValuesWithMax returns an array of random values with a given average. +func RandomValuesWithMax(count int, max float64) []float64 { + return Seq{NewRandomSequence().WithMax(max).WithLen(count)}.Values() +} + +// NewRandomSequence creates a new random seq. +func NewRandomSequence() *RandomSeq { + return &RandomSeq{ + rnd: rand.New(rand.NewSource(time.Now().Unix())), + } +} + +// RandomSeq is a random number seq generator. +type RandomSeq struct { + rnd *rand.Rand + max *float64 + min *float64 + len *int +} + +// Len returns the number of elements that will be generated. +func (r *RandomSeq) Len() int { + if r.len != nil { + return *r.len + } + return math.MaxInt32 +} + +// GetValue returns the value. +func (r *RandomSeq) GetValue(_ int) float64 { + if r.min != nil && r.max != nil { + var delta float64 + + if *r.max > *r.min { + delta = *r.max - *r.min + } else { + delta = *r.min - *r.max + } + + return *r.min + (r.rnd.Float64() * delta) + } else if r.max != nil { + return r.rnd.Float64() * *r.max + } else if r.min != nil { + return *r.min + (r.rnd.Float64()) + } + return r.rnd.Float64() +} + +// WithLen sets a maximum len +func (r *RandomSeq) WithLen(length int) *RandomSeq { + r.len = &length + return r +} + +// Min returns the minimum value. +func (r RandomSeq) Min() *float64 { + return r.min +} + +// WithMin sets the scale and returns the Random. +func (r *RandomSeq) WithMin(min float64) *RandomSeq { + r.min = &min + return r +} + +// Max returns the maximum value. +func (r RandomSeq) Max() *float64 { + return r.max +} + +// WithMax sets the average and returns the Random. +func (r *RandomSeq) WithMax(max float64) *RandomSeq { + r.max = &max + return r +} diff --git a/seq.go b/seq.go index 7e493e0..aabdc99 100644 --- a/seq.go +++ b/seq.go @@ -3,19 +3,22 @@ package chart import ( "math" "sort" - "time" - - "github.com/blend/go-sdk/timeutil" ) -// NewSeq wraps a provider with a seq. -func NewSeq(provider SeqProvider) Seq { - return Seq{SeqProvider: provider} +// ValueSequence returns a sequence for a given values set. +func ValueSequence(values ...float64) Seq { + return Seq{NewArray(values...)} +} + +// Sequence is a provider for values for a seq. +type Sequence interface { + Len() int + GetValue(int) float64 } // Seq is a utility wrapper for seq providers. type Seq struct { - SeqProvider + Sequence } // Values enumerates the seq into a slice. @@ -45,7 +48,7 @@ func (s Seq) Map(mapfn func(i int, v float64) float64) Seq { for i := 0; i < s.Len(); i++ { mapfn(i, s.GetValue(i)) } - return Seq{SeqArray(output)} + return Seq{Array(output)} } // FoldLeft collapses a seq from left to right. @@ -142,7 +145,7 @@ func (s Seq) Sort() Seq { } values := s.Values() sort.Float64s(values) - return Seq{SeqArray(values)} + return Seq{Array(values)} } // Median returns the median or middle value in the sorted seq. @@ -247,160 +250,5 @@ func (s Seq) Normalize() Seq { output[i] = (s.GetValue(i) - min) / delta } - return Seq{SeqProvider: SeqArray(output)} -} - -// SeqProvider is a provider for values for a seq. -type SeqProvider interface { - Len() int - GetValue(int) float64 -} - -// SeqArray is a wrapper for an array of floats that implements `ValuesProvider`. -type SeqArray []float64 - -// Len returns the value provider length. -func (a SeqArray) Len() int { - return len(a) -} - -// GetValue returns the value at a given index. -func (a SeqArray) GetValue(index int) float64 { - return a[index] -} - -// SeqDays generates a seq 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 -} - -// SeqHours returns a sequence of times by the hour for a given number of hours -// after a given start. -func SeqHours(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 -} - -// SeqHoursFilled adds zero values for the data bounded by the start and end of the xdata array. -func SeqHoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { - start, end := TimeMinMax(xdata...) - totalHours := DiffHours(start, end) - - finalTimes := SeqHours(start, totalHours+1) - finalValues := make([]float64, totalHours+1) - - var hoursFromStart int - for i, xd := range xdata { - hoursFromStart = DiffHours(start, xd) - finalValues[hoursFromStart] = ydata[i] - } - - return finalTimes, finalValues -} - -// Assert types implement interfaces. -var ( - _ SeqProvider = (*SeqTimes)(nil) -) - -// SeqTimes are an array of times. -// It wraps the array with methods that implement `seq.Provider`. -type SeqTimes []time.Time - -// Array returns the times to an array. -func (t SeqTimes) Array() []time.Time { - return []time.Time(t) -} - -// Len returns the length of the array. -func (t SeqTimes) Len() int { - return len(t) -} - -// GetValue returns a value at an index as a time. -func (t SeqTimes) GetValue(index int) float64 { - return timeutil.ToFloat64(t[index]) -} - -// SeqRange returns the array values of a linear seq with a given start, end and optional step. -func SeqRange(start, end float64) []float64 { - return Seq{NewSeqLinear().WithStart(start).WithEnd(end).WithStep(1.0)}.Values() -} - -// SeqRangeWithStep returns the array values of a linear seq with a given start, end and optional step. -func SeqRangeWithStep(start, end, step float64) []float64 { - return Seq{NewSeqLinear().WithStart(start).WithEnd(end).WithStep(step)}.Values() -} - -// NewSeqLinear returns a new linear generator. -func NewSeqLinear() *SeqLinear { - return &SeqLinear{step: 1.0} -} - -// SeqLinear is a stepwise generator. -type SeqLinear struct { - start float64 - end float64 - step float64 -} - -// Start returns the start value. -func (lg SeqLinear) Start() float64 { - return lg.start -} - -// End returns the end value. -func (lg SeqLinear) End() float64 { - return lg.end -} - -// Step returns the step value. -func (lg SeqLinear) Step() float64 { - return lg.step -} - -// Len returns the number of elements in the seq. -func (lg SeqLinear) Len() int { - if lg.start < lg.end { - return int((lg.end-lg.start)/lg.step) + 1 - } - return int((lg.start-lg.end)/lg.step) + 1 -} - -// GetValue returns the value at a given index. -func (lg SeqLinear) GetValue(index int) float64 { - fi := float64(index) - if lg.start < lg.end { - return lg.start + (fi * lg.step) - } - return lg.start - (fi * lg.step) -} - -// WithStart sets the start and returns the linear generator. -func (lg *SeqLinear) WithStart(start float64) *SeqLinear { - lg.start = start - return lg -} - -// WithEnd sets the end and returns the linear generator. -func (lg *SeqLinear) WithEnd(end float64) *SeqLinear { - lg.end = end - return lg -} - -// WithStep sets the step and returns the linear generator. -func (lg *SeqLinear) WithStep(step float64) *SeqLinear { - lg.step = step - return lg + return Seq{Array(output)} } diff --git a/seq_test.go b/seq_test.go new file mode 100644 index 0000000..bd0ce22 --- /dev/null +++ b/seq_test.go @@ -0,0 +1,136 @@ +package chart + +import ( + "testing" + + assert "github.com/blend/go-sdk/assert" +) + +func TestSeqEach(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + values.Each(func(i int, v float64) { + assert.Equal(i, v-1) + }) +} + +func TestSeqMap(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + mapped := values.Map(func(i int, v float64) float64 { + assert.Equal(i, v-1) + return v * 2 + }) + assert.Equal(4, mapped.Len()) +} + +func TestSeqFoldLeft(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + ten := values.FoldLeft(func(_ int, vp, v float64) float64 { + return vp + v + }) + assert.Equal(10, ten) + + orderTest := Seq{NewArray(10, 3, 2, 1)} + four := orderTest.FoldLeft(func(_ int, vp, v float64) float64 { + return vp - v + }) + assert.Equal(4, four) +} + +func TestSeqFoldRight(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + ten := values.FoldRight(func(_ int, vp, v float64) float64 { + return vp + v + }) + assert.Equal(10, ten) + + orderTest := Seq{NewArray(10, 3, 2, 1)} + notFour := orderTest.FoldRight(func(_ int, vp, v float64) float64 { + return vp - v + }) + assert.Equal(-14, notFour) +} + +func TestSeqSum(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + assert.Equal(10, values.Sum()) +} + +func TestSeqAverage(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4)} + assert.Equal(2.5, values.Average()) + + valuesOdd := Seq{NewArray(1, 2, 3, 4, 5)} + assert.Equal(3, valuesOdd.Average()) +} + +func TestSequenceVariance(t *testing.T) { + assert := assert.New(t) + + values := Seq{NewArray(1, 2, 3, 4, 5)} + assert.Equal(2, values.Variance()) +} + +func TestSequenceNormalize(t *testing.T) { + assert := assert.New(t) + + normalized := ValueSequence(1, 2, 3, 4, 5).Normalize().Values() + + assert.NotEmpty(normalized) + assert.Len(normalized, 5) + assert.Equal(0, normalized[0]) + assert.Equal(0.25, normalized[1]) + assert.Equal(1, normalized[4]) +} + +func TestLinearRange(t *testing.T) { + assert := assert.New(t) + + values := LinearRange(1, 100) + assert.Len(values, 100) + assert.Equal(1, values[0]) + assert.Equal(100, values[99]) +} + +func TestLinearRangeWithStep(t *testing.T) { + assert := assert.New(t) + + values := LinearRangeWithStep(0, 100, 5) + assert.Equal(100, values[20]) + assert.Len(values, 21) +} + +func TestLinearRangeReversed(t *testing.T) { + assert := assert.New(t) + + values := LinearRange(10.0, 1.0) + assert.Equal(10, len(values)) + assert.Equal(10.0, values[0]) + assert.Equal(1.0, values[9]) +} + +func TestLinearSequenceRegression(t *testing.T) { + assert := assert.New(t) + + // note; this assumes a 1.0 step is implicitly set in the constructor. + linearProvider := NewLinearSequence().WithStart(1.0).WithEnd(100.0) + assert.Equal(1, linearProvider.Start()) + assert.Equal(100, linearProvider.End()) + assert.Equal(100, linearProvider.Len()) + + values := Seq{linearProvider}.Values() + assert.Len(values, 100) + assert.Equal(1.0, values[0]) + assert.Equal(100, values[99]) +} diff --git a/sma_series_test.go b/sma_series_test.go index 87c1971..27e0c68 100644 --- a/sma_series_test.go +++ b/sma_series_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/blend/go-sdk/assert" - "github.com/wcharczuk/go-chart/seq" ) type mockValuesProvider struct { @@ -32,8 +31,8 @@ func TestSMASeriesGetValue(t *testing.T) { assert := assert.New(t) mockSeries := mockValuesProvider{ - seq.Range(1.0, 10.0), - seq.Range(10, 1.0), + LinearRange(1.0, 10.0), + LinearRange(10, 1.0), } assert.Equal(10, mockSeries.Len()) @@ -63,8 +62,8 @@ func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) { assert := assert.New(t) mockSeries := mockValuesProvider{ - seq.Range(1.0, 10.0), - seq.Range(10, 1.0), + LinearRange(1.0, 10.0), + LinearRange(10, 1.0), } assert.Equal(10, mockSeries.Len()) @@ -89,8 +88,8 @@ func TestSMASeriesGetLastValue(t *testing.T) { assert := assert.New(t) mockSeries := mockValuesProvider{ - seq.Range(1.0, 100.0), - seq.Range(100, 1.0), + LinearRange(1.0, 100.0), + LinearRange(100, 1.0), } assert.Equal(100, mockSeries.Len()) diff --git a/stacked_bar_chart.go b/stacked_bar_chart.go index 40eed89..06db425 100644 --- a/stacked_bar_chart.go +++ b/stacked_bar_chart.go @@ -162,7 +162,7 @@ func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar S } func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) { - if sbc.XAxis.Show { + if !sbc.XAxis.Hidden { axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes()) axisStyle.WriteToRenderer(r) @@ -196,7 +196,7 @@ func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) { } func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { - if sbc.YAxis.Show { + if !sbc.YAxis.Hidden { axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsAxes()) axisStyle.WriteToRenderer(r) r.MoveTo(canvasBox.Right, canvasBox.Top) @@ -207,7 +207,7 @@ func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom) r.Stroke() - ticks := SeqRangeWithStep(0.0, 1.0, 0.2) + ticks := LinearRangeWithStep(0.0, 1.0, 0.2) for _, t := range ticks { axisStyle.GetStrokeOptions().WriteToRenderer(r) ty := canvasBox.Bottom - int(t*float64(canvasBox.Height())) @@ -226,7 +226,7 @@ func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { } func (sbc StackedBarChart) drawTitle(r Renderer) { - if len(sbc.Title) > 0 && sbc.TitleStyle.Show { + if len(sbc.Title) > 0 && !sbc.TitleStyle.Hidden { r.SetFont(sbc.TitleStyle.GetFont(sbc.GetFont())) r.SetFontColor(sbc.TitleStyle.GetFontColor(sbc.GetColorPalette().TextColor())) titleFontSize := sbc.TitleStyle.GetFontSize(DefaultTitleFontSize) @@ -274,7 +274,7 @@ func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box { totalWidth += bar.GetWidth() + sbc.GetBarSpacing() } - if sbc.XAxis.Show { + if !sbc.XAxis.Hidden { xaxisHeight := DefaultVerticalTickHeight axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes()) diff --git a/style.go b/style.go index 04a73a5..fe1f35a 100644 --- a/style.go +++ b/style.go @@ -14,10 +14,18 @@ const ( Disabled = -1 ) -// StyleShow is a prebuilt style with the `Show` property set to true. -func StyleShow() Style { +// Hidden is a prebuilt style with the `Hidden` property set to true. +func Hidden() Style { return Style{ - Show: true, + Hidden: true, + } +} + +// Shown is a prebuilt style with the `Hidden` property set to false. +// You can also think of this as the default. +func Shown() Style { + return Style{ + Hidden: false, } } @@ -26,7 +34,7 @@ func StyleShow() Style { func StyleTextDefaults() Style { font, _ := GetDefaultFont() return Style{ - Show: true, + Hidden: false, Font: font, FontColor: DefaultTextColor, FontSize: DefaultTitleFontSize, @@ -35,7 +43,7 @@ func StyleTextDefaults() Style { // Style is a simple style set. type Style struct { - Show bool + Hidden bool Padding Box ClassName string @@ -65,7 +73,8 @@ type Style struct { // IsZero returns if the object is set or not. func (s Style) IsZero() bool { - return s.StrokeColor.IsZero() && + return !s.Hidden && + s.StrokeColor.IsZero() && s.StrokeWidth == 0 && s.DotColor.IsZero() && s.DotWidth == 0 && @@ -83,10 +92,10 @@ func (s Style) String() string { } var output []string - if s.Show { - output = []string{"\"show\": true"} + if s.Hidden { + output = []string{"\"hidden\": true"} } else { - output = []string{"\"show\": false"} + output = []string{"\"hidden\": false"} } if s.ClassName != "" { diff --git a/times.go b/times.go new file mode 100644 index 0000000..1a21f48 --- /dev/null +++ b/times.go @@ -0,0 +1,43 @@ +package chart + +import ( + "sort" + "time" + + "github.com/blend/go-sdk/timeutil" +) + +// Assert types implement interfaces. +var ( + _ Sequence = (*Times)(nil) + _ sort.Interface = (*Times)(nil) +) + +// Times are an array of times. +// It wraps the array with methods that implement `seq.Provider`. +type Times []time.Time + +// Array returns the times to an array. +func (t Times) Array() []time.Time { + return []time.Time(t) +} + +// Len returns the length of the array. +func (t Times) Len() int { + return len(t) +} + +// GetValue returns a value at an index as a time. +func (t Times) GetValue(index int) float64 { + return timeutil.ToFloat64(t[index]) +} + +// Swap implements sort.Interface. +func (t Times) Swap(i, j int) { + t[i], t[j] = t[j], t[i] +} + +// Less implements sort.Interface. +func (t Times) Less(i, j int) bool { + return t[i].Before(t[j]) +} diff --git a/timeutil.go b/timeutil.go index 4bf3485..aa6b9e4 100644 --- a/timeutil.go +++ b/timeutil.go @@ -80,6 +80,11 @@ func TimeToFloat64(t time.Time) float64 { return float64(t.UnixNano()) } +// TimeFromFloat64 returns a time from a float64. +func TimeFromFloat64(tf float64) time.Time { + return time.Unix(0, int64(tf)) +} + // TimeDescending sorts a given list of times ascending, or min to max. type TimeDescending []time.Time @@ -103,3 +108,43 @@ func (a TimeAscending) Swap(i, j int) { a[i], a[j] = a[j], a[i] } // Less implements sort.Sorter func (a TimeAscending) Less(i, j int) bool { return a[i].Before(a[j]) } + +// Days generates a seq of timestamps by day, from -days to today. +func 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 +} + +// Hours returns a sequence of times by the hour for a given number of hours +// after a given start. +func 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 HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) { + start, end := TimeMinMax(xdata...) + totalHours := DiffHours(start, end) + + finalTimes := Hours(start, totalHours+1) + finalValues := make([]float64, totalHours+1) + + var hoursFromStart int + for i, xd := range xdata { + hoursFromStart = DiffHours(start, xd) + finalValues[hoursFromStart] = ydata[i] + } + + return finalTimes, finalValues +} diff --git a/value_buffer.go b/value_buffer.go index 8b0fc66..d544bd3 100644 --- a/value_buffer.go +++ b/value_buffer.go @@ -149,7 +149,7 @@ func (b *ValueBuffer) TrimExcess() { } // Array returns the ring buffer, in order, as an array. -func (b *ValueBuffer) Array() SeqArray { +func (b *ValueBuffer) Array() Array { newArray := make([]float64, b.size) if b.size == 0 { @@ -163,7 +163,7 @@ func (b *ValueBuffer) Array() SeqArray { arrayCopy(b.array, 0, newArray, len(b.array)-b.head, b.tail) } - return SeqArray(newArray) + return Array(newArray) } // Each calls the consumer for each element in the buffer. diff --git a/value_buffer_test.go b/value_buffer_test.go index 96be1bf..e701eb4 100644 --- a/value_buffer_test.go +++ b/value_buffer_test.go @@ -6,10 +6,10 @@ import ( "github.com/blend/go-sdk/assert" ) -func TestRingBuffer(t *testing.T) { +func TestBuffer(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) { +func TestBufferClear(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 TestRingBufferContents(t *testing.T) { +func TestBufferArray(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.Contents() + contents := buffer.Array() assert.Len(contents, 5) assert.Equal(1, contents[0]) assert.Equal(2, contents[1]) @@ -140,145 +140,53 @@ func TestRingBufferContents(t *testing.T) { assert.Equal(5, contents[4]) } -func TestRingBufferDrain(t *testing.T) { +func TestBufferEach(t *testing.T) { assert := assert.New(t) - buffer := NewRingBuffer() - buffer.Enqueue(1) - buffer.Enqueue(2) - buffer.Enqueue(3) - buffer.Enqueue(4) - buffer.Enqueue(5) - - contents := buffer.Drain() - assert.Len(contents, 5) - assert.Equal(1, contents[0]) - assert.Equal(2, contents[1]) - assert.Equal(3, contents[2]) - assert.Equal(4, contents[3]) - assert.Equal(5, contents[4]) - - assert.Equal(0, buffer.Len()) - assert.Nil(buffer.Peek()) - assert.Nil(buffer.PeekBack()) -} - -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++ - } - } - }) - - assert.Equal(16, called) -} - -func TestRingBufferEachUntil(t *testing.T) { - assert := assert.New(t) - - buffer := NewRingBuffer() - - for x := 1; x < 17; x++ { - buffer.Enqueue(x) - } - - called := 0 - buffer.EachUntil(func(v interface{}) bool { - if typed, isTyped := v.(int); isTyped { - if typed > 10 { - return false - } - if typed == (called + 1) { - called++ - } - } - return true - }) - - assert.Equal(10, called) -} - -func TestRingBufferReverseEachUntil(t *testing.T) { - assert := assert.New(t) - - buffer := NewRingBufferWithCapacity(32) - - for x := 1; x < 17; x++ { - buffer.Enqueue(x) - } - - var values []int - buffer.ReverseEachUntil(func(v interface{}) bool { - if typed, isTyped := v.(int); isTyped { - if typed < 10 { - return false - } - values = append(values, typed) - return true - } - panic("value is not an integer") - }) - - assert.Len(values, 7) - assert.Equal(16, values[0]) - assert.Equal(10, values[6]) -} - -func TestRingBufferReverseEachUntilUndersized(t *testing.T) { - assert := assert.New(t) - - buffer := NewRingBuffer() - - for x := 1; x < 17; x++ { - buffer.Enqueue(x) - } - - var values []int - buffer.ReverseEachUntil(func(v interface{}) bool { - if typed, isTyped := v.(int); isTyped { - if typed < 10 { - return false - } - values = append(values, typed) - return true - } - panic("value is not an integer") - }) - - assert.Len(values, 7) - assert.Equal(16, values[0]) - assert.Equal(10, values[6]) -} - -func TestRingBufferConsume(t *testing.T) { - assert := assert.New(t) - - buffer := NewRingBuffer() - - for x := 1; x < 17; x++ { - buffer.Enqueue(x) - } - - assert.Equal(16, buffer.Len()) - - var called int - buffer.Consume(func(v interface{}) { - if _, isTyped := v.(int); isTyped { + buffer.Each(func(_ int, v float64) { + if v == float64(called+1) { called++ } }) assert.Equal(16, called) - assert.Zero(buffer.Len()) +} + +func TestNewBuffer(t *testing.T) { + assert := assert.New(t) + + empty := NewValueBuffer() + assert.NotNil(empty) + assert.Zero(empty.Len()) + assert.Equal(bufferDefaultCapacity, empty.Capacity()) + assert.Zero(empty.Peek()) + assert.Zero(empty.PeekBack()) +} + +func TestNewBufferWithValues(t *testing.T) { + assert := assert.New(t) + + values := NewValueBuffer(1, 2, 3, 4, 5) + assert.NotNil(values) + assert.Equal(5, values.Len()) + assert.Equal(1, values.Peek()) + assert.Equal(5, values.PeekBack()) +} + +func TestBufferGrowth(t *testing.T) { + assert := assert.New(t) + + values := NewValueBuffer(1, 2, 3, 4, 5) + for i := 0; i < 1<<10; i++ { + values.Enqueue(float64(i)) + } + + assert.Equal(1<<10-1, values.PeekBack()) } diff --git a/xaxis.go b/xaxis.go index 8b86316..fbde599 100644 --- a/xaxis.go +++ b/xaxis.go @@ -108,7 +108,7 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic bottom = MaxInt(bottom, ty) } - if xa.NameStyle.Show && len(xa.Name) > 0 { + if !xa.NameStyle.Hidden && len(xa.Name) > 0 { tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults)) bottom += DefaultXAxisMargin + tb.Height() } @@ -180,16 +180,16 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick } nameStyle := xa.NameStyle.InheritFrom(defaults) - if xa.NameStyle.Show && len(xa.Name) > 0 { + if !xa.NameStyle.Hidden && len(xa.Name) > 0 { tb := Draw.MeasureText(r, xa.Name, nameStyle) tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1) ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + tb.Height() Draw.Text(r, xa.Name, tx, ty, nameStyle) } - if xa.GridMajorStyle.Show || xa.GridMinorStyle.Show { + if !xa.GridMajorStyle.Hidden || !xa.GridMinorStyle.Hidden { for _, gl := range xa.GetGridLines(ticks) { - if (gl.IsMinor && xa.GridMinorStyle.Show) || (!gl.IsMinor && xa.GridMajorStyle.Show) { + if (gl.IsMinor && !xa.GridMinorStyle.Hidden) || (!gl.IsMinor && !xa.GridMajorStyle.Hidden) { defaults := xa.GridMajorStyle if gl.IsMinor { defaults = xa.GridMinorStyle diff --git a/yaxis.go b/yaxis.go index 3921545..028fcc8 100644 --- a/yaxis.go +++ b/yaxis.go @@ -117,7 +117,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic maxy = MaxInt(maxy, ly+tbh2) } - if ya.NameStyle.Show && len(ya.Name) > 0 { + if !ya.NameStyle.Hidden && len(ya.Name) > 0 { maxx += (DefaultYAxisMargin + maxTextHeight) } @@ -188,7 +188,7 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick } nameStyle := ya.NameStyle.InheritFrom(defaults.InheritFrom(Style{TextRotationDegrees: 90})) - if ya.NameStyle.Show && len(ya.Name) > 0 { + if !ya.NameStyle.Hidden && len(ya.Name) > 0 { nameStyle.GetTextOptions().WriteToRenderer(r) tb := Draw.MeasureText(r, ya.Name, nameStyle) @@ -209,13 +209,13 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick Draw.Text(r, ya.Name, tx, ty, nameStyle) } - if ya.Zero.Style.Show { + if !ya.Zero.Style.Hidden { ya.Zero.Render(r, canvasBox, ra, false, Style{}) } - if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show { + if !ya.GridMajorStyle.Hidden || !ya.GridMinorStyle.Hidden { for _, gl := range ya.GetGridLines(ticks) { - if (gl.IsMinor && ya.GridMinorStyle.Show) || (!gl.IsMinor && ya.GridMajorStyle.Show) { + if (gl.IsMinor && !ya.GridMinorStyle.Hidden) || (!gl.IsMinor && !ya.GridMajorStyle.Hidden) { defaults := ya.GridMajorStyle if gl.IsMinor { defaults = ya.GridMinorStyle