need to flesh out this test more.
This commit is contained in:
parent
3d9cf0da0c
commit
84df29b1c6
8 changed files with 141 additions and 76 deletions
|
@ -6,6 +6,7 @@ go:
|
|||
sudo: false
|
||||
|
||||
before:
|
||||
- go get -u github.com/blendlabs/go-assert
|
||||
- go get ./...
|
||||
|
||||
script:
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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: "!!"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
37
pie_chart.go
37
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 {
|
||||
|
|
31
pie_chart_test.go
Normal file
31
pie_chart_test.go
Normal file
|
@ -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())
|
||||
}
|
18
util.go
18
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
|
||||
|
|
46
value.go
Normal file
46
value.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue