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
|
sudo: false
|
||||||
|
|
||||||
before:
|
before:
|
||||||
|
- go get -u github.com/blendlabs/go-assert
|
||||||
- go get ./...
|
- go get ./...
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
|
|
@ -2,18 +2,12 @@ package chart
|
||||||
|
|
||||||
import "math"
|
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.
|
// AnnotationSeries is a series of labels on the chart.
|
||||||
type AnnotationSeries struct {
|
type AnnotationSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis YAxisType
|
||||||
Annotations []Annotation
|
Annotations []Value2
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetName returns the name of the time series.
|
// GetName returns the name of the time series.
|
||||||
|
@ -31,6 +25,17 @@ func (as AnnotationSeries) GetYAxis() YAxisType {
|
||||||
return as.YAxis
|
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.
|
// Measure returns a bounds box of the series.
|
||||||
func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
|
func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
|
||||||
box := Box{
|
box := Box{
|
||||||
|
@ -40,17 +45,11 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
|
||||||
Bottom: 0,
|
Bottom: 0,
|
||||||
}
|
}
|
||||||
if as.Style.IsZero() || as.Style.Show {
|
if as.Style.IsZero() || as.Style.Show {
|
||||||
style := as.Style.InheritFrom(Style{
|
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
|
||||||
Font: defaults.Font,
|
|
||||||
FillColor: DefaultAnnotationFillColor,
|
|
||||||
FontSize: DefaultAnnotationFontSize,
|
|
||||||
StrokeColor: defaults.StrokeColor,
|
|
||||||
StrokeWidth: defaults.StrokeWidth,
|
|
||||||
Padding: DefaultAnnotationPadding,
|
|
||||||
})
|
|
||||||
for _, a := range as.Annotations {
|
for _, a := range as.Annotations {
|
||||||
lx := canvasBox.Left + xrange.Translate(a.X)
|
style := a.Style.InheritFrom(seriesStyle)
|
||||||
ly := canvasBox.Bottom - yrange.Translate(a.Y)
|
lx := canvasBox.Left + xrange.Translate(a.XValue)
|
||||||
|
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
|
||||||
ab := MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
ab := MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
||||||
box.Top = MinInt(box.Top, ab.Top)
|
box.Top = MinInt(box.Top, ab.Top)
|
||||||
box.Left = MinInt(box.Left, ab.Left)
|
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.
|
// Render draws the series.
|
||||||
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
if as.Style.IsZero() || as.Style.Show {
|
if as.Style.IsZero() || as.Style.Show {
|
||||||
style := as.Style.InheritFrom(Style{
|
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
|
||||||
Font: defaults.Font,
|
|
||||||
FontColor: DefaultTextColor,
|
|
||||||
FillColor: DefaultAnnotationFillColor,
|
|
||||||
FontSize: DefaultAnnotationFontSize,
|
|
||||||
StrokeColor: defaults.StrokeColor,
|
|
||||||
StrokeWidth: defaults.StrokeWidth,
|
|
||||||
Padding: DefaultAnnotationPadding,
|
|
||||||
})
|
|
||||||
for _, a := range as.Annotations {
|
for _, a := range as.Annotations {
|
||||||
lx := canvasBox.Left + xrange.Translate(a.X)
|
style := a.Style.InheritFrom(seriesStyle)
|
||||||
ly := canvasBox.Bottom - yrange.Translate(a.Y)
|
lx := canvasBox.Left + xrange.Translate(a.XValue)
|
||||||
|
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
|
||||||
DrawAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
DrawAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,11 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
|
||||||
Style: Style{
|
Style: Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
},
|
},
|
||||||
Annotations: []Annotation{
|
Annotations: []Value2{
|
||||||
{X: 1.0, Y: 1.0, Label: "1.0"},
|
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||||
{X: 2.0, Y: 2.0, Label: "2.0"},
|
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||||
{X: 3.0, Y: 3.0, Label: "3.0"},
|
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||||
{X: 4.0, Y: 4.0, Label: "4.0"},
|
{XValue: 4.0, YValue: 4.0, Label: "4.0"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,11 +68,11 @@ func TestAnnotationSeriesRender(t *testing.T) {
|
||||||
FillColor: drawing.ColorWhite,
|
FillColor: drawing.ColorWhite,
|
||||||
StrokeColor: drawing.ColorBlack,
|
StrokeColor: drawing.ColorBlack,
|
||||||
},
|
},
|
||||||
Annotations: []Annotation{
|
Annotations: []Value2{
|
||||||
{X: 1.0, Y: 1.0, Label: "1.0"},
|
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||||
{X: 2.0, Y: 2.0, Label: "2.0"},
|
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||||
{X: 3.0, Y: 3.0, Label: "3.0"},
|
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||||
{X: 4.0, Y: 4.0, Label: "4.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{
|
Canvas: chart.Style{
|
||||||
FillColor: chart.ColorLightGray,
|
FillColor: chart.ColorLightGray,
|
||||||
},
|
},
|
||||||
Values: []chart.PieChartValue{
|
Values: []chart.Value{
|
||||||
{Value: 0.2, Label: "Blue"},
|
{Value: 10, Label: "Blue"},
|
||||||
{Value: 0.2, Label: "Green"},
|
{Value: 9, Label: "Green"},
|
||||||
{Value: 0.2, Label: "Gray"},
|
{Value: 8, Label: "Gray"},
|
||||||
{Value: 0.1, Label: "Orange"},
|
{Value: 7, Label: "Orange"},
|
||||||
{Value: 0.1, Label: "HEANG"},
|
{Value: 6, Label: "HEANG"},
|
||||||
{Value: 0.1, Label: "??"},
|
{Value: 5, Label: "??"},
|
||||||
{Value: 0.1, Label: "!!"},
|
{Value: 2, Label: "!!"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
37
pie_chart.go
37
pie_chart.go
|
@ -7,13 +7,6 @@ import (
|
||||||
"github.com/golang/freetype/truetype"
|
"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.
|
// PieChart is a chart that draws sections of a circle based on percentages.
|
||||||
type PieChart struct {
|
type PieChart struct {
|
||||||
Title string
|
Title string
|
||||||
|
@ -29,7 +22,7 @@ type PieChart struct {
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
defaultFont *truetype.Font
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
Values []PieChartValue
|
Values []Value
|
||||||
Elements []Renderable
|
Elements []Renderable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,11 +87,8 @@ func (pc PieChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
pc.drawBackground(r)
|
pc.drawBackground(r)
|
||||||
pc.drawCanvas(r, canvasBox)
|
pc.drawCanvas(r, canvasBox)
|
||||||
|
|
||||||
valuesWithPlaceholder, err := pc.finalizeValues(pc.Values)
|
finalValues := pc.finalizeValues(pc.Values)
|
||||||
if err != nil {
|
pc.drawSlices(r, canvasBox, finalValues)
|
||||||
return err
|
|
||||||
}
|
|
||||||
pc.drawSlices(r, canvasBox, valuesWithPlaceholder)
|
|
||||||
pc.drawTitle(r)
|
pc.drawTitle(r)
|
||||||
for _, a := range pc.Elements {
|
for _, a := range pc.Elements {
|
||||||
a(r, canvasBox, pc.styleDefaultsElements())
|
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()
|
cx, cy := canvasBox.Center()
|
||||||
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||||||
radius := float64(diameter >> 1)
|
radius := float64(diameter >> 1)
|
||||||
|
@ -172,6 +162,7 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []PieChartValue)
|
||||||
|
|
||||||
tb := r.MeasureText(v.Label)
|
tb := r.MeasureText(v.Label)
|
||||||
lx = lx - (tb.Width() >> 1)
|
lx = lx - (tb.Width() >> 1)
|
||||||
|
ly = ly + (tb.Height() >> 1)
|
||||||
|
|
||||||
r.Text(v.Label, lx, ly)
|
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) {
|
func (pc PieChart) finalizeValues(values []Value) []Value {
|
||||||
var total float64
|
return Values(values).Normalize()
|
||||||
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) getDefaultCanvasBox() Box {
|
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
|
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.
|
// SeqDays generates a sequence of timestamps by day, from -days to today.
|
||||||
func SeqDays(days int) []time.Time {
|
func SeqDays(days int) []time.Time {
|
||||||
var values []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