diff --git a/README.md b/README.md index 5fd93a5..96b8c2d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ graph := chart.Chart{ }, Annotations: []chart.Annotation{ chart.Annotation{ - X: float64(xvalues[len(xvalues)-1].Unix()), //todo: helpers for this. + X: chart.TimeToFloat64(xvalues[len(xvalues)-1]), Y: yvalues[len(yvalues)-1], Label: chart.FloatValueFormatter(yvalues[len(yvalues)-1]), }, diff --git a/annotation_series.go b/annotation_series.go index aeed26d..22ed729 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -1,45 +1,6 @@ package chart -import ( - "math" - "time" -) - -// CreateContinuousSeriesLastValueLabel returns a (1) value annotation series. -func CreateContinuousSeriesLastValueLabel(name string, xvalues, yvalues []float64, valueFormatter ValueFormatter) AnnotationSeries { - return AnnotationSeries{ - Name: name, - Style: Style{ - Show: true, - StrokeColor: GetDefaultSeriesStrokeColor(0), - }, - Annotations: []Annotation{ - Annotation{ - X: xvalues[len(xvalues)-1], - Y: yvalues[len(yvalues)-1], - Label: valueFormatter(yvalues[len(yvalues)-1]), - }, - }, - } -} - -// CreateTimeSeriesLastValueLabel returns a (1) value annotation series. -func CreateTimeSeriesLastValueLabel(name string, xvalues []time.Time, yvalues []float64, valueFormatter ValueFormatter) AnnotationSeries { - return AnnotationSeries{ - Name: name, - Style: Style{ - Show: true, - StrokeColor: GetDefaultSeriesStrokeColor(0), - }, - Annotations: []Annotation{ - Annotation{ - X: float64(xvalues[len(xvalues)-1].Unix()), - Y: yvalues[len(yvalues)-1], - Label: valueFormatter(yvalues[len(yvalues)-1]), - }, - }, - } -} +import "math" // Annotation is a label on the chart. type Annotation struct { diff --git a/style.go b/style.go index 5ffe2f5..0274535 100644 --- a/style.go +++ b/style.go @@ -15,17 +15,15 @@ type Style struct { StrokeWidth float64 StrokeColor drawing.Color - - FillColor drawing.Color - - FontSize float64 - FontColor drawing.Color - Font *truetype.Font + FillColor drawing.Color + FontSize float64 + FontColor drawing.Color + Font *truetype.Font } // IsZero returns if the object is set or not. func (s Style) IsZero() bool { - return s.StrokeColor.IsZero() && s.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontSize == 0 && s.Font == nil + return s.StrokeColor.IsZero() && s.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontColor.IsZero() && s.FontSize == 0 && s.Font == nil } // GetStrokeColor returns the stroke color. @@ -107,12 +105,12 @@ func (s Style) GetPadding(defaults ...Box) Box { // WithDefaultsFrom coalesces two styles into a new style. func (s Style) WithDefaultsFrom(defaults Style) (final Style) { + final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor) + final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth) final.FillColor = s.GetFillColor(defaults.FillColor) final.FontColor = s.GetFontColor(defaults.FontColor) final.Font = s.GetFont(defaults.Font) final.Padding = s.GetPadding(defaults.Padding) - final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor) - final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth) return } diff --git a/style_test.go b/style_test.go new file mode 100644 index 0000000..cb7ffdd --- /dev/null +++ b/style_test.go @@ -0,0 +1,237 @@ +package chart + +import ( + "strings" + "testing" + + "github.com/blendlabs/go-assert" + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/drawing" +) + +func TestStyleIsZero(t *testing.T) { + assert := assert.New(t) + zero := Style{} + assert.True(zero.IsZero()) + + strokeColor := Style{StrokeColor: drawing.ColorWhite} + assert.False(strokeColor.IsZero()) + + fillColor := Style{FillColor: drawing.ColorWhite} + assert.False(fillColor.IsZero()) + + strokeWidth := Style{StrokeWidth: 5.0} + assert.False(strokeWidth.IsZero()) + + fontSize := Style{FontSize: 12.0} + assert.False(fontSize.IsZero()) + + fontColor := Style{FontColor: drawing.ColorWhite} + assert.False(fontColor.IsZero()) + + font := Style{Font: &truetype.Font{}} + assert.False(font.IsZero()) +} + +func TestStyleGetStrokeColor(t *testing.T) { + assert := assert.New(t) + + unset := Style{} + assert.Equal(drawing.ColorTransparent, unset.GetStrokeColor()) + assert.Equal(drawing.ColorWhite, unset.GetStrokeColor(drawing.ColorWhite)) + + set := Style{StrokeColor: drawing.ColorWhite} + assert.Equal(drawing.ColorWhite, set.GetStrokeColor()) + assert.Equal(drawing.ColorWhite, set.GetStrokeColor(drawing.ColorBlack)) +} + +func TestStyleGetFillColor(t *testing.T) { + assert := assert.New(t) + + unset := Style{} + assert.Equal(drawing.ColorTransparent, unset.GetFillColor()) + assert.Equal(drawing.ColorWhite, unset.GetFillColor(drawing.ColorWhite)) + + set := Style{FillColor: drawing.ColorWhite} + assert.Equal(drawing.ColorWhite, set.GetFillColor()) + assert.Equal(drawing.ColorWhite, set.GetFillColor(drawing.ColorBlack)) +} + +func TestStyleGetStrokeWidth(t *testing.T) { + assert := assert.New(t) + + unset := Style{} + assert.Equal(DefaultStrokeWidth, unset.GetStrokeWidth()) + assert.Equal(DefaultStrokeWidth+1, unset.GetStrokeWidth(DefaultStrokeWidth+1)) + + set := Style{StrokeWidth: DefaultStrokeWidth + 2} + assert.Equal(DefaultStrokeWidth+2, set.GetStrokeWidth()) + assert.Equal(DefaultStrokeWidth+2, set.GetStrokeWidth(DefaultStrokeWidth+1)) +} + +func TestStyleGetFontSize(t *testing.T) { + assert := assert.New(t) + + unset := Style{} + assert.Equal(DefaultFontSize, unset.GetFontSize()) + assert.Equal(DefaultFontSize+1, unset.GetFontSize(DefaultFontSize+1)) + + set := Style{FontSize: DefaultFontSize + 2} + assert.Equal(DefaultFontSize+2, set.GetFontSize()) + assert.Equal(DefaultFontSize+2, set.GetFontSize(DefaultFontSize+1)) +} + +func TestStyleGetFontColor(t *testing.T) { + assert := assert.New(t) + + unset := Style{} + assert.Equal(drawing.ColorTransparent, unset.GetFontColor()) + assert.Equal(drawing.ColorWhite, unset.GetFontColor(drawing.ColorWhite)) + + set := Style{FontColor: drawing.ColorWhite} + assert.Equal(drawing.ColorWhite, set.GetFontColor()) + assert.Equal(drawing.ColorWhite, set.GetFontColor(drawing.ColorBlack)) +} + +func TestStyleGetFont(t *testing.T) { + assert := assert.New(t) + + f, err := GetDefaultFont() + assert.Nil(err) + + unset := Style{} + assert.Nil(unset.GetFont()) + assert.Equal(f, unset.GetFont(f)) + + set := Style{Font: f} + assert.NotNil(set.GetFont()) +} + +func TestStyleGetPadding(t *testing.T) { + assert := assert.New(t) + + unset := Style{} + assert.True(unset.GetPadding().IsZero()) + assert.False(unset.GetPadding(DefaultBackgroundPadding).IsZero()) + assert.Equal(DefaultBackgroundPadding, unset.GetPadding(DefaultBackgroundPadding)) + + set := Style{Padding: DefaultBackgroundPadding} + assert.False(set.GetPadding().IsZero()) + assert.Equal(DefaultBackgroundPadding, set.GetPadding()) + assert.Equal(DefaultBackgroundPadding, set.GetPadding(Box{ + Top: DefaultBackgroundPadding.Top + 1, + Left: DefaultBackgroundPadding.Left + 1, + Right: DefaultBackgroundPadding.Right + 1, + Bottom: DefaultBackgroundPadding.Bottom + 1, + })) +} + +func TestStyleWithDefaultsFrom(t *testing.T) { + assert := assert.New(t) + + f, err := GetDefaultFont() + assert.Nil(err) + + unset := Style{} + set := Style{ + StrokeColor: drawing.ColorWhite, + StrokeWidth: 5.0, + FillColor: drawing.ColorWhite, + FontColor: drawing.ColorWhite, + Font: f, + Padding: DefaultBackgroundPadding, + } + + coalesced := unset.WithDefaultsFrom(set) + assert.Equal(set, coalesced) +} + +func TestStyleSVG(t *testing.T) { + assert := assert.New(t) + + f, err := GetDefaultFont() + assert.Nil(err) + + set := Style{ + StrokeColor: drawing.ColorWhite, + StrokeWidth: 5.0, + FillColor: drawing.ColorWhite, + FontColor: drawing.ColorWhite, + Font: f, + Padding: DefaultBackgroundPadding, + } + + svgString := set.SVG(DefaultDPI) + assert.NotEmpty(svgString) + assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)")) + assert.True(strings.Contains(svgString, "stroke-width:5")) + assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)")) +} + +func TestStyleSVGStroke(t *testing.T) { + assert := assert.New(t) + + set := Style{ + StrokeColor: drawing.ColorWhite, + StrokeWidth: 5.0, + FillColor: drawing.ColorWhite, + FontColor: drawing.ColorWhite, + Padding: DefaultBackgroundPadding, + } + svgStroke := set.SVGStroke() + assert.False(svgStroke.StrokeColor.IsZero()) + assert.NotZero(svgStroke.StrokeWidth) + assert.True(svgStroke.FillColor.IsZero()) + assert.True(svgStroke.FontColor.IsZero()) +} + +func TestStyleSVGFill(t *testing.T) { + assert := assert.New(t) + + set := Style{ + StrokeColor: drawing.ColorWhite, + StrokeWidth: 5.0, + FillColor: drawing.ColorWhite, + FontColor: drawing.ColorWhite, + Padding: DefaultBackgroundPadding, + } + svgFill := set.SVGFill() + assert.False(svgFill.FillColor.IsZero()) + assert.Zero(svgFill.StrokeWidth) + assert.True(svgFill.StrokeColor.IsZero()) + assert.True(svgFill.FontColor.IsZero()) +} + +func TestStyleSVGFillAndStroke(t *testing.T) { + assert := assert.New(t) + + set := Style{ + StrokeColor: drawing.ColorWhite, + StrokeWidth: 5.0, + FillColor: drawing.ColorWhite, + FontColor: drawing.ColorWhite, + Padding: DefaultBackgroundPadding, + } + svgFillAndStroke := set.SVGFillAndStroke() + assert.False(svgFillAndStroke.FillColor.IsZero()) + assert.NotZero(svgFillAndStroke.StrokeWidth) + assert.False(svgFillAndStroke.StrokeColor.IsZero()) + assert.True(svgFillAndStroke.FontColor.IsZero()) +} + +func TestStyleSVGText(t *testing.T) { + assert := assert.New(t) + + set := Style{ + StrokeColor: drawing.ColorWhite, + StrokeWidth: 5.0, + FillColor: drawing.ColorWhite, + FontColor: drawing.ColorWhite, + Padding: DefaultBackgroundPadding, + } + svgStroke := set.SVGText() + assert.True(svgStroke.StrokeColor.IsZero()) + assert.Zero(svgStroke.StrokeWidth) + assert.True(svgStroke.FillColor.IsZero()) + assert.False(svgStroke.FontColor.IsZero()) +} diff --git a/tick.go b/tick.go index 374a621..b9f7a78 100644 --- a/tick.go +++ b/tick.go @@ -1,5 +1,17 @@ package chart +// GenerateTicksWithStep generates a set of ticks. +func GenerateTicksWithStep(ra Range, step float64, vf ValueFormatter) []Tick { + var ticks []Tick + for cursor := ra.Min; cursor < ra.Max; cursor += step { + ticks = append(ticks, Tick{ + Value: cursor, + Label: vf(cursor), + }) + } + return ticks +} + // Tick represents a label on an axis. type Tick struct { Value float64 diff --git a/tick_test.go b/tick_test.go new file mode 100644 index 0000000..58a733f --- /dev/null +++ b/tick_test.go @@ -0,0 +1 @@ +package chart diff --git a/util.go b/util.go index 406bc3f..1f8fc7c 100644 --- a/util.go +++ b/util.go @@ -5,6 +5,11 @@ import ( "time" ) +// TimeToFloat64 returns a float64 representation of a time. +func TimeToFloat64(t time.Time) float64 { + return float64(t.Unix()) +} + // MinAndMax returns both the min and max in one pass. func MinAndMax(values ...float64) (min float64, max float64) { if len(values) == 0 { diff --git a/value_formatter_test.go b/value_formatter_test.go new file mode 100644 index 0000000..83b347e --- /dev/null +++ b/value_formatter_test.go @@ -0,0 +1,38 @@ +package chart + +import ( + "testing" + "time" + + "github.com/blendlabs/go-assert" +) + +func TestTimeValueFormatterWithFormat(t *testing.T) { + assert := assert.New(t) + + d := time.Now() + di := d.Unix() + df := float64(di) + + s := TimeValueFormatterWithFormat(d, DefaultDateFormat) + si := TimeValueFormatterWithFormat(di, DefaultDateFormat) + sf := TimeValueFormatterWithFormat(df, DefaultDateFormat) + assert.Equal(s, si) + assert.Equal(s, sf) + + sd := TimeValueFormatter(d) + sdi := TimeValueFormatter(di) + sdf := TimeValueFormatter(df) + assert.Equal(s, sd) + assert.Equal(s, sdi) + assert.Equal(s, sdf) +} + +func TestFloatValueFormatterWithFormat(t *testing.T) { + assert := assert.New(t) + + v := 123.456 + sv := FloatValueFormatterWithFormat(v, "%.3f") + assert.Equal("123.456", sv) + assert.Equal("", FloatValueFormatterWithFormat(123, "%.3f")) +} diff --git a/xaxis.go b/xaxis.go index 0f02be9..46277db 100644 --- a/xaxis.go +++ b/xaxis.go @@ -37,7 +37,13 @@ func (xa XAxis) GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick { func (xa XAxis) generateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick { step := xa.getTickStep(r, ra, vf) - return xa.generateTicksWithStep(ra, step, vf) + return GenerateTicksWithStep(ra, step, vf) +} + +func (xa XAxis) getTickStep(r Renderer, ra Range, vf ValueFormatter) float64 { + tickCount := xa.getTickCount(r, ra, vf) + step := ra.Delta() / float64(tickCount) + return step } func (xa XAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int { @@ -58,27 +64,6 @@ func (xa XAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int { return count } -func (xa XAxis) getTickStep(r Renderer, ra Range, vf ValueFormatter) float64 { - tickCount := xa.getTickCount(r, ra, vf) - step := ra.Delta() / float64(tickCount) - return step -} - -func (xa XAxis) generateTicksWithStep(ra Range, step float64, vf ValueFormatter) []Tick { - var ticks []Tick - for cursor := ra.Min; cursor < ra.Max; cursor += step { - ticks = append(ticks, Tick{ - Value: cursor, - Label: vf(cursor), - }) - - if len(ticks) == 20 { - return ticks - } - } - return ticks -} - // Render renders the axis func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, ticks []Tick) { tickFontSize := xa.Style.GetFontSize(DefaultFontSize) diff --git a/yaxis.go b/yaxis.go index f4ed1f4..5ad2619 100644 --- a/yaxis.go +++ b/yaxis.go @@ -36,14 +36,7 @@ func (ya YAxis) GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick { func (ya YAxis) generateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick { step := ya.getTickStep(r, ra, vf) - return ya.generateTicksWithStep(ra, step, vf) -} - -func (ya YAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int { - textHeight := int(ya.Style.GetFontSize(DefaultFontSize)) - height := textHeight + DefaultMinimumTickVerticalSpacing - count := int(math.Ceil(float64(ra.Domain) / float64(height))) - return count + return GenerateTicksWithStep(ra, step, vf) } func (ya YAxis) getTickStep(r Renderer, ra Range, vf ValueFormatter) float64 { @@ -51,18 +44,11 @@ func (ya YAxis) getTickStep(r Renderer, ra Range, vf ValueFormatter) float64 { return ra.Delta() / float64(tickCount) } -func (ya YAxis) generateTicksWithStep(ra Range, step float64, vf ValueFormatter) []Tick { - var ticks []Tick - for cursor := ra.Min; cursor < ra.Max; cursor += step { - ticks = append(ticks, Tick{ - Value: cursor, - Label: vf(cursor), - }) - if len(ticks) == 20 { - return ticks - } - } - return ticks +func (ya YAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int { + textHeight := int(ya.Style.GetFontSize(DefaultFontSize)) + height := textHeight + DefaultMinimumTickVerticalSpacing + count := int(math.Ceil(float64(ra.Domain) / float64(height))) + return count } // Render renders the axis.