diff --git a/.travis.yml b/.travis.yml index 19db2a2..82e7d59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ go: sudo: false before: + - go get -u github.com/blendlabs/go-assert - go get ./... script: diff --git a/annotation_series.go b/annotation_series.go index 405fe55..1b2c3b0 100644 --- a/annotation_series.go +++ b/annotation_series.go @@ -2,18 +2,12 @@ package chart import "math" -// Annotation is a label on the chart. -type Annotation struct { - X, Y float64 - Label string -} - // AnnotationSeries is a series of labels on the chart. type AnnotationSeries struct { Name string Style Style YAxis YAxisType - Annotations []Annotation + Annotations []Value2 } // GetName returns the name of the time series. @@ -31,6 +25,17 @@ func (as AnnotationSeries) GetYAxis() YAxisType { return as.YAxis } +func (as AnnotationSeries) annotationStyleDefaults(defaults Style) Style { + return Style{ + Font: defaults.Font, + FillColor: DefaultAnnotationFillColor, + FontSize: DefaultAnnotationFontSize, + StrokeColor: defaults.StrokeColor, + StrokeWidth: defaults.StrokeWidth, + Padding: DefaultAnnotationPadding, + } +} + // Measure returns a bounds box of the series. func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box { box := Box{ @@ -40,17 +45,11 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran Bottom: 0, } if as.Style.IsZero() || as.Style.Show { - style := as.Style.InheritFrom(Style{ - Font: defaults.Font, - FillColor: DefaultAnnotationFillColor, - FontSize: DefaultAnnotationFontSize, - StrokeColor: defaults.StrokeColor, - StrokeWidth: defaults.StrokeWidth, - Padding: DefaultAnnotationPadding, - }) + seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) for _, a := range as.Annotations { - lx := canvasBox.Left + xrange.Translate(a.X) - ly := canvasBox.Bottom - yrange.Translate(a.Y) + style := a.Style.InheritFrom(seriesStyle) + lx := canvasBox.Left + xrange.Translate(a.XValue) + ly := canvasBox.Bottom - yrange.Translate(a.YValue) ab := MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label) box.Top = MinInt(box.Top, ab.Top) box.Left = MinInt(box.Left, ab.Left) @@ -64,18 +63,11 @@ 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.InheritFrom(Style{ - Font: defaults.Font, - FontColor: DefaultTextColor, - FillColor: DefaultAnnotationFillColor, - FontSize: DefaultAnnotationFontSize, - StrokeColor: defaults.StrokeColor, - StrokeWidth: defaults.StrokeWidth, - Padding: DefaultAnnotationPadding, - }) + seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults)) for _, a := range as.Annotations { - lx := canvasBox.Left + xrange.Translate(a.X) - ly := canvasBox.Bottom - yrange.Translate(a.Y) + style := a.Style.InheritFrom(seriesStyle) + lx := canvasBox.Left + xrange.Translate(a.XValue) + ly := canvasBox.Bottom - yrange.Translate(a.YValue) DrawAnnotation(r, canvasBox, style, lx, ly, a.Label) } } diff --git a/annotation_series_test.go b/annotation_series_test.go index 26953d9..4437f82 100644 --- a/annotation_series_test.go +++ b/annotation_series_test.go @@ -15,11 +15,11 @@ func TestAnnotationSeriesMeasure(t *testing.T) { Style: Style{ Show: true, }, - Annotations: []Annotation{ - {X: 1.0, Y: 1.0, Label: "1.0"}, - {X: 2.0, Y: 2.0, Label: "2.0"}, - {X: 3.0, Y: 3.0, Label: "3.0"}, - {X: 4.0, Y: 4.0, Label: "4.0"}, + Annotations: []Value2{ + {XValue: 1.0, YValue: 1.0, Label: "1.0"}, + {XValue: 2.0, YValue: 2.0, Label: "2.0"}, + {XValue: 3.0, YValue: 3.0, Label: "3.0"}, + {XValue: 4.0, YValue: 4.0, Label: "4.0"}, }, } @@ -68,11 +68,11 @@ func TestAnnotationSeriesRender(t *testing.T) { FillColor: drawing.ColorWhite, StrokeColor: drawing.ColorBlack, }, - Annotations: []Annotation{ - {X: 1.0, Y: 1.0, Label: "1.0"}, - {X: 2.0, Y: 2.0, Label: "2.0"}, - {X: 3.0, Y: 3.0, Label: "3.0"}, - {X: 4.0, Y: 4.0, Label: "4.0"}, + Annotations: []Value2{ + {XValue: 1.0, YValue: 1.0, Label: "1.0"}, + {XValue: 2.0, YValue: 2.0, Label: "2.0"}, + {XValue: 3.0, YValue: 3.0, Label: "3.0"}, + {XValue: 4.0, YValue: 4.0, Label: "4.0"}, }, } diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 80ea608..4054ca5 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -13,14 +13,14 @@ func drawChart(res http.ResponseWriter, req *http.Request) { Canvas: chart.Style{ FillColor: chart.ColorLightGray, }, - Values: []chart.PieChartValue{ - {Value: 0.2, Label: "Blue"}, - {Value: 0.2, Label: "Green"}, - {Value: 0.2, Label: "Gray"}, - {Value: 0.1, Label: "Orange"}, - {Value: 0.1, Label: "HEANG"}, - {Value: 0.1, Label: "??"}, - {Value: 0.1, Label: "!!"}, + Values: []chart.Value{ + {Value: 10, Label: "Blue"}, + {Value: 9, Label: "Green"}, + {Value: 8, Label: "Gray"}, + {Value: 7, Label: "Orange"}, + {Value: 6, Label: "HEANG"}, + {Value: 5, Label: "??"}, + {Value: 2, Label: "!!"}, }, } diff --git a/pie_chart.go b/pie_chart.go index a9dfd4a..9203099 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -7,13 +7,6 @@ import ( "github.com/golang/freetype/truetype" ) -// PieChartValue is a slice of a pie-chart. -type PieChartValue struct { - Style Style - Label string - Value float64 -} - // PieChart is a chart that draws sections of a circle based on percentages. type PieChart struct { Title string @@ -29,7 +22,7 @@ type PieChart struct { Font *truetype.Font defaultFont *truetype.Font - Values []PieChartValue + Values []Value Elements []Renderable } @@ -94,11 +87,8 @@ func (pc PieChart) Render(rp RendererProvider, w io.Writer) error { pc.drawBackground(r) pc.drawCanvas(r, canvasBox) - valuesWithPlaceholder, err := pc.finalizeValues(pc.Values) - if err != nil { - return err - } - pc.drawSlices(r, canvasBox, valuesWithPlaceholder) + finalValues := pc.finalizeValues(pc.Values) + pc.drawSlices(r, canvasBox, finalValues) pc.drawTitle(r) for _, a := range pc.Elements { a(r, canvasBox, pc.styleDefaultsElements()) @@ -137,7 +127,7 @@ func (pc PieChart) drawTitle(r Renderer) { } } -func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []PieChartValue) { +func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) { cx, cy := canvasBox.Center() diameter := MinInt(canvasBox.Width(), canvasBox.Height()) radius := float64(diameter >> 1) @@ -172,6 +162,7 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []PieChartValue) tb := r.MeasureText(v.Label) lx = lx - (tb.Width() >> 1) + ly = ly + (tb.Height() >> 1) r.Text(v.Label, lx, ly) } @@ -179,22 +170,8 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []PieChartValue) } } -func (pc PieChart) finalizeValues(values []PieChartValue) ([]PieChartValue, error) { - var total float64 - for _, v := range values { - total += v.Value - if total > 1.0 { - return nil, errors.New("Values total exceeded 1.0; please normalize pie chart values to [0,1.0)") - } - } - remainder := 1.0 - total - if RoundDown(remainder, 0.0001) > 0 { - return append(values, PieChartValue{ - Style: pc.styleDefaultsPieChartValue(), - Value: remainder, - }), nil - } - return values, nil +func (pc PieChart) finalizeValues(values []Value) []Value { + return Values(values).Normalize() } func (pc PieChart) getDefaultCanvasBox() Box { diff --git a/pie_chart_test.go b/pie_chart_test.go new file mode 100644 index 0000000..2f536cd --- /dev/null +++ b/pie_chart_test.go @@ -0,0 +1,31 @@ +package chart + +import ( + "bytes" + "testing" + + assert "github.com/blendlabs/go-assert" +) + +func TestPieChart(t *testing.T) { + assert := assert.New(t) + + pie := PieChart{ + Canvas: Style{ + FillColor: ColorLightGray, + }, + Values: []Value{ + {Value: 10, Label: "Blue"}, + {Value: 9, Label: "Green"}, + {Value: 8, Label: "Gray"}, + {Value: 7, Label: "Orange"}, + {Value: 6, Label: "HEANG"}, + {Value: 5, Label: "??"}, + {Value: 2, Label: "!!"}, + }, + } + + b := bytes.NewBuffer([]byte{}) + pie.Render(PNG, b) + assert.NotZero(b.Len()) +} diff --git a/util.go b/util.go index 6039142..f391d7e 100644 --- a/util.go +++ b/util.go @@ -176,6 +176,24 @@ func SeqRand(samples int, scale float64) []float64 { return values } +// Sum sums a set of values. +func Sum(values ...float64) float64 { + var total float64 + for _, v := range values { + total += v + } + return total +} + +// SumInt sums a set of values. +func SumInt(values ...int) int { + var total int + for _, v := range values { + total += v + } + return total +} + // SeqDays generates a sequence of timestamps by day, from -days to today. func SeqDays(days int) []time.Time { var values []time.Time diff --git a/value.go b/value.go new file mode 100644 index 0000000..0a2a27d --- /dev/null +++ b/value.go @@ -0,0 +1,46 @@ +package chart + +// Value is a chart value. +type Value struct { + Style Style + Label string + Value float64 +} + +// Values is an array of Value. +type Values []Value + +// Values returns the values. +func (vs Values) Values() []float64 { + values := make([]float64, len(vs)) + for index, v := range vs { + values[index] = v.Value + } + return values +} + +// ValuesNormalized returns normalized values. +func (vs Values) ValuesNormalized() []float64 { + return Normalize(vs.Values()...) +} + +// Normalize returns the values normalized. +func (vs Values) Normalize() []Value { + output := make([]Value, len(vs)) + total := Sum(vs.Values()...) + for index, v := range vs { + output[index] = Value{ + Style: v.Style, + Label: v.Label, + Value: (v.Value / total), + } + } + return output +} + +// Value2 is a two axis value. +type Value2 struct { + Style Style + Label string + XValue, YValue float64 +}