commit
1af266dc7e
82 changed files with 3969 additions and 1339 deletions
|
@ -6,8 +6,8 @@ go:
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- go build -i ./...
|
- go get -u github.com/blendlabs/go-assert
|
||||||
|
- go get ./...
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- go test
|
- go test ./...
|
||||||
- go test ./drawing/
|
|
||||||
|
|
9
Makefile
Normal file
9
Makefile
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
all: test
|
||||||
|
|
||||||
|
test:
|
||||||
|
@go test ./...
|
||||||
|
|
||||||
|
cover:
|
||||||
|
@go test -short -covermode=set -coverprofile=profile.cov
|
||||||
|
@go tool cover -html=profile.cov
|
||||||
|
@rm profile.cov
|
14
README.md
14
README.md
|
@ -31,13 +31,19 @@ Two axis:
|
||||||
|
|
||||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/two_axis.png)
|
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/two_axis.png)
|
||||||
|
|
||||||
Simple Moving Average:
|
# Other Chart Types
|
||||||
|
|
||||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/ma_goog_ltm.png)
|
Pie Chart:
|
||||||
|
|
||||||
Bollinger Bounds:
|
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/pie_chart.png)
|
||||||
|
|
||||||
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/spy_ltm_bbs.png)
|
The code for this chart can be found in `examples/pie_chart/main.go`.
|
||||||
|
|
||||||
|
Stacked Bar:
|
||||||
|
|
||||||
|
![](https://raw.githubusercontent.com/wcharczuk/go-chart/master/images/stacked_bar.png)
|
||||||
|
|
||||||
|
The code for this chart can be found in `examples/stacked_bar/main.go`.
|
||||||
|
|
||||||
# Code Examples
|
# Code Examples
|
||||||
|
|
||||||
|
|
|
@ -2,18 +2,12 @@ package chart
|
||||||
|
|
||||||
import "math"
|
import "math"
|
||||||
|
|
||||||
// Annotation is a label on the chart.
|
|
||||||
type Annotation struct {
|
|
||||||
X, Y float64
|
|
||||||
Label string
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnnotationSeries is a series of labels on the chart.
|
// AnnotationSeries is a series of labels on the chart.
|
||||||
type AnnotationSeries struct {
|
type AnnotationSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
Annotations []Annotation
|
Annotations []Value2
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetName returns the name of the time series.
|
// GetName returns the name of the time series.
|
||||||
|
@ -27,10 +21,21 @@ func (as AnnotationSeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (as AnnotationSeries) GetYAxis() YAxisType {
|
func (as AnnotationSeries) GetYAxis() yAxisType {
|
||||||
return as.YAxis
|
return as.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as AnnotationSeries) annotationStyleDefaults(defaults Style) Style {
|
||||||
|
return Style{
|
||||||
|
Font: defaults.Font,
|
||||||
|
FillColor: DefaultAnnotationFillColor,
|
||||||
|
FontSize: DefaultAnnotationFontSize,
|
||||||
|
StrokeColor: defaults.StrokeColor,
|
||||||
|
StrokeWidth: defaults.StrokeWidth,
|
||||||
|
Padding: DefaultAnnotationPadding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Measure returns a bounds box of the series.
|
// Measure returns a bounds box of the series.
|
||||||
func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
|
func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
|
||||||
box := Box{
|
box := Box{
|
||||||
|
@ -40,22 +45,16 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
|
||||||
Bottom: 0,
|
Bottom: 0,
|
||||||
}
|
}
|
||||||
if as.Style.IsZero() || as.Style.Show {
|
if as.Style.IsZero() || as.Style.Show {
|
||||||
style := as.Style.WithDefaultsFrom(Style{
|
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
|
||||||
Font: defaults.Font,
|
|
||||||
FillColor: DefaultAnnotationFillColor,
|
|
||||||
FontSize: DefaultAnnotationFontSize,
|
|
||||||
StrokeColor: defaults.StrokeColor,
|
|
||||||
StrokeWidth: defaults.StrokeWidth,
|
|
||||||
Padding: DefaultAnnotationPadding,
|
|
||||||
})
|
|
||||||
for _, a := range as.Annotations {
|
for _, a := range as.Annotations {
|
||||||
lx := canvasBox.Left + xrange.Translate(a.X)
|
style := a.Style.InheritFrom(seriesStyle)
|
||||||
ly := canvasBox.Bottom - yrange.Translate(a.Y)
|
lx := canvasBox.Left + xrange.Translate(a.XValue)
|
||||||
ab := MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
|
||||||
box.Top = MinInt(box.Top, ab.Top)
|
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
||||||
box.Left = MinInt(box.Left, ab.Left)
|
box.Top = Math.MinInt(box.Top, ab.Top)
|
||||||
box.Right = MaxInt(box.Right, ab.Right)
|
box.Left = Math.MinInt(box.Left, ab.Left)
|
||||||
box.Bottom = MaxInt(box.Bottom, ab.Bottom)
|
box.Right = Math.MaxInt(box.Right, ab.Right)
|
||||||
|
box.Bottom = Math.MaxInt(box.Bottom, ab.Bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return box
|
return box
|
||||||
|
@ -64,19 +63,12 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
|
||||||
// Render draws the series.
|
// Render draws the series.
|
||||||
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
if as.Style.IsZero() || as.Style.Show {
|
if as.Style.IsZero() || as.Style.Show {
|
||||||
style := as.Style.WithDefaultsFrom(Style{
|
seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
|
||||||
Font: defaults.Font,
|
|
||||||
FontColor: DefaultTextColor,
|
|
||||||
FillColor: DefaultAnnotationFillColor,
|
|
||||||
FontSize: DefaultAnnotationFontSize,
|
|
||||||
StrokeColor: defaults.StrokeColor,
|
|
||||||
StrokeWidth: defaults.StrokeWidth,
|
|
||||||
Padding: DefaultAnnotationPadding,
|
|
||||||
})
|
|
||||||
for _, a := range as.Annotations {
|
for _, a := range as.Annotations {
|
||||||
lx := canvasBox.Left + xrange.Translate(a.X)
|
style := a.Style.InheritFrom(seriesStyle)
|
||||||
ly := canvasBox.Bottom - yrange.Translate(a.Y)
|
lx := canvasBox.Left + xrange.Translate(a.XValue)
|
||||||
DrawAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
|
||||||
|
Draw.Annotation(r, canvasBox, style, lx, ly, a.Label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,11 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
|
||||||
Style: Style{
|
Style: Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
},
|
},
|
||||||
Annotations: []Annotation{
|
Annotations: []Value2{
|
||||||
{X: 1.0, Y: 1.0, Label: "1.0"},
|
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||||
{X: 2.0, Y: 2.0, Label: "2.0"},
|
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||||
{X: 3.0, Y: 3.0, Label: "3.0"},
|
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||||
{X: 4.0, Y: 4.0, Label: "4.0"},
|
{XValue: 4.0, YValue: 4.0, Label: "4.0"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,12 +29,12 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
|
||||||
f, err := GetDefaultFont()
|
f, err := GetDefaultFont()
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
|
|
||||||
xrange := Range{
|
xrange := &ContinuousRange{
|
||||||
Min: 1.0,
|
Min: 1.0,
|
||||||
Max: 4.0,
|
Max: 4.0,
|
||||||
Domain: 100,
|
Domain: 100,
|
||||||
}
|
}
|
||||||
yrange := Range{
|
yrange := &ContinuousRange{
|
||||||
Min: 1.0,
|
Min: 1.0,
|
||||||
Max: 4.0,
|
Max: 4.0,
|
||||||
Domain: 100,
|
Domain: 100,
|
||||||
|
@ -68,11 +68,11 @@ func TestAnnotationSeriesRender(t *testing.T) {
|
||||||
FillColor: drawing.ColorWhite,
|
FillColor: drawing.ColorWhite,
|
||||||
StrokeColor: drawing.ColorBlack,
|
StrokeColor: drawing.ColorBlack,
|
||||||
},
|
},
|
||||||
Annotations: []Annotation{
|
Annotations: []Value2{
|
||||||
{X: 1.0, Y: 1.0, Label: "1.0"},
|
{XValue: 1.0, YValue: 1.0, Label: "1.0"},
|
||||||
{X: 2.0, Y: 2.0, Label: "2.0"},
|
{XValue: 2.0, YValue: 2.0, Label: "2.0"},
|
||||||
{X: 3.0, Y: 3.0, Label: "3.0"},
|
{XValue: 3.0, YValue: 3.0, Label: "3.0"},
|
||||||
{X: 4.0, Y: 4.0, Label: "4.0"},
|
{XValue: 4.0, YValue: 4.0, Label: "4.0"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,12 +82,12 @@ func TestAnnotationSeriesRender(t *testing.T) {
|
||||||
f, err := GetDefaultFont()
|
f, err := GetDefaultFont()
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
|
|
||||||
xrange := Range{
|
xrange := &ContinuousRange{
|
||||||
Min: 1.0,
|
Min: 1.0,
|
||||||
Max: 4.0,
|
Max: 4.0,
|
||||||
Domain: 100,
|
Domain: 100,
|
||||||
}
|
}
|
||||||
yrange := Range{
|
yrange := &ContinuousRange{
|
||||||
Min: 1.0,
|
Min: 1.0,
|
||||||
Max: 4.0,
|
Max: 4.0,
|
||||||
Domain: 100,
|
Domain: 100,
|
||||||
|
|
31
axis.go
31
axis.go
|
@ -1,20 +1,41 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
|
type tickPosition int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TickPositionUnset means to use the default tick position.
|
||||||
|
TickPositionUnset tickPosition = 0
|
||||||
|
// TickPositionBetweenTicks draws the labels for a tick between the previous and current tick.
|
||||||
|
TickPositionBetweenTicks tickPosition = 1
|
||||||
|
// TickPositionUnderTick draws the tick below the tick.
|
||||||
|
TickPositionUnderTick tickPosition = 2
|
||||||
|
)
|
||||||
|
|
||||||
// YAxisType is a type of y-axis; it can either be primary or secondary.
|
// YAxisType is a type of y-axis; it can either be primary or secondary.
|
||||||
type YAxisType int
|
type yAxisType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// YAxisPrimary is the primary axis.
|
// YAxisPrimary is the primary axis.
|
||||||
YAxisPrimary YAxisType = 0
|
YAxisPrimary yAxisType = 0
|
||||||
// YAxisSecondary is the secondary axis.
|
// YAxisSecondary is the secondary axis.
|
||||||
YAxisSecondary YAxisType = 1
|
YAxisSecondary yAxisType = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Axis is a chart feature detailing what values happen where.
|
// Axis is a chart feature detailing what values happen where.
|
||||||
type Axis interface {
|
type Axis interface {
|
||||||
GetName() string
|
GetName() string
|
||||||
GetStyle() Style
|
GetStyle() Style
|
||||||
GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick
|
|
||||||
|
GetTicks() []Tick
|
||||||
|
GenerateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick
|
||||||
|
|
||||||
|
// GetGridLines returns the gridlines for the axis.
|
||||||
GetGridLines(ticks []Tick) []GridLine
|
GetGridLines(ticks []Tick) []GridLine
|
||||||
Render(c *Chart, r Renderer, canvasBox Box, ra Range, ticks []Tick)
|
|
||||||
|
// Measure should return an absolute box for the axis.
|
||||||
|
// This is used when auto-fitting the canvas to the background.
|
||||||
|
Measure(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick) Box
|
||||||
|
|
||||||
|
// Render renders the axis.
|
||||||
|
Render(r Renderer, canvasBox Box, ra Range, style Style, ticks []Tick)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import "math"
|
||||||
type BollingerBandsSeries struct {
|
type BollingerBandsSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
|
|
||||||
Period int
|
Period int
|
||||||
K float64
|
K float64
|
||||||
|
@ -27,7 +27,7 @@ func (bbs BollingerBandsSeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (bbs BollingerBandsSeries) GetYAxis() YAxisType {
|
func (bbs BollingerBandsSeries) GetYAxis() yAxisType {
|
||||||
return bbs.YAxis
|
return bbs.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,13 +108,13 @@ func (bbs *BollingerBandsSeries) GetBoundedLastValue() (x, y1, y2 float64) {
|
||||||
|
|
||||||
// Render renders the series.
|
// Render renders the series.
|
||||||
func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
s := bbs.Style.WithDefaultsFrom(defaults.WithDefaultsFrom(Style{
|
s := bbs.Style.InheritFrom(defaults.InheritFrom(Style{
|
||||||
StrokeWidth: 1.0,
|
StrokeWidth: 1.0,
|
||||||
StrokeColor: DefaultAxisColor.WithAlpha(64),
|
StrokeColor: DefaultAxisColor.WithAlpha(64),
|
||||||
FillColor: DefaultAxisColor.WithAlpha(32),
|
FillColor: DefaultAxisColor.WithAlpha(32),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
DrawBoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
|
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 {
|
func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 {
|
||||||
|
|
|
@ -11,8 +11,8 @@ func TestBollingerBandSeries(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
s1 := mockValueProvider{
|
s1 := mockValueProvider{
|
||||||
X: Seq(1.0, 100.0),
|
X: Sequence.Float64(1.0, 100.0),
|
||||||
Y: SeqRand(100, 1024),
|
Y: Sequence.Random(100, 1024),
|
||||||
}
|
}
|
||||||
|
|
||||||
bbs := &BollingerBandsSeries{
|
bbs := &BollingerBandsSeries{
|
||||||
|
@ -36,8 +36,8 @@ func TestBollingerBandLastValue(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
s1 := mockValueProvider{
|
s1 := mockValueProvider{
|
||||||
X: Seq(1.0, 100.0),
|
X: Sequence.Float64(1.0, 100.0),
|
||||||
Y: Seq(1.0, 100.0),
|
Y: Sequence.Float64(1.0, 100.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
bbs := &BollingerBandsSeries{
|
bbs := &BollingerBandsSeries{
|
||||||
|
|
33
box.go
33
box.go
|
@ -66,12 +66,12 @@ func (b Box) GetBottom(defaults ...int) int {
|
||||||
|
|
||||||
// Width returns the width
|
// Width returns the width
|
||||||
func (b Box) Width() int {
|
func (b Box) Width() int {
|
||||||
return AbsInt(b.Right - b.Left)
|
return Math.AbsInt(b.Right - b.Left)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Height returns the height
|
// Height returns the height
|
||||||
func (b Box) Height() int {
|
func (b Box) Height() int {
|
||||||
return AbsInt(b.Bottom - b.Top)
|
return Math.AbsInt(b.Bottom - b.Top)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center returns the center of the box
|
// Center returns the center of the box
|
||||||
|
@ -122,10 +122,10 @@ func (b Box) Equals(other Box) bool {
|
||||||
// Grow grows a box based on another box.
|
// Grow grows a box based on another box.
|
||||||
func (b Box) Grow(other Box) Box {
|
func (b Box) Grow(other Box) Box {
|
||||||
return Box{
|
return Box{
|
||||||
Top: MinInt(b.Top, other.Top),
|
Top: Math.MinInt(b.Top, other.Top),
|
||||||
Left: MinInt(b.Left, other.Left),
|
Left: Math.MinInt(b.Left, other.Left),
|
||||||
Right: MaxInt(b.Right, other.Right),
|
Right: Math.MaxInt(b.Right, other.Right),
|
||||||
Bottom: MaxInt(b.Bottom, other.Bottom),
|
Bottom: Math.MaxInt(b.Bottom, other.Bottom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,25 +185,12 @@ func (b Box) Fit(other Box) Box {
|
||||||
// more literally like the opposite of grow.
|
// more literally like the opposite of grow.
|
||||||
func (b Box) Constrain(other Box) Box {
|
func (b Box) Constrain(other Box) Box {
|
||||||
newBox := b.Clone()
|
newBox := b.Clone()
|
||||||
if other.Top < b.Top {
|
|
||||||
delta := b.Top - other.Top
|
|
||||||
newBox.Top = other.Top + delta
|
|
||||||
}
|
|
||||||
|
|
||||||
if other.Left < b.Left {
|
newBox.Top = Math.MaxInt(newBox.Top, other.Top)
|
||||||
delta := b.Left - other.Left
|
newBox.Left = Math.MaxInt(newBox.Left, other.Left)
|
||||||
newBox.Left = other.Left + delta
|
newBox.Right = Math.MinInt(newBox.Right, other.Right)
|
||||||
}
|
newBox.Bottom = Math.MinInt(newBox.Bottom, other.Bottom)
|
||||||
|
|
||||||
if other.Right > b.Right {
|
|
||||||
delta := other.Right - b.Right
|
|
||||||
newBox.Right = other.Right - delta
|
|
||||||
}
|
|
||||||
|
|
||||||
if other.Bottom > b.Bottom {
|
|
||||||
delta := other.Bottom - b.Bottom
|
|
||||||
newBox.Bottom = other.Bottom - delta
|
|
||||||
}
|
|
||||||
return newBox
|
return newBox
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
box_test.go
20
box_test.go
|
@ -86,6 +86,26 @@ func TestBoxFit(t *testing.T) {
|
||||||
assert.True(math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
|
assert.True(math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBoxConstrain(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
|
||||||
|
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
|
||||||
|
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
|
||||||
|
|
||||||
|
cab := a.Constrain(b)
|
||||||
|
assert.Equal(64, cab.Top)
|
||||||
|
assert.Equal(64, cab.Left)
|
||||||
|
assert.Equal(192, cab.Right)
|
||||||
|
assert.Equal(170, cab.Bottom)
|
||||||
|
|
||||||
|
cac := a.Constrain(c)
|
||||||
|
assert.Equal(64, cac.Top)
|
||||||
|
assert.Equal(64, cac.Left)
|
||||||
|
assert.Equal(170, cac.Right)
|
||||||
|
assert.Equal(192, cac.Bottom)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBoxOuterConstrain(t *testing.T) {
|
func TestBoxOuterConstrain(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
|
108
chart.go
108
chart.go
|
@ -137,7 +137,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
||||||
var miny, maxy float64 = math.MaxFloat64, 0
|
var miny, maxy float64 = math.MaxFloat64, 0
|
||||||
var minya, maxya float64 = math.MaxFloat64, 0
|
var minya, maxya float64 = math.MaxFloat64, 0
|
||||||
|
|
||||||
hasSecondaryAxis := false
|
seriesMappedToSecondaryAxis := false
|
||||||
|
|
||||||
// note: a possible future optimization is to not scan the series values if
|
// note: a possible future optimization is to not scan the series values if
|
||||||
// all axis are represented by either custom ticks or custom ranges.
|
// all axis are represented by either custom ticks or custom ranges.
|
||||||
|
@ -162,7 +162,7 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
||||||
minya = math.Min(minya, vy2)
|
minya = math.Min(minya, vy2)
|
||||||
maxya = math.Max(maxya, vy1)
|
maxya = math.Max(maxya, vy1)
|
||||||
maxya = math.Max(maxya, vy2)
|
maxya = math.Max(maxya, vy2)
|
||||||
hasSecondaryAxis = true
|
seriesMappedToSecondaryAxis = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if vp, isValueProvider := s.(ValueProvider); isValueProvider {
|
} else if vp, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||||
|
@ -179,27 +179,42 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
||||||
} else if seriesAxis == YAxisSecondary {
|
} else if seriesAxis == YAxisSecondary {
|
||||||
minya = math.Min(minya, vy)
|
minya = math.Min(minya, vy)
|
||||||
maxya = math.Max(maxya, vy)
|
maxya = math.Max(maxya, vy)
|
||||||
hasSecondaryAxis = true
|
seriesMappedToSecondaryAxis = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.XAxis.Range == nil {
|
||||||
|
xrange = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
xrange = c.XAxis.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.YAxis.Range == nil {
|
||||||
|
yrange = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
yrange = c.YAxis.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.YAxisSecondary.Range == nil {
|
||||||
|
yrangeAlt = &ContinuousRange{}
|
||||||
|
} else {
|
||||||
|
yrangeAlt = c.YAxisSecondary.Range
|
||||||
|
}
|
||||||
|
|
||||||
if len(c.XAxis.Ticks) > 0 {
|
if len(c.XAxis.Ticks) > 0 {
|
||||||
tickMin, tickMax := math.MaxFloat64, 0.0
|
tickMin, tickMax := math.MaxFloat64, 0.0
|
||||||
for _, t := range c.XAxis.Ticks {
|
for _, t := range c.XAxis.Ticks {
|
||||||
tickMin = math.Min(tickMin, t.Value)
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
tickMax = math.Max(tickMax, t.Value)
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
}
|
}
|
||||||
xrange.Min = tickMin
|
xrange.SetMin(tickMin)
|
||||||
xrange.Max = tickMax
|
xrange.SetMax(tickMax)
|
||||||
} else if !c.XAxis.Range.IsZero() {
|
} else if xrange.IsZero() {
|
||||||
xrange.Min = c.XAxis.Range.Min
|
xrange.SetMin(minx)
|
||||||
xrange.Max = c.XAxis.Range.Max
|
xrange.SetMax(maxx)
|
||||||
} else {
|
|
||||||
xrange.Min = minx
|
|
||||||
xrange.Max = maxx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.YAxis.Ticks) > 0 {
|
if len(c.YAxis.Ticks) > 0 {
|
||||||
|
@ -208,15 +223,17 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
||||||
tickMin = math.Min(tickMin, t.Value)
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
tickMax = math.Max(tickMax, t.Value)
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
}
|
}
|
||||||
yrange.Min = tickMin
|
yrange.SetMin(tickMin)
|
||||||
yrange.Max = tickMax
|
yrange.SetMax(tickMax)
|
||||||
} else if !c.YAxis.Range.IsZero() {
|
} else if yrange.IsZero() {
|
||||||
yrange.Min = c.YAxis.Range.Min
|
yrange.SetMin(miny)
|
||||||
yrange.Max = c.YAxis.Range.Max
|
yrange.SetMax(maxy)
|
||||||
} else {
|
|
||||||
yrange.Min = miny
|
delta := yrange.GetDelta()
|
||||||
yrange.Max = maxy
|
roundTo := Math.GetRoundToForDelta(delta)
|
||||||
yrange.Min, yrange.Max = yrange.GetRoundedRangeBounds()
|
rmin, rmax := Math.RoundDown(yrange.GetMin(), roundTo), Math.RoundUp(yrange.GetMax(), roundTo)
|
||||||
|
yrange.SetMin(rmin)
|
||||||
|
yrange.SetMax(rmax)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(c.YAxisSecondary.Ticks) > 0 {
|
if len(c.YAxisSecondary.Ticks) > 0 {
|
||||||
|
@ -225,30 +242,31 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
||||||
tickMin = math.Min(tickMin, t.Value)
|
tickMin = math.Min(tickMin, t.Value)
|
||||||
tickMax = math.Max(tickMax, t.Value)
|
tickMax = math.Max(tickMax, t.Value)
|
||||||
}
|
}
|
||||||
yrangeAlt.Min = tickMin
|
yrangeAlt.SetMin(tickMin)
|
||||||
yrangeAlt.Max = tickMax
|
yrangeAlt.SetMax(tickMax)
|
||||||
} else if !c.YAxisSecondary.Range.IsZero() {
|
} else if seriesMappedToSecondaryAxis && yrangeAlt.IsZero() {
|
||||||
yrangeAlt.Min = c.YAxisSecondary.Range.Min
|
yrangeAlt.SetMin(minya)
|
||||||
yrangeAlt.Max = c.YAxisSecondary.Range.Max
|
yrangeAlt.SetMax(maxya)
|
||||||
} else if hasSecondaryAxis {
|
|
||||||
yrangeAlt.Min = minya
|
delta := yrangeAlt.GetDelta()
|
||||||
yrangeAlt.Max = maxya
|
roundTo := Math.GetRoundToForDelta(delta)
|
||||||
yrangeAlt.Min, yrangeAlt.Max = yrangeAlt.GetRoundedRangeBounds()
|
rmin, rmax := Math.RoundDown(yrangeAlt.GetMin(), roundTo), Math.RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||||
|
yrangeAlt.SetMin(rmin)
|
||||||
|
yrangeAlt.SetMax(rmax)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) checkRanges(xr, yr, yra Range) error {
|
func (c Chart) checkRanges(xr, yr, yra Range) error {
|
||||||
|
if math.IsInf(xr.GetDelta(), 0) || math.IsNaN(xr.GetDelta()) || xr.GetDelta() == 0 {
|
||||||
if math.IsInf(xr.Delta(), 0) || math.IsNaN(xr.Delta()) {
|
|
||||||
return errors.New("Invalid (infinite or NaN) x-range delta")
|
return errors.New("Invalid (infinite or NaN) x-range delta")
|
||||||
}
|
}
|
||||||
if math.IsInf(yr.Delta(), 0) || math.IsNaN(yr.Delta()) {
|
if math.IsInf(yr.GetDelta(), 0) || math.IsNaN(yr.GetDelta()) || yr.GetDelta() == 0 {
|
||||||
return errors.New("Invalid (infinite or NaN) y-range delta")
|
return errors.New("Invalid (infinite or NaN) y-range delta")
|
||||||
}
|
}
|
||||||
if c.hasSecondarySeries() {
|
if c.hasSecondarySeries() {
|
||||||
if math.IsInf(yra.Delta(), 0) || math.IsNaN(yra.Delta()) {
|
if math.IsInf(yra.GetDelta(), 0) || math.IsNaN(yra.GetDelta()) || yra.GetDelta() == 0 {
|
||||||
return errors.New("Invalid (infinite or NaN) y-secondary-range delta")
|
return errors.New("Invalid (infinite or NaN) y-secondary-range delta")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,14 +338,11 @@ func (c Chart) getAxisAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra R
|
||||||
return canvasBox.OuterConstrain(c.Box(), axesOuterBox)
|
return canvasBox.OuterConstrain(c.Box(), axesOuterBox)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (xr2, yr2, yra2 Range) {
|
func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range, Range) {
|
||||||
xr2.Min, xr2.Max = xr.Min, xr.Max
|
xr.SetDomain(canvasBox.Width())
|
||||||
xr2.Domain = canvasBox.Width()
|
yr.SetDomain(canvasBox.Height())
|
||||||
yr2.Min, yr2.Max = yr.Min, yr.Max
|
yra.SetDomain(canvasBox.Height())
|
||||||
yr2.Domain = canvasBox.Height()
|
return xr, yr, yra
|
||||||
yra2.Min, yra2.Max = yra.Min, yra.Max
|
|
||||||
yra2.Domain = canvasBox.Height()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) hasAnnotationSeries() bool {
|
func (c Chart) hasAnnotationSeries() bool {
|
||||||
|
@ -372,22 +387,22 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) getBackgroundStyle() Style {
|
func (c Chart) getBackgroundStyle() Style {
|
||||||
return c.Background.WithDefaultsFrom(c.styleDefaultsBackground())
|
return c.Background.InheritFrom(c.styleDefaultsBackground())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) drawBackground(r Renderer) {
|
func (c Chart) drawBackground(r Renderer) {
|
||||||
DrawBox(r, Box{
|
Draw.Box(r, Box{
|
||||||
Right: c.GetWidth(),
|
Right: c.GetWidth(),
|
||||||
Bottom: c.GetHeight(),
|
Bottom: c.GetHeight(),
|
||||||
}, c.getBackgroundStyle())
|
}, c.getBackgroundStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) getCanvasStyle() Style {
|
func (c Chart) getCanvasStyle() Style {
|
||||||
return c.Canvas.WithDefaultsFrom(c.styleDefaultsCanvas())
|
return c.Canvas.InheritFrom(c.styleDefaultsCanvas())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
|
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
DrawBox(r, canvasBox, c.getCanvasStyle())
|
Draw.Box(r, canvasBox, c.getCanvasStyle())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
|
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
|
||||||
|
@ -448,7 +463,7 @@ func (c Chart) styleDefaultsCanvas() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
||||||
strokeColor := GetDefaultSeriesStrokeColor(seriesIndex)
|
strokeColor := GetDefaultColor(seriesIndex)
|
||||||
return Style{
|
return Style{
|
||||||
StrokeColor: strokeColor,
|
StrokeColor: strokeColor,
|
||||||
StrokeWidth: DefaultStrokeWidth,
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
@ -460,6 +475,7 @@ func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
|
||||||
func (c Chart) styleDefaultsAxis() Style {
|
func (c Chart) styleDefaultsAxis() Style {
|
||||||
return Style{
|
return Style{
|
||||||
Font: c.GetFont(),
|
Font: c.GetFont(),
|
||||||
|
FontColor: DefaultAxisColor,
|
||||||
FontSize: DefaultAxisFontSize,
|
FontSize: DefaultAxisFontSize,
|
||||||
StrokeColor: DefaultAxisColor,
|
StrokeColor: DefaultAxisColor,
|
||||||
StrokeWidth: DefaultAxisLineWidth,
|
StrokeWidth: DefaultAxisLineWidth,
|
||||||
|
|
|
@ -77,24 +77,24 @@ func TestChartGetRanges(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
xrange, yrange, yrangeAlt := c.getRanges()
|
xrange, yrange, yrangeAlt := c.getRanges()
|
||||||
assert.Equal(-2.0, xrange.Min)
|
assert.Equal(-2.0, xrange.GetMin())
|
||||||
assert.Equal(5.0, xrange.Max)
|
assert.Equal(5.0, xrange.GetMax())
|
||||||
|
|
||||||
assert.Equal(-2.1, yrange.Min)
|
assert.Equal(-2.1, yrange.GetMin())
|
||||||
assert.Equal(4.5, yrange.Max)
|
assert.Equal(4.5, yrange.GetMax())
|
||||||
|
|
||||||
assert.Equal(10.0, yrangeAlt.Min)
|
assert.Equal(10.0, yrangeAlt.GetMin())
|
||||||
assert.Equal(14.0, yrangeAlt.Max)
|
assert.Equal(14.0, yrangeAlt.GetMax())
|
||||||
|
|
||||||
cSet := Chart{
|
cSet := Chart{
|
||||||
XAxis: XAxis{
|
XAxis: XAxis{
|
||||||
Range: Range{Min: 9.8, Max: 19.8},
|
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
|
||||||
},
|
},
|
||||||
YAxis: YAxis{
|
YAxis: YAxis{
|
||||||
Range: Range{Min: 9.9, Max: 19.9},
|
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
|
||||||
},
|
},
|
||||||
YAxisSecondary: YAxis{
|
YAxisSecondary: YAxis{
|
||||||
Range: Range{Min: 9.7, Max: 19.7},
|
Range: &ContinuousRange{Min: 9.7, Max: 19.7},
|
||||||
},
|
},
|
||||||
Series: []Series{
|
Series: []Series{
|
||||||
ContinuousSeries{
|
ContinuousSeries{
|
||||||
|
@ -114,14 +114,14 @@ func TestChartGetRanges(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
xr2, yr2, yra2 := cSet.getRanges()
|
xr2, yr2, yra2 := cSet.getRanges()
|
||||||
assert.Equal(9.8, xr2.Min)
|
assert.Equal(9.8, xr2.GetMin())
|
||||||
assert.Equal(19.8, xr2.Max)
|
assert.Equal(19.8, xr2.GetMax())
|
||||||
|
|
||||||
assert.Equal(9.9, yr2.Min)
|
assert.Equal(9.9, yr2.GetMin())
|
||||||
assert.Equal(19.9, yr2.Max)
|
assert.Equal(19.9, yr2.GetMax())
|
||||||
|
|
||||||
assert.Equal(9.7, yra2.Min)
|
assert.Equal(9.7, yra2.GetMin())
|
||||||
assert.Equal(19.7, yra2.Max)
|
assert.Equal(19.7, yra2.GetMax())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestChartGetRangesUseTicks(t *testing.T) {
|
func TestChartGetRangesUseTicks(t *testing.T) {
|
||||||
|
@ -139,7 +139,7 @@ func TestChartGetRangesUseTicks(t *testing.T) {
|
||||||
{4.0, "4.0"},
|
{4.0, "4.0"},
|
||||||
{5.0, "Five"},
|
{5.0, "Five"},
|
||||||
},
|
},
|
||||||
Range: Range{
|
Range: &ContinuousRange{
|
||||||
Min: -5.0,
|
Min: -5.0,
|
||||||
Max: 5.0,
|
Max: 5.0,
|
||||||
},
|
},
|
||||||
|
@ -153,10 +153,10 @@ func TestChartGetRangesUseTicks(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
xr, yr, yar := c.getRanges()
|
xr, yr, yar := c.getRanges()
|
||||||
assert.Equal(-2.0, xr.Min)
|
assert.Equal(-2.0, xr.GetMin())
|
||||||
assert.Equal(2.0, xr.Max)
|
assert.Equal(2.0, xr.GetMax())
|
||||||
assert.Equal(0.0, yr.Min)
|
assert.Equal(0.0, yr.GetMin())
|
||||||
assert.Equal(5.0, yr.Max)
|
assert.Equal(5.0, yr.GetMax())
|
||||||
assert.True(yar.IsZero(), yar.String())
|
assert.True(yar.IsZero(), yar.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ func TestChartGetRangesUseUserRanges(t *testing.T) {
|
||||||
|
|
||||||
c := Chart{
|
c := Chart{
|
||||||
YAxis: YAxis{
|
YAxis: YAxis{
|
||||||
Range: Range{
|
Range: &ContinuousRange{
|
||||||
Min: -5.0,
|
Min: -5.0,
|
||||||
Max: 5.0,
|
Max: 5.0,
|
||||||
},
|
},
|
||||||
|
@ -179,10 +179,10 @@ func TestChartGetRangesUseUserRanges(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
xr, yr, yar := c.getRanges()
|
xr, yr, yar := c.getRanges()
|
||||||
assert.Equal(-2.0, xr.Min)
|
assert.Equal(-2.0, xr.GetMin())
|
||||||
assert.Equal(2.0, xr.Max)
|
assert.Equal(2.0, xr.GetMax())
|
||||||
assert.Equal(-5.0, yr.Min)
|
assert.Equal(-5.0, yr.GetMin())
|
||||||
assert.Equal(5.0, yr.Max)
|
assert.Equal(5.0, yr.GetMax())
|
||||||
assert.True(yar.IsZero(), yar.String())
|
assert.True(yar.IsZero(), yar.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,15 +310,15 @@ func TestChartGetAxesTicks(t *testing.T) {
|
||||||
c := Chart{
|
c := Chart{
|
||||||
XAxis: XAxis{
|
XAxis: XAxis{
|
||||||
Style: Style{Show: true},
|
Style: Style{Show: true},
|
||||||
Range: Range{Min: 9.8, Max: 19.8},
|
Range: &ContinuousRange{Min: 9.8, Max: 19.8},
|
||||||
},
|
},
|
||||||
YAxis: YAxis{
|
YAxis: YAxis{
|
||||||
Style: Style{Show: true},
|
Style: Style{Show: true},
|
||||||
Range: Range{Min: 9.9, Max: 19.9},
|
Range: &ContinuousRange{Min: 9.9, Max: 19.9},
|
||||||
},
|
},
|
||||||
YAxisSecondary: YAxis{
|
YAxisSecondary: YAxis{
|
||||||
Style: Style{Show: true},
|
Style: Style{Show: true},
|
||||||
Range: Range{Min: 9.7, Max: 19.7},
|
Range: &ContinuousRange{Min: 9.7, Max: 19.7},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
xr, yr, yar := c.getRanges()
|
xr, yr, yar := c.getRanges()
|
||||||
|
@ -337,7 +337,7 @@ func TestChartSingleSeries(t *testing.T) {
|
||||||
Width: 1024,
|
Width: 1024,
|
||||||
Height: 400,
|
Height: 400,
|
||||||
YAxis: YAxis{
|
YAxis: YAxis{
|
||||||
Range: Range{
|
Range: &ContinuousRange{
|
||||||
Min: 0.0,
|
Min: 0.0,
|
||||||
Max: 4.0,
|
Max: 4.0,
|
||||||
},
|
},
|
||||||
|
@ -377,15 +377,15 @@ func TestChartRegressionBadRangesByUser(t *testing.T) {
|
||||||
|
|
||||||
c := Chart{
|
c := Chart{
|
||||||
YAxis: YAxis{
|
YAxis: YAxis{
|
||||||
Range: Range{
|
Range: &ContinuousRange{
|
||||||
Min: math.Inf(-1),
|
Min: math.Inf(-1),
|
||||||
Max: math.Inf(1), // this could really happen? eh.
|
Max: math.Inf(1), // this could really happen? eh.
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Series: []Series{
|
Series: []Series{
|
||||||
ContinuousSeries{
|
ContinuousSeries{
|
||||||
XValues: Seq(1.0, 10.0),
|
XValues: Sequence.Float64(1.0, 10.0),
|
||||||
YValues: Seq(1.0, 10.0),
|
YValues: Sequence.Float64(1.0, 10.0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
32
concat_series.go
Normal file
32
concat_series.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
// ConcatSeries is a special type of series that concatenates its `InnerSeries`.
|
||||||
|
type ConcatSeries []Series
|
||||||
|
|
||||||
|
// Len returns the length of the concatenated set of series.
|
||||||
|
func (cs ConcatSeries) Len() int {
|
||||||
|
total := 0
|
||||||
|
for _, s := range cs {
|
||||||
|
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||||
|
total += typed.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the value at the (meta) index (i.e 0 => totalLen-1)
|
||||||
|
func (cs ConcatSeries) GetValue(index int) (x, y float64) {
|
||||||
|
cursor := 0
|
||||||
|
for _, s := range cs {
|
||||||
|
if typed, isValueProvider := s.(ValueProvider); isValueProvider {
|
||||||
|
len := typed.Len()
|
||||||
|
if index < cursor+len {
|
||||||
|
x, y = typed.GetValue(index - cursor) //FENCEPOSTS.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cursor += typed.Len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
41
concat_series_test.go
Normal file
41
concat_series_test.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConcatSeries(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
s1 := ContinuousSeries{
|
||||||
|
XValues: Sequence.Float64(1.0, 10.0),
|
||||||
|
YValues: Sequence.Float64(1.0, 10.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
s2 := ContinuousSeries{
|
||||||
|
XValues: Sequence.Float64(11, 20.0),
|
||||||
|
YValues: Sequence.Float64(10.0, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
s3 := ContinuousSeries{
|
||||||
|
XValues: Sequence.Float64(21, 30.0),
|
||||||
|
YValues: Sequence.Float64(1.0, 10.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := ConcatSeries([]Series{s1, s2, s3})
|
||||||
|
assert.Equal(30, cs.Len())
|
||||||
|
|
||||||
|
x0, y0 := cs.GetValue(0)
|
||||||
|
assert.Equal(1.0, x0)
|
||||||
|
assert.Equal(1.0, y0)
|
||||||
|
|
||||||
|
xm, ym := cs.GetValue(19)
|
||||||
|
assert.Equal(20.0, xm)
|
||||||
|
assert.Equal(1.0, ym)
|
||||||
|
|
||||||
|
xn, yn := cs.GetValue(29)
|
||||||
|
assert.Equal(30.0, xn)
|
||||||
|
assert.Equal(10.0, yn)
|
||||||
|
}
|
67
continuous_range.go
Normal file
67
continuous_range.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContinuousRange represents a boundary for a set of numbers.
|
||||||
|
type ContinuousRange struct {
|
||||||
|
Min float64
|
||||||
|
Max float64
|
||||||
|
Domain int
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the ContinuousRange has been set or not.
|
||||||
|
func (r ContinuousRange) IsZero() bool {
|
||||||
|
return (r.Min == 0 || math.IsNaN(r.Min)) &&
|
||||||
|
(r.Max == 0 || math.IsNaN(r.Max)) &&
|
||||||
|
r.Domain == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMin gets the min value for the continuous range.
|
||||||
|
func (r ContinuousRange) GetMin() float64 {
|
||||||
|
return r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMin sets the min value for the continuous range.
|
||||||
|
func (r *ContinuousRange) SetMin(min float64) {
|
||||||
|
r.Min = min
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMax returns the max value for the continuous range.
|
||||||
|
func (r ContinuousRange) GetMax() float64 {
|
||||||
|
return r.Max
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMax sets the max value for the continuous range.
|
||||||
|
func (r *ContinuousRange) SetMax(max float64) {
|
||||||
|
r.Max = max
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDelta returns the difference between the min and max value.
|
||||||
|
func (r ContinuousRange) GetDelta() float64 {
|
||||||
|
return r.Max - r.Min
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomain returns the range domain.
|
||||||
|
func (r ContinuousRange) GetDomain() int {
|
||||||
|
return r.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDomain sets the range domain.
|
||||||
|
func (r *ContinuousRange) SetDomain(domain int) {
|
||||||
|
r.Domain = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a simple string for the ContinuousRange.
|
||||||
|
func (r ContinuousRange) String() string {
|
||||||
|
return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate maps a given value into the ContinuousRange space.
|
||||||
|
func (r ContinuousRange) Translate(value float64) int {
|
||||||
|
normalized := value - r.Min
|
||||||
|
ratio := normalized / r.GetDelta()
|
||||||
|
return int(math.Ceil(ratio * float64(r.Domain)))
|
||||||
|
}
|
|
@ -9,8 +9,8 @@ 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, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
|
values := []float64{1.0, 2.0, 2.5, 2.7, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
|
||||||
r := Range{Domain: 1000}
|
r := ContinuousRange{Domain: 1000}
|
||||||
r.Min, r.Max = MinAndMax(values...)
|
r.Min, r.Max = Math.MinAndMax(values...)
|
||||||
|
|
||||||
// delta = ~7.0
|
// delta = ~7.0
|
||||||
// value = ~5.0
|
// value = ~5.0
|
|
@ -5,7 +5,7 @@ type ContinuousSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
|
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
|
|
||||||
XValues []float64
|
XValues []float64
|
||||||
YValues []float64
|
YValues []float64
|
||||||
|
@ -44,12 +44,12 @@ func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (cs ContinuousSeries) GetYAxis() YAxisType {
|
func (cs ContinuousSeries) GetYAxis() yAxisType {
|
||||||
return cs.YAxis
|
return cs.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders the series.
|
// Render renders the series.
|
||||||
func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
style := cs.Style.WithDefaultsFrom(defaults)
|
style := cs.Style.InheritFrom(defaults)
|
||||||
DrawLineSeries(r, canvasBox, xrange, yrange, style, cs)
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs)
|
||||||
}
|
}
|
||||||
|
|
31
continuous_series_test.go
Normal file
31
continuous_series_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestContinuousSeries(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cs := ContinuousSeries{
|
||||||
|
Name: "Test Series",
|
||||||
|
XValues: Sequence.Float64(1.0, 10.0),
|
||||||
|
YValues: Sequence.Float64(1.0, 10.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal("Test Series", cs.GetName())
|
||||||
|
assert.Equal(10, cs.Len())
|
||||||
|
x0, y0 := cs.GetValue(0)
|
||||||
|
assert.Equal(1.0, x0)
|
||||||
|
assert.Equal(1.0, y0)
|
||||||
|
|
||||||
|
xn, yn := cs.GetValue(9)
|
||||||
|
assert.Equal(10.0, xn)
|
||||||
|
assert.Equal(10.0, yn)
|
||||||
|
|
||||||
|
xn, yn = cs.GetLastValue()
|
||||||
|
assert.Equal(10.0, xn)
|
||||||
|
assert.Equal(10.0, yn)
|
||||||
|
}
|
353
date/util.go
Normal file
353
date/util.go
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
package date
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AllDaysMask is a bitmask of all the days of the week.
|
||||||
|
AllDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday) | 1<<uint(time.Saturday)
|
||||||
|
// WeekDaysMask is a bitmask of all the weekdays of the week.
|
||||||
|
WeekDaysMask = 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday)
|
||||||
|
//WeekendDaysMask is a bitmask of the weekend days of the week.
|
||||||
|
WeekendDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Saturday)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DaysOfWeek are all the time.Weekday in an array for utility purposes.
|
||||||
|
DaysOfWeek = []time.Weekday{
|
||||||
|
time.Sunday,
|
||||||
|
time.Monday,
|
||||||
|
time.Tuesday,
|
||||||
|
time.Wednesday,
|
||||||
|
time.Thursday,
|
||||||
|
time.Friday,
|
||||||
|
time.Saturday,
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeekDays are the business time.Weekday in an array.
|
||||||
|
WeekDays = []time.Weekday{
|
||||||
|
time.Monday,
|
||||||
|
time.Tuesday,
|
||||||
|
time.Wednesday,
|
||||||
|
time.Thursday,
|
||||||
|
time.Friday,
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeekendDays are the weekend time.Weekday in an array.
|
||||||
|
WeekendDays = []time.Weekday{
|
||||||
|
time.Sunday,
|
||||||
|
time.Saturday,
|
||||||
|
}
|
||||||
|
|
||||||
|
//Epoch is unix epoc saved for utility purposes.
|
||||||
|
Epoch = time.Unix(0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_easternLock sync.Mutex
|
||||||
|
_eastern *time.Location
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// NYSEOpen is when the NYSE opens.
|
||||||
|
NYSEOpen = ClockTime(9, 30, 0, 0, Eastern())
|
||||||
|
|
||||||
|
// NYSEClose is when the NYSE closes.
|
||||||
|
NYSEClose = ClockTime(16, 0, 0, 0, Eastern())
|
||||||
|
|
||||||
|
// NASDAQOpen is when NASDAQ opens.
|
||||||
|
NASDAQOpen = ClockTime(9, 30, 0, 0, Eastern())
|
||||||
|
|
||||||
|
// NASDAQClose is when NASDAQ closes.
|
||||||
|
NASDAQClose = ClockTime(16, 0, 0, 0, Eastern())
|
||||||
|
|
||||||
|
// NYSEArcaOpen is when NYSEARCA opens.
|
||||||
|
NYSEArcaOpen = ClockTime(4, 0, 0, 0, Eastern())
|
||||||
|
|
||||||
|
// NYSEArcaClose is when NYSEARCA closes.
|
||||||
|
NYSEArcaClose = ClockTime(20, 0, 0, 0, Eastern())
|
||||||
|
)
|
||||||
|
|
||||||
|
// HolidayProvider is a function that returns if a given time falls on a holiday.
|
||||||
|
type HolidayProvider func(time.Time) bool
|
||||||
|
|
||||||
|
// DefaultHolidayProvider implements `HolidayProvider` and just returns false.
|
||||||
|
func DefaultHolidayProvider(_ time.Time) bool { return false }
|
||||||
|
|
||||||
|
// IsNYSEHoliday returns if a date was/is on a nyse holiday day.
|
||||||
|
func IsNYSEHoliday(t time.Time) bool {
|
||||||
|
te := t.In(Eastern())
|
||||||
|
if te.Year() == 2013 {
|
||||||
|
if te.Month() == 1 {
|
||||||
|
return te.Day() == 1 || te.Day() == 21
|
||||||
|
} else if te.Month() == 2 {
|
||||||
|
return te.Day() == 18
|
||||||
|
} else if te.Month() == 3 {
|
||||||
|
return te.Day() == 29
|
||||||
|
} else if te.Month() == 5 {
|
||||||
|
return te.Day() == 27
|
||||||
|
} else if te.Month() == 7 {
|
||||||
|
return te.Day() == 4
|
||||||
|
} else if te.Month() == 9 {
|
||||||
|
return te.Day() == 2
|
||||||
|
} else if te.Month() == 11 {
|
||||||
|
return te.Day() == 28
|
||||||
|
} else if te.Month() == 12 {
|
||||||
|
return te.Day() == 25
|
||||||
|
}
|
||||||
|
} else if te.Year() == 2014 {
|
||||||
|
if te.Month() == 1 {
|
||||||
|
return te.Day() == 1 || te.Day() == 20
|
||||||
|
} else if te.Month() == 2 {
|
||||||
|
return te.Day() == 17
|
||||||
|
} else if te.Month() == 4 {
|
||||||
|
return te.Day() == 18
|
||||||
|
} else if te.Month() == 5 {
|
||||||
|
return te.Day() == 26
|
||||||
|
} else if te.Month() == 7 {
|
||||||
|
return te.Day() == 4
|
||||||
|
} else if te.Month() == 9 {
|
||||||
|
return te.Day() == 1
|
||||||
|
} else if te.Month() == 11 {
|
||||||
|
return te.Day() == 27
|
||||||
|
} else if te.Month() == 12 {
|
||||||
|
return te.Day() == 25
|
||||||
|
}
|
||||||
|
} else if te.Year() == 2015 {
|
||||||
|
if te.Month() == 1 {
|
||||||
|
return te.Day() == 1 || te.Day() == 19
|
||||||
|
} else if te.Month() == 2 {
|
||||||
|
return te.Day() == 16
|
||||||
|
} else if te.Month() == 4 {
|
||||||
|
return te.Day() == 3
|
||||||
|
} else if te.Month() == 5 {
|
||||||
|
return te.Day() == 25
|
||||||
|
} else if te.Month() == 7 {
|
||||||
|
return te.Day() == 3
|
||||||
|
} else if te.Month() == 9 {
|
||||||
|
return te.Day() == 7
|
||||||
|
} else if te.Month() == 11 {
|
||||||
|
return te.Day() == 26
|
||||||
|
} else if te.Month() == 12 {
|
||||||
|
return te.Day() == 25
|
||||||
|
}
|
||||||
|
} else if te.Year() == 2016 {
|
||||||
|
if te.Month() == 1 {
|
||||||
|
return te.Day() == 1 || te.Day() == 18
|
||||||
|
} else if te.Month() == 2 {
|
||||||
|
return te.Day() == 15
|
||||||
|
} else if te.Month() == 3 {
|
||||||
|
return te.Day() == 25
|
||||||
|
} else if te.Month() == 5 {
|
||||||
|
return te.Day() == 30
|
||||||
|
} else if te.Month() == 7 {
|
||||||
|
return te.Day() == 4
|
||||||
|
} else if te.Month() == 9 {
|
||||||
|
return te.Day() == 5
|
||||||
|
} else if te.Month() == 11 {
|
||||||
|
return te.Day() == 24 || te.Day() == 25
|
||||||
|
} else if te.Month() == 12 {
|
||||||
|
return te.Day() == 26
|
||||||
|
}
|
||||||
|
} else if te.Year() == 2017 {
|
||||||
|
if te.Month() == 1 {
|
||||||
|
return te.Day() == 1 || te.Day() == 16
|
||||||
|
} else if te.Month() == 2 {
|
||||||
|
return te.Day() == 20
|
||||||
|
} else if te.Month() == 4 {
|
||||||
|
return te.Day() == 15
|
||||||
|
} else if te.Month() == 5 {
|
||||||
|
return te.Day() == 29
|
||||||
|
} else if te.Month() == 7 {
|
||||||
|
return te.Day() == 4
|
||||||
|
} else if te.Month() == 9 {
|
||||||
|
return te.Day() == 4
|
||||||
|
} else if te.Month() == 11 {
|
||||||
|
return te.Day() == 23
|
||||||
|
} else if te.Month() == 12 {
|
||||||
|
return te.Day() == 25
|
||||||
|
}
|
||||||
|
} else if te.Year() == 2018 {
|
||||||
|
if te.Month() == 1 {
|
||||||
|
return te.Day() == 1 || te.Day() == 15
|
||||||
|
} else if te.Month() == 2 {
|
||||||
|
return te.Day() == 19
|
||||||
|
} else if te.Month() == 3 {
|
||||||
|
return te.Day() == 30
|
||||||
|
} else if te.Month() == 5 {
|
||||||
|
return te.Day() == 28
|
||||||
|
} else if te.Month() == 7 {
|
||||||
|
return te.Day() == 4
|
||||||
|
} else if te.Month() == 9 {
|
||||||
|
return te.Day() == 3
|
||||||
|
} else if te.Month() == 11 {
|
||||||
|
return te.Day() == 22
|
||||||
|
} else if te.Month() == 12 {
|
||||||
|
return te.Day() == 25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNYSEArcaHoliday returns that returns if a given time falls on a holiday.
|
||||||
|
func IsNYSEArcaHoliday(t time.Time) bool {
|
||||||
|
return IsNYSEHoliday(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNASDAQHoliday returns if a date was a NASDAQ holiday day.
|
||||||
|
func IsNASDAQHoliday(t time.Time) bool {
|
||||||
|
return IsNYSEHoliday(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eastern returns the eastern timezone.
|
||||||
|
func Eastern() *time.Location {
|
||||||
|
if _eastern == nil {
|
||||||
|
_easternLock.Lock()
|
||||||
|
defer _easternLock.Unlock()
|
||||||
|
if _eastern == nil {
|
||||||
|
_eastern, _ = time.LoadLocation("America/New_York")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _eastern
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClockTime returns a new time.Time for the given clock components.
|
||||||
|
func ClockTime(hour, min, sec, nsec int, loc *time.Location) time.Time {
|
||||||
|
return time.Date(0, 0, 0, hour, min, sec, nsec, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On returns the clock components of clock (hour,minute,second) on the date components of d.
|
||||||
|
func On(clock, d time.Time) time.Time {
|
||||||
|
return time.Date(d.Year(), d.Month(), d.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional returns a pointer reference to a given time.
|
||||||
|
func Optional(t time.Time) *time.Time {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWeekDay returns if the day is a monday->friday.
|
||||||
|
func IsWeekDay(day time.Weekday) bool {
|
||||||
|
return !IsWeekendDay(day)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWeekendDay returns if the day is a monday->friday.
|
||||||
|
func IsWeekendDay(day time.Weekday) bool {
|
||||||
|
return day == time.Saturday || day == time.Sunday
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeDate returns if a timestamp is strictly before another date (ignoring hours, minutes etc.)
|
||||||
|
func BeforeDate(before, reference time.Time) bool {
|
||||||
|
if before.Year() < reference.Year() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if before.Month() < reference.Month() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return before.Year() == reference.Year() && before.Month() == reference.Month() && before.Day() < reference.Day()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextMarketOpen returns the next market open after a given time.
|
||||||
|
func NextMarketOpen(after, openTime time.Time, isHoliday HolidayProvider) time.Time {
|
||||||
|
afterEastern := after.In(Eastern())
|
||||||
|
todaysOpen := On(openTime, afterEastern)
|
||||||
|
|
||||||
|
if isHoliday == nil {
|
||||||
|
isHoliday = DefaultHolidayProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
if afterEastern.Before(todaysOpen) && IsWeekDay(todaysOpen.Weekday()) && !isHoliday(todaysOpen) {
|
||||||
|
return todaysOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
if afterEastern.Equal(todaysOpen) { //rare but it might happen.
|
||||||
|
return todaysOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
for cursorDay := 1; cursorDay < 6; cursorDay++ {
|
||||||
|
newDay := todaysOpen.AddDate(0, 0, cursorDay)
|
||||||
|
if IsWeekDay(newDay.Weekday()) && !isHoliday(afterEastern) {
|
||||||
|
return On(openTime, newDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Epoch //we should never reach this.
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextMarketClose returns the next market close after a given time.
|
||||||
|
func NextMarketClose(after, closeTime time.Time, isHoliday HolidayProvider) time.Time {
|
||||||
|
afterEastern := after.In(Eastern())
|
||||||
|
|
||||||
|
if isHoliday == nil {
|
||||||
|
isHoliday = DefaultHolidayProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
todaysClose := On(closeTime, afterEastern)
|
||||||
|
if afterEastern.Before(todaysClose) && IsWeekDay(todaysClose.Weekday()) && !isHoliday(todaysClose) {
|
||||||
|
return todaysClose
|
||||||
|
}
|
||||||
|
|
||||||
|
if afterEastern.Equal(todaysClose) { //rare but it might happen.
|
||||||
|
return todaysClose
|
||||||
|
}
|
||||||
|
|
||||||
|
for cursorDay := 1; cursorDay < 6; cursorDay++ {
|
||||||
|
newDay := todaysClose.AddDate(0, 0, cursorDay)
|
||||||
|
if IsWeekDay(newDay.Weekday()) && !isHoliday(newDay) {
|
||||||
|
return On(closeTime, newDay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Epoch //we should never reach this.
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateMarketSecondsBetween calculates the number of seconds the market was open between two dates.
|
||||||
|
func CalculateMarketSecondsBetween(start, end, marketOpen, marketClose time.Time, isHoliday HolidayProvider) (seconds int64) {
|
||||||
|
startEastern := start.In(Eastern())
|
||||||
|
endEastern := end.In(Eastern())
|
||||||
|
|
||||||
|
startMarketOpen := On(marketOpen, startEastern)
|
||||||
|
startMarketClose := On(marketClose, startEastern)
|
||||||
|
|
||||||
|
if !IsWeekendDay(startMarketOpen.Weekday()) && !isHoliday(startMarketOpen) {
|
||||||
|
if (startEastern.Equal(startMarketOpen) || startEastern.After(startMarketOpen)) && startEastern.Before(startMarketClose) {
|
||||||
|
if endEastern.Before(startMarketClose) {
|
||||||
|
seconds += int64(endEastern.Sub(startEastern) / time.Second)
|
||||||
|
} else {
|
||||||
|
seconds += int64(startMarketClose.Sub(startEastern) / time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := NextMarketOpen(startMarketClose, marketOpen, isHoliday)
|
||||||
|
for BeforeDate(cursor, endEastern) {
|
||||||
|
if IsWeekDay(cursor.Weekday()) && !isHoliday(cursor) {
|
||||||
|
close := NextMarketClose(cursor, marketClose, isHoliday)
|
||||||
|
seconds += int64(close.Sub(cursor) / time.Second)
|
||||||
|
}
|
||||||
|
cursor = cursor.AddDate(0, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalMarketOpen := NextMarketOpen(cursor, marketOpen, isHoliday)
|
||||||
|
finalMarketClose := NextMarketClose(cursor, marketClose, isHoliday)
|
||||||
|
if endEastern.After(finalMarketOpen) {
|
||||||
|
if endEastern.Before(finalMarketClose) {
|
||||||
|
seconds += int64(endEastern.Sub(finalMarketOpen) / time.Second)
|
||||||
|
} else {
|
||||||
|
seconds += int64(finalMarketClose.Sub(finalMarketOpen) / time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format returns a string representation of a date.
|
||||||
|
func format(t time.Time) string {
|
||||||
|
return t.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse parses a date from a string.
|
||||||
|
func parse(str string) time.Time {
|
||||||
|
res, _ := time.Parse("2006-01-02", str)
|
||||||
|
return res
|
||||||
|
}
|
98
date/util_test.go
Normal file
98
date/util_test.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package date
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBeforeDate(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.True(BeforeDate(parse("2015-07-02"), parse("2016-07-01")))
|
||||||
|
assert.True(BeforeDate(parse("2016-06-01"), parse("2016-07-01")))
|
||||||
|
assert.True(BeforeDate(parse("2016-07-01"), parse("2016-07-02")))
|
||||||
|
|
||||||
|
assert.False(BeforeDate(parse("2016-07-01"), parse("2016-07-01")))
|
||||||
|
assert.False(BeforeDate(parse("2016-07-03"), parse("2016-07-01")))
|
||||||
|
assert.False(BeforeDate(parse("2016-08-03"), parse("2016-07-01")))
|
||||||
|
assert.False(BeforeDate(parse("2017-08-03"), parse("2016-07-01")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextMarketOpen(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
beforeOpen := time.Date(2016, 07, 18, 9, 0, 0, 0, Eastern())
|
||||||
|
todayOpen := time.Date(2016, 07, 18, 9, 30, 0, 0, Eastern())
|
||||||
|
|
||||||
|
afterOpen := time.Date(2016, 07, 18, 9, 31, 0, 0, Eastern())
|
||||||
|
tomorrowOpen := time.Date(2016, 07, 19, 9, 30, 0, 0, Eastern())
|
||||||
|
|
||||||
|
afterFriday := time.Date(2016, 07, 22, 9, 31, 0, 0, Eastern())
|
||||||
|
mondayOpen := time.Date(2016, 07, 25, 9, 30, 0, 0, Eastern())
|
||||||
|
|
||||||
|
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Eastern())
|
||||||
|
|
||||||
|
assert.True(todayOpen.Equal(NextMarketOpen(beforeOpen, NYSEOpen, IsNYSEHoliday)))
|
||||||
|
assert.True(tomorrowOpen.Equal(NextMarketOpen(afterOpen, NYSEOpen, IsNYSEHoliday)))
|
||||||
|
assert.True(mondayOpen.Equal(NextMarketOpen(afterFriday, NYSEOpen, IsNYSEHoliday)))
|
||||||
|
assert.True(mondayOpen.Equal(NextMarketOpen(weekend, NYSEOpen, IsNYSEHoliday)))
|
||||||
|
|
||||||
|
testRegression := time.Date(2016, 07, 18, 16, 0, 0, 0, Eastern())
|
||||||
|
shouldbe := time.Date(2016, 07, 19, 9, 30, 0, 0, Eastern())
|
||||||
|
|
||||||
|
assert.True(shouldbe.Equal(NextMarketOpen(testRegression, NYSEOpen, IsNYSEHoliday)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextMarketClose(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
beforeClose := time.Date(2016, 07, 18, 15, 0, 0, 0, Eastern())
|
||||||
|
todayClose := time.Date(2016, 07, 18, 16, 00, 0, 0, Eastern())
|
||||||
|
|
||||||
|
afterClose := time.Date(2016, 07, 18, 16, 1, 0, 0, Eastern())
|
||||||
|
tomorrowClose := time.Date(2016, 07, 19, 16, 00, 0, 0, Eastern())
|
||||||
|
|
||||||
|
afterFriday := time.Date(2016, 07, 22, 16, 1, 0, 0, Eastern())
|
||||||
|
mondayClose := time.Date(2016, 07, 25, 16, 0, 0, 0, Eastern())
|
||||||
|
|
||||||
|
weekend := time.Date(2016, 07, 23, 9, 31, 0, 0, Eastern())
|
||||||
|
|
||||||
|
assert.True(todayClose.Equal(NextMarketClose(beforeClose, NYSEClose, IsNYSEHoliday)))
|
||||||
|
assert.True(tomorrowClose.Equal(NextMarketClose(afterClose, NYSEClose, IsNYSEHoliday)))
|
||||||
|
assert.True(mondayClose.Equal(NextMarketClose(afterFriday, NYSEClose, IsNYSEHoliday)))
|
||||||
|
assert.True(mondayClose.Equal(NextMarketClose(weekend, NYSEClose, IsNYSEHoliday)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateMarketSecondsBetween(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
start := time.Date(2016, 07, 18, 9, 30, 0, 0, Eastern())
|
||||||
|
end := time.Date(2016, 07, 22, 16, 00, 0, 0, Eastern())
|
||||||
|
|
||||||
|
shouldbe := 5 * 6.5 * 60 * 60
|
||||||
|
|
||||||
|
assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, IsNYSEHoliday))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateMarketSecondsBetween1D(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
start := time.Date(2016, 07, 22, 9, 45, 0, 0, Eastern())
|
||||||
|
end := time.Date(2016, 07, 22, 15, 45, 0, 0, Eastern())
|
||||||
|
|
||||||
|
shouldbe := 6 * 60 * 60
|
||||||
|
|
||||||
|
assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, IsNYSEHoliday))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateMarketSecondsBetweenLTM(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
start := time.Date(2015, 07, 01, 9, 30, 0, 0, Eastern())
|
||||||
|
end := time.Date(2016, 07, 01, 9, 30, 0, 0, Eastern())
|
||||||
|
|
||||||
|
shouldbe := 253 * 6.5 * 60 * 60 //253 full market days since this date last year.
|
||||||
|
assert.Equal(shouldbe, CalculateMarketSecondsBetween(start, end, NYSEOpen, NYSEClose, IsNYSEHoliday))
|
||||||
|
}
|
136
defaults.go
136
defaults.go
|
@ -2,7 +2,6 @@ package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/drawing"
|
"github.com/wcharczuk/go-chart/drawing"
|
||||||
|
@ -34,6 +33,9 @@ const (
|
||||||
// DefaultTitleTop is the default distance from the top of the chart to put the title.
|
// DefaultTitleTop is the default distance from the top of the chart to put the title.
|
||||||
DefaultTitleTop = 10
|
DefaultTitleTop = 10
|
||||||
|
|
||||||
|
// DefaultLineSpacing is the default vertical distance between lines of text.
|
||||||
|
DefaultLineSpacing = 5
|
||||||
|
|
||||||
// DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels.
|
// DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels.
|
||||||
DefaultYAxisMargin = 10
|
DefaultYAxisMargin = 10
|
||||||
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
|
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
|
||||||
|
@ -59,7 +61,7 @@ const (
|
||||||
// DefaultDateHourFormat is the date format for hour timestamp formats.
|
// DefaultDateHourFormat is the date format for hour timestamp formats.
|
||||||
DefaultDateHourFormat = "01-02 3PM"
|
DefaultDateHourFormat = "01-02 3PM"
|
||||||
// DefaultDateMinuteFormat is the date format for minute range timestamp formats.
|
// DefaultDateMinuteFormat is the date format for minute range timestamp formats.
|
||||||
DefaultDateMinuteFormat = time.Kitchen
|
DefaultDateMinuteFormat = "01-02 3:04PM"
|
||||||
// DefaultFloatFormat is the default float format.
|
// DefaultFloatFormat is the default float format.
|
||||||
DefaultFloatFormat = "%.2f"
|
DefaultFloatFormat = "%.2f"
|
||||||
// DefaultPercentValueFormat is the default percent format.
|
// DefaultPercentValueFormat is the default percent format.
|
||||||
|
@ -67,42 +69,89 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultBackgroundColor is the default chart background color.
|
// ColorWhite is white.
|
||||||
// It is equivalent to css color:white.
|
ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
DefaultBackgroundColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
// ColorBlue is the basic theme blue color.
|
||||||
// DefaultBackgroundStrokeColor is the default chart border color.
|
ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
|
||||||
// It is equivalent to color:white.
|
// ColorCyan is the basic theme cyan color.
|
||||||
DefaultBackgroundStrokeColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
|
||||||
// DefaultCanvasColor is the default chart canvas color.
|
// ColorGreen is the basic theme green color.
|
||||||
// It is equivalent to css color:white.
|
ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
|
||||||
DefaultCanvasColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
// ColorRed is the basic theme red color.
|
||||||
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
|
ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
|
||||||
// It is equivalent to css color:white.
|
// ColorOrange is the basic theme orange color.
|
||||||
DefaultCanvasStrokeColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
|
||||||
// DefaultTextColor is the default chart text color.
|
// ColorYellow is the basic theme yellow color.
|
||||||
// It is equivalent to #333333.
|
ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
|
||||||
DefaultTextColor = drawing.Color{R: 51, G: 51, B: 51, A: 255}
|
// ColorBlack is the basic theme black color.
|
||||||
// DefaultAxisColor is the default chart axis line color.
|
ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
|
||||||
// It is equivalent to #333333.
|
// ColorLightGray is the basic theme light gray color.
|
||||||
DefaultAxisColor = drawing.Color{R: 51, G: 51, B: 51, A: 255}
|
ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
|
||||||
// DefaultStrokeColor is the default chart border color.
|
|
||||||
// It is equivalent to #efefef.
|
// ColorAlternateBlue is a alternate theme color.
|
||||||
DefaultStrokeColor = drawing.Color{R: 239, G: 239, B: 239, A: 255}
|
ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
|
||||||
// DefaultFillColor is the default fill color.
|
// ColorAlternateGreen is a alternate theme color.
|
||||||
// It is equivalent to #0074d9.
|
ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
|
||||||
DefaultFillColor = drawing.Color{R: 0, G: 217, B: 116, A: 255}
|
// ColorAlternateGray is a alternate theme color.
|
||||||
// DefaultAnnotationFillColor is the default annotation background color.
|
ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
|
||||||
DefaultAnnotationFillColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
|
// ColorAlternateYellow is a alternate theme color.
|
||||||
// DefaultGridLineColor is the default grid line color.
|
ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
|
||||||
DefaultGridLineColor = drawing.Color{R: 239, G: 239, B: 239, A: 255}
|
// ColorAlternateLightGray is a alternate theme color.
|
||||||
|
ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// DefaultSeriesStrokeColors are a couple default series colors.
|
// DefaultBackgroundColor is the default chart background color.
|
||||||
DefaultSeriesStrokeColors = []drawing.Color{
|
// It is equivalent to css color:white.
|
||||||
{R: 0, G: 116, B: 217, A: 255},
|
DefaultBackgroundColor = ColorWhite
|
||||||
{R: 0, G: 217, B: 116, A: 255},
|
// DefaultBackgroundStrokeColor is the default chart border color.
|
||||||
{R: 217, G: 0, B: 116, A: 255},
|
// It is equivalent to color:white.
|
||||||
|
DefaultBackgroundStrokeColor = ColorWhite
|
||||||
|
// DefaultCanvasColor is the default chart canvas color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultCanvasColor = ColorWhite
|
||||||
|
// DefaultCanvasStrokeColor is the default chart canvas stroke color.
|
||||||
|
// It is equivalent to css color:white.
|
||||||
|
DefaultCanvasStrokeColor = ColorWhite
|
||||||
|
// DefaultTextColor is the default chart text color.
|
||||||
|
// It is equivalent to #333333.
|
||||||
|
DefaultTextColor = ColorBlack
|
||||||
|
// DefaultAxisColor is the default chart axis line color.
|
||||||
|
// It is equivalent to #333333.
|
||||||
|
DefaultAxisColor = ColorBlack
|
||||||
|
// DefaultStrokeColor is the default chart border color.
|
||||||
|
// It is equivalent to #efefef.
|
||||||
|
DefaultStrokeColor = ColorLightGray
|
||||||
|
// DefaultFillColor is the default fill color.
|
||||||
|
// It is equivalent to #0074d9.
|
||||||
|
DefaultFillColor = ColorBlue
|
||||||
|
// DefaultAnnotationFillColor is the default annotation background color.
|
||||||
|
DefaultAnnotationFillColor = ColorWhite
|
||||||
|
// DefaultGridLineColor is the default grid line color.
|
||||||
|
DefaultGridLineColor = ColorLightGray
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultColors are a couple default series colors.
|
||||||
|
DefaultColors = []drawing.Color{
|
||||||
|
ColorBlue,
|
||||||
|
ColorGreen,
|
||||||
|
ColorRed,
|
||||||
|
ColorCyan,
|
||||||
|
ColorOrange,
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAlternateColors are a couple alternate colors.
|
||||||
|
DefaultAlternateColors = []drawing.Color{
|
||||||
|
ColorAlternateBlue,
|
||||||
|
ColorAlternateGreen,
|
||||||
|
ColorAlternateGray,
|
||||||
|
ColorAlternateYellow,
|
||||||
|
ColorBlue,
|
||||||
|
ColorGreen,
|
||||||
|
ColorRed,
|
||||||
|
ColorCyan,
|
||||||
|
ColorOrange,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -117,11 +166,18 @@ var (
|
||||||
DashArrayDashesLarge = []int{10, 10}
|
DashArrayDashesLarge = []int{10, 10}
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetDefaultSeriesStrokeColor returns a color from the default list by index.
|
// GetDefaultColor returns a color from the default list by index.
|
||||||
// NOTE: the index will wrap around (using a modulo).g
|
// NOTE: the index will wrap around (using a modulo).
|
||||||
func GetDefaultSeriesStrokeColor(index int) drawing.Color {
|
func GetDefaultColor(index int) drawing.Color {
|
||||||
finalIndex := index % len(DefaultSeriesStrokeColors)
|
finalIndex := index % len(DefaultColors)
|
||||||
return DefaultSeriesStrokeColors[finalIndex]
|
return DefaultColors[finalIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlternateColor returns a color from the default list by index.
|
||||||
|
// NOTE: the index will wrap around (using a modulo).
|
||||||
|
func GetAlternateColor(index int) drawing.Color {
|
||||||
|
finalIndex := index % len(DefaultAlternateColors)
|
||||||
|
return DefaultAlternateColors[finalIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
256
draw.go
Normal file
256
draw.go
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Draw contains helpers for drawing common objects.
|
||||||
|
Draw = &draw{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type draw struct{}
|
||||||
|
|
||||||
|
// LineSeries draws a line series with a renderer.
|
||||||
|
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider) {
|
||||||
|
if vs.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
v0x, v0y := vs.GetValue(0)
|
||||||
|
x0 := cl + xrange.Translate(v0x)
|
||||||
|
y0 := cb - yrange.Translate(v0y)
|
||||||
|
|
||||||
|
var vx, vy float64
|
||||||
|
var x, y int
|
||||||
|
|
||||||
|
fill := style.GetFillColor()
|
||||||
|
if !fill.IsZero() {
|
||||||
|
r.SetFillColor(fill)
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValue(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.LineTo(x, cb)
|
||||||
|
r.LineTo(x0, cb)
|
||||||
|
r.Close()
|
||||||
|
r.Fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
style.GetStrokeOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < vs.Len(); i++ {
|
||||||
|
vx, vy = vs.GetValue(i)
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoundedSeries draws a series that implements BoundedValueProvider.
|
||||||
|
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
|
||||||
|
drawOffsetIndex := 0
|
||||||
|
if len(drawOffsetIndexes) > 0 {
|
||||||
|
drawOffsetIndex = drawOffsetIndexes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
v0x, v0y1, v0y2 := bbs.GetBoundedValue(0)
|
||||||
|
x0 := cl + xrange.Translate(v0x)
|
||||||
|
y0 := cb - yrange.Translate(v0y1)
|
||||||
|
|
||||||
|
var vx, vy1, vy2 float64
|
||||||
|
var x, y int
|
||||||
|
|
||||||
|
xvalues := make([]float64, bbs.Len())
|
||||||
|
xvalues[0] = v0x
|
||||||
|
y2values := make([]float64, bbs.Len())
|
||||||
|
y2values[0] = v0y2
|
||||||
|
|
||||||
|
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
r.MoveTo(x0, y0)
|
||||||
|
for i := 1; i < bbs.Len(); i++ {
|
||||||
|
vx, vy1, vy2 = bbs.GetBoundedValue(i)
|
||||||
|
|
||||||
|
xvalues[i] = vx
|
||||||
|
y2values[i] = vy2
|
||||||
|
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy1)
|
||||||
|
if i > drawOffsetIndex {
|
||||||
|
r.LineTo(x, y)
|
||||||
|
} else {
|
||||||
|
r.MoveTo(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y = cb - yrange.Translate(vy2)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
for i := bbs.Len() - 1; i >= drawOffsetIndex; i-- {
|
||||||
|
vx, vy2 = xvalues[i], y2values[i]
|
||||||
|
x = cl + xrange.Translate(vx)
|
||||||
|
y = cb - yrange.Translate(vy2)
|
||||||
|
r.LineTo(x, y)
|
||||||
|
}
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HistogramSeries draws a value provider as boxes from 0.
|
||||||
|
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider, barWidths ...int) {
|
||||||
|
if vs.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//calculate bar width?
|
||||||
|
seriesLength := vs.Len()
|
||||||
|
barWidth := int(math.Floor(float64(xrange.GetDomain()) / float64(seriesLength)))
|
||||||
|
if len(barWidths) > 0 {
|
||||||
|
barWidth = barWidths[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
//foreach datapoint, draw a box.
|
||||||
|
for index := 0; index < seriesLength; index++ {
|
||||||
|
vx, vy := vs.GetValue(index)
|
||||||
|
y0 := yrange.Translate(0)
|
||||||
|
x := cl + xrange.Translate(vx)
|
||||||
|
y := yrange.Translate(vy)
|
||||||
|
|
||||||
|
d.Box(r, Box{
|
||||||
|
Top: cb - y0,
|
||||||
|
Left: x - (barWidth >> 1),
|
||||||
|
Right: x + (barWidth >> 1),
|
||||||
|
Bottom: cb - y,
|
||||||
|
}, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeasureAnnotation measures how big an annotation would be.
|
||||||
|
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
|
||||||
|
style.WriteToRenderer(r)
|
||||||
|
|
||||||
|
textBox := r.MeasureText(label)
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
textHeight := textBox.Height()
|
||||||
|
halfTextHeight := textHeight >> 1
|
||||||
|
|
||||||
|
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||||
|
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
||||||
|
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
|
||||||
|
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
||||||
|
|
||||||
|
strokeWidth := style.GetStrokeWidth()
|
||||||
|
|
||||||
|
top := ly - (pt + halfTextHeight)
|
||||||
|
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
|
||||||
|
bottom := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: top,
|
||||||
|
Left: lx,
|
||||||
|
Right: right,
|
||||||
|
Bottom: bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotation draws an anotation with a renderer.
|
||||||
|
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
|
||||||
|
style.WriteToRenderer(r)
|
||||||
|
|
||||||
|
textBox := r.MeasureText(label)
|
||||||
|
textWidth := textBox.Width()
|
||||||
|
halfTextHeight := textBox.Height() >> 1
|
||||||
|
|
||||||
|
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||||
|
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
||||||
|
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
|
||||||
|
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
||||||
|
|
||||||
|
textX := lx + pl + DefaultAnnotationDeltaWidth
|
||||||
|
textY := ly + halfTextHeight
|
||||||
|
|
||||||
|
ltx := lx + DefaultAnnotationDeltaWidth
|
||||||
|
lty := ly - (pt + halfTextHeight)
|
||||||
|
|
||||||
|
rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
||||||
|
rty := ly - (pt + halfTextHeight)
|
||||||
|
|
||||||
|
rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
||||||
|
rby := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
lbx := lx + DefaultAnnotationDeltaWidth
|
||||||
|
lby := ly + (pb + halfTextHeight)
|
||||||
|
|
||||||
|
r.MoveTo(lx, ly)
|
||||||
|
r.LineTo(ltx, lty)
|
||||||
|
r.LineTo(rtx, rty)
|
||||||
|
r.LineTo(rbx, rby)
|
||||||
|
r.LineTo(lbx, lby)
|
||||||
|
r.LineTo(lx, ly)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
|
||||||
|
r.Text(label, textX, textY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box draws a box with a given style.
|
||||||
|
func (d draw) Box(r Renderer, b Box, s Style) {
|
||||||
|
s.WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(b.Left, b.Top)
|
||||||
|
r.LineTo(b.Right, b.Top)
|
||||||
|
r.LineTo(b.Right, b.Bottom)
|
||||||
|
r.LineTo(b.Left, b.Bottom)
|
||||||
|
r.LineTo(b.Left, b.Top)
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawText draws text with a given style.
|
||||||
|
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
r.Text(text, x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextWithin draws the text within a given box.
|
||||||
|
func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
||||||
|
lines := Text.WrapFit(r, text, box.Width(), style)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, style)
|
||||||
|
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
y := box.Top
|
||||||
|
|
||||||
|
switch style.GetTextVerticalAlign() {
|
||||||
|
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
|
||||||
|
y = y - linesBox.Height()
|
||||||
|
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
|
||||||
|
y = (y - linesBox.Height()) >> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var tx, ty int
|
||||||
|
for _, line := range lines {
|
||||||
|
lineBox := r.MeasureText(line)
|
||||||
|
switch style.GetTextHorizontalAlign() {
|
||||||
|
case TextHorizontalAlignCenter:
|
||||||
|
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
|
||||||
|
case TextHorizontalAlignRight:
|
||||||
|
tx = box.Right - lineBox.Width()
|
||||||
|
default:
|
||||||
|
tx = box.Left
|
||||||
|
}
|
||||||
|
ty = y + lineBox.Height()
|
||||||
|
|
||||||
|
d.Text(r, line, tx, ty, style)
|
||||||
|
y += lineBox.Height() + style.GetTextLineSpacing()
|
||||||
|
}
|
||||||
|
}
|
|
@ -101,10 +101,10 @@ func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArcTo adds an arc to the path
|
// ArcTo adds an arc to the path
|
||||||
func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float64) {
|
func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
|
||||||
endAngle := startAngle + angle
|
endAngle := startAngle + delta
|
||||||
clockWise := true
|
clockWise := true
|
||||||
if angle < 0 {
|
if delta < 0 {
|
||||||
clockWise = false
|
clockWise = false
|
||||||
}
|
}
|
||||||
// normalize
|
// normalize
|
||||||
|
@ -124,7 +124,7 @@ func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, angle float64) {
|
||||||
} else {
|
} else {
|
||||||
p.MoveTo(startX, startY)
|
p.MoveTo(startX, startY)
|
||||||
}
|
}
|
||||||
p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, angle)
|
p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, delta)
|
||||||
p.x = cx + math.Cos(endAngle)*rx
|
p.x = cx + math.Cos(endAngle)*rx
|
||||||
p.y = cy + math.Sin(endAngle)*ry
|
p.y = cy + math.Sin(endAngle)*ry
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,8 +171,8 @@ func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ArcTo draws an arc.
|
// ArcTo draws an arc.
|
||||||
func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, angle float64) {
|
func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
|
||||||
gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, angle)
|
gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes a path.
|
// Close closes a path.
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
// Copyright 2010 The draw2d Authors. All rights reserved.
|
|
||||||
// created: 13/12/2010 by Laurent Le Goff
|
|
||||||
|
|
||||||
package drawing
|
package drawing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -1,372 +0,0 @@
|
||||||
package chart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/wcharczuk/go-chart/drawing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DrawLineSeries draws a line series with a renderer.
|
|
||||||
func DrawLineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider) {
|
|
||||||
if vs.Len() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cb := canvasBox.Bottom
|
|
||||||
cl := canvasBox.Left
|
|
||||||
|
|
||||||
v0x, v0y := vs.GetValue(0)
|
|
||||||
x0 := cl + xrange.Translate(v0x)
|
|
||||||
y0 := cb - yrange.Translate(v0y)
|
|
||||||
|
|
||||||
var vx, vy float64
|
|
||||||
var x, y int
|
|
||||||
|
|
||||||
fill := s.GetFillColor()
|
|
||||||
if !fill.IsZero() {
|
|
||||||
r.SetFillColor(fill)
|
|
||||||
r.MoveTo(x0, y0)
|
|
||||||
for i := 1; i < vs.Len(); i++ {
|
|
||||||
vx, vy = vs.GetValue(i)
|
|
||||||
x = cl + xrange.Translate(vx)
|
|
||||||
y = cb - yrange.Translate(vy)
|
|
||||||
r.LineTo(x, y)
|
|
||||||
}
|
|
||||||
r.LineTo(x, cb)
|
|
||||||
r.LineTo(x0, cb)
|
|
||||||
r.Close()
|
|
||||||
r.Fill()
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
|
||||||
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
|
||||||
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
|
|
||||||
|
|
||||||
r.MoveTo(x0, y0)
|
|
||||||
for i := 1; i < vs.Len(); i++ {
|
|
||||||
vx, vy = vs.GetValue(i)
|
|
||||||
x = cl + xrange.Translate(vx)
|
|
||||||
y = cb - yrange.Translate(vy)
|
|
||||||
r.LineTo(x, y)
|
|
||||||
}
|
|
||||||
r.Stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawBoundedSeries draws a series that implements BoundedValueProvider.
|
|
||||||
func DrawBoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
|
|
||||||
drawOffsetIndex := 0
|
|
||||||
if len(drawOffsetIndexes) > 0 {
|
|
||||||
drawOffsetIndex = drawOffsetIndexes[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
|
||||||
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
|
||||||
r.SetStrokeWidth(s.GetStrokeWidth())
|
|
||||||
r.SetFillColor(s.GetFillColor())
|
|
||||||
|
|
||||||
cb := canvasBox.Bottom
|
|
||||||
cl := canvasBox.Left
|
|
||||||
|
|
||||||
v0x, v0y1, v0y2 := bbs.GetBoundedValue(0)
|
|
||||||
x0 := cl + xrange.Translate(v0x)
|
|
||||||
y0 := cb - yrange.Translate(v0y1)
|
|
||||||
|
|
||||||
var vx, vy1, vy2 float64
|
|
||||||
var x, y int
|
|
||||||
|
|
||||||
xvalues := make([]float64, bbs.Len())
|
|
||||||
xvalues[0] = v0x
|
|
||||||
y2values := make([]float64, bbs.Len())
|
|
||||||
y2values[0] = v0y2
|
|
||||||
|
|
||||||
r.MoveTo(x0, y0)
|
|
||||||
for i := 1; i < bbs.Len(); i++ {
|
|
||||||
vx, vy1, vy2 = bbs.GetBoundedValue(i)
|
|
||||||
|
|
||||||
xvalues[i] = vx
|
|
||||||
y2values[i] = vy2
|
|
||||||
|
|
||||||
x = cl + xrange.Translate(vx)
|
|
||||||
y = cb - yrange.Translate(vy1)
|
|
||||||
if i > drawOffsetIndex {
|
|
||||||
r.LineTo(x, y)
|
|
||||||
} else {
|
|
||||||
r.MoveTo(x, y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y = cb - yrange.Translate(vy2)
|
|
||||||
r.LineTo(x, y)
|
|
||||||
for i := bbs.Len() - 1; i >= drawOffsetIndex; i-- {
|
|
||||||
vx, vy2 = xvalues[i], y2values[i]
|
|
||||||
x = cl + xrange.Translate(vx)
|
|
||||||
y = cb - yrange.Translate(vy2)
|
|
||||||
r.LineTo(x, y)
|
|
||||||
}
|
|
||||||
r.Close()
|
|
||||||
r.FillStroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawHistogramSeries draws a value provider as boxes from 0.
|
|
||||||
func DrawHistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider, barWidths ...int) {
|
|
||||||
if vs.Len() == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//calculate bar width?
|
|
||||||
seriesLength := vs.Len()
|
|
||||||
barWidth := int(math.Floor(float64(xrange.Domain) / float64(seriesLength)))
|
|
||||||
if len(barWidths) > 0 {
|
|
||||||
barWidth = barWidths[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
cb := canvasBox.Bottom
|
|
||||||
cl := canvasBox.Left
|
|
||||||
|
|
||||||
//foreach datapoint, draw a box.
|
|
||||||
for index := 0; index < seriesLength; index++ {
|
|
||||||
vx, vy := vs.GetValue(index)
|
|
||||||
y0 := yrange.Translate(0)
|
|
||||||
x := cl + xrange.Translate(vx)
|
|
||||||
y := yrange.Translate(vy)
|
|
||||||
|
|
||||||
DrawBox(r, Box{
|
|
||||||
Top: cb - y0,
|
|
||||||
Left: x - (barWidth >> 1),
|
|
||||||
Right: x + (barWidth >> 1),
|
|
||||||
Bottom: cb - y,
|
|
||||||
}, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MeasureAnnotation measures how big an annotation would be.
|
|
||||||
func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string) Box {
|
|
||||||
r.SetFillColor(s.GetFillColor(DefaultAnnotationFillColor))
|
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
|
||||||
r.SetStrokeWidth(s.GetStrokeWidth())
|
|
||||||
r.SetFont(s.GetFont())
|
|
||||||
r.SetFontColor(s.GetFontColor(DefaultTextColor))
|
|
||||||
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
|
|
||||||
|
|
||||||
textBox := r.MeasureText(label)
|
|
||||||
textWidth := textBox.Width()
|
|
||||||
textHeight := textBox.Height()
|
|
||||||
halfTextHeight := textHeight >> 1
|
|
||||||
|
|
||||||
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
|
|
||||||
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
|
||||||
pr := s.Padding.GetRight(DefaultAnnotationPadding.Right)
|
|
||||||
pb := s.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
|
||||||
|
|
||||||
strokeWidth := s.GetStrokeWidth()
|
|
||||||
|
|
||||||
top := ly - (pt + halfTextHeight)
|
|
||||||
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
|
|
||||||
bottom := ly + (pb + halfTextHeight)
|
|
||||||
|
|
||||||
return Box{
|
|
||||||
Top: top,
|
|
||||||
Left: lx,
|
|
||||||
Right: right,
|
|
||||||
Bottom: bottom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawAnnotation draws an anotation with a renderer.
|
|
||||||
func DrawAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
|
|
||||||
r.SetFillColor(style.GetFillColor())
|
|
||||||
r.SetStrokeColor(style.GetStrokeColor())
|
|
||||||
r.SetStrokeWidth(style.GetStrokeWidth())
|
|
||||||
r.SetStrokeDashArray(style.GetStrokeDashArray())
|
|
||||||
|
|
||||||
r.SetFont(style.GetFont())
|
|
||||||
r.SetFontColor(style.GetFontColor(DefaultTextColor))
|
|
||||||
r.SetFontSize(style.GetFontSize(DefaultAnnotationFontSize))
|
|
||||||
|
|
||||||
textBox := r.MeasureText(label)
|
|
||||||
textWidth := textBox.Width()
|
|
||||||
halfTextHeight := textBox.Height() >> 1
|
|
||||||
|
|
||||||
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
|
||||||
pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
|
||||||
pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
|
|
||||||
pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
|
||||||
|
|
||||||
textX := lx + pl + DefaultAnnotationDeltaWidth
|
|
||||||
textY := ly + halfTextHeight
|
|
||||||
|
|
||||||
ltx := lx + DefaultAnnotationDeltaWidth
|
|
||||||
lty := ly - (pt + halfTextHeight)
|
|
||||||
|
|
||||||
rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
|
||||||
rty := ly - (pt + halfTextHeight)
|
|
||||||
|
|
||||||
rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
|
|
||||||
rby := ly + (pb + halfTextHeight)
|
|
||||||
|
|
||||||
lbx := lx + DefaultAnnotationDeltaWidth
|
|
||||||
lby := ly + (pb + halfTextHeight)
|
|
||||||
|
|
||||||
r.MoveTo(lx, ly)
|
|
||||||
r.LineTo(ltx, lty)
|
|
||||||
r.LineTo(rtx, rty)
|
|
||||||
r.LineTo(rbx, rby)
|
|
||||||
r.LineTo(lbx, lby)
|
|
||||||
r.LineTo(lx, ly)
|
|
||||||
r.Close()
|
|
||||||
r.FillStroke()
|
|
||||||
|
|
||||||
r.Text(label, textX, textY)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawBox draws a box with a given style.
|
|
||||||
func DrawBox(r Renderer, b Box, s Style) {
|
|
||||||
r.SetFillColor(s.GetFillColor())
|
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
|
||||||
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
|
|
||||||
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
|
||||||
|
|
||||||
r.MoveTo(b.Left, b.Top)
|
|
||||||
r.LineTo(b.Right, b.Top)
|
|
||||||
r.LineTo(b.Right, b.Bottom)
|
|
||||||
r.LineTo(b.Left, b.Bottom)
|
|
||||||
r.LineTo(b.Left, b.Top)
|
|
||||||
r.FillStroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawText draws text with a given style.
|
|
||||||
func DrawText(r Renderer, text string, x, y int, s Style) {
|
|
||||||
r.SetFontColor(s.GetFontColor(DefaultTextColor))
|
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
|
||||||
r.SetStrokeWidth(s.GetStrokeWidth())
|
|
||||||
r.SetFont(s.GetFont())
|
|
||||||
r.SetFontSize(s.GetFontSize())
|
|
||||||
|
|
||||||
r.Text(text, x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawTextCentered draws text with a given style centered.
|
|
||||||
func DrawTextCentered(r Renderer, text string, x, y int, s Style) {
|
|
||||||
r.SetFontColor(s.GetFontColor(DefaultTextColor))
|
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
|
||||||
r.SetStrokeWidth(s.GetStrokeWidth())
|
|
||||||
r.SetFont(s.GetFont())
|
|
||||||
r.SetFontSize(s.GetFontSize())
|
|
||||||
|
|
||||||
tb := r.MeasureText(text)
|
|
||||||
tx := x - (tb.Width() >> 1)
|
|
||||||
ty := y - (tb.Height() >> 1)
|
|
||||||
r.Text(text, tx, ty)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateLegend returns a legend renderable function.
|
|
||||||
func CreateLegend(c *Chart, userDefaults ...Style) Renderable {
|
|
||||||
return func(r Renderer, cb Box, chartDefaults Style) {
|
|
||||||
legendDefaults := Style{
|
|
||||||
FillColor: drawing.ColorWhite,
|
|
||||||
FontColor: DefaultTextColor,
|
|
||||||
FontSize: 8.0,
|
|
||||||
StrokeColor: DefaultAxisColor,
|
|
||||||
StrokeWidth: DefaultAxisLineWidth,
|
|
||||||
}
|
|
||||||
|
|
||||||
var legendStyle Style
|
|
||||||
if len(userDefaults) > 0 {
|
|
||||||
legendStyle = userDefaults[0].WithDefaultsFrom(chartDefaults.WithDefaultsFrom(legendDefaults))
|
|
||||||
} else {
|
|
||||||
legendStyle = chartDefaults.WithDefaultsFrom(legendDefaults)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEFAULTS
|
|
||||||
legendPadding := Box{
|
|
||||||
Top: 5,
|
|
||||||
Left: 5,
|
|
||||||
Right: 5,
|
|
||||||
Bottom: 5,
|
|
||||||
}
|
|
||||||
lineTextGap := 5
|
|
||||||
lineLengthMinimum := 25
|
|
||||||
|
|
||||||
var labels []string
|
|
||||||
var lines []Style
|
|
||||||
for index, s := range c.Series {
|
|
||||||
if s.GetStyle().IsZero() || s.GetStyle().Show {
|
|
||||||
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
|
|
||||||
labels = append(labels, s.GetName())
|
|
||||||
lines = append(lines, s.GetStyle().WithDefaultsFrom(c.styleDefaultsSeries(index)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
legend := Box{
|
|
||||||
Top: cb.Top,
|
|
||||||
Left: cb.Left,
|
|
||||||
// bottom and right will be sized by the legend content + relevant padding.
|
|
||||||
}
|
|
||||||
|
|
||||||
legendContent := Box{
|
|
||||||
Top: legend.Top + legendPadding.Top,
|
|
||||||
Left: legend.Left + legendPadding.Left,
|
|
||||||
Right: legend.Left + legendPadding.Left,
|
|
||||||
Bottom: legend.Top + legendPadding.Top,
|
|
||||||
}
|
|
||||||
|
|
||||||
r.SetFont(legendStyle.GetFont())
|
|
||||||
r.SetFontColor(legendStyle.GetFontColor())
|
|
||||||
r.SetFontSize(legendStyle.GetFontSize())
|
|
||||||
|
|
||||||
// measure
|
|
||||||
labelCount := 0
|
|
||||||
for x := 0; x < len(labels); x++ {
|
|
||||||
if len(labels[x]) > 0 {
|
|
||||||
tb := r.MeasureText(labels[x])
|
|
||||||
if labelCount > 0 {
|
|
||||||
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
|
||||||
}
|
|
||||||
legendContent.Bottom += tb.Height()
|
|
||||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
|
||||||
legendContent.Right = MaxInt(legendContent.Right, right)
|
|
||||||
labelCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
legend = legend.Grow(legendContent)
|
|
||||||
legend.Right = legendContent.Right + legendPadding.Right
|
|
||||||
legend.Bottom = legendContent.Bottom + legendPadding.Bottom
|
|
||||||
|
|
||||||
DrawBox(r, legend, legendStyle)
|
|
||||||
|
|
||||||
ycursor := legendContent.Top
|
|
||||||
tx := legendContent.Left
|
|
||||||
legendCount := 0
|
|
||||||
for x := 0; x < len(labels); x++ {
|
|
||||||
if len(labels[x]) > 0 {
|
|
||||||
|
|
||||||
if legendCount > 0 {
|
|
||||||
ycursor += DefaultMinimumTickVerticalSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
tb := r.MeasureText(labels[x])
|
|
||||||
|
|
||||||
ty := ycursor + tb.Height()
|
|
||||||
r.Text(labels[x], tx, ty)
|
|
||||||
|
|
||||||
th2 := tb.Height() >> 1
|
|
||||||
|
|
||||||
lx := tx + tb.Width() + lineTextGap
|
|
||||||
ly := ty - th2
|
|
||||||
lx2 := legendContent.Right - legendPadding.Right
|
|
||||||
|
|
||||||
r.SetStrokeColor(lines[x].GetStrokeColor())
|
|
||||||
r.SetStrokeWidth(lines[x].GetStrokeWidth())
|
|
||||||
r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
|
|
||||||
|
|
||||||
r.MoveTo(lx, ly)
|
|
||||||
r.LineTo(lx2, ly)
|
|
||||||
r.Stroke()
|
|
||||||
|
|
||||||
ycursor += tb.Height()
|
|
||||||
legendCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,7 @@ const (
|
||||||
type EMASeries struct {
|
type EMASeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
|
|
||||||
Period int
|
Period int
|
||||||
InnerSeries ValueProvider
|
InnerSeries ValueProvider
|
||||||
|
@ -28,7 +28,7 @@ func (ema EMASeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (ema EMASeries) GetYAxis() YAxisType {
|
func (ema EMASeries) GetYAxis() yAxisType {
|
||||||
return ema.YAxis
|
return ema.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +96,6 @@ func (ema *EMASeries) ensureCachedValues() {
|
||||||
|
|
||||||
// Render renders the series.
|
// Render renders the series.
|
||||||
func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
style := ema.Style.WithDefaultsFrom(defaults)
|
style := ema.Style.InheritFrom(defaults)
|
||||||
DrawLineSeries(r, canvasBox, xrange, yrange, style, ema)
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
emaXValues = Seq(1.0, 50.0)
|
emaXValues = Sequence.Float64(1.0, 50.0)
|
||||||
emaYValues = []float64{
|
emaYValues = []float64{
|
||||||
1, 2, 3, 4, 5, 4, 3, 2,
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
1, 2, 3, 4, 5, 4, 3, 2,
|
1, 2, 3, 4, 5, 4, 3, 2,
|
||||||
|
|
|
@ -26,12 +26,12 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
},
|
},
|
||||||
chart.AnnotationSeries{
|
chart.AnnotationSeries{
|
||||||
Annotations: []chart.Annotation{
|
Annotations: []chart.Value2{
|
||||||
{X: 1.0, Y: 1.0, Label: "One"},
|
{XValue: 1.0, YValue: 1.0, Label: "One"},
|
||||||
{X: 2.0, Y: 2.0, Label: "Two"},
|
{XValue: 2.0, YValue: 2.0, Label: "Two"},
|
||||||
{X: 3.0, Y: 3.0, Label: "Three"},
|
{XValue: 3.0, YValue: 3.0, Label: "Three"},
|
||||||
{X: 4.0, Y: 4.0, Label: "Four"},
|
{XValue: 4.0, YValue: 4.0, Label: "Four"},
|
||||||
{X: 5.0, Y: 5.0, Label: "Five"},
|
{XValue: 5.0, YValue: 5.0, Label: "Five"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,8 +28,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
chart.ContinuousSeries{
|
chart.ContinuousSeries{
|
||||||
Style: chart.Style{
|
Style: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
StrokeColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64),
|
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64),
|
FillColor: chart.GetDefaultColor(0).WithAlpha(64),
|
||||||
},
|
},
|
||||||
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
XValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
YValues: []float64{1.0, 2.0, 3.0, 4.0, 5.0},
|
||||||
|
|
|
@ -30,8 +30,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
Series: []chart.Series{
|
Series: []chart.Series{
|
||||||
chart.ContinuousSeries{
|
chart.ContinuousSeries{
|
||||||
XValues: chart.Seq(1.0, 100.0),
|
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||||
YValues: chart.SeqRand(100.0, 256.0),
|
YValues: chart.Sequence.Random(100.0, 256.0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,8 @@ func drawChartDefault(res http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
Series: []chart.Series{
|
Series: []chart.Series{
|
||||||
chart.ContinuousSeries{
|
chart.ContinuousSeries{
|
||||||
XValues: chart.Seq(1.0, 100.0),
|
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||||
YValues: chart.SeqRand(100.0, 256.0),
|
YValues: chart.Sequence.Random(100.0, 256.0),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
Style: chart.Style{
|
Style: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
},
|
},
|
||||||
Range: chart.Range{
|
Range: &chart.ContinuousRange{
|
||||||
Min: 0.0,
|
Min: 0.0,
|
||||||
Max: 10.0,
|
Max: 10.0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
Style: chart.Style{
|
Style: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
},
|
},
|
||||||
Range: chart.Range{
|
Range: &chart.ContinuousRange{
|
||||||
Min: 0.0,
|
Min: 0.0,
|
||||||
Max: 4.0,
|
Max: 4.0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,7 +38,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
//note we have to do this as a separate step because we need a reference to graph
|
//note we have to do this as a separate step because we need a reference to graph
|
||||||
graph.Elements = []chart.Renderable{
|
graph.Elements = []chart.Renderable{
|
||||||
chart.CreateLegend(&graph),
|
chart.Legend(&graph),
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Header().Set("Content-Type", "image/png")
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
|
42
examples/linear_regression/main.go
Normal file
42
examples/linear_regression/main.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart"
|
||||||
|
)
|
||||||
|
|
||||||
|
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
In this example we add a new type of series, a `SimpleMovingAverageSeries` that takes another series as a required argument.
|
||||||
|
InnerSeries only needs to implement `ValueProvider`, so really you could chain `SimpleMovingAverageSeries` together if you wanted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mainSeries := chart.ContinuousSeries{
|
||||||
|
Name: "A test series",
|
||||||
|
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||||
|
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||||
|
}
|
||||||
|
|
||||||
|
// note we create a LinearRegressionSeries series by assignin the inner series.
|
||||||
|
// we need to use a reference because `.Render()` needs to modify state within the series.
|
||||||
|
linRegSeries := &chart.LinearRegressionSeries{
|
||||||
|
InnerSeries: mainSeries,
|
||||||
|
} // we can optionally set the `WindowSize` property which alters how the moving average is calculated.
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
Series: []chart.Series{
|
||||||
|
mainSeries,
|
||||||
|
linRegSeries,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
graph.Render(chart.PNG, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", drawChart)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
40
examples/pie_chart/main.go
Normal file
40
examples/pie_chart/main.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart"
|
||||||
|
)
|
||||||
|
|
||||||
|
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
pie := chart.PieChart{
|
||||||
|
Title: "test\nchart",
|
||||||
|
TitleStyle: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
Width: 512,
|
||||||
|
Height: 512,
|
||||||
|
Values: []chart.Value{
|
||||||
|
{Value: 5, Label: "Blue"},
|
||||||
|
{Value: 5, Label: "Green"},
|
||||||
|
{Value: 4, Label: "Gray"},
|
||||||
|
{Value: 4, Label: "Orange"},
|
||||||
|
{Value: 3, Label: "Test"},
|
||||||
|
{Value: 3, Label: "??"},
|
||||||
|
{Value: 1, Label: "!!"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
err := pie.Render(chart.PNG, res)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error rendering pie chart: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", drawChart)
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
|
@ -15,8 +15,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
mainSeries := chart.ContinuousSeries{
|
mainSeries := chart.ContinuousSeries{
|
||||||
Name: "A test series",
|
Name: "A test series",
|
||||||
XValues: chart.Seq(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
XValues: chart.Sequence.Float64(1.0, 100.0), //generates a []float64 from 1.0 to 100.0 in 1.0 step increments, or 100 elements.
|
||||||
YValues: chart.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
YValues: chart.Sequence.Random(100, 100), //generates a []float64 randomly from 0 to 100 with 100 elements.
|
||||||
}
|
}
|
||||||
|
|
||||||
// note we create a SimpleMovingAverage series by assignin the inner series.
|
// note we create a SimpleMovingAverage series by assignin the inner series.
|
||||||
|
|
62
examples/stacked_bar/main.go
Normal file
62
examples/stacked_bar/main.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart"
|
||||||
|
)
|
||||||
|
|
||||||
|
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
sbc := chart.StackedBarChart{
|
||||||
|
Height: 512,
|
||||||
|
XAxis: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
YAxis: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
|
Bars: []chart.StackedBar{
|
||||||
|
{
|
||||||
|
Name: "This is a very long string to test word break wrapping.",
|
||||||
|
Values: []chart.Value{
|
||||||
|
{Value: 5, Label: "Blue"},
|
||||||
|
{Value: 5, Label: "Green"},
|
||||||
|
{Value: 4, Label: "Gray"},
|
||||||
|
{Value: 3, Label: "Orange"},
|
||||||
|
{Value: 3, Label: "Test"},
|
||||||
|
{Value: 2, Label: "??"},
|
||||||
|
{Value: 1, Label: "!!"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Test",
|
||||||
|
Values: []chart.Value{
|
||||||
|
{Value: 10, Label: "Blue"},
|
||||||
|
{Value: 5, Label: "Green"},
|
||||||
|
{Value: 1, Label: "Gray"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Test 2",
|
||||||
|
Values: []chart.Value{
|
||||||
|
{Value: 10, Label: "Blue"},
|
||||||
|
{Value: 6, Label: "Green"},
|
||||||
|
{Value: 4, Label: "Gray"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
err := sbc.Render(chart.PNG, res)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error rendering chart: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", drawChart)
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
Name: "SPY",
|
Name: "SPY",
|
||||||
Style: chart.Style{
|
Style: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
StrokeColor: chart.GetDefaultSeriesStrokeColor(0),
|
StrokeColor: chart.GetDefaultColor(0),
|
||||||
},
|
},
|
||||||
XValues: xv,
|
XValues: xv,
|
||||||
YValues: yv,
|
YValues: yv,
|
||||||
|
@ -44,10 +44,11 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
graph := chart.Chart{
|
graph := chart.Chart{
|
||||||
XAxis: chart.XAxis{
|
XAxis: chart.XAxis{
|
||||||
Style: chart.Style{Show: true},
|
Style: chart.Style{Show: true},
|
||||||
|
TickPosition: chart.TickPositionBetweenTicks,
|
||||||
},
|
},
|
||||||
YAxis: chart.YAxis{
|
YAxis: chart.YAxis{
|
||||||
Style: chart.Style{Show: true},
|
Style: chart.Style{Show: true},
|
||||||
Range: chart.Range{
|
Range: &chart.ContinuousRange{
|
||||||
Max: 220.0,
|
Max: 220.0,
|
||||||
Min: 180.0,
|
Min: 180.0,
|
||||||
},
|
},
|
||||||
|
|
66
grid_line.go
66
grid_line.go
|
@ -1,31 +1,8 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
// GenerateGridLines generates grid lines.
|
// GridLineProvider is a type that provides grid lines.
|
||||||
func GenerateGridLines(ticks []Tick, isVertical bool) []GridLine {
|
type GridLineProvider interface {
|
||||||
var gl []GridLine
|
GetGridLines(ticks []Tick, isVertical bool, majorStyle, minorStyle Style) []GridLine
|
||||||
isMinor := false
|
|
||||||
minorStyle := Style{
|
|
||||||
StrokeColor: DefaultGridLineColor.WithAlpha(100),
|
|
||||||
StrokeWidth: 1.0,
|
|
||||||
}
|
|
||||||
majorStyle := Style{
|
|
||||||
StrokeColor: DefaultGridLineColor,
|
|
||||||
StrokeWidth: 1.0,
|
|
||||||
}
|
|
||||||
for _, t := range ticks {
|
|
||||||
s := majorStyle
|
|
||||||
if isMinor {
|
|
||||||
s = minorStyle
|
|
||||||
}
|
|
||||||
gl = append(gl, GridLine{
|
|
||||||
Style: s,
|
|
||||||
IsMinor: isMinor,
|
|
||||||
IsVertical: isVertical,
|
|
||||||
Value: t.Value,
|
|
||||||
})
|
|
||||||
isMinor = !isMinor
|
|
||||||
}
|
|
||||||
return gl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GridLine is a line on a graph canvas.
|
// GridLine is a line on a graph canvas.
|
||||||
|
@ -57,15 +34,16 @@ func (gl GridLine) Horizontal() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders the gridline
|
// Render renders the gridline
|
||||||
func (gl GridLine) Render(r Renderer, canvasBox Box, ra Range) {
|
func (gl GridLine) Render(r Renderer, canvasBox Box, ra Range, defaults Style) {
|
||||||
|
r.SetStrokeColor(gl.Style.GetStrokeColor(defaults.GetStrokeColor()))
|
||||||
|
r.SetStrokeWidth(gl.Style.GetStrokeWidth(defaults.GetStrokeWidth()))
|
||||||
|
r.SetStrokeDashArray(gl.Style.GetStrokeDashArray(defaults.GetStrokeDashArray()))
|
||||||
|
|
||||||
if gl.IsVertical {
|
if gl.IsVertical {
|
||||||
lineLeft := canvasBox.Left + ra.Translate(gl.Value)
|
lineLeft := canvasBox.Left + ra.Translate(gl.Value)
|
||||||
lineBottom := canvasBox.Bottom
|
lineBottom := canvasBox.Bottom
|
||||||
lineTop := canvasBox.Top
|
lineTop := canvasBox.Top
|
||||||
|
|
||||||
r.SetStrokeColor(gl.Style.GetStrokeColor(DefaultAxisColor))
|
|
||||||
r.SetStrokeWidth(gl.Style.GetStrokeWidth(DefaultAxisLineWidth))
|
|
||||||
|
|
||||||
r.MoveTo(lineLeft, lineBottom)
|
r.MoveTo(lineLeft, lineBottom)
|
||||||
r.LineTo(lineLeft, lineTop)
|
r.LineTo(lineLeft, lineTop)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
@ -74,11 +52,33 @@ func (gl GridLine) Render(r Renderer, canvasBox Box, ra Range) {
|
||||||
lineRight := canvasBox.Right
|
lineRight := canvasBox.Right
|
||||||
lineHeight := canvasBox.Bottom - ra.Translate(gl.Value)
|
lineHeight := canvasBox.Bottom - ra.Translate(gl.Value)
|
||||||
|
|
||||||
r.SetStrokeColor(gl.Style.GetStrokeColor(DefaultAxisColor))
|
|
||||||
r.SetStrokeWidth(gl.Style.GetStrokeWidth(DefaultAxisLineWidth))
|
|
||||||
|
|
||||||
r.MoveTo(lineLeft, lineHeight)
|
r.MoveTo(lineLeft, lineHeight)
|
||||||
r.LineTo(lineRight, lineHeight)
|
r.LineTo(lineRight, lineHeight)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateGridLines generates grid lines.
|
||||||
|
func GenerateGridLines(ticks []Tick, majorStyle, minorStyle Style, isVertical bool) []GridLine {
|
||||||
|
var gl []GridLine
|
||||||
|
isMinor := false
|
||||||
|
|
||||||
|
if len(ticks) < 3 {
|
||||||
|
return gl
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range ticks[1 : len(ticks)-1] {
|
||||||
|
s := majorStyle
|
||||||
|
if isMinor {
|
||||||
|
s = minorStyle
|
||||||
|
}
|
||||||
|
gl = append(gl, GridLine{
|
||||||
|
Style: s,
|
||||||
|
IsMinor: isMinor,
|
||||||
|
IsVertical: isVertical,
|
||||||
|
Value: t.Value,
|
||||||
|
})
|
||||||
|
isMinor = !isMinor
|
||||||
|
}
|
||||||
|
return gl
|
||||||
|
}
|
||||||
|
|
|
@ -16,12 +16,11 @@ func TestGenerateGridLines(t *testing.T) {
|
||||||
{Value: 4.0, Label: "4.0"},
|
{Value: 4.0, Label: "4.0"},
|
||||||
}
|
}
|
||||||
|
|
||||||
gl := GenerateGridLines(ticks, true)
|
gl := GenerateGridLines(ticks, Style{}, Style{}, true)
|
||||||
assert.Len(gl, 4)
|
assert.Len(gl, 2)
|
||||||
assert.Equal(1.0, gl[0].Value)
|
|
||||||
assert.Equal(2.0, gl[1].Value)
|
assert.Equal(2.0, gl[0].Value)
|
||||||
assert.Equal(3.0, gl[2].Value)
|
assert.Equal(3.0, gl[1].Value)
|
||||||
assert.Equal(4.0, gl[3].Value)
|
|
||||||
|
|
||||||
assert.True(gl[0].IsVertical)
|
assert.True(gl[0].IsVertical)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ package chart
|
||||||
type HistogramSeries struct {
|
type HistogramSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
InnerSeries ValueProvider
|
InnerSeries ValueProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ func (hs HistogramSeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which yaxis the series is mapped to.
|
// GetYAxis returns which yaxis the series is mapped to.
|
||||||
func (hs HistogramSeries) GetYAxis() YAxisType {
|
func (hs HistogramSeries) GetYAxis() yAxisType {
|
||||||
return hs.YAxis
|
return hs.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,6 @@ func (hs HistogramSeries) GetBoundedValue(index int) (x, y1, y2 float64) {
|
||||||
|
|
||||||
// Render implements Series.Render.
|
// Render implements Series.Render.
|
||||||
func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
style := hs.Style.WithDefaultsFrom(defaults)
|
style := hs.Style.InheritFrom(defaults)
|
||||||
DrawHistogramSeries(r, canvasBox, xrange, yrange, style, hs)
|
Draw.HistogramSeries(r, canvasBox, xrange, yrange, style, hs)
|
||||||
}
|
}
|
||||||
|
|
31
histogram_series_test.go
Normal file
31
histogram_series_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHistogramSeries(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cs := ContinuousSeries{
|
||||||
|
Name: "Test Series",
|
||||||
|
XValues: Sequence.Float64(1.0, 20.0),
|
||||||
|
YValues: Sequence.Float64(10.0, -10.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
hs := HistogramSeries{
|
||||||
|
InnerSeries: cs,
|
||||||
|
}
|
||||||
|
|
||||||
|
for x := 0; x < hs.Len(); x++ {
|
||||||
|
csx, csy := cs.GetValue(0)
|
||||||
|
hsx, hsy1, hsy2 := hs.GetBoundedValue(0)
|
||||||
|
assert.Equal(csx, hsx)
|
||||||
|
assert.True(hsy1 > 0)
|
||||||
|
assert.True(hsy2 <= 0)
|
||||||
|
assert.True(csy < 0 || (csy > 0 && csy == hsy1))
|
||||||
|
assert.True(csy > 0 || (csy < 0 && csy == hsy2))
|
||||||
|
}
|
||||||
|
}
|
BIN
images/pie_chart.png
Normal file
BIN
images/pie_chart.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
images/stacked_bar.png
Normal file
BIN
images/stacked_bar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
116
legend.go
Normal file
116
legend.go
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import "github.com/wcharczuk/go-chart/drawing"
|
||||||
|
|
||||||
|
// Legend returns a legend renderable function.
|
||||||
|
func Legend(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
return func(r Renderer, cb Box, chartDefaults Style) {
|
||||||
|
legendDefaults := Style{
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
FontSize: 8.0,
|
||||||
|
StrokeColor: DefaultAxisColor,
|
||||||
|
StrokeWidth: DefaultAxisLineWidth,
|
||||||
|
}
|
||||||
|
|
||||||
|
var legendStyle Style
|
||||||
|
if len(userDefaults) > 0 {
|
||||||
|
legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults))
|
||||||
|
} else {
|
||||||
|
legendStyle = chartDefaults.InheritFrom(legendDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEFAULTS
|
||||||
|
legendPadding := Box{
|
||||||
|
Top: 5,
|
||||||
|
Left: 5,
|
||||||
|
Right: 5,
|
||||||
|
Bottom: 5,
|
||||||
|
}
|
||||||
|
lineTextGap := 5
|
||||||
|
lineLengthMinimum := 25
|
||||||
|
|
||||||
|
var labels []string
|
||||||
|
var lines []Style
|
||||||
|
for index, s := range c.Series {
|
||||||
|
if s.GetStyle().IsZero() || s.GetStyle().Show {
|
||||||
|
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
|
||||||
|
labels = append(labels, s.GetName())
|
||||||
|
lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legend := Box{
|
||||||
|
Top: cb.Top,
|
||||||
|
Left: cb.Left,
|
||||||
|
// bottom and right will be sized by the legend content + relevant padding.
|
||||||
|
}
|
||||||
|
|
||||||
|
legendContent := Box{
|
||||||
|
Top: legend.Top + legendPadding.Top,
|
||||||
|
Left: legend.Left + legendPadding.Left,
|
||||||
|
Right: legend.Left + legendPadding.Left,
|
||||||
|
Bottom: legend.Top + legendPadding.Top,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.SetFont(legendStyle.GetFont())
|
||||||
|
r.SetFontColor(legendStyle.GetFontColor())
|
||||||
|
r.SetFontSize(legendStyle.GetFontSize())
|
||||||
|
|
||||||
|
// measure
|
||||||
|
labelCount := 0
|
||||||
|
for x := 0; x < len(labels); x++ {
|
||||||
|
if len(labels[x]) > 0 {
|
||||||
|
tb := r.MeasureText(labels[x])
|
||||||
|
if labelCount > 0 {
|
||||||
|
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||||
|
}
|
||||||
|
legendContent.Bottom += tb.Height()
|
||||||
|
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||||
|
legendContent.Right = Math.MaxInt(legendContent.Right, right)
|
||||||
|
labelCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legend = legend.Grow(legendContent)
|
||||||
|
legend.Right = legendContent.Right + legendPadding.Right
|
||||||
|
legend.Bottom = legendContent.Bottom + legendPadding.Bottom
|
||||||
|
|
||||||
|
Draw.Box(r, legend, legendStyle)
|
||||||
|
|
||||||
|
ycursor := legendContent.Top
|
||||||
|
tx := legendContent.Left
|
||||||
|
legendCount := 0
|
||||||
|
for x := 0; x < len(labels); x++ {
|
||||||
|
if len(labels[x]) > 0 {
|
||||||
|
|
||||||
|
if legendCount > 0 {
|
||||||
|
ycursor += DefaultMinimumTickVerticalSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
tb := r.MeasureText(labels[x])
|
||||||
|
|
||||||
|
ty := ycursor + tb.Height()
|
||||||
|
r.Text(labels[x], tx, ty)
|
||||||
|
|
||||||
|
th2 := tb.Height() >> 1
|
||||||
|
|
||||||
|
lx := tx + tb.Width() + lineTextGap
|
||||||
|
ly := ty - th2
|
||||||
|
lx2 := legendContent.Right - legendPadding.Right
|
||||||
|
|
||||||
|
r.SetStrokeColor(lines[x].GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(lines[x].GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
|
||||||
|
|
||||||
|
r.MoveTo(lx, ly)
|
||||||
|
r.LineTo(lx2, ly)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
ycursor += tb.Height()
|
||||||
|
legendCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
linear_regression_series.go
Normal file
132
linear_regression_series.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
// LinearRegressionSeries is a series that plots the n-nearest neighbors
|
||||||
|
// linear regression for the values.
|
||||||
|
type LinearRegressionSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis yAxisType
|
||||||
|
|
||||||
|
Window int
|
||||||
|
Offset int
|
||||||
|
InnerSeries ValueProvider
|
||||||
|
|
||||||
|
m float64
|
||||||
|
b float64
|
||||||
|
avgx float64
|
||||||
|
stddevx float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (lrs LinearRegressionSeries) GetName() string {
|
||||||
|
return lrs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (lrs LinearRegressionSeries) GetStyle() Style {
|
||||||
|
return lrs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (lrs LinearRegressionSeries) GetYAxis() yAxisType {
|
||||||
|
return lrs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (lrs LinearRegressionSeries) Len() int {
|
||||||
|
return Math.MinInt(lrs.GetWindow(), lrs.InnerSeries.Len()-lrs.GetOffset())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindow returns the window size.
|
||||||
|
func (lrs LinearRegressionSeries) GetWindow() int {
|
||||||
|
if lrs.Window == 0 {
|
||||||
|
return lrs.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
return lrs.Window
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEndIndex returns the effective window end.
|
||||||
|
func (lrs LinearRegressionSeries) GetEndIndex() int {
|
||||||
|
return Math.MinInt(lrs.GetOffset()+(lrs.Len()), (lrs.InnerSeries.Len() - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOffset returns the data offset.
|
||||||
|
func (lrs LinearRegressionSeries) GetOffset() int {
|
||||||
|
if lrs.Offset == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return lrs.Offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue gets a value at a given index.
|
||||||
|
func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) {
|
||||||
|
if lrs.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if lrs.m == 0 && lrs.b == 0 {
|
||||||
|
lrs.computeCoefficients()
|
||||||
|
}
|
||||||
|
offset := lrs.GetOffset()
|
||||||
|
effectiveIndex := Math.MinInt(index+offset, lrs.InnerSeries.Len())
|
||||||
|
x, y = lrs.InnerSeries.GetValue(effectiveIndex)
|
||||||
|
y = (lrs.m * lrs.normalize(x)) + lrs.b
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastValue computes the last moving average value but walking back window size samples,
|
||||||
|
// and recomputing the last moving average chunk.
|
||||||
|
func (lrs *LinearRegressionSeries) GetLastValue() (x, y float64) {
|
||||||
|
if lrs.InnerSeries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if lrs.m == 0 && lrs.b == 0 {
|
||||||
|
lrs.computeCoefficients()
|
||||||
|
}
|
||||||
|
endIndex := lrs.GetEndIndex()
|
||||||
|
x, y = lrs.InnerSeries.GetValue(endIndex)
|
||||||
|
y = (lrs.m * lrs.normalize(x)) + lrs.b
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lrs *LinearRegressionSeries) normalize(xvalue float64) float64 {
|
||||||
|
return (xvalue - lrs.avgx) / lrs.stddevx
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`.
|
||||||
|
func (lrs *LinearRegressionSeries) computeCoefficients() {
|
||||||
|
startIndex := lrs.GetOffset()
|
||||||
|
endIndex := lrs.GetEndIndex()
|
||||||
|
|
||||||
|
p := float64(endIndex - startIndex)
|
||||||
|
|
||||||
|
xvalues := NewRingBufferWithCapacity(lrs.Len())
|
||||||
|
for index := startIndex; index < endIndex; index++ {
|
||||||
|
|
||||||
|
x, _ := lrs.InnerSeries.GetValue(index)
|
||||||
|
xvalues.Enqueue(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
lrs.avgx = xvalues.Average()
|
||||||
|
lrs.stddevx = xvalues.StdDev()
|
||||||
|
|
||||||
|
var sumx, sumy, sumxx, sumxy float64
|
||||||
|
for index := startIndex; index < endIndex; index++ {
|
||||||
|
x, y := lrs.InnerSeries.GetValue(index)
|
||||||
|
|
||||||
|
x = lrs.normalize(x)
|
||||||
|
|
||||||
|
sumx += x
|
||||||
|
sumy += y
|
||||||
|
sumxx += x * x
|
||||||
|
sumxy += x * y
|
||||||
|
}
|
||||||
|
|
||||||
|
lrs.m = (p*sumxy - sumx*sumy) / (p*sumxx - sumx*sumx)
|
||||||
|
lrs.b = (sumy / p) - (lrs.m * sumx / p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := lrs.Style.InheritFrom(defaults)
|
||||||
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs)
|
||||||
|
}
|
77
linear_regression_series_test.go
Normal file
77
linear_regression_series_test.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLinearRegressionSeries(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
mainSeries := ContinuousSeries{
|
||||||
|
Name: "A test series",
|
||||||
|
XValues: Sequence.Float64(1.0, 100.0),
|
||||||
|
YValues: Sequence.Float64(1.0, 100.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
linRegSeries := &LinearRegressionSeries{
|
||||||
|
InnerSeries: mainSeries,
|
||||||
|
}
|
||||||
|
|
||||||
|
lrx0, lry0 := linRegSeries.GetValue(0)
|
||||||
|
assert.InDelta(1.0, lrx0, 0.0000001)
|
||||||
|
assert.InDelta(1.0, lry0, 0.0000001)
|
||||||
|
|
||||||
|
lrxn, lryn := linRegSeries.GetLastValue()
|
||||||
|
assert.InDelta(100.0, lrxn, 0.0000001)
|
||||||
|
assert.InDelta(100.0, lryn, 0.0000001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinearRegressionSeriesDesc(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
mainSeries := ContinuousSeries{
|
||||||
|
Name: "A test series",
|
||||||
|
XValues: Sequence.Float64(100.0, 1.0),
|
||||||
|
YValues: Sequence.Float64(100.0, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
linRegSeries := &LinearRegressionSeries{
|
||||||
|
InnerSeries: mainSeries,
|
||||||
|
}
|
||||||
|
|
||||||
|
lrx0, lry0 := linRegSeries.GetValue(0)
|
||||||
|
assert.InDelta(100.0, lrx0, 0.0000001)
|
||||||
|
assert.InDelta(100.0, lry0, 0.0000001)
|
||||||
|
|
||||||
|
lrxn, lryn := linRegSeries.GetLastValue()
|
||||||
|
assert.InDelta(1.0, lrxn, 0.0000001)
|
||||||
|
assert.InDelta(1.0, lryn, 0.0000001)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
mainSeries := ContinuousSeries{
|
||||||
|
Name: "A test series",
|
||||||
|
XValues: Sequence.Float64(100.0, 1.0),
|
||||||
|
YValues: Sequence.Float64(100.0, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
linRegSeries := &LinearRegressionSeries{
|
||||||
|
InnerSeries: mainSeries,
|
||||||
|
Offset: 10,
|
||||||
|
Window: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(10, linRegSeries.Len())
|
||||||
|
|
||||||
|
lrx0, lry0 := linRegSeries.GetValue(0)
|
||||||
|
assert.InDelta(90.0, lrx0, 0.0000001)
|
||||||
|
assert.InDelta(90.0, lry0, 0.0000001)
|
||||||
|
|
||||||
|
lrxn, lryn := linRegSeries.GetLastValue()
|
||||||
|
assert.InDelta(80.0, lrxn, 0.0000001)
|
||||||
|
assert.InDelta(80.0, lryn, 0.0000001)
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ const (
|
||||||
type MACDSeries struct {
|
type MACDSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
InnerSeries ValueProvider
|
InnerSeries ValueProvider
|
||||||
|
|
||||||
PrimaryPeriod int
|
PrimaryPeriod int
|
||||||
|
@ -56,7 +56,7 @@ func (macd MACDSeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (macd MACDSeries) GetYAxis() YAxisType {
|
func (macd MACDSeries) GetYAxis() yAxisType {
|
||||||
return macd.YAxis
|
return macd.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ func (macd *MACDSeries) ensureChildSeries() {
|
||||||
type MACDSignalSeries struct {
|
type MACDSignalSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
InnerSeries ValueProvider
|
InnerSeries ValueProvider
|
||||||
|
|
||||||
PrimaryPeriod int
|
PrimaryPeriod int
|
||||||
|
@ -150,7 +150,7 @@ func (macds MACDSignalSeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (macds MACDSignalSeries) GetYAxis() YAxisType {
|
func (macds MACDSignalSeries) GetYAxis() yAxisType {
|
||||||
return macds.YAxis
|
return macds.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,15 +192,15 @@ func (macds *MACDSignalSeries) ensureSignal() {
|
||||||
|
|
||||||
// Render renders the series.
|
// Render renders the series.
|
||||||
func (macds *MACDSignalSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (macds *MACDSignalSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
style := macds.Style.WithDefaultsFrom(defaults)
|
style := macds.Style.InheritFrom(defaults)
|
||||||
DrawLineSeries(r, canvasBox, xrange, yrange, style, macds)
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MACDLineSeries is a series that computes the inner ema1-ema2 value as a series.
|
// MACDLineSeries is a series that computes the inner ema1-ema2 value as a series.
|
||||||
type MACDLineSeries struct {
|
type MACDLineSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
InnerSeries ValueProvider
|
InnerSeries ValueProvider
|
||||||
|
|
||||||
PrimaryPeriod int
|
PrimaryPeriod int
|
||||||
|
@ -223,7 +223,7 @@ func (macdl MACDLineSeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (macdl MACDLineSeries) GetYAxis() YAxisType {
|
func (macdl MACDLineSeries) GetYAxis() yAxisType {
|
||||||
return macdl.YAxis
|
return macdl.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,6 +284,6 @@ func (macdl *MACDLineSeries) ensureEMASeries() {
|
||||||
|
|
||||||
// Render renders the series.
|
// Render renders the series.
|
||||||
func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
style := macdl.Style.WithDefaultsFrom(defaults)
|
style := macdl.Style.InheritFrom(defaults)
|
||||||
DrawLineSeries(r, canvasBox, xrange, yrange, style, macdl)
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macdl)
|
||||||
}
|
}
|
||||||
|
|
133
market_hours_range.go
Normal file
133
market_hours_range.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/date"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarketHoursRange is a special type of range that compresses a time range into just the
|
||||||
|
// market (i.e. NYSE operating hours and days) range.
|
||||||
|
type MarketHoursRange struct {
|
||||||
|
Min time.Time
|
||||||
|
Max time.Time
|
||||||
|
|
||||||
|
MarketOpen time.Time
|
||||||
|
MarketClose time.Time
|
||||||
|
|
||||||
|
HolidayProvider date.HolidayProvider
|
||||||
|
|
||||||
|
ValueFormatter ValueFormatter
|
||||||
|
|
||||||
|
Domain int
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the range is setup or not.
|
||||||
|
func (mhr MarketHoursRange) IsZero() bool {
|
||||||
|
return mhr.Min.IsZero() && mhr.Max.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMin returns the min value.
|
||||||
|
func (mhr MarketHoursRange) GetMin() float64 {
|
||||||
|
return TimeToFloat64(mhr.Min)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMax returns the max value.
|
||||||
|
func (mhr MarketHoursRange) GetMax() float64 {
|
||||||
|
return TimeToFloat64(mhr.GetEffectiveMax())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEffectiveMax gets either the close on the max, or the max itself.
|
||||||
|
func (mhr MarketHoursRange) GetEffectiveMax() time.Time {
|
||||||
|
maxClose := date.On(mhr.MarketClose, mhr.Max)
|
||||||
|
if maxClose.After(mhr.Max) {
|
||||||
|
return maxClose
|
||||||
|
}
|
||||||
|
return mhr.Max
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMin sets the min value.
|
||||||
|
func (mhr *MarketHoursRange) SetMin(min float64) {
|
||||||
|
mhr.Min = Float64ToTime(min)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMax sets the max value.
|
||||||
|
func (mhr *MarketHoursRange) SetMax(max float64) {
|
||||||
|
mhr.Max = Float64ToTime(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDelta gets the delta.
|
||||||
|
func (mhr MarketHoursRange) GetDelta() float64 {
|
||||||
|
min := mhr.GetMin()
|
||||||
|
max := mhr.GetMax()
|
||||||
|
return max - min
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomain gets the domain.
|
||||||
|
func (mhr MarketHoursRange) GetDomain() int {
|
||||||
|
return mhr.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDomain sets the domain.
|
||||||
|
func (mhr *MarketHoursRange) SetDomain(domain int) {
|
||||||
|
mhr.Domain = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider.
|
||||||
|
func (mhr MarketHoursRange) GetHolidayProvider() date.HolidayProvider {
|
||||||
|
if mhr.HolidayProvider == nil {
|
||||||
|
return date.DefaultHolidayProvider
|
||||||
|
}
|
||||||
|
return mhr.HolidayProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTicks returns the ticks for the range.
|
||||||
|
// This is to override the default continous ticks that would be generated for the range.
|
||||||
|
func (mhr *MarketHoursRange) GetTicks(vf ValueFormatter) []Tick {
|
||||||
|
var ticks []Tick
|
||||||
|
|
||||||
|
cursor := date.On(mhr.MarketClose, mhr.Min)
|
||||||
|
maxClose := date.On(mhr.MarketClose, mhr.Max)
|
||||||
|
|
||||||
|
if mhr.Min.Before(cursor) {
|
||||||
|
ticks = append(ticks, Tick{
|
||||||
|
Value: TimeToFloat64(cursor),
|
||||||
|
Label: vf(cursor),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for date.BeforeDate(cursor, maxClose) {
|
||||||
|
if date.IsWeekDay(cursor.Weekday()) && !mhr.GetHolidayProvider()(cursor) {
|
||||||
|
ticks = append(ticks, Tick{
|
||||||
|
Value: TimeToFloat64(cursor),
|
||||||
|
Label: vf(cursor),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = cursor.AddDate(0, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
endMarketClose := date.On(mhr.MarketClose, cursor)
|
||||||
|
if date.IsWeekDay(endMarketClose.Weekday()) && !mhr.GetHolidayProvider()(endMarketClose) {
|
||||||
|
ticks = append(ticks, Tick{
|
||||||
|
Value: TimeToFloat64(endMarketClose),
|
||||||
|
Label: vf(endMarketClose),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ticks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mhr MarketHoursRange) String() string {
|
||||||
|
return fmt.Sprintf("MarketHoursRange [%s, %s] => %d", mhr.Min.Format(DefaultDateMinuteFormat), mhr.Max.Format(DefaultDateMinuteFormat), mhr.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate maps a given value into the ContinuousRange space.
|
||||||
|
func (mhr MarketHoursRange) Translate(value float64) int {
|
||||||
|
valueTime := Float64ToTime(value)
|
||||||
|
valueTimeEastern := valueTime.In(date.Eastern())
|
||||||
|
totalSeconds := date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.MarketOpen, mhr.MarketClose, mhr.HolidayProvider)
|
||||||
|
valueDelta := date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.MarketOpen, mhr.MarketClose, mhr.HolidayProvider)
|
||||||
|
translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain))
|
||||||
|
return translated
|
||||||
|
}
|
61
market_hours_range_test.go
Normal file
61
market_hours_range_test.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
"github.com/wcharczuk/go-chart/date"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMarketHoursRangeGetDelta(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
r := &MarketHoursRange{
|
||||||
|
Min: time.Date(2016, 07, 19, 9, 30, 0, 0, date.Eastern()),
|
||||||
|
Max: time.Date(2016, 07, 22, 16, 00, 0, 0, date.Eastern()),
|
||||||
|
MarketOpen: date.NYSEOpen,
|
||||||
|
MarketClose: date.NYSEClose,
|
||||||
|
HolidayProvider: date.IsNYSEHoliday,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotZero(r.GetDelta())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarketHoursRangeTranslate(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
r := &MarketHoursRange{
|
||||||
|
Min: time.Date(2016, 07, 18, 9, 30, 0, 0, date.Eastern()),
|
||||||
|
Max: time.Date(2016, 07, 22, 16, 00, 0, 0, date.Eastern()),
|
||||||
|
MarketOpen: date.NYSEOpen,
|
||||||
|
MarketClose: date.NYSEClose,
|
||||||
|
HolidayProvider: date.IsNYSEHoliday,
|
||||||
|
Domain: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
weds := time.Date(2016, 07, 20, 9, 30, 0, 0, date.Eastern())
|
||||||
|
|
||||||
|
assert.Equal(0, r.Translate(TimeToFloat64(r.Min)))
|
||||||
|
assert.Equal(400, r.Translate(TimeToFloat64(weds)))
|
||||||
|
assert.Equal(1000, r.Translate(TimeToFloat64(r.Max)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarketHoursRangeGetTicks(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
r := &MarketHoursRange{
|
||||||
|
Min: time.Date(2016, 07, 18, 9, 30, 0, 0, date.Eastern()),
|
||||||
|
Max: time.Date(2016, 07, 22, 16, 00, 0, 0, date.Eastern()),
|
||||||
|
MarketOpen: date.NYSEOpen,
|
||||||
|
MarketClose: date.NYSEClose,
|
||||||
|
HolidayProvider: date.IsNYSEHoliday,
|
||||||
|
Domain: 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
ticks := r.GetTicks(TimeValueFormatter)
|
||||||
|
assert.NotEmpty(ticks)
|
||||||
|
assert.Len(ticks, 6)
|
||||||
|
assert.NotEqual(TimeToFloat64(r.Min), ticks[0].Value)
|
||||||
|
assert.NotEmpty(ticks[0].Label)
|
||||||
|
}
|
222
math.go
Normal file
222
math.go
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_pi = math.Pi
|
||||||
|
_2pi = 2 * math.Pi
|
||||||
|
_3pi4 = (3 * math.Pi) / 4.0
|
||||||
|
_4pi3 = (4 * math.Pi) / 3.0
|
||||||
|
_3pi2 = (3 * math.Pi) / 2.0
|
||||||
|
_5pi4 = (5 * math.Pi) / 4.0
|
||||||
|
_7pi4 = (7 * math.Pi) / 4.0
|
||||||
|
_pi2 = math.Pi / 2.0
|
||||||
|
_pi4 = math.Pi / 4.0
|
||||||
|
_d2r = (math.Pi / 180.0)
|
||||||
|
_r2d = (180.0 / math.Pi)
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeToFloat64 returns a float64 representation of a time.
|
||||||
|
func TimeToFloat64(t time.Time) float64 {
|
||||||
|
return float64(t.UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64ToTime returns a time from a float64.
|
||||||
|
func Float64ToTime(tf float64) time.Time {
|
||||||
|
return time.Unix(0, int64(tf))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Math contains helper methods for common math operations.
|
||||||
|
Math = &mathUtil{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type mathUtil struct{}
|
||||||
|
|
||||||
|
// MinAndMax returns both the min and max in one pass.
|
||||||
|
func (m mathUtil) 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 (m mathUtil) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoundToForDelta returns a `roundTo` value for a given delta.
|
||||||
|
func (m mathUtil) GetRoundToForDelta(delta float64) float64 {
|
||||||
|
startingDeltaBound := math.Pow(10.0, 10.0)
|
||||||
|
for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 {
|
||||||
|
if delta > cursor {
|
||||||
|
return cursor / 10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundUp rounds up to a given roundTo value.
|
||||||
|
func (m mathUtil) RoundUp(value, roundTo float64) float64 {
|
||||||
|
d1 := math.Ceil(value / roundTo)
|
||||||
|
return d1 * roundTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundDown rounds down to a given roundTo value.
|
||||||
|
func (m mathUtil) RoundDown(value, roundTo float64) float64 {
|
||||||
|
d1 := math.Floor(value / roundTo)
|
||||||
|
return d1 * roundTo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize returns a set of numbers on the interval [0,1] for a given set of inputs.
|
||||||
|
// An example: 4,3,2,1 => 0.4, 0.3, 0.2, 0.1
|
||||||
|
// Caveat; the total may be < 1.0; there are going to be issues with irrational numbers etc.
|
||||||
|
func (m mathUtil) Normalize(values ...float64) []float64 {
|
||||||
|
var total float64
|
||||||
|
for _, v := range values {
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
output := make([]float64, len(values))
|
||||||
|
for x, v := range values {
|
||||||
|
output[x] = m.RoundDown(v/total, 0.0001)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinInt returns the minimum of a set of integers.
|
||||||
|
func (m mathUtil) MinInt(values ...int) int {
|
||||||
|
min := math.MaxInt32
|
||||||
|
for _, v := range values {
|
||||||
|
if v < min {
|
||||||
|
min = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxInt returns the maximum of a set of integers.
|
||||||
|
func (m mathUtil) MaxInt(values ...int) int {
|
||||||
|
max := math.MinInt32
|
||||||
|
for _, v := range values {
|
||||||
|
if v > max {
|
||||||
|
max = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbsInt returns the absolute value of an integer.
|
||||||
|
func (m mathUtil) AbsInt(value int) int {
|
||||||
|
if value < 0 {
|
||||||
|
return -value
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum sums a set of values.
|
||||||
|
func (m mathUtil) Sum(values ...float64) float64 {
|
||||||
|
var total float64
|
||||||
|
for _, v := range values {
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumInt sums a set of values.
|
||||||
|
func (m mathUtil) SumInt(values ...int) int {
|
||||||
|
var total int
|
||||||
|
for _, v := range values {
|
||||||
|
total += v
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// PercentDifference computes the percentage difference between two values.
|
||||||
|
// The formula is (v2-v1)/v1.
|
||||||
|
func (m mathUtil) PercentDifference(v1, v2 float64) float64 {
|
||||||
|
if v1 == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (v2 - v1) / v1
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegreesToRadians returns degrees as radians.
|
||||||
|
func (m mathUtil) DegreesToRadians(degrees float64) float64 {
|
||||||
|
return degrees * _d2r
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadiansToDegrees translates a radian value to a degree value.
|
||||||
|
func (m mathUtil) RadiansToDegrees(value float64) float64 {
|
||||||
|
return math.Mod(value, _2pi) * _r2d
|
||||||
|
}
|
||||||
|
|
||||||
|
// PercentToRadians converts a normalized value (0,1) to radians.
|
||||||
|
func (m mathUtil) PercentToRadians(pct float64) float64 {
|
||||||
|
return m.DegreesToRadians(360.0 * pct)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadianAdd adds a delta to a base in radians.
|
||||||
|
func (m mathUtil) RadianAdd(base, delta float64) float64 {
|
||||||
|
value := base + delta
|
||||||
|
if value > _2pi {
|
||||||
|
return math.Mod(value, _2pi)
|
||||||
|
} else if value < 0 {
|
||||||
|
return math.Mod(_2pi+value, _2pi)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegreesAdd adds a delta to a base in radians.
|
||||||
|
func (m mathUtil) DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
|
||||||
|
value := baseDegrees + deltaDegrees
|
||||||
|
if value > _2pi {
|
||||||
|
return math.Mod(value, 360.0)
|
||||||
|
} else if value < 0 {
|
||||||
|
return math.Mod(360.0+value, 360.0)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// DegreesToCompass returns the degree value in compass / clock orientation.
|
||||||
|
func (m mathUtil) DegreesToCompass(deg float64) float64 {
|
||||||
|
return m.DegreesAdd(deg, -90.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CirclePoint returns the absolute position of a circle diameter point given
|
||||||
|
// by the radius and the angle.
|
||||||
|
func (m mathUtil) CirclePoint(cx, cy int, radius, angleRadians float64) (x, y int) {
|
||||||
|
x = cx + int(radius*math.Sin(angleRadians))
|
||||||
|
y = cy - int(radius*math.Cos(angleRadians))
|
||||||
|
return
|
||||||
|
}
|
162
math_test.go
Normal file
162
math_test.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
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 := Math.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 := Math.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 := Math.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 := Math.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 := Math.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 := Math.MinAndMaxOfTime(values...)
|
||||||
|
assert.Equal(time.Time{}, min)
|
||||||
|
assert.Equal(time.Time{}, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRoundToForDelta(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Equal(100.0, Math.GetRoundToForDelta(1001.00))
|
||||||
|
assert.Equal(10.0, Math.GetRoundToForDelta(101.00))
|
||||||
|
assert.Equal(1.0, Math.GetRoundToForDelta(11.00))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoundUp(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Equal(0.5, Math.RoundUp(0.49, 0.1))
|
||||||
|
assert.Equal(1.0, Math.RoundUp(0.51, 1.0))
|
||||||
|
assert.Equal(0.4999, Math.RoundUp(0.49988, 0.0001))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRoundDown(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
assert.Equal(0.5, Math.RoundDown(0.51, 0.1))
|
||||||
|
assert.Equal(1.0, Math.RoundDown(1.01, 1.0))
|
||||||
|
assert.Equal(0.5001, Math.RoundDown(0.50011, 0.0001))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPercentDifference(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Equal(0.5, Math.PercentDifference(1.0, 1.5))
|
||||||
|
assert.Equal(-0.5, Math.PercentDifference(2.0, 1.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalize(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
values := []float64{10, 9, 8, 7, 6}
|
||||||
|
normalized := Math.Normalize(values...)
|
||||||
|
assert.Len(normalized, 5)
|
||||||
|
assert.Equal(0.25, normalized[0])
|
||||||
|
assert.Equal(0.1499, normalized[4])
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_degreesToRadians = map[float64]float64{
|
||||||
|
0: 0, // !_2pi b/c no irrational nums in floats.
|
||||||
|
45: _pi4,
|
||||||
|
90: _pi2,
|
||||||
|
135: _3pi4,
|
||||||
|
180: _pi,
|
||||||
|
225: _5pi4,
|
||||||
|
270: _3pi2,
|
||||||
|
315: _7pi4,
|
||||||
|
}
|
||||||
|
|
||||||
|
_compassToRadians = map[float64]float64{
|
||||||
|
0: _pi2,
|
||||||
|
45: _pi4,
|
||||||
|
90: 0, // !_2pi b/c no irrational nums in floats.
|
||||||
|
135: _7pi4,
|
||||||
|
180: _3pi2,
|
||||||
|
225: _5pi4,
|
||||||
|
270: _pi,
|
||||||
|
315: _3pi4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDegreesToRadians(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for d, r := range _degreesToRadians {
|
||||||
|
assert.Equal(r, Math.DegreesToRadians(d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPercentToRadians(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for d, r := range _degreesToRadians {
|
||||||
|
assert.Equal(r, Math.PercentToRadians(d/360.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRadiansToDegrees(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for d, r := range _degreesToRadians {
|
||||||
|
assert.Equal(d, Math.RadiansToDegrees(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRadianAdd(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Equal(_pi, Math.RadianAdd(_pi2, _pi2))
|
||||||
|
assert.Equal(_3pi2, Math.RadianAdd(_pi2, _pi))
|
||||||
|
assert.Equal(_pi, Math.RadianAdd(_pi, _2pi))
|
||||||
|
assert.Equal(_pi, Math.RadianAdd(_pi, -_2pi))
|
||||||
|
}
|
279
pie_chart.go
Normal file
279
pie_chart.go
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PieChart is a chart that draws sections of a circle based on percentages.
|
||||||
|
type PieChart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
SliceStyle Style
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Values []Value
|
||||||
|
Elements []Renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (pc PieChart) GetDPI(defaults ...float64) float64 {
|
||||||
|
if pc.DPI == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return pc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (pc PieChart) GetFont() *truetype.Font {
|
||||||
|
if pc.Font == nil {
|
||||||
|
return pc.defaultFont
|
||||||
|
}
|
||||||
|
return pc.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (pc PieChart) GetWidth() int {
|
||||||
|
if pc.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return pc.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (pc PieChart) GetHeight() int {
|
||||||
|
if pc.Height == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return pc.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (pc PieChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(pc.Values) == 0 {
|
||||||
|
return errors.New("Please provide at least one value.")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := rp(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pc.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pc.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(pc.GetDPI(DefaultDPI))
|
||||||
|
|
||||||
|
canvasBox := pc.getDefaultCanvasBox()
|
||||||
|
canvasBox = pc.getCircleAdjustedCanvasBox(canvasBox)
|
||||||
|
|
||||||
|
pc.drawBackground(r)
|
||||||
|
pc.drawCanvas(r, canvasBox)
|
||||||
|
|
||||||
|
finalValues := pc.finalizeValues(pc.Values)
|
||||||
|
pc.drawSlices(r, canvasBox, finalValues)
|
||||||
|
pc.drawTitle(r)
|
||||||
|
for _, a := range pc.Elements {
|
||||||
|
a(r, canvasBox, pc.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawBackground(r Renderer) {
|
||||||
|
Draw.Box(r, Box{
|
||||||
|
Right: pc.GetWidth(),
|
||||||
|
Bottom: pc.GetHeight(),
|
||||||
|
}, pc.getBackgroundStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
|
||||||
|
Draw.Box(r, canvasBox, pc.getCanvasStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawTitle(r Renderer) {
|
||||||
|
if len(pc.Title) > 0 && pc.TitleStyle.Show {
|
||||||
|
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
||||||
|
cx, cy := canvasBox.Center()
|
||||||
|
diameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
|
||||||
|
radius := float64(diameter >> 1)
|
||||||
|
labelRadius := (radius * 2.0) / 3.0
|
||||||
|
|
||||||
|
// draw the pie slices
|
||||||
|
var rads, delta, delta2, total float64
|
||||||
|
var lx, ly int
|
||||||
|
for index, v := range values {
|
||||||
|
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(cx, cy)
|
||||||
|
rads = Math.PercentToRadians(total)
|
||||||
|
delta = Math.PercentToRadians(v.Value)
|
||||||
|
|
||||||
|
r.ArcTo(cx, cy, radius, radius, rads, delta)
|
||||||
|
|
||||||
|
r.LineTo(cx, cy)
|
||||||
|
r.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
total = total + v.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the labels
|
||||||
|
total = 0
|
||||||
|
for index, v := range values {
|
||||||
|
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||||
|
if len(v.Label) > 0 {
|
||||||
|
delta2 = Math.PercentToRadians(total + (v.Value / 2.0))
|
||||||
|
delta2 = Math.RadianAdd(delta2, _pi2)
|
||||||
|
lx, ly = Math.CirclePoint(cx, cy, labelRadius, delta2)
|
||||||
|
|
||||||
|
tb := r.MeasureText(v.Label)
|
||||||
|
lx = lx - (tb.Width() >> 1)
|
||||||
|
ly = ly + (tb.Height() >> 1)
|
||||||
|
|
||||||
|
r.Text(v.Label, lx, ly)
|
||||||
|
}
|
||||||
|
total = total + v.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) finalizeValues(values []Value) []Value {
|
||||||
|
return Values(values).Normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getDefaultCanvasBox() Box {
|
||||||
|
return pc.Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
|
||||||
|
circleDiameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
|
||||||
|
|
||||||
|
square := Box{
|
||||||
|
Right: circleDiameter,
|
||||||
|
Bottom: circleDiameter,
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasBox.Fit(square)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getBackgroundStyle() Style {
|
||||||
|
return pc.Background.InheritFrom(pc.styleDefaultsBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getCanvasStyle() Style {
|
||||||
|
return pc.Canvas.InheritFrom(pc.styleDefaultsCanvas())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsCanvas() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: DefaultCanvasColor,
|
||||||
|
StrokeColor: DefaultCanvasStrokeColor,
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsPieChartValue() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: ColorWhite,
|
||||||
|
StrokeWidth: 5.0,
|
||||||
|
FillColor: ColorWhite,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) stylePieChartValue(index int) Style {
|
||||||
|
return pc.SliceStyle.InheritFrom(Style{
|
||||||
|
StrokeColor: ColorWhite,
|
||||||
|
StrokeWidth: 5.0,
|
||||||
|
FillColor: GetAlternateColor(index),
|
||||||
|
FontSize: pc.getScaledFontSize(),
|
||||||
|
FontColor: ColorWhite,
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getScaledFontSize() float64 {
|
||||||
|
effectiveDimension := Math.MinInt(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48.0
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24.0
|
||||||
|
} else if effectiveDimension > 512 {
|
||||||
|
return 18.0
|
||||||
|
} else if effectiveDimension > 256 {
|
||||||
|
return 12.0
|
||||||
|
}
|
||||||
|
return 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsBackground() Style {
|
||||||
|
return Style{
|
||||||
|
FillColor: DefaultBackgroundColor,
|
||||||
|
StrokeColor: DefaultBackgroundStrokeColor,
|
||||||
|
StrokeWidth: DefaultStrokeWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) styleDefaultsTitle() Style {
|
||||||
|
return pc.TitleStyle.InheritFrom(Style{
|
||||||
|
FontColor: ColorWhite,
|
||||||
|
Font: pc.GetFont(),
|
||||||
|
FontSize: pc.getTitleFontSize(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc PieChart) getTitleFontSize() float64 {
|
||||||
|
effectiveDimension := Math.MinInt(pc.GetWidth(), pc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24
|
||||||
|
} else if effectiveDimension >= 512 {
|
||||||
|
return 18
|
||||||
|
} else if effectiveDimension >= 256 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box returns the chart bounds as a box.
|
||||||
|
func (pc PieChart) Box() Box {
|
||||||
|
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
|
||||||
|
dpb := pc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: pc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
||||||
|
Left: pc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
|
||||||
|
Right: pc.GetWidth() - dpr,
|
||||||
|
Bottom: pc.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
31
pie_chart_test.go
Normal file
31
pie_chart_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPieChart(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
pie := PieChart{
|
||||||
|
Canvas: Style{
|
||||||
|
FillColor: ColorLightGray,
|
||||||
|
},
|
||||||
|
Values: []Value{
|
||||||
|
{Value: 10, Label: "Blue"},
|
||||||
|
{Value: 9, Label: "Green"},
|
||||||
|
{Value: 8, Label: "Gray"},
|
||||||
|
{Value: 7, Label: "Orange"},
|
||||||
|
{Value: 6, Label: "HEANG"},
|
||||||
|
{Value: 5, Label: "??"},
|
||||||
|
{Value: 2, Label: "!!"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
b := bytes.NewBuffer([]byte{})
|
||||||
|
pie.Render(PNG, b)
|
||||||
|
assert.NotZero(b.Len())
|
||||||
|
}
|
61
range.go
61
range.go
|
@ -1,44 +1,41 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
import (
|
// NameProvider is a type that returns a name.
|
||||||
"fmt"
|
type NameProvider interface {
|
||||||
"math"
|
GetName() string
|
||||||
)
|
|
||||||
|
|
||||||
// Range represents a boundary for a set of numbers.
|
|
||||||
type Range struct {
|
|
||||||
Min float64
|
|
||||||
Max float64
|
|
||||||
Domain int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsZero returns if the range has been set or not.
|
// StyleProvider is a type that returns a style.
|
||||||
func (r Range) IsZero() bool {
|
type StyleProvider interface {
|
||||||
return (r.Min == 0 || math.IsNaN(r.Min)) &&
|
GetStyle() Style
|
||||||
(r.Max == 0 || math.IsNaN(r.Max)) &&
|
|
||||||
r.Domain == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delta returns the difference between the min and max value.
|
// IsZeroable is a type that returns if it's been set or not.
|
||||||
func (r Range) Delta() float64 {
|
type IsZeroable interface {
|
||||||
return r.Max - r.Min
|
IsZero() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a simple string for the range.
|
// Stringable is a type that has a string representation.
|
||||||
func (r Range) String() string {
|
type Stringable interface {
|
||||||
return fmt.Sprintf("Range [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
|
String() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate maps a given value into the range space.
|
// Range is a
|
||||||
func (r Range) Translate(value float64) int {
|
type Range interface {
|
||||||
normalized := value - r.Min
|
Stringable
|
||||||
ratio := normalized / r.Delta()
|
IsZeroable
|
||||||
return int(math.Ceil(ratio * float64(r.Domain)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoundedRangeBounds returns some `prettified` range bounds.
|
GetMin() float64
|
||||||
func (r Range) GetRoundedRangeBounds() (min, max float64) {
|
SetMin(min float64)
|
||||||
delta := r.Max - r.Min
|
|
||||||
roundTo := GetRoundToForDelta(delta)
|
GetMax() float64
|
||||||
return RoundDown(r.Min, roundTo), RoundUp(r.Max, roundTo)
|
SetMax(max float64)
|
||||||
|
|
||||||
|
GetDelta() float64
|
||||||
|
|
||||||
|
GetDomain() int
|
||||||
|
SetDomain(domain int)
|
||||||
|
|
||||||
|
// Translate the range to the domain.
|
||||||
|
Translate(value float64) int
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,16 @@ func (rr *rasterRenderer) LineTo(x, y int) {
|
||||||
rr.gc.LineTo(float64(x), float64(y))
|
rr.gc.LineTo(float64(x), float64(y))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuadCurveTo implements the interface method.
|
||||||
|
func (rr *rasterRenderer) QuadCurveTo(cx, cy, x, y int) {
|
||||||
|
rr.gc.QuadCurveTo(float64(cx), float64(cy), float64(x), float64(y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArcTo implements the interface method.
|
||||||
|
func (rr *rasterRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
|
||||||
|
rr.gc.ArcTo(float64(cx), float64(cy), rx, ry, startAngle, delta)
|
||||||
|
}
|
||||||
|
|
||||||
// Close implements the interface method.
|
// Close implements the interface method.
|
||||||
func (rr *rasterRenderer) Close() {
|
func (rr *rasterRenderer) Close() {
|
||||||
rr.gc.Close()
|
rr.gc.Close()
|
||||||
|
|
|
@ -34,6 +34,14 @@ type Renderer interface {
|
||||||
// from the previous point.
|
// from the previous point.
|
||||||
LineTo(x, y int)
|
LineTo(x, y int)
|
||||||
|
|
||||||
|
// QuadCurveTo draws a quad curve.
|
||||||
|
// cx and cy represent the bezier "control points".
|
||||||
|
QuadCurveTo(cx, cy, x, y int)
|
||||||
|
|
||||||
|
// ArcTo draws an arc with a given center (cx,cy)
|
||||||
|
// a given set of radii (rx,ry), a startAngle and delta (in radians).
|
||||||
|
ArcTo(cx, cy int, rx, ry, startAngle, delta float64)
|
||||||
|
|
||||||
// Close finalizes a shape as drawn by LineTo.
|
// Close finalizes a shape as drawn by LineTo.
|
||||||
Close()
|
Close()
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -200,6 +201,40 @@ func (rb *RingBuffer) String() string {
|
||||||
return strings.Join(values, " <= ")
|
return strings.Join(values, " <= ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Average returns the float average of the values in the buffer.
|
||||||
|
func (rb *RingBuffer) Average() float64 {
|
||||||
|
var accum float64
|
||||||
|
rb.Each(func(v interface{}) {
|
||||||
|
if typed, isTyped := v.(float64); isTyped {
|
||||||
|
accum += typed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return accum / float64(rb.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variance computes the variance of the buffer.
|
||||||
|
func (rb *RingBuffer) Variance() float64 {
|
||||||
|
if rb.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var variance float64
|
||||||
|
m := rb.Average()
|
||||||
|
|
||||||
|
rb.Each(func(v interface{}) {
|
||||||
|
if n, isTyped := v.(float64); isTyped {
|
||||||
|
variance += (float64(n) - m) * (float64(n) - m)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return variance / float64(rb.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdDev returns the standard deviation.
|
||||||
|
func (rb *RingBuffer) StdDev() float64 {
|
||||||
|
return math.Pow(rb.Variance(), 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
func arrayClear(source []interface{}, index, length int) {
|
func arrayClear(source []interface{}, index, length int) {
|
||||||
for x := 0; x < length; x++ {
|
for x := 0; x < length; x++ {
|
||||||
absoluteIndex := x + index
|
absoluteIndex := x + index
|
||||||
|
|
55
sequence.go
Normal file
55
sequence.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Sequence contains some sequence utilities.
|
||||||
|
// These utilities can be useful for generating test data.
|
||||||
|
Sequence = &sequence{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type sequence struct{}
|
||||||
|
|
||||||
|
// Float64 produces an array of floats from [start,end] by optional steps.
|
||||||
|
func (s sequence) Float64(start, end float64, steps ...float64) []float64 {
|
||||||
|
var values []float64
|
||||||
|
step := 1.0
|
||||||
|
if len(steps) > 0 {
|
||||||
|
step = steps[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if start < end {
|
||||||
|
for x := start; x <= end; x += step {
|
||||||
|
values = append(values, x)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for x := start; x >= end; x = x - step {
|
||||||
|
values = append(values, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random generates a fixed length sequence of random values between (0, scale).
|
||||||
|
func (s sequence) Random(samples int, scale float64) []float64 {
|
||||||
|
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
||||||
|
values := make([]float64, samples)
|
||||||
|
|
||||||
|
for x := 0; x < samples; x++ {
|
||||||
|
values[x] = rnd.Float64() * scale
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days generates a sequence of timestamps by day, from -days to today.
|
||||||
|
func (s sequence) Days(days int) []time.Time {
|
||||||
|
var values []time.Time
|
||||||
|
for day := days; day >= 0; day-- {
|
||||||
|
values = append(values, time.Now().AddDate(0, 0, -day))
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
17
sequence_test.go
Normal file
17
sequence_test.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSequenceFloat64(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
asc := Sequence.Float64(1.0, 10.0)
|
||||||
|
assert.Len(asc, 10)
|
||||||
|
|
||||||
|
desc := Sequence.Float64(10.0, 1.0)
|
||||||
|
assert.Len(desc, 10)
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ package chart
|
||||||
// Series is an alias to Renderable.
|
// Series is an alias to Renderable.
|
||||||
type Series interface {
|
type Series interface {
|
||||||
GetName() string
|
GetName() string
|
||||||
GetYAxis() YAxisType
|
GetYAxis() yAxisType
|
||||||
GetStyle() Style
|
GetStyle() Style
|
||||||
Render(r Renderer, canvasBox Box, xrange, yrange Range, s Style)
|
Render(r Renderer, canvasBox Box, xrange, yrange Range, s Style)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ const (
|
||||||
type SMASeries struct {
|
type SMASeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
|
|
||||||
Period int
|
Period int
|
||||||
InnerSeries ValueProvider
|
InnerSeries ValueProvider
|
||||||
|
@ -26,7 +26,7 @@ func (sma SMASeries) GetStyle() Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (sma SMASeries) GetYAxis() YAxisType {
|
func (sma SMASeries) GetYAxis() yAxisType {
|
||||||
return sma.YAxis
|
return sma.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,17 @@ func (sma SMASeries) Len() int {
|
||||||
return sma.InnerSeries.Len()
|
return sma.InnerSeries.Len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPeriod returns the window size.
|
||||||
|
func (sma SMASeries) GetPeriod(defaults ...int) int {
|
||||||
|
if sma.Period == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultSimpleMovingAveragePeriod
|
||||||
|
}
|
||||||
|
return sma.Period
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
@ -59,20 +70,9 @@ func (sma SMASeries) GetLastValue() (x, y float64) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPeriod returns the window size.
|
|
||||||
func (sma SMASeries) GetPeriod(defaults ...int) int {
|
|
||||||
if sma.Period == 0 {
|
|
||||||
if len(defaults) > 0 {
|
|
||||||
return defaults[0]
|
|
||||||
}
|
|
||||||
return DefaultSimpleMovingAveragePeriod
|
|
||||||
}
|
|
||||||
return sma.Period
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sma SMASeries) getAverage(index int) float64 {
|
func (sma SMASeries) getAverage(index int) float64 {
|
||||||
period := sma.GetPeriod()
|
period := sma.GetPeriod()
|
||||||
floor := MaxInt(0, index-period)
|
floor := Math.MaxInt(0, index-period)
|
||||||
var accum float64
|
var accum float64
|
||||||
var count float64
|
var count float64
|
||||||
for x := index; x >= floor; x-- {
|
for x := index; x >= floor; x-- {
|
||||||
|
@ -85,6 +85,6 @@ func (sma SMASeries) getAverage(index int) float64 {
|
||||||
|
|
||||||
// Render renders the series.
|
// Render renders the series.
|
||||||
func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
style := sma.Style.WithDefaultsFrom(defaults)
|
style := sma.Style.InheritFrom(defaults)
|
||||||
DrawLineSeries(r, canvasBox, xrange, yrange, style, sma)
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, sma)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,14 @@ type mockValueProvider struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mockValueProvider) Len() int {
|
func (m mockValueProvider) Len() int {
|
||||||
return MinInt(len(m.X), len(m.Y))
|
return Math.MinInt(len(m.X), len(m.Y))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mockValueProvider) GetValue(index int) (x, y float64) {
|
func (m mockValueProvider) GetValue(index int) (x, y float64) {
|
||||||
if index < 0 {
|
if index < 0 {
|
||||||
panic("negative index at GetValue()")
|
panic("negative index at GetValue()")
|
||||||
}
|
}
|
||||||
if index > MinInt(len(m.X), len(m.Y)) {
|
if index > Math.MinInt(len(m.X), len(m.Y)) {
|
||||||
panic("index is outside the length of m.X or m.Y")
|
panic("index is outside the length of m.X or m.Y")
|
||||||
}
|
}
|
||||||
x = m.X[index]
|
x = m.X[index]
|
||||||
|
@ -31,8 +31,8 @@ func TestSMASeriesGetValue(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
mockSeries := mockValueProvider{
|
mockSeries := mockValueProvider{
|
||||||
Seq(1.0, 10.0),
|
Sequence.Float64(1.0, 10.0),
|
||||||
Seq(10, 1.0),
|
Sequence.Float64(10, 1.0),
|
||||||
}
|
}
|
||||||
assert.Equal(10, mockSeries.Len())
|
assert.Equal(10, mockSeries.Len())
|
||||||
|
|
||||||
|
@ -62,8 +62,8 @@ func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
mockSeries := mockValueProvider{
|
mockSeries := mockValueProvider{
|
||||||
Seq(1.0, 10.0),
|
Sequence.Float64(1.0, 10.0),
|
||||||
Seq(10, 1.0),
|
Sequence.Float64(10, 1.0),
|
||||||
}
|
}
|
||||||
assert.Equal(10, mockSeries.Len())
|
assert.Equal(10, mockSeries.Len())
|
||||||
|
|
||||||
|
@ -88,8 +88,8 @@ func TestSMASeriesGetLastValue(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
mockSeries := mockValueProvider{
|
mockSeries := mockValueProvider{
|
||||||
Seq(1.0, 100.0),
|
Sequence.Float64(1.0, 100.0),
|
||||||
Seq(100, 1.0),
|
Sequence.Float64(100, 1.0),
|
||||||
}
|
}
|
||||||
assert.Equal(100, mockSeries.Len())
|
assert.Equal(100, mockSeries.Len())
|
||||||
|
|
||||||
|
|
335
stacked_bar_chart.go
Normal file
335
stacked_bar_chart.go
Normal file
|
@ -0,0 +1,335 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackedBar is a bar within a StackedBarChart.
|
||||||
|
type StackedBar struct {
|
||||||
|
Name string
|
||||||
|
Width int
|
||||||
|
Values []Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the width of the bar.
|
||||||
|
func (sb StackedBar) GetWidth() int {
|
||||||
|
if sb.Width == 0 {
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
return sb.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackedBarChart is a chart that draws sections of a bar based on percentages.
|
||||||
|
type StackedBarChart struct {
|
||||||
|
Title string
|
||||||
|
TitleStyle Style
|
||||||
|
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
DPI float64
|
||||||
|
|
||||||
|
Background Style
|
||||||
|
Canvas Style
|
||||||
|
|
||||||
|
XAxis Style
|
||||||
|
YAxis Style
|
||||||
|
|
||||||
|
BarSpacing int
|
||||||
|
|
||||||
|
Font *truetype.Font
|
||||||
|
defaultFont *truetype.Font
|
||||||
|
|
||||||
|
Bars []StackedBar
|
||||||
|
Elements []Renderable
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDPI returns the dpi for the chart.
|
||||||
|
func (sbc StackedBarChart) GetDPI(defaults ...float64) float64 {
|
||||||
|
if sbc.DPI == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultDPI
|
||||||
|
}
|
||||||
|
return sbc.DPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFont returns the text font.
|
||||||
|
func (sbc StackedBarChart) GetFont() *truetype.Font {
|
||||||
|
if sbc.Font == nil {
|
||||||
|
return sbc.defaultFont
|
||||||
|
}
|
||||||
|
return sbc.Font
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the chart width or the default value.
|
||||||
|
func (sbc StackedBarChart) GetWidth() int {
|
||||||
|
if sbc.Width == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return sbc.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the chart height or the default value.
|
||||||
|
func (sbc StackedBarChart) GetHeight() int {
|
||||||
|
if sbc.Height == 0 {
|
||||||
|
return DefaultChartWidth
|
||||||
|
}
|
||||||
|
return sbc.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBarSpacing returns the spacing between bars.
|
||||||
|
func (sbc StackedBarChart) GetBarSpacing() int {
|
||||||
|
if sbc.BarSpacing == 0 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return sbc.BarSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
|
func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
if len(sbc.Bars) == 0 {
|
||||||
|
return errors.New("Please provide at least one bar.")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := rp(sbc.GetWidth(), sbc.GetHeight())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if sbc.Font == nil {
|
||||||
|
defaultFont, err := GetDefaultFont()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sbc.defaultFont = defaultFont
|
||||||
|
}
|
||||||
|
r.SetDPI(sbc.GetDPI(DefaultDPI))
|
||||||
|
|
||||||
|
canvasBox := sbc.getAdjustedCanvasBox(r, sbc.getDefaultCanvasBox())
|
||||||
|
sbc.drawBars(r, canvasBox)
|
||||||
|
sbc.drawXAxis(r, canvasBox)
|
||||||
|
sbc.drawYAxis(r, canvasBox)
|
||||||
|
|
||||||
|
sbc.drawTitle(r)
|
||||||
|
for _, a := range sbc.Elements {
|
||||||
|
a(r, canvasBox, sbc.styleDefaultsElements())
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawBars(r Renderer, canvasBox Box) {
|
||||||
|
xoffset := canvasBox.Left
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
sbc.drawBar(r, canvasBox, xoffset, bar)
|
||||||
|
xoffset += (sbc.GetBarSpacing() + bar.GetWidth())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar StackedBar) int {
|
||||||
|
barSpacing2 := sbc.GetBarSpacing() >> 1
|
||||||
|
bxl := xoffset + barSpacing2
|
||||||
|
bxr := bxl + bar.GetWidth()
|
||||||
|
|
||||||
|
normalizedBarComponents := Values(bar.Values).Normalize()
|
||||||
|
yoffset := canvasBox.Top
|
||||||
|
for index, bv := range normalizedBarComponents {
|
||||||
|
barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Height())))
|
||||||
|
barBox := Box{
|
||||||
|
Top: yoffset,
|
||||||
|
Left: bxl,
|
||||||
|
Right: bxr,
|
||||||
|
Bottom: Math.MinInt(yoffset+barHeight, canvasBox.Bottom-DefaultStrokeWidth),
|
||||||
|
}
|
||||||
|
Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)))
|
||||||
|
yoffset += barHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return bxr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) {
|
||||||
|
if sbc.XAxis.Show {
|
||||||
|
axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
|
||||||
|
Bottom: sbc.GetHeight(),
|
||||||
|
}
|
||||||
|
if len(bar.Name) > 0 {
|
||||||
|
Draw.TextWithin(r, bar.Name, barLabelBox, axisStyle)
|
||||||
|
}
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
r.MoveTo(barLabelBox.Right, canvasBox.Bottom)
|
||||||
|
r.LineTo(barLabelBox.Right, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
|
r.Stroke()
|
||||||
|
cursor += bar.GetWidth() + sbc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) {
|
||||||
|
if sbc.YAxis.Show {
|
||||||
|
axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
r.MoveTo(canvasBox.Right, canvasBox.Top)
|
||||||
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
|
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
ticks := Sequence.Float64(1.0, 0.0, 0.2)
|
||||||
|
for _, t := range ticks {
|
||||||
|
axisStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||||
|
ty := canvasBox.Bottom - int(t*float64(canvasBox.Height()))
|
||||||
|
r.MoveTo(canvasBox.Right, ty)
|
||||||
|
r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
axisStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
text := fmt.Sprintf("%0.0f%%", t*100)
|
||||||
|
|
||||||
|
tb := r.MeasureText(text)
|
||||||
|
Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) drawTitle(r Renderer) {
|
||||||
|
if len(sbc.Title) > 0 && sbc.TitleStyle.Show {
|
||||||
|
Draw.TextWithin(r, sbc.Title, sbc.Box(), sbc.styleDefaultsTitle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getDefaultCanvasBox() Box {
|
||||||
|
return sbc.Box()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box {
|
||||||
|
var totalWidth int
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
totalWidth += bar.GetWidth() + sbc.GetBarSpacing()
|
||||||
|
}
|
||||||
|
|
||||||
|
if sbc.XAxis.Show {
|
||||||
|
xaxisHeight := DefaultVerticalTickHeight
|
||||||
|
|
||||||
|
axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes())
|
||||||
|
axisStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
|
cursor := canvasBox.Left
|
||||||
|
for _, bar := range sbc.Bars {
|
||||||
|
if len(bar.Name) > 0 {
|
||||||
|
barLabelBox := Box{
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Left: cursor,
|
||||||
|
Right: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
|
||||||
|
Bottom: sbc.GetHeight(),
|
||||||
|
}
|
||||||
|
lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle)
|
||||||
|
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||||
|
|
||||||
|
xaxisHeight = Math.MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left,
|
||||||
|
Right: canvasBox.Left + totalWidth,
|
||||||
|
Bottom: sbc.GetHeight() - xaxisHeight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Box{
|
||||||
|
Top: canvasBox.Top,
|
||||||
|
Left: canvasBox.Left,
|
||||||
|
Right: canvasBox.Left + totalWidth,
|
||||||
|
Bottom: canvasBox.Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box returns the chart bounds as a box.
|
||||||
|
func (sbc StackedBarChart) Box() Box {
|
||||||
|
dpr := sbc.Background.Padding.GetRight(10)
|
||||||
|
dpb := sbc.Background.Padding.GetBottom(50)
|
||||||
|
|
||||||
|
return Box{
|
||||||
|
Top: 20,
|
||||||
|
Left: 20,
|
||||||
|
Right: sbc.GetWidth() - dpr,
|
||||||
|
Bottom: sbc.GetHeight() - dpb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsStackedBarValue(index int) Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: GetAlternateColor(index),
|
||||||
|
StrokeWidth: 3.0,
|
||||||
|
FillColor: GetAlternateColor(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsTitle() Style {
|
||||||
|
return sbc.TitleStyle.InheritFrom(Style{
|
||||||
|
FontColor: DefaultTextColor,
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
FontSize: sbc.getTitleFontSize(),
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) getTitleFontSize() float64 {
|
||||||
|
effectiveDimension := Math.MinInt(sbc.GetWidth(), sbc.GetHeight())
|
||||||
|
if effectiveDimension >= 2048 {
|
||||||
|
return 48
|
||||||
|
} else if effectiveDimension >= 1024 {
|
||||||
|
return 24
|
||||||
|
} else if effectiveDimension >= 512 {
|
||||||
|
return 18
|
||||||
|
} else if effectiveDimension >= 256 {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
return 10
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sbc StackedBarChart) styleDefaultsAxes() Style {
|
||||||
|
return Style{
|
||||||
|
StrokeColor: DefaultAxisColor,
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
FontSize: DefaultAxisFontSize,
|
||||||
|
FontColor: DefaultAxisColor,
|
||||||
|
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||||
|
TextVerticalAlign: TextVerticalAlignTop,
|
||||||
|
TextWrap: TextWrapWord,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (sbc StackedBarChart) styleDefaultsElements() Style {
|
||||||
|
return Style{
|
||||||
|
Font: sbc.GetFont(),
|
||||||
|
}
|
||||||
|
}
|
171
style.go
171
style.go
|
@ -21,6 +21,11 @@ type Style struct {
|
||||||
FontSize float64
|
FontSize float64
|
||||||
FontColor drawing.Color
|
FontColor drawing.Color
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
|
|
||||||
|
TextHorizontalAlign textHorizontalAlign
|
||||||
|
TextVerticalAlign textVerticalAlign
|
||||||
|
TextWrap textWrap
|
||||||
|
TextLineSpacing int
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsZero returns if the object is set or not.
|
// IsZero returns if the object is set or not.
|
||||||
|
@ -28,6 +33,7 @@ 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.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontColor.IsZero() && s.FontSize == 0 && s.Font == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns a text representation of the style.
|
||||||
func (s Style) String() string {
|
func (s Style) String() string {
|
||||||
if s.IsZero() {
|
if s.IsZero() {
|
||||||
return "{}"
|
return "{}"
|
||||||
|
@ -78,11 +84,11 @@ func (s Style) String() string {
|
||||||
if s.FontSize != 0 {
|
if s.FontSize != 0 {
|
||||||
output = append(output, fmt.Sprintf("\"font_size\": \"%0.2fpt\"", s.FontSize))
|
output = append(output, fmt.Sprintf("\"font_size\": \"%0.2fpt\"", s.FontSize))
|
||||||
} else {
|
} else {
|
||||||
output = append(output, "\"fill_color\": null")
|
output = append(output, "\"font_size\": null")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.FillColor.IsZero() {
|
if !s.FontColor.IsZero() {
|
||||||
output = append(output, fmt.Sprintf("\"font_color\": %s", s.FillColor.String()))
|
output = append(output, fmt.Sprintf("\"font_color\": %s", s.FontColor.String()))
|
||||||
} else {
|
} else {
|
||||||
output = append(output, "\"font_color\": null")
|
output = append(output, "\"font_color\": null")
|
||||||
}
|
}
|
||||||
|
@ -184,8 +190,78 @@ func (s Style) GetPadding(defaults ...Box) Box {
|
||||||
return s.Padding
|
return s.Padding
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithDefaultsFrom coalesces two styles into a new style.
|
// GetTextHorizontalAlign returns the horizontal alignment.
|
||||||
func (s Style) WithDefaultsFrom(defaults Style) (final Style) {
|
func (s Style) GetTextHorizontalAlign(defaults ...textHorizontalAlign) textHorizontalAlign {
|
||||||
|
if s.TextHorizontalAlign == TextHorizontalAlignUnset {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return TextHorizontalAlignUnset
|
||||||
|
}
|
||||||
|
return s.TextHorizontalAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextVerticalAlign returns the vertical alignment.
|
||||||
|
func (s Style) GetTextVerticalAlign(defaults ...textVerticalAlign) textVerticalAlign {
|
||||||
|
if s.TextVerticalAlign == TextVerticalAlignUnset {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return TextVerticalAlignUnset
|
||||||
|
}
|
||||||
|
return s.TextVerticalAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextWrap returns the word wrap.
|
||||||
|
func (s Style) GetTextWrap(defaults ...textWrap) textWrap {
|
||||||
|
if s.TextWrap == TextWrapUnset {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return TextWrapUnset
|
||||||
|
}
|
||||||
|
return s.TextWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextLineSpacing returns the spacing in pixels between lines of text (vertically).
|
||||||
|
func (s Style) GetTextLineSpacing(defaults ...int) int {
|
||||||
|
if s.TextLineSpacing == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultLineSpacing
|
||||||
|
}
|
||||||
|
return s.TextLineSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteToRenderer passes the style's options to a renderer.
|
||||||
|
func (s Style) WriteToRenderer(r Renderer) {
|
||||||
|
r.SetStrokeColor(s.GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(s.GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||||
|
r.SetFillColor(s.GetFillColor())
|
||||||
|
r.SetFont(s.GetFont())
|
||||||
|
r.SetFontColor(s.GetFontColor())
|
||||||
|
r.SetFontSize(s.GetFontSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer.
|
||||||
|
func (s Style) WriteDrawingOptionsToRenderer(r Renderer) {
|
||||||
|
r.SetStrokeColor(s.GetStrokeColor())
|
||||||
|
r.SetStrokeWidth(s.GetStrokeWidth())
|
||||||
|
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||||
|
r.SetFillColor(s.GetFillColor())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTextOptionsToRenderer passes just the text style options to a renderer.
|
||||||
|
func (s Style) WriteTextOptionsToRenderer(r Renderer) {
|
||||||
|
r.SetFont(s.GetFont())
|
||||||
|
r.SetFontColor(s.GetFontColor())
|
||||||
|
r.SetFontSize(s.GetFontSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
// InheritFrom coalesces two styles into a new style.
|
||||||
|
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)
|
||||||
|
@ -194,47 +270,15 @@ func (s Style) WithDefaultsFrom(defaults Style) (final Style) {
|
||||||
final.FontSize = s.GetFontSize(defaults.FontSize)
|
final.FontSize = s.GetFontSize(defaults.FontSize)
|
||||||
final.Font = s.GetFont(defaults.Font)
|
final.Font = s.GetFont(defaults.Font)
|
||||||
final.Padding = s.GetPadding(defaults.Padding)
|
final.Padding = s.GetPadding(defaults.Padding)
|
||||||
|
final.TextHorizontalAlign = s.GetTextHorizontalAlign(defaults.TextHorizontalAlign)
|
||||||
|
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
|
||||||
|
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
|
||||||
|
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG returns the style as a svg style string.
|
// GetStrokeOptions returns the stroke components.
|
||||||
func (s Style) SVG(dpi float64) string {
|
func (s Style) GetStrokeOptions() Style {
|
||||||
sw := s.StrokeWidth
|
|
||||||
sc := s.StrokeColor
|
|
||||||
fc := s.FillColor
|
|
||||||
fs := s.FontSize
|
|
||||||
fnc := s.FontColor
|
|
||||||
|
|
||||||
strokeWidthText := "stroke-width:0"
|
|
||||||
if sw != 0 {
|
|
||||||
strokeWidthText = "stroke-width:" + fmt.Sprintf("%d", int(sw))
|
|
||||||
}
|
|
||||||
|
|
||||||
strokeText := "stroke:none"
|
|
||||||
if !sc.IsZero() {
|
|
||||||
strokeText = "stroke:" + sc.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
fillText := "fill:none"
|
|
||||||
if !fc.IsZero() {
|
|
||||||
fillText = "fill:" + fc.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
fontSizeText := ""
|
|
||||||
if fs != 0 {
|
|
||||||
fontSizeText = "font-size:" + fmt.Sprintf("%.1fpx", drawing.PointsToPixels(dpi, fs))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !fnc.IsZero() {
|
|
||||||
fillText = "fill:" + fnc.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
fontText := s.SVGFontFace()
|
|
||||||
return strings.Join([]string{strokeWidthText, strokeText, fillText, fontSizeText, fontText}, ";")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SVGStroke returns the stroke components.
|
|
||||||
func (s Style) SVGStroke() Style {
|
|
||||||
return Style{
|
return Style{
|
||||||
StrokeDashArray: s.StrokeDashArray,
|
StrokeDashArray: s.StrokeDashArray,
|
||||||
StrokeColor: s.StrokeColor,
|
StrokeColor: s.StrokeColor,
|
||||||
|
@ -242,15 +286,15 @@ func (s Style) SVGStroke() Style {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVGFill returns the fill components.
|
// GetFillOptions returns the fill components.
|
||||||
func (s Style) SVGFill() Style {
|
func (s Style) GetFillOptions() Style {
|
||||||
return Style{
|
return Style{
|
||||||
FillColor: s.FillColor,
|
FillColor: s.FillColor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVGFillAndStroke returns the fill and stroke components.
|
// GetFillAndStrokeOptions returns the fill and stroke components.
|
||||||
func (s Style) SVGFillAndStroke() Style {
|
func (s Style) GetFillAndStrokeOptions() Style {
|
||||||
return Style{
|
return Style{
|
||||||
StrokeDashArray: s.StrokeDashArray,
|
StrokeDashArray: s.StrokeDashArray,
|
||||||
FillColor: s.FillColor,
|
FillColor: s.FillColor,
|
||||||
|
@ -259,34 +303,15 @@ func (s Style) SVGFillAndStroke() Style {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVGText returns just the text components of the style.
|
// GetTextOptions returns just the text components of the style.
|
||||||
func (s Style) SVGText() Style {
|
func (s Style) GetTextOptions() Style {
|
||||||
return Style{
|
return Style{
|
||||||
FontColor: s.FontColor,
|
FontColor: s.FontColor,
|
||||||
FontSize: s.FontSize,
|
FontSize: s.FontSize,
|
||||||
|
Font: s.Font,
|
||||||
|
TextHorizontalAlign: s.TextHorizontalAlign,
|
||||||
|
TextVerticalAlign: s.TextVerticalAlign,
|
||||||
|
TextWrap: s.TextWrap,
|
||||||
|
TextLineSpacing: s.TextLineSpacing,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVGFontFace returns the font face for the style.
|
|
||||||
func (s Style) SVGFontFace() string {
|
|
||||||
family := "sans-serif"
|
|
||||||
if s.GetFont() != nil {
|
|
||||||
name := s.GetFont().Name(truetype.NameIDFontFamily)
|
|
||||||
if len(name) != 0 {
|
|
||||||
family = fmt.Sprintf(`'%s',%s`, name, family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("font-family:%s", family)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SVGStrokeDashArray returns the stroke-dasharray property of a style.
|
|
||||||
func (s Style) SVGStrokeDashArray() string {
|
|
||||||
if len(s.StrokeDashArray) > 0 {
|
|
||||||
var values []string
|
|
||||||
for _, v := range s.StrokeDashArray {
|
|
||||||
values = append(values, fmt.Sprintf("%0.1f", v))
|
|
||||||
}
|
|
||||||
return "stroke-dasharray=\"" + strings.Join(values, ", ") + "\""
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/blendlabs/go-assert"
|
"github.com/blendlabs/go-assert"
|
||||||
|
@ -142,33 +141,11 @@ func TestStyleWithDefaultsFrom(t *testing.T) {
|
||||||
Padding: DefaultBackgroundPadding,
|
Padding: DefaultBackgroundPadding,
|
||||||
}
|
}
|
||||||
|
|
||||||
coalesced := unset.WithDefaultsFrom(set)
|
coalesced := unset.InheritFrom(set)
|
||||||
assert.Equal(set, coalesced)
|
assert.Equal(set, coalesced)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStyleSVG(t *testing.T) {
|
func TestStyleGetStrokeOptions(t *testing.T) {
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
f, err := GetDefaultFont()
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
set := Style{
|
|
||||||
StrokeColor: drawing.ColorWhite,
|
|
||||||
StrokeWidth: 5.0,
|
|
||||||
FillColor: drawing.ColorWhite,
|
|
||||||
FontColor: drawing.ColorWhite,
|
|
||||||
Font: f,
|
|
||||||
Padding: DefaultBackgroundPadding,
|
|
||||||
}
|
|
||||||
|
|
||||||
svgString := set.SVG(DefaultDPI)
|
|
||||||
assert.NotEmpty(svgString)
|
|
||||||
assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)"))
|
|
||||||
assert.True(strings.Contains(svgString, "stroke-width:5"))
|
|
||||||
assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStyleSVGStroke(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
set := Style{
|
set := Style{
|
||||||
|
@ -178,14 +155,14 @@ func TestStyleSVGStroke(t *testing.T) {
|
||||||
FontColor: drawing.ColorWhite,
|
FontColor: drawing.ColorWhite,
|
||||||
Padding: DefaultBackgroundPadding,
|
Padding: DefaultBackgroundPadding,
|
||||||
}
|
}
|
||||||
svgStroke := set.SVGStroke()
|
svgStroke := set.GetStrokeOptions()
|
||||||
assert.False(svgStroke.StrokeColor.IsZero())
|
assert.False(svgStroke.StrokeColor.IsZero())
|
||||||
assert.NotZero(svgStroke.StrokeWidth)
|
assert.NotZero(svgStroke.StrokeWidth)
|
||||||
assert.True(svgStroke.FillColor.IsZero())
|
assert.True(svgStroke.FillColor.IsZero())
|
||||||
assert.True(svgStroke.FontColor.IsZero())
|
assert.True(svgStroke.FontColor.IsZero())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStyleSVGFill(t *testing.T) {
|
func TestStyleGetFillOptions(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
set := Style{
|
set := Style{
|
||||||
|
@ -195,14 +172,14 @@ func TestStyleSVGFill(t *testing.T) {
|
||||||
FontColor: drawing.ColorWhite,
|
FontColor: drawing.ColorWhite,
|
||||||
Padding: DefaultBackgroundPadding,
|
Padding: DefaultBackgroundPadding,
|
||||||
}
|
}
|
||||||
svgFill := set.SVGFill()
|
svgFill := set.GetFillOptions()
|
||||||
assert.False(svgFill.FillColor.IsZero())
|
assert.False(svgFill.FillColor.IsZero())
|
||||||
assert.Zero(svgFill.StrokeWidth)
|
assert.Zero(svgFill.StrokeWidth)
|
||||||
assert.True(svgFill.StrokeColor.IsZero())
|
assert.True(svgFill.StrokeColor.IsZero())
|
||||||
assert.True(svgFill.FontColor.IsZero())
|
assert.True(svgFill.FontColor.IsZero())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStyleSVGFillAndStroke(t *testing.T) {
|
func TestStyleGetFillAndStrokeOptions(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
set := Style{
|
set := Style{
|
||||||
|
@ -212,14 +189,14 @@ func TestStyleSVGFillAndStroke(t *testing.T) {
|
||||||
FontColor: drawing.ColorWhite,
|
FontColor: drawing.ColorWhite,
|
||||||
Padding: DefaultBackgroundPadding,
|
Padding: DefaultBackgroundPadding,
|
||||||
}
|
}
|
||||||
svgFillAndStroke := set.SVGFillAndStroke()
|
svgFillAndStroke := set.GetFillAndStrokeOptions()
|
||||||
assert.False(svgFillAndStroke.FillColor.IsZero())
|
assert.False(svgFillAndStroke.FillColor.IsZero())
|
||||||
assert.NotZero(svgFillAndStroke.StrokeWidth)
|
assert.NotZero(svgFillAndStroke.StrokeWidth)
|
||||||
assert.False(svgFillAndStroke.StrokeColor.IsZero())
|
assert.False(svgFillAndStroke.StrokeColor.IsZero())
|
||||||
assert.True(svgFillAndStroke.FontColor.IsZero())
|
assert.True(svgFillAndStroke.FontColor.IsZero())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStyleSVGText(t *testing.T) {
|
func TestStyleGetTextOptions(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
set := Style{
|
set := Style{
|
||||||
|
@ -229,7 +206,7 @@ func TestStyleSVGText(t *testing.T) {
|
||||||
FontColor: drawing.ColorWhite,
|
FontColor: drawing.ColorWhite,
|
||||||
Padding: DefaultBackgroundPadding,
|
Padding: DefaultBackgroundPadding,
|
||||||
}
|
}
|
||||||
svgStroke := set.SVGText()
|
svgStroke := set.GetTextOptions()
|
||||||
assert.True(svgStroke.StrokeColor.IsZero())
|
assert.True(svgStroke.StrokeColor.IsZero())
|
||||||
assert.Zero(svgStroke.StrokeWidth)
|
assert.Zero(svgStroke.StrokeWidth)
|
||||||
assert.True(svgStroke.FillColor.IsZero())
|
assert.True(svgStroke.FillColor.IsZero())
|
||||||
|
|
164
text.go
Normal file
164
text.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// TextHorizontalAlign is an enum for the horizontal alignment options.
|
||||||
|
type textHorizontalAlign int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TextHorizontalAlignUnset is the unset state for text horizontal alignment.
|
||||||
|
TextHorizontalAlignUnset textHorizontalAlign = 0
|
||||||
|
// TextHorizontalAlignLeft aligns a string horizontally so that it's left ligature starts at horizontal pixel 0.
|
||||||
|
TextHorizontalAlignLeft textHorizontalAlign = 1
|
||||||
|
// TextHorizontalAlignCenter left aligns a string horizontally so that there are equal pixels
|
||||||
|
// to the left and to the right of a string within a box.
|
||||||
|
TextHorizontalAlignCenter textHorizontalAlign = 2
|
||||||
|
// TextHorizontalAlignRight right aligns a string horizontally so that the right ligature ends at the right-most pixel
|
||||||
|
// of a box.
|
||||||
|
TextHorizontalAlignRight textHorizontalAlign = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextWrap is an enum for the word wrap options.
|
||||||
|
type textWrap int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TextWrapUnset is the unset state for text wrap options.
|
||||||
|
TextWrapUnset textWrap = 0
|
||||||
|
// TextWrapNone will spill text past horizontal boundaries.
|
||||||
|
TextWrapNone textWrap = 1
|
||||||
|
// TextWrapWord will split a string on words (i.e. spaces) to fit within a horizontal boundary.
|
||||||
|
TextWrapWord textWrap = 2
|
||||||
|
// TextWrapRune will split a string on a rune (i.e. utf-8 codepage) to fit within a horizontal boundary.
|
||||||
|
TextWrapRune textWrap = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextVerticalAlign is an enum for the vertical alignment options.
|
||||||
|
type textVerticalAlign int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TextVerticalAlignUnset is the unset state for vertical alignment options.
|
||||||
|
TextVerticalAlignUnset textVerticalAlign = 0
|
||||||
|
// TextVerticalAlignBaseline aligns text according to the "baseline" of the string, or where a normal ascender begins.
|
||||||
|
TextVerticalAlignBaseline textVerticalAlign = 1
|
||||||
|
// TextVerticalAlignBottom aligns the text according to the lowers pixel of any of the ligatures (ex. g or q both extend below the baseline).
|
||||||
|
TextVerticalAlignBottom textVerticalAlign = 2
|
||||||
|
// TextVerticalAlignMiddle aligns the text so that there is an equal amount of space above and below the top and bottom of the ligatures.
|
||||||
|
TextVerticalAlignMiddle textVerticalAlign = 3
|
||||||
|
// TextVerticalAlignMiddleBaseline aligns the text veritcally so that there is an equal number of pixels above and below the baseline of the string.
|
||||||
|
TextVerticalAlignMiddleBaseline textVerticalAlign = 4
|
||||||
|
// TextVerticalAlignTop alignts the text so that the top of the ligatures are at y-pixel 0 in the container.
|
||||||
|
TextVerticalAlignTop textVerticalAlign = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Text contains utilities for text.
|
||||||
|
Text = &text{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextStyle encapsulates text style options.
|
||||||
|
type TextStyle struct {
|
||||||
|
HorizontalAlign textHorizontalAlign
|
||||||
|
VerticalAlign textVerticalAlign
|
||||||
|
Wrap textWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
type text struct{}
|
||||||
|
|
||||||
|
func (t text) WrapFit(r Renderer, value string, width int, style Style) []string {
|
||||||
|
switch style.TextWrap {
|
||||||
|
case TextWrapRune:
|
||||||
|
return t.WrapFitRune(r, value, width, style)
|
||||||
|
case TextWrapWord:
|
||||||
|
return t.WrapFitWord(r, value, width, style)
|
||||||
|
}
|
||||||
|
return []string{value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t text) WrapFitWord(r Renderer, value string, width int, style Style) []string {
|
||||||
|
style.WriteToRenderer(r)
|
||||||
|
|
||||||
|
var output []string
|
||||||
|
var line string
|
||||||
|
var word string
|
||||||
|
|
||||||
|
var textBox Box
|
||||||
|
|
||||||
|
for _, c := range value {
|
||||||
|
if c == rune('\n') { // commit the line to output
|
||||||
|
output = append(output, t.Trim(line+word))
|
||||||
|
line = ""
|
||||||
|
word = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
textBox = r.MeasureText(line + word + string(c))
|
||||||
|
|
||||||
|
if textBox.Width() >= width {
|
||||||
|
output = append(output, t.Trim(line))
|
||||||
|
line = word
|
||||||
|
word = string(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == rune(' ') || c == rune('\t') {
|
||||||
|
line = line + word + string(c)
|
||||||
|
word = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
word = word + string(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(output, t.Trim(line+word))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t text) WrapFitRune(r Renderer, value string, width int, style Style) []string {
|
||||||
|
style.WriteToRenderer(r)
|
||||||
|
|
||||||
|
var output []string
|
||||||
|
var line string
|
||||||
|
var textBox Box
|
||||||
|
for _, c := range value {
|
||||||
|
if c == rune('\n') {
|
||||||
|
output = append(output, line)
|
||||||
|
line = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
textBox = r.MeasureText(line + string(c))
|
||||||
|
|
||||||
|
if textBox.Width() >= width {
|
||||||
|
output = append(output, line)
|
||||||
|
line = string(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = line + string(c)
|
||||||
|
}
|
||||||
|
return t.appendLast(output, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t text) Trim(value string) string {
|
||||||
|
return strings.Trim(value, " \t\n\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t text) MeasureLines(r Renderer, lines []string, style Style) Box {
|
||||||
|
style.WriteTextOptionsToRenderer(r)
|
||||||
|
var output Box
|
||||||
|
for index, line := range lines {
|
||||||
|
lineBox := r.MeasureText(line)
|
||||||
|
output.Right = Math.MaxInt(lineBox.Right, output.Right)
|
||||||
|
output.Bottom += lineBox.Height()
|
||||||
|
if index < len(lines)-1 {
|
||||||
|
output.Bottom += +style.GetTextLineSpacing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t text) appendLast(lines []string, text string) []string {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return []string{text}
|
||||||
|
}
|
||||||
|
lastLine := lines[len(lines)-1]
|
||||||
|
lines[len(lines)-1] = lastLine + text
|
||||||
|
return lines
|
||||||
|
}
|
60
text_test.go
Normal file
60
text_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTextWrapWord(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
r, err := PNG(1024, 1024)
|
||||||
|
assert.Nil(err)
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
basicTextStyle := Style{Font: f, FontSize: 24}
|
||||||
|
|
||||||
|
output := Text.WrapFitWord(r, "this is a test string", 100, basicTextStyle)
|
||||||
|
assert.NotEmpty(output)
|
||||||
|
assert.Len(output, 3)
|
||||||
|
|
||||||
|
for _, line := range output {
|
||||||
|
basicTextStyle.WriteToRenderer(r)
|
||||||
|
lineBox := r.MeasureText(line)
|
||||||
|
assert.True(lineBox.Width() < 100, line+": "+lineBox.String())
|
||||||
|
}
|
||||||
|
assert.Equal("this is", output[0])
|
||||||
|
assert.Equal("a test", output[1])
|
||||||
|
assert.Equal("string", output[2])
|
||||||
|
|
||||||
|
output = Text.WrapFitWord(r, "foo", 100, basicTextStyle)
|
||||||
|
assert.Len(output, 1)
|
||||||
|
assert.Equal("foo", output[0])
|
||||||
|
|
||||||
|
// test that it handles newlines.
|
||||||
|
output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring", 100, basicTextStyle)
|
||||||
|
assert.Len(output, 5)
|
||||||
|
|
||||||
|
// test that it handles newlines and long lines.
|
||||||
|
output = Text.WrapFitWord(r, "this\nis\na\ntest\nstring that is very long", 100, basicTextStyle)
|
||||||
|
assert.Len(output, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTextWrapRune(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
r, err := PNG(1024, 1024)
|
||||||
|
assert.Nil(err)
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
basicTextStyle := Style{Font: f, FontSize: 24}
|
||||||
|
|
||||||
|
output := Text.WrapFitRune(r, "this is a test string", 150, basicTextStyle)
|
||||||
|
assert.NotEmpty(output)
|
||||||
|
assert.Len(output, 2)
|
||||||
|
assert.Equal("this is a t", output[0])
|
||||||
|
assert.Equal("est string", output[1])
|
||||||
|
}
|
68
tick.go
68
tick.go
|
@ -1,21 +1,10 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
// GenerateTicksWithStep generates a set of ticks.
|
import "math"
|
||||||
func GenerateTicksWithStep(ra Range, step float64, vf ValueFormatter) []Tick {
|
|
||||||
var ticks []Tick
|
|
||||||
min, max := ra.Min, ra.Max
|
|
||||||
for cursor := min; cursor <= max; cursor += step {
|
|
||||||
ticks = append(ticks, Tick{
|
|
||||||
Value: cursor,
|
|
||||||
Label: vf(cursor),
|
|
||||||
})
|
|
||||||
|
|
||||||
// this guard is in place in case step is super, super small.
|
// TicksProvider is a type that provides ticks.
|
||||||
if len(ticks) > DefaultTickCountSanityCheck {
|
type TicksProvider interface {
|
||||||
return ticks
|
GetTicks(vf ValueFormatter) []Tick
|
||||||
}
|
|
||||||
}
|
|
||||||
return ticks
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tick represents a label on an axis.
|
// Tick represents a label on an axis.
|
||||||
|
@ -41,3 +30,52 @@ func (t Ticks) Swap(i, j int) {
|
||||||
func (t Ticks) Less(i, j int) bool {
|
func (t Ticks) Less(i, j int) bool {
|
||||||
return t[i].Value < t[j].Value
|
return t[i].Value < t[j].Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateContinuousTicksWithStep generates a set of ticks.
|
||||||
|
func GenerateContinuousTicksWithStep(ra Range, step float64, vf ValueFormatter, includeMax bool) []Tick {
|
||||||
|
var ticks []Tick
|
||||||
|
min, max := ra.GetMin(), ra.GetMax()
|
||||||
|
for cursor := min; cursor <= max; cursor += step {
|
||||||
|
ticks = append(ticks, Tick{
|
||||||
|
Value: cursor,
|
||||||
|
Label: vf(cursor),
|
||||||
|
})
|
||||||
|
|
||||||
|
// this guard is in place in case step is super, super small.
|
||||||
|
if len(ticks) > DefaultTickCountSanityCheck {
|
||||||
|
return ticks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if includeMax {
|
||||||
|
ticks = append(ticks, Tick{
|
||||||
|
Value: ra.GetMax(),
|
||||||
|
Label: vf(ra.GetMax()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ticks
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateContinuousTickStep calculates the continous range interval between ticks.
|
||||||
|
func CalculateContinuousTickStep(r Renderer, ra Range, isVertical bool, style Style, vf ValueFormatter) float64 {
|
||||||
|
r.SetFont(style.GetFont())
|
||||||
|
r.SetFontSize(style.GetFontSize())
|
||||||
|
if isVertical {
|
||||||
|
label := vf(ra.GetMin())
|
||||||
|
tb := r.MeasureText(label)
|
||||||
|
count := int(math.Ceil(float64(ra.GetDomain()) / float64(tb.Height()+DefaultMinimumTickVerticalSpacing)))
|
||||||
|
return ra.GetDelta() / float64(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// take a cut at determining the 'widest' value.
|
||||||
|
l0 := vf(ra.GetMin())
|
||||||
|
ln := vf(ra.GetMax())
|
||||||
|
ll := l0
|
||||||
|
if len(ln) > len(l0) {
|
||||||
|
ll = ln
|
||||||
|
}
|
||||||
|
llb := r.MeasureText(ll)
|
||||||
|
textWidth := llb.Width()
|
||||||
|
width := textWidth + DefaultMinimumTickHorizontalSpacing
|
||||||
|
count := int(math.Ceil(float64(ra.GetDomain()) / float64(width)))
|
||||||
|
return ra.GetDelta() / float64(count)
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,6 @@ import (
|
||||||
func TestGenerateTicksWithStep(t *testing.T) {
|
func TestGenerateTicksWithStep(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
ticks := GenerateTicksWithStep(Range{Min: 1.0, Max: 10.0, Domain: 100}, 1.0, FloatValueFormatter)
|
ticks := GenerateContinuousTicksWithStep(&ContinuousRange{Min: 1.0, Max: 10.0, Domain: 100}, 1.0, FloatValueFormatter, false)
|
||||||
assert.Len(ticks, 10)
|
assert.Len(ticks, 10)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ type TimeSeries struct {
|
||||||
Name string
|
Name string
|
||||||
Style Style
|
Style Style
|
||||||
|
|
||||||
YAxis YAxisType
|
YAxis yAxisType
|
||||||
|
|
||||||
XValues []time.Time
|
XValues []time.Time
|
||||||
YValues []float64
|
YValues []float64
|
||||||
|
@ -50,12 +50,12 @@ func (ts TimeSeries) GetValueFormatters() (x, y ValueFormatter) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetYAxis returns which YAxis the series draws on.
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
func (ts TimeSeries) GetYAxis() YAxisType {
|
func (ts TimeSeries) GetYAxis() yAxisType {
|
||||||
return ts.YAxis
|
return ts.YAxis
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders the series.
|
// Render renders the series.
|
||||||
func (ts TimeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
func (ts TimeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
style := ts.Style.WithDefaultsFrom(defaults)
|
style := ts.Style.InheritFrom(defaults)
|
||||||
DrawLineSeries(r, canvasBox, xrange, yrange, style, ts)
|
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ts)
|
||||||
}
|
}
|
||||||
|
|
172
util.go
172
util.go
|
@ -1,172 +0,0 @@
|
||||||
package chart
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Float is an alias for float64 that provides a better .String() method.
|
|
||||||
type Float float64
|
|
||||||
|
|
||||||
// String returns the string representation of a float.
|
|
||||||
func (f Float) String() string {
|
|
||||||
return fmt.Sprintf("%.2f", f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeToFloat64 returns a float64 representation of a time.
|
|
||||||
func TimeToFloat64(t time.Time) float64 {
|
|
||||||
return float64(t.UnixNano())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slices generates N slices that span the total.
|
|
||||||
// The resulting array will be intermediate indexes until total.
|
|
||||||
func Slices(count int, total float64) []float64 {
|
|
||||||
var values []float64
|
|
||||||
sliceWidth := float64(total) / float64(count)
|
|
||||||
for cursor := 0.0; cursor < total; cursor += sliceWidth {
|
|
||||||
values = append(values, cursor)
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoundToForDelta returns a `roundTo` value for a given delta.
|
|
||||||
func GetRoundToForDelta(delta float64) float64 {
|
|
||||||
startingDeltaBound := math.Pow(10.0, 10.0)
|
|
||||||
for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 {
|
|
||||||
if delta > cursor {
|
|
||||||
return cursor / 10.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundUp rounds up to a given roundTo value.
|
|
||||||
func RoundUp(value, roundTo float64) float64 {
|
|
||||||
d1 := math.Ceil(value / roundTo)
|
|
||||||
return d1 * roundTo
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundDown rounds down to a given roundTo value.
|
|
||||||
func RoundDown(value, roundTo float64) float64 {
|
|
||||||
d1 := math.Floor(value / roundTo)
|
|
||||||
return d1 * roundTo
|
|
||||||
}
|
|
||||||
|
|
||||||
// MinInt returns the minimum of a set of integers.
|
|
||||||
func MinInt(values ...int) int {
|
|
||||||
min := math.MaxInt32
|
|
||||||
for _, v := range values {
|
|
||||||
if v < min {
|
|
||||||
min = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxInt returns the maximum of a set of integers.
|
|
||||||
func MaxInt(values ...int) int {
|
|
||||||
max := math.MinInt32
|
|
||||||
for _, v := range values {
|
|
||||||
if v > max {
|
|
||||||
max = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbsInt returns the absolute value of an integer.
|
|
||||||
func AbsInt(value int) int {
|
|
||||||
if value < 0 {
|
|
||||||
return -value
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seq produces an array of floats from [start,end] by optional steps.
|
|
||||||
func Seq(start, end float64, steps ...float64) []float64 {
|
|
||||||
var values []float64
|
|
||||||
step := 1.0
|
|
||||||
if len(steps) > 0 {
|
|
||||||
step = steps[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if start < end {
|
|
||||||
for x := start; x <= end; x += step {
|
|
||||||
values = append(values, x)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for x := start; x >= end; x = x - step {
|
|
||||||
values = append(values, x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeqRand generates a random sequence.
|
|
||||||
func SeqRand(samples int, scale float64) []float64 {
|
|
||||||
rnd := rand.New(rand.NewSource(time.Now().Unix()))
|
|
||||||
values := make([]float64, samples)
|
|
||||||
|
|
||||||
for x := 0; x < samples; x++ {
|
|
||||||
values[x] = rnd.Float64() * scale
|
|
||||||
}
|
|
||||||
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeqDays generates a sequence of timestamps by day, from -days to today.
|
|
||||||
func SeqDays(days int) []time.Time {
|
|
||||||
var values []time.Time
|
|
||||||
for day := days; day >= 0; day-- {
|
|
||||||
values = append(values, time.Now().AddDate(0, 0, -day))
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// PercentDifference computes the percentage difference between two values.
|
|
||||||
// The formula is (v2-v1)/v1.
|
|
||||||
func PercentDifference(v1, v2 float64) float64 {
|
|
||||||
return (v2 - v1) / v1
|
|
||||||
}
|
|
102
util_test.go
102
util_test.go
|
@ -1,102 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlices(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
s := Slices(10, 100)
|
|
||||||
assert.Len(s, 10)
|
|
||||||
assert.Equal(0, s[0])
|
|
||||||
assert.Equal(10, s[1])
|
|
||||||
assert.Equal(20, s[2])
|
|
||||||
assert.Equal(90, s[9])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRoundToForDelta(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal(100.0, GetRoundToForDelta(1001.00))
|
|
||||||
assert.Equal(10.0, GetRoundToForDelta(101.00))
|
|
||||||
assert.Equal(1.0, GetRoundToForDelta(11.00))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeq(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
asc := Seq(1.0, 10.0)
|
|
||||||
assert.Len(asc, 10)
|
|
||||||
|
|
||||||
desc := Seq(10.0, 1.0)
|
|
||||||
assert.Len(desc, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPercentDifference(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
assert.Equal(0.5, PercentDifference(1.0, 1.5))
|
|
||||||
assert.Equal(-0.5, PercentDifference(2.0, 1.0))
|
|
||||||
}
|
|
46
value.go
Normal file
46
value.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
// Value is a chart value.
|
||||||
|
type Value struct {
|
||||||
|
Style Style
|
||||||
|
Label string
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values is an array of Value.
|
||||||
|
type Values []Value
|
||||||
|
|
||||||
|
// Values returns the values.
|
||||||
|
func (vs Values) Values() []float64 {
|
||||||
|
values := make([]float64, len(vs))
|
||||||
|
for index, v := range vs {
|
||||||
|
values[index] = v.Value
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValuesNormalized returns normalized values.
|
||||||
|
func (vs Values) ValuesNormalized() []float64 {
|
||||||
|
return Math.Normalize(vs.Values()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize returns the values normalized.
|
||||||
|
func (vs Values) Normalize() []Value {
|
||||||
|
output := make([]Value, len(vs))
|
||||||
|
total := Math.Sum(vs.Values()...)
|
||||||
|
for index, v := range vs {
|
||||||
|
output[index] = Value{
|
||||||
|
Style: v.Style,
|
||||||
|
Label: v.Label,
|
||||||
|
Value: Math.RoundDown(v.Value/total, 0.0001),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value2 is a two axis value.
|
||||||
|
type Value2 struct {
|
||||||
|
Style Style
|
||||||
|
Label string
|
||||||
|
XValue, YValue float64
|
||||||
|
}
|
69
value_test.go
Normal file
69
value_test.go
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValuesValues(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
vs := []Value{
|
||||||
|
{Value: 10, Label: "Blue"},
|
||||||
|
{Value: 9, Label: "Green"},
|
||||||
|
{Value: 8, Label: "Gray"},
|
||||||
|
{Value: 7, Label: "Orange"},
|
||||||
|
{Value: 6, Label: "HEANG"},
|
||||||
|
{Value: 5, Label: "??"},
|
||||||
|
{Value: 2, Label: "!!"},
|
||||||
|
}
|
||||||
|
|
||||||
|
values := Values(vs).Values()
|
||||||
|
assert.Len(values, 7)
|
||||||
|
assert.Equal(10, values[0])
|
||||||
|
assert.Equal(9, values[1])
|
||||||
|
assert.Equal(8, values[2])
|
||||||
|
assert.Equal(7, values[3])
|
||||||
|
assert.Equal(6, values[4])
|
||||||
|
assert.Equal(5, values[5])
|
||||||
|
assert.Equal(2, values[6])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValuesValuesNormalized(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
vs := []Value{
|
||||||
|
{Value: 10, Label: "Blue"},
|
||||||
|
{Value: 9, Label: "Green"},
|
||||||
|
{Value: 8, Label: "Gray"},
|
||||||
|
{Value: 7, Label: "Orange"},
|
||||||
|
{Value: 6, Label: "HEANG"},
|
||||||
|
{Value: 5, Label: "??"},
|
||||||
|
{Value: 2, Label: "!!"},
|
||||||
|
}
|
||||||
|
|
||||||
|
values := Values(vs).ValuesNormalized()
|
||||||
|
assert.Len(values, 7)
|
||||||
|
assert.Equal(0.2127, values[0])
|
||||||
|
assert.Equal(0.0425, values[6])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValuesNormalize(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
vs := []Value{
|
||||||
|
{Value: 10, Label: "Blue"},
|
||||||
|
{Value: 9, Label: "Green"},
|
||||||
|
{Value: 8, Label: "Gray"},
|
||||||
|
{Value: 7, Label: "Orange"},
|
||||||
|
{Value: 6, Label: "HEANG"},
|
||||||
|
{Value: 5, Label: "??"},
|
||||||
|
{Value: 2, Label: "!!"},
|
||||||
|
}
|
||||||
|
|
||||||
|
values := Values(vs).Normalize()
|
||||||
|
assert.Len(values, 7)
|
||||||
|
assert.Equal(0.2127, values[0].Value)
|
||||||
|
assert.Equal(0.0425, values[6].Value)
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/image/font"
|
"golang.org/x/image/font"
|
||||||
|
@ -76,6 +77,32 @@ func (vr *vectorRenderer) LineTo(x, y int) {
|
||||||
vr.p = append(vr.p, fmt.Sprintf("L %d %d", x, y))
|
vr.p = append(vr.p, fmt.Sprintf("L %d %d", x, y))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuadCurveTo draws a quad curve.
|
||||||
|
func (vr *vectorRenderer) QuadCurveTo(cx, cy, x, y int) {
|
||||||
|
vr.p = append(vr.p, fmt.Sprintf("Q%d,%d %d,%d", cx, cy, x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
|
||||||
|
startAngle = Math.RadianAdd(startAngle, _pi2)
|
||||||
|
endAngle := Math.RadianAdd(startAngle, delta)
|
||||||
|
|
||||||
|
startx := cx + int(rx*math.Sin(startAngle))
|
||||||
|
starty := cy - int(ry*math.Cos(startAngle))
|
||||||
|
|
||||||
|
if len(vr.p) > 0 {
|
||||||
|
vr.p = append(vr.p, fmt.Sprintf("L %d %d", startx, starty))
|
||||||
|
} else {
|
||||||
|
vr.p = append(vr.p, fmt.Sprintf("M %d %d", startx, starty))
|
||||||
|
}
|
||||||
|
|
||||||
|
endx := cx + int(rx*math.Sin(endAngle))
|
||||||
|
endy := cy - int(ry*math.Cos(endAngle))
|
||||||
|
|
||||||
|
dd := Math.RadiansToDegrees(delta)
|
||||||
|
|
||||||
|
vr.p = append(vr.p, fmt.Sprintf("A %d %d %0.2f 0 1 %d %d", int(rx), int(ry), dd, endx, endy))
|
||||||
|
}
|
||||||
|
|
||||||
// Close closes a shape.
|
// Close closes a shape.
|
||||||
func (vr *vectorRenderer) Close() {
|
func (vr *vectorRenderer) Close() {
|
||||||
vr.p = append(vr.p, fmt.Sprintf("Z"))
|
vr.p = append(vr.p, fmt.Sprintf("Z"))
|
||||||
|
@ -83,30 +110,28 @@ func (vr *vectorRenderer) Close() {
|
||||||
|
|
||||||
// Stroke draws the path with no fill.
|
// Stroke draws the path with no fill.
|
||||||
func (vr *vectorRenderer) Stroke() {
|
func (vr *vectorRenderer) Stroke() {
|
||||||
vr.drawPath(vr.s.SVGStroke())
|
vr.drawPath(vr.s.GetStrokeOptions())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill draws the path with no stroke.
|
// Fill draws the path with no stroke.
|
||||||
func (vr *vectorRenderer) Fill() {
|
func (vr *vectorRenderer) Fill() {
|
||||||
vr.drawPath(vr.s.SVGFill())
|
vr.drawPath(vr.s.GetFillOptions())
|
||||||
}
|
}
|
||||||
|
|
||||||
// FillStroke draws the path with both fill and stroke.
|
// FillStroke draws the path with both fill and stroke.
|
||||||
func (vr *vectorRenderer) FillStroke() {
|
func (vr *vectorRenderer) FillStroke() {
|
||||||
s := vr.s.SVGFillAndStroke()
|
vr.drawPath(vr.s.GetFillAndStrokeOptions())
|
||||||
vr.drawPath(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawPath draws a path.
|
// drawPath draws a path.
|
||||||
func (vr *vectorRenderer) drawPath(s Style) {
|
func (vr *vectorRenderer) drawPath(s Style) {
|
||||||
vr.c.Path(strings.Join(vr.p, "\n"), &s)
|
vr.c.Path(strings.Join(vr.p, "\n"), vr.s.GetFillAndStrokeOptions())
|
||||||
vr.p = []string{} // clear the path
|
vr.p = []string{} // clear the path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Circle implements the interface method.
|
// Circle implements the interface method.
|
||||||
func (vr *vectorRenderer) Circle(radius float64, x, y int) {
|
func (vr *vectorRenderer) Circle(radius float64, x, y int) {
|
||||||
style := vr.s.SVGFillAndStroke()
|
vr.c.Circle(x, y, int(radius), vr.s.GetFillAndStrokeOptions())
|
||||||
vr.c.Circle(x, y, int(radius), &style)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFont implements the interface method.
|
// SetFont implements the interface method.
|
||||||
|
@ -126,8 +151,7 @@ func (vr *vectorRenderer) SetFontSize(size float64) {
|
||||||
|
|
||||||
// Text draws a text blob.
|
// Text draws a text blob.
|
||||||
func (vr *vectorRenderer) Text(body string, x, y int) {
|
func (vr *vectorRenderer) Text(body string, x, y int) {
|
||||||
style := vr.s.SVGText()
|
vr.c.Text(x, y, body, vr.s.GetTextOptions())
|
||||||
vr.c.Text(x, y, body, &style)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MeasureText uses the truetype font drawer to measure the width of text.
|
// MeasureText uses the truetype font drawer to measure the width of text.
|
||||||
|
@ -173,22 +197,82 @@ func (c *canvas) Start(width, height int) {
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d">\n`, c.width, c.height)))
|
c.w.Write([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d">\n`, c.width, c.height)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *canvas) Path(d string, style *Style) {
|
func (c *canvas) Path(d string, style Style) {
|
||||||
var strokeDashArrayProperty string
|
var strokeDashArrayProperty string
|
||||||
if len(style.StrokeDashArray) > 0 {
|
if len(style.StrokeDashArray) > 0 {
|
||||||
strokeDashArrayProperty = style.SVGStrokeDashArray()
|
strokeDashArrayProperty = c.getStrokeDashArray(style)
|
||||||
}
|
}
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" style="%s"/>\n`, strokeDashArrayProperty, d, style.SVG(c.dpi))))
|
c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" style="%s"/>\n`, strokeDashArrayProperty, d, c.styleAsSVG(style))))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *canvas) Text(x, y int, body string, style *Style) {
|
func (c *canvas) Text(x, y int, body string, style Style) {
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, style.SVG(c.dpi), body)))
|
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *canvas) Circle(x, y, r int, style *Style) {
|
func (c *canvas) Circle(x, y, r int, style Style) {
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" style="%s">`, x, y, r, style.SVG(c.dpi))))
|
c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" style="%s">`, x, y, r, c.styleAsSVG(style))))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *canvas) End() {
|
func (c *canvas) End() {
|
||||||
c.w.Write([]byte("</svg>"))
|
c.w.Write([]byte("</svg>"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getStrokeDashArray returns the stroke-dasharray property of a style.
|
||||||
|
func (c *canvas) getStrokeDashArray(s Style) string {
|
||||||
|
if len(s.StrokeDashArray) > 0 {
|
||||||
|
var values []string
|
||||||
|
for _, v := range s.StrokeDashArray {
|
||||||
|
values = append(values, fmt.Sprintf("%0.1f", v))
|
||||||
|
}
|
||||||
|
return "stroke-dasharray=\"" + strings.Join(values, ", ") + "\""
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFontFace returns the font face for the style.
|
||||||
|
func (c *canvas) getFontFace(s Style) string {
|
||||||
|
family := "sans-serif"
|
||||||
|
if s.GetFont() != nil {
|
||||||
|
name := s.GetFont().Name(truetype.NameIDFontFamily)
|
||||||
|
if len(name) != 0 {
|
||||||
|
family = fmt.Sprintf(`'%s',%s`, name, family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("font-family:%s", family)
|
||||||
|
}
|
||||||
|
|
||||||
|
// styleAsSVG returns the style as a svg style string.
|
||||||
|
func (c *canvas) styleAsSVG(s Style) string {
|
||||||
|
sw := s.StrokeWidth
|
||||||
|
sc := s.StrokeColor
|
||||||
|
fc := s.FillColor
|
||||||
|
fs := s.FontSize
|
||||||
|
fnc := s.FontColor
|
||||||
|
|
||||||
|
strokeWidthText := "stroke-width:0"
|
||||||
|
if sw != 0 {
|
||||||
|
strokeWidthText = "stroke-width:" + fmt.Sprintf("%d", int(sw))
|
||||||
|
}
|
||||||
|
|
||||||
|
strokeText := "stroke:none"
|
||||||
|
if !sc.IsZero() {
|
||||||
|
strokeText = "stroke:" + sc.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
fillText := "fill:none"
|
||||||
|
if !fc.IsZero() {
|
||||||
|
fillText = "fill:" + fc.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSizeText := ""
|
||||||
|
if fs != 0 {
|
||||||
|
fontSizeText = "font-size:" + fmt.Sprintf("%.1fpx", drawing.PointsToPixels(c.dpi, fs))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fnc.IsZero() {
|
||||||
|
fillText = "fill:" + fnc.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
fontText := c.getFontFace(s)
|
||||||
|
return strings.Join([]string{strokeWidthText, strokeText, fillText, fontSizeText, fontText}, ";")
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/blendlabs/go-assert"
|
"github.com/blendlabs/go-assert"
|
||||||
|
"github.com/wcharczuk/go-chart/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestVectorRendererPath(t *testing.T) {
|
func TestVectorRendererPath(t *testing.T) {
|
||||||
|
@ -50,3 +51,27 @@ func TestVectorRendererMeasureText(t *testing.T) {
|
||||||
assert.Equal(21, tb.Width())
|
assert.Equal(21, tb.Width())
|
||||||
assert.Equal(15, tb.Height())
|
assert.Equal(15, tb.Height())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanvasStyleSVG(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
assert.Nil(err)
|
||||||
|
|
||||||
|
set := Style{
|
||||||
|
StrokeColor: drawing.ColorWhite,
|
||||||
|
StrokeWidth: 5.0,
|
||||||
|
FillColor: drawing.ColorWhite,
|
||||||
|
FontColor: drawing.ColorWhite,
|
||||||
|
Font: f,
|
||||||
|
Padding: DefaultBackgroundPadding,
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas := &canvas{dpi: DefaultDPI}
|
||||||
|
|
||||||
|
svgString := canvas.styleAsSVG(set)
|
||||||
|
assert.NotEmpty(svgString)
|
||||||
|
assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)"))
|
||||||
|
assert.True(strings.Contains(svgString, "stroke-width:5"))
|
||||||
|
assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)"))
|
||||||
|
}
|
||||||
|
|
118
xaxis.go
118
xaxis.go
|
@ -13,6 +13,8 @@ type XAxis struct {
|
||||||
Range Range
|
Range Range
|
||||||
Ticks []Tick
|
Ticks []Tick
|
||||||
|
|
||||||
|
TickPosition tickPosition
|
||||||
|
|
||||||
GridLines []GridLine
|
GridLines []GridLine
|
||||||
GridMajorStyle Style
|
GridMajorStyle Style
|
||||||
GridMinorStyle Style
|
GridMinorStyle Style
|
||||||
|
@ -28,42 +30,31 @@ func (xa XAxis) GetStyle() Style {
|
||||||
return xa.Style
|
return xa.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTicks returns the ticks for a series. It coalesces between user provided ticks and
|
// GetTickPosition returns the tick position option for the axis.
|
||||||
// generated ticks.
|
func (xa XAxis) GetTickPosition(defaults ...tickPosition) tickPosition {
|
||||||
|
if xa.TickPosition == TickPositionUnset {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return TickPositionUnderTick
|
||||||
|
}
|
||||||
|
return xa.TickPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTicks returns the ticks for a series.
|
||||||
|
// The coalesce priority is:
|
||||||
|
// - User Supplied Ticks (i.e. Ticks array on the axis itself).
|
||||||
|
// - Range ticks (i.e. if the range provides ticks).
|
||||||
|
// - Generating continuous ticks based on minimum spacing and canvas width.
|
||||||
func (xa XAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
|
func (xa XAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
|
||||||
if len(xa.Ticks) > 0 {
|
if len(xa.Ticks) > 0 {
|
||||||
return xa.Ticks
|
return xa.Ticks
|
||||||
}
|
}
|
||||||
return xa.generateTicks(r, ra, defaults, vf)
|
if tp, isTickProvider := ra.(TicksProvider); isTickProvider {
|
||||||
|
return tp.GetTicks(vf)
|
||||||
}
|
}
|
||||||
|
step := CalculateContinuousTickStep(r, ra, false, xa.Style.InheritFrom(defaults), vf)
|
||||||
func (xa XAxis) generateTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
|
return GenerateContinuousTicksWithStep(ra, step, vf, xa.TickPosition == TickPositionBetweenTicks)
|
||||||
step := xa.getTickStep(r, ra, defaults, vf)
|
|
||||||
return GenerateTicksWithStep(ra, step, vf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xa XAxis) getTickStep(r Renderer, ra Range, defaults Style, vf ValueFormatter) float64 {
|
|
||||||
tickCount := xa.getTickCount(r, ra, defaults, vf)
|
|
||||||
step := ra.Delta() / float64(tickCount)
|
|
||||||
return step
|
|
||||||
}
|
|
||||||
|
|
||||||
func (xa XAxis) getTickCount(r Renderer, ra Range, defaults Style, vf ValueFormatter) int {
|
|
||||||
r.SetFont(xa.Style.GetFont(defaults.GetFont()))
|
|
||||||
r.SetFontSize(xa.Style.GetFontSize(defaults.GetFontSize(DefaultFontSize)))
|
|
||||||
|
|
||||||
// take a cut at determining the 'widest' value.
|
|
||||||
l0 := vf(ra.Min)
|
|
||||||
ln := vf(ra.Max)
|
|
||||||
ll := l0
|
|
||||||
if len(ln) > len(l0) {
|
|
||||||
ll = ln
|
|
||||||
}
|
|
||||||
llb := r.MeasureText(ll)
|
|
||||||
textWidth := llb.Width()
|
|
||||||
width := textWidth + DefaultMinimumTickHorizontalSpacing
|
|
||||||
count := int(math.Ceil(float64(ra.Domain) / float64(width)))
|
|
||||||
return count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGridLines returns the gridlines for the axis.
|
// GetGridLines returns the gridlines for the axis.
|
||||||
|
@ -71,18 +62,12 @@ func (xa XAxis) GetGridLines(ticks []Tick) []GridLine {
|
||||||
if len(xa.GridLines) > 0 {
|
if len(xa.GridLines) > 0 {
|
||||||
return xa.GridLines
|
return xa.GridLines
|
||||||
}
|
}
|
||||||
return GenerateGridLines(ticks, true)
|
return GenerateGridLines(ticks, xa.GridMajorStyle, xa.GridMinorStyle, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure returns the bounds of the axis.
|
// Measure returns the bounds of the axis.
|
||||||
func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
||||||
r.SetStrokeColor(xa.Style.GetStrokeColor(defaults.StrokeColor))
|
xa.Style.InheritFrom(defaults).WriteToRenderer(r)
|
||||||
r.SetStrokeWidth(xa.Style.GetStrokeWidth(defaults.StrokeWidth))
|
|
||||||
r.SetStrokeDashArray(xa.Style.GetStrokeDashArray())
|
|
||||||
r.SetFont(xa.Style.GetFont(defaults.GetFont()))
|
|
||||||
r.SetFontColor(xa.Style.GetFontColor(DefaultAxisColor))
|
|
||||||
r.SetFontSize(xa.Style.GetFontSize(defaults.GetFontSize()))
|
|
||||||
|
|
||||||
sort.Sort(Ticks(ticks))
|
sort.Sort(Ticks(ticks))
|
||||||
|
|
||||||
var left, right, top, bottom = math.MaxInt32, 0, math.MaxInt32, 0
|
var left, right, top, bottom = math.MaxInt32, 0, math.MaxInt32, 0
|
||||||
|
@ -94,10 +79,10 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
tx := canvasBox.Left + lx
|
tx := canvasBox.Left + lx
|
||||||
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
||||||
|
|
||||||
top = MinInt(top, canvasBox.Bottom)
|
top = Math.MinInt(top, canvasBox.Bottom)
|
||||||
left = MinInt(left, tx-(tb.Width()>>1))
|
left = Math.MinInt(left, tx-(tb.Width()>>1))
|
||||||
right = MaxInt(right, tx+(tb.Width()>>1))
|
right = Math.MaxInt(right, tx+(tb.Width()>>1))
|
||||||
bottom = MaxInt(bottom, ty)
|
bottom = Math.MaxInt(bottom, ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Box{
|
return Box{
|
||||||
|
@ -110,37 +95,58 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
|
|
||||||
// Render renders the axis
|
// Render renders the axis
|
||||||
func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
||||||
r.SetStrokeColor(xa.Style.GetStrokeColor(defaults.StrokeColor))
|
tickStyle := xa.Style.InheritFrom(defaults)
|
||||||
r.SetStrokeWidth(xa.Style.GetStrokeWidth(defaults.StrokeWidth))
|
|
||||||
r.SetStrokeDashArray(xa.Style.GetStrokeDashArray())
|
|
||||||
r.SetFont(xa.Style.GetFont(defaults.GetFont()))
|
|
||||||
r.SetFontColor(xa.Style.GetFontColor(DefaultAxisColor))
|
|
||||||
r.SetFontSize(xa.Style.GetFontSize(defaults.GetFontSize()))
|
|
||||||
|
|
||||||
|
tickStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||||
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
r.LineTo(canvasBox.Right, canvasBox.Bottom)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
sort.Sort(Ticks(ticks))
|
sort.Sort(Ticks(ticks))
|
||||||
|
|
||||||
for _, t := range ticks {
|
tp := xa.GetTickPosition()
|
||||||
|
|
||||||
|
var tx, ty int
|
||||||
|
for index, t := range ticks {
|
||||||
v := t.Value
|
v := t.Value
|
||||||
lx := ra.Translate(v)
|
lx := ra.Translate(v)
|
||||||
tb := r.MeasureText(t.Label)
|
tb := r.MeasureText(t.Label)
|
||||||
tx := canvasBox.Left + lx
|
|
||||||
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
|
||||||
r.Text(t.Label, tx-tb.Width()>>1, ty)
|
|
||||||
|
|
||||||
|
tx = canvasBox.Left + lx
|
||||||
|
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
||||||
|
|
||||||
|
tickStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||||
r.MoveTo(tx, canvasBox.Bottom)
|
r.MoveTo(tx, canvasBox.Bottom)
|
||||||
r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight)
|
r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
|
switch tp {
|
||||||
|
case TickPositionUnderTick:
|
||||||
|
tickStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
r.Text(t.Label, tx-tb.Width()>>1, ty)
|
||||||
|
case TickPositionBetweenTicks:
|
||||||
|
if index > 0 {
|
||||||
|
llx := ra.Translate(ticks[index-1].Value)
|
||||||
|
ltx := canvasBox.Left + llx
|
||||||
|
Draw.TextWithin(r, t.Label, Box{
|
||||||
|
Left: ltx,
|
||||||
|
Right: tx,
|
||||||
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
|
Bottom: canvasBox.Bottom + DefaultXAxisMargin + tb.Height(),
|
||||||
|
}, tickStyle.InheritFrom(Style{TextHorizontalAlign: TextHorizontalAlignCenter}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if xa.GridMajorStyle.Show || xa.GridMinorStyle.Show {
|
if xa.GridMajorStyle.Show || xa.GridMinorStyle.Show {
|
||||||
for _, gl := range xa.GetGridLines(ticks) {
|
for _, gl := range xa.GetGridLines(ticks) {
|
||||||
if (gl.IsMinor && xa.GridMinorStyle.Show) ||
|
if (gl.IsMinor && xa.GridMinorStyle.Show) || (!gl.IsMinor && xa.GridMajorStyle.Show) {
|
||||||
(!gl.IsMinor && xa.GridMajorStyle.Show) {
|
defaults := xa.GridMajorStyle
|
||||||
gl.Render(r, canvasBox, ra)
|
if gl.IsMinor {
|
||||||
|
defaults = xa.GridMinorStyle
|
||||||
|
}
|
||||||
|
gl.Render(r, canvasBox, ra, defaults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,46 +6,6 @@ import (
|
||||||
"github.com/blendlabs/go-assert"
|
"github.com/blendlabs/go-assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestXAxisGetTickCount(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
r, err := PNG(1024, 1024)
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
f, err := GetDefaultFont()
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
xa := XAxis{}
|
|
||||||
xr := Range{Min: 10, Max: 100, Domain: 1024}
|
|
||||||
styleDefaults := Style{
|
|
||||||
Font: f,
|
|
||||||
FontSize: 10.0,
|
|
||||||
}
|
|
||||||
vf := FloatValueFormatter
|
|
||||||
count := xa.getTickCount(r, xr, styleDefaults, vf)
|
|
||||||
assert.Equal(16, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestXAxisGetTickStep(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
r, err := PNG(1024, 1024)
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
f, err := GetDefaultFont()
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
xa := XAxis{}
|
|
||||||
xr := Range{Min: 10, Max: 100, Domain: 1024}
|
|
||||||
styleDefaults := Style{
|
|
||||||
Font: f,
|
|
||||||
FontSize: 10.0,
|
|
||||||
}
|
|
||||||
vf := FloatValueFormatter
|
|
||||||
step := xa.getTickStep(r, xr, styleDefaults, vf)
|
|
||||||
assert.Equal(xr.Delta()/16.0, step)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestXAxisGetTicks(t *testing.T) {
|
func TestXAxisGetTicks(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
@ -56,7 +16,7 @@ func TestXAxisGetTicks(t *testing.T) {
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
|
|
||||||
xa := XAxis{}
|
xa := XAxis{}
|
||||||
xr := Range{Min: 10, Max: 100, Domain: 1024}
|
xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024}
|
||||||
styleDefaults := Style{
|
styleDefaults := Style{
|
||||||
Font: f,
|
Font: f,
|
||||||
FontSize: 10.0,
|
FontSize: 10.0,
|
||||||
|
@ -78,7 +38,7 @@ func TestXAxisGetTicksWithUserDefaults(t *testing.T) {
|
||||||
xa := XAxis{
|
xa := XAxis{
|
||||||
Ticks: []Tick{{Value: 1.0, Label: "1.0"}},
|
Ticks: []Tick{{Value: 1.0, Label: "1.0"}},
|
||||||
}
|
}
|
||||||
xr := Range{Min: 10, Max: 100, Domain: 1024}
|
xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024}
|
||||||
styleDefaults := Style{
|
styleDefaults := Style{
|
||||||
Font: f,
|
Font: f,
|
||||||
FontSize: 10.0,
|
FontSize: 10.0,
|
||||||
|
@ -87,3 +47,21 @@ func TestXAxisGetTicksWithUserDefaults(t *testing.T) {
|
||||||
ticks := xa.GetTicks(r, xr, styleDefaults, vf)
|
ticks := xa.GetTicks(r, xr, styleDefaults, vf)
|
||||||
assert.Len(ticks, 1)
|
assert.Len(ticks, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXAxisMeasure(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
assert.Nil(err)
|
||||||
|
style := Style{
|
||||||
|
Font: f,
|
||||||
|
FontSize: 10.0,
|
||||||
|
}
|
||||||
|
r, err := PNG(100, 100)
|
||||||
|
assert.Nil(err)
|
||||||
|
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
||||||
|
xa := XAxis{}
|
||||||
|
xab := xa.Measure(r, Box{0, 0, 100, 100}, &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
||||||
|
assert.Equal(122, xab.Width())
|
||||||
|
assert.Equal(21, xab.Height())
|
||||||
|
}
|
||||||
|
|
73
yaxis.go
73
yaxis.go
|
@ -13,13 +13,14 @@ type YAxis struct {
|
||||||
|
|
||||||
Zero GridLine
|
Zero GridLine
|
||||||
|
|
||||||
AxisType YAxisType
|
AxisType yAxisType
|
||||||
|
|
||||||
ValueFormatter ValueFormatter
|
ValueFormatter ValueFormatter
|
||||||
Range Range
|
Range Range
|
||||||
Ticks []Tick
|
|
||||||
|
|
||||||
|
Ticks []Tick
|
||||||
GridLines []GridLine
|
GridLines []GridLine
|
||||||
|
|
||||||
GridMajorStyle Style
|
GridMajorStyle Style
|
||||||
GridMinorStyle Style
|
GridMinorStyle Style
|
||||||
}
|
}
|
||||||
|
@ -34,35 +35,20 @@ func (ya YAxis) GetStyle() Style {
|
||||||
return ya.Style
|
return ya.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTicks returns the ticks for a series. It coalesces between user provided ticks and
|
// GetTicks returns the ticks for a series.
|
||||||
// generated ticks.
|
// The coalesce priority is:
|
||||||
|
// - User Supplied Ticks (i.e. Ticks array on the axis itself).
|
||||||
|
// - Range ticks (i.e. if the range provides ticks).
|
||||||
|
// - Generating continuous ticks based on minimum spacing and canvas width.
|
||||||
func (ya YAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
|
func (ya YAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
|
||||||
if len(ya.Ticks) > 0 {
|
if len(ya.Ticks) > 0 {
|
||||||
return ya.Ticks
|
return ya.Ticks
|
||||||
}
|
}
|
||||||
return ya.generateTicks(r, ra, defaults, vf)
|
if tp, isTickProvider := ra.(TicksProvider); isTickProvider {
|
||||||
|
return tp.GetTicks(vf)
|
||||||
}
|
}
|
||||||
|
step := CalculateContinuousTickStep(r, ra, true, ya.Style.InheritFrom(defaults), vf)
|
||||||
func (ya YAxis) generateTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
|
return GenerateContinuousTicksWithStep(ra, step, vf, true)
|
||||||
step := ya.getTickStep(r, ra, defaults, vf)
|
|
||||||
ticks := GenerateTicksWithStep(ra, step, vf)
|
|
||||||
return ticks
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ya YAxis) getTickStep(r Renderer, ra Range, defaults Style, vf ValueFormatter) float64 {
|
|
||||||
tickCount := ya.getTickCount(r, ra, defaults, vf)
|
|
||||||
step := ra.Delta() / float64(tickCount)
|
|
||||||
return step
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ya YAxis) getTickCount(r Renderer, ra Range, defaults Style, vf ValueFormatter) int {
|
|
||||||
r.SetFont(ya.Style.GetFont(defaults.GetFont()))
|
|
||||||
r.SetFontSize(ya.Style.GetFontSize(defaults.GetFontSize(DefaultFontSize)))
|
|
||||||
//given the domain, figure out how many ticks we can draw ...
|
|
||||||
label := vf(ra.Min)
|
|
||||||
tb := r.MeasureText(label)
|
|
||||||
count := int(math.Ceil(float64(ra.Domain) / float64(tb.Height()+DefaultMinimumTickVerticalSpacing)))
|
|
||||||
return count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGridLines returns the gridlines for the axis.
|
// GetGridLines returns the gridlines for the axis.
|
||||||
|
@ -70,16 +56,12 @@ func (ya YAxis) GetGridLines(ticks []Tick) []GridLine {
|
||||||
if len(ya.GridLines) > 0 {
|
if len(ya.GridLines) > 0 {
|
||||||
return ya.GridLines
|
return ya.GridLines
|
||||||
}
|
}
|
||||||
return GenerateGridLines(ticks, false)
|
return GenerateGridLines(ticks, ya.GridMajorStyle, ya.GridMinorStyle, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure returns the bounds of the axis.
|
// Measure returns the bounds of the axis.
|
||||||
func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
||||||
r.SetStrokeColor(ya.Style.GetStrokeColor(defaults.StrokeColor))
|
ya.Style.InheritFrom(defaults).WriteToRenderer(r)
|
||||||
r.SetStrokeWidth(ya.Style.GetStrokeWidth(defaults.StrokeWidth))
|
|
||||||
r.SetFont(ya.Style.GetFont(defaults.GetFont()))
|
|
||||||
r.SetFontColor(ya.Style.GetFontColor(DefaultAxisColor))
|
|
||||||
r.SetFontSize(ya.Style.GetFontSize(defaults.GetFontSize()))
|
|
||||||
|
|
||||||
sort.Sort(Ticks(ticks))
|
sort.Sort(Ticks(ticks))
|
||||||
|
|
||||||
|
@ -103,13 +85,13 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
|
|
||||||
if ya.AxisType == YAxisPrimary {
|
if ya.AxisType == YAxisPrimary {
|
||||||
minx = canvasBox.Right
|
minx = canvasBox.Right
|
||||||
maxx = MaxInt(maxx, tx+tb.Width())
|
maxx = Math.MaxInt(maxx, tx+tb.Width())
|
||||||
} else if ya.AxisType == YAxisSecondary {
|
} else if ya.AxisType == YAxisSecondary {
|
||||||
minx = MinInt(minx, finalTextX)
|
minx = Math.MinInt(minx, finalTextX)
|
||||||
maxx = MaxInt(maxx, tx)
|
maxx = Math.MaxInt(maxx, tx)
|
||||||
}
|
}
|
||||||
miny = MinInt(miny, ly-tb.Height()>>1)
|
miny = Math.MinInt(miny, ly-tb.Height()>>1)
|
||||||
maxy = MaxInt(maxy, ly+tb.Height()>>1)
|
maxy = Math.MaxInt(maxy, ly+tb.Height()>>1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Box{
|
return Box{
|
||||||
|
@ -122,11 +104,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
|
|
||||||
// Render renders the axis.
|
// Render renders the axis.
|
||||||
func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
||||||
r.SetStrokeColor(ya.Style.GetStrokeColor(defaults.StrokeColor))
|
ya.Style.InheritFrom(defaults).WriteToRenderer(r)
|
||||||
r.SetStrokeWidth(ya.Style.GetStrokeWidth(defaults.StrokeWidth))
|
|
||||||
r.SetFont(ya.Style.GetFont(defaults.GetFont()))
|
|
||||||
r.SetFontColor(ya.Style.GetFontColor(DefaultAxisColor))
|
|
||||||
r.SetFontSize(ya.Style.GetFontSize(defaults.GetFontSize(DefaultFontSize)))
|
|
||||||
|
|
||||||
sort.Sort(Ticks(ticks))
|
sort.Sort(Ticks(ticks))
|
||||||
|
|
||||||
|
@ -170,14 +148,17 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
}
|
}
|
||||||
|
|
||||||
if ya.Zero.Style.Show {
|
if ya.Zero.Style.Show {
|
||||||
ya.Zero.Render(r, canvasBox, ra)
|
ya.Zero.Render(r, canvasBox, ra, Style{})
|
||||||
}
|
}
|
||||||
|
|
||||||
if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show {
|
if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show {
|
||||||
for _, gl := range ya.GetGridLines(ticks) {
|
for _, gl := range ya.GetGridLines(ticks) {
|
||||||
if (gl.IsMinor && ya.GridMinorStyle.Show) ||
|
if (gl.IsMinor && ya.GridMinorStyle.Show) || (!gl.IsMinor && ya.GridMajorStyle.Show) {
|
||||||
(!gl.IsMinor && ya.GridMajorStyle.Show) {
|
defaults := ya.GridMajorStyle
|
||||||
gl.Render(r, canvasBox, ra)
|
if gl.IsMinor {
|
||||||
|
defaults = ya.GridMinorStyle
|
||||||
|
}
|
||||||
|
gl.Render(r, canvasBox, ra, defaults)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,46 +6,6 @@ import (
|
||||||
"github.com/blendlabs/go-assert"
|
"github.com/blendlabs/go-assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestYAxisGetTickCount(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
r, err := PNG(1024, 1024)
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
f, err := GetDefaultFont()
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
ya := YAxis{}
|
|
||||||
yr := Range{Min: 10, Max: 100, Domain: 1024}
|
|
||||||
styleDefaults := Style{
|
|
||||||
Font: f,
|
|
||||||
FontSize: 10.0,
|
|
||||||
}
|
|
||||||
vf := FloatValueFormatter
|
|
||||||
count := ya.getTickCount(r, yr, styleDefaults, vf)
|
|
||||||
assert.Equal(34, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestYAxisGetTickStep(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
r, err := PNG(1024, 1024)
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
f, err := GetDefaultFont()
|
|
||||||
assert.Nil(err)
|
|
||||||
|
|
||||||
ya := YAxis{}
|
|
||||||
yr := Range{Min: 10, Max: 100, Domain: 1024}
|
|
||||||
styleDefaults := Style{
|
|
||||||
Font: f,
|
|
||||||
FontSize: 10.0,
|
|
||||||
}
|
|
||||||
vf := FloatValueFormatter
|
|
||||||
step := ya.getTickStep(r, yr, styleDefaults, vf)
|
|
||||||
assert.Equal(yr.Delta()/34.0, step)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestYAxisGetTicks(t *testing.T) {
|
func TestYAxisGetTicks(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
@ -56,14 +16,14 @@ func TestYAxisGetTicks(t *testing.T) {
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
|
|
||||||
ya := YAxis{}
|
ya := YAxis{}
|
||||||
yr := Range{Min: 10, Max: 100, Domain: 1024}
|
yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024}
|
||||||
styleDefaults := Style{
|
styleDefaults := Style{
|
||||||
Font: f,
|
Font: f,
|
||||||
FontSize: 10.0,
|
FontSize: 10.0,
|
||||||
}
|
}
|
||||||
vf := FloatValueFormatter
|
vf := FloatValueFormatter
|
||||||
ticks := ya.GetTicks(r, yr, styleDefaults, vf)
|
ticks := ya.GetTicks(r, yr, styleDefaults, vf)
|
||||||
assert.Len(ticks, 35)
|
assert.Len(ticks, 36)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestYAxisGetTicksWithUserDefaults(t *testing.T) {
|
func TestYAxisGetTicksWithUserDefaults(t *testing.T) {
|
||||||
|
@ -78,7 +38,7 @@ func TestYAxisGetTicksWithUserDefaults(t *testing.T) {
|
||||||
ya := YAxis{
|
ya := YAxis{
|
||||||
Ticks: []Tick{{Value: 1.0, Label: "1.0"}},
|
Ticks: []Tick{{Value: 1.0, Label: "1.0"}},
|
||||||
}
|
}
|
||||||
yr := Range{Min: 10, Max: 100, Domain: 1024}
|
yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024}
|
||||||
styleDefaults := Style{
|
styleDefaults := Style{
|
||||||
Font: f,
|
Font: f,
|
||||||
FontSize: 10.0,
|
FontSize: 10.0,
|
||||||
|
@ -87,3 +47,39 @@ func TestYAxisGetTicksWithUserDefaults(t *testing.T) {
|
||||||
ticks := ya.GetTicks(r, yr, styleDefaults, vf)
|
ticks := ya.GetTicks(r, yr, styleDefaults, vf)
|
||||||
assert.Len(ticks, 1)
|
assert.Len(ticks, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestYAxisMeasure(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
assert.Nil(err)
|
||||||
|
style := Style{
|
||||||
|
Font: f,
|
||||||
|
FontSize: 10.0,
|
||||||
|
}
|
||||||
|
r, err := PNG(100, 100)
|
||||||
|
assert.Nil(err)
|
||||||
|
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
||||||
|
ya := YAxis{}
|
||||||
|
yab := ya.Measure(r, Box{0, 0, 100, 100}, &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
||||||
|
assert.Equal(32, yab.Width())
|
||||||
|
assert.Equal(110, yab.Height())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestYAxisSecondaryMeasure(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
f, err := GetDefaultFont()
|
||||||
|
assert.Nil(err)
|
||||||
|
style := Style{
|
||||||
|
Font: f,
|
||||||
|
FontSize: 10.0,
|
||||||
|
}
|
||||||
|
r, err := PNG(100, 100)
|
||||||
|
assert.Nil(err)
|
||||||
|
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
|
||||||
|
ya := YAxis{AxisType: YAxisSecondary}
|
||||||
|
yab := ya.Measure(r, Box{0, 0, 100, 100}, &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
|
||||||
|
assert.Equal(32, yab.Width())
|
||||||
|
assert.Equal(110, yab.Height())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue