Compare commits
12 commits
main
...
candlestic
Author | SHA1 | Date | |
---|---|---|---|
|
de6df027fc | ||
|
e3e851d2d1 | ||
|
b537fd02cb | ||
|
5cf4f5f0d7 | ||
|
04a4edcb46 | ||
|
5936b89e89 | ||
|
51f3cca5d7 | ||
|
7ba2992824 | ||
|
7d1401898a | ||
|
e39acdfb76 | ||
|
566d798b32 | ||
|
73e3e439c5 |
40 changed files with 1288 additions and 303 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
.vscode
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
language: go
|
language: go
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.6.2
|
- 1.8.1
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
|
|
||||||
|
|
82
_examples/candlestick_series/main.go
Normal file
82
_examples/candlestick_series/main.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
chart "github.com/wcharczuk/go-chart"
|
||||||
|
util "github.com/wcharczuk/go-chart/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func stockData() (times []time.Time, prices []float64) {
|
||||||
|
start := time.Date(2017, 05, 15, 9, 30, 0, 0, util.Date.Eastern())
|
||||||
|
price := 256.0
|
||||||
|
for day := 0; day < 60; day++ {
|
||||||
|
cursor := start.AddDate(0, 0, day)
|
||||||
|
|
||||||
|
if util.Date.IsNYSEHoliday(cursor) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for minute := 0; minute < ((6 * 60) + 30); minute++ {
|
||||||
|
cursor = cursor.Add(time.Minute)
|
||||||
|
|
||||||
|
if rand.Float64() >= 0.5 {
|
||||||
|
price = price + (rand.Float64() * (price * 0.01))
|
||||||
|
} else {
|
||||||
|
price = price - (rand.Float64() * (price * 0.01))
|
||||||
|
}
|
||||||
|
|
||||||
|
times = append(times, cursor)
|
||||||
|
prices = append(prices, price)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
xv, yv := stockData()
|
||||||
|
|
||||||
|
priceSeries := chart.TimeSeries{
|
||||||
|
Name: "SPY",
|
||||||
|
Style: chart.Style{
|
||||||
|
Show: false,
|
||||||
|
StrokeColor: chart.GetDefaultColor(0),
|
||||||
|
},
|
||||||
|
XValues: xv,
|
||||||
|
YValues: yv,
|
||||||
|
}
|
||||||
|
|
||||||
|
candleSeries := chart.CandlestickSeries{
|
||||||
|
Name: "SPY",
|
||||||
|
XValues: xv,
|
||||||
|
YValues: yv,
|
||||||
|
}
|
||||||
|
|
||||||
|
graph := chart.Chart{
|
||||||
|
XAxis: chart.XAxis{
|
||||||
|
Style: chart.Style{Show: true, FontSize: 8, TextRotationDegrees: 45},
|
||||||
|
TickPosition: chart.TickPositionUnderTick,
|
||||||
|
Range: &chart.MarketHoursRange{},
|
||||||
|
},
|
||||||
|
YAxis: chart.YAxis{
|
||||||
|
Style: chart.Style{Show: true},
|
||||||
|
},
|
||||||
|
Series: []chart.Series{
|
||||||
|
candleSeries,
|
||||||
|
priceSeries,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
err := graph.Render(chart.PNG, res)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", drawChart)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
49
_examples/overlap/main.go
Normal file
49
_examples/overlap/main.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
chart "github.com/wcharczuk/go-chart"
|
||||||
|
"github.com/wcharczuk/go-chart/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func conditionalColor(condition bool, trueColor drawing.Color, falseColor drawing.Color) drawing.Color {
|
||||||
|
if condition {
|
||||||
|
return trueColor
|
||||||
|
}
|
||||||
|
return falseColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
r, _ := chart.PNG(1024, 1024)
|
||||||
|
|
||||||
|
b0 := chart.Box{Left: 100, Top: 100, Right: 400, Bottom: 200}
|
||||||
|
b1 := chart.Box{Left: 500, Top: 100, Right: 900, Bottom: 200}
|
||||||
|
b0r := b0.Corners().Rotate(45).Shift(0, 200)
|
||||||
|
|
||||||
|
chart.Draw.Box(r, b0, chart.Style{
|
||||||
|
StrokeColor: drawing.ColorRed,
|
||||||
|
StrokeWidth: 2,
|
||||||
|
FillColor: conditionalColor(b0.Corners().Overlaps(b1.Corners()), drawing.ColorRed, drawing.ColorTransparent),
|
||||||
|
})
|
||||||
|
|
||||||
|
chart.Draw.Box(r, b1, chart.Style{
|
||||||
|
StrokeColor: drawing.ColorBlue,
|
||||||
|
StrokeWidth: 2,
|
||||||
|
FillColor: conditionalColor(b1.Corners().Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent),
|
||||||
|
})
|
||||||
|
|
||||||
|
chart.Draw.Box2d(r, b0r, chart.Style{
|
||||||
|
StrokeColor: drawing.ColorGreen,
|
||||||
|
StrokeWidth: 2,
|
||||||
|
FillColor: conditionalColor(b0r.Overlaps(b0.Corners()), drawing.ColorRed, drawing.ColorTransparent),
|
||||||
|
})
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
r.Save(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", drawChart)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
|
@ -32,11 +32,17 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
tbc := tb.Corners().Rotate(45)
|
tbc := tb.Corners().Rotate(45)
|
||||||
|
|
||||||
chart.Draw.BoxCorners(r, tbc, chart.Style{
|
chart.Draw.Box2d(r, tbc, chart.Style{
|
||||||
StrokeColor: drawing.ColorRed,
|
StrokeColor: drawing.ColorRed,
|
||||||
StrokeWidth: 2,
|
StrokeWidth: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tbc2 := tbc.Shift(tbc.Height(), 0)
|
||||||
|
chart.Draw.Box2d(r, tbc2, chart.Style{
|
||||||
|
StrokeColor: drawing.ColorGreen,
|
||||||
|
StrokeWidth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
tbcb := tbc.Box()
|
tbcb := tbc.Box()
|
||||||
chart.Draw.Box(r, tbcb, chart.Style{
|
chart.Draw.Box(r, tbcb, chart.Style{
|
||||||
StrokeColor: drawing.ColorBlue,
|
StrokeColor: drawing.ColorBlue,
|
||||||
|
|
|
@ -261,7 +261,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
var ty int
|
var ty int
|
||||||
var tb Box
|
var tb Box2d
|
||||||
for _, t := range ticks {
|
for _, t := range ticks {
|
||||||
ty = canvasBox.Bottom - yr.Translate(t.Value)
|
ty = canvasBox.Bottom - yr.Translate(t.Value)
|
||||||
|
|
||||||
|
@ -272,7 +272,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
|
||||||
|
|
||||||
axisStyle.GetTextOptions().WriteToRenderer(r)
|
axisStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
tb = r.MeasureText(t.Label)
|
tb = r.MeasureText(t.Label)
|
||||||
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
|
Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(int(tb.Height())>>1), axisStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -369,7 +369,7 @@ func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range,
|
||||||
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
|
lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
|
||||||
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||||
|
|
||||||
xaxisHeight = util.Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
xaxisHeight = util.Math.MinInt(int(linesBox.Height())+(2*DefaultXAxisMargin), xaxisHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
109
box.go
109
box.go
|
@ -2,7 +2,6 @@ package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
|
|
||||||
util "github.com/wcharczuk/go-chart/util"
|
util "github.com/wcharczuk/go-chart/util"
|
||||||
)
|
)
|
||||||
|
@ -166,12 +165,12 @@ func (b Box) Shift(x, y int) Box {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Corners returns the box as a set of corners.
|
// Corners returns the box as a set of corners.
|
||||||
func (b Box) Corners() BoxCorners {
|
func (b Box) Corners() Box2d {
|
||||||
return BoxCorners{
|
return Box2d{
|
||||||
TopLeft: Point{b.Left, b.Top},
|
TopLeft: Point{float64(b.Left), float64(b.Top)},
|
||||||
TopRight: Point{b.Right, b.Top},
|
TopRight: Point{float64(b.Right), float64(b.Top)},
|
||||||
BottomRight: Point{b.Right, b.Bottom},
|
BottomRight: Point{float64(b.Right), float64(b.Bottom)},
|
||||||
BottomLeft: Point{b.Left, b.Bottom},
|
BottomLeft: Point{float64(b.Left), float64(b.Bottom)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,99 +254,3 @@ func (b Box) OuterConstrain(bounds, other Box) Box {
|
||||||
}
|
}
|
||||||
return newBox
|
return newBox
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoxCorners is a box with independent corners.
|
|
||||||
type BoxCorners struct {
|
|
||||||
TopLeft, TopRight, BottomRight, BottomLeft Point
|
|
||||||
}
|
|
||||||
|
|
||||||
// Box return the BoxCorners as a regular box.
|
|
||||||
func (bc BoxCorners) Box() Box {
|
|
||||||
return Box{
|
|
||||||
Top: util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
|
|
||||||
Left: util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
|
|
||||||
Right: util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
|
|
||||||
Bottom: util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Width returns the width
|
|
||||||
func (bc BoxCorners) Width() int {
|
|
||||||
minLeft := util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
|
|
||||||
maxRight := util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
|
|
||||||
return maxRight - minLeft
|
|
||||||
}
|
|
||||||
|
|
||||||
// Height returns the height
|
|
||||||
func (bc BoxCorners) Height() int {
|
|
||||||
minTop := util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
|
|
||||||
maxBottom := util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
|
||||||
return maxBottom - minTop
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center returns the center of the box
|
|
||||||
func (bc BoxCorners) Center() (x, y int) {
|
|
||||||
|
|
||||||
left := util.Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
|
|
||||||
right := util.Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
|
|
||||||
x = ((right - left) >> 1) + left
|
|
||||||
|
|
||||||
top := util.Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
|
|
||||||
bottom := util.Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
|
|
||||||
y = ((bottom - top) >> 1) + top
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rotate rotates the box.
|
|
||||||
func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
|
|
||||||
cx, cy := bc.Center()
|
|
||||||
|
|
||||||
thetaRadians := util.Math.DegreesToRadians(thetaDegrees)
|
|
||||||
|
|
||||||
tlx, tly := util.Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
|
|
||||||
trx, try := util.Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
|
|
||||||
brx, bry := util.Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
|
|
||||||
blx, bly := util.Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
|
|
||||||
|
|
||||||
return BoxCorners{
|
|
||||||
TopLeft: Point{tlx, tly},
|
|
||||||
TopRight: Point{trx, try},
|
|
||||||
BottomRight: Point{brx, bry},
|
|
||||||
BottomLeft: Point{blx, bly},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals returns if the box equals another box.
|
|
||||||
func (bc BoxCorners) Equals(other BoxCorners) bool {
|
|
||||||
return bc.TopLeft.Equals(other.TopLeft) &&
|
|
||||||
bc.TopRight.Equals(other.TopRight) &&
|
|
||||||
bc.BottomRight.Equals(other.BottomRight) &&
|
|
||||||
bc.BottomLeft.Equals(other.BottomLeft)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc BoxCorners) String() string {
|
|
||||||
return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point is an X,Y pair
|
|
||||||
type Point struct {
|
|
||||||
X, Y int
|
|
||||||
}
|
|
||||||
|
|
||||||
// DistanceTo calculates the distance to another point.
|
|
||||||
func (p Point) DistanceTo(other Point) float64 {
|
|
||||||
dx := math.Pow(float64(p.X-other.X), 2)
|
|
||||||
dy := math.Pow(float64(p.Y-other.Y), 2)
|
|
||||||
return math.Pow(dx+dy, 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals returns if a point equals another point.
|
|
||||||
func (p Point) Equals(other Point) bool {
|
|
||||||
return p.X == other.X && p.Y == other.Y
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of the point.
|
|
||||||
func (p Point) String() string {
|
|
||||||
return fmt.Sprintf("P{%d,%d}", p.X, p.Y)
|
|
||||||
}
|
|
||||||
|
|
183
box_2d.go
Normal file
183
box_2d.go
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
util "github.com/wcharczuk/go-chart/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Box2d is a box with (4) independent corners.
|
||||||
|
// It is used when dealing with ~rotated~ boxes.
|
||||||
|
type Box2d struct {
|
||||||
|
TopLeft, TopRight, BottomRight, BottomLeft Point
|
||||||
|
}
|
||||||
|
|
||||||
|
// Points returns the constituent points of the box.
|
||||||
|
func (bc Box2d) Points() []Point {
|
||||||
|
return []Point{
|
||||||
|
bc.TopRight,
|
||||||
|
bc.BottomRight,
|
||||||
|
bc.BottomLeft,
|
||||||
|
bc.TopLeft,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box return the Box2d as a regular box.
|
||||||
|
func (bc Box2d) Box() Box {
|
||||||
|
return Box{
|
||||||
|
Top: int(bc.Top()),
|
||||||
|
Left: int(bc.Left()),
|
||||||
|
Right: int(bc.Right()),
|
||||||
|
Bottom: int(bc.Bottom()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top returns the top-most corner y value.
|
||||||
|
func (bc Box2d) Top() float64 {
|
||||||
|
return math.Min(bc.TopLeft.Y, bc.TopRight.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left returns the left-most corner x value.
|
||||||
|
func (bc Box2d) Left() float64 {
|
||||||
|
return math.Min(bc.TopLeft.X, bc.BottomLeft.X)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right returns the right-most corner x value.
|
||||||
|
func (bc Box2d) Right() float64 {
|
||||||
|
return math.Max(bc.TopRight.X, bc.BottomRight.X)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom returns the bottom-most corner y value.
|
||||||
|
func (bc Box2d) Bottom() float64 {
|
||||||
|
return math.Max(bc.BottomLeft.Y, bc.BottomLeft.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the width
|
||||||
|
func (bc Box2d) Width() float64 {
|
||||||
|
minLeft := math.Min(bc.TopLeft.X, bc.BottomLeft.X)
|
||||||
|
maxRight := math.Max(bc.TopRight.X, bc.BottomRight.X)
|
||||||
|
return maxRight - minLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns the height
|
||||||
|
func (bc Box2d) Height() float64 {
|
||||||
|
minTop := math.Min(bc.TopLeft.Y, bc.TopRight.Y)
|
||||||
|
maxBottom := math.Max(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||||
|
return maxBottom - minTop
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center returns the center of the box
|
||||||
|
func (bc Box2d) Center() (x, y float64) {
|
||||||
|
left := util.Math.Mean(bc.TopLeft.X, bc.BottomLeft.X)
|
||||||
|
right := util.Math.Mean(bc.TopRight.X, bc.BottomRight.X)
|
||||||
|
x = ((right - left) / 2.0) + left
|
||||||
|
|
||||||
|
top := util.Math.Mean(bc.TopLeft.Y, bc.TopRight.Y)
|
||||||
|
bottom := util.Math.Mean(bc.BottomLeft.Y, bc.BottomRight.Y)
|
||||||
|
y = ((bottom - top) / 2.0) + top
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate rotates the box.
|
||||||
|
func (bc Box2d) Rotate(thetaDegrees float64) Box2d {
|
||||||
|
cx, cy := bc.Center()
|
||||||
|
|
||||||
|
thetaRadians := util.Math.DegreesToRadians(thetaDegrees)
|
||||||
|
|
||||||
|
tlx, tly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopLeft.X), int(bc.TopLeft.Y), thetaRadians)
|
||||||
|
trx, try := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.TopRight.X), int(bc.TopRight.Y), thetaRadians)
|
||||||
|
brx, bry := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomRight.X), int(bc.BottomRight.Y), thetaRadians)
|
||||||
|
blx, bly := util.Math.RotateCoordinate(int(cx), int(cy), int(bc.BottomLeft.X), int(bc.BottomLeft.Y), thetaRadians)
|
||||||
|
|
||||||
|
return Box2d{
|
||||||
|
TopLeft: Point{float64(tlx), float64(tly)},
|
||||||
|
TopRight: Point{float64(trx), float64(try)},
|
||||||
|
BottomRight: Point{float64(brx), float64(bry)},
|
||||||
|
BottomLeft: Point{float64(blx), float64(bly)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift shifts a box by a given x and y value.
|
||||||
|
func (bc Box2d) Shift(x, y float64) Box2d {
|
||||||
|
return Box2d{
|
||||||
|
TopLeft: bc.TopLeft.Shift(x, y),
|
||||||
|
TopRight: bc.TopRight.Shift(x, y),
|
||||||
|
BottomRight: bc.BottomRight.Shift(x, y),
|
||||||
|
BottomLeft: bc.BottomLeft.Shift(x, y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns if the box equals another box.
|
||||||
|
func (bc Box2d) Equals(other Box2d) bool {
|
||||||
|
return bc.TopLeft.Equals(other.TopLeft) &&
|
||||||
|
bc.TopRight.Equals(other.TopRight) &&
|
||||||
|
bc.BottomRight.Equals(other.BottomRight) &&
|
||||||
|
bc.BottomLeft.Equals(other.BottomLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlaps returns if two boxes overlap.
|
||||||
|
func (bc Box2d) Overlaps(other Box2d) bool {
|
||||||
|
pa := bc.Points()
|
||||||
|
pb := other.Points()
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
for j := 0; j < 4; j++ {
|
||||||
|
pa0 := pa[i]
|
||||||
|
pa1 := pa[(i+1)%4]
|
||||||
|
|
||||||
|
pb0 := pb[j]
|
||||||
|
pb1 := pb[(j+1)%4]
|
||||||
|
|
||||||
|
if util.Math.LinesIntersect(pa0.X, pa0.Y, pa1.X, pa1.Y, pb0.X, pb0.Y, pb1.X, pb1.Y) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow grows a box by a given set of dimensions.
|
||||||
|
func (bc Box2d) Grow(by Box) Box2d {
|
||||||
|
top, left, right, bottom := float64(by.Top), float64(by.Left), float64(by.Right), float64(by.Bottom)
|
||||||
|
return Box2d{
|
||||||
|
TopLeft: Point{X: bc.TopLeft.X - left, Y: bc.TopLeft.Y - top},
|
||||||
|
TopRight: Point{X: bc.TopRight.X + right, Y: bc.TopRight.Y - top},
|
||||||
|
BottomRight: Point{X: bc.BottomRight.X + right, Y: bc.BottomRight.Y + bottom},
|
||||||
|
BottomLeft: Point{X: bc.BottomLeft.X - left, Y: bc.BottomLeft.Y + bottom},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc Box2d) String() string {
|
||||||
|
return fmt.Sprintf("Box2d{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point is an X,Y pair
|
||||||
|
type Point struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift shifts a point.
|
||||||
|
func (p Point) Shift(x, y float64) Point {
|
||||||
|
return Point{
|
||||||
|
X: p.X + x,
|
||||||
|
Y: p.Y + y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DistanceTo calculates the distance to another point.
|
||||||
|
func (p Point) DistanceTo(other Point) float64 {
|
||||||
|
dx := math.Pow(p.X-other.X, 2)
|
||||||
|
dy := math.Pow(p.Y-other.Y, 2)
|
||||||
|
return math.Pow(dx+dy, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns if a point equals another point.
|
||||||
|
func (p Point) Equals(other Point) bool {
|
||||||
|
return p.X == other.X && p.Y == other.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the point.
|
||||||
|
func (p Point) String() string {
|
||||||
|
return fmt.Sprintf("(%.2f,%.2f)", p.X, p.Y)
|
||||||
|
}
|
66
box_2d_test.go
Normal file
66
box_2d_test.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBox2dCenter(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
bc := Box2d{
|
||||||
|
TopLeft: Point{5, 5},
|
||||||
|
TopRight: Point{15, 5},
|
||||||
|
BottomRight: Point{15, 15},
|
||||||
|
BottomLeft: Point{5, 15},
|
||||||
|
}
|
||||||
|
|
||||||
|
cx, cy := bc.Center()
|
||||||
|
assert.Equal(10, cx)
|
||||||
|
assert.Equal(10, cy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBox2dRotate(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
bc := Box2d{
|
||||||
|
TopLeft: Point{5, 5},
|
||||||
|
TopRight: Point{15, 5},
|
||||||
|
BottomRight: Point{15, 15},
|
||||||
|
BottomLeft: Point{5, 15},
|
||||||
|
}
|
||||||
|
|
||||||
|
rotated := bc.Rotate(45)
|
||||||
|
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBox2dOverlaps(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
bc := Box2d{
|
||||||
|
TopLeft: Point{5, 5},
|
||||||
|
TopRight: Point{15, 5},
|
||||||
|
BottomRight: Point{15, 15},
|
||||||
|
BottomLeft: Point{5, 15},
|
||||||
|
}
|
||||||
|
|
||||||
|
// shift meaningfully the full width of bc right.
|
||||||
|
bc2 := bc.Shift(bc.Width()+1, 0)
|
||||||
|
assert.False(bc.Overlaps(bc2), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc2))
|
||||||
|
|
||||||
|
// shift meaningfully the full height of bc down.
|
||||||
|
bc3 := bc.Shift(0, bc.Height()+1)
|
||||||
|
assert.False(bc.Overlaps(bc3), fmt.Sprintf("%v\n\t\tshould not overlap\n\t%v", bc, bc3))
|
||||||
|
|
||||||
|
bc4 := bc.Shift(5, 0)
|
||||||
|
assert.True(bc.Overlaps(bc4))
|
||||||
|
|
||||||
|
bc5 := bc.Shift(0, 5)
|
||||||
|
assert.True(bc.Overlaps(bc5))
|
||||||
|
|
||||||
|
bcr := bc.Rotate(45)
|
||||||
|
bcr2 := bc.Rotate(45).Shift(bc.Width()/2.0, 0)
|
||||||
|
assert.True(bcr.Overlaps(bcr2), fmt.Sprintf("%v\n\t\tshould overlap\n\t%v", bcr, bcr2))
|
||||||
|
}
|
29
box_test.go
29
box_test.go
|
@ -157,32 +157,3 @@ func TestBoxCenter(t *testing.T) {
|
||||||
assert.Equal(15, cx)
|
assert.Equal(15, cx)
|
||||||
assert.Equal(20, cy)
|
assert.Equal(20, cy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBoxCornersCenter(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
bc := BoxCorners{
|
|
||||||
TopLeft: Point{5, 5},
|
|
||||||
TopRight: Point{15, 5},
|
|
||||||
BottomRight: Point{15, 15},
|
|
||||||
BottomLeft: Point{5, 15},
|
|
||||||
}
|
|
||||||
|
|
||||||
cx, cy := bc.Center()
|
|
||||||
assert.Equal(10, cx)
|
|
||||||
assert.Equal(10, cy)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBoxCornersRotate(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
bc := BoxCorners{
|
|
||||||
TopLeft: Point{5, 5},
|
|
||||||
TopRight: Point{15, 5},
|
|
||||||
BottomRight: Point{15, 15},
|
|
||||||
BottomLeft: Point{5, 15},
|
|
||||||
}
|
|
||||||
|
|
||||||
rotated := bc.Rotate(45)
|
|
||||||
assert.True(rotated.TopLeft.Equals(Point{10, 3}), rotated.String())
|
|
||||||
}
|
|
||||||
|
|
157
candlestick_series.go
Normal file
157
candlestick_series.go
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CandleValue is a day's data for a candlestick plot.
|
||||||
|
type CandleValue struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
High float64
|
||||||
|
Low float64
|
||||||
|
Open float64
|
||||||
|
Close float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string value for the candle value.
|
||||||
|
func (cv CandleValue) String() string {
|
||||||
|
return fmt.Sprintf("candle %s high: %.2f low: %.2f open: %.2f close: %.2f", cv.Timestamp.Format("2006-01-02"), cv.High, cv.Low, cv.Open, cv.Close)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the value is zero or not.
|
||||||
|
func (cv CandleValue) IsZero() bool {
|
||||||
|
return cv.Timestamp.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CandlestickSeries is a special type of series that takes a norma value provider
|
||||||
|
// and maps it to day value stats (high, low, open, close).
|
||||||
|
type CandlestickSeries struct {
|
||||||
|
Name string
|
||||||
|
Style Style
|
||||||
|
YAxis YAxisType
|
||||||
|
|
||||||
|
// CandleValues will be used in place of creating them from the `InnerSeries`.
|
||||||
|
CandleValues []CandleValue
|
||||||
|
|
||||||
|
// InnerSeries is used if the `CandleValues` are not set.
|
||||||
|
InnerSeries ValuesProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName implements Series.GetName.
|
||||||
|
func (cs *CandlestickSeries) GetName() string {
|
||||||
|
return cs.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStyle implements Series.GetStyle.
|
||||||
|
func (cs *CandlestickSeries) GetStyle() Style {
|
||||||
|
return cs.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYAxis returns which yaxis the series is mapped to.
|
||||||
|
func (cs *CandlestickSeries) GetYAxis() YAxisType {
|
||||||
|
return cs.YAxis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the series.
|
||||||
|
func (cs *CandlestickSeries) Len() int {
|
||||||
|
return len(cs.GetCandleValues())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBoundedValues returns the bounded values at a given index.
|
||||||
|
func (cs *CandlestickSeries) GetBoundedValues(index int) (x, y0, y1 float64) {
|
||||||
|
value := cs.GetCandleValues()[index]
|
||||||
|
return util.Time.ToFloat64(value.Timestamp), value.Low, value.High
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCandleValues returns the candle values.
|
||||||
|
func (cs CandlestickSeries) GetCandleValues() []CandleValue {
|
||||||
|
if cs.CandleValues == nil {
|
||||||
|
cs.CandleValues = cs.GenerateCandleValues()
|
||||||
|
}
|
||||||
|
return cs.CandleValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCandleValues returns the candlestick values for each day represented by the inner series.
|
||||||
|
func (cs CandlestickSeries) GenerateCandleValues() []CandleValue {
|
||||||
|
if cs.InnerSeries == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
totalValues := cs.InnerSeries.Len()
|
||||||
|
if totalValues == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var values []CandleValue
|
||||||
|
var lastYear, lastMonth, lastDay int
|
||||||
|
var year, month, day int
|
||||||
|
|
||||||
|
var t time.Time
|
||||||
|
var tv, lv, v float64
|
||||||
|
|
||||||
|
tv, v = cs.InnerSeries.GetValues(0)
|
||||||
|
t = util.Time.FromFloat64(tv)
|
||||||
|
year, month, day = t.Year(), int(t.Month()), t.Day()
|
||||||
|
|
||||||
|
lastYear, lastMonth, lastDay = year, month, day
|
||||||
|
|
||||||
|
value := CandleValue{
|
||||||
|
Timestamp: cs.newTimestamp(year, month, day),
|
||||||
|
Open: v,
|
||||||
|
Low: v,
|
||||||
|
High: v,
|
||||||
|
}
|
||||||
|
lv = v
|
||||||
|
|
||||||
|
for i := 1; i < totalValues; i++ {
|
||||||
|
tv, v = cs.InnerSeries.GetValues(i)
|
||||||
|
t = util.Time.FromFloat64(tv)
|
||||||
|
year, month, day = t.Year(), int(t.Month()), t.Day()
|
||||||
|
|
||||||
|
// if we've transitioned to a new day or we're on the last value
|
||||||
|
if lastYear != year || lastMonth != month || lastDay != day || i == (totalValues-1) {
|
||||||
|
value.Close = lv
|
||||||
|
values = append(values, value)
|
||||||
|
|
||||||
|
value = CandleValue{
|
||||||
|
Timestamp: cs.newTimestamp(year, month, day),
|
||||||
|
Open: v,
|
||||||
|
High: v,
|
||||||
|
Low: v,
|
||||||
|
}
|
||||||
|
|
||||||
|
lastYear = year
|
||||||
|
lastMonth = month
|
||||||
|
lastDay = day
|
||||||
|
} else {
|
||||||
|
value.Low = math.Min(value.Low, v)
|
||||||
|
value.High = math.Max(value.High, v)
|
||||||
|
}
|
||||||
|
lv = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs CandlestickSeries) newTimestamp(year, month, day int) time.Time {
|
||||||
|
return time.Date(year, time.Month(month), day, 12, 0, 0, 0, util.Date.Eastern())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render implements Series.Render.
|
||||||
|
func (cs CandlestickSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
|
||||||
|
style := cs.Style.InheritFrom(defaults)
|
||||||
|
Draw.CandlestickSeries(r, canvasBox, xrange, yrange, style, cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the series.
|
||||||
|
func (cs CandlestickSeries) Validate() error {
|
||||||
|
if cs.CandleValues == nil && cs.InnerSeries == nil {
|
||||||
|
return fmt.Errorf("candlestick series requires either `CandleValues` or `InnerSeries` to be set")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
52
candlestick_series_test.go
Normal file
52
candlestick_series_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
"github.com/wcharczuk/go-chart/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateDummyStockData() (times []time.Time, prices []float64) {
|
||||||
|
start := util.Date.On(util.NYSEOpen(), time.Date(2017, 05, 15, 0, 0, 0, 0, util.Date.Eastern()))
|
||||||
|
cursor := start
|
||||||
|
for day := 0; day < 60; day++ {
|
||||||
|
|
||||||
|
if util.Date.IsWeekendDay(cursor.Weekday()) {
|
||||||
|
cursor = start.AddDate(0, 0, day)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for hour := 0; hour < 7; hour++ {
|
||||||
|
for minute := 0; minute < 60; minute++ {
|
||||||
|
times = append(times, cursor)
|
||||||
|
prices = append(prices, rand.Float64()*256)
|
||||||
|
cursor = cursor.Add(time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = cursor.Add(time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = start.AddDate(0, 0, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCandlestickSeriesCandleValues(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
xdata, ydata := generateDummyStockData()
|
||||||
|
|
||||||
|
candleSeries := &CandlestickSeries{
|
||||||
|
InnerSeries: TimeSeries{
|
||||||
|
XValues: xdata,
|
||||||
|
YValues: ydata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
values := candleSeries.GetCandleValues()
|
||||||
|
assert.Len(values, 43) // should be 60 days per the generator.
|
||||||
|
}
|
4
chart.go
4
chart.go
|
@ -502,8 +502,8 @@ func (c Chart) drawTitle(r Renderer) {
|
||||||
textWidth := textBox.Width()
|
textWidth := textBox.Width()
|
||||||
textHeight := textBox.Height()
|
textHeight := textBox.Height()
|
||||||
|
|
||||||
titleX := (c.GetWidth() >> 1) - (textWidth >> 1)
|
titleX := (int(c.GetWidth()) >> 1) - (int(textWidth) >> 1)
|
||||||
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(textHeight)
|
||||||
|
|
||||||
r.Text(c.Title, titleX, titleY)
|
r.Text(c.Title, titleX, titleY)
|
||||||
}
|
}
|
||||||
|
|
BIN
debug.test
Executable file
BIN
debug.test
Executable file
Binary file not shown.
93
draw.go
93
draw.go
|
@ -168,14 +168,73 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d draw) CandlestickSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, cs CandlestickSeries) {
|
||||||
|
if cs.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
candleValues := cs.GetCandleValues()
|
||||||
|
|
||||||
|
cb := canvasBox.Bottom
|
||||||
|
cl := canvasBox.Left
|
||||||
|
|
||||||
|
var cv CandleValue
|
||||||
|
for index := 0; index < len(candleValues); index++ {
|
||||||
|
cv = candleValues[index]
|
||||||
|
|
||||||
|
y0 := yrange.Translate(cv.Open)
|
||||||
|
y1 := yrange.Translate(cv.Close)
|
||||||
|
|
||||||
|
x0 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEOpen(), cv.Timestamp)))
|
||||||
|
x1 := cl + xrange.Translate(util.Time.ToFloat64(util.Date.On(util.NYSEClose(), cv.Timestamp)))
|
||||||
|
|
||||||
|
x := x0 + ((x1 - x0) >> 1)
|
||||||
|
|
||||||
|
// draw open / close box.
|
||||||
|
if cv.Open < cv.Close {
|
||||||
|
d.Box(r, Box{
|
||||||
|
Top: cb - y0,
|
||||||
|
Left: x0,
|
||||||
|
Right: x1,
|
||||||
|
Bottom: cb - y1,
|
||||||
|
}, style.InheritFrom(Style{FillColor: ColorAlternateGreen}))
|
||||||
|
} else {
|
||||||
|
d.Box(r, Box{
|
||||||
|
Top: cb - y1,
|
||||||
|
Left: x0,
|
||||||
|
Right: x1,
|
||||||
|
Bottom: cb - y0,
|
||||||
|
}, style.InheritFrom(Style{FillColor: ColorRed}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw high / low t bars
|
||||||
|
y0 = yrange.Translate(cv.High)
|
||||||
|
y1 = yrange.Translate(cv.Low)
|
||||||
|
|
||||||
|
style.InheritFrom(Style{StrokeColor: DefaultStrokeColor}).WriteToRenderer(r)
|
||||||
|
|
||||||
|
r.MoveTo(x0, cb-y0)
|
||||||
|
r.LineTo(x1, cb-y0)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(x, cb-y0)
|
||||||
|
r.LineTo(x, cb-y1)
|
||||||
|
r.Stroke()
|
||||||
|
|
||||||
|
r.MoveTo(x0, cb-y1)
|
||||||
|
r.LineTo(x1, cb-y1)
|
||||||
|
r.Stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MeasureAnnotation measures how big an annotation would be.
|
// MeasureAnnotation measures how big an annotation would be.
|
||||||
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
|
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
|
||||||
style.WriteToRenderer(r)
|
style.WriteToRenderer(r)
|
||||||
defer r.ResetStyle()
|
defer r.ResetStyle()
|
||||||
|
|
||||||
textBox := r.MeasureText(label)
|
textBox := r.MeasureText(label)
|
||||||
textWidth := textBox.Width()
|
textWidth := int(textBox.Width())
|
||||||
textHeight := textBox.Height()
|
textHeight := int(textBox.Height())
|
||||||
halfTextHeight := textHeight >> 1
|
halfTextHeight := textHeight >> 1
|
||||||
|
|
||||||
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
|
||||||
|
@ -203,8 +262,8 @@ func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, lab
|
||||||
defer r.ResetStyle()
|
defer r.ResetStyle()
|
||||||
|
|
||||||
textBox := r.MeasureText(label)
|
textBox := r.MeasureText(label)
|
||||||
textWidth := textBox.Width()
|
textWidth := int(textBox.Width())
|
||||||
halfTextHeight := textBox.Height() >> 1
|
halfTextHeight := int(textBox.Height()) >> 1
|
||||||
|
|
||||||
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
style.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
|
@ -255,17 +314,17 @@ func (d draw) Box(r Renderer, b Box, s Style) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
|
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
|
||||||
d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
|
d.Box2d(r, b.Corners().Rotate(thetaDegrees), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) {
|
func (d draw) Box2d(r Renderer, bc Box2d, s Style) {
|
||||||
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
defer r.ResetStyle()
|
defer r.ResetStyle()
|
||||||
|
|
||||||
r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y)
|
r.MoveTo(int(bc.TopLeft.X), int(bc.TopLeft.Y))
|
||||||
r.LineTo(bc.TopRight.X, bc.TopRight.Y)
|
r.LineTo(int(bc.TopRight.X), int(bc.TopRight.Y))
|
||||||
r.LineTo(bc.BottomRight.X, bc.BottomRight.Y)
|
r.LineTo(int(bc.BottomRight.X), int(bc.BottomRight.Y))
|
||||||
r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y)
|
r.LineTo(int(bc.BottomLeft.X), int(bc.BottomLeft.Y))
|
||||||
r.Close()
|
r.Close()
|
||||||
r.FillStroke()
|
r.FillStroke()
|
||||||
}
|
}
|
||||||
|
@ -278,7 +337,7 @@ func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
||||||
r.Text(text, x, y)
|
r.Text(text, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d draw) MeasureText(r Renderer, text string, style Style) Box {
|
func (d draw) MeasureText(r Renderer, text string, style Style) Box2d {
|
||||||
style.GetTextOptions().WriteToRenderer(r)
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
defer r.ResetStyle()
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
@ -297,9 +356,9 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
||||||
|
|
||||||
switch style.GetTextVerticalAlign() {
|
switch style.GetTextVerticalAlign() {
|
||||||
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
|
case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
|
||||||
y = y - linesBox.Height()
|
y = y - int(linesBox.Height())
|
||||||
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
|
case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
|
||||||
y = (y - linesBox.Height()) >> 1
|
y = (y - int(linesBox.Height())) >> 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var tx, ty int
|
var tx, ty int
|
||||||
|
@ -307,19 +366,19 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
||||||
lineBox := r.MeasureText(line)
|
lineBox := r.MeasureText(line)
|
||||||
switch style.GetTextHorizontalAlign() {
|
switch style.GetTextHorizontalAlign() {
|
||||||
case TextHorizontalAlignCenter:
|
case TextHorizontalAlignCenter:
|
||||||
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
|
tx = box.Left + ((int(box.Width()) - int(lineBox.Width())) >> 1)
|
||||||
case TextHorizontalAlignRight:
|
case TextHorizontalAlignRight:
|
||||||
tx = box.Right - lineBox.Width()
|
tx = box.Right - int(lineBox.Width())
|
||||||
default:
|
default:
|
||||||
tx = box.Left
|
tx = box.Left
|
||||||
}
|
}
|
||||||
if style.TextRotationDegrees == 0 {
|
if style.TextRotationDegrees == 0 {
|
||||||
ty = y + lineBox.Height()
|
ty = y + int(lineBox.Height())
|
||||||
} else {
|
} else {
|
||||||
ty = y
|
ty = y
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Text(line, tx, ty)
|
r.Text(line, tx, ty)
|
||||||
y += lineBox.Height() + style.GetTextLineSpacing()
|
y += int(lineBox.Height()) + style.GetTextLineSpacing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
legend.go
34
legend.go
|
@ -67,8 +67,8 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
||||||
if labelCount > 0 {
|
if labelCount > 0 {
|
||||||
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||||
}
|
}
|
||||||
legendContent.Bottom += tb.Height()
|
legendContent.Bottom += int(tb.Height())
|
||||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
|
||||||
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
||||||
labelCount++
|
labelCount++
|
||||||
}
|
}
|
||||||
|
@ -95,12 +95,12 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
|
||||||
tb := r.MeasureText(label)
|
tb := r.MeasureText(label)
|
||||||
|
|
||||||
ty := ycursor + tb.Height()
|
ty := ycursor + int(tb.Height())
|
||||||
r.Text(label, tx, ty)
|
r.Text(label, tx, ty)
|
||||||
|
|
||||||
th2 := tb.Height() >> 1
|
th2 := int(tb.Height()) >> 1
|
||||||
|
|
||||||
lx := tx + tb.Width() + lineTextGap
|
lx := tx + int(tb.Width()) + lineTextGap
|
||||||
ly := ty - th2
|
ly := ty - th2
|
||||||
lx2 := legendContent.Right - legendPadding.Right
|
lx2 := legendContent.Right - legendPadding.Right
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
||||||
r.LineTo(lx2, ly)
|
r.LineTo(lx2, ly)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
ycursor += tb.Height()
|
ycursor += int(tb.Height())
|
||||||
legendCount++
|
legendCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -160,12 +160,12 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
|
||||||
var textHeight int
|
var textHeight int
|
||||||
var textWidth int
|
var textWidth int
|
||||||
var textBox Box
|
var textBox Box2d
|
||||||
for x := 0; x < len(labels); x++ {
|
for x := 0; x < len(labels); x++ {
|
||||||
if len(labels[x]) > 0 {
|
if len(labels[x]) > 0 {
|
||||||
textBox = r.MeasureText(labels[x])
|
textBox = r.MeasureText(labels[x])
|
||||||
textHeight = util.Math.MaxInt(textBox.Height(), textHeight)
|
textHeight = util.Math.MaxInt(int(textBox.Height()), textHeight)
|
||||||
textWidth = util.Math.MaxInt(textBox.Width(), textWidth)
|
textWidth = util.Math.MaxInt(int(textBox.Width()), textWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +200,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
||||||
textBox = r.MeasureText(label)
|
textBox = r.MeasureText(label)
|
||||||
r.Text(label, tx, ty)
|
r.Text(label, tx, ty)
|
||||||
|
|
||||||
lx = tx + textBox.Width() + lineTextGap
|
lx = tx + int(textBox.Width()) + lineTextGap
|
||||||
ly = ty - th2
|
ly = ty - th2
|
||||||
|
|
||||||
r.SetStrokeColor(lines[index].GetStrokeColor())
|
r.SetStrokeColor(lines[index].GetStrokeColor())
|
||||||
|
@ -211,7 +211,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
||||||
r.LineTo(lx+lineLengthMinimum, ly)
|
r.LineTo(lx+lineLengthMinimum, ly)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
tx += textBox.Width() + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
|
tx += int(textBox.Width()) + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,8 +279,8 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
||||||
if labelCount > 0 {
|
if labelCount > 0 {
|
||||||
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||||
}
|
}
|
||||||
legendContent.Bottom += tb.Height()
|
legendContent.Bottom += int(tb.Height())
|
||||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
|
||||||
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
||||||
labelCount++
|
labelCount++
|
||||||
}
|
}
|
||||||
|
@ -307,12 +307,12 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
|
||||||
tb := r.MeasureText(label)
|
tb := r.MeasureText(label)
|
||||||
|
|
||||||
ty := ycursor + tb.Height()
|
ty := ycursor + int(tb.Height())
|
||||||
r.Text(label, tx, ty)
|
r.Text(label, tx, ty)
|
||||||
|
|
||||||
th2 := tb.Height() >> 1
|
th2 := int(tb.Height()) >> 1
|
||||||
|
|
||||||
lx := tx + tb.Width() + lineTextGap
|
lx := tx + int(tb.Width()) + lineTextGap
|
||||||
ly := ty - th2
|
ly := ty - th2
|
||||||
lx2 := legendContent.Right - legendPadding.Right
|
lx2 := legendContent.Right - legendPadding.Right
|
||||||
|
|
||||||
|
@ -324,7 +324,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
||||||
r.LineTo(lx2, ly)
|
r.LineTo(lx2, ly)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
ycursor += tb.Height()
|
ycursor += int(tb.Height())
|
||||||
legendCount++
|
legendCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ func (mhr *MarketHoursRange) SetDomain(domain int) {
|
||||||
// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider.
|
// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider.
|
||||||
func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider {
|
func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider {
|
||||||
if mhr.HolidayProvider == nil {
|
if mhr.HolidayProvider == nil {
|
||||||
return func(_ time.Time) bool { return false }
|
return util.Date.IsNYSEHoliday
|
||||||
}
|
}
|
||||||
return mhr.HolidayProvider
|
return mhr.HolidayProvider
|
||||||
}
|
}
|
||||||
|
@ -115,38 +115,37 @@ func (mhr MarketHoursRange) GetMarketClose() time.Time {
|
||||||
// GetTicks returns the ticks for the range.
|
// GetTicks returns the ticks for the range.
|
||||||
// This is to override the default continous ticks that would be generated for the range.
|
// This is to override the default continous ticks that would be generated for the range.
|
||||||
func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick {
|
func (mhr *MarketHoursRange) GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick {
|
||||||
times := seq.Time.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
times := seq.TimeUtil.MarketHours(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||||
timesWidth := mhr.measureTimes(r, defaults, vf, times)
|
timesWidth := mhr.measureTimes(r, defaults, vf, times)
|
||||||
if timesWidth <= mhr.Domain {
|
if timesWidth <= mhr.Domain {
|
||||||
return mhr.makeTicks(vf, times)
|
return mhr.makeTicks(vf, times)
|
||||||
}
|
}
|
||||||
|
|
||||||
times = seq.Time.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
times = seq.TimeUtil.MarketHourQuarters(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||||
if timesWidth <= mhr.Domain {
|
if timesWidth <= mhr.Domain {
|
||||||
return mhr.makeTicks(vf, times)
|
return mhr.makeTicks(vf, times)
|
||||||
}
|
}
|
||||||
|
|
||||||
times = seq.Time.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
times = seq.TimeUtil.MarketDayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||||
if timesWidth <= mhr.Domain {
|
if timesWidth <= mhr.Domain {
|
||||||
return mhr.makeTicks(vf, times)
|
return mhr.makeTicks(vf, times)
|
||||||
}
|
}
|
||||||
|
|
||||||
times = seq.Time.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
times = seq.TimeUtil.MarketDayAlternateCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||||
if timesWidth <= mhr.Domain {
|
if timesWidth <= mhr.Domain {
|
||||||
return mhr.makeTicks(vf, times)
|
return mhr.makeTicks(vf, times)
|
||||||
}
|
}
|
||||||
|
|
||||||
times = seq.Time.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
times = seq.TimeUtil.MarketDayMondayCloses(mhr.Min, mhr.Max, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||||
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
timesWidth = mhr.measureTimes(r, defaults, vf, times)
|
||||||
if timesWidth <= mhr.Domain {
|
if timesWidth <= mhr.Domain {
|
||||||
return mhr.makeTicks(vf, times)
|
return mhr.makeTicks(vf, times)
|
||||||
}
|
}
|
||||||
|
|
||||||
return GenerateContinuousTicks(r, mhr, false, defaults, vf)
|
return GenerateContinuousTicks(r, mhr, false, defaults, vf)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int {
|
func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFormatter, times []time.Time) int {
|
||||||
|
@ -156,7 +155,7 @@ func (mhr *MarketHoursRange) measureTimes(r Renderer, defaults Style, vf ValueFo
|
||||||
timeLabel := vf(t)
|
timeLabel := vf(t)
|
||||||
|
|
||||||
labelBox := r.MeasureText(timeLabel)
|
labelBox := r.MeasureText(timeLabel)
|
||||||
total += labelBox.Width()
|
total += int(labelBox.Width())
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
total += DefaultMinimumTickHorizontalSpacing
|
total += DefaultMinimumTickHorizontalSpacing
|
||||||
}
|
}
|
||||||
|
@ -183,8 +182,8 @@ func (mhr MarketHoursRange) String() string {
|
||||||
func (mhr MarketHoursRange) Translate(value float64) int {
|
func (mhr MarketHoursRange) Translate(value float64) int {
|
||||||
valueTime := util.Time.FromFloat64(value)
|
valueTime := util.Time.FromFloat64(value)
|
||||||
valueTimeEastern := valueTime.In(util.Date.Eastern())
|
valueTimeEastern := valueTime.In(util.Date.Eastern())
|
||||||
totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
|
totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||||
valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
|
valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.GetHolidayProvider())
|
||||||
translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain))
|
translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain))
|
||||||
|
|
||||||
if mhr.IsDescending() {
|
if mhr.IsDescending() {
|
||||||
|
|
|
@ -162,8 +162,8 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
|
||||||
lx, ly = util.Math.CirclePoint(cx, cy, labelRadius, delta2)
|
lx, ly = util.Math.CirclePoint(cx, cy, labelRadius, delta2)
|
||||||
|
|
||||||
tb := r.MeasureText(v.Label)
|
tb := r.MeasureText(v.Label)
|
||||||
lx = lx - (tb.Width() >> 1)
|
lx = lx - (int(tb.Width()) >> 1)
|
||||||
ly = ly + (tb.Height() >> 1)
|
ly = ly + (int(tb.Height()) >> 1)
|
||||||
|
|
||||||
r.Text(v.Label, lx, ly)
|
r.Text(v.Label, lx, ly)
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,13 +155,13 @@ func (rr *rasterRenderer) Text(body string, x, y int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MeasureText returns the height and width in pixels of a string.
|
// MeasureText returns the height and width in pixels of a string.
|
||||||
func (rr *rasterRenderer) MeasureText(body string) Box {
|
func (rr *rasterRenderer) MeasureText(body string) Box2d {
|
||||||
rr.gc.SetFont(rr.s.Font)
|
rr.gc.SetFont(rr.s.Font)
|
||||||
rr.gc.SetFontSize(rr.s.FontSize)
|
rr.gc.SetFontSize(rr.s.FontSize)
|
||||||
rr.gc.SetFillColor(rr.s.FontColor)
|
rr.gc.SetFillColor(rr.s.FontColor)
|
||||||
l, t, r, b, err := rr.gc.GetStringBounds(body)
|
l, t, r, b, err := rr.gc.GetStringBounds(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Box{}
|
return Box2d{}
|
||||||
}
|
}
|
||||||
if l < 0 {
|
if l < 0 {
|
||||||
r = r - l // equivalent to r+(-1*l)
|
r = r - l // equivalent to r+(-1*l)
|
||||||
|
@ -189,10 +189,10 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
|
||||||
Bottom: int(math.Ceil(b)),
|
Bottom: int(math.Ceil(b)),
|
||||||
}
|
}
|
||||||
if rr.rotateRadians == nil {
|
if rr.rotateRadians == nil {
|
||||||
return textBox
|
return textBox.Corners()
|
||||||
}
|
}
|
||||||
|
|
||||||
return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians)).Box()
|
return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTextRotation sets a text rotation.
|
// SetTextRotation sets a text rotation.
|
||||||
|
|
|
@ -73,7 +73,7 @@ type Renderer interface {
|
||||||
Text(body string, x, y int)
|
Text(body string, x, y int)
|
||||||
|
|
||||||
// MeasureText measures text.
|
// MeasureText measures text.
|
||||||
MeasureText(body string) Box
|
MeasureText(body string) Box2d
|
||||||
|
|
||||||
// SetTextRotatation sets a rotation for drawing elements.
|
// SetTextRotatation sets a rotation for drawing elements.
|
||||||
SetTextRotation(radians float64)
|
SetTextRotation(radians float64)
|
||||||
|
|
15
seq/array.go
15
seq/array.go
|
@ -1,5 +1,7 @@
|
||||||
package seq
|
package seq
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
// NewArray creates a new array.
|
// NewArray creates a new array.
|
||||||
func NewArray(values ...float64) Array {
|
func NewArray(values ...float64) Array {
|
||||||
return Array(values)
|
return Array(values)
|
||||||
|
@ -17,3 +19,16 @@ func (a Array) Len() int {
|
||||||
func (a Array) GetValue(index int) float64 {
|
func (a Array) GetValue(index int) float64 {
|
||||||
return a[index]
|
return a[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArrayOfTimes wraps an array of times as a sequence provider.
|
||||||
|
type ArrayOfTimes []time.Time
|
||||||
|
|
||||||
|
// Len returns the length of the array.
|
||||||
|
func (aot ArrayOfTimes) Len() int {
|
||||||
|
return len(aot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns the time at the given index as a time.Time.
|
||||||
|
func (aot ArrayOfTimes) GetValue(index int) time.Time {
|
||||||
|
return aot[index]
|
||||||
|
}
|
||||||
|
|
15
seq/provider.go
Normal file
15
seq/provider.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package seq
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Provider is a provider for values for a seq.
|
||||||
|
type Provider interface {
|
||||||
|
Len() int
|
||||||
|
GetValue(int) float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeProvider is a provider for values for a seq.
|
||||||
|
type TimeProvider interface {
|
||||||
|
Len() int
|
||||||
|
GetValue(int) time.Time
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ func RandomValues(count int) []float64 {
|
||||||
return Seq{NewRandom().WithLen(count)}.Array()
|
return Seq{NewRandom().WithLen(count)}.Array()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RandomValuesWithAverage returns an array of random values with a given average.
|
// RandomValuesWithMax returns an array of random values with a given average.
|
||||||
func RandomValuesWithMax(count int, max float64) []float64 {
|
func RandomValuesWithMax(count int, max float64) []float64 {
|
||||||
return Seq{NewRandom().WithMax(max).WithLen(count)}.Array()
|
return Seq{NewRandom().WithMax(max).WithLen(count)}.Array()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,6 @@ func Values(values ...float64) Seq {
|
||||||
return Seq{Provider: Array(values)}
|
return Seq{Provider: Array(values)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider is a provider for values for a seq.
|
|
||||||
type Provider interface {
|
|
||||||
Len() int
|
|
||||||
GetValue(int) float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seq is a utility wrapper for seq providers.
|
// Seq is a utility wrapper for seq providers.
|
||||||
type Seq struct {
|
type Seq struct {
|
||||||
Provider
|
Provider
|
||||||
|
@ -28,12 +22,13 @@ type Seq struct {
|
||||||
|
|
||||||
// Array enumerates the seq into a slice.
|
// Array enumerates the seq into a slice.
|
||||||
func (s Seq) Array() (output []float64) {
|
func (s Seq) Array() (output []float64) {
|
||||||
if s.Len() == 0 {
|
slen := s.Len()
|
||||||
|
if slen == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output = make([]float64, s.Len())
|
output = make([]float64, slen)
|
||||||
for i := 0; i < s.Len(); i++ {
|
for i := 0; i < slen; i++ {
|
||||||
output[i] = s.GetValue(i)
|
output[i] = s.GetValue(i)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -142,6 +137,22 @@ func (s Seq) MinMax() (min, max float64) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// First returns the value at index 0.
|
||||||
|
func (s Seq) First() float64 {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.GetValue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last returns the value at index (len)-1.
|
||||||
|
func (s Seq) Last() float64 {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.GetValue(s.Len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
// Sort returns the seq sorted in ascending order.
|
// Sort returns the seq sorted in ascending order.
|
||||||
// This fully enumerates the seq.
|
// This fully enumerates the seq.
|
||||||
func (s Seq) Sort() Seq {
|
func (s Seq) Sort() Seq {
|
||||||
|
@ -149,7 +160,43 @@ func (s Seq) Sort() Seq {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
values := s.Array()
|
values := s.Array()
|
||||||
sort.Float64s(values)
|
sort.Slice(values, func(i, j int) bool {
|
||||||
|
return values[i] < values[j]
|
||||||
|
})
|
||||||
|
return Seq{Provider: Array(values)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortDescending returns the seq sorted in descending order.
|
||||||
|
// This fully enumerates the seq.
|
||||||
|
func (s Seq) SortDescending() Seq {
|
||||||
|
if s.Len() == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
values := s.Array()
|
||||||
|
sort.Slice(values, func(i, j int) bool {
|
||||||
|
return values[i] > values[j]
|
||||||
|
})
|
||||||
|
return Seq{Provider: Array(values)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse reverses the sequence's order.
|
||||||
|
func (s Seq) Reverse() Seq {
|
||||||
|
slen := s.Len()
|
||||||
|
if slen == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
slen2 := slen >> 1
|
||||||
|
values := s.Array()
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
j := slen - 1
|
||||||
|
for i < slen2 {
|
||||||
|
values[i], values[j] = values[j], values[i]
|
||||||
|
i++
|
||||||
|
j--
|
||||||
|
}
|
||||||
|
|
||||||
return Seq{Provider: Array(values)}
|
return Seq{Provider: Array(values)}
|
||||||
}
|
}
|
||||||
|
|
68
seq/time.go
68
seq/time.go
|
@ -6,21 +6,12 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/util"
|
"github.com/wcharczuk/go-chart/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Time is a utility singleton with helper functions for time seq generation.
|
// TimeUtil is a utility singleton with helper functions for time seq generation.
|
||||||
var Time timeSequence
|
var TimeUtil timeUtil
|
||||||
|
|
||||||
type timeSequence struct{}
|
type timeUtil struct{}
|
||||||
|
|
||||||
// Days generates a seq of timestamps by day, from -days to today.
|
func (tu timeUtil) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
||||||
func (ts timeSequence) 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ts timeSequence) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
cursor := util.Date.On(marketOpen, from)
|
cursor := util.Date.On(marketOpen, from)
|
||||||
toClose := util.Date.On(marketClose, to)
|
toClose := util.Date.On(marketClose, to)
|
||||||
|
@ -41,7 +32,7 @@ func (ts timeSequence) MarketHours(from, to time.Time, marketOpen, marketClose t
|
||||||
return times
|
return times
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts timeSequence) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
func (tu timeUtil) MarketHourQuarters(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
cursor := util.Date.On(marketOpen, from)
|
cursor := util.Date.On(marketOpen, from)
|
||||||
toClose := util.Date.On(marketClose, to)
|
toClose := util.Date.On(marketClose, to)
|
||||||
|
@ -62,15 +53,15 @@ func (ts timeSequence) MarketHourQuarters(from, to time.Time, marketOpen, market
|
||||||
return times
|
return times
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts timeSequence) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
func (tu timeUtil) MarketDayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
cursor := util.Date.On(marketOpen, from)
|
cursor := util.Date.On(marketOpen, from)
|
||||||
toClose := util.Date.On(marketClose, to)
|
toClose := util.Date.On(marketClose, to)
|
||||||
for cursor.Before(toClose) || cursor.Equal(toClose) {
|
for cursor.Before(toClose) || cursor.Equal(toClose) {
|
||||||
isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday())
|
isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday())
|
||||||
if isValidTradingDay {
|
if isValidTradingDay {
|
||||||
todayClose := util.Date.On(marketClose, cursor)
|
newValue := util.Date.NoonOn(cursor)
|
||||||
times = append(times, todayClose)
|
times = append(times, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor = util.Date.NextDay(cursor)
|
cursor = util.Date.NextDay(cursor)
|
||||||
|
@ -78,7 +69,7 @@ func (ts timeSequence) MarketDayCloses(from, to time.Time, marketOpen, marketClo
|
||||||
return times
|
return times
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts timeSequence) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
func (tu timeUtil) MarketDayAlternateCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
cursor := util.Date.On(marketOpen, from)
|
cursor := util.Date.On(marketOpen, from)
|
||||||
toClose := util.Date.On(marketClose, to)
|
toClose := util.Date.On(marketClose, to)
|
||||||
|
@ -94,7 +85,7 @@ func (ts timeSequence) MarketDayAlternateCloses(from, to time.Time, marketOpen,
|
||||||
return times
|
return times
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts timeSequence) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
func (tu timeUtil) MarketDayMondayCloses(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
||||||
var times []time.Time
|
var times []time.Time
|
||||||
cursor := util.Date.On(marketClose, from)
|
cursor := util.Date.On(marketClose, from)
|
||||||
toClose := util.Date.On(marketClose, to)
|
toClose := util.Date.On(marketClose, to)
|
||||||
|
@ -109,7 +100,7 @@ func (ts timeSequence) MarketDayMondayCloses(from, to time.Time, marketOpen, mar
|
||||||
return times
|
return times
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts timeSequence) Hours(start time.Time, totalHours int) []time.Time {
|
func (tu timeUtil) Hours(start time.Time, totalHours int) []time.Time {
|
||||||
times := make([]time.Time, totalHours)
|
times := make([]time.Time, totalHours)
|
||||||
|
|
||||||
last := start
|
last := start
|
||||||
|
@ -122,13 +113,12 @@ func (ts timeSequence) Hours(start time.Time, totalHours int) []time.Time {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HoursFilled adds zero values for the data bounded by the start and end of the xdata array.
|
// HoursFilled adds zero values for the data bounded by the start and end of the xdata array.
|
||||||
func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) {
|
func (tu timeUtil) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) {
|
||||||
start := Time.Start(xdata)
|
start, end := Times(xdata...).MinAndMax()
|
||||||
end := Time.End(xdata)
|
|
||||||
|
|
||||||
totalHours := util.Math.AbsInt(util.Date.DiffHours(start, end))
|
totalHours := util.Math.AbsInt(util.Date.DiffHours(start, end))
|
||||||
|
|
||||||
finalTimes := ts.Hours(start, totalHours+1)
|
finalTimes := tu.Hours(start, totalHours+1)
|
||||||
finalValues := make([]float64, totalHours+1)
|
finalValues := make([]float64, totalHours+1)
|
||||||
|
|
||||||
var hoursFromStart int
|
var hoursFromStart int
|
||||||
|
@ -139,33 +129,3 @@ func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.T
|
||||||
|
|
||||||
return finalTimes, finalValues
|
return finalTimes, finalValues
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start returns the earliest (min) time in a list of times.
|
|
||||||
func (ts timeSequence) Start(times []time.Time) time.Time {
|
|
||||||
if len(times) == 0 {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
start := times[0]
|
|
||||||
for _, t := range times[1:] {
|
|
||||||
if t.Before(start) {
|
|
||||||
start = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return start
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start returns the earliest (min) time in a list of times.
|
|
||||||
func (ts timeSequence) End(times []time.Time) time.Time {
|
|
||||||
if len(times) == 0 {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
end := times[0]
|
|
||||||
for _, t := range times[1:] {
|
|
||||||
if t.After(end) {
|
|
||||||
end = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return end
|
|
||||||
}
|
|
||||||
|
|
261
seq/time_seq.go
Normal file
261
seq/time_seq.go
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
package seq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TimeZero is the zero time.
|
||||||
|
TimeZero = time.Time{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Times returns a new time sequence.
|
||||||
|
func Times(values ...time.Time) TimeSeq {
|
||||||
|
return TimeSeq{TimeProvider: ArrayOfTimes(values)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeSeq is a sequence of times.
|
||||||
|
type TimeSeq struct {
|
||||||
|
TimeProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array converts the sequence to times.
|
||||||
|
func (ts TimeSeq) Array() (output []time.Time) {
|
||||||
|
slen := ts.Len()
|
||||||
|
if slen == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output = make([]time.Time, slen)
|
||||||
|
for i := 0; i < slen; i++ {
|
||||||
|
output[i] = ts.GetValue(i)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each applies the `mapfn` to all values in the value provider.
|
||||||
|
func (ts TimeSeq) Each(mapfn func(int, time.Time)) {
|
||||||
|
for i := 0; i < ts.Len(); i++ {
|
||||||
|
mapfn(i, ts.GetValue(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map applies the `mapfn` to all values in the value provider,
|
||||||
|
// returning a new seq.
|
||||||
|
func (ts TimeSeq) Map(mapfn func(int, time.Time) time.Time) TimeSeq {
|
||||||
|
output := make([]time.Time, ts.Len())
|
||||||
|
for i := 0; i < ts.Len(); i++ {
|
||||||
|
mapfn(i, ts.GetValue(i))
|
||||||
|
}
|
||||||
|
return TimeSeq{ArrayOfTimes(output)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FoldLeft collapses a seq from left to right.
|
||||||
|
func (ts TimeSeq) FoldLeft(mapfn func(i int, v0, v time.Time) time.Time) (v0 time.Time) {
|
||||||
|
tslen := ts.Len()
|
||||||
|
if tslen == 0 {
|
||||||
|
return TimeZero
|
||||||
|
}
|
||||||
|
|
||||||
|
if tslen == 1 {
|
||||||
|
return ts.GetValue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
v0 = ts.GetValue(0)
|
||||||
|
for i := 1; i < tslen; i++ {
|
||||||
|
v0 = mapfn(i, v0, ts.GetValue(i))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FoldRight collapses a seq from right to left.
|
||||||
|
func (ts TimeSeq) FoldRight(mapfn func(i int, v0, v time.Time) time.Time) (v0 time.Time) {
|
||||||
|
tslen := ts.Len()
|
||||||
|
if tslen == 0 {
|
||||||
|
return TimeZero
|
||||||
|
}
|
||||||
|
|
||||||
|
if tslen == 1 {
|
||||||
|
return ts.GetValue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
v0 = ts.GetValue(tslen - 1)
|
||||||
|
for i := tslen - 2; i >= 0; i-- {
|
||||||
|
v0 = mapfn(i, v0, ts.GetValue(i))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort returns the seq in ascending order.
|
||||||
|
func (ts TimeSeq) Sort() TimeSeq {
|
||||||
|
if ts.Len() == 0 {
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
values := ts.Array()
|
||||||
|
sort.Slice(values, func(i, j int) bool {
|
||||||
|
return values[i].Before(values[j])
|
||||||
|
})
|
||||||
|
return TimeSeq{TimeProvider: ArrayOfTimes(values)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SortDescending returns the seq in descending order.
|
||||||
|
func (ts TimeSeq) SortDescending() TimeSeq {
|
||||||
|
if ts.Len() == 0 {
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
values := ts.Array()
|
||||||
|
sort.Slice(values, func(i, j int) bool {
|
||||||
|
return values[i].After(values[j])
|
||||||
|
})
|
||||||
|
return TimeSeq{TimeProvider: ArrayOfTimes(values)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min returns the minimum (or earliest) time in the sequence.
|
||||||
|
func (ts TimeSeq) Min() (min time.Time) {
|
||||||
|
tslen := ts.Len()
|
||||||
|
if tslen == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
min = ts.GetValue(0)
|
||||||
|
var tv time.Time
|
||||||
|
for i := 1; i < tslen; i++ {
|
||||||
|
tv = ts.GetValue(i)
|
||||||
|
if tv.Before(min) {
|
||||||
|
min = tv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start is an alias to `Min`.
|
||||||
|
func (ts TimeSeq) Start() time.Time {
|
||||||
|
return ts.Min()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns the maximum (or latest) time in the sequence.
|
||||||
|
func (ts TimeSeq) Max() (max time.Time) {
|
||||||
|
tslen := ts.Len()
|
||||||
|
if tslen == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
max = ts.GetValue(0)
|
||||||
|
var tv time.Time
|
||||||
|
for i := 1; i < tslen; i++ {
|
||||||
|
tv = ts.GetValue(i)
|
||||||
|
if tv.After(max) {
|
||||||
|
max = tv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// End is an alias to `Max`.
|
||||||
|
func (ts TimeSeq) End() time.Time {
|
||||||
|
return ts.Max()
|
||||||
|
}
|
||||||
|
|
||||||
|
// First returns the first value in the sequence.
|
||||||
|
func (ts TimeSeq) First() time.Time {
|
||||||
|
if ts.Len() == 0 {
|
||||||
|
return TimeZero
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.GetValue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last returns the last value in the sequence.
|
||||||
|
func (ts TimeSeq) Last() time.Time {
|
||||||
|
if ts.Len() == 0 {
|
||||||
|
return TimeZero
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.GetValue(ts.Len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinAndMax returns both the earliest and latest value from a sequence in one pass.
|
||||||
|
func (ts TimeSeq) MinAndMax() (min, max time.Time) {
|
||||||
|
tslen := ts.Len()
|
||||||
|
if tslen == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
min = ts.GetValue(0)
|
||||||
|
max = ts.GetValue(0)
|
||||||
|
var tv time.Time
|
||||||
|
for i := 1; i < tslen; i++ {
|
||||||
|
tv = ts.GetValue(i)
|
||||||
|
if tv.Before(min) {
|
||||||
|
min = tv
|
||||||
|
}
|
||||||
|
if tv.After(max) {
|
||||||
|
max = tv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapDistinct maps values given a map function to their distinct outputs.
|
||||||
|
func (ts TimeSeq) MapDistinct(mapFn func(time.Time) time.Time) TimeSeq {
|
||||||
|
tslen := ts.Len()
|
||||||
|
if tslen == 0 {
|
||||||
|
return TimeSeq{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var output []time.Time
|
||||||
|
hourLookup := SetOfTime{}
|
||||||
|
|
||||||
|
// add the initial value
|
||||||
|
tv := ts.GetValue(0)
|
||||||
|
tvh := mapFn(tv)
|
||||||
|
hourLookup.Add(tvh)
|
||||||
|
output = append(output, tvh)
|
||||||
|
|
||||||
|
for i := 1; i < tslen; i++ {
|
||||||
|
tv = ts.GetValue(i)
|
||||||
|
tvh = mapFn(tv)
|
||||||
|
if !hourLookup.Has(tvh) {
|
||||||
|
hourLookup.Add(tvh)
|
||||||
|
output = append(output, tvh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeSeq{ArrayOfTimes(output)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hours returns times in each distinct hour represented by the sequence.
|
||||||
|
func (ts TimeSeq) Hours() TimeSeq {
|
||||||
|
return ts.MapDistinct(ts.trimToHour)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days returns times in each distinct day represented by the sequence.
|
||||||
|
func (ts TimeSeq) Days() TimeSeq {
|
||||||
|
return ts.MapDistinct(ts.trimToDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Months returns times in each distinct months represented by the sequence.
|
||||||
|
func (ts TimeSeq) Months() TimeSeq {
|
||||||
|
return ts.MapDistinct(ts.trimToMonth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Years returns times in each distinc year represented by the sequence.
|
||||||
|
func (ts TimeSeq) Years() TimeSeq {
|
||||||
|
return ts.MapDistinct(ts.trimToYear)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts TimeSeq) trimToHour(tv time.Time) time.Time {
|
||||||
|
return time.Date(tv.Year(), tv.Month(), tv.Day(), tv.Hour(), 0, 0, 0, tv.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts TimeSeq) trimToDay(tv time.Time) time.Time {
|
||||||
|
return time.Date(tv.Year(), tv.Month(), tv.Day(), 0, 0, 0, 0, tv.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts TimeSeq) trimToMonth(tv time.Time) time.Time {
|
||||||
|
return time.Date(tv.Year(), tv.Month(), 1, 0, 0, 0, 0, tv.Location())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts TimeSeq) trimToYear(tv time.Time) time.Time {
|
||||||
|
return time.Date(tv.Year(), 1, 1, 0, 0, 0, 0, tv.Location())
|
||||||
|
}
|
81
seq/time_seq_test.go
Normal file
81
seq/time_seq_test.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package seq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeSeqTimes(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
seq := Times(time.Now(), time.Now(), time.Now())
|
||||||
|
assert.Equal(3, seq.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTime(str string) time.Time {
|
||||||
|
tv, _ := time.Parse("2006-01-02 15:04:05", str)
|
||||||
|
return tv
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeSeqSort(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
seq := Times(
|
||||||
|
parseTime("2016-05-14 12:00:00"),
|
||||||
|
parseTime("2017-05-14 12:00:00"),
|
||||||
|
parseTime("2015-05-14 12:00:00"),
|
||||||
|
parseTime("2017-05-13 12:00:00"),
|
||||||
|
)
|
||||||
|
|
||||||
|
sorted := seq.Sort()
|
||||||
|
assert.Equal(4, sorted.Len())
|
||||||
|
min, max := sorted.MinAndMax()
|
||||||
|
assert.Equal(parseTime("2015-05-14 12:00:00"), min)
|
||||||
|
assert.Equal(parseTime("2017-05-14 12:00:00"), max)
|
||||||
|
|
||||||
|
first, last := sorted.First(), sorted.Last()
|
||||||
|
assert.Equal(min, first)
|
||||||
|
assert.Equal(max, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeSeqSortDescending(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
seq := Times(
|
||||||
|
parseTime("2016-05-14 12:00:00"),
|
||||||
|
parseTime("2017-05-14 12:00:00"),
|
||||||
|
parseTime("2015-05-14 12:00:00"),
|
||||||
|
parseTime("2017-05-13 12:00:00"),
|
||||||
|
)
|
||||||
|
|
||||||
|
sorted := seq.SortDescending()
|
||||||
|
assert.Equal(4, sorted.Len())
|
||||||
|
min, max := sorted.MinAndMax()
|
||||||
|
assert.Equal(parseTime("2015-05-14 12:00:00"), min)
|
||||||
|
assert.Equal(parseTime("2017-05-14 12:00:00"), max)
|
||||||
|
|
||||||
|
first, last := sorted.First(), sorted.Last()
|
||||||
|
assert.Equal(max, first)
|
||||||
|
assert.Equal(min, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeSeqDays(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
seq := Times(
|
||||||
|
parseTime("2017-05-10 12:00:00"),
|
||||||
|
parseTime("2017-05-10 16:00:00"),
|
||||||
|
parseTime("2017-05-11 12:00:00"),
|
||||||
|
parseTime("2015-05-12 12:00:00"),
|
||||||
|
parseTime("2015-05-12 16:00:00"),
|
||||||
|
parseTime("2017-05-13 12:00:00"),
|
||||||
|
parseTime("2017-05-14 12:00:00"),
|
||||||
|
)
|
||||||
|
|
||||||
|
days := seq.Days()
|
||||||
|
assert.Equal(5, days.Len())
|
||||||
|
assert.Equal(10, days.First().Day())
|
||||||
|
assert.Equal(14, days.Last().Day())
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ func TestTimeMarketHours(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern())
|
today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern())
|
||||||
mh := Time.MarketHours(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday)
|
mh := TimeUtil.MarketHours(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday)
|
||||||
assert.Len(mh, 8)
|
assert.Len(mh, 8)
|
||||||
assert.Equal(util.Date.Eastern(), mh[0].Location())
|
assert.Equal(util.Date.Eastern(), mh[0].Location())
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ func TestTimeMarketHours(t *testing.T) {
|
||||||
func TestTimeMarketHourQuarters(t *testing.T) {
|
func TestTimeMarketHourQuarters(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern())
|
today := time.Date(2016, 07, 01, 12, 0, 0, 0, util.Date.Eastern())
|
||||||
mh := Time.MarketHourQuarters(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday)
|
mh := TimeUtil.MarketHourQuarters(today, today, util.NYSEOpen(), util.NYSEClose(), util.Date.IsNYSEHoliday)
|
||||||
assert.Len(mh, 4)
|
assert.Len(mh, 4)
|
||||||
assert.Equal(9, mh[0].Hour())
|
assert.Equal(9, mh[0].Hour())
|
||||||
assert.Equal(30, mh[0].Minute())
|
assert.Equal(30, mh[0].Minute())
|
||||||
|
@ -39,9 +39,9 @@ func TestTimeHours(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
today := time.Date(2016, 07, 01, 12, 0, 0, 0, time.UTC)
|
today := time.Date(2016, 07, 01, 12, 0, 0, 0, time.UTC)
|
||||||
seq := Time.Hours(today, 24)
|
seq := TimeUtil.Hours(today, 24)
|
||||||
|
|
||||||
end := Time.End(seq)
|
end := Times(seq...).Max()
|
||||||
assert.Len(seq, 24)
|
assert.Len(seq, 24)
|
||||||
assert.Equal(2016, end.Year())
|
assert.Equal(2016, end.Year())
|
||||||
assert.Equal(07, int(end.Month()))
|
assert.Equal(07, int(end.Month()))
|
||||||
|
@ -72,8 +72,8 @@ func TestSequenceHoursFill(t *testing.T) {
|
||||||
0.6,
|
0.6,
|
||||||
}
|
}
|
||||||
|
|
||||||
filledTimes, filledValues := Time.HoursFilled(xdata, ydata)
|
filledTimes, filledValues := TimeUtil.HoursFilled(xdata, ydata)
|
||||||
assert.Len(filledTimes, util.Date.DiffHours(Time.Start(xdata), Time.End(xdata))+1)
|
assert.Len(filledTimes, util.Date.DiffHours(Times(xdata...).Start(), Times(xdata...).End())+1)
|
||||||
assert.Equal(len(filledValues), len(filledTimes))
|
assert.Equal(len(filledValues), len(filledTimes))
|
||||||
|
|
||||||
assert.NotZero(filledValues[0])
|
assert.NotZero(filledValues[0])
|
||||||
|
@ -93,7 +93,7 @@ func TestTimeStart(t *testing.T) {
|
||||||
time.Now().AddDate(0, 0, -5),
|
time.Now().AddDate(0, 0, -5),
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.InTimeDelta(Time.Start(times), times[4], time.Millisecond)
|
assert.InTimeDelta(Times(times...).Start(), times[4], time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeEnd(t *testing.T) {
|
func TestTimeEnd(t *testing.T) {
|
||||||
|
@ -107,5 +107,5 @@ func TestTimeEnd(t *testing.T) {
|
||||||
time.Now().AddDate(0, 0, -5),
|
time.Now().AddDate(0, 0, -5),
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.InTimeDelta(Time.End(times), times[2], time.Millisecond)
|
assert.InTimeDelta(Times(times...).End(), times[2], time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
26
seq/util.go
26
seq/util.go
|
@ -1,6 +1,11 @@
|
||||||
package seq
|
package seq
|
||||||
|
|
||||||
import "math"
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/util"
|
||||||
|
)
|
||||||
|
|
||||||
func round(input float64, places int) (rounded float64) {
|
func round(input float64, places int) (rounded float64) {
|
||||||
if math.IsNaN(input) {
|
if math.IsNaN(input) {
|
||||||
|
@ -30,3 +35,22 @@ func f64i(value float64) int {
|
||||||
r := round(value, 0)
|
r := round(value, 0)
|
||||||
return int(r)
|
return int(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetOfTime is a simple hash set for timestamps as float64s.
|
||||||
|
type SetOfTime map[float64]bool
|
||||||
|
|
||||||
|
// Add adds the value to the hash set.
|
||||||
|
func (sot SetOfTime) Add(tv time.Time) {
|
||||||
|
sot[util.Time.ToFloat64(tv)] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has returns if the set contains a given time.
|
||||||
|
func (sot SetOfTime) Has(tv time.Time) bool {
|
||||||
|
_, hasValue := sot[util.Time.ToFloat64(tv)]
|
||||||
|
return hasValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the value from the set.
|
||||||
|
func (sot SetOfTime) Remove(tv time.Time) {
|
||||||
|
delete(sot, util.Time.ToFloat64(tv))
|
||||||
|
}
|
||||||
|
|
|
@ -214,7 +214,7 @@ func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) {
|
||||||
text := fmt.Sprintf("%0.0f%%", t*100)
|
text := fmt.Sprintf("%0.0f%%", t*100)
|
||||||
|
|
||||||
tb := r.MeasureText(text)
|
tb := r.MeasureText(text)
|
||||||
Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
|
Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(int(tb.Height())>>1), axisStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -254,7 +254,7 @@ func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box {
|
||||||
lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle)
|
lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle)
|
||||||
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
linesBox := Text.MeasureLines(r, lines, axisStyle)
|
||||||
|
|
||||||
xaxisHeight = util.Math.MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
|
xaxisHeight = util.Math.MaxInt(int(linesBox.Height())+(2*DefaultXAxisMargin), xaxisHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Box{
|
return Box{
|
||||||
|
|
16
text.go
16
text.go
|
@ -85,7 +85,7 @@ func (t text) WrapFitWord(r Renderer, value string, width int, style Style) []st
|
||||||
var line string
|
var line string
|
||||||
var word string
|
var word string
|
||||||
|
|
||||||
var textBox Box
|
var textBox Box2d
|
||||||
|
|
||||||
for _, c := range value {
|
for _, c := range value {
|
||||||
if c == rune('\n') { // commit the line to output
|
if c == rune('\n') { // commit the line to output
|
||||||
|
@ -97,7 +97,7 @@ func (t text) WrapFitWord(r Renderer, value string, width int, style Style) []st
|
||||||
|
|
||||||
textBox = r.MeasureText(line + word + string(c))
|
textBox = r.MeasureText(line + word + string(c))
|
||||||
|
|
||||||
if textBox.Width() >= width {
|
if int(textBox.Width()) >= width {
|
||||||
output = append(output, t.Trim(line))
|
output = append(output, t.Trim(line))
|
||||||
line = word
|
line = word
|
||||||
word = string(c)
|
word = string(c)
|
||||||
|
@ -120,7 +120,7 @@ func (t text) WrapFitRune(r Renderer, value string, width int, style Style) []st
|
||||||
|
|
||||||
var output []string
|
var output []string
|
||||||
var line string
|
var line string
|
||||||
var textBox Box
|
var textBox Box2d
|
||||||
for _, c := range value {
|
for _, c := range value {
|
||||||
if c == rune('\n') {
|
if c == rune('\n') {
|
||||||
output = append(output, line)
|
output = append(output, line)
|
||||||
|
@ -130,7 +130,7 @@ func (t text) WrapFitRune(r Renderer, value string, width int, style Style) []st
|
||||||
|
|
||||||
textBox = r.MeasureText(line + string(c))
|
textBox = r.MeasureText(line + string(c))
|
||||||
|
|
||||||
if textBox.Width() >= width {
|
if int(textBox.Width()) >= width {
|
||||||
output = append(output, line)
|
output = append(output, line)
|
||||||
line = string(c)
|
line = string(c)
|
||||||
continue
|
continue
|
||||||
|
@ -144,18 +144,18 @@ func (t text) Trim(value string) string {
|
||||||
return strings.Trim(value, " \t\n\r")
|
return strings.Trim(value, " \t\n\r")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t text) MeasureLines(r Renderer, lines []string, style Style) Box {
|
func (t text) MeasureLines(r Renderer, lines []string, style Style) Box2d {
|
||||||
style.WriteTextOptionsToRenderer(r)
|
style.WriteTextOptionsToRenderer(r)
|
||||||
var output Box
|
var output Box
|
||||||
for index, line := range lines {
|
for index, line := range lines {
|
||||||
lineBox := r.MeasureText(line)
|
lineBox := r.MeasureText(line)
|
||||||
output.Right = util.Math.MaxInt(lineBox.Right, output.Right)
|
output.Right = util.Math.MaxInt(int(lineBox.Right()), output.Right)
|
||||||
output.Bottom += lineBox.Height()
|
output.Bottom += int(lineBox.Height())
|
||||||
if index < len(lines)-1 {
|
if index < len(lines)-1 {
|
||||||
output.Bottom += +style.GetTextLineSpacing()
|
output.Bottom += +style.GetTextLineSpacing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return output
|
return output.Corners()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t text) appendLast(lines []string, text string) []string {
|
func (t text) appendLast(lines []string, text string) []string {
|
||||||
|
|
14
util/math.go
14
util/math.go
|
@ -251,3 +251,17 @@ func (m mathUtil) RotateCoordinate(cx, cy, x, y int, thetaRadians float64) (rx,
|
||||||
ry = int(rotatedY) + cy
|
ry = int(rotatedY) + cy
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m mathUtil) LinesIntersect(l0x0, l0y0, l0x1, l0y1, l1x0, l1y0, l1x1, l1y1 float64) bool {
|
||||||
|
var s0x, s0y, s1x, s1y float64
|
||||||
|
s0x = l0x1 - l0x0
|
||||||
|
s0y = l0y1 - l0y0
|
||||||
|
s1x = l1x1 - l1x0
|
||||||
|
s1y = l1y1 - l1y0
|
||||||
|
|
||||||
|
var s, t float64
|
||||||
|
s = (-s0y*(l0x0-l1x0) + s0x*(l0y0-l1y0)) / (-s1x*s0y + s0x*s1y)
|
||||||
|
t = (s1x*(l0y0-l1y0) - s1y*(l0x0-l1x0)) / (-s1x*s0y + s0x*s1y)
|
||||||
|
|
||||||
|
return s >= 0 && s <= 1 && t >= 0 && t <= 1
|
||||||
|
}
|
||||||
|
|
|
@ -182,3 +182,27 @@ func TestRotateCoordinate45(t *testing.T) {
|
||||||
assert.Equal(7, rx)
|
assert.Equal(7, rx)
|
||||||
assert.Equal(7, ry)
|
assert.Equal(7, ry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLinesIntersect(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
p0x := 1.0
|
||||||
|
p0y := 1.0
|
||||||
|
|
||||||
|
p1x := 3.0
|
||||||
|
p1y := 1.0
|
||||||
|
|
||||||
|
p2x := 2.0
|
||||||
|
p2y := 2.0
|
||||||
|
|
||||||
|
p3x := 2.0
|
||||||
|
p3y := 0.0
|
||||||
|
|
||||||
|
p4x := 2.0
|
||||||
|
p4y := 2.0
|
||||||
|
p5x := 3.0
|
||||||
|
p5y := 2.0
|
||||||
|
|
||||||
|
assert.True(Math.LinesIntersect(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y))
|
||||||
|
assert.False(Math.LinesIntersect(p0x, p0y, p1x, p1y, p4x, p4y, p5x, p5y))
|
||||||
|
}
|
||||||
|
|
16
util/time_test.go
Normal file
16
util/time_test.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
assert "github.com/blendlabs/go-assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeFromFloat64(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
assert.InTimeDelta(now, Time.FromFloat64(Time.ToFloat64(now)), time.Microsecond)
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import "github.com/wcharczuk/go-chart/drawing"
|
||||||
// ValuesProvider is a type that produces values.
|
// ValuesProvider is a type that produces values.
|
||||||
type ValuesProvider interface {
|
type ValuesProvider interface {
|
||||||
Len() int
|
Len() int
|
||||||
GetValues(index int) (float64, float64)
|
GetValues(index int) (x float64, y float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoundedValuesProvider allows series to return a range.
|
// BoundedValuesProvider allows series to return a range.
|
||||||
|
|
|
@ -7,11 +7,10 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/image/font"
|
|
||||||
|
|
||||||
util "github.com/blendlabs/go-util"
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/wcharczuk/go-chart/drawing"
|
"github.com/wcharczuk/go-chart/drawing"
|
||||||
|
"github.com/wcharczuk/go-chart/util"
|
||||||
|
"golang.org/x/image/font"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SVG returns a new png/raster renderer.
|
// SVG returns a new png/raster renderer.
|
||||||
|
@ -162,7 +161,8 @@ func (vr *vectorRenderer) Text(body string, x, y int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MeasureText uses the truetype font drawer to measure the width of text.
|
// MeasureText uses the truetype font drawer to measure the width of text.
|
||||||
func (vr *vectorRenderer) MeasureText(body string) (box Box) {
|
func (vr *vectorRenderer) MeasureText(body string) Box2d {
|
||||||
|
var box Box
|
||||||
if vr.s.GetFont() != nil {
|
if vr.s.GetFont() != nil {
|
||||||
vr.fc = &font.Drawer{
|
vr.fc = &font.Drawer{
|
||||||
Face: truetype.NewFace(vr.s.GetFont(), &truetype.Options{
|
Face: truetype.NewFace(vr.s.GetFont(), &truetype.Options{
|
||||||
|
@ -175,11 +175,11 @@ func (vr *vectorRenderer) MeasureText(body string) (box Box) {
|
||||||
box.Right = w
|
box.Right = w
|
||||||
box.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
|
box.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
|
||||||
if vr.c.textTheta == nil {
|
if vr.c.textTheta == nil {
|
||||||
return
|
return box.Corners()
|
||||||
}
|
}
|
||||||
box = box.Corners().Rotate(util.Math.RadiansToDegrees(*vr.c.textTheta)).Box()
|
return box.Corners().Rotate(util.Math.RadiansToDegrees(*vr.c.textTheta))
|
||||||
}
|
}
|
||||||
return
|
return box.Corners()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTextRotation sets the text rotation.
|
// SetTextRotation sets the text rotation.
|
||||||
|
|
22
xaxis.go
22
xaxis.go
|
@ -91,11 +91,11 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
tb := Draw.MeasureText(r, t.Label, tickStyle.GetTextOptions())
|
tb := Draw.MeasureText(r, t.Label, tickStyle.GetTextOptions())
|
||||||
|
|
||||||
tx = canvasBox.Left + ra.Translate(v)
|
tx = canvasBox.Left + ra.Translate(v)
|
||||||
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
ty = canvasBox.Bottom + DefaultXAxisMargin + int(tb.Height())
|
||||||
switch tp {
|
switch tp {
|
||||||
case TickPositionUnderTick, TickPositionUnset:
|
case TickPositionUnderTick, TickPositionUnset:
|
||||||
ltx = tx - tb.Width()>>1
|
ltx = tx - int(tb.Width())>>1
|
||||||
rtx = tx + tb.Width()>>1
|
rtx = tx + int(tb.Width())>>1
|
||||||
break
|
break
|
||||||
case TickPositionBetweenTicks:
|
case TickPositionBetweenTicks:
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
|
@ -112,7 +112,7 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
|
|
||||||
if xa.NameStyle.Show && len(xa.Name) > 0 {
|
if xa.NameStyle.Show && len(xa.Name) > 0 {
|
||||||
tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults))
|
tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults))
|
||||||
bottom += DefaultXAxisMargin + tb.Height()
|
bottom += DefaultXAxisMargin + int(tb.Height())
|
||||||
}
|
}
|
||||||
|
|
||||||
return Box{
|
return Box{
|
||||||
|
@ -153,13 +153,13 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
switch tp {
|
switch tp {
|
||||||
case TickPositionUnderTick, TickPositionUnset:
|
case TickPositionUnderTick, TickPositionUnset:
|
||||||
if tickStyle.TextRotationDegrees == 0 {
|
if tickStyle.TextRotationDegrees == 0 {
|
||||||
tx = tx - tb.Width()>>1
|
tx = tx - int(tb.Width())>>1
|
||||||
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
ty = canvasBox.Bottom + DefaultXAxisMargin + int(tb.Height())
|
||||||
} else {
|
} else {
|
||||||
ty = canvasBox.Bottom + (2 * DefaultXAxisMargin)
|
ty = canvasBox.Bottom + (1.5 * DefaultXAxisMargin)
|
||||||
}
|
}
|
||||||
Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle)
|
Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle)
|
||||||
maxTextHeight = util.Math.MaxInt(maxTextHeight, tb.Height())
|
maxTextHeight = util.Math.MaxInt(maxTextHeight, int(tb.Height()))
|
||||||
break
|
break
|
||||||
case TickPositionBetweenTicks:
|
case TickPositionBetweenTicks:
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
|
@ -175,7 +175,7 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
}, finalTickStyle)
|
}, finalTickStyle)
|
||||||
|
|
||||||
ftb := Text.MeasureLines(r, Text.WrapFit(r, t.Label, tx-ltx, finalTickStyle), finalTickStyle)
|
ftb := Text.MeasureLines(r, Text.WrapFit(r, t.Label, tx-ltx, finalTickStyle), finalTickStyle)
|
||||||
maxTextHeight = util.Math.MaxInt(maxTextHeight, ftb.Height())
|
maxTextHeight = util.Math.MaxInt(maxTextHeight, int(ftb.Height()))
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -184,8 +184,8 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
nameStyle := xa.NameStyle.InheritFrom(defaults)
|
nameStyle := xa.NameStyle.InheritFrom(defaults)
|
||||||
if xa.NameStyle.Show && len(xa.Name) > 0 {
|
if xa.NameStyle.Show && len(xa.Name) > 0 {
|
||||||
tb := Draw.MeasureText(r, xa.Name, nameStyle)
|
tb := Draw.MeasureText(r, xa.Name, nameStyle)
|
||||||
tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1)
|
tx := canvasBox.Right - (canvasBox.Width()>>1 + int(tb.Width())>>1)
|
||||||
ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + tb.Height()
|
ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + int(tb.Height())
|
||||||
Draw.Text(r, xa.Name, tx, ty, nameStyle)
|
Draw.Text(r, xa.Name, tx, ty, nameStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
yaxis.go
20
yaxis.go
|
@ -99,17 +99,17 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
ly := canvasBox.Bottom - ra.Translate(v)
|
ly := canvasBox.Bottom - ra.Translate(v)
|
||||||
|
|
||||||
tb := r.MeasureText(t.Label)
|
tb := r.MeasureText(t.Label)
|
||||||
tbh2 := tb.Height() >> 1
|
tbh2 := int(tb.Height()) >> 1
|
||||||
finalTextX := tx
|
finalTextX := tx
|
||||||
if ya.AxisType == YAxisSecondary {
|
if ya.AxisType == YAxisSecondary {
|
||||||
finalTextX = tx - tb.Width()
|
finalTextX = tx - int(tb.Width())
|
||||||
}
|
}
|
||||||
|
|
||||||
maxTextHeight = util.Math.MaxInt(tb.Height(), maxTextHeight)
|
maxTextHeight = util.Math.MaxInt(int(tb.Height()), maxTextHeight)
|
||||||
|
|
||||||
if ya.AxisType == YAxisPrimary {
|
if ya.AxisType == YAxisPrimary {
|
||||||
minx = canvasBox.Right
|
minx = canvasBox.Right
|
||||||
maxx = util.Math.MaxInt(maxx, tx+tb.Width())
|
maxx = util.Math.MaxInt(maxx, tx+int(tb.Width()))
|
||||||
} else if ya.AxisType == YAxisSecondary {
|
} else if ya.AxisType == YAxisSecondary {
|
||||||
minx = util.Math.MinInt(minx, finalTextX)
|
minx = util.Math.MinInt(minx, finalTextX)
|
||||||
maxx = util.Math.MaxInt(maxx, tx)
|
maxx = util.Math.MaxInt(maxx, tx)
|
||||||
|
@ -160,18 +160,18 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
|
|
||||||
tb := Draw.MeasureText(r, t.Label, tickStyle)
|
tb := Draw.MeasureText(r, t.Label, tickStyle)
|
||||||
|
|
||||||
if tb.Width() > maxTextWidth {
|
if int(tb.Width()) > maxTextWidth {
|
||||||
maxTextWidth = tb.Width()
|
maxTextWidth = int(tb.Width())
|
||||||
}
|
}
|
||||||
|
|
||||||
if ya.AxisType == YAxisSecondary {
|
if ya.AxisType == YAxisSecondary {
|
||||||
finalTextX = tx - tb.Width()
|
finalTextX = tx - int(tb.Width())
|
||||||
} else {
|
} else {
|
||||||
finalTextX = tx
|
finalTextX = tx
|
||||||
}
|
}
|
||||||
|
|
||||||
if tickStyle.TextRotationDegrees == 0 {
|
if tickStyle.TextRotationDegrees == 0 {
|
||||||
finalTextY = ly + tb.Height()>>1
|
finalTextY = ly + int(tb.Height())>>1
|
||||||
} else {
|
} else {
|
||||||
finalTextY = ly
|
finalTextY = ly
|
||||||
}
|
}
|
||||||
|
@ -203,9 +203,9 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
|
|
||||||
var ty int
|
var ty int
|
||||||
if nameStyle.TextRotationDegrees == 0 {
|
if nameStyle.TextRotationDegrees == 0 {
|
||||||
ty = canvasBox.Top + (canvasBox.Height()>>1 - tb.Width()>>1)
|
ty = canvasBox.Top + (canvasBox.Height()>>1 - int(tb.Width())>>1)
|
||||||
} else {
|
} else {
|
||||||
ty = canvasBox.Top + (canvasBox.Height()>>1 - tb.Height()>>1)
|
ty = canvasBox.Top + (canvasBox.Height()>>1 - int(tb.Height())>>1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Draw.Text(r, ya.Name, tx, ty, nameStyle)
|
Draw.Text(r, ya.Name, tx, ty, nameStyle)
|
||||||
|
|
Loading…
Reference in a new issue