big api overhauls.
This commit is contained in:
parent
d84d6790c0
commit
cbc0002d2a
33 changed files with 356 additions and 297 deletions
|
@ -51,10 +51,10 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
|
|||
lx := canvasBox.Left + xrange.Translate(a.XValue)
|
||||
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
|
||||
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
|
||||
box.Top = MinInt(box.Top, ab.Top)
|
||||
box.Left = MinInt(box.Left, ab.Left)
|
||||
box.Right = MaxInt(box.Right, ab.Right)
|
||||
box.Bottom = MaxInt(box.Bottom, ab.Bottom)
|
||||
box.Top = Math.MinInt(box.Top, ab.Top)
|
||||
box.Left = Math.MinInt(box.Left, ab.Left)
|
||||
box.Right = Math.MaxInt(box.Right, ab.Right)
|
||||
box.Bottom = Math.MaxInt(box.Bottom, ab.Bottom)
|
||||
}
|
||||
}
|
||||
return box
|
||||
|
|
|
@ -11,8 +11,8 @@ func TestBollingerBandSeries(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
s1 := mockValueProvider{
|
||||
X: Seq(1.0, 100.0),
|
||||
Y: SeqRand(100, 1024),
|
||||
X: Sequence.Float64(1.0, 100.0),
|
||||
Y: Sequence.Random(100, 1024),
|
||||
}
|
||||
|
||||
bbs := &BollingerBandsSeries{
|
||||
|
@ -36,8 +36,8 @@ func TestBollingerBandLastValue(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
s1 := mockValueProvider{
|
||||
X: Seq(1.0, 100.0),
|
||||
Y: Seq(1.0, 100.0),
|
||||
X: Sequence.Float64(1.0, 100.0),
|
||||
Y: Sequence.Float64(1.0, 100.0),
|
||||
}
|
||||
|
||||
bbs := &BollingerBandsSeries{
|
||||
|
|
20
box.go
20
box.go
|
@ -66,12 +66,12 @@ func (b Box) GetBottom(defaults ...int) int {
|
|||
|
||||
// Width returns the width
|
||||
func (b Box) Width() int {
|
||||
return AbsInt(b.Right - b.Left)
|
||||
return Math.AbsInt(b.Right - b.Left)
|
||||
}
|
||||
|
||||
// Height returns the height
|
||||
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
|
||||
|
@ -122,10 +122,10 @@ func (b Box) Equals(other Box) bool {
|
|||
// Grow grows a box based on another box.
|
||||
func (b Box) Grow(other Box) Box {
|
||||
return Box{
|
||||
Top: MinInt(b.Top, other.Top),
|
||||
Left: MinInt(b.Left, other.Left),
|
||||
Right: MaxInt(b.Right, other.Right),
|
||||
Bottom: MaxInt(b.Bottom, other.Bottom),
|
||||
Top: Math.MinInt(b.Top, other.Top),
|
||||
Left: Math.MinInt(b.Left, other.Left),
|
||||
Right: Math.MaxInt(b.Right, other.Right),
|
||||
Bottom: Math.MaxInt(b.Bottom, other.Bottom),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,10 +186,10 @@ func (b Box) Fit(other Box) Box {
|
|||
func (b Box) Constrain(other Box) Box {
|
||||
newBox := b.Clone()
|
||||
|
||||
newBox.Top = MaxInt(newBox.Top, other.Top)
|
||||
newBox.Left = MaxInt(newBox.Left, other.Left)
|
||||
newBox.Right = MinInt(newBox.Right, other.Right)
|
||||
newBox.Bottom = MinInt(newBox.Bottom, other.Bottom)
|
||||
newBox.Top = Math.MaxInt(newBox.Top, other.Top)
|
||||
newBox.Left = Math.MaxInt(newBox.Left, other.Left)
|
||||
newBox.Right = Math.MinInt(newBox.Right, other.Right)
|
||||
newBox.Bottom = Math.MinInt(newBox.Bottom, other.Bottom)
|
||||
|
||||
return newBox
|
||||
}
|
||||
|
|
8
chart.go
8
chart.go
|
@ -230,8 +230,8 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
yrange.SetMax(maxy)
|
||||
|
||||
delta := yrange.GetDelta()
|
||||
roundTo := GetRoundToForDelta(delta)
|
||||
rmin, rmax := RoundDown(yrange.GetMin(), roundTo), RoundUp(yrange.GetMax(), roundTo)
|
||||
roundTo := Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := Math.RoundDown(yrange.GetMin(), roundTo), Math.RoundUp(yrange.GetMax(), roundTo)
|
||||
yrange.SetMin(rmin)
|
||||
yrange.SetMax(rmax)
|
||||
}
|
||||
|
@ -249,8 +249,8 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
|
|||
yrangeAlt.SetMax(maxya)
|
||||
|
||||
delta := yrangeAlt.GetDelta()
|
||||
roundTo := GetRoundToForDelta(delta)
|
||||
rmin, rmax := RoundDown(yrangeAlt.GetMin(), roundTo), RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||
roundTo := Math.GetRoundToForDelta(delta)
|
||||
rmin, rmax := Math.RoundDown(yrangeAlt.GetMin(), roundTo), Math.RoundUp(yrangeAlt.GetMax(), roundTo)
|
||||
yrangeAlt.SetMin(rmin)
|
||||
yrangeAlt.SetMax(rmax)
|
||||
}
|
||||
|
|
|
@ -384,8 +384,8 @@ func TestChartRegressionBadRangesByUser(t *testing.T) {
|
|||
},
|
||||
Series: []Series{
|
||||
ContinuousSeries{
|
||||
XValues: Seq(1.0, 10.0),
|
||||
YValues: Seq(1.0, 10.0),
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -10,18 +10,18 @@ func TestConcatSeries(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
s1 := ContinuousSeries{
|
||||
XValues: Seq(1.0, 10.0),
|
||||
YValues: Seq(1.0, 10.0),
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
|
||||
s2 := ContinuousSeries{
|
||||
XValues: Seq(11, 20.0),
|
||||
YValues: Seq(10.0, 1.0),
|
||||
XValues: Sequence.Float64(11, 20.0),
|
||||
YValues: Sequence.Float64(10.0, 1.0),
|
||||
}
|
||||
|
||||
s3 := ContinuousSeries{
|
||||
XValues: Seq(21, 30.0),
|
||||
YValues: Seq(1.0, 10.0),
|
||||
XValues: Sequence.Float64(21, 30.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
|
||||
cs := ConcatSeries([]Series{s1, s2, s3})
|
||||
|
|
|
@ -10,7 +10,7 @@ func TestRangeTranslate(t *testing.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}
|
||||
r := ContinuousRange{Domain: 1000}
|
||||
r.Min, r.Max = MinAndMax(values...)
|
||||
r.Min, r.Max = Math.MinAndMax(values...)
|
||||
|
||||
// delta = ~7.0
|
||||
// value = ~5.0
|
||||
|
|
|
@ -11,8 +11,8 @@ func TestContinuousSeries(t *testing.T) {
|
|||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Seq(1.0, 10.0),
|
||||
YValues: Seq(1.0, 10.0),
|
||||
XValues: Sequence.Float64(1.0, 10.0),
|
||||
YValues: Sequence.Float64(1.0, 10.0),
|
||||
}
|
||||
|
||||
assert.Equal("Test Series", cs.GetName())
|
||||
|
|
|
@ -33,6 +33,9 @@ const (
|
|||
// DefaultTitleTop is the default distance from the top of the chart to put the title.
|
||||
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 = 10
|
||||
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
|
||||
|
|
94
draw.go
94
draw.go
|
@ -10,7 +10,7 @@ var (
|
|||
type draw struct{}
|
||||
|
||||
// LineSeries draws a line series with a renderer.
|
||||
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider) {
|
||||
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider) {
|
||||
if vs.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
|
|||
var vx, vy float64
|
||||
var x, y int
|
||||
|
||||
fill := s.GetFillColor()
|
||||
fill := style.GetFillColor()
|
||||
if !fill.IsZero() {
|
||||
r.SetFillColor(fill)
|
||||
r.MoveTo(x0, y0)
|
||||
|
@ -41,9 +41,9 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
|
|||
r.Fill()
|
||||
}
|
||||
|
||||
r.SetStrokeColor(s.GetStrokeColor())
|
||||
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
|
||||
r.SetStrokeColor(style.GetStrokeColor())
|
||||
r.SetStrokeDashArray(style.GetStrokeDashArray())
|
||||
r.SetStrokeWidth(style.GetStrokeWidth())
|
||||
|
||||
r.MoveTo(x0, y0)
|
||||
for i := 1; i < vs.Len(); i++ {
|
||||
|
@ -56,16 +56,13 @@ func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
|
|||
}
|
||||
|
||||
// BoundedSeries draws a series that implements BoundedValueProvider.
|
||||
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
|
||||
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]
|
||||
}
|
||||
|
||||
r.SetStrokeColor(s.GetStrokeColor())
|
||||
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||
r.SetStrokeWidth(s.GetStrokeWidth())
|
||||
r.SetFillColor(s.GetFillColor())
|
||||
style.WriteToRenderer(r)
|
||||
|
||||
cb := canvasBox.Bottom
|
||||
cl := canvasBox.Left
|
||||
|
@ -110,7 +107,7 @@ func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s S
|
|||
}
|
||||
|
||||
// HistogramSeries draws a value provider as boxes from 0.
|
||||
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider, barWidths ...int) {
|
||||
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValueProvider, barWidths ...int) {
|
||||
if vs.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -137,30 +134,25 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
|
|||
Left: x - (barWidth >> 1),
|
||||
Right: x + (barWidth >> 1),
|
||||
Bottom: cb - y,
|
||||
}, s)
|
||||
}, style)
|
||||
}
|
||||
}
|
||||
|
||||
// MeasureAnnotation measures how big an annotation would be.
|
||||
func (d draw) 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))
|
||||
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 := s.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
|
||||
pr := s.Padding.GetRight(DefaultAnnotationPadding.Right)
|
||||
pb := s.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
|
||||
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 := s.GetStrokeWidth()
|
||||
strokeWidth := style.GetStrokeWidth()
|
||||
|
||||
top := ly - (pt + halfTextHeight)
|
||||
right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
|
||||
|
@ -176,14 +168,7 @@ func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int,
|
|||
|
||||
// Annotation draws an anotation with a renderer.
|
||||
func (d draw) Annotation(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))
|
||||
style.WriteToRenderer(r)
|
||||
|
||||
textBox := r.MeasureText(label)
|
||||
textWidth := textBox.Width()
|
||||
|
@ -223,10 +208,7 @@ func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, lab
|
|||
|
||||
// Box draws a box with a given style.
|
||||
func (d draw) Box(r Renderer, b Box, s Style) {
|
||||
r.SetFillColor(s.GetFillColor())
|
||||
r.SetStrokeColor(s.GetStrokeColor())
|
||||
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
|
||||
r.SetStrokeDashArray(s.GetStrokeDashArray())
|
||||
s.WriteToRenderer(r)
|
||||
|
||||
r.MoveTo(b.Left, b.Top)
|
||||
r.LineTo(b.Right, b.Top)
|
||||
|
@ -237,17 +219,41 @@ func (d draw) Box(r Renderer, b Box, s Style) {
|
|||
}
|
||||
|
||||
// DrawText draws text with a given style.
|
||||
func (d draw) Text(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())
|
||||
|
||||
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, s Style) {
|
||||
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 + ((lineBox.Width() - box.Left) >> 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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
emaXValues = Seq(1.0, 50.0)
|
||||
emaXValues = Sequence.Float64(1.0, 50.0)
|
||||
emaYValues = []float64{
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
1, 2, 3, 4, 5, 4, 3, 2,
|
||||
|
|
|
@ -30,8 +30,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: chart.Seq(1.0, 100.0),
|
||||
YValues: chart.SeqRand(100.0, 256.0),
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||
YValues: chart.Sequence.Random(100.0, 256.0),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -57,8 +57,8 @@ func drawChartDefault(res http.ResponseWriter, req *http.Request) {
|
|||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: chart.Seq(1.0, 100.0),
|
||||
YValues: chart.SeqRand(100.0, 256.0),
|
||||
XValues: chart.Sequence.Float64(1.0, 100.0),
|
||||
YValues: chart.Sequence.Random(100.0, 256.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
|
||||
graph.Elements = []chart.Renderable{
|
||||
chart.CreateLegend(&graph),
|
||||
chart.Legend(&graph),
|
||||
}
|
||||
|
||||
res.Header().Set("Content-Type", "image/png")
|
||||
|
|
|
@ -15,8 +15,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
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.
|
||||
YValues: chart.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 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.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.
|
||||
|
|
|
@ -15,8 +15,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
mainSeries := chart.ContinuousSeries{
|
||||
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.
|
||||
YValues: chart.SeqRand(100, 100), //generates a []float64 randomly from 0 to 100 with 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.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.
|
||||
|
|
|
@ -11,8 +11,8 @@ func TestHistogramSeries(t *testing.T) {
|
|||
|
||||
cs := ContinuousSeries{
|
||||
Name: "Test Series",
|
||||
XValues: Seq(1.0, 20.0),
|
||||
YValues: Seq(10.0, -10.0),
|
||||
XValues: Sequence.Float64(1.0, 20.0),
|
||||
YValues: Sequence.Float64(10.0, -10.0),
|
||||
}
|
||||
|
||||
hs := HistogramSeries{
|
||||
|
|
|
@ -68,7 +68,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
|||
}
|
||||
legendContent.Bottom += tb.Height()
|
||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||
legendContent.Right = MaxInt(legendContent.Right, right)
|
||||
legendContent.Right = Math.MaxInt(legendContent.Right, right)
|
||||
labelCount++
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ func (lrs LinearRegressionSeries) GetYAxis() YAxisType {
|
|||
|
||||
// Len returns the number of elements in the series.
|
||||
func (lrs LinearRegressionSeries) Len() int {
|
||||
return MinInt(lrs.GetWindow(), lrs.InnerSeries.Len()-lrs.GetOffset())
|
||||
return Math.MinInt(lrs.GetWindow(), lrs.InnerSeries.Len()-lrs.GetOffset())
|
||||
}
|
||||
|
||||
// GetWindow returns the window size.
|
||||
|
@ -47,7 +47,7 @@ func (lrs LinearRegressionSeries) GetWindow() int {
|
|||
|
||||
// GetEndIndex returns the effective window end.
|
||||
func (lrs LinearRegressionSeries) GetEndIndex() int {
|
||||
return MinInt(lrs.GetOffset()+(lrs.Len()), (lrs.InnerSeries.Len() - 1))
|
||||
return Math.MinInt(lrs.GetOffset()+(lrs.Len()), (lrs.InnerSeries.Len() - 1))
|
||||
}
|
||||
|
||||
// GetOffset returns the data offset.
|
||||
|
@ -67,7 +67,7 @@ func (lrs *LinearRegressionSeries) GetValue(index int) (x, y float64) {
|
|||
lrs.computeCoefficients()
|
||||
}
|
||||
offset := lrs.GetOffset()
|
||||
effectiveIndex := MinInt(index+offset, lrs.InnerSeries.Len())
|
||||
effectiveIndex := Math.MinInt(index+offset, lrs.InnerSeries.Len())
|
||||
x, y = lrs.InnerSeries.GetValue(effectiveIndex)
|
||||
y = (lrs.m * lrs.normalize(x)) + lrs.b
|
||||
return
|
||||
|
|
|
@ -11,8 +11,8 @@ func TestLinearRegressionSeries(t *testing.T) {
|
|||
|
||||
mainSeries := ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: Seq(1.0, 100.0),
|
||||
YValues: Seq(1.0, 100.0),
|
||||
XValues: Sequence.Float64(1.0, 100.0),
|
||||
YValues: Sequence.Float64(1.0, 100.0),
|
||||
}
|
||||
|
||||
linRegSeries := &LinearRegressionSeries{
|
||||
|
@ -33,8 +33,8 @@ func TestLinearRegressionSeriesDesc(t *testing.T) {
|
|||
|
||||
mainSeries := ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: Seq(100.0, 1.0),
|
||||
YValues: Seq(100.0, 1.0),
|
||||
XValues: Sequence.Float64(100.0, 1.0),
|
||||
YValues: Sequence.Float64(100.0, 1.0),
|
||||
}
|
||||
|
||||
linRegSeries := &LinearRegressionSeries{
|
||||
|
@ -55,8 +55,8 @@ func TestLinearRegressionSeriesWindowAndOffset(t *testing.T) {
|
|||
|
||||
mainSeries := ContinuousSeries{
|
||||
Name: "A test series",
|
||||
XValues: Seq(100.0, 1.0),
|
||||
YValues: Seq(100.0, 1.0),
|
||||
XValues: Sequence.Float64(100.0, 1.0),
|
||||
YValues: Sequence.Float64(100.0, 1.0),
|
||||
}
|
||||
|
||||
linRegSeries := &LinearRegressionSeries{
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
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)
|
||||
}
|
||||
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 {
|
||||
|
@ -25,8 +29,15 @@ 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 MinAndMax(values ...float64) (min float64, max float64) {
|
||||
func (m mathUtil) MinAndMax(values ...float64) (min float64, max float64) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -45,7 +56,7 @@ func MinAndMax(values ...float64) (min float64, max float64) {
|
|||
|
||||
// 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) {
|
||||
func (m mathUtil) MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
|
||||
if len(values) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -64,19 +75,8 @@ func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
|
|||
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 {
|
||||
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 {
|
||||
|
@ -88,13 +88,13 @@ func GetRoundToForDelta(delta float64) float64 {
|
|||
}
|
||||
|
||||
// RoundUp rounds up to a given roundTo value.
|
||||
func RoundUp(value, roundTo float64) float64 {
|
||||
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 RoundDown(value, roundTo float64) float64 {
|
||||
func (m mathUtil) RoundDown(value, roundTo float64) float64 {
|
||||
d1 := math.Floor(value / roundTo)
|
||||
return d1 * roundTo
|
||||
}
|
||||
|
@ -102,20 +102,20 @@ func RoundDown(value, roundTo float64) float64 {
|
|||
// 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 Normalize(values ...float64) []float64 {
|
||||
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] = RoundDown(v/total, 0.0001)
|
||||
output[x] = m.RoundDown(v/total, 0.0001)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// MinInt returns the minimum of a set of integers.
|
||||
func MinInt(values ...int) int {
|
||||
func (m mathUtil) MinInt(values ...int) int {
|
||||
min := math.MaxInt32
|
||||
for _, v := range values {
|
||||
if v < min {
|
||||
|
@ -126,7 +126,7 @@ func MinInt(values ...int) int {
|
|||
}
|
||||
|
||||
// MaxInt returns the maximum of a set of integers.
|
||||
func MaxInt(values ...int) int {
|
||||
func (m mathUtil) MaxInt(values ...int) int {
|
||||
max := math.MinInt32
|
||||
for _, v := range values {
|
||||
if v > max {
|
||||
|
@ -137,47 +137,15 @@ func MaxInt(values ...int) int {
|
|||
}
|
||||
|
||||
// AbsInt returns the absolute value of an integer.
|
||||
func AbsInt(value int) int {
|
||||
func (m mathUtil) 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
|
||||
}
|
||||
|
||||
// Sum sums a set of values.
|
||||
func Sum(values ...float64) float64 {
|
||||
func (m mathUtil) Sum(values ...float64) float64 {
|
||||
var total float64
|
||||
for _, v := range values {
|
||||
total += v
|
||||
|
@ -186,7 +154,7 @@ func Sum(values ...float64) float64 {
|
|||
}
|
||||
|
||||
// SumInt sums a set of values.
|
||||
func SumInt(values ...int) int {
|
||||
func (m mathUtil) SumInt(values ...int) int {
|
||||
var total int
|
||||
for _, v := range values {
|
||||
total += v
|
||||
|
@ -194,52 +162,32 @@ func SumInt(values ...int) int {
|
|||
return total
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (m mathUtil) PercentDifference(v1, v2 float64) float64 {
|
||||
if v1 == 0 {
|
||||
return 0
|
||||
}
|
||||
return (v2 - v1) / v1
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
// DegreesToRadians returns degrees as radians.
|
||||
func DegreesToRadians(degrees float64) float64 {
|
||||
func (m mathUtil) DegreesToRadians(degrees float64) float64 {
|
||||
return degrees * _d2r
|
||||
}
|
||||
|
||||
// RadiansToDegrees translates a radian value to a degree value.
|
||||
func RadiansToDegrees(value float64) float64 {
|
||||
func (m mathUtil) RadiansToDegrees(value float64) float64 {
|
||||
return math.Mod(value, _2pi) * _r2d
|
||||
}
|
||||
|
||||
// PercentToRadians converts a normalized value (0,1) to radians.
|
||||
func PercentToRadians(pct float64) float64 {
|
||||
return DegreesToRadians(360.0 * pct)
|
||||
func (m mathUtil) PercentToRadians(pct float64) float64 {
|
||||
return m.DegreesToRadians(360.0 * pct)
|
||||
}
|
||||
|
||||
// RadianAdd adds a delta to a base in radians.
|
||||
func RadianAdd(base, delta float64) float64 {
|
||||
func (m mathUtil) RadianAdd(base, delta float64) float64 {
|
||||
value := base + delta
|
||||
if value > _2pi {
|
||||
return math.Mod(value, _2pi)
|
||||
|
@ -250,7 +198,7 @@ func RadianAdd(base, delta float64) float64 {
|
|||
}
|
||||
|
||||
// DegreesAdd adds a delta to a base in radians.
|
||||
func DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
|
||||
func (m mathUtil) DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
|
||||
value := baseDegrees + deltaDegrees
|
||||
if value > _2pi {
|
||||
return math.Mod(value, 360.0)
|
||||
|
@ -261,13 +209,13 @@ func DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
|
|||
}
|
||||
|
||||
// DegreesToCompass returns the degree value in compass / clock orientation.
|
||||
func DegreesToCompass(deg float64) float64 {
|
||||
return DegreesAdd(deg, -90.0)
|
||||
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 CirclePoint(cx, cy int, radius, angleRadians float64) (x, y int) {
|
||||
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
|
|
@ -10,7 +10,7 @@ import (
|
|||
func TestMinAndMax(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []float64{1.0, 2.0, 3.0, 4.0}
|
||||
min, max := MinAndMax(values...)
|
||||
min, max := Math.MinAndMax(values...)
|
||||
assert.Equal(1.0, min)
|
||||
assert.Equal(4.0, max)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ func TestMinAndMax(t *testing.T) {
|
|||
func TestMinAndMaxReversed(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []float64{4.0, 2.0, 3.0, 1.0}
|
||||
min, max := MinAndMax(values...)
|
||||
min, max := Math.MinAndMax(values...)
|
||||
assert.Equal(1.0, min)
|
||||
assert.Equal(4.0, max)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ func TestMinAndMaxReversed(t *testing.T) {
|
|||
func TestMinAndMaxEmpty(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []float64{}
|
||||
min, max := MinAndMax(values...)
|
||||
min, max := Math.MinAndMax(values...)
|
||||
assert.Equal(0.0, min)
|
||||
assert.Equal(0.0, max)
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ func TestMinAndMaxOfTime(t *testing.T) {
|
|||
time.Now().AddDate(0, 0, -3),
|
||||
time.Now().AddDate(0, 0, -4),
|
||||
}
|
||||
min, max := MinAndMaxOfTime(values...)
|
||||
min, max := Math.MinAndMaxOfTime(values...)
|
||||
assert.Equal(values[3], min)
|
||||
assert.Equal(values[0], max)
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ func TestMinAndMaxOfTimeReversed(t *testing.T) {
|
|||
time.Now().AddDate(0, 0, -3),
|
||||
time.Now().AddDate(0, 0, -1),
|
||||
}
|
||||
min, max := MinAndMaxOfTime(values...)
|
||||
min, max := Math.MinAndMaxOfTime(values...)
|
||||
assert.Equal(values[0], min)
|
||||
assert.Equal(values[3], max)
|
||||
}
|
||||
|
@ -60,66 +60,45 @@ func TestMinAndMaxOfTimeReversed(t *testing.T) {
|
|||
func TestMinAndMaxOfTimeEmpty(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
values := []time.Time{}
|
||||
min, max := MinAndMaxOfTime(values...)
|
||||
min, max := Math.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))
|
||||
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, RoundUp(0.49, 0.1))
|
||||
assert.Equal(1.0, RoundUp(0.51, 1.0))
|
||||
assert.Equal(0.4999, RoundUp(0.49988, 0.0001))
|
||||
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, RoundDown(0.51, 0.1))
|
||||
assert.Equal(1.0, RoundDown(1.01, 1.0))
|
||||
assert.Equal(0.5001, RoundDown(0.50011, 0.0001))
|
||||
}
|
||||
|
||||
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)
|
||||
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, PercentDifference(1.0, 1.5))
|
||||
assert.Equal(-0.5, PercentDifference(2.0, 1.0))
|
||||
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 := Normalize(values...)
|
||||
normalized := Math.Normalize(values...)
|
||||
assert.Len(normalized, 5)
|
||||
assert.Equal(0.25, normalized[0])
|
||||
assert.Equal(0.1499, normalized[4])
|
||||
|
@ -153,7 +132,7 @@ func TestDegreesToRadians(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
for d, r := range _degreesToRadians {
|
||||
assert.Equal(r, DegreesToRadians(d))
|
||||
assert.Equal(r, Math.DegreesToRadians(d))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,7 +140,7 @@ func TestPercentToRadians(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
for d, r := range _degreesToRadians {
|
||||
assert.Equal(r, PercentToRadians(d/360.0))
|
||||
assert.Equal(r, Math.PercentToRadians(d/360.0))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,15 +148,15 @@ func TestRadiansToDegrees(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
for d, r := range _degreesToRadians {
|
||||
assert.Equal(d, RadiansToDegrees(r))
|
||||
assert.Equal(d, Math.RadiansToDegrees(r))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRadianAdd(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
assert.Equal(_pi, RadianAdd(_pi2, _pi2))
|
||||
assert.Equal(_3pi2, RadianAdd(_pi2, _pi))
|
||||
assert.Equal(_pi, RadianAdd(_pi, _2pi))
|
||||
assert.Equal(_pi, RadianAdd(_pi, -_2pi))
|
||||
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))
|
||||
}
|
42
pie_chart.go
42
pie_chart.go
|
@ -111,26 +111,13 @@ func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
|
|||
|
||||
func (pc PieChart) drawTitle(r Renderer) {
|
||||
if len(pc.Title) > 0 && pc.TitleStyle.Show {
|
||||
r.SetFont(pc.TitleStyle.GetFont(pc.GetFont()))
|
||||
r.SetFontColor(pc.TitleStyle.GetFontColor(DefaultTextColor))
|
||||
titleFontSize := pc.TitleStyle.GetFontSize(DefaultTitleFontSize)
|
||||
r.SetFontSize(titleFontSize)
|
||||
|
||||
textBox := r.MeasureText(pc.Title)
|
||||
|
||||
textWidth := textBox.Width()
|
||||
textHeight := textBox.Height()
|
||||
|
||||
titleX := (pc.GetWidth() >> 1) - (textWidth >> 1)
|
||||
titleY := pc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||||
|
||||
r.Text(pc.Title, titleX, titleY)
|
||||
Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
|
||||
}
|
||||
}
|
||||
|
||||
func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
||||
cx, cy := canvasBox.Center()
|
||||
diameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||||
diameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
|
||||
radius := float64(diameter >> 1)
|
||||
labelRadius := (radius * 2.0) / 3.0
|
||||
|
||||
|
@ -141,8 +128,8 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
|||
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||
|
||||
r.MoveTo(cx, cy)
|
||||
rads = PercentToRadians(total)
|
||||
delta = PercentToRadians(v.Value)
|
||||
rads = Math.PercentToRadians(total)
|
||||
delta = Math.PercentToRadians(v.Value)
|
||||
|
||||
r.ArcTo(cx, cy, radius, radius, rads, delta)
|
||||
|
||||
|
@ -157,9 +144,9 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
|||
for index, v := range values {
|
||||
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
|
||||
if len(v.Label) > 0 {
|
||||
delta2 = PercentToRadians(total + (v.Value / 2.0))
|
||||
delta2 = RadianAdd(delta2, _pi2)
|
||||
lx, ly = CirclePoint(cx, cy, labelRadius, delta2)
|
||||
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)
|
||||
|
@ -180,7 +167,7 @@ func (pc PieChart) getDefaultCanvasBox() Box {
|
|||
}
|
||||
|
||||
func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
|
||||
circleDiameter := MinInt(canvasBox.Width(), canvasBox.Height())
|
||||
circleDiameter := Math.MinInt(canvasBox.Width(), canvasBox.Height())
|
||||
|
||||
square := Box{
|
||||
Right: circleDiameter,
|
||||
|
@ -226,7 +213,7 @@ func (pc PieChart) stylePieChartValue(index int) Style {
|
|||
}
|
||||
|
||||
func (pc PieChart) getScaledFontSize() float64 {
|
||||
effectiveDimension := MinInt(pc.GetWidth(), pc.GetHeight())
|
||||
effectiveDimension := Math.MinInt(pc.GetWidth(), pc.GetHeight())
|
||||
if effectiveDimension >= 2048 {
|
||||
return 48.0
|
||||
} else if effectiveDimension >= 1024 {
|
||||
|
@ -253,6 +240,17 @@ func (pc PieChart) styleDefaultsElements() Style {
|
|||
}
|
||||
}
|
||||
|
||||
func (pc PieChart) styleDefaultsTitle() Style {
|
||||
return pc.TitleStyle.InheritFrom(Style{
|
||||
FontColor: DefaultTextColor,
|
||||
Font: pc.GetFont(),
|
||||
FontSize: 24.0,
|
||||
TextHorizontalAlign: TextHorizontalAlignCenter,
|
||||
TextVerticalAlign: TextVerticalAlignTop,
|
||||
TextWrap: TextWrapNone,
|
||||
})
|
||||
}
|
||||
|
||||
// Box returns the chart bounds as a box.
|
||||
func (pc PieChart) Box() Box {
|
||||
dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
|
||||
|
|
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)
|
||||
}
|
|
@ -72,7 +72,7 @@ func (sma SMASeries) GetLastValue() (x, y float64) {
|
|||
|
||||
func (sma SMASeries) getAverage(index int) float64 {
|
||||
period := sma.GetPeriod()
|
||||
floor := MaxInt(0, index-period)
|
||||
floor := Math.MaxInt(0, index-period)
|
||||
var accum float64
|
||||
var count float64
|
||||
for x := index; x >= floor; x-- {
|
||||
|
|
|
@ -12,14 +12,14 @@ type mockValueProvider struct {
|
|||
}
|
||||
|
||||
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) {
|
||||
if index < 0 {
|
||||
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")
|
||||
}
|
||||
x = m.X[index]
|
||||
|
@ -31,8 +31,8 @@ func TestSMASeriesGetValue(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
mockSeries := mockValueProvider{
|
||||
Seq(1.0, 10.0),
|
||||
Seq(10, 1.0),
|
||||
Sequence.Float64(1.0, 10.0),
|
||||
Sequence.Float64(10, 1.0),
|
||||
}
|
||||
assert.Equal(10, mockSeries.Len())
|
||||
|
||||
|
@ -62,8 +62,8 @@ func TestSMASeriesGetLastValueWindowOverlap(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
mockSeries := mockValueProvider{
|
||||
Seq(1.0, 10.0),
|
||||
Seq(10, 1.0),
|
||||
Sequence.Float64(1.0, 10.0),
|
||||
Sequence.Float64(10, 1.0),
|
||||
}
|
||||
assert.Equal(10, mockSeries.Len())
|
||||
|
||||
|
@ -88,8 +88,8 @@ func TestSMASeriesGetLastValue(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
mockSeries := mockValueProvider{
|
||||
Seq(1.0, 100.0),
|
||||
Seq(100, 1.0),
|
||||
Sequence.Float64(1.0, 100.0),
|
||||
Sequence.Float64(100, 1.0),
|
||||
}
|
||||
assert.Equal(100, mockSeries.Len())
|
||||
|
||||
|
|
14
style.go
14
style.go
|
@ -25,6 +25,7 @@ type Style struct {
|
|||
TextHorizontalAlign textHorizontalAlign
|
||||
TextVerticalAlign textVerticalAlign
|
||||
TextWrap textWrap
|
||||
TextLineSpacing int
|
||||
}
|
||||
|
||||
// IsZero returns if the object is set or not.
|
||||
|
@ -222,6 +223,17 @@ func (s Style) GetTextWrap(defaults ...textWrap) textWrap {
|
|||
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())
|
||||
|
@ -261,6 +273,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -299,5 +312,6 @@ func (s Style) GetTextOptions() Style {
|
|||
TextHorizontalAlign: s.TextHorizontalAlign,
|
||||
TextVerticalAlign: s.TextVerticalAlign,
|
||||
TextWrap: s.TextWrap,
|
||||
TextLineSpacing: s.TextLineSpacing,
|
||||
}
|
||||
}
|
||||
|
|
29
text.go
29
text.go
|
@ -64,15 +64,12 @@ type TextStyle struct {
|
|||
|
||||
type text struct{}
|
||||
|
||||
func (t text) WrapFit(r Renderer, value string, width int, style Style, wrapOption textWrap) []string {
|
||||
valueBox := r.MeasureText(value)
|
||||
if valueBox.Width() > width {
|
||||
switch wrapOption {
|
||||
case TextWrapRune:
|
||||
return t.WrapFitRune(r, value, width, style)
|
||||
case TextWrapWord:
|
||||
return t.WrapFitWord(r, value, width, style)
|
||||
}
|
||||
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}
|
||||
}
|
||||
|
@ -143,6 +140,20 @@ 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}
|
||||
|
|
28
text_test.go
28
text_test.go
|
@ -25,8 +25,36 @@ func TestTextWrapWord(t *testing.T) {
|
|||
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])
|
||||
}
|
||||
|
|
6
value.go
6
value.go
|
@ -21,18 +21,18 @@ func (vs Values) Values() []float64 {
|
|||
|
||||
// ValuesNormalized returns normalized values.
|
||||
func (vs Values) ValuesNormalized() []float64 {
|
||||
return Normalize(vs.Values()...)
|
||||
return Math.Normalize(vs.Values()...)
|
||||
}
|
||||
|
||||
// Normalize returns the values normalized.
|
||||
func (vs Values) Normalize() []Value {
|
||||
output := make([]Value, len(vs))
|
||||
total := Sum(vs.Values()...)
|
||||
total := Math.Sum(vs.Values()...)
|
||||
for index, v := range vs {
|
||||
output[index] = Value{
|
||||
Style: v.Style,
|
||||
Label: v.Label,
|
||||
Value: RoundDown(v.Value/total, 0.0001),
|
||||
Value: Math.RoundDown(v.Value/total, 0.0001),
|
||||
}
|
||||
}
|
||||
return output
|
||||
|
|
|
@ -83,8 +83,8 @@ func (vr *vectorRenderer) QuadCurveTo(cx, cy, x, y int) {
|
|||
}
|
||||
|
||||
func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
|
||||
startAngle = RadianAdd(startAngle, _pi2)
|
||||
endAngle := RadianAdd(startAngle, delta)
|
||||
startAngle = Math.RadianAdd(startAngle, _pi2)
|
||||
endAngle := Math.RadianAdd(startAngle, delta)
|
||||
|
||||
startx := cx + int(rx*math.Sin(startAngle))
|
||||
starty := cy - int(ry*math.Cos(startAngle))
|
||||
|
@ -98,7 +98,7 @@ func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
|
|||
endx := cx + int(rx*math.Sin(endAngle))
|
||||
endy := cy - int(ry*math.Cos(endAngle))
|
||||
|
||||
dd := RadiansToDegrees(delta)
|
||||
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))
|
||||
}
|
||||
|
|
8
xaxis.go
8
xaxis.go
|
@ -66,10 +66,10 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
|||
tx := canvasBox.Left + lx
|
||||
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
||||
|
||||
top = MinInt(top, canvasBox.Bottom)
|
||||
left = MinInt(left, tx-(tb.Width()>>1))
|
||||
right = MaxInt(right, tx+(tb.Width()>>1))
|
||||
bottom = MaxInt(bottom, ty)
|
||||
top = Math.MinInt(top, canvasBox.Bottom)
|
||||
left = Math.MinInt(left, tx-(tb.Width()>>1))
|
||||
right = Math.MaxInt(right, tx+(tb.Width()>>1))
|
||||
bottom = Math.MaxInt(bottom, ty)
|
||||
}
|
||||
|
||||
return Box{
|
||||
|
|
10
yaxis.go
10
yaxis.go
|
@ -85,13 +85,13 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
|||
|
||||
if ya.AxisType == YAxisPrimary {
|
||||
minx = canvasBox.Right
|
||||
maxx = MaxInt(maxx, tx+tb.Width())
|
||||
maxx = Math.MaxInt(maxx, tx+tb.Width())
|
||||
} else if ya.AxisType == YAxisSecondary {
|
||||
minx = MinInt(minx, finalTextX)
|
||||
maxx = MaxInt(maxx, tx)
|
||||
minx = Math.MinInt(minx, finalTextX)
|
||||
maxx = Math.MaxInt(maxx, tx)
|
||||
}
|
||||
miny = MinInt(miny, ly-tb.Height()>>1)
|
||||
maxy = MaxInt(maxy, ly+tb.Height()>>1)
|
||||
miny = Math.MinInt(miny, ly-tb.Height()>>1)
|
||||
maxy = Math.MaxInt(maxy, ly+tb.Height()>>1)
|
||||
}
|
||||
|
||||
return Box{
|
||||
|
|
Loading…
Reference in a new issue