diff --git a/_examples/twopoint/main.go b/_examples/twopoint/main.go new file mode 100644 index 0000000..d703431 --- /dev/null +++ b/_examples/twopoint/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "bytes" + "log" + "os" + //"time" + "github.com/wcharczuk/go-chart" //exposes "chart" +) + +func main() { + + var b float64 + b = 1000 + + ts1 := chart.ContinuousSeries{ //TimeSeries{ + Name: "Time Series", + Style: chart.Style{ + Show: true, + }, + + //XValues: []time.Time{time.Unix(3*b,0),time.Unix(4*b,0),time.Unix(5*b,0),time.Unix(6*b,0),time.Unix(7*b,0),time.Unix(8*b,0),time.Unix(9*b,0),time.Unix(10*b,0)}, + XValues: []float64{10 * b, 20 * b, 30 * b, 40 * b, 50 * b, 60 * b, 70 * b, 80 * b}, + YValues: []float64{1.0, 2.0, 30.0, 4.0, 50.0, 6.0, 7.0, 88.0}, + } + + ts2 := chart.ContinuousSeries{ //TimeSeries{ + Style: chart.Style{ + Show: true, + StrokeColor: chart.GetDefaultColor(1), + }, + + XValues: []float64{10 * b, 20 * b, 30 * b, 40 * b, 50 * b, 60 * b, 70 * b, 80 * b}, + YValues: []float64{15.0, 52.0, 30.0, 42.0, 50.0, 26.0, 77.0, 38.0}, + } + + graph := chart.Chart{ + + XAxis: chart.XAxis{ + Name: "The XAxis", + NameStyle: chart.StyleShow(), + Style: chart.StyleShow(), + ValueFormatter: chart.TimeMinuteValueFormatter, //TimeHourValueFormatter, + }, + + YAxis: chart.YAxis{ + Name: "The YAxis", + NameStyle: chart.StyleShow(), + Style: chart.StyleShow(), + }, + + Series: []chart.Series{ + ts1, + ts2, + }, + } + + buffer := bytes.NewBuffer([]byte{}) + err := graph.Render(chart.PNG, buffer) + if err != nil { + log.Fatal(err) + } + + fo, err := os.Create("output.png") + if err != nil { + panic(err) + } + + if _, err := fo.Write(buffer.Bytes()); err != nil { + panic(err) + } +} diff --git a/_examples/twopoint/output.png b/_examples/twopoint/output.png new file mode 100644 index 0000000..8d35b97 Binary files /dev/null and b/_examples/twopoint/output.png differ diff --git a/annotation_series.go b/annotation_series.go index 01d8569..6ec28e4 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -1,6 +1,9 @@ package chart -import "math" +import ( + "fmt" + "math" +) // AnnotationSeries is a series of labels on the chart. type AnnotationSeries struct { @@ -73,3 +76,11 @@ func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Rang } } } + +// Validate validates the series. +func (as AnnotationSeries) Validate() error { + if len(as.Annotations) == 0 { + return fmt.Errorf("annotation series requires annotations to be set and not empty") + } + return nil +} diff --git a/bollinger_band_series.go b/bollinger_band_series.go index f74b489..902b541 100644 --- a/bollinger_band_series.go +++ b/bollinger_band_series.go @@ -1,6 +1,9 @@ package chart -import "math" +import ( + "fmt" + "math" +) // 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. @@ -147,3 +150,11 @@ func (bbs BollingerBandsSeries) getVariance(valueBuffer *RingBuffer) float64 { 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 { + return fmt.Errorf("bollinger bands series requires InnerSeries to be set") + } + return nil +} diff --git a/chart.go b/chart.go index 236d714..7dee166 100644 --- a/chart.go +++ b/chart.go @@ -132,6 +132,17 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error { return r.Save(w) } +func (c Chart) validateSeries() error { + var err error + for _, s := range c.Series { + err = s.Validate() + if err != nil { + return err + } + } + return nil +} + func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { var minx, maxx float64 = math.MaxFloat64, -math.MaxFloat64 var miny, maxy float64 = math.MaxFloat64, -math.MaxFloat64 diff --git a/concat_series.go b/concat_series.go index 2c2098f..46d8dda 100644 --- a/concat_series.go +++ b/concat_series.go @@ -30,3 +30,15 @@ func (cs ConcatSeries) GetValue(index int) (x, y float64) { } return } + +// Validate validates the series. +func (cs ConcatSeries) Validate() error { + var err error + for _, s := range cs { + err = s.Validate() + if err != nil { + return err + } + } + return nil +} diff --git a/continuous_series.go b/continuous_series.go index f182eb1..d6252f3 100644 --- a/continuous_series.go +++ b/continuous_series.go @@ -1,5 +1,7 @@ package chart +import "fmt" + // ContinuousSeries represents a line on a chart. type ContinuousSeries struct { Name string @@ -64,3 +66,15 @@ func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Rang style := cs.Style.InheritFrom(defaults) Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs) } + +// Validate validates the series. +func (cs ContinuousSeries) Validate() error { + if len(cs.XValues) == 0 { + return fmt.Errorf("continuous series must have xvalues set") + } + + if len(cs.YValues) == 0 { + return fmt.Errorf("continuous series must have yvalues set") + } + return nil +} diff --git a/continuous_series_test.go b/continuous_series_test.go index e8df2d6..c5d950c 100644 --- a/continuous_series_test.go +++ b/continuous_series_test.go @@ -47,3 +47,26 @@ func TestContinuousSeriesValueFormatter(t *testing.T) { assert.Equal("0.100000 foo", xf(0.1)) assert.Equal("0.100000 bar", yf(0.1)) } + +func TestContinuousSeriesValidate(t *testing.T) { + assert := assert.New(t) + + cs := ContinuousSeries{ + Name: "Test Series", + XValues: Sequence.Float64(1.0, 10.0), + YValues: Sequence.Float64(1.0, 10.0), + } + assert.Nil(cs.Validate()) + + cs = ContinuousSeries{ + Name: "Test Series", + XValues: Sequence.Float64(1.0, 10.0), + } + assert.NotNil(cs.Validate()) + + cs = ContinuousSeries{ + Name: "Test Series", + YValues: Sequence.Float64(1.0, 10.0), + } + assert.NotNil(cs.Validate()) +} diff --git a/ema_series.go b/ema_series.go index affadc1..44406aa 100644 --- a/ema_series.go +++ b/ema_series.go @@ -1,5 +1,7 @@ package chart +import "fmt" + const ( // DefaultEMAPeriod is the default EMA period used in the sigma calculation. DefaultEMAPeriod = 12 @@ -99,3 +101,11 @@ func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, de style := ema.Style.InheritFrom(defaults) Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema) } + +// Validate validates the series. +func (ema *EMASeries) Validate() error { + if ema.InnerSeries == nil { + return fmt.Errorf("ema series requires InnerSeries to be set") + } + return nil +} diff --git a/histogram_series.go b/histogram_series.go index 0542c1a..153837b 100644 --- a/histogram_series.go +++ b/histogram_series.go @@ -1,5 +1,7 @@ package chart +import "fmt" + // HistogramSeries is a special type of series that draws as a histogram. // Some peculiarities; it will always be lower bounded at 0 (at the very least). // This may alter ranges a bit and generally you want to put a histogram series on it's own y-axis. @@ -55,3 +57,11 @@ func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range style := hs.Style.InheritFrom(defaults) Draw.HistogramSeries(r, canvasBox, xrange, yrange, style, hs) } + +// Validate validates the series. +func (hs HistogramSeries) Validate() error { + if hs.InnerSeries == nil { + return fmt.Errorf("histogram series requires InnerSeries to be set") + } + return nil +} diff --git a/image_writer.go b/image_writer.go index e9cfd7a..85ce0c0 100644 --- a/image_writer.go +++ b/image_writer.go @@ -38,5 +38,5 @@ func (ir *ImageWriter) Image() (image.Image, error) { if ir.contents != nil && ir.contents.Len() > 0 { return png.Decode(ir.contents) } - return nil, errors.New("No valid sources for image data, cannot continue.") + return nil, errors.New("no valid sources for image data, cannot continue") } diff --git a/linear_regression_series.go b/linear_regression_series.go index a33a0b1..bcd045b 100644 --- a/linear_regression_series.go +++ b/linear_regression_series.go @@ -1,5 +1,7 @@ package chart +import "fmt" + // LinearRegressionSeries is a series that plots the n-nearest neighbors // linear regression for the values. type LinearRegressionSeries struct { @@ -130,3 +132,11 @@ func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yra style := lrs.Style.InheritFrom(defaults) Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs) } + +// Validate validates the series. +func (lrs *LinearRegressionSeries) Validate() error { + if lrs.InnerSeries == nil { + return fmt.Errorf("linear regression series requires InnerSeries to be set") + } + return nil +} diff --git a/macd_series.go b/macd_series.go index b3b80c0..aa3b683 100644 --- a/macd_series.go +++ b/macd_series.go @@ -1,5 +1,7 @@ package chart +import "fmt" + const ( // DefaultMACDPeriodPrimary is the long window. DefaultMACDPeriodPrimary = 26 @@ -287,3 +289,11 @@ func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Ra style := macdl.Style.InheritFrom(defaults) Draw.LineSeries(r, canvasBox, xrange, yrange, style, macdl) } + +// Validate validates the series. +func (macdl *MACDLineSeries) Validate() error { + if macdl.InnerSeries == nil { + return fmt.Errorf("macd line series requires InnerSeries to be set") + } + return nil +} diff --git a/min_max_series.go b/min_max_series.go index 4900c6d..db153aa 100644 --- a/min_max_series.go +++ b/min_max_series.go @@ -1,6 +1,9 @@ package chart -import "math" +import ( + "fmt" + "math" +) // MinSeries draws a horizontal line at the minimum value of the inner series. type MinSeries struct { @@ -60,6 +63,14 @@ func (ms *MinSeries) ensureMinValue() { } } +// Validate validates the series. +func (ms *MinSeries) Validate() error { + if ms.InnerSeries == nil { + return fmt.Errorf("min series requires InnerSeries to be set") + } + return nil +} + // MaxSeries draws a horizontal line at the maximum value of the inner series. type MaxSeries struct { Name string @@ -117,3 +128,11 @@ func (ms *MaxSeries) ensureMaxValue() { ms.maxValue = &maxValue } } + +// Validate validates the series. +func (ms *MaxSeries) Validate() error { + if ms.InnerSeries == nil { + return fmt.Errorf("max series requires InnerSeries to be set") + } + return nil +} diff --git a/pie_chart.go b/pie_chart.go index b2be1a6..c2d485c 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -66,7 +66,7 @@ func (pc PieChart) GetHeight() int { // Render renders the chart with the given renderer to the given io.Writer. func (pc PieChart) Render(rp RendererProvider, w io.Writer) error { if len(pc.Values) == 0 { - return errors.New("Please provide at least one value.") + return errors.New("please provide at least one value") } r, err := rp(pc.GetWidth(), pc.GetHeight()) diff --git a/series.go b/series.go index 6145dcb..7986d01 100644 --- a/series.go +++ b/series.go @@ -5,5 +5,6 @@ type Series interface { GetName() string GetYAxis() YAxisType GetStyle() Style + Validate() error Render(r Renderer, canvasBox Box, xrange, yrange Range, s Style) } diff --git a/sma_series.go b/sma_series.go index a7e0034..d9f7f13 100644 --- a/sma_series.go +++ b/sma_series.go @@ -1,5 +1,7 @@ package chart +import "fmt" + const ( // DefaultSimpleMovingAveragePeriod is the default number of values to average. DefaultSimpleMovingAveragePeriod = 16 @@ -88,3 +90,11 @@ func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, def style := sma.Style.InheritFrom(defaults) Draw.LineSeries(r, canvasBox, xrange, yrange, style, sma) } + +// Validate validates the series. +func (sma SMASeries) Validate() error { + if sma.InnerSeries == nil { + return fmt.Errorf("sma series requires InnerSeries to be set") + } + return nil +} diff --git a/stacked_bar_chart.go b/stacked_bar_chart.go index 511be46..16c4919 100644 --- a/stacked_bar_chart.go +++ b/stacked_bar_chart.go @@ -94,7 +94,7 @@ func (sbc StackedBarChart) GetBarSpacing() int { // Render renders the chart with the given renderer to the given io.Writer. func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error { if len(sbc.Bars) == 0 { - return errors.New("Please provide at least one bar.") + return errors.New("please provide at least one bar") } r, err := rp(sbc.GetWidth(), sbc.GetHeight()) diff --git a/time_series.go b/time_series.go index 025e86a..edade97 100644 --- a/time_series.go +++ b/time_series.go @@ -1,6 +1,9 @@ package chart -import "time" +import ( + "fmt" + "time" +) // TimeSeries is a line on a chart. type TimeSeries struct { @@ -59,3 +62,15 @@ func (ts TimeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, def style := ts.Style.InheritFrom(defaults) Draw.LineSeries(r, canvasBox, xrange, yrange, style, ts) } + +// Validate validates the series. +func (ts TimeSeries) Validate() error { + if len(ts.XValues) == 0 { + return fmt.Errorf("time series must have xvalues set") + } + + if len(ts.YValues) == 0 { + return fmt.Errorf("time series must have yvalues set") + } + return nil +} diff --git a/time_series_test.go b/time_series_test.go index 3194997..717477b 100644 --- a/time_series_test.go +++ b/time_series_test.go @@ -28,3 +28,42 @@ func TestTimeSeriesGetValue(t *testing.T) { assert.NotZero(x0) assert.Equal(1.0, y0) } + +func TestTimeSeriesValidate(t *testing.T) { + assert := assert.New(t) + + cs := TimeSeries{ + Name: "Test Series", + XValues: []time.Time{ + time.Now().AddDate(0, 0, -5), + time.Now().AddDate(0, 0, -4), + time.Now().AddDate(0, 0, -3), + time.Now().AddDate(0, 0, -2), + time.Now().AddDate(0, 0, -1), + }, + YValues: []float64{ + 1.0, 2.0, 3.0, 4.0, 5.0, + }, + } + assert.Nil(cs.Validate()) + + cs = TimeSeries{ + Name: "Test Series", + XValues: []time.Time{ + time.Now().AddDate(0, 0, -5), + time.Now().AddDate(0, 0, -4), + time.Now().AddDate(0, 0, -3), + time.Now().AddDate(0, 0, -2), + time.Now().AddDate(0, 0, -1), + }, + } + assert.NotNil(cs.Validate()) + + cs = TimeSeries{ + Name: "Test Series", + YValues: []float64{ + 1.0, 2.0, 3.0, 4.0, 5.0, + }, + } + assert.NotNil(cs.Validate()) +}