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
}

220
chart.go
View file

@ -1,138 +1,83 @@
package chart
import (
"image/color"
"io"
"github.com/golang/freetype/truetype"
)
// 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 {
Title string
TitleFontSize float64
TitleStyle Style
Width int
Height int
Padding int
Padding Box
BackgroundColor color.RGBA
CanvasBackgroundColor color.RGBA
Background Style
Canvas Style
Axes Style
FinalValueLabel Style
AxisShow bool
AxisStyle Style
AxisFontSize float64
XRange Range
YRange Range
CanvasBorderShow bool
CanvasBorderStyle Style
FinalValueLabelShow bool
FinalValueStyle Style
FontColor color.RGBA
Font *truetype.Font
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.
func (c Chart) GetCanvasTop() int {
return c.Padding
return c.Padding.GetTop(DefaultCanvasPadding.Top)
}
// GetCanvasLeft gets the left corner pixel.
func (c Chart) GetCanvasLeft() int {
return c.Padding
return c.Padding.GetLeft(DefaultCanvasPadding.Left)
}
// GetCanvasBottom gets the bottom corner pixel.
func (c Chart) GetCanvasBottom() int {
return c.Height - c.Padding
return c.Height - c.Padding.GetBottom(DefaultCanvasPadding.Bottom)
}
// GetCanvasRight gets the right corner pixel.
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.
func (c Chart) GetCanvasWidth() int {
if c.Padding > 0 {
return c.Width - (c.Padding << 1)
}
return c.Width
return c.Width - (c.Padding.GetLeft(DefaultCanvasPadding.Left) + c.Padding.GetRight(DefaultCanvasPadding.Right))
}
// GetCanvasHeight returns the height of the canvas.
func (c Chart) GetCanvasHeight() int {
if c.Padding > 0 {
return c.Height - (c.Padding << 1)
}
return c.Height
return c.Height - (c.Padding.GetTop(DefaultCanvasPadding.Top) + c.Padding.GetBottom(DefaultCanvasPadding.Bottom))
}
// GetBackgroundColor returns the chart background color.
func (c Chart) GetBackgroundColor() color.RGBA {
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) {
// GetFont returns the text font.
func (c Chart) GetFont() (*truetype.Font, error) {
if c.Font != nil {
return c.Font, nil
}
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.
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)
c.drawBackground(r)
c.drawCanvas(r)
c.drawAxes(r)
for _, series := range c.Series {
c.drawSeries(r, series)
c.drawSeries(r, series, xrange, yrange)
}
err := c.drawTitle(r)
if err != nil {
@ -141,46 +86,83 @@ func (c Chart) Render(provider RendererProvider, w io.Writer) error {
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) {
r.SetStrokeColor(c.GetBackgroundColor())
r.SetFillColor(c.GetBackgroundColor())
r.SetLineWidth(0)
r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor))
r.MoveTo(0, 0)
r.LineTo(c.Width, 0)
r.LineTo(c.Width, c.Height)
r.LineTo(0, c.Height)
r.LineTo(0, 0)
r.FillStroke()
r.Close()
r.Fill()
}
func (c Chart) drawCanvas(r Renderer) {
if !c.CanvasBorderStyle.IsZero() {
r.SetStrokeColor(c.CanvasBorderStyle.GetStrokeColor())
r.SetLineWidth(c.CanvasBorderStyle.GetStrokeWidth())
} else {
r.SetStrokeColor(c.GetCanvasBackgroundColor())
r.SetLineWidth(0)
}
r.SetFillColor(c.GetCanvasBackgroundColor())
r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor))
r.MoveTo(c.GetCanvasLeft(), c.GetCanvasTop())
r.LineTo(c.GetCanvasRight(), c.GetCanvasTop())
r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasLeft(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasLeft(), c.GetCanvasTop())
r.FillStroke()
r.Fill()
r.Close()
}
func (c Chart) drawAxes(r Renderer) {
if c.AxisShow {
if !c.AxisStyle.IsZero() {
r.SetStrokeColor(c.AxisStyle.GetStrokeColor())
r.SetLineWidth(c.AxisStyle.GetStrokeWidth())
} else {
r.SetStrokeColor(DefaultAxisColor)
r.SetLineWidth(DefaultAxisLineWidth)
}
if c.Axes.Show {
r.SetStrokeColor(c.Axes.GetStrokeColor(DefaultAxisColor))
r.SetLineWidth(c.Axes.GetStrokeWidth(DefaultLineWidth))
r.MoveTo(c.GetCanvasLeft(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom())
r.LineTo(c.GetCanvasRight(), c.GetCanvasTop())
@ -188,46 +170,48 @@ func (c Chart) drawAxes(r Renderer) {
}
}
func (c Chart) drawSeries(r Renderer, s Series) {
r.SetLineWidth(s.GetStyle().GetStrokeWidth())
r.SetStrokeColor(s.GetStyle().GetStrokeColor())
xrange := s.GetXRange(c.GetCanvasWidth())
yrange := s.GetYRange(c.GetCanvasHeight())
func (c Chart) drawSeries(r Renderer, s Series, xrange, yrange Range) {
r.SetStrokeColor(s.GetStyle().GetStrokeColor(DefaultLineColor))
r.SetLineWidth(s.GetStyle().GetStrokeWidth(DefaultLineWidth))
if s.Len() == 0 {
return
}
v0x, v0y := s.GetValue(0)
x0 := xrange.Translate(v0x)
y0 := yrange.Translate(v0y)
r.MoveTo(x0, y0)
px := c.Padding.GetLeft(DefaultCanvasPadding.Left)
py := c.Padding.GetTop(DefaultCanvasPadding.Top)
var vx interface{}
var vy float64
cw := c.GetCanvasWidth()
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
for index := 0; index < s.Len(); index++ {
for index := 1; index < s.Len(); index++ {
vx, vy = s.GetValue(index)
x = xrange.Translate(vx)
x = cw - xrange.Translate(vx)
y = yrange.Translate(vy)
r.LineTo(x, y)
r.LineTo(x+px, y+py)
}
r.Stroke()
}
func (c Chart) drawTitle(r Renderer) error {
if len(c.Title) > 0 {
font, err := c.GetTextFont()
if len(c.Title) > 0 && c.TitleStyle.Show {
font, err := c.GetFont()
if err != nil {
return err
}
r.SetFontColor(c.GetTextFontColor())
r.SetFontColor(c.Canvas.GetFontColor(DefaultTextColor))
r.SetFont(font)
r.SetFontSize(c.GetTitleFontSize())
titleFontSize := c.Canvas.GetFontSize(DefaultTitleFontSize)
r.SetFontSize(titleFontSize)
textWidth := r.MeasureText(c.Title)
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)
}
return nil

View file

@ -15,9 +15,13 @@ func TestChartSingleSeries(t *testing.T) {
Title: "Hello!",
Width: 1024,
Height: 400,
YRange: Range{
Min: 0.0,
Max: 4.0,
},
Series: []Series{
TimeSeries{
Name: "Goog",
Name: "goog",
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},
},

View file

@ -12,9 +12,6 @@ const (
DefaultChartHeight = 400
// DefaultChartWidth is the default chart width.
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 = 2.0
// DefaultAxisLineWidth is the line width of the axis lines.
@ -23,12 +20,18 @@ const (
DefaultDPI = 120.0
// DefaultMinimumFontSize is the default minimum font size.
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 (
// DefaultBackgroundColor is the default chart background color.
// 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.
// It is equivalent to css color:white.
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}
// DefaultLineColor is the default (1st) series line color.
// 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.
// It is equivalent to #0074d9.
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 (
_defaultFontLock sync.Mutex
_defaultFont *truetype.Font

View file

@ -1,97 +1,36 @@
package chart
import (
"fmt"
"math"
"time"
)
// Range is a type that translates values from a range to a domain.
type Range interface {
GetMin() interface{}
GetMax() interface{}
Translate(value interface{}) int
}
// NewRangeOfFloat64 returns a new Range
func NewRangeOfFloat64(domain int, values ...float64) Range {
min, max := MinAndMax(values...)
return &RangeOfFloat64{
MinValue: min,
MaxValue: max,
MinMaxDelta: max - min,
Domain: domain,
}
}
// RangeOfFloat64 represents a continuous range
// of float64 values mapped to a [0...WindowMaxValue]
// interval.
type RangeOfFloat64 struct {
MinValue float64
MaxValue float64
MinMaxDelta float64
// Range represents a continuous range,
type Range struct {
Min float64
Max float64
Domain int
}
// GetMin implements the interface method.
func (r RangeOfFloat64) GetMin() interface{} {
return r.MinValue
// IsZero returns if the range has been set or not.
func (r Range) IsZero() bool {
return r.Min == 0 && r.Max == 0 && r.Domain == 0
}
// GetMax implements the interface method.
func (r RangeOfFloat64) GetMax() interface{} {
return r.MaxValue
// Delta returns the difference between the min and max value.
func (r Range) Delta() float64 {
return r.Max - r.Min
}
// String returns a simple string for the range.
func (r Range) String() string {
return fmt.Sprintf("Range [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
}
// 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.
// Translate(50) would yield (50.0/90.0)*600 ~= 333.33
func (r RangeOfFloat64) Translate(value interface{}) int {
if typedValue, isTyped := value.(float64); isTyped {
finalValue := ((r.MaxValue - typedValue) / r.MinMaxDelta) * float64(r.Domain)
func (r Range) Translate(value float64) int {
finalValue := ((r.Max - value) / r.Delta()) * float64(r.Domain)
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 (
"testing"
"time"
"github.com/blendlabs/go-assert"
)
@ -10,26 +9,7 @@ import (
func TestRangeTranslate(t *testing.T) {
assert := assert.New(t)
values := []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
r := NewRangeOfFloat64(1000, values...)
assert.Equal(1.0, r.GetMin())
assert.Equal(8.0, r.GetMax())
r := Range{Domain: 1000}
r.Min, r.Max = MinAndMax(values...)
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
import (
"fmt"
"image"
"image/color"
"image/png"
@ -34,55 +35,60 @@ type rasterRenderer struct {
// SetStrokeColor implements the interface method.
func (rr *rasterRenderer) SetStrokeColor(c color.RGBA) {
println("SetStrokeColor")
println("RasterRenderer :: SetStrokeColor", ColorAsString(c))
rr.gc.SetStrokeColor(c)
}
// SetFillColor implements the interface method.
func (rr *rasterRenderer) SetFillColor(c color.RGBA) {
println("SetFillColor")
println("RasterRenderer :: SetFillColor", ColorAsString(c))
rr.gc.SetFillColor(c)
}
// SetLineWidth implements the interface method.
func (rr *rasterRenderer) SetLineWidth(width int) {
println("SetLineWidth", width)
rr.gc.SetLineWidth(float64(width))
func (rr *rasterRenderer) SetLineWidth(width float64) {
println("RasterRenderer :: SetLineWidth", width)
rr.gc.SetLineWidth(width)
}
// MoveTo implements the interface method.
func (rr *rasterRenderer) MoveTo(x, y int) {
println("MoveTo", x, y)
println("RasterRenderer :: MoveTo", x, y)
rr.gc.MoveTo(float64(x), float64(y))
}
// LineTo implements the interface method.
func (rr *rasterRenderer) LineTo(x, y int) {
println("LineTo", x, y)
println("RasterRenderer :: LineTo", x, y)
rr.gc.LineTo(float64(x), float64(y))
}
// Close implements the interface method.
func (rr *rasterRenderer) Close() {
println("Close")
println("RasterRenderer :: Close")
rr.gc.Close()
}
// Stroke implements the interface method.
func (rr *rasterRenderer) Stroke() {
println("Stroke")
println("RasterRenderer :: Stroke")
rr.gc.Stroke()
}
// Fill implements the interface method.
func (rr *rasterRenderer) Fill() {
println("RasterRenderer :: Fill")
rr.gc.Fill()
}
// FillStroke implements the interface method.
func (rr *rasterRenderer) FillStroke() {
println("FillStroke")
println("RasterRenderer :: FillStroke")
rr.gc.FillStroke()
}
// Circle implements the interface method.
func (rr *rasterRenderer) Circle(radius float64, x, y int) {
println("Circle", radius, x, y)
xf := float64(x)
yf := float64(y)
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.
func (rr *rasterRenderer) SetFont(f *truetype.Font) {
println("SetFont")
rr.font = f
rr.gc.SetFont(f)
}
// SetFontSize implements the interface method.
func (rr *rasterRenderer) SetFontSize(size float64) {
println("SetFontSize", size)
println("RasterRenderer :: SetFontSize", fmt.Sprintf("%.2f", size))
rr.fontSize = size
rr.gc.SetFontSize(size)
}
// SetFontColor implements the interface method.
func (rr *rasterRenderer) SetFontColor(c color.RGBA) {
println("SetFontColor")
println("RasterRenderer :: SetFontColor", ColorAsString(c))
rr.fontColor = c
rr.gc.SetStrokeColor(c)
}
// Text implements the interface method.
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))
}
// MeasureText implements the interface method.
func (rr *rasterRenderer) MeasureText(body string) int {
println("MeasureText", body)
if rr.fc == nil && rr.font != nil {
rr.fc = &font.Drawer{
Face: truetype.NewFace(rr.font, &truetype.Options{
@ -141,6 +145,6 @@ func (rr *rasterRenderer) MeasureText(body string) int {
// Save implements the interface method.
func (rr *rasterRenderer) Save(w io.Writer) error {
println("Save")
println("RasterRenderer :: Save")
return png.Encode(w, rr.i)
}

View file

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

View file

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

View file

@ -28,29 +28,3 @@ func TestTimeSeriesGetValue(t *testing.T) {
assert.NotZero(x0)
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.
type Style struct {
Show bool
StrokeColor color.RGBA
FillColor color.RGBA
StrokeWidth int
StrokeWidth float64
FontSize float64
FontColor color.RGBA
}
// IsZero returns if the object is set or not.
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.
func (s Style) GetStrokeColor() color.RGBA {
func (s Style) GetStrokeColor(defaults ...color.RGBA) color.RGBA {
if ColorIsZero(s.StrokeColor) {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultLineColor
}
return s.StrokeColor
}
// 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 len(defaults) > 0 {
return defaults[0]
}
return DefaultFillColor
}
return s.FillColor
}
// GetStrokeWidth returns the stroke width.
func (s Style) GetStrokeWidth() int {
func (s Style) GetStrokeWidth(defaults ...float64) float64 {
if s.StrokeWidth == 0 {
if len(defaults) > 0 {
return defaults[0]
}
return DefaultLineWidth
}
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
import (
"fmt"
"image/color"
"time"
)
@ -10,6 +11,11 @@ func ColorIsZero(c color.RGBA) bool {
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.
func MinAndMax(values ...float64) (min float64, max float64) {
if len(values) == 0 {