updates.
This commit is contained in:
parent
e09aa43a1e
commit
cbc4672f1e
13 changed files with 359 additions and 303 deletions
65
box.go
Normal file
65
box.go
Normal 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
220
chart.go
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
},
|
||||
|
|
18
defaults.go
18
defaults.go
|
@ -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
|
||||
|
|
97
range.go
97
range.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
47
series.go
47
series.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
44
style.go
44
style.go
|
@ -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
58
testserver/main.go
Normal 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())
|
||||
}
|
6
util.go
6
util.go
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue