Merge pull request #13 from wcharczuk/text-rotation
Adds `TextRotationDegrees`
This commit is contained in:
commit
27a5efdd2d
23 changed files with 546 additions and 114 deletions
18
.vscode/launch.json
vendored
Normal file
18
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "test",
|
||||||
|
"remotePath": "",
|
||||||
|
"port": 2345,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"program": "${workspaceRoot}",
|
||||||
|
"env": {},
|
||||||
|
"args": [],
|
||||||
|
"showLog": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -2,23 +2,33 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
util "github.com/blendlabs/go-util"
|
|
||||||
"github.com/wcharczuk/go-chart"
|
"github.com/wcharczuk/go-chart"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func parseInt(str string) int {
|
||||||
|
v, _ := strconv.Atoi(str)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloat64(str string) float64 {
|
||||||
|
v, _ := strconv.ParseFloat(str, 64)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func readData() ([]time.Time, []float64) {
|
func readData() ([]time.Time, []float64) {
|
||||||
var xvalues []time.Time
|
var xvalues []time.Time
|
||||||
var yvalues []float64
|
var yvalues []float64
|
||||||
util.ReadFileByLines("requests.csv", func(line string) {
|
chart.File.ReadByLines("requests.csv", func(line string) {
|
||||||
parts := strings.Split(line, ",")
|
parts := strings.Split(line, ",")
|
||||||
year := util.ParseInt(parts[0])
|
year := parseInt(parts[0])
|
||||||
month := util.ParseInt(parts[1])
|
month := parseInt(parts[1])
|
||||||
day := util.ParseInt(parts[2])
|
day := parseInt(parts[2])
|
||||||
hour := util.ParseInt(parts[3])
|
hour := parseInt(parts[3])
|
||||||
elapsedMillis := util.ParseFloat64(parts[4])
|
elapsedMillis := parseFloat64(parts[4])
|
||||||
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
|
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
|
||||||
yvalues = append(yvalues, elapsedMillis)
|
yvalues = append(yvalues, elapsedMillis)
|
||||||
})
|
})
|
||||||
|
@ -27,12 +37,12 @@ func readData() ([]time.Time, []float64) {
|
||||||
|
|
||||||
func releases() []chart.GridLine {
|
func releases() []chart.GridLine {
|
||||||
return []chart.GridLine{
|
return []chart.GridLine{
|
||||||
{Value: chart.TimeToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))},
|
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 1, 9, 30, 0, 0, time.UTC))},
|
||||||
{Value: chart.TimeToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))},
|
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))},
|
||||||
{Value: chart.TimeToFloat64(time.Date(2016, 8, 3, 9, 30, 0, 0, time.UTC))},
|
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 3, 9, 30, 0, 0, time.UTC))},
|
||||||
{Value: chart.TimeToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))},
|
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))},
|
||||||
{Value: chart.TimeToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))},
|
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 5, 9, 30, 0, 0, time.UTC))},
|
||||||
{Value: chart.TimeToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))},
|
{Value: chart.Time.ToFloat64(time.Date(2016, 8, 6, 9, 30, 0, 0, time.UTC))},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,9 +91,14 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
Name: "Elapsed Millis",
|
Name: "Elapsed Millis",
|
||||||
NameStyle: chart.StyleShow(),
|
NameStyle: chart.StyleShow(),
|
||||||
Style: chart.StyleShow(),
|
Style: chart.StyleShow(),
|
||||||
|
TickStyle: chart.Style{
|
||||||
|
TextRotationDegrees: 45.0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
XAxis: chart.XAxis{
|
XAxis: chart.XAxis{
|
||||||
Style: chart.StyleShow(),
|
Style: chart.Style{
|
||||||
|
Show: true,
|
||||||
|
},
|
||||||
ValueFormatter: chart.TimeHourValueFormatter,
|
ValueFormatter: chart.TimeHourValueFormatter,
|
||||||
GridMajorStyle: chart.Style{
|
GridMajorStyle: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
|
|
|
@ -60,8 +60,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Header().Set("Content-Type", "image/svg+xml")
|
res.Header().Set("Content-Type", "image/png")
|
||||||
graph.Render(chart.SVG, res)
|
graph.Render(chart.PNG, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func xvalues() []time.Time {
|
func xvalues() []time.Time {
|
||||||
|
|
53
_examples/text_rotation/main.go
Normal file
53
_examples/text_rotation/main.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart"
|
||||||
|
"github.com/wcharczuk/go-chart/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func drawChart(res http.ResponseWriter, req *http.Request) {
|
||||||
|
f, _ := chart.GetDefaultFont()
|
||||||
|
r, _ := chart.PNG(1024, 1024)
|
||||||
|
|
||||||
|
chart.Draw.Text(r, "Test", 64, 64, chart.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 18,
|
||||||
|
Font: f,
|
||||||
|
})
|
||||||
|
|
||||||
|
chart.Draw.Text(r, "Test", 64, 64, chart.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 18,
|
||||||
|
Font: f,
|
||||||
|
TextRotationDegrees: 45.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
tb := chart.Draw.MeasureText(r, "Test", chart.Style{
|
||||||
|
FontColor: drawing.ColorBlack,
|
||||||
|
FontSize: 18,
|
||||||
|
Font: f,
|
||||||
|
}).Shift(64, 64)
|
||||||
|
|
||||||
|
tbc := tb.Corners().Rotate(45)
|
||||||
|
|
||||||
|
chart.Draw.BoxCorners(r, tbc, chart.Style{
|
||||||
|
StrokeColor: drawing.ColorRed,
|
||||||
|
StrokeWidth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
tbcb := tbc.Box()
|
||||||
|
chart.Draw.Box(r, tbcb, chart.Style{
|
||||||
|
StrokeColor: drawing.ColorBlue,
|
||||||
|
StrokeWidth: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
res.Header().Set("Content-Type", "image/png")
|
||||||
|
r.Save(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", drawChart)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
115
box.go
115
box.go
|
@ -1,6 +1,9 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
// Box represents the main 4 dimensions of a box.
|
// Box represents the main 4 dimensions of a box.
|
||||||
type Box struct {
|
type Box struct {
|
||||||
|
@ -76,8 +79,8 @@ func (b Box) Height() int {
|
||||||
|
|
||||||
// Center returns the center of the box
|
// Center returns the center of the box
|
||||||
func (b Box) Center() (x, y int) {
|
func (b Box) Center() (x, y int) {
|
||||||
w, h := b.Width(), b.Height()
|
w2, h2 := b.Width()>>1, b.Height()>>1
|
||||||
return b.Left + w>>1, b.Top + h>>1
|
return b.Left + w2, b.Top + h2
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aspect returns the aspect ratio of the box.
|
// Aspect returns the aspect ratio of the box.
|
||||||
|
@ -139,6 +142,16 @@ 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},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fit is functionally the inverse of grow.
|
// Fit is functionally the inverse of grow.
|
||||||
// Fit maintains the original aspect ratio of the `other` box,
|
// Fit maintains the original aspect ratio of the `other` box,
|
||||||
// but constrains it to the bounds of the target box.
|
// but constrains it to the bounds of the target box.
|
||||||
|
@ -219,3 +232,99 @@ 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: Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
|
||||||
|
Left: Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
|
||||||
|
Right: Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
|
||||||
|
Bottom: Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the width
|
||||||
|
func (bc BoxCorners) Width() int {
|
||||||
|
minLeft := Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||||
|
maxRight := Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
|
||||||
|
return maxRight - minLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns the height
|
||||||
|
func (bc BoxCorners) Height() int {
|
||||||
|
minTop := Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||||
|
maxBottom := 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 := Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
|
||||||
|
right := Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
|
||||||
|
x = ((right - left) >> 1) + left
|
||||||
|
|
||||||
|
top := Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
|
||||||
|
bottom := 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 := Math.DegreesToRadians(thetaDegrees)
|
||||||
|
|
||||||
|
tlx, tly := Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
|
||||||
|
trx, try := Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
|
||||||
|
brx, bry := Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
|
||||||
|
blx, bly := 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)
|
||||||
|
}
|
||||||
|
|
43
box_test.go
43
box_test.go
|
@ -143,3 +143,46 @@ func TestBoxShift(t *testing.T) {
|
||||||
assert.Equal(11, shifted.Right)
|
assert.Equal(11, shifted.Right)
|
||||||
assert.Equal(12, shifted.Bottom)
|
assert.Equal(12, shifted.Bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBoxCenter(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
b := Box{
|
||||||
|
Top: 10,
|
||||||
|
Left: 10,
|
||||||
|
Right: 20,
|
||||||
|
Bottom: 30,
|
||||||
|
}
|
||||||
|
cx, cy := b.Center()
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
6
chart.go
6
chart.go
|
@ -102,12 +102,12 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
|
||||||
|
|
||||||
if c.hasAxes() {
|
if c.hasAxes() {
|
||||||
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||||
canvasBox = c.getAxisAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||||
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
|
|
||||||
// do a second pass in case things haven't settled yet.
|
// do a second pass in case things haven't settled yet.
|
||||||
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
|
||||||
canvasBox = c.getAxisAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
|
||||||
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,7 +320,7 @@ func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueForm
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) getAxisAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
|
func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
|
||||||
axesOuterBox := canvasBox.Clone()
|
axesOuterBox := canvasBox.Clone()
|
||||||
if c.XAxis.Style.Show {
|
if c.XAxis.Style.Show {
|
||||||
axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks)
|
axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks)
|
||||||
|
|
40
draw.go
40
draw.go
|
@ -140,6 +140,7 @@ func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s
|
||||||
// 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()
|
||||||
|
|
||||||
textBox := r.MeasureText(label)
|
textBox := r.MeasureText(label)
|
||||||
textWidth := textBox.Width()
|
textWidth := textBox.Width()
|
||||||
|
@ -168,6 +169,8 @@ func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly i
|
||||||
// Annotation draws an anotation with a renderer.
|
// Annotation draws an anotation with a renderer.
|
||||||
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
|
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
|
||||||
style.GetTextOptions().WriteToRenderer(r)
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
textBox := r.MeasureText(label)
|
textBox := r.MeasureText(label)
|
||||||
textWidth := textBox.Width()
|
textWidth := textBox.Width()
|
||||||
halfTextHeight := textBox.Height() >> 1
|
halfTextHeight := textBox.Height() >> 1
|
||||||
|
@ -209,7 +212,8 @@ func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, lab
|
||||||
|
|
||||||
// Box draws a box with a given style.
|
// Box draws a box with a given style.
|
||||||
func (d draw) Box(r Renderer, b Box, s Style) {
|
func (d draw) Box(r Renderer, b Box, s Style) {
|
||||||
s.WriteToRenderer(r)
|
s.GetFillAndStrokeOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
r.MoveTo(b.Left, b.Top)
|
r.MoveTo(b.Left, b.Top)
|
||||||
r.LineTo(b.Right, b.Top)
|
r.LineTo(b.Right, b.Top)
|
||||||
|
@ -219,19 +223,45 @@ func (d draw) Box(r Renderer, b Box, s Style) {
|
||||||
r.FillStroke()
|
r.FillStroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
|
||||||
|
d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d draw) BoxCorners(r Renderer, bc BoxCorners, 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.Close()
|
||||||
|
r.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
// DrawText draws text with a given style.
|
// DrawText draws text with a given style.
|
||||||
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
|
||||||
style.GetTextOptions().WriteToRenderer(r)
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
r.Text(text, x, y)
|
r.Text(text, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d draw) MeasureText(r Renderer, text string, style Style) Box {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
|
return r.MeasureText(text)
|
||||||
|
}
|
||||||
|
|
||||||
// TextWithin draws the text within a given box.
|
// TextWithin draws the text within a given box.
|
||||||
func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
||||||
|
style.GetTextOptions().WriteToRenderer(r)
|
||||||
|
defer r.ResetStyle()
|
||||||
|
|
||||||
lines := Text.WrapFit(r, text, box.Width(), style)
|
lines := Text.WrapFit(r, text, box.Width(), style)
|
||||||
linesBox := Text.MeasureLines(r, lines, style)
|
linesBox := Text.MeasureLines(r, lines, style)
|
||||||
|
|
||||||
style.GetTextOptions().WriteToRenderer(r)
|
|
||||||
|
|
||||||
y := box.Top
|
y := box.Top
|
||||||
|
|
||||||
switch style.GetTextVerticalAlign() {
|
switch style.GetTextVerticalAlign() {
|
||||||
|
@ -252,7 +282,11 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
|
||||||
default:
|
default:
|
||||||
tx = box.Left
|
tx = box.Left
|
||||||
}
|
}
|
||||||
|
if style.TextRotationDegrees == 0 {
|
||||||
ty = y + lineBox.Height()
|
ty = y + lineBox.Height()
|
||||||
|
} else {
|
||||||
|
ty = y
|
||||||
|
}
|
||||||
|
|
||||||
d.Text(r, line, tx, ty, style)
|
d.Text(r, line, tx, ty, style)
|
||||||
y += lineBox.Height() + style.GetTextLineSpacing()
|
y += lineBox.Height() + style.GetTextLineSpacing()
|
||||||
|
|
52
file_util.go
Normal file
52
file_util.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
exception "github.com/blendlabs/go-exception"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// File contains file utility functions
|
||||||
|
File = fileUtil{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileUtil struct{}
|
||||||
|
|
||||||
|
// ReadByLines reads a file and calls the handler for each line.
|
||||||
|
func (fu fileUtil) ReadByLines(filePath string, handler func(line string)) error {
|
||||||
|
if f, err := os.Open(filePath); err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
handler(line)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return exception.Wrap(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadByChunks reads a file in `chunkSize` pieces, dispatched to the handler.
|
||||||
|
func (fu fileUtil) ReadByChunks(filePath string, chunkSize int, handler func(line []byte)) error {
|
||||||
|
if f, err := os.Open(filePath); err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
chunk := make([]byte, chunkSize)
|
||||||
|
for {
|
||||||
|
readBytes, err := f.Read(chunk)
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
readData := chunk[:readBytes]
|
||||||
|
handler(readData)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return exception.Wrap(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -54,9 +54,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
||||||
Bottom: legend.Top + legendPadding.Top,
|
Bottom: legend.Top + legendPadding.Top,
|
||||||
}
|
}
|
||||||
|
|
||||||
r.SetFont(legendStyle.GetFont())
|
legendStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
r.SetFontColor(legendStyle.GetFontColor())
|
|
||||||
r.SetFontSize(legendStyle.GetFontSize())
|
|
||||||
|
|
||||||
// measure
|
// measure
|
||||||
labelCount := 0
|
labelCount := 0
|
||||||
|
@ -79,6 +77,8 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
|
||||||
|
|
||||||
Draw.Box(r, legend, legendStyle)
|
Draw.Box(r, legend, legendStyle)
|
||||||
|
|
||||||
|
legendStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
|
||||||
ycursor := legendContent.Top
|
ycursor := legendContent.Top
|
||||||
tx := legendContent.Left
|
tx := legendContent.Left
|
||||||
legendCount := 0
|
legendCount := 0
|
||||||
|
|
|
@ -33,12 +33,12 @@ func (mhr MarketHoursRange) IsZero() bool {
|
||||||
|
|
||||||
// GetMin returns the min value.
|
// GetMin returns the min value.
|
||||||
func (mhr MarketHoursRange) GetMin() float64 {
|
func (mhr MarketHoursRange) GetMin() float64 {
|
||||||
return TimeToFloat64(mhr.Min)
|
return Time.ToFloat64(mhr.Min)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMax returns the max value.
|
// GetMax returns the max value.
|
||||||
func (mhr MarketHoursRange) GetMax() float64 {
|
func (mhr MarketHoursRange) GetMax() float64 {
|
||||||
return TimeToFloat64(mhr.GetEffectiveMax())
|
return Time.ToFloat64(mhr.GetEffectiveMax())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEffectiveMax gets either the close on the max, or the max itself.
|
// GetEffectiveMax gets either the close on the max, or the max itself.
|
||||||
|
@ -52,13 +52,13 @@ func (mhr MarketHoursRange) GetEffectiveMax() time.Time {
|
||||||
|
|
||||||
// SetMin sets the min value.
|
// SetMin sets the min value.
|
||||||
func (mhr *MarketHoursRange) SetMin(min float64) {
|
func (mhr *MarketHoursRange) SetMin(min float64) {
|
||||||
mhr.Min = Float64ToTime(min)
|
mhr.Min = Time.FromFloat64(min)
|
||||||
mhr.Min = mhr.Min.In(mhr.GetTimezone())
|
mhr.Min = mhr.Min.In(mhr.GetTimezone())
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMax sets the max value.
|
// SetMax sets the max value.
|
||||||
func (mhr *MarketHoursRange) SetMax(max float64) {
|
func (mhr *MarketHoursRange) SetMax(max float64) {
|
||||||
mhr.Max = Float64ToTime(max)
|
mhr.Max = Time.FromFloat64(max)
|
||||||
mhr.Max = mhr.Max.In(mhr.GetTimezone())
|
mhr.Max = mhr.Max.In(mhr.GetTimezone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ func (mhr *MarketHoursRange) makeTicks(vf ValueFormatter, times []time.Time) []T
|
||||||
ticks := make([]Tick, len(times))
|
ticks := make([]Tick, len(times))
|
||||||
for index, t := range times {
|
for index, t := range times {
|
||||||
ticks[index] = Tick{
|
ticks[index] = Tick{
|
||||||
Value: TimeToFloat64(t),
|
Value: Time.ToFloat64(t),
|
||||||
Label: vf(t),
|
Label: vf(t),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,7 @@ func (mhr MarketHoursRange) String() string {
|
||||||
|
|
||||||
// Translate maps a given value into the ContinuousRange space.
|
// Translate maps a given value into the ContinuousRange space.
|
||||||
func (mhr MarketHoursRange) Translate(value float64) int {
|
func (mhr MarketHoursRange) Translate(value float64) int {
|
||||||
valueTime := Float64ToTime(value)
|
valueTime := Time.FromFloat64(value)
|
||||||
valueTimeEastern := valueTime.In(Date.Eastern())
|
valueTimeEastern := valueTime.In(Date.Eastern())
|
||||||
totalSeconds := Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
|
totalSeconds := Date.CalculateMarketSecondsBetween(mhr.Min, mhr.GetEffectiveMax(), mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
|
||||||
valueDelta := Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
|
valueDelta := Date.CalculateMarketSecondsBetween(mhr.Min, valueTimeEastern, mhr.GetMarketOpen(), mhr.GetMarketClose(), mhr.HolidayProvider)
|
||||||
|
|
|
@ -35,9 +35,9 @@ func TestMarketHoursRangeTranslate(t *testing.T) {
|
||||||
|
|
||||||
weds := time.Date(2016, 07, 20, 9, 30, 0, 0, Date.Eastern())
|
weds := time.Date(2016, 07, 20, 9, 30, 0, 0, Date.Eastern())
|
||||||
|
|
||||||
assert.Equal(0, r.Translate(TimeToFloat64(r.Min)))
|
assert.Equal(0, r.Translate(Time.ToFloat64(r.Min)))
|
||||||
assert.Equal(400, r.Translate(TimeToFloat64(weds)))
|
assert.Equal(400, r.Translate(Time.ToFloat64(weds)))
|
||||||
assert.Equal(1000, r.Translate(TimeToFloat64(r.Max)))
|
assert.Equal(1000, r.Translate(Time.ToFloat64(r.Max)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarketHoursRangeGetTicks(t *testing.T) {
|
func TestMarketHoursRangeGetTicks(t *testing.T) {
|
||||||
|
@ -67,6 +67,6 @@ func TestMarketHoursRangeGetTicks(t *testing.T) {
|
||||||
ticks := ra.GetTicks(r, defaults, TimeValueFormatter)
|
ticks := ra.GetTicks(r, defaults, TimeValueFormatter)
|
||||||
assert.NotEmpty(ticks)
|
assert.NotEmpty(ticks)
|
||||||
assert.Len(ticks, 5)
|
assert.Len(ticks, 5)
|
||||||
assert.NotEqual(TimeToFloat64(ra.Min), ticks[0].Value)
|
assert.NotEqual(Time.ToFloat64(ra.Min), ticks[0].Value)
|
||||||
assert.NotEmpty(ticks[0].Label)
|
assert.NotEmpty(ticks[0].Label)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,16 +19,6 @@ const (
|
||||||
_r2d = (180.0 / math.Pi)
|
_r2d = (180.0 / math.Pi)
|
||||||
)
|
)
|
||||||
|
|
||||||
// TimeToFloat64 returns a float64 representation of a time.
|
|
||||||
func TimeToFloat64(t time.Time) float64 {
|
|
||||||
return float64(t.UnixNano())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float64ToTime returns a time from a float64.
|
|
||||||
func Float64ToTime(tf float64) time.Time {
|
|
||||||
return time.Unix(0, int64(tf))
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Math contains helper methods for common math operations.
|
// Math contains helper methods for common math operations.
|
||||||
Math = &mathUtil{}
|
Math = &mathUtil{}
|
||||||
|
@ -144,6 +134,16 @@ func (m mathUtil) AbsInt(value int) int {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mean returns the mean of a set of values
|
||||||
|
func (m mathUtil) Mean(values ...float64) float64 {
|
||||||
|
return m.Sum(values...) / float64(len(values))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MeanInt returns the mean of a set of integer values.
|
||||||
|
func (m mathUtil) MeanInt(values ...int) int {
|
||||||
|
return m.SumInt(values...) / len(values)
|
||||||
|
}
|
||||||
|
|
||||||
// Sum sums a set of values.
|
// Sum sums a set of values.
|
||||||
func (m mathUtil) Sum(values ...float64) float64 {
|
func (m mathUtil) Sum(values ...float64) float64 {
|
||||||
var total float64
|
var total float64
|
||||||
|
@ -214,9 +214,18 @@ func (m mathUtil) DegreesToCompass(deg float64) float64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CirclePoint returns the absolute position of a circle diameter point given
|
// CirclePoint returns the absolute position of a circle diameter point given
|
||||||
// by the radius and the angle.
|
// by the radius and the theta.
|
||||||
func (m mathUtil) CirclePoint(cx, cy int, radius, angleRadians float64) (x, y int) {
|
func (m mathUtil) CirclePoint(cx, cy int, radius, thetaRadians float64) (x, y int) {
|
||||||
x = cx + int(radius*math.Sin(angleRadians))
|
x = cx + int(radius*math.Sin(thetaRadians))
|
||||||
y = cy - int(radius*math.Cos(angleRadians))
|
y = cy - int(radius*math.Cos(thetaRadians))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mathUtil) RotateCoordinate(cx, cy, x, y int, thetaRadians float64) (rx, ry int) {
|
||||||
|
tempX, tempY := float64(x-cx), float64(y-cy)
|
||||||
|
rotatedX := tempX*math.Cos(thetaRadians) - tempY*math.Sin(thetaRadians)
|
||||||
|
rotatedY := tempX*math.Sin(thetaRadians) + tempY*math.Cos(thetaRadians)
|
||||||
|
rx = int(rotatedX) + cx
|
||||||
|
ry = int(rotatedY) + cy
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -160,3 +160,25 @@ func TestRadianAdd(t *testing.T) {
|
||||||
assert.Equal(_pi, Math.RadianAdd(_pi, _2pi))
|
assert.Equal(_pi, Math.RadianAdd(_pi, _2pi))
|
||||||
assert.Equal(_pi, Math.RadianAdd(_pi, -_2pi))
|
assert.Equal(_pi, Math.RadianAdd(_pi, -_2pi))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRotateCoordinate90(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cx, cy := 10, 10
|
||||||
|
x, y := 5, 10
|
||||||
|
|
||||||
|
rx, ry := Math.RotateCoordinate(cx, cy, x, y, Math.DegreesToRadians(90))
|
||||||
|
assert.Equal(10, rx)
|
||||||
|
assert.Equal(5, ry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRotateCoordinate45(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cx, cy := 10, 10
|
||||||
|
x, y := 5, 10
|
||||||
|
|
||||||
|
rx, ry := Math.RotateCoordinate(cx, cy, x, y, Math.DegreesToRadians(45))
|
||||||
|
assert.Equal(7, rx)
|
||||||
|
assert.Equal(7, ry)
|
||||||
|
}
|
|
@ -28,11 +28,16 @@ type rasterRenderer struct {
|
||||||
i *image.RGBA
|
i *image.RGBA
|
||||||
gc *drawing.RasterGraphicContext
|
gc *drawing.RasterGraphicContext
|
||||||
|
|
||||||
rotateRadians float64
|
rotateRadians *float64
|
||||||
|
|
||||||
s Style
|
s Style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rr *rasterRenderer) ResetStyle() {
|
||||||
|
rr.s = Style{Font: rr.s.Font}
|
||||||
|
rr.ClearTextRotation()
|
||||||
|
}
|
||||||
|
|
||||||
// GetDPI returns the dpi.
|
// GetDPI returns the dpi.
|
||||||
func (rr *rasterRenderer) GetDPI() float64 {
|
func (rr *rasterRenderer) GetDPI() float64 {
|
||||||
return rr.gc.GetDPI()
|
return rr.gc.GetDPI()
|
||||||
|
@ -177,35 +182,40 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
|
||||||
t = 0
|
t = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return Box{
|
textBox := Box{
|
||||||
Top: int(math.Ceil(t)),
|
Top: int(math.Ceil(t)),
|
||||||
Left: int(math.Ceil(l)),
|
Left: int(math.Ceil(l)),
|
||||||
Right: int(math.Ceil(r)),
|
Right: int(math.Ceil(r)),
|
||||||
Bottom: int(math.Ceil(b)),
|
Bottom: int(math.Ceil(b)),
|
||||||
}
|
}
|
||||||
|
if rr.rotateRadians == nil {
|
||||||
|
return textBox
|
||||||
|
}
|
||||||
|
|
||||||
|
return textBox.Corners().Rotate(Math.RadiansToDegrees(*rr.rotateRadians)).Box()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTextRotation sets a text rotation.
|
// SetTextRotation sets a text rotation.
|
||||||
func (rr *rasterRenderer) SetTextRotation(radians float64) {
|
func (rr *rasterRenderer) SetTextRotation(radians float64) {
|
||||||
rr.rotateRadians = radians
|
rr.rotateRadians = &radians
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *rasterRenderer) getCoords(x, y int) (xf, yf int) {
|
func (rr *rasterRenderer) getCoords(x, y int) (xf, yf int) {
|
||||||
if rr.rotateRadians == 0 {
|
if rr.rotateRadians == nil {
|
||||||
xf = x
|
xf = x
|
||||||
yf = y
|
yf = y
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rr.gc.Translate(float64(x), float64(y))
|
rr.gc.Translate(float64(x), float64(y))
|
||||||
rr.gc.Rotate(rr.rotateRadians)
|
rr.gc.Rotate(*rr.rotateRadians)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearTextRotation clears text rotation.
|
// ClearTextRotation clears text rotation.
|
||||||
func (rr *rasterRenderer) ClearTextRotation() {
|
func (rr *rasterRenderer) ClearTextRotation() {
|
||||||
rr.gc.SetMatrixTransform(drawing.NewIdentityMatrix())
|
rr.gc.SetMatrixTransform(drawing.NewIdentityMatrix())
|
||||||
rr.rotateRadians = 0
|
rr.rotateRadians = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save implements the interface method.
|
// Save implements the interface method.
|
||||||
|
|
|
@ -9,6 +9,9 @@ import (
|
||||||
|
|
||||||
// Renderer represents the basic methods required to draw a chart.
|
// Renderer represents the basic methods required to draw a chart.
|
||||||
type Renderer interface {
|
type Renderer interface {
|
||||||
|
// ResetStyle should reset any style related settings on the renderer.
|
||||||
|
ResetStyle()
|
||||||
|
|
||||||
// GetDPI gets the DPI for the renderer.
|
// GetDPI gets the DPI for the renderer.
|
||||||
GetDPI() float64
|
GetDPI() float64
|
||||||
|
|
||||||
|
|
18
style.go
18
style.go
|
@ -33,6 +33,7 @@ type Style struct {
|
||||||
TextVerticalAlign TextVerticalAlign
|
TextVerticalAlign TextVerticalAlign
|
||||||
TextWrap TextWrap
|
TextWrap TextWrap
|
||||||
TextLineSpacing int
|
TextLineSpacing int
|
||||||
|
TextRotationDegrees float64 //0 is unset or normal
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsZero returns if the object is set or not.
|
// IsZero returns if the object is set or not.
|
||||||
|
@ -241,6 +242,16 @@ func (s Style) GetTextLineSpacing(defaults ...int) int {
|
||||||
return s.TextLineSpacing
|
return s.TextLineSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTextRotationDegrees returns the text rotation in degrees.
|
||||||
|
func (s Style) GetTextRotationDegrees(defaults ...float64) float64 {
|
||||||
|
if s.TextRotationDegrees == 0 {
|
||||||
|
if len(defaults) > 0 {
|
||||||
|
return defaults[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.TextRotationDegrees
|
||||||
|
}
|
||||||
|
|
||||||
// WriteToRenderer passes the style's options to a renderer.
|
// WriteToRenderer passes the style's options to a renderer.
|
||||||
func (s Style) WriteToRenderer(r Renderer) {
|
func (s Style) WriteToRenderer(r Renderer) {
|
||||||
r.SetStrokeColor(s.GetStrokeColor())
|
r.SetStrokeColor(s.GetStrokeColor())
|
||||||
|
@ -250,6 +261,11 @@ func (s Style) WriteToRenderer(r Renderer) {
|
||||||
r.SetFont(s.GetFont())
|
r.SetFont(s.GetFont())
|
||||||
r.SetFontColor(s.GetFontColor())
|
r.SetFontColor(s.GetFontColor())
|
||||||
r.SetFontSize(s.GetFontSize())
|
r.SetFontSize(s.GetFontSize())
|
||||||
|
|
||||||
|
r.ClearTextRotation()
|
||||||
|
if s.GetTextRotationDegrees() != 0 {
|
||||||
|
r.SetTextRotation(Math.DegreesToRadians(s.GetTextRotationDegrees()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer.
|
// WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer.
|
||||||
|
@ -281,6 +297,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
|
||||||
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
|
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
|
||||||
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
|
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
|
||||||
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
|
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
|
||||||
|
final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,5 +337,6 @@ func (s Style) GetTextOptions() Style {
|
||||||
TextVerticalAlign: s.TextVerticalAlign,
|
TextVerticalAlign: s.TextVerticalAlign,
|
||||||
TextWrap: s.TextWrap,
|
TextWrap: s.TextWrap,
|
||||||
TextLineSpacing: s.TextLineSpacing,
|
TextLineSpacing: s.TextLineSpacing,
|
||||||
|
TextRotationDegrees: s.TextRotationDegrees,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,14 +30,14 @@ func (ts TimeSeries) Len() int {
|
||||||
|
|
||||||
// GetValue gets a value at a given index.
|
// GetValue gets a value at a given index.
|
||||||
func (ts TimeSeries) GetValue(index int) (x, y float64) {
|
func (ts TimeSeries) GetValue(index int) (x, y float64) {
|
||||||
x = TimeToFloat64(ts.XValues[index])
|
x = Time.ToFloat64(ts.XValues[index])
|
||||||
y = ts.YValues[index]
|
y = ts.YValues[index]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLastValue gets the last value.
|
// GetLastValue gets the last value.
|
||||||
func (ts TimeSeries) GetLastValue() (x, y float64) {
|
func (ts TimeSeries) GetLastValue() (x, y float64) {
|
||||||
x = TimeToFloat64(ts.XValues[len(ts.XValues)-1])
|
x = Time.ToFloat64(ts.XValues[len(ts.XValues)-1])
|
||||||
y = ts.YValues[len(ts.YValues)-1]
|
y = ts.YValues[len(ts.YValues)-1]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
20
time_util.go
Normal file
20
time_util.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Time contains time utility functions.
|
||||||
|
Time = timeUtil{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type timeUtil struct{}
|
||||||
|
|
||||||
|
// TimeToFloat64 returns a float64 representation of a time.
|
||||||
|
func (tu timeUtil) ToFloat64(t time.Time) float64 {
|
||||||
|
return float64(t.UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64ToTime returns a time from a float64.
|
||||||
|
func (tu timeUtil) FromFloat64(tf float64) time.Time {
|
||||||
|
return time.Unix(0, int64(tf))
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ func TestTimeValueFormatterWithFormat(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
d := time.Now()
|
d := time.Now()
|
||||||
di := TimeToFloat64(d)
|
di := Time.ToFloat64(d)
|
||||||
df := float64(di)
|
df := float64(di)
|
||||||
|
|
||||||
s := TimeValueFormatterWithFormat(d, DefaultDateFormat)
|
s := TimeValueFormatterWithFormat(d, DefaultDateFormat)
|
||||||
|
|
|
@ -32,11 +32,15 @@ type vectorRenderer struct {
|
||||||
b *bytes.Buffer
|
b *bytes.Buffer
|
||||||
c *canvas
|
c *canvas
|
||||||
s *Style
|
s *Style
|
||||||
r float64
|
|
||||||
p []string
|
p []string
|
||||||
fc *font.Drawer
|
fc *font.Drawer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (vr *vectorRenderer) ResetStyle() {
|
||||||
|
vr.s = &Style{Font: vr.s.Font}
|
||||||
|
vr.fc = nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetDPI returns the dpi.
|
// GetDPI returns the dpi.
|
||||||
func (vr *vectorRenderer) GetDPI() float64 {
|
func (vr *vectorRenderer) GetDPI() float64 {
|
||||||
return vr.dpi
|
return vr.dpi
|
||||||
|
@ -168,18 +172,22 @@ 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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
box = box.Corners().Rotate(Math.RadiansToDegrees(*vr.c.textTheta)).Box()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTextRotation sets the text rotation.
|
// SetTextRotation sets the text rotation.
|
||||||
func (vr *vectorRenderer) SetTextRotation(radians float64) {
|
func (vr *vectorRenderer) SetTextRotation(radians float64) {
|
||||||
vr.c.r = radians
|
vr.c.textTheta = &radians
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearTextRotation clears the text rotation.
|
// ClearTextRotation clears the text rotation.
|
||||||
func (vr *vectorRenderer) ClearTextRotation() {
|
func (vr *vectorRenderer) ClearTextRotation() {
|
||||||
vr.c.r = 0
|
vr.c.textTheta = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the renderer's contents to a writer.
|
// Save saves the renderer's contents to a writer.
|
||||||
|
@ -198,7 +206,7 @@ func newCanvas(w io.Writer) *canvas {
|
||||||
type canvas struct {
|
type canvas struct {
|
||||||
w io.Writer
|
w io.Writer
|
||||||
dpi float64
|
dpi float64
|
||||||
r float64
|
textTheta *float64
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
}
|
}
|
||||||
|
@ -218,10 +226,10 @@ func (c *canvas) Path(d string, style Style) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *canvas) Text(x, y int, body string, style Style) {
|
func (c *canvas) Text(x, y int, body string, style Style) {
|
||||||
if c.r == 0 {
|
if c.textTheta == nil {
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body)))
|
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body)))
|
||||||
} else {
|
} else {
|
||||||
transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, Math.RadiansToDegrees(c.r), x, y)
|
transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, Math.RadiansToDegrees(*c.textTheta), x, y)
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s"%s>%s</text>`, x, y, c.styleAsSVG(style), transform, body)))
|
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s"%s>%s</text>`, x, y, c.styleAsSVG(style), transform, body)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
41
xaxis.go
41
xaxis.go
|
@ -9,11 +9,13 @@ import (
|
||||||
type XAxis struct {
|
type XAxis struct {
|
||||||
Name string
|
Name string
|
||||||
NameStyle Style
|
NameStyle Style
|
||||||
|
|
||||||
Style Style
|
Style Style
|
||||||
ValueFormatter ValueFormatter
|
ValueFormatter ValueFormatter
|
||||||
Range Range
|
Range Range
|
||||||
Ticks []Tick
|
|
||||||
|
|
||||||
|
TickStyle Style
|
||||||
|
Ticks []Tick
|
||||||
TickPosition TickPosition
|
TickPosition TickPosition
|
||||||
|
|
||||||
GridLines []GridLine
|
GridLines []GridLine
|
||||||
|
@ -68,20 +70,20 @@ func (xa XAxis) GetGridLines(ticks []Tick) []GridLine {
|
||||||
|
|
||||||
// Measure returns the bounds of the axis.
|
// Measure returns the bounds of the axis.
|
||||||
func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
||||||
tickStyle := xa.Style.InheritFrom(defaults)
|
tickStyle := xa.TickStyle.InheritFrom(xa.Style.InheritFrom(defaults))
|
||||||
sort.Sort(Ticks(ticks))
|
sort.Sort(Ticks(ticks))
|
||||||
|
|
||||||
tp := xa.GetTickPosition()
|
tp := xa.GetTickPosition()
|
||||||
|
|
||||||
|
var ltx, rtx int
|
||||||
|
var tx, ty int
|
||||||
var left, right, bottom = math.MaxInt32, 0, 0
|
var left, right, bottom = math.MaxInt32, 0, 0
|
||||||
for index, t := range ticks {
|
for index, t := range ticks {
|
||||||
v := t.Value
|
v := t.Value
|
||||||
tickStyle.GetTextOptions().WriteToRenderer(r)
|
tb := Draw.MeasureText(r, t.Label, tickStyle.GetTextOptions())
|
||||||
tb := r.MeasureText(t.Label)
|
|
||||||
|
|
||||||
var ltx, rtx int
|
tx = canvasBox.Left + ra.Translate(v)
|
||||||
tx := canvasBox.Left + ra.Translate(v)
|
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
||||||
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
|
||||||
switch tp {
|
switch tp {
|
||||||
case TickPositionUnderTick, TickPositionUnset:
|
case TickPositionUnderTick, TickPositionUnset:
|
||||||
ltx = tx - tb.Width()>>1
|
ltx = tx - tb.Width()>>1
|
||||||
|
@ -101,7 +103,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 := r.MeasureText(xa.Name)
|
tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults))
|
||||||
bottom += DefaultXAxisMargin + tb.Height()
|
bottom += DefaultXAxisMargin + tb.Height()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +117,7 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
|
|
||||||
// Render renders the axis
|
// Render renders the axis
|
||||||
func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
||||||
tickStyle := xa.Style.InheritFrom(defaults)
|
tickStyle := xa.TickStyle.InheritFrom(xa.Style.InheritFrom(defaults))
|
||||||
|
|
||||||
tickStyle.GetStrokeOptions().WriteToRenderer(r)
|
tickStyle.GetStrokeOptions().WriteToRenderer(r)
|
||||||
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
|
||||||
|
@ -139,25 +141,31 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight)
|
r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
tickStyle.GetTextOptions().WriteToRenderer(r)
|
tickWithAxisStyle := xa.TickStyle.InheritFrom(xa.Style.InheritFrom(defaults))
|
||||||
tb := r.MeasureText(t.Label)
|
tb := Draw.MeasureText(r, t.Label, tickWithAxisStyle)
|
||||||
|
|
||||||
switch tp {
|
switch tp {
|
||||||
case TickPositionUnderTick, TickPositionUnset:
|
case TickPositionUnderTick, TickPositionUnset:
|
||||||
|
if tickStyle.TextRotationDegrees == 0 {
|
||||||
|
tx = tx - tb.Width()>>1
|
||||||
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
|
||||||
r.Text(t.Label, tx-tb.Width()>>1, ty)
|
} else {
|
||||||
|
ty = canvasBox.Bottom + (2 * DefaultXAxisMargin)
|
||||||
|
}
|
||||||
|
Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle)
|
||||||
maxTextHeight = Math.MaxInt(maxTextHeight, tb.Height())
|
maxTextHeight = Math.MaxInt(maxTextHeight, tb.Height())
|
||||||
break
|
break
|
||||||
case TickPositionBetweenTicks:
|
case TickPositionBetweenTicks:
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
llx := ra.Translate(ticks[index-1].Value)
|
llx := ra.Translate(ticks[index-1].Value)
|
||||||
ltx := canvasBox.Left + llx
|
ltx := canvasBox.Left + llx
|
||||||
finalTickStyle := tickStyle.InheritFrom(Style{TextHorizontalAlign: TextHorizontalAlignCenter})
|
finalTickStyle := tickWithAxisStyle.InheritFrom(Style{TextHorizontalAlign: TextHorizontalAlignCenter})
|
||||||
|
|
||||||
Draw.TextWithin(r, t.Label, Box{
|
Draw.TextWithin(r, t.Label, Box{
|
||||||
Left: ltx,
|
Left: ltx,
|
||||||
Right: tx,
|
Right: tx,
|
||||||
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
Top: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
Bottom: canvasBox.Bottom + DefaultXAxisMargin + tb.Height(),
|
Bottom: canvasBox.Bottom + DefaultXAxisMargin,
|
||||||
}, 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)
|
||||||
|
@ -169,11 +177,10 @@ 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 {
|
||||||
nameStyle.GetTextOptions().WriteToRenderer(r)
|
tb := Draw.MeasureText(r, xa.Name, nameStyle)
|
||||||
tb := r.MeasureText(xa.Name)
|
|
||||||
tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1)
|
tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1)
|
||||||
ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + tb.Height()
|
ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + tb.Height()
|
||||||
r.Text(xa.Name, tx, ty)
|
Draw.Text(r, xa.Name, tx, ty, nameStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if xa.GridMajorStyle.Show || xa.GridMinorStyle.Show {
|
if xa.GridMajorStyle.Show || xa.GridMinorStyle.Show {
|
||||||
|
|
55
yaxis.go
55
yaxis.go
|
@ -20,9 +20,10 @@ type YAxis struct {
|
||||||
ValueFormatter ValueFormatter
|
ValueFormatter ValueFormatter
|
||||||
Range Range
|
Range Range
|
||||||
|
|
||||||
|
TickStyle Style
|
||||||
Ticks []Tick
|
Ticks []Tick
|
||||||
GridLines []GridLine
|
|
||||||
|
|
||||||
|
GridLines []GridLine
|
||||||
GridMajorStyle Style
|
GridMajorStyle Style
|
||||||
GridMinorStyle Style
|
GridMinorStyle Style
|
||||||
}
|
}
|
||||||
|
@ -42,6 +43,11 @@ func (ya YAxis) GetStyle() Style {
|
||||||
return ya.Style
|
return ya.Style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTickStyle returns the tick style.
|
||||||
|
func (ya YAxis) GetTickStyle() Style {
|
||||||
|
return ya.TickStyle
|
||||||
|
}
|
||||||
|
|
||||||
// GetTicks returns the ticks for a series.
|
// GetTicks returns the ticks for a series.
|
||||||
// The coalesce priority is:
|
// The coalesce priority is:
|
||||||
// - User Supplied Ticks (i.e. Ticks array on the axis itself).
|
// - User Supplied Ticks (i.e. Ticks array on the axis itself).
|
||||||
|
@ -68,8 +74,6 @@ func (ya YAxis) GetGridLines(ticks []Tick) []GridLine {
|
||||||
|
|
||||||
// Measure returns the bounds of the axis.
|
// Measure returns the bounds of the axis.
|
||||||
func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
|
||||||
ya.Style.InheritFrom(defaults).WriteToRenderer(r)
|
|
||||||
|
|
||||||
sort.Sort(Ticks(ticks))
|
sort.Sort(Ticks(ticks))
|
||||||
|
|
||||||
var tx int
|
var tx int
|
||||||
|
@ -79,6 +83,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
tx = canvasBox.Left - DefaultYAxisMargin
|
tx = canvasBox.Left - DefaultYAxisMargin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ya.TickStyle.InheritFrom(ya.Style.InheritFrom(defaults)).WriteToRenderer(r)
|
||||||
var minx, maxx, miny, maxy = math.MaxInt32, 0, math.MaxInt32, 0
|
var minx, maxx, miny, maxy = math.MaxInt32, 0, math.MaxInt32, 0
|
||||||
var maxTextHeight int
|
var maxTextHeight int
|
||||||
for _, t := range ticks {
|
for _, t := range ticks {
|
||||||
|
@ -86,14 +91,13 @@ 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
|
||||||
finalTextX := tx
|
finalTextX := tx
|
||||||
if ya.AxisType == YAxisSecondary {
|
if ya.AxisType == YAxisSecondary {
|
||||||
finalTextX = tx - tb.Width()
|
finalTextX = tx - tb.Width()
|
||||||
}
|
}
|
||||||
|
|
||||||
if tb.Height() > maxTextHeight {
|
maxTextHeight = Math.MaxInt(tb.Height(), maxTextHeight)
|
||||||
maxTextHeight = tb.Height()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ya.AxisType == YAxisPrimary {
|
if ya.AxisType == YAxisPrimary {
|
||||||
minx = canvasBox.Right
|
minx = canvasBox.Right
|
||||||
|
@ -102,8 +106,9 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
minx = Math.MinInt(minx, finalTextX)
|
minx = Math.MinInt(minx, finalTextX)
|
||||||
maxx = Math.MaxInt(maxx, tx)
|
maxx = Math.MaxInt(maxx, tx)
|
||||||
}
|
}
|
||||||
miny = Math.MinInt(miny, ly-tb.Height()>>1)
|
|
||||||
maxy = Math.MaxInt(maxy, ly+tb.Height()>>1)
|
miny = Math.MinInt(miny, ly-tbh2)
|
||||||
|
maxy = Math.MaxInt(maxy, ly+tbh2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ya.NameStyle.Show && len(ya.Name) > 0 {
|
if ya.NameStyle.Show && len(ya.Name) > 0 {
|
||||||
|
@ -120,11 +125,12 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
|
||||||
|
|
||||||
// Render renders the axis.
|
// Render renders the axis.
|
||||||
func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
|
||||||
ya.Style.InheritFrom(defaults).WriteToRenderer(r)
|
tickStyle := ya.TickStyle.InheritFrom(ya.Style.InheritFrom(defaults))
|
||||||
|
tickStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
sort.Sort(Ticks(ticks))
|
sort.Sort(Ticks(ticks))
|
||||||
|
|
||||||
sw := ya.Style.GetStrokeWidth(defaults.StrokeWidth)
|
sw := tickStyle.GetStrokeWidth(defaults.StrokeWidth)
|
||||||
|
|
||||||
var lx int
|
var lx int
|
||||||
var tx int
|
var tx int
|
||||||
|
@ -141,23 +147,30 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
var maxTextWidth int
|
var maxTextWidth int
|
||||||
|
var finalTextX, finalTextY int
|
||||||
for _, t := range ticks {
|
for _, t := range ticks {
|
||||||
v := t.Value
|
v := t.Value
|
||||||
ly := canvasBox.Bottom - ra.Translate(v)
|
ly := canvasBox.Bottom - ra.Translate(v)
|
||||||
|
|
||||||
tb := r.MeasureText(t.Label)
|
tb := Draw.MeasureText(r, t.Label, tickStyle)
|
||||||
|
|
||||||
if tb.Width() > maxTextWidth {
|
if tb.Width() > maxTextWidth {
|
||||||
maxTextWidth = tb.Width()
|
maxTextWidth = tb.Width()
|
||||||
}
|
}
|
||||||
|
|
||||||
finalTextX := tx
|
|
||||||
finalTextY := ly + tb.Height()>>1
|
|
||||||
if ya.AxisType == YAxisSecondary {
|
if ya.AxisType == YAxisSecondary {
|
||||||
finalTextX = tx - tb.Width()
|
finalTextX = tx - tb.Width()
|
||||||
|
} else {
|
||||||
|
finalTextX = tx
|
||||||
}
|
}
|
||||||
|
|
||||||
r.Text(t.Label, finalTextX, finalTextY)
|
if tickStyle.TextRotationDegrees == 0 {
|
||||||
|
finalTextY = ly + tb.Height()>>1
|
||||||
|
} else {
|
||||||
|
finalTextY = ly
|
||||||
|
}
|
||||||
|
|
||||||
|
tickStyle.WriteToRenderer(r)
|
||||||
|
|
||||||
r.MoveTo(lx, ly)
|
r.MoveTo(lx, ly)
|
||||||
if ya.AxisType == YAxisPrimary {
|
if ya.AxisType == YAxisPrimary {
|
||||||
|
@ -166,15 +179,14 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
r.LineTo(lx-DefaultHorizontalTickWidth, ly)
|
r.LineTo(lx-DefaultHorizontalTickWidth, ly)
|
||||||
}
|
}
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
|
Draw.Text(r, t.Label, finalTextX, finalTextY, tickStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
nameStyle := ya.NameStyle.InheritFrom(defaults)
|
nameStyle := ya.NameStyle.InheritFrom(defaults.InheritFrom(Style{TextRotationDegrees: 90}))
|
||||||
if ya.NameStyle.Show && len(ya.Name) > 0 {
|
if ya.NameStyle.Show && len(ya.Name) > 0 {
|
||||||
nameStyle.GetTextOptions().WriteToRenderer(r)
|
nameStyle.GetTextOptions().WriteToRenderer(r)
|
||||||
|
tb := Draw.MeasureText(r, ya.Name, nameStyle)
|
||||||
r.SetTextRotation(Math.DegreesToRadians(90))
|
|
||||||
|
|
||||||
tb := r.MeasureText(ya.Name)
|
|
||||||
|
|
||||||
var tx int
|
var tx int
|
||||||
if ya.AxisType == YAxisPrimary {
|
if ya.AxisType == YAxisPrimary {
|
||||||
|
@ -183,10 +195,9 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
|
||||||
tx = canvasBox.Left - (DefaultYAxisMargin + int(sw) + maxTextWidth + DefaultYAxisMargin)
|
tx = canvasBox.Left - (DefaultYAxisMargin + int(sw) + maxTextWidth + DefaultYAxisMargin)
|
||||||
}
|
}
|
||||||
|
|
||||||
ty := canvasBox.Bottom - (canvasBox.Height()>>1 + tb.Width()>>1)
|
ty := canvasBox.Top + (canvasBox.Height()>>1 + tb.Width()>>1)
|
||||||
|
|
||||||
r.Text(ya.Name, tx, ty)
|
Draw.Text(r, ya.Name, tx, ty, nameStyle)
|
||||||
r.ClearTextRotation()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ya.Zero.Style.Show {
|
if ya.Zero.Style.Show {
|
||||||
|
|
Loading…
Reference in a new issue