moving averages! dashes!
This commit is contained in:
parent
e4c410621d
commit
96e957daaf
6 changed files with 483 additions and 7 deletions
|
@ -33,6 +33,9 @@ func DrawLineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs
|
||||||
}
|
}
|
||||||
|
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
r.SetStrokeColor(s.GetStrokeColor())
|
||||||
|
if len(s.GetStrokeDashArray()) > 0 {
|
||||||
|
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||||
|
}
|
||||||
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
|
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
|
||||||
|
|
||||||
r.MoveTo(x0, y0)
|
r.MoveTo(x0, y0)
|
||||||
|
|
81
moving_average_series.go
Normal file
81
moving_average_series.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultMovingAverageWindowSize is the default number of values to average.
|
||||||
|
DefaultMovingAverageWindowSize = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// MovingAverageSeries is a computed series.
|
||||||
|
type MovingAverageSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
WindowSize int
|
||||||
|
InnerSeries ValueProvider
|
||||||
|
valueBuffer *RingBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the time series.
|
||||||
|
func (mas MovingAverageSeries) GetName() string {
|
||||||
|
return mas.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle returns the line style.
|
||||||
|
func (mas MovingAverageSeries) GetStyle() Style {
|
||||||
|
return mas.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which YAxis the series draws on.
|
||||||
|
func (mas MovingAverageSeries) GetYAxis() YAxisType {
|
||||||
|
return mas.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements in the series.
|
||||||
|
func (mas *MovingAverageSeries) Len() int {
|
||||||
|
return mas.InnerSeries.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue gets a value at a given index.
|
||||||
|
func (mas *MovingAverageSeries) GetValue(index int) (x float64, y float64) {
|
||||||
|
if mas.valueBuffer == nil {
|
||||||
|
mas.valueBuffer = NewRingBufferWithCapacity(mas.GetWindowSize())
|
||||||
|
}
|
||||||
|
if mas.valueBuffer.Len() >= mas.GetWindowSize() {
|
||||||
|
mas.valueBuffer.Dequeue()
|
||||||
|
}
|
||||||
|
x, y = mas.InnerSeries.GetValue(index)
|
||||||
|
mas.valueBuffer.Enqueue(y)
|
||||||
|
if mas.valueBuffer.Len() < mas.GetWindowSize() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
y = mas.getAverage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWindowSize returns the window size.
|
||||||
|
func (mas MovingAverageSeries) GetWindowSize(defaults ...int) int {
|
||||||
|
if mas.WindowSize == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
return DefaultMovingAverageWindowSize
|
||||||
|
}
|
||||||
|
return mas.WindowSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mas MovingAverageSeries) getAverage() float64 {
|
||||||
|
var accum float64
|
||||||
|
mas.valueBuffer.Each(func(v interface{}) {
|
||||||
|
if typed, isTyped := v.(float64); isTyped {
|
||||||
|
accum += typed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return accum / float64(mas.valueBuffer.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the series.
|
||||||
|
func (mas *MovingAverageSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := mas.Style.WithDefaultsFrom(defaults)
|
||||||
|
DrawLineSeries(r, canvasBox, xrange, yrange, style, mas)
|
||||||
|
}
|
217
ring_buffer.go
Normal file
217
ring_buffer.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ringBufferMinimumGrow = 4
|
||||||
|
ringBufferShrinkThreshold = 32
|
||||||
|
ringBufferGrowFactor = 200
|
||||||
|
ringBufferDefaultCapacity = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
emptyArray = make([]interface{}, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRingBuffer creates a new, empty, RingBuffer.
|
||||||
|
func NewRingBuffer() *RingBuffer {
|
||||||
|
return &RingBuffer{
|
||||||
|
array: make([]interface{}, ringBufferDefaultCapacity),
|
||||||
|
head: 0,
|
||||||
|
tail: 0,
|
||||||
|
size: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRingBufferWithCapacity creates a new RingBuffer pre-allocated with the given capacity.
|
||||||
|
func NewRingBufferWithCapacity(capacity int) *RingBuffer {
|
||||||
|
return &RingBuffer{
|
||||||
|
array: make([]interface{}, capacity),
|
||||||
|
head: 0,
|
||||||
|
tail: 0,
|
||||||
|
size: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRingBufferFromSlice createsa ring buffer out of a slice.
|
||||||
|
func NewRingBufferFromSlice(values []interface{}) *RingBuffer {
|
||||||
|
return &RingBuffer{
|
||||||
|
array: values,
|
||||||
|
head: 0,
|
||||||
|
tail: len(values) - 1,
|
||||||
|
size: len(values),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RingBuffer is a fifo buffer that is backed by a pre-allocated array, instead of allocating
|
||||||
|
// a whole new node object for each element (which saves GC churn).
|
||||||
|
// Enqueue can be O(n), Dequeue can be O(1).
|
||||||
|
type RingBuffer struct {
|
||||||
|
array []interface{}
|
||||||
|
head int
|
||||||
|
tail int
|
||||||
|
size int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the ring buffer (as it is currently populated).
|
||||||
|
// Actual memory footprint may be different.
|
||||||
|
func (rb *RingBuffer) Len() int {
|
||||||
|
return rb.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalLen returns the total size of the ring bufffer, including empty elements.
|
||||||
|
func (rb *RingBuffer) TotalLen() int {
|
||||||
|
return len(rb.array)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all objects from the RingBuffer.
|
||||||
|
func (rb *RingBuffer) Clear() {
|
||||||
|
if rb.head < rb.tail {
|
||||||
|
arrayClear(rb.array, rb.head, rb.size)
|
||||||
|
} else {
|
||||||
|
arrayClear(rb.array, rb.head, len(rb.array)-rb.head)
|
||||||
|
arrayClear(rb.array, 0, rb.tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.head = 0
|
||||||
|
rb.tail = 0
|
||||||
|
rb.size = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue adds an element to the "back" of the RingBuffer.
|
||||||
|
func (rb *RingBuffer) Enqueue(object interface{}) {
|
||||||
|
if rb.size == len(rb.array) {
|
||||||
|
newCapacity := int(len(rb.array) * int(ringBufferGrowFactor/100))
|
||||||
|
if newCapacity < (len(rb.array) + ringBufferMinimumGrow) {
|
||||||
|
newCapacity = len(rb.array) + ringBufferMinimumGrow
|
||||||
|
}
|
||||||
|
rb.setCapacity(newCapacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.array[rb.tail] = object
|
||||||
|
rb.tail = (rb.tail + 1) % len(rb.array)
|
||||||
|
rb.size++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dequeue removes the first element from the RingBuffer.
|
||||||
|
func (rb *RingBuffer) Dequeue() interface{} {
|
||||||
|
if rb.size == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
removed := rb.array[rb.head]
|
||||||
|
rb.head = (rb.head + 1) % len(rb.array)
|
||||||
|
rb.size--
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek returns but does not remove the first element.
|
||||||
|
func (rb *RingBuffer) Peek() interface{} {
|
||||||
|
if rb.size == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rb.array[rb.head]
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekBack returns but does not remove the last element.
|
||||||
|
func (rb *RingBuffer) PeekBack() interface{} {
|
||||||
|
if rb.size == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if rb.tail == 0 {
|
||||||
|
return rb.array[len(rb.array)-1]
|
||||||
|
}
|
||||||
|
return rb.array[rb.tail-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *RingBuffer) setCapacity(capacity int) {
|
||||||
|
newArray := make([]interface{}, capacity)
|
||||||
|
if rb.size > 0 {
|
||||||
|
if rb.head < rb.tail {
|
||||||
|
arrayCopy(rb.array, rb.head, newArray, 0, rb.size)
|
||||||
|
} else {
|
||||||
|
arrayCopy(rb.array, rb.head, newArray, 0, len(rb.array)-rb.head)
|
||||||
|
arrayCopy(rb.array, 0, newArray, len(rb.array)-rb.head, rb.tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rb.array = newArray
|
||||||
|
rb.head = 0
|
||||||
|
if rb.size == capacity {
|
||||||
|
rb.tail = 0
|
||||||
|
} else {
|
||||||
|
rb.tail = rb.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimExcess resizes the buffer to better fit the contents.
|
||||||
|
func (rb *RingBuffer) TrimExcess() {
|
||||||
|
threshold := float64(len(rb.array)) * 0.9
|
||||||
|
if rb.size < int(threshold) {
|
||||||
|
rb.setCapacity(rb.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsSlice returns the ring buffer, in order, as a slice.
|
||||||
|
func (rb *RingBuffer) AsSlice() []interface{} {
|
||||||
|
newArray := make([]interface{}, rb.size)
|
||||||
|
|
||||||
|
if rb.size == 0 {
|
||||||
|
return newArray
|
||||||
|
}
|
||||||
|
|
||||||
|
if rb.head < rb.tail {
|
||||||
|
arrayCopy(rb.array, rb.head, newArray, 0, rb.size)
|
||||||
|
} else {
|
||||||
|
arrayCopy(rb.array, rb.head, newArray, 0, len(rb.array)-rb.head)
|
||||||
|
arrayCopy(rb.array, 0, newArray, len(rb.array)-rb.head, rb.tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each calls the consumer for each element in the buffer.
|
||||||
|
func (rb *RingBuffer) Each(consumer func(value interface{})) {
|
||||||
|
if rb.size == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rb.head < rb.tail {
|
||||||
|
for cursor := rb.head; cursor < rb.tail; cursor++ {
|
||||||
|
consumer(rb.array[cursor])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for cursor := rb.head; cursor < len(rb.array); cursor++ {
|
||||||
|
consumer(rb.array[cursor])
|
||||||
|
}
|
||||||
|
for cursor := 0; cursor < rb.tail; cursor++ {
|
||||||
|
consumer(rb.array[cursor])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *RingBuffer) String() string {
|
||||||
|
var values []string
|
||||||
|
for _, elem := range rb.AsSlice() {
|
||||||
|
values = append(values, fmt.Sprintf("%v", elem))
|
||||||
|
}
|
||||||
|
return strings.Join(values, " <= ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func arrayClear(source []interface{}, index, length int) {
|
||||||
|
for x := 0; x < length; x++ {
|
||||||
|
absoluteIndex := x + index
|
||||||
|
source[absoluteIndex] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func arrayCopy(source []interface{}, sourceIndex int, destination []interface{}, destinationIndex, length int) {
|
||||||
|
for x := 0; x < length; x++ {
|
||||||
|
from := sourceIndex + x
|
||||||
|
to := destinationIndex + x
|
||||||
|
|
||||||
|
destination[to] = source[from]
|
||||||
|
}
|
||||||
|
}
|
162
ring_buffer_test.go
Normal file
162
ring_buffer_test.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRingBuffer(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
buffer := NewRingBuffer()
|
||||||
|
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
assert.Equal(1, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(1, buffer.PeekBack())
|
||||||
|
|
||||||
|
buffer.Enqueue(2)
|
||||||
|
assert.Equal(2, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(2, buffer.PeekBack())
|
||||||
|
|
||||||
|
buffer.Enqueue(3)
|
||||||
|
assert.Equal(3, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(3, buffer.PeekBack())
|
||||||
|
|
||||||
|
buffer.Enqueue(4)
|
||||||
|
assert.Equal(4, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(4, buffer.PeekBack())
|
||||||
|
|
||||||
|
buffer.Enqueue(5)
|
||||||
|
assert.Equal(5, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(5, buffer.PeekBack())
|
||||||
|
|
||||||
|
buffer.Enqueue(6)
|
||||||
|
assert.Equal(6, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(6, buffer.PeekBack())
|
||||||
|
|
||||||
|
buffer.Enqueue(7)
|
||||||
|
assert.Equal(7, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(7, buffer.PeekBack())
|
||||||
|
|
||||||
|
buffer.Enqueue(8)
|
||||||
|
assert.Equal(8, buffer.Len())
|
||||||
|
assert.Equal(1, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value := buffer.Dequeue()
|
||||||
|
assert.Equal(1, value)
|
||||||
|
assert.Equal(7, buffer.Len())
|
||||||
|
assert.Equal(2, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value = buffer.Dequeue()
|
||||||
|
assert.Equal(2, value)
|
||||||
|
assert.Equal(6, buffer.Len())
|
||||||
|
assert.Equal(3, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value = buffer.Dequeue()
|
||||||
|
assert.Equal(3, value)
|
||||||
|
assert.Equal(5, buffer.Len())
|
||||||
|
assert.Equal(4, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value = buffer.Dequeue()
|
||||||
|
assert.Equal(4, value)
|
||||||
|
assert.Equal(4, buffer.Len())
|
||||||
|
assert.Equal(5, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value = buffer.Dequeue()
|
||||||
|
assert.Equal(5, value)
|
||||||
|
assert.Equal(3, buffer.Len())
|
||||||
|
assert.Equal(6, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value = buffer.Dequeue()
|
||||||
|
assert.Equal(6, value)
|
||||||
|
assert.Equal(2, buffer.Len())
|
||||||
|
assert.Equal(7, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value = buffer.Dequeue()
|
||||||
|
assert.Equal(7, value)
|
||||||
|
assert.Equal(1, buffer.Len())
|
||||||
|
assert.Equal(8, buffer.Peek())
|
||||||
|
assert.Equal(8, buffer.PeekBack())
|
||||||
|
|
||||||
|
value = buffer.Dequeue()
|
||||||
|
assert.Equal(8, value)
|
||||||
|
assert.Equal(0, buffer.Len())
|
||||||
|
assert.Nil(buffer.Peek())
|
||||||
|
assert.Nil(buffer.PeekBack())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRingBufferClear(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
buffer := NewRingBuffer()
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
|
||||||
|
assert.Equal(8, buffer.Len())
|
||||||
|
|
||||||
|
buffer.Clear()
|
||||||
|
assert.Equal(0, buffer.Len())
|
||||||
|
assert.Nil(buffer.Peek())
|
||||||
|
assert.Nil(buffer.PeekBack())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRingBufferAsSlice(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
buffer := NewRingBuffer()
|
||||||
|
buffer.Enqueue(1)
|
||||||
|
buffer.Enqueue(2)
|
||||||
|
buffer.Enqueue(3)
|
||||||
|
buffer.Enqueue(4)
|
||||||
|
buffer.Enqueue(5)
|
||||||
|
|
||||||
|
contents := buffer.AsSlice()
|
||||||
|
assert.Len(contents, 5)
|
||||||
|
assert.Equal(1, contents[0])
|
||||||
|
assert.Equal(2, contents[1])
|
||||||
|
assert.Equal(3, contents[2])
|
||||||
|
assert.Equal(4, contents[3])
|
||||||
|
assert.Equal(5, contents[4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRingBufferEach(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
buffer := NewRingBuffer()
|
||||||
|
|
||||||
|
for x := 1; x < 17; x++ {
|
||||||
|
buffer.Enqueue(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
called := 0
|
||||||
|
buffer.Each(func(v interface{}) {
|
||||||
|
if typed, isTyped := v.(int); isTyped {
|
||||||
|
if typed == (called + 1) {
|
||||||
|
called++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(16, called)
|
||||||
|
}
|
1
style.go
1
style.go
|
@ -120,6 +120,7 @@ func (s Style) GetPadding(defaults ...Box) Box {
|
||||||
func (s Style) WithDefaultsFrom(defaults Style) (final Style) {
|
func (s Style) WithDefaultsFrom(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.FillColor = s.GetFillColor(defaults.FillColor)
|
final.FillColor = s.GetFillColor(defaults.FillColor)
|
||||||
final.FontColor = s.GetFontColor(defaults.FontColor)
|
final.FontColor = s.GetFontColor(defaults.FontColor)
|
||||||
final.Font = s.GetFont(defaults.Font)
|
final.Font = s.GetFont(defaults.Font)
|
||||||
|
|
|
@ -35,6 +35,16 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
|
||||||
s1y = append(s1y, rnd.Float64()*1024)
|
s1y = append(s1y, rnd.Float64()*1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s1 := chart.TimeSeries{
|
||||||
|
Name: "a",
|
||||||
|
XValues: s1x,
|
||||||
|
YValues: s1y,
|
||||||
|
Style: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
c := chart.Chart{
|
c := chart.Chart{
|
||||||
Title: "A Test Chart",
|
Title: "A Test Chart",
|
||||||
TitleStyle: chart.Style{
|
TitleStyle: chart.Style{
|
||||||
|
@ -65,17 +75,19 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Series: []chart.Series{
|
Series: []chart.Series{
|
||||||
chart.TimeSeries{
|
s1,
|
||||||
Name: "a",
|
&chart.MovingAverageSeries{
|
||||||
XValues: s1x,
|
Name: "Average",
|
||||||
YValues: s1y,
|
|
||||||
Style: chart.Style{
|
Style: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
FillColor: chart.GetDefaultSeriesStrokeColor(0).WithAlpha(64),
|
StrokeColor: drawing.ColorRed,
|
||||||
|
StrokeDashArray: []float64{5, 1, 1},
|
||||||
},
|
},
|
||||||
|
WindowSize: 10,
|
||||||
|
InnerSeries: s1,
|
||||||
},
|
},
|
||||||
chart.AnnotationSeries{
|
chart.AnnotationSeries{
|
||||||
Name: fmt.Sprintf("%s - Last Value", "Test"),
|
Name: fmt.Sprintf("Last Value"),
|
||||||
Style: chart.Style{
|
Style: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
StrokeColor: chart.GetDefaultSeriesStrokeColor(0),
|
StrokeColor: chart.GetDefaultSeriesStrokeColor(0),
|
||||||
|
|
Loading…
Reference in a new issue