Adds the ability to draw an XY scatter plot. (#27)
* works more or less * updating comment * removing debugging printf * adding output * tweaks * missed a couple series validations * testing auto coloring * updated output.png * color tests etc. * sanity check tests. * should not use unkeyed fields anyway.
This commit is contained in:
parent
17b28beae8
commit
b713ff85cc
|
@ -23,7 +23,7 @@ func parseFloat64(str string) float64 {
|
||||||
func readData() ([]time.Time, []float64) {
|
func readData() ([]time.Time, []float64) {
|
||||||
var xvalues []time.Time
|
var xvalues []time.Time
|
||||||
var yvalues []float64
|
var yvalues []float64
|
||||||
chart.File.ReadByLines("requests.csv", func(line string) {
|
err := chart.File.ReadByLines("requests.csv", func(line string) {
|
||||||
parts := strings.Split(line, ",")
|
parts := strings.Split(line, ",")
|
||||||
year := parseInt(parts[0])
|
year := parseInt(parts[0])
|
||||||
month := parseInt(parts[1])
|
month := parseInt(parts[1])
|
||||||
|
@ -33,6 +33,9 @@ func readData() ([]time.Time, []float64) {
|
||||||
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
|
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
|
||||||
yvalues = append(yvalues, elapsedMillis)
|
yvalues = append(yvalues, elapsedMillis)
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
return xvalues, yvalues
|
return xvalues, yvalues
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
80
_examples/scatter/main.go
Normal file
80
_examples/scatter/main.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart"
|
||||||
|
)
|
||||||
|
|
||||||
|
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
graph := chart.Chart{
|
||||||
|
Series: []chart.Series{
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
Style: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
StrokeWidth: chart.Disabled,
|
||||||
|
DotWidth: 3,
|
||||||
|
},
|
||||||
|
XValues: chart.Sequence.Random(32, 1024),
|
||||||
|
YValues: chart.Sequence.Random(32, 1024),
|
||||||
|
},
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
Style: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
StrokeWidth: chart.Disabled,
|
||||||
|
DotWidth: 5,
|
||||||
|
},
|
||||||
|
XValues: chart.Sequence.Random(16, 1024),
|
||||||
|
YValues: chart.Sequence.Random(16, 1024),
|
||||||
|
},
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
Style: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
StrokeWidth: chart.Disabled,
|
||||||
|
DotWidth: 7,
|
||||||
|
},
|
||||||
|
XValues: chart.Sequence.Random(8, 1024),
|
||||||
|
YValues: chart.Sequence.Random(8, 1024),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
err := graph.Render(chart.PNG, res)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func unit(res http.ResponseWriter, req *http.Request) {
|
||||||
|
graph := chart.Chart{
|
||||||
|
Height: 50,
|
||||||
|
Width: 50,
|
||||||
|
Canvas: chart.Style{
|
||||||
|
Padding: chart.Box{IsSet: true},
|
||||||
|
},
|
||||||
|
Background: chart.Style{
|
||||||
|
Padding: chart.Box{IsSet: true},
|
||||||
|
},
|
||||||
|
Series: []chart.Series{
|
||||||
|
chart.ContinuousSeries{
|
||||||
|
XValues: chart.Sequence.Float64(0, 4, 1),
|
||||||
|
YValues: chart.Sequence.Float64(0, 4, 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
err := graph.Render(chart.PNG, res)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", drawChart)
|
||||||
|
http.HandleFunc("/unit", unit)
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
BIN
_examples/scatter/output.png
Normal file
BIN
_examples/scatter/output.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
29
box.go
29
box.go
|
@ -5,16 +5,36 @@ import (
|
||||||
"math"
|
"math"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// BoxZero is a preset box that represents an intentional zero value.
|
||||||
|
BoxZero = Box{IsSet: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBox returns a new (set) box.
|
||||||
|
func NewBox(top, left, right, bottom int) Box {
|
||||||
|
return Box{
|
||||||
|
IsSet: true,
|
||||||
|
Top: top,
|
||||||
|
Left: left,
|
||||||
|
Right: right,
|
||||||
|
Bottom: bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Box represents the main 4 dimensions of a box.
|
// Box represents the main 4 dimensions of a box.
|
||||||
type Box struct {
|
type Box struct {
|
||||||
Top int
|
Top int
|
||||||
Left int
|
Left int
|
||||||
Right int
|
Right int
|
||||||
Bottom int
|
Bottom int
|
||||||
|
IsSet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsZero returns if the box is set or not.
|
// IsZero returns if the box is set or not.
|
||||||
func (b Box) IsZero() bool {
|
func (b Box) IsZero() bool {
|
||||||
|
if b.IsSet {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0
|
return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +45,7 @@ func (b Box) String() string {
|
||||||
|
|
||||||
// GetTop returns a coalesced value with a default.
|
// GetTop returns a coalesced value with a default.
|
||||||
func (b Box) GetTop(defaults ...int) int {
|
func (b Box) GetTop(defaults ...int) int {
|
||||||
if b.Top == 0 {
|
if !b.IsSet && b.Top == 0 {
|
||||||
if len(defaults) > 0 {
|
if len(defaults) > 0 {
|
||||||
return defaults[0]
|
return defaults[0]
|
||||||
}
|
}
|
||||||
|
@ -36,7 +56,7 @@ func (b Box) GetTop(defaults ...int) int {
|
||||||
|
|
||||||
// GetLeft returns a coalesced value with a default.
|
// GetLeft returns a coalesced value with a default.
|
||||||
func (b Box) GetLeft(defaults ...int) int {
|
func (b Box) GetLeft(defaults ...int) int {
|
||||||
if b.Left == 0 {
|
if !b.IsSet && b.Left == 0 {
|
||||||
if len(defaults) > 0 {
|
if len(defaults) > 0 {
|
||||||
return defaults[0]
|
return defaults[0]
|
||||||
}
|
}
|
||||||
|
@ -47,7 +67,7 @@ func (b Box) GetLeft(defaults ...int) int {
|
||||||
|
|
||||||
// GetRight returns a coalesced value with a default.
|
// GetRight returns a coalesced value with a default.
|
||||||
func (b Box) GetRight(defaults ...int) int {
|
func (b Box) GetRight(defaults ...int) int {
|
||||||
if b.Right == 0 {
|
if !b.IsSet && b.Right == 0 {
|
||||||
if len(defaults) > 0 {
|
if len(defaults) > 0 {
|
||||||
return defaults[0]
|
return defaults[0]
|
||||||
}
|
}
|
||||||
|
@ -58,7 +78,7 @@ func (b Box) GetRight(defaults ...int) int {
|
||||||
|
|
||||||
// GetBottom returns a coalesced value with a default.
|
// GetBottom returns a coalesced value with a default.
|
||||||
func (b Box) GetBottom(defaults ...int) int {
|
func (b Box) GetBottom(defaults ...int) int {
|
||||||
if b.Bottom == 0 {
|
if !b.IsSet && b.Bottom == 0 {
|
||||||
if len(defaults) > 0 {
|
if len(defaults) > 0 {
|
||||||
return defaults[0]
|
return defaults[0]
|
||||||
}
|
}
|
||||||
|
@ -91,6 +111,7 @@ func (b Box) Aspect() float64 {
|
||||||
// Clone returns a new copy of the box.
|
// Clone returns a new copy of the box.
|
||||||
func (b Box) Clone() Box {
|
func (b Box) Clone() Box {
|
||||||
return Box{
|
return Box{
|
||||||
|
IsSet: b.IsSet,
|
||||||
Top: b.Top,
|
Top: b.Top,
|
||||||
Left: b.Left,
|
Left: b.Left,
|
||||||
Right: b.Right,
|
Right: b.Right,
|
||||||
|
|
|
@ -109,9 +109,9 @@ func TestBoxConstrain(t *testing.T) {
|
||||||
func TestBoxOuterConstrain(t *testing.T) {
|
func TestBoxOuterConstrain(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
box := Box{0, 0, 100, 100}
|
box := NewBox(0, 0, 100, 100)
|
||||||
canvas := Box{5, 5, 95, 95}
|
canvas := NewBox(5, 5, 95, 95)
|
||||||
taller := Box{-10, 5, 50, 50}
|
taller := NewBox(-10, 5, 50, 50)
|
||||||
|
|
||||||
c := canvas.OuterConstrain(box, taller)
|
c := canvas.OuterConstrain(box, taller)
|
||||||
assert.Equal(15, c.Top, c.String())
|
assert.Equal(15, c.Top, c.String())
|
||||||
|
@ -119,7 +119,7 @@ func TestBoxOuterConstrain(t *testing.T) {
|
||||||
assert.Equal(95, c.Right, c.String())
|
assert.Equal(95, c.Right, c.String())
|
||||||
assert.Equal(95, c.Bottom, c.String())
|
assert.Equal(95, c.Bottom, c.String())
|
||||||
|
|
||||||
wider := Box{5, 5, 110, 50}
|
wider := NewBox(5, 5, 110, 50)
|
||||||
d := canvas.OuterConstrain(box, wider)
|
d := canvas.OuterConstrain(box, wider)
|
||||||
assert.Equal(5, d.Top, d.String())
|
assert.Equal(5, d.Top, d.String())
|
||||||
assert.Equal(5, d.Left, d.String())
|
assert.Equal(5, d.Left, d.String())
|
||||||
|
|
5
chart.go
5
chart.go
|
@ -141,8 +141,10 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
|
||||||
func (c Chart) checkHasVisibleSeries() error {
|
func (c Chart) checkHasVisibleSeries() error {
|
||||||
hasVisibleSeries := false
|
hasVisibleSeries := false
|
||||||
|
var style Style
|
||||||
for _, s := range c.Series {
|
for _, s := range c.Series {
|
||||||
hasVisibleSeries = hasVisibleSeries || (s.GetStyle().IsZero() || s.GetStyle().Show)
|
style = s.GetStyle()
|
||||||
|
hasVisibleSeries = hasVisibleSeries || (style.IsZero() || style.Show)
|
||||||
}
|
}
|
||||||
if !hasVisibleSeries {
|
if !hasVisibleSeries {
|
||||||
return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true")
|
return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true")
|
||||||
|
@ -511,6 +513,7 @@ func (c Chart) styleDefaultsCanvas() Style {
|
||||||
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
||||||
strokeColor := GetDefaultColor(seriesIndex)
|
strokeColor := GetDefaultColor(seriesIndex)
|
||||||
return Style{
|
return Style{
|
||||||
|
DotColor: strokeColor,
|
||||||
StrokeColor: strokeColor,
|
StrokeColor: strokeColor,
|
||||||
StrokeWidth: DefaultSeriesLineWidth,
|
StrokeWidth: DefaultSeriesLineWidth,
|
||||||
Font: c.GetFont(),
|
Font: c.GetFont(),
|
||||||
|
|
|
@ -2,6 +2,8 @@ package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -483,3 +485,91 @@ func TestChartCheckRangesWithRanges(t *testing.T) {
|
||||||
xr, yr, yra := c.getRanges()
|
xr, yr, yra := c.getRanges()
|
||||||
assert.Nil(c.checkRanges(xr, yr, yra))
|
assert.Nil(c.checkRanges(xr, yr, yra))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func at(i image.Image, x, y int) drawing.Color {
|
||||||
|
return drawing.ColorFromAlphaMixedRGBA(i.At(x, y).RGBA())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartE2ELine(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Height: 50,
|
||||||
|
Width: 50,
|
||||||
|
Canvas: Style{
|
||||||
|
Padding: Box{IsSet: true},
|
||||||
|
},
|
||||||
|
Background: Style{
|
||||||
|
Padding: Box{IsSet: true},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
XValues: Sequence.Float64(0, 4, 1),
|
||||||
|
YValues: Sequence.Float64(0, 4, 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = &bytes.Buffer{}
|
||||||
|
err := c.Render(PNG, buffer)
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
// do color tests ...
|
||||||
|
|
||||||
|
i, err := png.Decode(buffer)
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
// test the bottom and top of the line
|
||||||
|
assert.Equal(drawing.ColorWhite, at(i, 0, 0))
|
||||||
|
assert.Equal(drawing.ColorWhite, at(i, 49, 49))
|
||||||
|
|
||||||
|
// test a line mid point
|
||||||
|
defaultSeriesColor := GetDefaultColor(0)
|
||||||
|
assert.Equal(defaultSeriesColor, at(i, 0, 49))
|
||||||
|
assert.Equal(defaultSeriesColor, at(i, 49, 0))
|
||||||
|
assert.Equal(drawing.ColorFromHex("bddbf6"), at(i, 24, 24))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChartE2ELineWithFill(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
c := Chart{
|
||||||
|
Height: 50,
|
||||||
|
Width: 50,
|
||||||
|
Canvas: Style{
|
||||||
|
Padding: Box{IsSet: true},
|
||||||
|
},
|
||||||
|
Background: Style{
|
||||||
|
Padding: Box{IsSet: true},
|
||||||
|
},
|
||||||
|
Series: []Series{
|
||||||
|
ContinuousSeries{
|
||||||
|
Style: Style{
|
||||||
|
Show: true,
|
||||||
|
StrokeColor: drawing.ColorBlue,
|
||||||
|
FillColor: drawing.ColorRed,
|
||||||
|
},
|
||||||
|
XValues: Sequence.Float64(0, 4, 1),
|
||||||
|
YValues: Sequence.Float64(0, 4, 1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = &bytes.Buffer{}
|
||||||
|
err := c.Render(PNG, buffer)
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
// do color tests ...
|
||||||
|
|
||||||
|
i, err := png.Decode(buffer)
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
// test the bottom and top of the line
|
||||||
|
assert.Equal(drawing.ColorWhite, at(i, 0, 0))
|
||||||
|
assert.Equal(drawing.ColorRed, at(i, 49, 49))
|
||||||
|
|
||||||
|
// test a line mid point
|
||||||
|
defaultSeriesColor := drawing.ColorBlue
|
||||||
|
assert.Equal(defaultSeriesColor, at(i, 0, 49))
|
||||||
|
assert.Equal(defaultSeriesColor, at(i, 49, 0))
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ const (
|
||||||
DefaultChartWidth = 1024
|
DefaultChartWidth = 1024
|
||||||
// DefaultStrokeWidth is the default chart stroke width.
|
// DefaultStrokeWidth is the default chart stroke width.
|
||||||
DefaultStrokeWidth = 0.0
|
DefaultStrokeWidth = 0.0
|
||||||
|
// DefaultDotWidth is the default chart dot width.
|
||||||
|
DefaultDotWidth = 0.0
|
||||||
// DefaultSeriesLineWidth is the default line width.
|
// DefaultSeriesLineWidth is the default line width.
|
||||||
DefaultSeriesLineWidth = 1.0
|
DefaultSeriesLineWidth = 1.0
|
||||||
// DefaultAxisLineWidth is the line width of the axis lines.
|
// DefaultAxisLineWidth is the line width of the axis lines.
|
||||||
|
|
38
draw.go
38
draw.go
|
@ -27,9 +27,8 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
||||||
var vx, vy float64
|
var vx, vy float64
|
||||||
var x, y int
|
var x, y int
|
||||||
|
|
||||||
fill := style.GetFillColor()
|
if style.ShouldDrawStroke() && style.ShouldDrawFill() {
|
||||||
if !fill.IsZero() {
|
style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
style.GetFillOptions().WriteToRenderer(r)
|
|
||||||
r.MoveTo(x0, y0)
|
r.MoveTo(x0, y0)
|
||||||
for i := 1; i < vs.Len(); i++ {
|
for i := 1; i < vs.Len(); i++ {
|
||||||
vx, vy = vs.GetValue(i)
|
vx, vy = vs.GetValue(i)
|
||||||
|
@ -43,16 +42,33 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style
|
||||||
r.Fill()
|
r.Fill()
|
||||||
}
|
}
|
||||||
|
|
||||||
style.GetStrokeOptions().WriteToRenderer(r)
|
if style.ShouldDrawStroke() {
|
||||||
|
style.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
|
||||||
r.MoveTo(x0, y0)
|
r.MoveTo(x0, y0)
|
||||||
for i := 1; i < vs.Len(); i++ {
|
for i := 1; i < vs.Len(); i++ {
|
||||||
vx, vy = vs.GetValue(i)
|
vx, vy = vs.GetValue(i)
|
||||||
x = cl + xrange.Translate(vx)
|
x = cl + xrange.Translate(vx)
|
||||||
y = cb - yrange.Translate(vy)
|
y = cb - yrange.Translate(vy)
|
||||||
r.LineTo(x, y)
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
if style.ShouldDrawDot() {
|
||||||
|
dotWidth := style.GetDotWidth()
|
||||||
|
|
||||||
|
style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
|
||||||
|
|
||||||
|
for i := 0; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValue(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
|
||||||
|
r.Circle(dotWidth, x, y)
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
r.Stroke()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoundedSeries draws a series that implements BoundedValueProvider.
|
// BoundedSeries draws a series that implements BoundedValueProvider.
|
||||||
|
|
|
@ -46,12 +46,20 @@ func ColorFromHex(hex string) Color {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values.
|
||||||
|
func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
|
||||||
|
fa := float64(a) / 255.0
|
||||||
|
var c Color
|
||||||
|
c.R = uint8(float64(r) / fa)
|
||||||
|
c.G = uint8(float64(g) / fa)
|
||||||
|
c.B = uint8(float64(b) / fa)
|
||||||
|
c.A = uint8(a | (a >> 8))
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// Color is our internal color type because color.Color is bullshit.
|
// Color is our internal color type because color.Color is bullshit.
|
||||||
type Color struct {
|
type Color struct {
|
||||||
R uint8
|
R, G, B, A uint8
|
||||||
G uint8
|
|
||||||
B uint8
|
|
||||||
A uint8
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RGBA returns the color as a pre-alpha mixed color set.
|
// RGBA returns the color as a pre-alpha mixed color set.
|
||||||
|
@ -88,6 +96,24 @@ func (c Color) WithAlpha(a uint8) Color {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equals returns true if the color equals another.
|
||||||
|
func (c Color) Equals(other Color) bool {
|
||||||
|
return c.R == other.R &&
|
||||||
|
c.G == other.G &&
|
||||||
|
c.B == other.B &&
|
||||||
|
c.A == other.A
|
||||||
|
}
|
||||||
|
|
||||||
|
// AverageWith averages two colors.
|
||||||
|
func (c Color) AverageWith(other Color) Color {
|
||||||
|
return Color{
|
||||||
|
R: (c.R + other.R) >> 1,
|
||||||
|
G: (c.G + other.G) >> 1,
|
||||||
|
B: (c.B + other.B) >> 1,
|
||||||
|
A: c.A,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// String returns a css string representation of the color.
|
// String returns a css string representation of the color.
|
||||||
func (c Color) String() string {
|
func (c Color) String() string {
|
||||||
fa := float64(c.A) / float64(255)
|
fa := float64(c.A) / float64(255)
|
||||||
|
|
|
@ -3,6 +3,8 @@ package drawing
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"image/color"
|
||||||
|
|
||||||
"github.com/blendlabs/go-assert"
|
"github.com/blendlabs/go-assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,3 +41,13 @@ func TestColorFromHex(t *testing.T) {
|
||||||
shortBlue := ColorFromHex("00F")
|
shortBlue := ColorFromHex("00F")
|
||||||
assert.Equal(ColorBlue, shortBlue)
|
assert.Equal(ColorBlue, shortBlue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestColorFromAlphaMixedRGBA(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
black := ColorFromAlphaMixedRGBA(color.Black.RGBA())
|
||||||
|
assert.True(black.Equals(ColorBlack), black.String())
|
||||||
|
|
||||||
|
white := ColorFromAlphaMixedRGBA(color.White.RGBA())
|
||||||
|
assert.True(white.Equals(ColorWhite), white.String())
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package drawing
|
package drawing
|
||||||
|
|
||||||
import (
|
import "math"
|
||||||
"math"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
|
// CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
|
||||||
|
@ -98,31 +96,60 @@ func SubdivideQuad(c, c1, c2 []float64) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func traceWindowIndices(i int) (startAt, endAt int) {
|
||||||
|
startAt = i * 6
|
||||||
|
endAt = startAt + 6
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceCalcDeltas(c []float64) (dx, dy, d float64) {
|
||||||
|
dx = c[4] - c[0]
|
||||||
|
dy = c[5] - c[1]
|
||||||
|
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceIsFlat(dx, dy, d, threshold float64) bool {
|
||||||
|
return (d * d) < threshold*(dx*dx+dy*dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func traceGetWindow(curves []float64, i int) []float64 {
|
||||||
|
startAt, endAt := traceWindowIndices(i)
|
||||||
|
return curves[startAt:endAt]
|
||||||
|
}
|
||||||
|
|
||||||
// TraceQuad generate lines subdividing the curve using a Liner
|
// TraceQuad generate lines subdividing the curve using a Liner
|
||||||
// flattening_threshold helps determines the flattening expectation of the curve
|
// flattening_threshold helps determines the flattening expectation of the curve
|
||||||
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
|
||||||
|
const curveLen = CurveRecursionLimit * 6
|
||||||
|
const curveEndIndex = curveLen - 1
|
||||||
|
const lastIteration = CurveRecursionLimit - 1
|
||||||
|
|
||||||
// Allocates curves stack
|
// Allocates curves stack
|
||||||
var curves [CurveRecursionLimit * 6]float64
|
curves := make([]float64, curveLen)
|
||||||
|
|
||||||
|
// copy 6 elements from the quad path to the stack
|
||||||
copy(curves[0:6], quad[0:6])
|
copy(curves[0:6], quad[0:6])
|
||||||
i := 0
|
|
||||||
// current curve
|
var i int
|
||||||
var c []float64
|
var c []float64
|
||||||
var dx, dy, d float64
|
var dx, dy, d float64
|
||||||
|
|
||||||
for i >= 0 {
|
for i >= 0 {
|
||||||
c = curves[i*6:]
|
c = traceGetWindow(curves, i)
|
||||||
dx = c[4] - c[0]
|
dx, dy, d = traceCalcDeltas(c)
|
||||||
dy = c[5] - c[1]
|
|
||||||
|
|
||||||
d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
|
// bail early if the distance is 0
|
||||||
|
if d == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// if it's flat then trace a line
|
// if it's flat then trace a line
|
||||||
if (d*d) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
|
if traceIsFlat(dx, dy, d, flatteningThreshold) || i == lastIteration {
|
||||||
t.LineTo(c[4], c[5])
|
t.LineTo(c[4], c[5])
|
||||||
i--
|
i--
|
||||||
} else {
|
} else {
|
||||||
// second half of bezier go lower onto the stack
|
SubdivideQuad(c, traceGetWindow(curves, i+1), traceGetWindow(curves, i))
|
||||||
SubdivideQuad(c, curves[(i+1)*6:], curves[i*6:])
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
35
drawing/curve_test.go
Normal file
35
drawing/curve_test.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package drawing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type point struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockLine struct {
|
||||||
|
inner []point
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml *mockLine) LineTo(x, y float64) {
|
||||||
|
ml.inner = append(ml.inner, point{x, y})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ml mockLine) Len() int {
|
||||||
|
return len(ml.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTraceQuad(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
// Quad
|
||||||
|
// x1, y1, cpx1, cpy2, x2, y2 float64
|
||||||
|
// do the 9->12 circle segment
|
||||||
|
quad := []float64{10, 20, 20, 20, 20, 10}
|
||||||
|
liner := &mockLine{}
|
||||||
|
TraceQuad(liner, quad, 0.5)
|
||||||
|
assert.NotZero(liner.Len())
|
||||||
|
}
|
|
@ -23,10 +23,10 @@ type Flattener interface {
|
||||||
// Flatten convert curves into straight segments keeping join segments info
|
// Flatten convert curves into straight segments keeping join segments info
|
||||||
func Flatten(path *Path, flattener Flattener, scale float64) {
|
func Flatten(path *Path, flattener Flattener, scale float64) {
|
||||||
// First Point
|
// First Point
|
||||||
var startX, startY float64 = 0, 0
|
var startX, startY float64
|
||||||
// Current Point
|
// Current Point
|
||||||
var x, y float64 = 0, 0
|
var x, y float64
|
||||||
i := 0
|
var i int
|
||||||
for _, cmp := range path.Components {
|
for _, cmp := range path.Components {
|
||||||
switch cmp {
|
switch cmp {
|
||||||
case MoveToComponent:
|
case MoveToComponent:
|
||||||
|
@ -43,6 +43,7 @@ func Flatten(path *Path, flattener Flattener, scale float64) {
|
||||||
flattener.LineJoin()
|
flattener.LineJoin()
|
||||||
i += 2
|
i += 2
|
||||||
case QuadCurveToComponent:
|
case QuadCurveToComponent:
|
||||||
|
// we include the previous point for the start of the curve
|
||||||
TraceQuad(flattener, path.Points[i-2:], 0.5)
|
TraceQuad(flattener, path.Points[i-2:], 0.5)
|
||||||
x, y = path.Points[i+2], path.Points[i+3]
|
x, y = path.Points[i+2], path.Points[i+3]
|
||||||
flattener.LineTo(x, y)
|
flattener.LineTo(x, y)
|
||||||
|
|
|
@ -49,7 +49,9 @@ func (lrs LinearRegressionSeries) GetWindow() int {
|
||||||
|
|
||||||
// GetEndIndex returns the effective window end.
|
// GetEndIndex returns the effective window end.
|
||||||
func (lrs LinearRegressionSeries) GetEndIndex() int {
|
func (lrs LinearRegressionSeries) GetEndIndex() int {
|
||||||
return Math.MinInt(lrs.GetOffset()+(lrs.Len()), (lrs.InnerSeries.Len() - 1))
|
offset := lrs.GetOffset() + lrs.Len()
|
||||||
|
innerSeriesLastIndex := lrs.InnerSeries.Len() - 1
|
||||||
|
return Math.MinInt(offset, innerSeriesLastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOffset returns the data offset.
|
// GetOffset returns the data offset.
|
||||||
|
@ -62,7 +64,7 @@ func (lrs LinearRegressionSeries) GetOffset() int {
|
||||||
|
|
||||||
// GetValue gets a value at a given index.
|
// GetValue gets a value at a given index.
|
||||||
func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) {
|
func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) {
|
||||||
if lrs.InnerSeries == nil {
|
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if lrs.m == 0 && lrs.b == 0 {
|
if lrs.m == 0 && lrs.b == 0 {
|
||||||
|
@ -78,7 +80,7 @@ func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) {
|
||||||
// GetLastValue computes the last moving average value but walking back window size samples,
|
// GetLastValue computes the last moving average value but walking back window size samples,
|
||||||
// and recomputing the last moving average chunk.
|
// and recomputing the last moving average chunk.
|
||||||
func (lrs *LinearRegressionSeries) GetLastValue() (x, y float64) {
|
func (lrs *LinearRegressionSeries) GetLastValue() (x, y float64) {
|
||||||
if lrs.InnerSeries == nil {
|
if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if lrs.m == 0 && lrs.b == 0 {
|
if lrs.m == 0 && lrs.b == 0 {
|
||||||
|
|
|
@ -27,6 +27,24 @@ type MACDSeries struct {
|
||||||
macdl *MACDLineSeries
|
macdl *MACDLineSeries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (macd MACDSeries) Validate() error {
|
||||||
|
var err error
|
||||||
|
if macd.signal != nil {
|
||||||
|
err = macd.signal.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if macd.macdl != nil {
|
||||||
|
err = macd.macdl.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetPeriods returns the primary and secondary periods.
|
// GetPeriods returns the primary and secondary periods.
|
||||||
func (macd MACDSeries) GetPeriods() (w1, w2, sig int) {
|
func (macd MACDSeries) GetPeriods() (w1, w2, sig int) {
|
||||||
if macd.PrimaryPeriod == 0 {
|
if macd.PrimaryPeriod == 0 {
|
||||||
|
@ -121,6 +139,14 @@ type MACDSignalSeries struct {
|
||||||
signal *EMASeries
|
signal *EMASeries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (macds MACDSignalSeries) Validate() error {
|
||||||
|
if macds.signal != nil {
|
||||||
|
return macds.signal.Validate()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetPeriods returns the primary and secondary periods.
|
// GetPeriods returns the primary and secondary periods.
|
||||||
func (macds MACDSignalSeries) GetPeriods() (w1, w2, sig int) {
|
func (macds MACDSignalSeries) GetPeriods() (w1, w2, sig int) {
|
||||||
if macds.PrimaryPeriod == 0 {
|
if macds.PrimaryPeriod == 0 {
|
||||||
|
@ -214,6 +240,27 @@ type MACDLineSeries struct {
|
||||||
Sigma float64
|
Sigma float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (macdl MACDLineSeries) Validate() error {
|
||||||
|
var err error
|
||||||
|
if macdl.ema1 != nil {
|
||||||
|
err = macdl.ema1.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if macdl.ema2 != nil {
|
||||||
|
err = macdl.ema2.Validate()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if macdl.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("MACDLineSeries: must provide an inner series")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetName returns the name of the time series.
|
// GetName returns the name of the time series.
|
||||||
func (macdl MACDLineSeries) GetName() string {
|
func (macdl MACDLineSeries) GetName() string {
|
||||||
return macdl.Name
|
return macdl.Name
|
||||||
|
@ -289,11 +336,3 @@ func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Ra
|
||||||
style := macdl.Style.InheritFrom(defaults)
|
style := macdl.Style.InheritFrom(defaults)
|
||||||
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macdl)
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macdl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the series.
|
|
||||||
func (macdl *MACDLineSeries) Validate() error {
|
|
||||||
if macdl.InnerSeries == nil {
|
|
||||||
return fmt.Errorf("macd line series requires InnerSeries to be set")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -116,17 +116,16 @@ func (rr *rasterRenderer) FillStroke() {
|
||||||
rr.gc.FillStroke()
|
rr.gc.FillStroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Circle implements the interface method.
|
// Circle fully draws a circle at a given point but does not apply the fill or stroke.
|
||||||
func (rr *rasterRenderer) Circle(radius float64, x, y int) {
|
func (rr *rasterRenderer) Circle(radius float64, x, y int) {
|
||||||
xf := float64(x)
|
xf := float64(x)
|
||||||
yf := float64(y)
|
yf := float64(y)
|
||||||
rr.gc.MoveTo(xf-radius, yf) //9
|
|
||||||
rr.gc.QuadCurveTo(xf, yf, xf, yf-radius) //12
|
rr.gc.MoveTo(xf-radius, yf) //9
|
||||||
rr.gc.QuadCurveTo(xf, yf, xf+radius, yf) //3
|
rr.gc.QuadCurveTo(xf-radius, yf-radius, xf, yf-radius) //12
|
||||||
rr.gc.QuadCurveTo(xf, yf, xf, yf+radius) //6
|
rr.gc.QuadCurveTo(xf+radius, yf-radius, xf+radius, yf) //3
|
||||||
rr.gc.QuadCurveTo(xf, yf, xf-radius, yf) //9
|
rr.gc.QuadCurveTo(xf+radius, yf+radius, xf, yf+radius) //6
|
||||||
rr.gc.Close()
|
rr.gc.QuadCurveTo(xf-radius, yf+radius, xf-radius, yf) //9
|
||||||
rr.gc.FillStroke()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFont implements the interface method.
|
// SetFont implements the interface method.
|
||||||
|
|
14
sequence.go
14
sequence.go
|
@ -8,10 +8,14 @@ import (
|
||||||
var (
|
var (
|
||||||
// Sequence contains some sequence utilities.
|
// Sequence contains some sequence utilities.
|
||||||
// These utilities can be useful for generating test data.
|
// These utilities can be useful for generating test data.
|
||||||
Sequence = &sequence{}
|
Sequence = &sequence{
|
||||||
|
rnd: rand.New(rand.NewSource(time.Now().Unix())),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type sequence struct{}
|
type sequence struct {
|
||||||
|
rnd *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
// Float64 produces an array of floats from [start,end] by optional steps.
|
// Float64 produces an array of floats from [start,end] by optional steps.
|
||||||
func (s sequence) Float64(start, end float64, steps ...float64) []float64 {
|
func (s sequence) Float64(start, end float64, steps ...float64) []float64 {
|
||||||
|
@ -35,11 +39,10 @@ func (s sequence) Float64(start, end float64, steps ...float64) []float64 {
|
||||||
|
|
||||||
// Random generates a fixed length sequence of random values between (0, scale).
|
// Random generates a fixed length sequence of random values between (0, scale).
|
||||||
func (s sequence) Random(samples int, scale float64) []float64 {
|
func (s sequence) Random(samples int, scale float64) []float64 {
|
||||||
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
|
||||||
values := make([]float64, samples)
|
values := make([]float64, samples)
|
||||||
|
|
||||||
for x := 0; x < samples; x++ {
|
for x := 0; x < samples; x++ {
|
||||||
values[x] = rnd.Float64() * scale
|
values[x] = s.rnd.Float64() * scale
|
||||||
}
|
}
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
@ -47,11 +50,10 @@ func (s sequence) Random(samples int, scale float64) []float64 {
|
||||||
|
|
||||||
// Random generates a fixed length sequence of random values with a given average, above and below that average by (-scale, scale)
|
// Random generates a fixed length sequence of random values with a given average, above and below that average by (-scale, scale)
|
||||||
func (s sequence) RandomWithAverage(samples int, average, scale float64) []float64 {
|
func (s sequence) RandomWithAverage(samples int, average, scale float64) []float64 {
|
||||||
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
|
||||||
values := make([]float64, samples)
|
values := make([]float64, samples)
|
||||||
|
|
||||||
for x := 0; x < samples; x++ {
|
for x := 0; x < samples; x++ {
|
||||||
jitter := scale - (rnd.Float64() * (2 * scale))
|
jitter := scale - (s.rnd.Float64() * (2 * scale))
|
||||||
values[x] = average + jitter
|
values[x] = average + jitter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ func (sma SMASeries) GetPeriod(defaults ...int) int {
|
||||||
|
|
||||||
// GetValue gets a value at a given index.
|
// GetValue gets a value at a given index.
|
||||||
func (sma SMASeries) GetValue(index int) (x, y float64) {
|
func (sma SMASeries) GetValue(index int) (x, y float64) {
|
||||||
if sma.InnerSeries == nil {
|
if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
px, _ := sma.InnerSeries.GetValue(index)
|
px, _ := sma.InnerSeries.GetValue(index)
|
||||||
|
@ -62,7 +62,7 @@ func (sma SMASeries) GetValue(index int) (x, y float64) {
|
||||||
// GetLastValue computes the last moving average value but walking back window size samples,
|
// GetLastValue computes the last moving average value but walking back window size samples,
|
||||||
// and recomputing the last moving average chunk.
|
// and recomputing the last moving average chunk.
|
||||||
func (sma SMASeries) GetLastValue() (x, y float64) {
|
func (sma SMASeries) GetLastValue() (x, y float64) {
|
||||||
if sma.InnerSeries == nil {
|
if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
seriesLen := sma.InnerSeries.Len()
|
seriesLen := sma.InnerSeries.Len()
|
||||||
|
|
83
style.go
83
style.go
|
@ -8,6 +8,12 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/drawing"
|
"github.com/wcharczuk/go-chart/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Disabled indicates if the value should be interpreted as set intentionally to zero.
|
||||||
|
// this is because golang optionals aren't here yet.
|
||||||
|
Disabled = -1
|
||||||
|
)
|
||||||
|
|
||||||
// StyleShow is a prebuilt style with the `Show` property set to true.
|
// StyleShow is a prebuilt style with the `Show` property set to true.
|
||||||
func StyleShow() Style {
|
func StyleShow() Style {
|
||||||
return Style{
|
return Style{
|
||||||
|
@ -24,7 +30,11 @@ type Style struct {
|
||||||
StrokeColor drawing.Color
|
StrokeColor drawing.Color
|
||||||
StrokeDashArray []float64
|
StrokeDashArray []float64
|
||||||
|
|
||||||
|
DotColor drawing.Color
|
||||||
|
DotWidth float64
|
||||||
|
|
||||||
FillColor drawing.Color
|
FillColor drawing.Color
|
||||||
|
|
||||||
FontSize float64
|
FontSize float64
|
||||||
FontColor drawing.Color
|
FontColor drawing.Color
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
|
@ -38,7 +48,14 @@ type Style struct {
|
||||||
|
|
||||||
// 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 s.StrokeColor.IsZero() && s.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontColor.IsZero() && s.FontSize == 0 && s.Font == nil
|
return s.StrokeColor.IsZero() &&
|
||||||
|
s.StrokeWidth == 0 &&
|
||||||
|
s.DotColor.IsZero() &&
|
||||||
|
s.DotWidth == 0 &&
|
||||||
|
s.FillColor.IsZero() &&
|
||||||
|
s.FontColor.IsZero() &&
|
||||||
|
s.FontSize == 0 &&
|
||||||
|
s.Font == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a text representation of the style.
|
// String returns a text representation of the style.
|
||||||
|
@ -83,6 +100,18 @@ func (s Style) String() string {
|
||||||
output = append(output, "\"stroke_dash_array\": null")
|
output = append(output, "\"stroke_dash_array\": null")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.DotWidth >= 0 {
|
||||||
|
output = append(output, fmt.Sprintf("\"dot_width\": %0.2f", s.DotWidth))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"dot_width\": null")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.DotColor.IsZero() {
|
||||||
|
output = append(output, fmt.Sprintf("\"dot_color\": %s", s.DotColor.String()))
|
||||||
|
} else {
|
||||||
|
output = append(output, "\"dot_color\": null")
|
||||||
|
}
|
||||||
|
|
||||||
if !s.FillColor.IsZero() {
|
if !s.FillColor.IsZero() {
|
||||||
output = append(output, fmt.Sprintf("\"fill_color\": %s", s.FillColor.String()))
|
output = append(output, fmt.Sprintf("\"fill_color\": %s", s.FillColor.String()))
|
||||||
} else {
|
} else {
|
||||||
|
@ -132,6 +161,17 @@ func (s Style) GetFillColor(defaults ...drawing.Color) drawing.Color {
|
||||||
return s.FillColor
|
return s.FillColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDotColor returns the stroke color.
|
||||||
|
func (s Style) GetDotColor(defaults ...drawing.Color) drawing.Color {
|
||||||
|
if s.DotColor.IsZero() {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return drawing.ColorTransparent
|
||||||
|
}
|
||||||
|
return s.DotColor
|
||||||
|
}
|
||||||
|
|
||||||
// GetStrokeWidth returns the stroke width.
|
// GetStrokeWidth returns the stroke width.
|
||||||
func (s Style) GetStrokeWidth(defaults ...float64) float64 {
|
func (s Style) GetStrokeWidth(defaults ...float64) float64 {
|
||||||
if s.StrokeWidth == 0 {
|
if s.StrokeWidth == 0 {
|
||||||
|
@ -143,6 +183,17 @@ func (s Style) GetStrokeWidth(defaults ...float64) float64 {
|
||||||
return s.StrokeWidth
|
return s.StrokeWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDotWidth returns the dot width for scatter plots.
|
||||||
|
func (s Style) GetDotWidth(defaults ...float64) float64 {
|
||||||
|
if s.DotWidth == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDotWidth
|
||||||
|
}
|
||||||
|
return s.DotWidth
|
||||||
|
}
|
||||||
|
|
||||||
// GetStrokeDashArray returns the stroke dash array.
|
// GetStrokeDashArray returns the stroke dash array.
|
||||||
func (s Style) GetStrokeDashArray(defaults ...[]float64) []float64 {
|
func (s Style) GetStrokeDashArray(defaults ...[]float64) []float64 {
|
||||||
if len(s.StrokeDashArray) == 0 {
|
if len(s.StrokeDashArray) == 0 {
|
||||||
|
@ -288,6 +339,10 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
|
||||||
final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
|
final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
|
||||||
final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth)
|
final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth)
|
||||||
final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray)
|
final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray)
|
||||||
|
|
||||||
|
final.DotColor = s.GetDotColor(defaults.DotColor)
|
||||||
|
final.DotWidth = s.GetDotWidth(defaults.DotWidth)
|
||||||
|
|
||||||
final.FillColor = s.GetFillColor(defaults.FillColor)
|
final.FillColor = s.GetFillColor(defaults.FillColor)
|
||||||
final.FontColor = s.GetFontColor(defaults.FontColor)
|
final.FontColor = s.GetFontColor(defaults.FontColor)
|
||||||
final.FontSize = s.GetFontSize(defaults.FontSize)
|
final.FontSize = s.GetFontSize(defaults.FontSize)
|
||||||
|
@ -298,6 +353,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
|
||||||
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
|
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
|
||||||
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
|
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
|
||||||
final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees)
|
final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,6 +373,16 @@ func (s Style) GetFillOptions() Style {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDotOptions returns the dot components.
|
||||||
|
func (s Style) GetDotOptions() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeDashArray: nil,
|
||||||
|
FillColor: s.DotColor,
|
||||||
|
StrokeColor: s.DotColor,
|
||||||
|
StrokeWidth: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetFillAndStrokeOptions returns the fill and stroke components.
|
// GetFillAndStrokeOptions returns the fill and stroke components.
|
||||||
func (s Style) GetFillAndStrokeOptions() Style {
|
func (s Style) GetFillAndStrokeOptions() Style {
|
||||||
return Style{
|
return Style{
|
||||||
|
@ -340,3 +406,18 @@ func (s Style) GetTextOptions() Style {
|
||||||
TextRotationDegrees: s.TextRotationDegrees,
|
TextRotationDegrees: s.TextRotationDegrees,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldDrawStroke tells drawing functions if they should draw the stroke.
|
||||||
|
func (s Style) ShouldDrawStroke() bool {
|
||||||
|
return !s.StrokeColor.IsZero() && s.StrokeWidth > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldDrawDot tells drawing functions if they should draw the dot.
|
||||||
|
func (s Style) ShouldDrawDot() bool {
|
||||||
|
return !s.DotColor.IsZero() && s.DotWidth > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldDrawFill tells drawing functions if they should draw the stroke.
|
||||||
|
func (s Style) ShouldDrawFill() bool {
|
||||||
|
return !s.FillColor.IsZero()
|
||||||
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ func TestXAxisMeasure(t *testing.T) {
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
||||||
xa := XAxis{}
|
xa := XAxis{}
|
||||||
xab := xa.Measure(r, Box{0, 0, 100, 100}, &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
xab := xa.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
||||||
assert.Equal(122, xab.Width())
|
assert.Equal(122, xab.Width())
|
||||||
assert.Equal(21, xab.Height())
|
assert.Equal(21, xab.Height())
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ func TestYAxisMeasure(t *testing.T) {
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
||||||
ya := YAxis{}
|
ya := YAxis{}
|
||||||
yab := ya.Measure(r, Box{0, 0, 100, 100}, &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
||||||
assert.Equal(32, yab.Width())
|
assert.Equal(32, yab.Width())
|
||||||
assert.Equal(110, yab.Height())
|
assert.Equal(110, yab.Height())
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ func TestYAxisSecondaryMeasure(t *testing.T) {
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
||||||
ya := YAxis{AxisType: YAxisSecondary}
|
ya := YAxis{AxisType: YAxisSecondary}
|
||||||
yab := ya.Measure(r, Box{0, 0, 100, 100}, &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
||||||
assert.Equal(32, yab.Width())
|
assert.Equal(32, yab.Width())
|
||||||
assert.Equal(110, yab.Height())
|
assert.Equal(110, yab.Height())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user