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
|
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
|
||||||
|
|
|
@ -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},
|
||||||
},
|
},
|
||||||
|
|
18
defaults.go
18
defaults.go
|
@ -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
|
||||||
|
|
97
range.go
97
range.go
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
Domain int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMin implements the interface method.
|
// IsZero returns if the range has been set or not.
|
||||||
func (r RangeOfFloat64) GetMin() interface{} {
|
func (r Range) IsZero() bool {
|
||||||
return r.MinValue
|
return r.Min == 0 && r.Max == 0 && r.Domain == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMax implements the interface method.
|
// Delta returns the difference between the min and max value.
|
||||||
func (r RangeOfFloat64) GetMax() interface{} {
|
func (r Range) Delta() float64 {
|
||||||
return r.MaxValue
|
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.
|
// 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)))
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
47
series.go
47
series.go
|
@ -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...)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
44
style.go
44
style.go
|
@ -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
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
|
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 {
|
||||||
|
|
Loading…
Reference in a new issue