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
|
||||
.DS_Store
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.6.2
|
||||
- 1.8.1
|
||||
|
||||
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)
|
||||
|
||||
chart.Draw.BoxCorners(r, tbc, chart.Style{
|
||||
chart.Draw.Box2d(r, tbc, chart.Style{
|
||||
StrokeColor: drawing.ColorRed,
|
||||
StrokeWidth: 2,
|
||||
})
|
||||
|
||||
tbc2 := tbc.Shift(tbc.Height(), 0)
|
||||
chart.Draw.Box2d(r, tbc2, chart.Style{
|
||||
StrokeColor: drawing.ColorGreen,
|
||||
StrokeWidth: 2,
|
||||
})
|
||||
|
||||
tbcb := tbc.Box()
|
||||
chart.Draw.Box(r, tbcb, chart.Style{
|
||||
StrokeColor: drawing.ColorBlue,
|
||||
|
|
|
@ -261,7 +261,7 @@ func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick)
|
|||
r.Stroke()
|
||||
|
||||
var ty int
|
||||
var tb Box
|
||||
var tb Box2d
|
||||
for _, t := range ticks {
|
||||
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)
|
||||
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)
|
||||
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 (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
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.
|
||||
func (b Box) Corners() BoxCorners {
|
||||
return BoxCorners{
|
||||
TopLeft: Point{b.Left, b.Top},
|
||||
TopRight: Point{b.Right, b.Top},
|
||||
BottomRight: Point{b.Right, b.Bottom},
|
||||
BottomLeft: Point{b.Left, b.Bottom},
|
||||
func (b Box) Corners() Box2d {
|
||||
return Box2d{
|
||||
TopLeft: Point{float64(b.Left), float64(b.Top)},
|
||||
TopRight: Point{float64(b.Right), float64(b.Top)},
|
||||
BottomRight: Point{float64(b.Right), float64(b.Bottom)},
|
||||
BottomLeft: Point{float64(b.Left), float64(b.Bottom)},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,99 +254,3 @@ func (b Box) OuterConstrain(bounds, other Box) Box {
|
|||
}
|
||||
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(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()
|
||||
textHeight := textBox.Height()
|
||||
|
||||
titleX := (c.GetWidth() >> 1) - (textWidth >> 1)
|
||||
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
|
||||
titleX := (int(c.GetWidth()) >> 1) - (int(textWidth) >> 1)
|
||||
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(textHeight)
|
||||
|
||||
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.
|
||||
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
|
||||
style.WriteToRenderer(r)
|
||||
defer r.ResetStyle()
|
||||
|
||||
textBox := r.MeasureText(label)
|
||||
textWidth := textBox.Width()
|
||||
textHeight := textBox.Height()
|
||||
textWidth := int(textBox.Width())
|
||||
textHeight := int(textBox.Height())
|
||||
halfTextHeight := textHeight >> 1
|
||||
|
||||
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()
|
||||
|
||||
textBox := r.MeasureText(label)
|
||||
textWidth := textBox.Width()
|
||||
halfTextHeight := textBox.Height() >> 1
|
||||
textWidth := int(textBox.Width())
|
||||
halfTextHeight := int(textBox.Height()) >> 1
|
||||
|
||||
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) {
|
||||
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)
|
||||
defer r.ResetStyle()
|
||||
|
||||
r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y)
|
||||
r.LineTo(bc.TopRight.X, bc.TopRight.Y)
|
||||
r.LineTo(bc.BottomRight.X, bc.BottomRight.Y)
|
||||
r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y)
|
||||
r.MoveTo(int(bc.TopLeft.X), int(bc.TopLeft.Y))
|
||||
r.LineTo(int(bc.TopRight.X), int(bc.TopRight.Y))
|
||||
r.LineTo(int(bc.BottomRight.X), int(bc.BottomRight.Y))
|
||||
r.LineTo(int(bc.BottomLeft.X), int(bc.BottomLeft.Y))
|
||||
r.Close()
|
||||
r.FillStroke()
|
||||
}
|
||||
|
@ -278,7 +337,7 @@ func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
|||
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)
|
||||
defer r.ResetStyle()
|
||||
|
||||
|
@ -297,9 +356,9 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
|||
|
||||
switch style.GetTextVerticalAlign() {
|
||||
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:
|
||||
y = (y - linesBox.Height()) >> 1
|
||||
y = (y - int(linesBox.Height())) >> 1
|
||||
}
|
||||
|
||||
var tx, ty int
|
||||
|
@ -307,19 +366,19 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
|||
lineBox := r.MeasureText(line)
|
||||
switch style.GetTextHorizontalAlign() {
|
||||
case TextHorizontalAlignCenter:
|
||||
tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
|
||||
tx = box.Left + ((int(box.Width()) - int(lineBox.Width())) >> 1)
|
||||
case TextHorizontalAlignRight:
|
||||
tx = box.Right - lineBox.Width()
|
||||
tx = box.Right - int(lineBox.Width())
|
||||
default:
|
||||
tx = box.Left
|
||||
}
|
||||
if style.TextRotationDegrees == 0 {
|
||||
ty = y + lineBox.Height()
|
||||
ty = y + int(lineBox.Height())
|
||||
} else {
|
||||
ty = y
|
||||
}
|
||||
|
||||
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 {
|
||||
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||
}
|
||||
legendContent.Bottom += tb.Height()
|
||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||
legendContent.Bottom += int(tb.Height())
|
||||
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
|
||||
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
||||
labelCount++
|
||||
}
|
||||
|
@ -95,12 +95,12 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
|||
|
||||
tb := r.MeasureText(label)
|
||||
|
||||
ty := ycursor + tb.Height()
|
||||
ty := ycursor + int(tb.Height())
|
||||
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
|
||||
lx2 := legendContent.Right - legendPadding.Right
|
||||
|
||||
|
@ -112,7 +112,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
|||
r.LineTo(lx2, ly)
|
||||
r.Stroke()
|
||||
|
||||
ycursor += tb.Height()
|
||||
ycursor += int(tb.Height())
|
||||
legendCount++
|
||||
}
|
||||
}
|
||||
|
@ -160,12 +160,12 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
|||
|
||||
var textHeight int
|
||||
var textWidth int
|
||||
var textBox Box
|
||||
var textBox Box2d
|
||||
for x := 0; x < len(labels); x++ {
|
||||
if len(labels[x]) > 0 {
|
||||
textBox = r.MeasureText(labels[x])
|
||||
textHeight = util.Math.MaxInt(textBox.Height(), textHeight)
|
||||
textWidth = util.Math.MaxInt(textBox.Width(), textWidth)
|
||||
textHeight = util.Math.MaxInt(int(textBox.Height()), textHeight)
|
||||
textWidth = util.Math.MaxInt(int(textBox.Width()), textWidth)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,7 +200,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
|||
textBox = r.MeasureText(label)
|
||||
r.Text(label, tx, ty)
|
||||
|
||||
lx = tx + textBox.Width() + lineTextGap
|
||||
lx = tx + int(textBox.Width()) + lineTextGap
|
||||
ly = ty - th2
|
||||
|
||||
r.SetStrokeColor(lines[index].GetStrokeColor())
|
||||
|
@ -211,7 +211,7 @@ func LegendThin(c *Chart, userDefaults ...Style) Renderable {
|
|||
r.LineTo(lx+lineLengthMinimum, ly)
|
||||
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 {
|
||||
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
|
||||
}
|
||||
legendContent.Bottom += tb.Height()
|
||||
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
|
||||
legendContent.Bottom += int(tb.Height())
|
||||
right := legendContent.Left + int(tb.Width()) + lineTextGap + lineLengthMinimum
|
||||
legendContent.Right = util.Math.MaxInt(legendContent.Right, right)
|
||||
labelCount++
|
||||
}
|
||||
|
@ -307,12 +307,12 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
|||
|
||||
tb := r.MeasureText(label)
|
||||
|
||||
ty := ycursor + tb.Height()
|
||||
ty := ycursor + int(tb.Height())
|
||||
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
|
||||
lx2 := legendContent.Right - legendPadding.Right
|
||||
|
||||
|
@ -324,7 +324,7 @@ func LegendLeft(c *Chart, userDefaults ...Style) Renderable {
|
|||
r.LineTo(lx2, ly)
|
||||
r.Stroke()
|
||||
|
||||
ycursor += tb.Height()
|
||||
ycursor += int(tb.Height())
|
||||
legendCount++
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ func (mhr *MarketHoursRange) SetDomain(domain int) {
|
|||
// GetHolidayProvider coalesces a userprovided holiday provider and the date.DefaultHolidayProvider.
|
||||
func (mhr MarketHoursRange) GetHolidayProvider() util.HolidayProvider {
|
||||
if mhr.HolidayProvider == nil {
|
||||
return func(_ time.Time) bool { return false }
|
||||
return util.Date.IsNYSEHoliday
|
||||
}
|
||||
return mhr.HolidayProvider
|
||||
}
|
||||
|
@ -115,38 +115,37 @@ func (mhr MarketHoursRange) GetMarketClose() time.Time {
|
|||
// GetTicks returns the ticks for the range.
|
||||
// This is to override the default continous ticks that would be generated for the range.
|
||||
func (mhr *MarketHoursRange) GetTicks(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)
|
||||
if timesWidth <= mhr.Domain {
|
||||
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)
|
||||
if timesWidth <= mhr.Domain {
|
||||
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)
|
||||
if timesWidth <= mhr.Domain {
|
||||
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)
|
||||
if timesWidth <= mhr.Domain {
|
||||
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)
|
||||
if timesWidth <= mhr.Domain {
|
||||
return mhr.makeTicks(vf, times)
|
||||
}
|
||||
|
||||
return GenerateContinuousTicks(r, mhr, false, defaults, vf)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
labelBox := r.MeasureText(timeLabel)
|
||||
total += labelBox.Width()
|
||||
total += int(labelBox.Width())
|
||||
if index > 0 {
|
||||
total += DefaultMinimumTickHorizontalSpacing
|
||||
}
|
||||
|
@ -183,8 +182,8 @@ func (mhr MarketHoursRange) String() string {
|
|||
func (mhr MarketHoursRange) Translate(value float64) int {
|
||||
valueTime := util.Time.FromFloat64(value)
|
||||
valueTimeEastern := valueTime.In(util.Date.Eastern())
|
||||
totalSeconds := util.Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
|
||||
valueDelta := util.Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, 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.GetHolidayProvider())
|
||||
translated := int((float64(valueDelta) / float64(totalSeconds)) * float64(mhr.Domain))
|
||||
|
||||
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)
|
||||
|
||||
tb := r.MeasureText(v.Label)
|
||||
lx = lx - (tb.Width() >> 1)
|
||||
ly = ly + (tb.Height() >> 1)
|
||||
lx = lx - (int(tb.Width()) >> 1)
|
||||
ly = ly + (int(tb.Height()) >> 1)
|
||||
|
||||
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.
|
||||
func (rr *rasterRenderer) MeasureText(body string) Box {
|
||||
func (rr *rasterRenderer) MeasureText(body string) Box2d {
|
||||
rr.gc.SetFont(rr.s.Font)
|
||||
rr.gc.SetFontSize(rr.s.FontSize)
|
||||
rr.gc.SetFillColor(rr.s.FontColor)
|
||||
l, t, r, b, err := rr.gc.GetStringBounds(body)
|
||||
if err != nil {
|
||||
return Box{}
|
||||
return Box2d{}
|
||||
}
|
||||
if l < 0 {
|
||||
r = r - l // equivalent to r+(-1*l)
|
||||
|
@ -189,10 +189,10 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
|
|||
Bottom: int(math.Ceil(b)),
|
||||
}
|
||||
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.
|
||||
|
|
|
@ -73,7 +73,7 @@ type Renderer interface {
|
|||
Text(body string, x, y int)
|
||||
|
||||
// MeasureText measures text.
|
||||
MeasureText(body string) Box
|
||||
MeasureText(body string) Box2d
|
||||
|
||||
// SetTextRotatation sets a rotation for drawing elements.
|
||||
SetTextRotation(radians float64)
|
||||
|
|
15
seq/array.go
15
seq/array.go
|
@ -1,5 +1,7 @@
|
|||
package seq
|
||||
|
||||
import "time"
|
||||
|
||||
// NewArray creates a new array.
|
||||
func NewArray(values ...float64) Array {
|
||||
return Array(values)
|
||||
|
@ -17,3 +19,16 @@ func (a Array) Len() int {
|
|||
func (a Array) GetValue(index int) float64 {
|
||||
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()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return Seq{NewRandom().WithMax(max).WithLen(count)}.Array()
|
||||
}
|
||||
|
|
|
@ -15,12 +15,6 @@ func Values(values ...float64) Seq {
|
|||
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.
|
||||
type Seq struct {
|
||||
Provider
|
||||
|
@ -28,12 +22,13 @@ type Seq struct {
|
|||
|
||||
// Array enumerates the seq into a slice.
|
||||
func (s Seq) Array() (output []float64) {
|
||||
if s.Len() == 0 {
|
||||
slen := s.Len()
|
||||
if slen == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
output = make([]float64, s.Len())
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
output = make([]float64, slen)
|
||||
for i := 0; i < slen; i++ {
|
||||
output[i] = s.GetValue(i)
|
||||
}
|
||||
return
|
||||
|
@ -142,6 +137,22 @@ func (s Seq) MinMax() (min, max float64) {
|
|||
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.
|
||||
// This fully enumerates the seq.
|
||||
func (s Seq) Sort() Seq {
|
||||
|
@ -149,7 +160,43 @@ func (s Seq) Sort() Seq {
|
|||
return s
|
||||
}
|
||||
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)}
|
||||
}
|
||||
|
68
seq/time.go
68
seq/time.go
|
@ -6,21 +6,12 @@ import (
|
|||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
// Time is a utility singleton with helper functions for time seq generation.
|
||||
var Time timeSequence
|
||||
// TimeUtil is a utility singleton with helper functions for time seq generation.
|
||||
var TimeUtil timeUtil
|
||||
|
||||
type timeSequence struct{}
|
||||
type timeUtil struct{}
|
||||
|
||||
// Days generates a seq of timestamps by day, from -days to today.
|
||||
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 {
|
||||
func (tu timeUtil) MarketHours(from, to time.Time, marketOpen, marketClose time.Time, isHoliday util.HolidayProvider) []time.Time {
|
||||
var times []time.Time
|
||||
cursor := util.Date.On(marketOpen, from)
|
||||
toClose := util.Date.On(marketClose, to)
|
||||
|
@ -41,7 +32,7 @@ func (ts timeSequence) MarketHours(from, to time.Time, marketOpen, marketClose t
|
|||
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
|
||||
cursor := util.Date.On(marketOpen, from)
|
||||
toClose := util.Date.On(marketClose, to)
|
||||
|
@ -62,15 +53,15 @@ func (ts timeSequence) MarketHourQuarters(from, to time.Time, marketOpen, market
|
|||
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
|
||||
cursor := util.Date.On(marketOpen, from)
|
||||
toClose := util.Date.On(marketClose, to)
|
||||
for cursor.Before(toClose) || cursor.Equal(toClose) {
|
||||
isValidTradingDay := !isHoliday(cursor) && util.Date.IsWeekDay(cursor.Weekday())
|
||||
if isValidTradingDay {
|
||||
todayClose := util.Date.On(marketClose, cursor)
|
||||
times = append(times, todayClose)
|
||||
newValue := util.Date.NoonOn(cursor)
|
||||
times = append(times, newValue)
|
||||
}
|
||||
|
||||
cursor = util.Date.NextDay(cursor)
|
||||
|
@ -78,7 +69,7 @@ func (ts timeSequence) MarketDayCloses(from, to time.Time, marketOpen, marketClo
|
|||
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
|
||||
cursor := util.Date.On(marketOpen, from)
|
||||
toClose := util.Date.On(marketClose, to)
|
||||
|
@ -94,7 +85,7 @@ func (ts timeSequence) MarketDayAlternateCloses(from, to time.Time, marketOpen,
|
|||
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
|
||||
cursor := util.Date.On(marketClose, from)
|
||||
toClose := util.Date.On(marketClose, to)
|
||||
|
@ -109,7 +100,7 @@ func (ts timeSequence) MarketDayMondayCloses(from, to time.Time, marketOpen, mar
|
|||
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)
|
||||
|
||||
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.
|
||||
func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) {
|
||||
start := Time.Start(xdata)
|
||||
end := Time.End(xdata)
|
||||
func (tu timeUtil) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) {
|
||||
start, end := Times(xdata...).MinAndMax()
|
||||
|
||||
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)
|
||||
|
||||
var hoursFromStart int
|
||||
|
@ -139,33 +129,3 @@ func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.T
|
|||
|
||||
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)
|
||||
|
||||
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.Equal(util.Date.Eastern(), mh[0].Location())
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ func TestTimeMarketHours(t *testing.T) {
|
|||
func TestTimeMarketHourQuarters(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
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.Equal(9, mh[0].Hour())
|
||||
assert.Equal(30, mh[0].Minute())
|
||||
|
@ -39,9 +39,9 @@ func TestTimeHours(t *testing.T) {
|
|||
assert := assert.New(t)
|
||||
|
||||
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.Equal(2016, end.Year())
|
||||
assert.Equal(07, int(end.Month()))
|
||||
|
@ -72,8 +72,8 @@ func TestSequenceHoursFill(t *testing.T) {
|
|||
0.6,
|
||||
}
|
||||
|
||||
filledTimes, filledValues := Time.HoursFilled(xdata, ydata)
|
||||
assert.Len(filledTimes, util.Date.DiffHours(Time.Start(xdata), Time.End(xdata))+1)
|
||||
filledTimes, filledValues := TimeUtil.HoursFilled(xdata, ydata)
|
||||
assert.Len(filledTimes, util.Date.DiffHours(Times(xdata...).Start(), Times(xdata...).End())+1)
|
||||
assert.Equal(len(filledValues), len(filledTimes))
|
||||
|
||||
assert.NotZero(filledValues[0])
|
||||
|
@ -93,7 +93,7 @@ func TestTimeStart(t *testing.T) {
|
|||
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) {
|
||||
|
@ -107,5 +107,5 @@ func TestTimeEnd(t *testing.T) {
|
|||
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
|
||||
|
||||
import "math"
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
)
|
||||
|
||||
func round(input float64, places int) (rounded float64) {
|
||||
if math.IsNaN(input) {
|
||||
|
@ -30,3 +35,22 @@ func f64i(value float64) int {
|
|||
r := round(value, 0)
|
||||
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)
|
||||
|
||||
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)
|
||||
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{
|
||||
|
|
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 word string
|
||||
|
||||
var textBox Box
|
||||
var textBox Box2d
|
||||
|
||||
for _, c := range value {
|
||||
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))
|
||||
|
||||
if textBox.Width() >= width {
|
||||
if int(textBox.Width()) >= width {
|
||||
output = append(output, t.Trim(line))
|
||||
line = word
|
||||
word = string(c)
|
||||
|
@ -120,7 +120,7 @@ func (t text) WrapFitRune(r Renderer, value string, width int, style Style) []st
|
|||
|
||||
var output []string
|
||||
var line string
|
||||
var textBox Box
|
||||
var textBox Box2d
|
||||
for _, c := range value {
|
||||
if c == rune('\n') {
|
||||
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))
|
||||
|
||||
if textBox.Width() >= width {
|
||||
if int(textBox.Width()) >= width {
|
||||
output = append(output, line)
|
||||
line = string(c)
|
||||
continue
|
||||
|
@ -144,18 +144,18 @@ func (t text) Trim(value string) string {
|
|||
return strings.Trim(value, " \t\n\r")
|
||||
}
|
||||
|
||||
func (t text) MeasureLines(r Renderer, lines []string, style Style) Box {
|
||||
func (t text) MeasureLines(r Renderer, lines []string, style Style) Box2d {
|
||||
style.WriteTextOptionsToRenderer(r)
|
||||
var output Box
|
||||
for index, line := range lines {
|
||||
lineBox := r.MeasureText(line)
|
||||
output.Right = util.Math.MaxInt(lineBox.Right, output.Right)
|
||||
output.Bottom += lineBox.Height()
|
||||
output.Right = util.Math.MaxInt(int(lineBox.Right()), output.Right)
|
||||
output.Bottom += int(lineBox.Height())
|
||||
if index < len(lines)-1 {
|
||||
output.Bottom += +style.GetTextLineSpacing()
|
||||
}
|
||||
}
|
||||
return output
|
||||
return output.Corners()
|
||||
}
|
||||
|
||||
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
|
||||
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, 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.
|
||||
type ValuesProvider interface {
|
||||
Len() int
|
||||
GetValues(index int) (float64, float64)
|
||||
GetValues(index int) (x float64, y float64)
|
||||
}
|
||||
|
||||
// BoundedValuesProvider allows series to return a range.
|
||||
|
|
|
@ -7,11 +7,10 @@ import (
|
|||
"math"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/image/font"
|
||||
|
||||
util "github.com/blendlabs/go-util"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/wcharczuk/go-chart/drawing"
|
||||
"github.com/wcharczuk/go-chart/util"
|
||||
"golang.org/x/image/font"
|
||||
)
|
||||
|
||||
// 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.
|
||||
func (vr *vectorRenderer) MeasureText(body string) (box Box) {
|
||||
func (vr *vectorRenderer) MeasureText(body string) Box2d {
|
||||
var box Box
|
||||
if vr.s.GetFont() != nil {
|
||||
vr.fc = &font.Drawer{
|
||||
Face: truetype.NewFace(vr.s.GetFont(), &truetype.Options{
|
||||
|
@ -175,11 +175,11 @@ func (vr *vectorRenderer) MeasureText(body string) (box Box) {
|
|||
box.Right = w
|
||||
box.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
|
||||
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.
|
||||
|
|
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())
|
||||
|
||||
tx = canvasBox.Left + ra.Translate(v)
|
||||
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
||||
ty = canvasBox.Bottom + DefaultXAxisMargin + int(tb.Height())
|
||||
switch tp {
|
||||
case TickPositionUnderTick, TickPositionUnset:
|
||||
ltx = tx - tb.Width()>>1
|
||||
rtx = tx + tb.Width()>>1
|
||||
ltx = tx - int(tb.Width())>>1
|
||||
rtx = tx + int(tb.Width())>>1
|
||||
break
|
||||
case TickPositionBetweenTicks:
|
||||
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 {
|
||||
tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults))
|
||||
bottom += DefaultXAxisMargin + tb.Height()
|
||||
bottom += DefaultXAxisMargin + int(tb.Height())
|
||||
}
|
||||
|
||||
return Box{
|
||||
|
@ -153,13 +153,13 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
|||
switch tp {
|
||||
case TickPositionUnderTick, TickPositionUnset:
|
||||
if tickStyle.TextRotationDegrees == 0 {
|
||||
tx = tx - tb.Width()>>1
|
||||
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
||||
tx = tx - int(tb.Width())>>1
|
||||
ty = canvasBox.Bottom + DefaultXAxisMargin + int(tb.Height())
|
||||
} else {
|
||||
ty = canvasBox.Bottom + (2 * DefaultXAxisMargin)
|
||||
ty = canvasBox.Bottom + (1.5 * DefaultXAxisMargin)
|
||||
}
|
||||
Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle)
|
||||
maxTextHeight = util.Math.MaxInt(maxTextHeight, tb.Height())
|
||||
maxTextHeight = util.Math.MaxInt(maxTextHeight, int(tb.Height()))
|
||||
break
|
||||
case TickPositionBetweenTicks:
|
||||
if index > 0 {
|
||||
|
@ -175,7 +175,7 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
|||
}, 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
|
||||
}
|
||||
|
@ -184,8 +184,8 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
|||
nameStyle := xa.NameStyle.InheritFrom(defaults)
|
||||
if xa.NameStyle.Show && len(xa.Name) > 0 {
|
||||
tb := Draw.MeasureText(r, xa.Name, nameStyle)
|
||||
tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1)
|
||||
ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + tb.Height()
|
||||
tx := canvasBox.Right - (canvasBox.Width()>>1 + int(tb.Width())>>1)
|
||||
ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + int(tb.Height())
|
||||
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)
|
||||
|
||||
tb := r.MeasureText(t.Label)
|
||||
tbh2 := tb.Height() >> 1
|
||||
tbh2 := int(tb.Height()) >> 1
|
||||
finalTextX := tx
|
||||
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 {
|
||||
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 {
|
||||
minx = util.Math.MinInt(minx, finalTextX)
|
||||
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)
|
||||
|
||||
if tb.Width() > maxTextWidth {
|
||||
maxTextWidth = tb.Width()
|
||||
if int(tb.Width()) > maxTextWidth {
|
||||
maxTextWidth = int(tb.Width())
|
||||
}
|
||||
|
||||
if ya.AxisType == YAxisSecondary {
|
||||
finalTextX = tx - tb.Width()
|
||||
finalTextX = tx - int(tb.Width())
|
||||
} else {
|
||||
finalTextX = tx
|
||||
}
|
||||
|
||||
if tickStyle.TextRotationDegrees == 0 {
|
||||
finalTextY = ly + tb.Height()>>1
|
||||
finalTextY = ly + int(tb.Height())>>1
|
||||
} else {
|
||||
finalTextY = ly
|
||||
}
|
||||
|
@ -203,9 +203,9 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
|||
|
||||
var ty int
|
||||
if nameStyle.TextRotationDegrees == 0 {
|
||||
ty = canvasBox.Top + (canvasBox.Height()>>1 - tb.Width()>>1)
|
||||
ty = canvasBox.Top + (canvasBox.Height()>>1 - int(tb.Width())>>1)
|
||||
} 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)
|
||||
|
|
Loading…
Reference in a new issue