From b0934ee2e36ce288df997ce8d989bff26ddc14dd Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Thu, 21 Jul 2016 14:11:27 -0700 Subject: [PATCH] introduces the `Range` interface (instead of a concrete type). --- annotation_series.go | 4 +- annotation_series_test.go | 8 +- axis.go | 14 ++- bollinger_band_series.go | 2 +- chart.go | 101 +++++++++++++--------- chart_test.go | 60 ++++++------- concat_series.go | 32 +++++++ concat_series_test.go | 41 +++++++++ continuous_range.go | 67 ++++++++++++++ range_test.go => continuous_range_test.go | 2 +- continuous_series.go | 2 +- drawing_helpers.go | 8 +- ema_series.go | 2 +- examples/custom_ranges/main.go | 2 +- histogram_series.go | 2 +- macd_series.go | 4 +- range.go | 61 +++++++------ sma_series.go | 2 +- style.go | 15 +++- style_test.go | 2 +- tick.go | 2 +- tick_test.go | 2 +- time_series.go | 2 +- xaxis.go | 23 ++--- xaxis_test.go | 10 +-- yaxis.go | 23 ++--- yaxis_test.go | 10 +-- 27 files changed, 331 insertions(+), 172 deletions(-) create mode 100644 concat_series.go create mode 100644 concat_series_test.go create mode 100644 continuous_range.go rename range_test.go => continuous_range_test.go (92%) diff --git a/annotation_series.go b/annotation_series.go index 8a9b891..405fe55 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -40,7 +40,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran Bottom: 0, } if as.Style.IsZero() || as.Style.Show { - style := as.Style.WithDefaultsFrom(Style{ + style := as.Style.InheritFrom(Style{ Font: defaults.Font, FillColor: DefaultAnnotationFillColor, FontSize: DefaultAnnotationFontSize, @@ -64,7 +64,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 { - style := as.Style.WithDefaultsFrom(Style{ + style := as.Style.InheritFrom(Style{ Font: defaults.Font, FontColor: DefaultTextColor, FillColor: DefaultAnnotationFillColor, diff --git a/annotation_series_test.go b/annotation_series_test.go index d25f4e9..26953d9 100644 --- a/annotation_series_test.go +++ b/annotation_series_test.go @@ -29,12 +29,12 @@ func TestAnnotationSeriesMeasure(t *testing.T) { f, err := GetDefaultFont() assert.Nil(err) - xrange := Range{ + xrange := &ContinuousRange{ Min: 1.0, Max: 4.0, Domain: 100, } - yrange := Range{ + yrange := &ContinuousRange{ Min: 1.0, Max: 4.0, Domain: 100, @@ -82,12 +82,12 @@ func TestAnnotationSeriesRender(t *testing.T) { f, err := GetDefaultFont() assert.Nil(err) - xrange := Range{ + xrange := &ContinuousRange{ Min: 1.0, Max: 4.0, Domain: 100, } - yrange := Range{ + yrange := &ContinuousRange{ Min: 1.0, Max: 4.0, Domain: 100, diff --git a/axis.go b/axis.go index f018df6..1c6d272 100644 --- a/axis.go +++ b/axis.go @@ -14,7 +14,17 @@ const ( type Axis interface { GetName() string GetStyle() Style - GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick + + GetTicks() []Tick + GenerateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick + + // GetGridLines returns the gridlines for the axis. GetGridLines(ticks []Tick) []GridLine - Render(c *Chart, r Renderer, canvasBox Box, ra Range, ticks []Tick) + + // Measure should return an absolute box for the axis. + // This is used when auto-fitting the canvas to the background. + Measure(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) Box + + // Render renders the axis. + Render(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) } diff --git a/bollinger_band_series.go b/bollinger_band_series.go index 739c0ad..c0fe7a6 100644 --- a/bollinger_band_series.go +++ b/bollinger_band_series.go @@ -108,7 +108,7 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) { // Render renders the series. func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - s := bbs.Style.WithDefaultsFrom(defaults.WithDefaultsFrom(Style{ + s := bbs.Style.InheritFrom(defaults.InheritFrom(Style{ StrokeWidth: 1.0, StrokeColor: DefaultAxisColor.WithAlpha(64), FillColor: DefaultAxisColor.WithAlpha(32), diff --git a/chart.go b/chart.go index 1eb4bdf..4427592 100644 --- a/chart.go +++ b/chart.go @@ -137,7 +137,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { var miny, maxy float64 = math.MaxFloat64, 0 var minya, maxya float64 = math.MaxFloat64, 0 - hasSecondaryAxis := false + seriesMappedToSecondaryAxis := false // note: a possible future optimization is to not scan the series values if // all axis are represented by either custom ticks or custom ranges. @@ -162,7 +162,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { minya = math.Min(minya, vy2) maxya = math.Max(maxya, vy1) maxya = math.Max(maxya, vy2) - hasSecondaryAxis = true + seriesMappedToSecondaryAxis = true } } } else if vp, isValueProvider := s.(ValueProvider); isValueProvider { @@ -179,27 +179,40 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { } else if seriesAxis == YAxisSecondary { minya = math.Min(minya, vy) maxya = math.Max(maxya, vy) - hasSecondaryAxis = true + seriesMappedToSecondaryAxis = true } } } } } + if xrange == nil { + xrange = &ContinuousRange{} + } + + if yrange == nil { + yrange = &ContinuousRange{} + } + + if yrangeAlt == nil { + yrangeAlt = &ContinuousRange{} + } + if len(c.XAxis.Ticks) > 0 { tickMin, tickMax := math.MaxFloat64, 0.0 for _, t := range c.XAxis.Ticks { tickMin = math.Min(tickMin, t.Value) tickMax = math.Max(tickMax, t.Value) } - xrange.Min = tickMin - xrange.Max = tickMax - } else if !c.XAxis.Range.IsZero() { - xrange.Min = c.XAxis.Range.Min - xrange.Max = c.XAxis.Range.Max + + xrange.SetMin(tickMin) + xrange.SetMax(tickMax) + } else if c.XAxis.Range != nil && !c.XAxis.Range.IsZero() { + xrange.SetMin(c.XAxis.Range.GetMin()) + xrange.SetMax(c.XAxis.Range.GetMax()) } else { - xrange.Min = minx - xrange.Max = maxx + xrange.SetMin(minx) + xrange.SetMax(maxx) } if len(c.YAxis.Ticks) > 0 { @@ -208,15 +221,20 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { tickMin = math.Min(tickMin, t.Value) tickMax = math.Max(tickMax, t.Value) } - yrange.Min = tickMin - yrange.Max = tickMax - } else if !c.YAxis.Range.IsZero() { - yrange.Min = c.YAxis.Range.Min - yrange.Max = c.YAxis.Range.Max + yrange.SetMin(tickMin) + yrange.SetMax(tickMax) + } else if c.YAxis.Range != nil && !c.YAxis.Range.IsZero() { + yrange.SetMin(c.YAxis.Range.GetMin()) + yrange.SetMax(c.YAxis.Range.GetMax()) } else { - yrange.Min = miny - yrange.Max = maxy - yrange.Min, yrange.Max = yrange.GetRoundedRangeBounds() + yrange.SetMin(miny) + yrange.SetMax(maxy) + + delta := yrange.GetDelta() + roundTo := GetRoundToForDelta(delta) + rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo) + yrange.SetMin(rmin) + yrange.SetMax(rmax) } if len(c.YAxisSecondary.Ticks) > 0 { @@ -225,30 +243,34 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { tickMin = math.Min(tickMin, t.Value) tickMax = math.Max(tickMax, t.Value) } - yrangeAlt.Min = tickMin - yrangeAlt.Max = tickMax - } else if !c.YAxisSecondary.Range.IsZero() { - yrangeAlt.Min = c.YAxisSecondary.Range.Min - yrangeAlt.Max = c.YAxisSecondary.Range.Max - } else if hasSecondaryAxis { - yrangeAlt.Min = minya - yrangeAlt.Max = maxya - yrangeAlt.Min, yrangeAlt.Max = yrangeAlt.GetRoundedRangeBounds() + yrangeAlt.SetMin(tickMin) + yrangeAlt.SetMax(tickMax) + } else if c.YAxisSecondary.Range != nil && !c.YAxisSecondary.Range.IsZero() { + yrangeAlt.SetMin(c.YAxisSecondary.Range.GetMin()) + yrangeAlt.SetMax(c.YAxisSecondary.Range.GetMax()) + } else if seriesMappedToSecondaryAxis { + yrangeAlt.SetMin(minya) + yrangeAlt.SetMax(maxya) + + delta := yrangeAlt.GetDelta() + roundTo := GetRoundToForDelta(delta) + rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo) + yrangeAlt.SetMin(rmin) + yrangeAlt.SetMax(rmax) } return } func (c Chart) checkRanges(xr, yr, yra Range) error { - - if math.IsInf(xr.Delta(), 0) || math.IsNaN(xr.Delta()) { + if math.IsInf(xr.GetDelta(), 0) || math.IsNaN(xr.GetDelta()) || xr.GetDelta() == 0 { return errors.New("Invalid (infinite or NaN) x-range delta") } - if math.IsInf(yr.Delta(), 0) || math.IsNaN(yr.Delta()) { + if math.IsInf(yr.GetDelta(), 0) || math.IsNaN(yr.GetDelta()) || yr.GetDelta() == 0 { return errors.New("Invalid (infinite or NaN) y-range delta") } if c.hasSecondarySeries() { - if math.IsInf(yra.Delta(), 0) || math.IsNaN(yra.Delta()) { + if math.IsInf(yra.GetDelta(), 0) || math.IsNaN(yra.GetDelta()) || yra.GetDelta() == 0 { return errors.New("Invalid (infinite or NaN) y-secondary-range delta") } } @@ -320,14 +342,11 @@ func (c Chart) getAxisAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra R return canvasBox.OuterConstrain(c.Box(), axesOuterBox) } -func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (xr2, yr2, yra2 Range) { - xr2.Min, xr2.Max = xr.Min, xr.Max - xr2.Domain = canvasBox.Width() - yr2.Min, yr2.Max = yr.Min, yr.Max - yr2.Domain = canvasBox.Height() - yra2.Min, yra2.Max = yra.Min, yra.Max - yra2.Domain = canvasBox.Height() - return +func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range, Range) { + xr.SetDomain(canvasBox.Width()) + yr.SetDomain(canvasBox.Height()) + yra.SetDomain(canvasBox.Height()) + return xr, yr, yra } func (c Chart) hasAnnotationSeries() bool { @@ -372,7 +391,7 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, } func (c Chart) getBackgroundStyle() Style { - return c.Background.WithDefaultsFrom(c.styleDefaultsBackground()) + return c.Background.InheritFrom(c.styleDefaultsBackground()) } func (c Chart) drawBackground(r Renderer) { @@ -383,7 +402,7 @@ func (c Chart) drawBackground(r Renderer) { } func (c Chart) getCanvasStyle() Style { - return c.Canvas.WithDefaultsFrom(c.styleDefaultsCanvas()) + return c.Canvas.InheritFrom(c.styleDefaultsCanvas()) } func (c Chart) drawCanvas(r Renderer, canvasBox Box) { diff --git a/chart_test.go b/chart_test.go index add643e..eca6f98 100644 --- a/chart_test.go +++ b/chart_test.go @@ -77,24 +77,24 @@ func TestChartGetRanges(t *testing.T) { } xrange, yrange, yrangeAlt := c.getRanges() - assert.Equal(-2.0, xrange.Min) - assert.Equal(5.0, xrange.Max) + assert.Equal(-2.0, xrange.GetMin()) + assert.Equal(5.0, xrange.GetMax()) - assert.Equal(-2.1, yrange.Min) - assert.Equal(4.5, yrange.Max) + assert.Equal(-2.1, yrange.GetMin()) + assert.Equal(4.5, yrange.GetMax()) - assert.Equal(10.0, yrangeAlt.Min) - assert.Equal(14.0, yrangeAlt.Max) + assert.Equal(10.0, yrangeAlt.GetMin()) + assert.Equal(14.0, yrangeAlt.GetMax()) cSet := Chart{ XAxis: XAxis{ - Range: Range{Min: 9.8, Max: 19.8}, + Range: &ContinuousRange{Min: 9.8, Max: 19.8}, }, YAxis: YAxis{ - Range: Range{Min: 9.9, Max: 19.9}, + Range: &ContinuousRange{Min: 9.9, Max: 19.9}, }, YAxisSecondary: YAxis{ - Range: Range{Min: 9.7, Max: 19.7}, + Range: &ContinuousRange{Min: 9.7, Max: 19.7}, }, Series: []Series{ ContinuousSeries{ @@ -114,14 +114,14 @@ func TestChartGetRanges(t *testing.T) { } xr2, yr2, yra2 := cSet.getRanges() - assert.Equal(9.8, xr2.Min) - assert.Equal(19.8, xr2.Max) + assert.Equal(9.8, xr2.GetMin()) + assert.Equal(19.8, xr2.GetMax()) - assert.Equal(9.9, yr2.Min) - assert.Equal(19.9, yr2.Max) + assert.Equal(9.9, yr2.GetMin()) + assert.Equal(19.9, yr2.GetMax()) - assert.Equal(9.7, yra2.Min) - assert.Equal(19.7, yra2.Max) + assert.Equal(9.7, yra2.GetMin()) + assert.Equal(19.7, yra2.GetMax()) } func TestChartGetRangesUseTicks(t *testing.T) { @@ -139,7 +139,7 @@ func TestChartGetRangesUseTicks(t *testing.T) { {4.0, "4.0"}, {5.0, "Five"}, }, - Range: Range{ + Range: &ContinuousRange{ Min: -5.0, Max: 5.0, }, @@ -153,10 +153,10 @@ func TestChartGetRangesUseTicks(t *testing.T) { } xr, yr, yar := c.getRanges() - assert.Equal(-2.0, xr.Min) - assert.Equal(2.0, xr.Max) - assert.Equal(0.0, yr.Min) - assert.Equal(5.0, yr.Max) + assert.Equal(-2.0, xr.GetMin()) + assert.Equal(2.0, xr.GetMax()) + assert.Equal(0.0, yr.GetMin()) + assert.Equal(5.0, yr.GetMax()) assert.True(yar.IsZero(), yar.String()) } @@ -165,7 +165,7 @@ func TestChartGetRangesUseUserRanges(t *testing.T) { c := Chart{ YAxis: YAxis{ - Range: Range{ + Range: &ContinuousRange{ Min: -5.0, Max: 5.0, }, @@ -179,10 +179,10 @@ func TestChartGetRangesUseUserRanges(t *testing.T) { } xr, yr, yar := c.getRanges() - assert.Equal(-2.0, xr.Min) - assert.Equal(2.0, xr.Max) - assert.Equal(-5.0, yr.Min) - assert.Equal(5.0, yr.Max) + assert.Equal(-2.0, xr.GetMin()) + assert.Equal(2.0, xr.GetMax()) + assert.Equal(-5.0, yr.GetMin()) + assert.Equal(5.0, yr.GetMax()) assert.True(yar.IsZero(), yar.String()) } @@ -310,15 +310,15 @@ func TestChartGetAxesTicks(t *testing.T) { c := Chart{ XAxis: XAxis{ Style: Style{Show: true}, - Range: Range{Min: 9.8, Max: 19.8}, + Range: &ContinuousRange{Min: 9.8, Max: 19.8}, }, YAxis: YAxis{ Style: Style{Show: true}, - Range: Range{Min: 9.9, Max: 19.9}, + Range: &ContinuousRange{Min: 9.9, Max: 19.9}, }, YAxisSecondary: YAxis{ Style: Style{Show: true}, - Range: Range{Min: 9.7, Max: 19.7}, + Range: &ContinuousRange{Min: 9.7, Max: 19.7}, }, } xr, yr, yar := c.getRanges() @@ -337,7 +337,7 @@ func TestChartSingleSeries(t *testing.T) { Width: 1024, Height: 400, YAxis: YAxis{ - Range: Range{ + Range: &ContinuousRange{ Min: 0.0, Max: 4.0, }, @@ -377,7 +377,7 @@ func TestChartRegressionBadRangesByUser(t *testing.T) { c := Chart{ YAxis: YAxis{ - Range: Range{ + Range: &ContinuousRange{ Min: math.Inf(-1), Max: math.Inf(1), // this could really happen? eh. }, diff --git a/concat_series.go b/concat_series.go new file mode 100644 index 0000000..2c2098f --- /dev/null +++ b/concat_series.go @@ -0,0 +1,32 @@ +package chart + +// ConcatSeries is a special type of series that concatenates its `InnerSeries`. +type ConcatSeries []Series + +// Len returns the length of the concatenated set of series. +func (cs ConcatSeries) Len() int { + total := 0 + for _, s := range cs { + if typed, isValueProvider := s.(ValueProvider); isValueProvider { + total += typed.Len() + } + } + + return total +} + +// GetValue returns the value at the (meta) index (i.e 0 => totalLen-1) +func (cs ConcatSeries) GetValue(index int) (x, y float64) { + cursor := 0 + for _, s := range cs { + if typed, isValueProvider := s.(ValueProvider); isValueProvider { + len := typed.Len() + if index < cursor+len { + x, y = typed.GetValue(index - cursor) //FENCEPOSTS. + return + } + cursor += typed.Len() + } + } + return +} diff --git a/concat_series_test.go b/concat_series_test.go new file mode 100644 index 0000000..f9f93cd --- /dev/null +++ b/concat_series_test.go @@ -0,0 +1,41 @@ +package chart + +import ( + "testing" + + assert "github.com/blendlabs/go-assert" +) + +func TestConcatSeries(t *testing.T) { + assert := assert.New(t) + + s1 := ContinuousSeries{ + XValues: Seq(1.0, 10.0), + YValues: Seq(1.0, 10.0), + } + + s2 := ContinuousSeries{ + XValues: Seq(11, 20.0), + YValues: Seq(10.0, 1.0), + } + + s3 := ContinuousSeries{ + XValues: Seq(21, 30.0), + YValues: Seq(1.0, 10.0), + } + + cs := ConcatSeries([]Series{s1, s2, s3}) + assert.Equal(30, cs.Len()) + + x0, y0 := cs.GetValue(0) + assert.Equal(1.0, x0) + assert.Equal(1.0, y0) + + xm, ym := cs.GetValue(19) + assert.Equal(20.0, xm) + assert.Equal(1.0, ym) + + xn, yn := cs.GetValue(29) + assert.Equal(30.0, xn) + assert.Equal(10.0, yn) +} diff --git a/continuous_range.go b/continuous_range.go new file mode 100644 index 0000000..d119649 --- /dev/null +++ b/continuous_range.go @@ -0,0 +1,67 @@ +package chart + +import ( + "fmt" + "math" +) + +// ContinuousRange represents a boundary for a set of numbers. +type ContinuousRange struct { + Min float64 + Max float64 + Domain int +} + +// IsZero returns if the ContinuousRange has been set or not. +func (r ContinuousRange) IsZero() bool { + return (r.Min == 0 || math.IsNaN(r.Min)) && + (r.Max == 0 || math.IsNaN(r.Max)) && + r.Domain == 0 +} + +// GetMin gets the min value for the continuous range. +func (r ContinuousRange) GetMin() float64 { + return r.Min +} + +// SetMin sets the min value for the continuous range. +func (r *ContinuousRange) SetMin(min float64) { + r.Min = min +} + +// GetMax returns the max value for the continuous range. +func (r ContinuousRange) GetMax() float64 { + return r.Max +} + +// SetMax sets the max value for the continuous range. +func (r *ContinuousRange) SetMax(max float64) { + r.Max = max +} + +// GetDelta returns the difference between the min and max value. +func (r ContinuousRange) GetDelta() float64 { + return r.Max - r.Min +} + +// GetDomain returns the range domain. +func (r ContinuousRange) GetDomain() int { + return r.Domain +} + +// SetDomain sets the range domain. +func (r *ContinuousRange) SetDomain(domain int) { + r.Domain = domain +} + +// String returns a simple string for the ContinuousRange. +func (r ContinuousRange) String() string { + return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) +} + +// Translate maps a given value into the ContinuousRange space. +func (r ContinuousRange) Translate(value float64) int { + normalized := value - r.Min + ratio := normalized / r.GetDelta() + return int(math.Ceil(ratio * float64(r.Domain))) +} diff --git a/range_test.go b/continuous_range_test.go similarity index 92% rename from range_test.go rename to continuous_range_test.go index f71f955..4400366 100644 --- a/range_test.go +++ b/continuous_range_test.go @@ -9,7 +9,7 @@ import ( func TestRangeTranslate(t *testing.T) { assert := assert.New(t) values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} - r := Range{Domain: 1000} + r := ContinuousRange{Domain: 1000} r.Min, r.Max = MinAndMax(values...) // delta = ~7.0 diff --git a/continuous_series.go b/continuous_series.go index fe0581d..0a122d0 100644 --- a/continuous_series.go +++ b/continuous_series.go @@ -50,6 +50,6 @@ func (cs ContinuousSeries) GetYAxis() YAxisType { // Render renders the series. func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := cs.Style.WithDefaultsFrom(defaults) + style := cs.Style.InheritFrom(defaults) DrawLineSeries(r, canvasBox, xrange, yrange, style, cs) } diff --git a/drawing_helpers.go b/drawing_helpers.go index 276e3bd..beada8e 100644 --- a/drawing_helpers.go +++ b/drawing_helpers.go @@ -114,7 +114,7 @@ func DrawHistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl //calculate bar width? seriesLength := vs.Len() - barWidth := int(math.Floor(float64(xrange.Domain) / float64(seriesLength))) + barWidth := int(math.Floor(float64(xrange.GetDomain()) / float64(seriesLength))) if len(barWidths) > 0 { barWidth = barWidths[0] } @@ -271,9 +271,9 @@ func CreateLegend(c *Chart, userDefaults ...Style) Renderable { var legendStyle Style if len(userDefaults) > 0 { - legendStyle = userDefaults[0].WithDefaultsFrom(chartDefaults.WithDefaultsFrom(legendDefaults)) + legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults)) } else { - legendStyle = chartDefaults.WithDefaultsFrom(legendDefaults) + legendStyle = chartDefaults.InheritFrom(legendDefaults) } // DEFAULTS @@ -292,7 +292,7 @@ func CreateLegend(c *Chart, userDefaults ...Style) Renderable { if s.GetStyle().IsZero() || s.GetStyle().Show { if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) - lines = append(lines, s.GetStyle().WithDefaultsFrom(c.styleDefaultsSeries(index))) + lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index))) } } } diff --git a/ema_series.go b/ema_series.go index 2c0bc64..2bd808d 100644 --- a/ema_series.go +++ b/ema_series.go @@ -96,6 +96,6 @@ func (ema *EMASeries) ensureCachedValues() { // Render renders the series. func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := ema.Style.WithDefaultsFrom(defaults) + style := ema.Style.InheritFrom(defaults) DrawLineSeries(r, canvasBox, xrange, yrange, style, ema) } diff --git a/examples/custom_ranges/main.go b/examples/custom_ranges/main.go index b6a6657..ad8cb6a 100644 --- a/examples/custom_ranges/main.go +++ b/examples/custom_ranges/main.go @@ -17,7 +17,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) { Style: chart.Style{ Show: true, }, - Range: chart.Range{ + Range: chart.ContinuousRange{ Min: 0.0, Max: 10.0, }, diff --git a/histogram_series.go b/histogram_series.go index 3f0b34c..08ba1b9 100644 --- a/histogram_series.go +++ b/histogram_series.go @@ -52,6 +52,6 @@ func (hs HistogramSeries) GetBoundedValue(index int) (x, y1, y2 float64) { // Render implements Series.Render. func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := hs.Style.WithDefaultsFrom(defaults) + style := hs.Style.InheritFrom(defaults) DrawHistogramSeries(r, canvasBox, xrange, yrange, style, hs) } diff --git a/macd_series.go b/macd_series.go index b5ee558..b174b74 100644 --- a/macd_series.go +++ b/macd_series.go @@ -192,7 +192,7 @@ func (macds *MACDSignalSeries) ensureSignal() { // Render renders the series. func (macds *MACDSignalSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := macds.Style.WithDefaultsFrom(defaults) + style := macds.Style.InheritFrom(defaults) DrawLineSeries(r, canvasBox, xrange, yrange, style, macds) } @@ -284,6 +284,6 @@ func (macdl *MACDLineSeries) ensureEMASeries() { // Render renders the series. func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := macdl.Style.WithDefaultsFrom(defaults) + style := macdl.Style.InheritFrom(defaults) DrawLineSeries(r, canvasBox, xrange, yrange, style, macdl) } diff --git a/range.go b/range.go index ddea1ee..9cea1fb 100644 --- a/range.go +++ b/range.go @@ -1,44 +1,41 @@ package chart -import ( - "fmt" - "math" -) - -// Range represents a boundary for a set of numbers. -type Range struct { - Min float64 - Max float64 - Domain int +// NameProvider is a type that returns a name. +type NameProvider interface { + GetName() string } -// IsZero returns if the range has been set or not. -func (r Range) IsZero() bool { - return (r.Min == 0 || math.IsNaN(r.Min)) && - (r.Max == 0 || math.IsNaN(r.Max)) && - r.Domain == 0 +// StyleProvider is a type that returns a style. +type StyleProvider interface { + GetStyle() Style } -// Delta returns the difference between the min and max value. -func (r Range) Delta() float64 { - return r.Max - r.Min +// IsZeroable is a type that returns if it's been set or not. +type IsZeroable interface { + IsZero() bool } -// String returns a simple string for the range. -func (r Range) String() string { - return fmt.Sprintf("Range [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain) +// Stringable is a type that has a string representation. +type Stringable interface { + String() string } -// Translate maps a given value into the range space. -func (r Range) Translate(value float64) int { - normalized := value - r.Min - ratio := normalized / r.Delta() - return int(math.Ceil(ratio * float64(r.Domain))) -} +// Range is a +type Range interface { + Stringable + IsZeroable -// GetRoundedRangeBounds returns some `prettified` range bounds. -func (r Range) GetRoundedRangeBounds() (min, max float64) { - delta := r.Max - r.Min - roundTo := GetRoundToForDelta(delta) - return RoundDown(r.Min, roundTo), RoundUp(r.Max, roundTo) + GetMin() float64 + SetMin(min float64) + + GetMax() float64 + SetMax(max float64) + + GetDelta() float64 + + GetDomain() int + SetDomain(domain int) + + // Translate the range to the domain. + Translate(value float64) int } diff --git a/sma_series.go b/sma_series.go index ca43e2b..245f8c9 100644 --- a/sma_series.go +++ b/sma_series.go @@ -85,6 +85,6 @@ func (sma SMASeries) getAverage(index int) float64 { // Render renders the series. func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := sma.Style.WithDefaultsFrom(defaults) + style := sma.Style.InheritFrom(defaults) DrawLineSeries(r, canvasBox, xrange, yrange, style, sma) } diff --git a/style.go b/style.go index dbfb4fb..51ac94d 100644 --- a/style.go +++ b/style.go @@ -28,6 +28,7 @@ func (s Style) IsZero() bool { return s.StrokeColor.IsZero() && s.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontColor.IsZero() && s.FontSize == 0 && s.Font == nil } +// String returns a text representation of the style. func (s Style) String() string { if s.IsZero() { return "{}" @@ -184,8 +185,18 @@ func (s Style) GetPadding(defaults ...Box) Box { return s.Padding } -// WithDefaultsFrom coalesces two styles into a new style. -func (s Style) WithDefaultsFrom(defaults Style) (final Style) { +// PersistToRenderer passes the style onto a renderer. +func (s Style) PersistToRenderer(r Renderer) { + r.SetStrokeColor(s.GetStrokeColor()) + r.SetStrokeWidth(s.GetStrokeWidth()) + r.SetStrokeDashArray(s.GetStrokeDashArray()) + r.SetFont(s.GetFont()) + r.SetFontColor(s.GetFontColor()) + r.SetFontSize(s.GetFontSize()) +} + +// InheritFrom coalesces two styles into a new style. +func (s Style) InheritFrom(defaults Style) (final Style) { final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor) final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth) final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray) diff --git a/style_test.go b/style_test.go index cb7ffdd..520692e 100644 --- a/style_test.go +++ b/style_test.go @@ -142,7 +142,7 @@ func TestStyleWithDefaultsFrom(t *testing.T) { Padding: DefaultBackgroundPadding, } - coalesced := unset.WithDefaultsFrom(set) + coalesced := unset.InheritFrom(set) assert.Equal(set, coalesced) } diff --git a/tick.go b/tick.go index 850adb7..a372ce3 100644 --- a/tick.go +++ b/tick.go @@ -3,7 +3,7 @@ package chart // GenerateTicksWithStep generates a set of ticks. func GenerateTicksWithStep(ra Range, step float64, vf ValueFormatter) []Tick { var ticks []Tick - min, max := ra.Min, ra.Max + min, max := ra.GetMin(), ra.GetMax() for cursor := min; cursor <= max; cursor += step { ticks = append(ticks, Tick{ Value: cursor, diff --git a/tick_test.go b/tick_test.go index 8d75f01..66fb237 100644 --- a/tick_test.go +++ b/tick_test.go @@ -9,6 +9,6 @@ import ( func TestGenerateTicksWithStep(t *testing.T) { assert := assert.New(t) - ticks := GenerateTicksWithStep(Range{Min: 1.0, Max: 10.0, Domain: 100}, 1.0, FloatValueFormatter) + ticks := GenerateTicksWithStep(&ContinuousRange{Min: 1.0, Max: 10.0, Domain: 100}, 1.0, FloatValueFormatter) assert.Len(ticks, 10) } diff --git a/time_series.go b/time_series.go index 37ba6b9..5287cf7 100644 --- a/time_series.go +++ b/time_series.go @@ -56,6 +56,6 @@ func (ts TimeSeries) GetYAxis() YAxisType { // Render renders the series. func (ts TimeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) { - style := ts.Style.WithDefaultsFrom(defaults) + style := ts.Style.InheritFrom(defaults) DrawLineSeries(r, canvasBox, xrange, yrange, style, ts) } diff --git a/xaxis.go b/xaxis.go index 0f3650e..60a1882 100644 --- a/xaxis.go +++ b/xaxis.go @@ -44,7 +44,7 @@ func (xa XAxis) generateTicks(r Renderer, ra Range, defaults Style, vf ValueForm func (xa XAxis) getTickStep(r Renderer, ra Range, defaults Style, vf ValueFormatter) float64 { tickCount := xa.getTickCount(r, ra, defaults, vf) - step := ra.Delta() / float64(tickCount) + step := ra.GetDelta() / float64(tickCount) return step } @@ -53,8 +53,8 @@ func (xa XAxis) getTickCount(r Renderer, ra Range, defaults Style, vf ValueForma r.SetFontSize(xa.Style.GetFontSize(defaults.GetFontSize(DefaultFontSize))) // take a cut at determining the 'widest' value. - l0 := vf(ra.Min) - ln := vf(ra.Max) + l0 := vf(ra.GetMin()) + ln := vf(ra.GetMax()) ll := l0 if len(ln) > len(l0) { ll = ln @@ -62,7 +62,7 @@ func (xa XAxis) getTickCount(r Renderer, ra Range, defaults Style, vf ValueForma llb := r.MeasureText(ll) textWidth := llb.Width() width := textWidth + DefaultMinimumTickHorizontalSpacing - count := int(math.Ceil(float64(ra.Domain) / float64(width))) + count := int(math.Ceil(float64(ra.GetDomain()) / float64(width))) return count } @@ -76,13 +76,7 @@ func (xa XAxis) GetGridLines(ticks []Tick) []GridLine { // Measure returns the bounds of the axis. func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box { - r.SetStrokeColor(xa.Style.GetStrokeColor(defaults.StrokeColor)) - r.SetStrokeWidth(xa.Style.GetStrokeWidth(defaults.StrokeWidth)) - r.SetStrokeDashArray(xa.Style.GetStrokeDashArray()) - r.SetFont(xa.Style.GetFont(defaults.GetFont())) - r.SetFontColor(xa.Style.GetFontColor(DefaultAxisColor)) - r.SetFontSize(xa.Style.GetFontSize(defaults.GetFontSize())) - + xa.Style.InheritFrom(defaults).PersistToRenderer(r) sort.Sort(Ticks(ticks)) var left, right, top, bottom = math.MaxInt32, 0, math.MaxInt32, 0 @@ -110,12 +104,7 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic // Render renders the axis func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) { - r.SetStrokeColor(xa.Style.GetStrokeColor(defaults.StrokeColor)) - r.SetStrokeWidth(xa.Style.GetStrokeWidth(defaults.StrokeWidth)) - r.SetStrokeDashArray(xa.Style.GetStrokeDashArray()) - r.SetFont(xa.Style.GetFont(defaults.GetFont())) - r.SetFontColor(xa.Style.GetFontColor(DefaultAxisColor)) - r.SetFontSize(xa.Style.GetFontSize(defaults.GetFontSize())) + xa.Style.InheritFrom(defaults).PersistToRenderer(r) r.MoveTo(canvasBox.Left, canvasBox.Bottom) r.LineTo(canvasBox.Right, canvasBox.Bottom) diff --git a/xaxis_test.go b/xaxis_test.go index 933388b..cfc05c5 100644 --- a/xaxis_test.go +++ b/xaxis_test.go @@ -16,7 +16,7 @@ func TestXAxisGetTickCount(t *testing.T) { assert.Nil(err) xa := XAxis{} - xr := Range{Min: 10, Max: 100, Domain: 1024} + xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0, @@ -36,14 +36,14 @@ func TestXAxisGetTickStep(t *testing.T) { assert.Nil(err) xa := XAxis{} - xr := Range{Min: 10, Max: 100, Domain: 1024} + xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0, } vf := FloatValueFormatter step := xa.getTickStep(r, xr, styleDefaults, vf) - assert.Equal(xr.Delta()/16.0, step) + assert.Equal(xr.GetDelta()/16.0, step) } func TestXAxisGetTicks(t *testing.T) { @@ -56,7 +56,7 @@ func TestXAxisGetTicks(t *testing.T) { assert.Nil(err) xa := XAxis{} - xr := Range{Min: 10, Max: 100, Domain: 1024} + xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0, @@ -78,7 +78,7 @@ func TestXAxisGetTicksWithUserDefaults(t *testing.T) { xa := XAxis{ Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, } - xr := Range{Min: 10, Max: 100, Domain: 1024} + xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0, diff --git a/yaxis.go b/yaxis.go index c4557e2..5d7e8cd 100644 --- a/yaxis.go +++ b/yaxis.go @@ -17,9 +17,10 @@ type YAxis struct { ValueFormatter ValueFormatter Range Range - Ticks []Tick - GridLines []GridLine + Ticks []Tick + GridLines []GridLine + GridMajorStyle Style GridMinorStyle Style } @@ -51,7 +52,7 @@ func (ya YAxis) generateTicks(r Renderer, ra Range, defaults Style, vf ValueForm func (ya YAxis) getTickStep(r Renderer, ra Range, defaults Style, vf ValueFormatter) float64 { tickCount := ya.getTickCount(r, ra, defaults, vf) - step := ra.Delta() / float64(tickCount) + step := ra.GetDelta() / float64(tickCount) return step } @@ -59,9 +60,9 @@ func (ya YAxis) getTickCount(r Renderer, ra Range, defaults Style, vf ValueForma r.SetFont(ya.Style.GetFont(defaults.GetFont())) r.SetFontSize(ya.Style.GetFontSize(defaults.GetFontSize(DefaultFontSize))) //given the domain, figure out how many ticks we can draw ... - label := vf(ra.Min) + label := vf(ra.GetMin()) tb := r.MeasureText(label) - count := int(math.Ceil(float64(ra.Domain) / float64(tb.Height()+DefaultMinimumTickVerticalSpacing))) + count := int(math.Ceil(float64(ra.GetDomain()) / float64(tb.Height()+DefaultMinimumTickVerticalSpacing))) return count } @@ -75,11 +76,7 @@ func (ya YAxis) GetGridLines(ticks []Tick) []GridLine { // Measure returns the bounds of the axis. func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box { - r.SetStrokeColor(ya.Style.GetStrokeColor(defaults.StrokeColor)) - r.SetStrokeWidth(ya.Style.GetStrokeWidth(defaults.StrokeWidth)) - r.SetFont(ya.Style.GetFont(defaults.GetFont())) - r.SetFontColor(ya.Style.GetFontColor(DefaultAxisColor)) - r.SetFontSize(ya.Style.GetFontSize(defaults.GetFontSize())) + ya.Style.InheritFrom(defaults).PersistToRenderer(r) sort.Sort(Ticks(ticks)) @@ -122,11 +119,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic // Render renders the axis. func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) { - r.SetStrokeColor(ya.Style.GetStrokeColor(defaults.StrokeColor)) - r.SetStrokeWidth(ya.Style.GetStrokeWidth(defaults.StrokeWidth)) - r.SetFont(ya.Style.GetFont(defaults.GetFont())) - r.SetFontColor(ya.Style.GetFontColor(DefaultAxisColor)) - r.SetFontSize(ya.Style.GetFontSize(defaults.GetFontSize(DefaultFontSize))) + ya.Style.InheritFrom(defaults).PersistToRenderer(r) sort.Sort(Ticks(ticks)) diff --git a/yaxis_test.go b/yaxis_test.go index fdd7a72..c7c19de 100644 --- a/yaxis_test.go +++ b/yaxis_test.go @@ -16,7 +16,7 @@ func TestYAxisGetTickCount(t *testing.T) { assert.Nil(err) ya := YAxis{} - yr := Range{Min: 10, Max: 100, Domain: 1024} + yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0, @@ -36,14 +36,14 @@ func TestYAxisGetTickStep(t *testing.T) { assert.Nil(err) ya := YAxis{} - yr := Range{Min: 10, Max: 100, Domain: 1024} + yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0, } vf := FloatValueFormatter step := ya.getTickStep(r, yr, styleDefaults, vf) - assert.Equal(yr.Delta()/34.0, step) + assert.Equal(yr.GetDelta()/34.0, step) } func TestYAxisGetTicks(t *testing.T) { @@ -56,7 +56,7 @@ func TestYAxisGetTicks(t *testing.T) { assert.Nil(err) ya := YAxis{} - yr := Range{Min: 10, Max: 100, Domain: 1024} + yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0, @@ -78,7 +78,7 @@ func TestYAxisGetTicksWithUserDefaults(t *testing.T) { ya := YAxis{ Ticks: []Tick{{Value: 1.0, Label: "1.0"}}, } - yr := Range{Min: 10, Max: 100, Domain: 1024} + yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024} styleDefaults := Style{ Font: f, FontSize: 10.0,