initial commit.
This commit is contained in:
commit
2dd44d3675
210
chart.go
Normal file
210
chart.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
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
|
||||
|
||||
Width int
|
||||
Height int
|
||||
Padding int
|
||||
|
||||
BackgroundColor color.RGBA
|
||||
CanvasBackgroundColor color.RGBA
|
||||
|
||||
AxisShow bool
|
||||
AxisStyle Style
|
||||
AxisFontSize float64
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetCanvasLeft gets the left corner pixel.
|
||||
func (c Chart) GetCanvasLeft() int {
|
||||
return c.Padding
|
||||
}
|
||||
|
||||
// GetCanvasBottom gets the bottom corner pixel.
|
||||
func (c Chart) GetCanvasBottom() int {
|
||||
return c.Height - c.Padding
|
||||
}
|
||||
|
||||
// GetCanvasRight gets the right corner pixel.
|
||||
func (c Chart) GetCanvasRight() int {
|
||||
return c.Width - c.Padding
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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 {
|
||||
r := provider(c.Width, c.Height)
|
||||
c.drawBackground(r)
|
||||
c.drawCanvas(r)
|
||||
c.drawAxes(r)
|
||||
|
||||
for _, series := range c.Series {
|
||||
c.drawSeries(r, series)
|
||||
}
|
||||
err := c.drawTitle(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.Save(w)
|
||||
}
|
||||
|
||||
func (c Chart) drawBackground(r Renderer) {
|
||||
r.SetStrokeColor(c.GetBackgroundColor())
|
||||
r.SetFillColor(c.GetBackgroundColor())
|
||||
r.SetLineWidth(0)
|
||||
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()
|
||||
}
|
||||
|
||||
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.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.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(DefaultLineWidth)
|
||||
}
|
||||
r.MoveTo(c.GetCanvasLeft(), c.GetCanvasBottom())
|
||||
r.LineTo(c.GetCanvasRight(), c.GetCanvasBottom())
|
||||
r.LineTo(c.GetCanvasRight(), c.GetCanvasTop())
|
||||
r.Stroke()
|
||||
}
|
||||
}
|
||||
|
||||
func (c Chart) drawSeries(r Renderer, s Series) {
|
||||
|
||||
}
|
||||
|
||||
func (c Chart) drawTitle(r Renderer) error {
|
||||
if len(c.Title) > 0 {
|
||||
font, err := c.GetTextFont()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.SetFontColor(c.GetTextFontColor())
|
||||
r.SetFont(font)
|
||||
r.SetFontSize(c.GetTitleFontSize())
|
||||
textWidth := r.MeasureText(c.Title)
|
||||
titleX := (c.Width >> 1) - (textWidth >> 1)
|
||||
titleY := c.GetCanvasTop() + int(c.GetTitleFontSize()/2.0)
|
||||
r.Text(c.Title, titleX, titleY)
|
||||
}
|
||||
return nil
|
||||
}
|
30
chart_test.go
Normal file
30
chart_test.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestChartSingleSeries(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
now := time.Now()
|
||||
c := Chart{
|
||||
Title: "Hello!",
|
||||
Width: 1024,
|
||||
Height: 400,
|
||||
Series: []Series{
|
||||
TimeSeries{
|
||||
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},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
c.Render(PNG, buffer)
|
||||
assert.NotEmpty(buffer.Bytes())
|
||||
}
|
69
defaults.go
Normal file
69
defaults.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"sync"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultChartHeight is the default chart height.
|
||||
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
|
||||
//DefaultDPI is the default dots per inch for the chart.
|
||||
DefaultDPI = 120.0
|
||||
// DefaultMinimumFontSize is the default minimum font size.
|
||||
DefaultMinimumFontSize = 8.0
|
||||
)
|
||||
|
||||
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}
|
||||
// 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}
|
||||
// DefaultTextColor is the default chart text color.
|
||||
// It is equivalent to #333333.
|
||||
DefaultTextColor = color.RGBA{R: 51, G: 51, B: 51, A: 255}
|
||||
// DefaultAxisColor is the default chart axis line color.
|
||||
// It is equivalent to #333333.
|
||||
DefaultAxisColor = color.RGBA{R: 51, G: 51, B: 51, A: 255}
|
||||
// DefaultBorderColor is the default chart border color.
|
||||
// It is equivalent to #efefef.
|
||||
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}
|
||||
// DefaultFillColor is the default fill color.
|
||||
// It is equivalent to #0074d9.
|
||||
DefaultFillColor = color.RGBA{R: 0, G: 217, B: 116, A: 255}
|
||||
)
|
||||
|
||||
var (
|
||||
_defaultFontLock sync.Mutex
|
||||
_defaultFont *truetype.Font
|
||||
)
|
||||
|
||||
// GetDefaultFont returns the default font (Roboto-Medium).
|
||||
func GetDefaultFont() (*truetype.Font, error) {
|
||||
if _defaultFont == nil {
|
||||
_defaultFontLock.Lock()
|
||||
defer _defaultFontLock.Unlock()
|
||||
if _defaultFont == nil {
|
||||
font, err := truetype.Parse(roboto)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_defaultFont = font
|
||||
}
|
||||
}
|
||||
return _defaultFont, nil
|
||||
}
|
41
point.go
Normal file
41
point.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package chart
|
||||
|
||||
// Points are an array of points.
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Point represents a x,y coordinate.
|
||||
type Point struct {
|
||||
X float64
|
||||
Y float64
|
||||
}
|
||||
|
||||
// Points represents a group of points.
|
||||
type Points []Point
|
||||
|
||||
// String returns a string representation of the points.
|
||||
func (p Points) String() string {
|
||||
var values []string
|
||||
for _, v := range p {
|
||||
values = append(values, fmt.Sprintf("%d,%d", int(v.X), int(v.Y)))
|
||||
}
|
||||
return strings.Join(values, "\n")
|
||||
}
|
||||
|
||||
// Len returns the length of the points set.
|
||||
func (p Points) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
|
||||
// Swap swaps two elments.
|
||||
func (p Points) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
|
||||
// Less returns if the X value of one element is less than another.
|
||||
// This is the default sort for charts where you plot by x values in order.
|
||||
func (p Points) Less(i, j int) bool {
|
||||
return p[i].X < p[j].X
|
||||
}
|
97
range.go
Normal file
97
range.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"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
|
||||
Domain int
|
||||
}
|
||||
|
||||
// GetMin implements the interface method.
|
||||
func (r RangeOfFloat64) GetMin() interface{} {
|
||||
return r.MinValue
|
||||
}
|
||||
|
||||
// GetMax implements the interface method.
|
||||
func (r RangeOfFloat64) GetMax() interface{} {
|
||||
return r.MaxValue
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
39
range_test.go
Normal file
39
range_test.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
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 := NewRange(1000, values...)
|
||||
assert.Equal(1.0, r.MinValue)
|
||||
assert.Equal(8.0, r.MaxValue)
|
||||
assert.Equal(7.0, r.MinMaxDelta)
|
||||
assert.Equal(1000, r.Domain)
|
||||
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.MinValue)
|
||||
assert.Equal(values[0], r.MaxValue)
|
||||
assert.Equal(values[0].Unix()-values[7].Unix(), r.MinMaxDelta)
|
||||
assert.Equal(1000, r.Domain)
|
||||
assert.Equal(571, r.Translate(time.Now().AddDate(0, 0, -5)))
|
||||
}
|
131
raster_renderer.go
Normal file
131
raster_renderer.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
drawing "github.com/llgcode/draw2d/draw2dimg"
|
||||
)
|
||||
|
||||
// PNG returns a new png/raster renderer.
|
||||
func PNG(width, height int) Renderer {
|
||||
i := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
return &rasterRenderer{
|
||||
i: i,
|
||||
gc: drawing.NewGraphicContext(i),
|
||||
}
|
||||
}
|
||||
|
||||
// RasterRenderer renders chart commands to a bitmap.
|
||||
type rasterRenderer struct {
|
||||
i *image.RGBA
|
||||
gc *drawing.GraphicContext
|
||||
fc *font.Drawer
|
||||
|
||||
font *truetype.Font
|
||||
fontColor color.RGBA
|
||||
fontSize float64
|
||||
}
|
||||
|
||||
// SetStrokeColor implements the interface method.
|
||||
func (rr *rasterRenderer) SetStrokeColor(c color.RGBA) {
|
||||
rr.gc.SetStrokeColor(c)
|
||||
}
|
||||
|
||||
// SetFillColor implements the interface method.
|
||||
func (rr *rasterRenderer) SetFillColor(c color.RGBA) {
|
||||
rr.gc.SetFillColor(c)
|
||||
}
|
||||
|
||||
// SetLineWidth implements the interface method.
|
||||
func (rr *rasterRenderer) SetLineWidth(width int) {
|
||||
rr.gc.SetLineWidth(float64(width))
|
||||
}
|
||||
|
||||
// MoveTo implements the interface method.
|
||||
func (rr *rasterRenderer) MoveTo(x, y int) {
|
||||
rr.gc.MoveTo(float64(x), float64(y))
|
||||
}
|
||||
|
||||
// LineTo implements the interface method.
|
||||
func (rr *rasterRenderer) LineTo(x, y int) {
|
||||
rr.gc.LineTo(float64(x), float64(y))
|
||||
}
|
||||
|
||||
// Close implements the interface method.
|
||||
func (rr *rasterRenderer) Close() {
|
||||
rr.gc.Close()
|
||||
}
|
||||
|
||||
// Stroke implements the interface method.
|
||||
func (rr *rasterRenderer) Stroke() {
|
||||
rr.gc.Stroke()
|
||||
}
|
||||
|
||||
// FillStroke implements the interface method.
|
||||
func (rr *rasterRenderer) FillStroke() {
|
||||
rr.gc.FillStroke()
|
||||
}
|
||||
|
||||
// Circle implements the interface method.
|
||||
func (rr *rasterRenderer) Circle(radius float64, x, y int) {
|
||||
xf := float64(x)
|
||||
yf := float64(y)
|
||||
rr.gc.MoveTo(xf-radius, yf) //9
|
||||
rr.gc.QuadCurveTo(xf, yf, xf, yf-radius) //12
|
||||
rr.gc.QuadCurveTo(xf, yf, xf+radius, yf) //3
|
||||
rr.gc.QuadCurveTo(xf, yf, xf, yf+radius) //6
|
||||
rr.gc.QuadCurveTo(xf, yf, xf-radius, yf) //9
|
||||
rr.gc.Close()
|
||||
rr.gc.FillStroke()
|
||||
}
|
||||
|
||||
// SetFont implements the interface method.
|
||||
func (rr *rasterRenderer) SetFont(f *truetype.Font) {
|
||||
rr.font = f
|
||||
rr.gc.SetFont(f)
|
||||
}
|
||||
|
||||
// SetFontSize implements the interface method.
|
||||
func (rr *rasterRenderer) SetFontSize(size float64) {
|
||||
rr.fontSize = size
|
||||
rr.gc.SetFontSize(size)
|
||||
}
|
||||
|
||||
// SetFontColor implements the interface method.
|
||||
func (rr *rasterRenderer) SetFontColor(c color.RGBA) {
|
||||
rr.fontColor = c
|
||||
rr.gc.SetStrokeColor(c)
|
||||
}
|
||||
|
||||
// Text implements the interface method.
|
||||
func (rr *rasterRenderer) Text(body string, x, y int) {
|
||||
rr.gc.CreateStringPath(body, float64(x), float64(y))
|
||||
}
|
||||
|
||||
// MeasureText implements the interface method.
|
||||
func (rr *rasterRenderer) MeasureText(body string) int {
|
||||
if rr.fc == nil && rr.font != nil {
|
||||
rr.fc = &font.Drawer{
|
||||
Face: truetype.NewFace(rr.font, &truetype.Options{
|
||||
DPI: DefaultDPI,
|
||||
Size: rr.fontSize,
|
||||
}),
|
||||
}
|
||||
}
|
||||
if rr.fc != nil {
|
||||
dimensions := rr.fc.MeasureString(body)
|
||||
return dimensions.Floor()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Save implements the interface method.
|
||||
func (rr *rasterRenderer) Save(w io.Writer) error {
|
||||
return png.Encode(w, rr.i)
|
||||
}
|
60
renderer.go
Normal file
60
renderer.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"io"
|
||||
|
||||
"github.com/golang/freetype/truetype"
|
||||
)
|
||||
|
||||
// RendererProvider is a function that returns a renderer.
|
||||
type RendererProvider func(int, int) Renderer
|
||||
|
||||
// Renderer represents the basic methods required to draw a chart.
|
||||
type Renderer interface {
|
||||
// SetStrokeColor sets the current stroke color.
|
||||
SetStrokeColor(color.RGBA)
|
||||
|
||||
// SetFillColor sets the current fill color.
|
||||
SetFillColor(color.RGBA)
|
||||
|
||||
// SetLineWidth sets the stroke line width.
|
||||
SetLineWidth(width int)
|
||||
|
||||
// MoveTo moves the cursor to a given point.
|
||||
MoveTo(x, y int)
|
||||
|
||||
// LineTo both starts a shape and draws a line to a given point
|
||||
// from the previous point.
|
||||
LineTo(x, y int)
|
||||
|
||||
// Close finalizes a shape as drawn by LineTo.
|
||||
Close()
|
||||
|
||||
// Stroke draws the 'stroke' or line component of a shape.
|
||||
Stroke()
|
||||
|
||||
// FillStroke draws the 'stroke' and 'fills' a shape.
|
||||
FillStroke()
|
||||
|
||||
// Circle draws a circle at the given coords with a given radius.
|
||||
Circle(radius float64, x, y int)
|
||||
|
||||
// SetFont sets a font for a text field.
|
||||
SetFont(*truetype.Font)
|
||||
|
||||
// SetFontColor sets a font's color
|
||||
SetFontColor(color.RGBA)
|
||||
|
||||
// SetFontSize sets the font size for a text field.
|
||||
SetFontSize(size float64)
|
||||
|
||||
// Text draws a text blob.
|
||||
Text(body string, x, y int)
|
||||
|
||||
// MeasureText measures text.
|
||||
MeasureText(body string) int
|
||||
|
||||
// Save writes the image to the given writer.
|
||||
Save(w io.Writer) error
|
||||
}
|
92
series.go
Normal file
92
series.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package chart
|
||||
|
||||
import "time"
|
||||
|
||||
// Series is a entity data set.
|
||||
type Series interface {
|
||||
GetName() string
|
||||
GetStyle() Style
|
||||
Len() int
|
||||
GetValue(index int) Point
|
||||
|
||||
GetXRange(domain int) Range
|
||||
GetYRange(domain int) Range
|
||||
}
|
||||
|
||||
// TimeSeries is a line on a chart.
|
||||
type TimeSeries struct {
|
||||
Name string
|
||||
Style Style
|
||||
|
||||
XValues []time.Time
|
||||
YValues []float64
|
||||
}
|
||||
|
||||
// GetName returns the name of the time series.
|
||||
func (ts TimeSeries) GetName() string {
|
||||
return ts.Name
|
||||
}
|
||||
|
||||
// GetStyle returns the line style.
|
||||
func (ts TimeSeries) GetStyle() Style {
|
||||
return ts.Style
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the series.
|
||||
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) Point {
|
||||
return Point{X: float64(ts.XValues[index].Unix()), Y: ts.YValues[index]}
|
||||
}
|
||||
|
||||
// ContinousSeries represents a line on a chart.
|
||||
type ContinousSeries struct {
|
||||
Name string
|
||||
Style Style
|
||||
|
||||
XValues []float64
|
||||
YValues []float64
|
||||
}
|
||||
|
||||
// GetName returns the name of the time series.
|
||||
func (cs ContinousSeries) GetName() string {
|
||||
return cs.Name
|
||||
}
|
||||
|
||||
// GetStyle returns the line style.
|
||||
func (cs ContinousSeries) GetStyle() Style {
|
||||
return cs.Style
|
||||
}
|
||||
|
||||
// Len returns the number of elements in the series.
|
||||
func (cs ContinousSeries) Len() int {
|
||||
return len(cs.XValues)
|
||||
}
|
||||
|
||||
// GetValue gets a value at a given index.
|
||||
func (cs ContinousSeries) GetValue(index int) Point {
|
||||
return Point{X: cs.XValues[index], Y: 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...)
|
||||
}
|
1
series_test.go
Normal file
1
series_test.go
Normal file
|
@ -0,0 +1 @@
|
|||
package chart
|
39
style.go
Normal file
39
style.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package chart
|
||||
|
||||
import "image/color"
|
||||
|
||||
// Style is a simple style set.
|
||||
type Style struct {
|
||||
StrokeColor color.RGBA
|
||||
FillColor color.RGBA
|
||||
StrokeWidth int
|
||||
}
|
||||
|
||||
// IsZero returns if the object is set or not.
|
||||
func (s Style) IsZero() bool {
|
||||
return ColorIsZero(s.StrokeColor) && ColorIsZero(s.FillColor) && s.StrokeWidth == 0
|
||||
}
|
||||
|
||||
// GetStrokeColor returns the stroke color.
|
||||
func (s Style) GetStrokeColor() color.RGBA {
|
||||
if ColorIsZero(s.StrokeColor) {
|
||||
return DefaultLineColor
|
||||
}
|
||||
return s.StrokeColor
|
||||
}
|
||||
|
||||
// GetFillColor returns the fill color.
|
||||
func (s Style) GetFillColor() color.RGBA {
|
||||
if ColorIsZero(s.FillColor) {
|
||||
return DefaultFillColor
|
||||
}
|
||||
return s.FillColor
|
||||
}
|
||||
|
||||
// GetStrokeWidth returns the stroke width.
|
||||
func (s Style) GetStrokeWidth() int {
|
||||
if s.StrokeWidth == 0 {
|
||||
return DefaultLineWidth
|
||||
}
|
||||
return s.StrokeWidth
|
||||
}
|
50
util.go
Normal file
50
util.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ColorIsZero returns if a color.RGBA is unset or not.
|
||||
func ColorIsZero(c color.RGBA) bool {
|
||||
return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0
|
||||
}
|
||||
|
||||
// MinAndMax returns both the min and max in one pass.
|
||||
func MinAndMax(values ...float64) (min float64, max float64) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
min = values[0]
|
||||
max = values[0]
|
||||
for _, v := range values {
|
||||
if max < v {
|
||||
max = v
|
||||
}
|
||||
if min > v {
|
||||
min = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MinAndMaxOfTime returns the min and max of a given set of times
|
||||
// in one pass.
|
||||
func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
min = values[0]
|
||||
max = values[0]
|
||||
|
||||
for _, v := range values {
|
||||
if max.Before(v) {
|
||||
max = v
|
||||
}
|
||||
if min.After(v) {
|
||||
min = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
66
util_test.go
Normal file
66
util_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package chart
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/blendlabs/go-assert"
|
||||
)
|
||||
|
||||
func TestMinAndMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []float64{1.0, 2.0, 3.0, 4.0}
|
||||
min, max := MinAndMax(values...)
|
||||
assert.Equal(1.0, min)
|
||||
assert.Equal(4.0, max)
|
||||
}
|
||||
|
||||
func TestMinAndMaxReversed(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []float64{4.0, 2.0, 3.0, 1.0}
|
||||
min, max := MinAndMax(values...)
|
||||
assert.Equal(1.0, min)
|
||||
assert.Equal(4.0, max)
|
||||
}
|
||||
|
||||
func TestMinAndMaxEmpty(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []float64{}
|
||||
min, max := MinAndMax(values...)
|
||||
assert.Equal(0.0, min)
|
||||
assert.Equal(0.0, max)
|
||||
}
|
||||
|
||||
func TestMinAndMaxOfTime(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),
|
||||
}
|
||||
min, max := MinAndMaxOfTime(values...)
|
||||
assert.Equal(values[3], min)
|
||||
assert.Equal(values[0], max)
|
||||
}
|
||||
|
||||
func TestMinAndMaxOfTimeReversed(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []time.Time{
|
||||
time.Now().AddDate(0, 0, -4),
|
||||
time.Now().AddDate(0, 0, -2),
|
||||
time.Now().AddDate(0, 0, -3),
|
||||
time.Now().AddDate(0, 0, -1),
|
||||
}
|
||||
min, max := MinAndMaxOfTime(values...)
|
||||
assert.Equal(values[0], min)
|
||||
assert.Equal(values[3], max)
|
||||
}
|
||||
|
||||
func TestMinAndMaxOfTimeEmpty(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []time.Time{}
|
||||
min, max := MinAndMaxOfTime(values...)
|
||||
assert.Equal(time.Time{}, min)
|
||||
assert.Equal(time.Time{}, max)
|
||||
}
|
Loading…
Reference in New Issue
Block a user