This commit is contained in:
Will Charczuk 2016-07-07 14:44:03 -07:00
parent e09aa43a1e
commit cbc4672f1e
13 changed files with 359 additions and 303 deletions

65
box.go Normal file
View file

@ -0,0 +1,65 @@
package chart
import "fmt"
// Box represents the main 4 dimensions of a box.
type Box struct {
Top int
Left int
Right int
Bottom int
}
// IsZero returns if the box is set or not.
func (b Box) IsZero() bool {
return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0
}
// String returns a string representation of the box.
func (b Box) String() string {
return fmt.Sprintf("Box(%d,%d,%d,%d)", b.Top, b.Left, b.Right, b.Bottom)
}
// GetTop returns a coalesced value with a default.
func (b Box) GetTop(defaults ...int) int {
if b.Top == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return 0
}
return b.Top
}
// GetLeft returns a coalesced value with a default.
func (b Box) GetLeft(defaults ...int) int {
if b.Left == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return 0
}
return b.Left
}
// GetRight returns a coalesced value with a default.
func (b Box) GetRight(defaults ...int) int {
if b.Right == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return 0
}
return b.Right
}
// GetBottom returns a coalesced value with a default.
func (b Box) GetBottom(defaults ...int) int {
if b.Bottom == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return 0
}
return b.Bottom
}

224
chart.go
View file

@ -1,138 +1,83 @@
package chart package chart
import ( import (
"image/color"
"io" "io"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
) )
// Chart is what we're drawing. // Chart is what we're drawing.
/*
The chart box model is as follows:
0,0 width,0
cl,ct cr,ct
cl,cb cr,cb
0, height width,height
*/
type Chart struct { type Chart struct {
Title string Title string
TitleFontSize float64 TitleStyle Style
Width int Width int
Height int Height int
Padding int Padding Box
BackgroundColor color.RGBA Background Style
CanvasBackgroundColor color.RGBA Canvas Style
Axes Style
FinalValueLabel Style
AxisShow bool XRange Range
AxisStyle Style YRange Range
AxisFontSize float64
CanvasBorderShow bool
CanvasBorderStyle Style
FinalValueLabelShow bool
FinalValueStyle Style
FontColor color.RGBA
Font *truetype.Font
Font *truetype.Font
Series []Series Series []Series
} }
// GetTitleFontSize calculates or returns the title font size.
func (c Chart) GetTitleFontSize() float64 {
if c.TitleFontSize != 0 {
if c.TitleFontSize > DefaultMinimumFontSize {
return c.TitleFontSize
}
}
fontSize := float64(c.Height >> 3)
if fontSize > DefaultMinimumFontSize {
return fontSize
}
return DefaultMinimumFontSize
}
// GetCanvasTop gets the top corner pixel. // GetCanvasTop gets the top corner pixel.
func (c Chart) GetCanvasTop() int { func (c Chart) GetCanvasTop() int {
return c.Padding return c.Padding.GetTop(DefaultCanvasPadding.Top)
} }
// GetCanvasLeft gets the left corner pixel. // GetCanvasLeft gets the left corner pixel.
func (c Chart) GetCanvasLeft() int { func (c Chart) GetCanvasLeft() int {
return c.Padding return c.Padding.GetLeft(DefaultCanvasPadding.Left)
} }
// GetCanvasBottom gets the bottom corner pixel. // GetCanvasBottom gets the bottom corner pixel.
func (c Chart) GetCanvasBottom() int { func (c Chart) GetCanvasBottom() int {
return c.Height - c.Padding return c.Height - c.Padding.GetBottom(DefaultCanvasPadding.Bottom)
} }
// GetCanvasRight gets the right corner pixel. // GetCanvasRight gets the right corner pixel.
func (c Chart) GetCanvasRight() int { func (c Chart) GetCanvasRight() int {
return c.Width - c.Padding return c.Width - c.Padding.GetRight(DefaultCanvasPadding.Right)
} }
// GetCanvasWidth returns the width of the canvas. // GetCanvasWidth returns the width of the canvas.
func (c Chart) GetCanvasWidth() int { func (c Chart) GetCanvasWidth() int {
if c.Padding > 0 { return c.Width - (c.Padding.GetLeft(DefaultCanvasPadding.Left) + c.Padding.GetRight(DefaultCanvasPadding.Right))
return c.Width - (c.Padding << 1)
}
return c.Width
} }
// GetCanvasHeight returns the height of the canvas. // GetCanvasHeight returns the height of the canvas.
func (c Chart) GetCanvasHeight() int { func (c Chart) GetCanvasHeight() int {
if c.Padding > 0 { return c.Height - (c.Padding.GetTop(DefaultCanvasPadding.Top) + c.Padding.GetBottom(DefaultCanvasPadding.Bottom))
return c.Height - (c.Padding << 1)
}
return c.Height
} }
// GetBackgroundColor returns the chart background color. // GetFont returns the text font.
func (c Chart) GetBackgroundColor() color.RGBA { func (c Chart) GetFont() (*truetype.Font, error) {
if ColorIsZero(c.BackgroundColor) {
c.BackgroundColor = DefaultBackgroundColor
}
return c.BackgroundColor
}
// GetCanvasBackgroundColor returns the canvas background color.
func (c Chart) GetCanvasBackgroundColor() color.RGBA {
if ColorIsZero(c.CanvasBackgroundColor) {
c.CanvasBackgroundColor = DefaultCanvasColor
}
return c.CanvasBackgroundColor
}
// GetTextFont returns the text font.
func (c Chart) GetTextFont() (*truetype.Font, error) {
if c.Font != nil { if c.Font != nil {
return c.Font, nil return c.Font, nil
} }
return GetDefaultFont() return GetDefaultFont()
} }
// GetTextFontColor returns the text font color.
func (c Chart) GetTextFontColor() color.RGBA {
if ColorIsZero(c.FontColor) {
c.FontColor = DefaultTextColor
}
return c.FontColor
}
// Render renders the chart with the given renderer to the given io.Writer. // Render renders the chart with the given renderer to the given io.Writer.
func (c Chart) Render(provider RendererProvider, w io.Writer) error { func (c *Chart) Render(provider RendererProvider, w io.Writer) error {
xrange, yrange := c.initRanges()
println("xrange", xrange.String())
println("yrange", yrange.String())
r := provider(c.Width, c.Height) r := provider(c.Width, c.Height)
c.drawBackground(r) c.drawBackground(r)
c.drawCanvas(r) c.drawCanvas(r)
c.drawAxes(r) c.drawAxes(r)
for _, series := range c.Series { for _, series := range c.Series {
c.drawSeries(r, series) c.drawSeries(r, series, xrange, yrange)
} }
err := c.drawTitle(r) err := c.drawTitle(r)
if err != nil { if err != nil {
@ -141,46 +86,83 @@ func (c Chart) Render(provider RendererProvider, w io.Writer) error {
return r.Save(w) return r.Save(w)
} }
func (c Chart) initRanges() (xrange Range, yrange Range) {
//iterate over each series, pull out the min/max for x,y
var didSetFirstValues bool
var globalMinY, globalMinX float64
var globalMaxY, globalMaxX float64
for _, s := range c.Series {
seriesLength := s.Len()
for index := 0; index < seriesLength; index++ {
vx, vy := s.GetValue(index)
if didSetFirstValues {
if globalMinX > vx {
globalMinX = vx
}
if globalMinY > vy {
globalMinY = vy
}
if globalMaxX < vx {
globalMaxX = vx
}
if globalMaxY < vy {
globalMaxY = vy
}
} else {
globalMinX, globalMaxX = vx, vx
globalMinY, globalMaxY = vy, vy
didSetFirstValues = true
}
}
}
if c.XRange.IsZero() {
xrange.Min = globalMinX
xrange.Max = globalMaxX
} else {
xrange.Min = c.XRange.Min
xrange.Max = c.XRange.Max
}
xrange.Domain = c.GetCanvasWidth()
if c.YRange.IsZero() {
yrange.Min = globalMinY
yrange.Max = globalMaxY
} else {
yrange.Min = c.YRange.Min
yrange.Max = c.YRange.Max
}
yrange.Domain = c.GetCanvasHeight()
return
}
func (c Chart) drawBackground(r Renderer) { func (c Chart) drawBackground(r Renderer) {
r.SetStrokeColor(c.GetBackgroundColor()) r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor))
r.SetFillColor(c.GetBackgroundColor())
r.SetLineWidth(0)
r.MoveTo(0, 0) r.MoveTo(0, 0)
r.LineTo(c.Width, 0) r.LineTo(c.Width, 0)
r.LineTo(c.Width, c.Height) r.LineTo(c.Width, c.Height)
r.LineTo(0, c.Height) r.LineTo(0, c.Height)
r.LineTo(0, 0) r.LineTo(0, 0)
r.FillStroke()
r.Close() r.Close()
r.Fill()
} }
func (c Chart) drawCanvas(r Renderer) { func (c Chart) drawCanvas(r Renderer) {
if !c.CanvasBorderStyle.IsZero() { r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor))
r.SetStrokeColor(c.CanvasBorderStyle.GetStrokeColor())
r.SetLineWidth(c.CanvasBorderStyle.GetStrokeWidth())
} else {
r.SetStrokeColor(c.GetCanvasBackgroundColor())
r.SetLineWidth(0)
}
r.SetFillColor(c.GetCanvasBackgroundColor())
r.MoveTo(c.GetCanvasLeft(), c.GetCanvasTop()) r.MoveTo(c.GetCanvasLeft(), c.GetCanvasTop())
r.LineTo(c.GetCanvasRight(), c.GetCanvasTop()) r.LineTo(c.GetCanvasRight(), c.GetCanvasTop())
r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom()) r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasLeft(), c.GetCanvasBottom()) r.LineTo(c.GetCanvasLeft(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasLeft(), c.GetCanvasTop()) r.LineTo(c.GetCanvasLeft(), c.GetCanvasTop())
r.FillStroke() r.Fill()
r.Close() r.Close()
} }
func (c Chart) drawAxes(r Renderer) { func (c Chart) drawAxes(r Renderer) {
if c.AxisShow { if c.Axes.Show {
if !c.AxisStyle.IsZero() { r.SetStrokeColor(c.Axes.GetStrokeColor(DefaultAxisColor))
r.SetStrokeColor(c.AxisStyle.GetStrokeColor()) r.SetLineWidth(c.Axes.GetStrokeWidth(DefaultLineWidth))
r.SetLineWidth(c.AxisStyle.GetStrokeWidth())
} else {
r.SetStrokeColor(DefaultAxisColor)
r.SetLineWidth(DefaultAxisLineWidth)
}
r.MoveTo(c.GetCanvasLeft(), c.GetCanvasBottom()) r.MoveTo(c.GetCanvasLeft(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom()) r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasRight(), c.GetCanvasTop()) r.LineTo(c.GetCanvasRight(), c.GetCanvasTop())
@ -188,46 +170,48 @@ func (c Chart) drawAxes(r Renderer) {
} }
} }
func (c Chart) drawSeries(r Renderer, s Series) { func (c Chart) drawSeries(r Renderer, s Series, xrange, yrange Range) {
r.SetLineWidth(s.GetStyle().GetStrokeWidth()) r.SetStrokeColor(s.GetStyle().GetStrokeColor(DefaultLineColor))
r.SetStrokeColor(s.GetStyle().GetStrokeColor()) r.SetLineWidth(s.GetStyle().GetStrokeWidth(DefaultLineWidth))
xrange := s.GetXRange(c.GetCanvasWidth())
yrange := s.GetYRange(c.GetCanvasHeight())
if s.Len() == 0 { if s.Len() == 0 {
return return
} }
v0x, v0y := s.GetValue(0) px := c.Padding.GetLeft(DefaultCanvasPadding.Left)
x0 := xrange.Translate(v0x) py := c.Padding.GetTop(DefaultCanvasPadding.Top)
y0 := yrange.Translate(v0y)
r.MoveTo(x0, y0)
var vx interface{} cw := c.GetCanvasWidth()
var vy float64
v0x, v0y := s.GetValue(0)
x0 := cw - xrange.Translate(v0x)
y0 := yrange.Translate(v0y)
r.MoveTo(x0+px, y0+py)
var vx, vy float64
var x, y int var x, y int
for index := 0; index < s.Len(); index++ { for index := 1; index < s.Len(); index++ {
vx, vy = s.GetValue(index) vx, vy = s.GetValue(index)
x = xrange.Translate(vx) x = cw - xrange.Translate(vx)
y = yrange.Translate(vy) y = yrange.Translate(vy)
r.LineTo(x, y) r.LineTo(x+px, y+py)
} }
r.Stroke() r.Stroke()
} }
func (c Chart) drawTitle(r Renderer) error { func (c Chart) drawTitle(r Renderer) error {
if len(c.Title) > 0 { if len(c.Title) > 0 && c.TitleStyle.Show {
font, err := c.GetTextFont() font, err := c.GetFont()
if err != nil { if err != nil {
return err return err
} }
r.SetFontColor(c.GetTextFontColor()) r.SetFontColor(c.Canvas.GetFontColor(DefaultTextColor))
r.SetFont(font) r.SetFont(font)
r.SetFontSize(c.GetTitleFontSize()) titleFontSize := c.Canvas.GetFontSize(DefaultTitleFontSize)
r.SetFontSize(titleFontSize)
textWidth := r.MeasureText(c.Title) textWidth := r.MeasureText(c.Title)
titleX := (c.Width >> 1) - (textWidth >> 1) titleX := (c.Width >> 1) - (textWidth >> 1)
titleY := c.GetCanvasTop() + int(c.GetTitleFontSize()/2.0) titleY := c.GetCanvasTop() + int(titleFontSize)
r.Text(c.Title, titleX, titleY) r.Text(c.Title, titleX, titleY)
} }
return nil return nil

View file

@ -15,9 +15,13 @@ func TestChartSingleSeries(t *testing.T) {
Title: "Hello!", Title: "Hello!",
Width: 1024, Width: 1024,
Height: 400, Height: 400,
YRange: Range{
Min: 0.0,
Max: 4.0,
},
Series: []Series{ Series: []Series{
TimeSeries{ TimeSeries{
Name: "Goog", Name: "goog",
XValues: []time.Time{now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)}, XValues: []time.Time{now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)},
YValues: []float64{1.0, 2.0, 3.0}, YValues: []float64{1.0, 2.0, 3.0},
}, },

View file

@ -12,9 +12,6 @@ const (
DefaultChartHeight = 400 DefaultChartHeight = 400
// DefaultChartWidth is the default chart width. // DefaultChartWidth is the default chart width.
DefaultChartWidth = 200 DefaultChartWidth = 200
// DefaultPadding is the default gap between the image border and
// chart content (referred to as the "canvas").
DefaultPadding = 10
// DefaultLineWidth is the default chart line width. // DefaultLineWidth is the default chart line width.
DefaultLineWidth = 2.0 DefaultLineWidth = 2.0
// DefaultAxisLineWidth is the line width of the axis lines. // DefaultAxisLineWidth is the line width of the axis lines.
@ -23,12 +20,18 @@ const (
DefaultDPI = 120.0 DefaultDPI = 120.0
// DefaultMinimumFontSize is the default minimum font size. // DefaultMinimumFontSize is the default minimum font size.
DefaultMinimumFontSize = 8.0 DefaultMinimumFontSize = 8.0
// DefaultFontSize is the default font size.
DefaultFontSize = 10.0
// DefaultTitleFontSize is the default title font size.
DefaultTitleFontSize = 18.0
// DefaultDateFormat is the default date format.
DefaultDateFormat = "2006-01-02"
) )
var ( var (
// DefaultBackgroundColor is the default chart background color. // DefaultBackgroundColor is the default chart background color.
// It is equivalent to css color:white. // It is equivalent to css color:white.
DefaultBackgroundColor = color.RGBA{R: 255, G: 255, B: 255, A: 255} DefaultBackgroundColor = color.RGBA{R: 239, G: 239, B: 239, A: 255} //color.RGBA{R: 255, G: 255, B: 255, A: 255}
// DefaultCanvasColor is the default chart canvas color. // DefaultCanvasColor is the default chart canvas color.
// It is equivalent to css color:white. // It is equivalent to css color:white.
DefaultCanvasColor = color.RGBA{R: 255, G: 255, B: 255, A: 255} DefaultCanvasColor = color.RGBA{R: 255, G: 255, B: 255, A: 255}
@ -43,12 +46,17 @@ var (
DefaultBorderColor = color.RGBA{R: 239, G: 239, B: 239, A: 255} DefaultBorderColor = color.RGBA{R: 239, G: 239, B: 239, A: 255}
// DefaultLineColor is the default (1st) series line color. // DefaultLineColor is the default (1st) series line color.
// It is equivalent to #0074d9. // It is equivalent to #0074d9.
DefaultLineColor = color.RGBA{R: 0, G: 217, B: 116, A: 255} DefaultLineColor = color.RGBA{R: 0, G: 116, B: 217, A: 255}
// DefaultFillColor is the default fill color. // DefaultFillColor is the default fill color.
// It is equivalent to #0074d9. // It is equivalent to #0074d9.
DefaultFillColor = color.RGBA{R: 0, G: 217, B: 116, A: 255} DefaultFillColor = color.RGBA{R: 0, G: 217, B: 116, A: 255}
) )
var (
// DefaultCanvasPadding is the default canvas padding config.
DefaultCanvasPadding = Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
)
var ( var (
_defaultFontLock sync.Mutex _defaultFontLock sync.Mutex
_defaultFont *truetype.Font _defaultFont *truetype.Font

View file

@ -1,97 +1,36 @@
package chart package chart
import ( import (
"fmt"
"math" "math"
"time"
) )
// Range is a type that translates values from a range to a domain. // Range represents a continuous range,
type Range interface { type Range struct {
GetMin() interface{} Min float64
GetMax() interface{} Max float64
Translate(value interface{}) int Domain int
} }
// NewRangeOfFloat64 returns a new Range // IsZero returns if the range has been set or not.
func NewRangeOfFloat64(domain int, values ...float64) Range { func (r Range) IsZero() bool {
min, max := MinAndMax(values...) return r.Min == 0 && r.Max == 0 && r.Domain == 0
return &RangeOfFloat64{
MinValue: min,
MaxValue: max,
MinMaxDelta: max - min,
Domain: domain,
}
} }
// RangeOfFloat64 represents a continuous range // Delta returns the difference between the min and max value.
// of float64 values mapped to a [0...WindowMaxValue] func (r Range) Delta() float64 {
// interval. return r.Max - r.Min
type RangeOfFloat64 struct {
MinValue float64
MaxValue float64
MinMaxDelta float64
Domain int
} }
// GetMin implements the interface method. // String returns a simple string for the range.
func (r RangeOfFloat64) GetMin() interface{} { func (r Range) String() string {
return r.MinValue return fmt.Sprintf("Range [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
}
// GetMax implements the interface method.
func (r RangeOfFloat64) GetMax() interface{} {
return r.MaxValue
} }
// Translate maps a given value into the range space. // Translate maps a given value into the range space.
// An example would be a 600 px image, with a min of 10 and a max of 100. // An example would be a 600 px image, with a min of 10 and a max of 100.
// Translate(50) would yield (50.0/90.0)*600 ~= 333.33 // Translate(50) would yield (50.0/90.0)*600 ~= 333.33
func (r RangeOfFloat64) Translate(value interface{}) int { func (r Range) Translate(value float64) int {
if typedValue, isTyped := value.(float64); isTyped { finalValue := ((r.Max - value) / r.Delta()) * float64(r.Domain)
finalValue := ((r.MaxValue - typedValue) / r.MinMaxDelta) * float64(r.Domain) return int(math.Floor(finalValue))
return int(math.Floor(finalValue))
}
return 0
}
// NewRangeOfTime makes a new range of time with the given time values.
func NewRangeOfTime(domain int, values ...time.Time) Range {
min, max := MinAndMaxOfTime(values...)
r := &RangeOfTime{
MinValue: min,
MaxValue: max,
MinMaxDelta: max.Unix() - min.Unix(),
Domain: domain,
}
return r
}
// RangeOfTime represents a timeseries.
type RangeOfTime struct {
MinValue time.Time
MaxValue time.Time
MinMaxDelta int64 //unix time difference
Domain int
}
// GetMin implements the interface method.
func (r RangeOfTime) GetMin() interface{} {
return r.MinValue
}
// GetMax implements the interface method.
func (r RangeOfTime) GetMax() interface{} {
return r.MaxValue
}
// Translate maps a given value into the range space (of time).
// An example would be a 600 px image, with a min of jan-01-2016 and a max of jun-01-2016.
// Translate(may-01-2016) would yield ... something.
func (r RangeOfTime) Translate(value interface{}) int {
if typed, isTyped := value.(time.Time); isTyped {
valueDelta := r.MaxValue.Unix() - typed.Unix()
finalValue := (float64(valueDelta) / float64(r.MinMaxDelta)) * float64(r.Domain)
return int(math.Floor(finalValue))
}
return 0
} }

View file

@ -2,7 +2,6 @@ package chart
import ( import (
"testing" "testing"
"time"
"github.com/blendlabs/go-assert" "github.com/blendlabs/go-assert"
) )
@ -10,26 +9,7 @@ import (
func TestRangeTranslate(t *testing.T) { func TestRangeTranslate(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
values := []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0} values := []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
r := NewRangeOfFloat64(1000, values...) r := Range{Domain: 1000}
assert.Equal(1.0, r.GetMin()) r.Min, r.Max = MinAndMax(values...)
assert.Equal(8.0, r.GetMax())
assert.Equal(428, r.Translate(5.0)) assert.Equal(428, r.Translate(5.0))
} }
func TestRangeOfTimeTranslate(t *testing.T) {
assert := assert.New(t)
values := []time.Time{
time.Now().AddDate(0, 0, -1),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -4),
time.Now().AddDate(0, 0, -5),
time.Now().AddDate(0, 0, -6),
time.Now().AddDate(0, 0, -7),
time.Now().AddDate(0, 0, -8),
}
r := NewRangeOfTime(1000, values...)
assert.Equal(values[7], r.GetMin())
assert.Equal(values[0], r.GetMax())
assert.Equal(571, r.Translate(time.Now().AddDate(0, 0, -5)))
}

View file

@ -1,6 +1,7 @@
package chart package chart
import ( import (
"fmt"
"image" "image"
"image/color" "image/color"
"image/png" "image/png"
@ -34,55 +35,60 @@ type rasterRenderer struct {
// SetStrokeColor implements the interface method. // SetStrokeColor implements the interface method.
func (rr *rasterRenderer) SetStrokeColor(c color.RGBA) { func (rr *rasterRenderer) SetStrokeColor(c color.RGBA) {
println("SetStrokeColor") println("RasterRenderer :: SetStrokeColor", ColorAsString(c))
rr.gc.SetStrokeColor(c) rr.gc.SetStrokeColor(c)
} }
// SetFillColor implements the interface method. // SetFillColor implements the interface method.
func (rr *rasterRenderer) SetFillColor(c color.RGBA) { func (rr *rasterRenderer) SetFillColor(c color.RGBA) {
println("SetFillColor") println("RasterRenderer :: SetFillColor", ColorAsString(c))
rr.gc.SetFillColor(c) rr.gc.SetFillColor(c)
} }
// SetLineWidth implements the interface method. // SetLineWidth implements the interface method.
func (rr *rasterRenderer) SetLineWidth(width int) { func (rr *rasterRenderer) SetLineWidth(width float64) {
println("SetLineWidth", width) println("RasterRenderer :: SetLineWidth", width)
rr.gc.SetLineWidth(float64(width)) rr.gc.SetLineWidth(width)
} }
// MoveTo implements the interface method. // MoveTo implements the interface method.
func (rr *rasterRenderer) MoveTo(x, y int) { func (rr *rasterRenderer) MoveTo(x, y int) {
println("MoveTo", x, y) println("RasterRenderer :: MoveTo", x, y)
rr.gc.MoveTo(float64(x), float64(y)) rr.gc.MoveTo(float64(x), float64(y))
} }
// LineTo implements the interface method. // LineTo implements the interface method.
func (rr *rasterRenderer) LineTo(x, y int) { func (rr *rasterRenderer) LineTo(x, y int) {
println("LineTo", x, y) println("RasterRenderer :: LineTo", x, y)
rr.gc.LineTo(float64(x), float64(y)) rr.gc.LineTo(float64(x), float64(y))
} }
// Close implements the interface method. // Close implements the interface method.
func (rr *rasterRenderer) Close() { func (rr *rasterRenderer) Close() {
println("Close") println("RasterRenderer :: Close")
rr.gc.Close() rr.gc.Close()
} }
// Stroke implements the interface method. // Stroke implements the interface method.
func (rr *rasterRenderer) Stroke() { func (rr *rasterRenderer) Stroke() {
println("Stroke") println("RasterRenderer :: Stroke")
rr.gc.Stroke() rr.gc.Stroke()
} }
// Fill implements the interface method.
func (rr *rasterRenderer) Fill() {
println("RasterRenderer :: Fill")
rr.gc.Fill()
}
// FillStroke implements the interface method. // FillStroke implements the interface method.
func (rr *rasterRenderer) FillStroke() { func (rr *rasterRenderer) FillStroke() {
println("FillStroke") println("RasterRenderer :: FillStroke")
rr.gc.FillStroke() rr.gc.FillStroke()
} }
// Circle implements the interface method. // Circle implements the interface method.
func (rr *rasterRenderer) Circle(radius float64, x, y int) { func (rr *rasterRenderer) Circle(radius float64, x, y int) {
println("Circle", radius, x, y)
xf := float64(x) xf := float64(x)
yf := float64(y) yf := float64(y)
rr.gc.MoveTo(xf-radius, yf) //9 rr.gc.MoveTo(xf-radius, yf) //9
@ -96,34 +102,32 @@ func (rr *rasterRenderer) Circle(radius float64, x, y int) {
// SetFont implements the interface method. // SetFont implements the interface method.
func (rr *rasterRenderer) SetFont(f *truetype.Font) { func (rr *rasterRenderer) SetFont(f *truetype.Font) {
println("SetFont")
rr.font = f rr.font = f
rr.gc.SetFont(f) rr.gc.SetFont(f)
} }
// SetFontSize implements the interface method. // SetFontSize implements the interface method.
func (rr *rasterRenderer) SetFontSize(size float64) { func (rr *rasterRenderer) SetFontSize(size float64) {
println("SetFontSize", size) println("RasterRenderer :: SetFontSize", fmt.Sprintf("%.2f", size))
rr.fontSize = size rr.fontSize = size
rr.gc.SetFontSize(size) rr.gc.SetFontSize(size)
} }
// SetFontColor implements the interface method. // SetFontColor implements the interface method.
func (rr *rasterRenderer) SetFontColor(c color.RGBA) { func (rr *rasterRenderer) SetFontColor(c color.RGBA) {
println("SetFontColor") println("RasterRenderer :: SetFontColor", ColorAsString(c))
rr.fontColor = c rr.fontColor = c
rr.gc.SetStrokeColor(c) rr.gc.SetStrokeColor(c)
} }
// Text implements the interface method. // Text implements the interface method.
func (rr *rasterRenderer) Text(body string, x, y int) { func (rr *rasterRenderer) Text(body string, x, y int) {
println("Text", body, x, y) println("RasterRenderer :: Text", body, x, y)
rr.gc.CreateStringPath(body, float64(x), float64(y)) rr.gc.CreateStringPath(body, float64(x), float64(y))
} }
// MeasureText implements the interface method. // MeasureText implements the interface method.
func (rr *rasterRenderer) MeasureText(body string) int { func (rr *rasterRenderer) MeasureText(body string) int {
println("MeasureText", body)
if rr.fc == nil && rr.font != nil { if rr.fc == nil && rr.font != nil {
rr.fc = &font.Drawer{ rr.fc = &font.Drawer{
Face: truetype.NewFace(rr.font, &truetype.Options{ Face: truetype.NewFace(rr.font, &truetype.Options{
@ -141,6 +145,6 @@ func (rr *rasterRenderer) MeasureText(body string) int {
// Save implements the interface method. // Save implements the interface method.
func (rr *rasterRenderer) Save(w io.Writer) error { func (rr *rasterRenderer) Save(w io.Writer) error {
println("Save") println("RasterRenderer :: Save")
return png.Encode(w, rr.i) return png.Encode(w, rr.i)
} }

View file

@ -19,7 +19,7 @@ type Renderer interface {
SetFillColor(color.RGBA) SetFillColor(color.RGBA)
// SetLineWidth sets the stroke line width. // SetLineWidth sets the stroke line width.
SetLineWidth(width int) SetLineWidth(width float64)
// MoveTo moves the cursor to a given point. // MoveTo moves the cursor to a given point.
MoveTo(x, y int) MoveTo(x, y int)
@ -31,10 +31,13 @@ type Renderer interface {
// Close finalizes a shape as drawn by LineTo. // Close finalizes a shape as drawn by LineTo.
Close() Close()
// Stroke draws the 'stroke' or line component of a shape. // Stroke strokes the path.
Stroke() Stroke()
// FillStroke draws the 'stroke' and 'fills' a shape. // Fill fills the path, but does not stroke.
Fill()
// FillStroke fills and strokes a path.
FillStroke() FillStroke()
// Circle draws a circle at the given coords with a given radius. // Circle draws a circle at the given coords with a given radius.

View file

@ -1,16 +1,17 @@
package chart package chart
import "time" import (
"fmt"
"time"
)
// Series is a entity data set. // Series is a entity data set.
type Series interface { type Series interface {
GetName() string GetName() string
GetStyle() Style GetStyle() Style
Len() int Len() int
GetValue(index int) (interface{}, float64) GetValue(index int) (float64, float64)
GetLabel(index int) (string, string)
GetXRange(domain int) Range
GetYRange(domain int) Range
} }
// TimeSeries is a line on a chart. // TimeSeries is a line on a chart.
@ -37,19 +38,18 @@ func (ts TimeSeries) Len() int {
return len(ts.XValues) return len(ts.XValues)
} }
// GetXRange returns the x range.
func (ts TimeSeries) GetXRange(domain int) Range {
return NewRangeOfTime(domain, ts.XValues...)
}
// GetYRange returns the x range.
func (ts TimeSeries) GetYRange(domain int) Range {
return NewRangeOfFloat64(domain, ts.YValues...)
}
// GetValue gets a value at a given index. // GetValue gets a value at a given index.
func (ts TimeSeries) GetValue(index int) (interface{}, float64) { func (ts TimeSeries) GetValue(index int) (x float64, y float64) {
return ts.XValues[index], ts.YValues[index] x = float64(ts.XValues[index].Unix())
y = ts.YValues[index]
return
}
// GetLabel gets a label for the values at a given index.
func (ts TimeSeries) GetLabel(index int) (xLabel string, yLabel string) {
xLabel = ts.XValues[index].Format(DefaultDateFormat)
yLabel = fmt.Sprintf("%0.2f", ts.YValues[index])
return
} }
// ContinousSeries represents a line on a chart. // ContinousSeries represents a line on a chart.
@ -81,12 +81,9 @@ func (cs ContinousSeries) GetValue(index int) (interface{}, float64) {
return cs.XValues[index], cs.YValues[index] return cs.XValues[index], cs.YValues[index]
} }
// GetXRange returns the x range. // GetLabel gets a label for the values at a given index.
func (cs ContinousSeries) GetXRange(domain int) Range { func (cs ContinousSeries) GetLabel(index int) (xLabel string, yLabel string) {
return NewRangeOfFloat64(domain, cs.XValues...) xLabel = fmt.Sprintf("%0.2f", cs.XValues[index])
} yLabel = fmt.Sprintf("%0.2f", cs.YValues[index])
return
// GetYRange returns the x range.
func (cs ContinousSeries) GetYRange(domain int) Range {
return NewRangeOfFloat64(domain, cs.YValues...)
} }

View file

@ -28,29 +28,3 @@ func TestTimeSeriesGetValue(t *testing.T) {
assert.NotZero(x0) assert.NotZero(x0)
assert.Equal(1.0, y0) assert.Equal(1.0, y0)
} }
func TestTimeSeriesRanges(t *testing.T) {
assert := assert.New(t)
ts := TimeSeries{
Name: "Test",
XValues: []time.Time{
time.Now().AddDate(0, 0, -5),
time.Now().AddDate(0, 0, -4),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -1),
},
YValues: []float64{
1.0, 2.0, 3.0, 4.0, 5.0,
},
}
xrange := ts.GetXRange(1000)
x0 := xrange.Translate(time.Now().AddDate(0, 0, -3))
assert.Equal(500, x0)
yrange := ts.GetYRange(400)
y0 := yrange.Translate(3.0)
assert.Equal(200, y0)
}

View file

@ -4,36 +4,70 @@ import "image/color"
// Style is a simple style set. // Style is a simple style set.
type Style struct { type Style struct {
Show bool
StrokeColor color.RGBA StrokeColor color.RGBA
FillColor color.RGBA FillColor color.RGBA
StrokeWidth int StrokeWidth float64
FontSize float64
FontColor color.RGBA
} }
// IsZero returns if the object is set or not. // IsZero returns if the object is set or not.
func (s Style) IsZero() bool { func (s Style) IsZero() bool {
return ColorIsZero(s.StrokeColor) && ColorIsZero(s.FillColor) && s.StrokeWidth == 0 return ColorIsZero(s.StrokeColor) && ColorIsZero(s.FillColor) && s.StrokeWidth == 0 && s.FontSize == 0
} }
// GetStrokeColor returns the stroke color. // GetStrokeColor returns the stroke color.
func (s Style) GetStrokeColor() color.RGBA { func (s Style) GetStrokeColor(defaults ...color.RGBA) color.RGBA {
if ColorIsZero(s.StrokeColor) { if ColorIsZero(s.StrokeColor) {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultLineColor return DefaultLineColor
} }
return s.StrokeColor return s.StrokeColor
} }
// GetFillColor returns the fill color. // GetFillColor returns the fill color.
func (s Style) GetFillColor() color.RGBA { func (s Style) GetFillColor(defaults ...color.RGBA) color.RGBA {
if ColorIsZero(s.FillColor) { if ColorIsZero(s.FillColor) {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultFillColor return DefaultFillColor
} }
return s.FillColor return s.FillColor
} }
// GetStrokeWidth returns the stroke width. // GetStrokeWidth returns the stroke width.
func (s Style) GetStrokeWidth() int { func (s Style) GetStrokeWidth(defaults ...float64) float64 {
if s.StrokeWidth == 0 { if s.StrokeWidth == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultLineWidth return DefaultLineWidth
} }
return s.StrokeWidth return s.StrokeWidth
} }
// GetFontSize gets the font size.
func (s Style) GetFontSize(defaults ...float64) float64 {
if s.FontSize == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultFontSize
}
return s.FontSize
}
// GetFontColor gets the font size.
func (s Style) GetFontColor(defaults ...color.RGBA) color.RGBA {
if ColorIsZero(s.FontColor) {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultTextColor
}
return s.FontColor
}

58
testserver/main.go Normal file
View file

@ -0,0 +1,58 @@
package main
import (
"bytes"
"log"
"time"
"github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-web"
)
func main() {
app := web.New()
app.SetName("Chart Test Server")
app.SetLogger(web.NewStandardOutputLogger())
app.GET("/", func(rc *web.RequestContext) web.ControllerResult {
rc.Response.Header().Set("Content-Type", "image/png")
now := time.Now()
c := chart.Chart{
Title: "Hello!",
TitleStyle: chart.Style{
Show: true,
},
Width: 1024,
Height: 400,
Padding: chart.Box{
Right: 40,
Bottom: 40,
},
Axes: chart.Style{
Show: true,
StrokeWidth: 1.0,
},
YRange: chart.Range{
Min: 0.0,
Max: 7.0,
},
Series: []chart.Series{
chart.TimeSeries{
Name: "goog",
Style: chart.Style{
StrokeWidth: 1.0,
},
XValues: []time.Time{now.AddDate(0, 0, -4), now.AddDate(0, 0, -3), now.AddDate(0, 0, -2), now.AddDate(0, 0, -1)},
YValues: []float64{2.5, 5.0, 2.0, 3.0},
},
},
}
buffer := bytes.NewBuffer([]byte{})
err := c.Render(chart.PNG, buffer)
if err != nil {
return rc.API().InternalError(err)
}
return rc.Raw(buffer.Bytes())
})
log.Fatal(app.Start())
}

View file

@ -1,6 +1,7 @@
package chart package chart
import ( import (
"fmt"
"image/color" "image/color"
"time" "time"
) )
@ -10,6 +11,11 @@ func ColorIsZero(c color.RGBA) bool {
return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0 return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0
} }
// ColorAsString returns if a color.RGBA is unset or not.
func ColorAsString(c color.RGBA) string {
return fmt.Sprintf("RGBA(%v,%v,%v,%v)", c.R, c.G, c.G, c.A)
}
// MinAndMax returns both the min and max in one pass. // MinAndMax returns both the min and max in one pass.
func MinAndMax(values ...float64) (min float64, max float64) { func MinAndMax(values ...float64) (min float64, max float64) {
if len(values) == 0 { if len(values) == 0 {