diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..ae3f4cb
--- /dev/null
+++ b/.vscode/launch.json
@@ -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
+ }
+ ]
+}
\ No newline at end of file
diff --git a/_examples/request_timings/main.go b/_examples/request_timings/main.go
index dac7e28..7306062 100644
--- a/_examples/request_timings/main.go
+++ b/_examples/request_timings/main.go
@@ -2,23 +2,33 @@ package main
import (
"net/http"
+ "strconv"
"strings"
"time"
- util "github.com/blendlabs/go-util"
"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) {
var xvalues []time.Time
var yvalues []float64
- util.ReadFileByLines("requests.csv", func(line string) {
+ chart.File.ReadByLines("requests.csv", func(line string) {
parts := strings.Split(line, ",")
- year := util.ParseInt(parts[0])
- month := util.ParseInt(parts[1])
- day := util.ParseInt(parts[2])
- hour := util.ParseInt(parts[3])
- elapsedMillis := util.ParseFloat64(parts[4])
+ year := parseInt(parts[0])
+ month := parseInt(parts[1])
+ day := parseInt(parts[2])
+ hour := parseInt(parts[3])
+ elapsedMillis := parseFloat64(parts[4])
xvalues = append(xvalues, time.Date(year, time.Month(month), day, hour, 0, 0, 0, time.UTC))
yvalues = append(yvalues, elapsedMillis)
})
@@ -27,12 +37,12 @@ func readData() ([]time.Time, []float64) {
func releases() []chart.GridLine {
return []chart.GridLine{
- {Value: chart.TimeToFloat64(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.TimeToFloat64(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.TimeToFloat64(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, 1, 9, 30, 0, 0, time.UTC))},
+ {Value: chart.Time.ToFloat64(time.Date(2016, 8, 2, 9, 30, 0, 0, time.UTC))},
+ {Value: chart.Time.ToFloat64(time.Date(2016, 8, 3, 9, 30, 0, 0, time.UTC))},
+ {Value: chart.Time.ToFloat64(time.Date(2016, 8, 4, 9, 30, 0, 0, time.UTC))},
+ {Value: chart.Time.ToFloat64(time.Date(2016, 8, 5, 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",
NameStyle: chart.StyleShow(),
Style: chart.StyleShow(),
+ TickStyle: chart.Style{
+ TextRotationDegrees: 45.0,
+ },
},
XAxis: chart.XAxis{
- Style: chart.StyleShow(),
+ Style: chart.Style{
+ Show: true,
+ },
ValueFormatter: chart.TimeHourValueFormatter,
GridMajorStyle: chart.Style{
Show: true,
diff --git a/_examples/stock_analysis/main.go b/_examples/stock_analysis/main.go
index 3f17025..ddfa4b3 100644
--- a/_examples/stock_analysis/main.go
+++ b/_examples/stock_analysis/main.go
@@ -60,8 +60,8 @@ func drawChart(res http.ResponseWriter, req *http.Request) {
},
}
- res.Header().Set("Content-Type", "image/svg+xml")
- graph.Render(chart.SVG, res)
+ res.Header().Set("Content-Type", "image/png")
+ graph.Render(chart.PNG, res)
}
func xvalues() []time.Time {
diff --git a/_examples/text_rotation/main.go b/_examples/text_rotation/main.go
new file mode 100644
index 0000000..76bb2b0
--- /dev/null
+++ b/_examples/text_rotation/main.go
@@ -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)
+}
diff --git a/box.go b/box.go
index 0705749..98c1f17 100644
--- a/box.go
+++ b/box.go
@@ -1,6 +1,9 @@
package chart
-import "fmt"
+import (
+ "fmt"
+ "math"
+)
// Box represents the main 4 dimensions of a box.
type Box struct {
@@ -76,8 +79,8 @@ func (b Box) Height() int {
// Center returns the center of the box
func (b Box) Center() (x, y int) {
- w, h := b.Width(), b.Height()
- return b.Left + w>>1, b.Top + h>>1
+ w2, h2 := b.Width()>>1, b.Height()>>1
+ return b.Left + w2, b.Top + h2
}
// 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 maintains the original aspect ratio of the `other` 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
}
+
+// 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)
+}
diff --git a/box_test.go b/box_test.go
index ba3b1ed..89eafcf 100644
--- a/box_test.go
+++ b/box_test.go
@@ -143,3 +143,46 @@ func TestBoxShift(t *testing.T) {
assert.Equal(11, shifted.Right)
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())
+}
diff --git a/chart.go b/chart.go
index d53003d..236d714 100644
--- a/chart.go
+++ b/chart.go
@@ -102,12 +102,12 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
if c.hasAxes() {
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)
// do a second pass in case things haven't settled yet.
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)
}
@@ -320,7 +320,7 @@ func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueForm
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()
if c.XAxis.Style.Show {
axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks)
diff --git a/draw.go b/draw.go
index 25ca38c..e06462e 100644
--- a/draw.go
+++ b/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.
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()
@@ -168,6 +169,8 @@ func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly i
// Annotation draws an anotation with a renderer.
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
style.GetTextOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
textBox := r.MeasureText(label)
textWidth := textBox.Width()
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.
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.LineTo(b.Right, b.Top)
@@ -219,19 +223,45 @@ func (d draw) Box(r Renderer, b Box, s Style) {
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.
func (d draw) Text(r Renderer, text string, x, y int, style Style) {
style.GetTextOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
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.
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)
linesBox := Text.MeasureLines(r, lines, style)
- style.GetTextOptions().WriteToRenderer(r)
-
y := box.Top
switch style.GetTextVerticalAlign() {
@@ -252,7 +282,11 @@ func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
default:
tx = box.Left
}
- ty = y + lineBox.Height()
+ if style.TextRotationDegrees == 0 {
+ ty = y + lineBox.Height()
+ } else {
+ ty = y
+ }
d.Text(r, line, tx, ty, style)
y += lineBox.Height() + style.GetTextLineSpacing()
diff --git a/file_util.go b/file_util.go
new file mode 100644
index 0000000..7fd66bf
--- /dev/null
+++ b/file_util.go
@@ -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
+}
diff --git a/legend.go b/legend.go
index 6722a6d..20d4a04 100644
--- a/legend.go
+++ b/legend.go
@@ -54,9 +54,7 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
Bottom: legend.Top + legendPadding.Top,
}
- r.SetFont(legendStyle.GetFont())
- r.SetFontColor(legendStyle.GetFontColor())
- r.SetFontSize(legendStyle.GetFontSize())
+ legendStyle.GetTextOptions().WriteToRenderer(r)
// measure
labelCount := 0
@@ -79,6 +77,8 @@ func Legend(c *Chart, userDefaults ...Style) Renderable {
Draw.Box(r, legend, legendStyle)
+ legendStyle.GetTextOptions().WriteToRenderer(r)
+
ycursor := legendContent.Top
tx := legendContent.Left
legendCount := 0
diff --git a/market_hours_range.go b/market_hours_range.go
index 3b7c570..265cffa 100644
--- a/market_hours_range.go
+++ b/market_hours_range.go
@@ -33,12 +33,12 @@ func (mhr MarketHoursRange) IsZero() bool {
// GetMin returns the min value.
func (mhr MarketHoursRange) GetMin() float64 {
- return TimeToFloat64(mhr.Min)
+ return Time.ToFloat64(mhr.Min)
}
// GetMax returns the max value.
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.
@@ -52,13 +52,13 @@ func (mhr MarketHoursRange) GetEffectiveMax() time.Time {
// SetMin sets the min value.
func (mhr *MarketHoursRange) SetMin(min float64) {
- mhr.Min = Float64ToTime(min)
+ mhr.Min = Time.FromFloat64(min)
mhr.Min = mhr.Min.In(mhr.GetTimezone())
}
// SetMax sets the max value.
func (mhr *MarketHoursRange) SetMax(max float64) {
- mhr.Max = Float64ToTime(max)
+ mhr.Max = Time.FromFloat64(max)
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))
for index, t := range times {
ticks[index] = Tick{
- Value: TimeToFloat64(t),
+ Value: Time.ToFloat64(t),
Label: vf(t),
}
}
@@ -172,7 +172,7 @@ func (mhr MarketHoursRange) String() string {
// Translate maps a given value into the ContinuousRange space.
func (mhr MarketHoursRange) Translate(value float64) int {
- valueTime := Float64ToTime(value)
+ valueTime := Time.FromFloat64(value)
valueTimeEastern := valueTime.In(Date.Eastern())
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)
diff --git a/market_hours_range_test.go b/market_hours_range_test.go
index 14b20fb..45458c4 100644
--- a/market_hours_range_test.go
+++ b/market_hours_range_test.go
@@ -35,9 +35,9 @@ func TestMarketHoursRangeTranslate(t *testing.T) {
weds := time.Date(2016, 07, 20, 9, 30, 0, 0, Date.Eastern())
- assert.Equal(0, r.Translate(TimeToFloat64(r.Min)))
- assert.Equal(400, r.Translate(TimeToFloat64(weds)))
- assert.Equal(1000, r.Translate(TimeToFloat64(r.Max)))
+ assert.Equal(0, r.Translate(Time.ToFloat64(r.Min)))
+ assert.Equal(400, r.Translate(Time.ToFloat64(weds)))
+ assert.Equal(1000, r.Translate(Time.ToFloat64(r.Max)))
}
func TestMarketHoursRangeGetTicks(t *testing.T) {
@@ -67,6 +67,6 @@ func TestMarketHoursRangeGetTicks(t *testing.T) {
ticks := ra.GetTicks(r, defaults, TimeValueFormatter)
assert.NotEmpty(ticks)
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)
}
diff --git a/math.go b/math_util.go
similarity index 84%
rename from math.go
rename to math_util.go
index 77180e8..8c3f5da 100644
--- a/math.go
+++ b/math_util.go
@@ -19,16 +19,6 @@ const (
_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 (
// Math contains helper methods for common math operations.
Math = &mathUtil{}
@@ -144,6 +134,16 @@ func (m mathUtil) AbsInt(value int) int {
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.
func (m mathUtil) Sum(values ...float64) 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
-// by the radius and the angle.
-func (m mathUtil) CirclePoint(cx, cy int, radius, angleRadians float64) (x, y int) {
- x = cx + int(radius*math.Sin(angleRadians))
- y = cy - int(radius*math.Cos(angleRadians))
+// by the radius and the theta.
+func (m mathUtil) CirclePoint(cx, cy int, radius, thetaRadians float64) (x, y int) {
+ x = cx + int(radius*math.Sin(thetaRadians))
+ 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
}
diff --git a/math_test.go b/math_util_test.go
similarity index 89%
rename from math_test.go
rename to math_util_test.go
index a4c4006..c629af4 100644
--- a/math_test.go
+++ b/math_util_test.go
@@ -160,3 +160,25 @@ func TestRadianAdd(t *testing.T) {
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)
+}
diff --git a/raster_renderer.go b/raster_renderer.go
index 1b0cf96..326dcb5 100644
--- a/raster_renderer.go
+++ b/raster_renderer.go
@@ -28,11 +28,16 @@ type rasterRenderer struct {
i *image.RGBA
gc *drawing.RasterGraphicContext
- rotateRadians float64
+ rotateRadians *float64
s Style
}
+func (rr *rasterRenderer) ResetStyle() {
+ rr.s = Style{Font: rr.s.Font}
+ rr.ClearTextRotation()
+}
+
// GetDPI returns the dpi.
func (rr *rasterRenderer) GetDPI() float64 {
return rr.gc.GetDPI()
@@ -177,35 +182,40 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
t = 0
}
- return Box{
+ textBox := Box{
Top: int(math.Ceil(t)),
Left: int(math.Ceil(l)),
Right: int(math.Ceil(r)),
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.
func (rr *rasterRenderer) SetTextRotation(radians float64) {
- rr.rotateRadians = radians
+ rr.rotateRadians = &radians
}
func (rr *rasterRenderer) getCoords(x, y int) (xf, yf int) {
- if rr.rotateRadians == 0 {
+ if rr.rotateRadians == nil {
xf = x
yf = y
return
}
rr.gc.Translate(float64(x), float64(y))
- rr.gc.Rotate(rr.rotateRadians)
+ rr.gc.Rotate(*rr.rotateRadians)
return
}
// ClearTextRotation clears text rotation.
func (rr *rasterRenderer) ClearTextRotation() {
rr.gc.SetMatrixTransform(drawing.NewIdentityMatrix())
- rr.rotateRadians = 0
+ rr.rotateRadians = nil
}
// Save implements the interface method.
diff --git a/renderer.go b/renderer.go
index 2047a40..7eb06bb 100644
--- a/renderer.go
+++ b/renderer.go
@@ -9,6 +9,9 @@ import (
// Renderer represents the basic methods required to draw a chart.
type Renderer interface {
+ // ResetStyle should reset any style related settings on the renderer.
+ ResetStyle()
+
// GetDPI gets the DPI for the renderer.
GetDPI() float64
diff --git a/style.go b/style.go
index 9563066..d1db16b 100644
--- a/style.go
+++ b/style.go
@@ -33,6 +33,7 @@ type Style struct {
TextVerticalAlign TextVerticalAlign
TextWrap TextWrap
TextLineSpacing int
+ TextRotationDegrees float64 //0 is unset or normal
}
// IsZero returns if the object is set or not.
@@ -241,6 +242,16 @@ func (s Style) GetTextLineSpacing(defaults ...int) int {
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.
func (s Style) WriteToRenderer(r Renderer) {
r.SetStrokeColor(s.GetStrokeColor())
@@ -250,6 +261,11 @@ func (s Style) WriteToRenderer(r Renderer) {
r.SetFont(s.GetFont())
r.SetFontColor(s.GetFontColor())
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.
@@ -281,6 +297,7 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
+ final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees)
return
}
@@ -320,5 +337,6 @@ func (s Style) GetTextOptions() Style {
TextVerticalAlign: s.TextVerticalAlign,
TextWrap: s.TextWrap,
TextLineSpacing: s.TextLineSpacing,
+ TextRotationDegrees: s.TextRotationDegrees,
}
}
diff --git a/time_series.go b/time_series.go
index df779eb..025e86a 100644
--- a/time_series.go
+++ b/time_series.go
@@ -30,14 +30,14 @@ func (ts TimeSeries) Len() int {
// GetValue gets a value at a given index.
func (ts TimeSeries) GetValue(index int) (x, y float64) {
- x = TimeToFloat64(ts.XValues[index])
+ x = Time.ToFloat64(ts.XValues[index])
y = ts.YValues[index]
return
}
// GetLastValue gets the last value.
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]
return
}
diff --git a/time_util.go b/time_util.go
new file mode 100644
index 0000000..8937546
--- /dev/null
+++ b/time_util.go
@@ -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))
+}
diff --git a/value_formatter_test.go b/value_formatter_test.go
index 0675914..3bd8409 100644
--- a/value_formatter_test.go
+++ b/value_formatter_test.go
@@ -11,7 +11,7 @@ func TestTimeValueFormatterWithFormat(t *testing.T) {
assert := assert.New(t)
d := time.Now()
- di := TimeToFloat64(d)
+ di := Time.ToFloat64(d)
df := float64(di)
s := TimeValueFormatterWithFormat(d, DefaultDateFormat)
diff --git a/vector_renderer.go b/vector_renderer.go
index d65e497..c36f065 100644
--- a/vector_renderer.go
+++ b/vector_renderer.go
@@ -32,11 +32,15 @@ type vectorRenderer struct {
b *bytes.Buffer
c *canvas
s *Style
- r float64
p []string
fc *font.Drawer
}
+func (vr *vectorRenderer) ResetStyle() {
+ vr.s = &Style{Font: vr.s.Font}
+ vr.fc = nil
+}
+
// GetDPI returns the dpi.
func (vr *vectorRenderer) GetDPI() float64 {
return vr.dpi
@@ -168,18 +172,22 @@ 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
+ }
+ box = box.Corners().Rotate(Math.RadiansToDegrees(*vr.c.textTheta)).Box()
}
return
}
// SetTextRotation sets the text rotation.
func (vr *vectorRenderer) SetTextRotation(radians float64) {
- vr.c.r = radians
+ vr.c.textTheta = &radians
}
// ClearTextRotation clears the text rotation.
func (vr *vectorRenderer) ClearTextRotation() {
- vr.c.r = 0
+ vr.c.textTheta = nil
}
// Save saves the renderer's contents to a writer.
@@ -196,11 +204,11 @@ func newCanvas(w io.Writer) *canvas {
}
type canvas struct {
- w io.Writer
- dpi float64
- r float64
- width int
- height int
+ w io.Writer
+ dpi float64
+ textTheta *float64
+ width int
+ height int
}
func (c *canvas) Start(width, 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) {
- if c.r == 0 {
+ if c.textTheta == nil {
c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body)))
} 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(`%s`, x, y, c.styleAsSVG(style), transform, body)))
}
}
diff --git a/xaxis.go b/xaxis.go
index f1326db..0728e9b 100644
--- a/xaxis.go
+++ b/xaxis.go
@@ -7,13 +7,15 @@ import (
// XAxis represents the horizontal axis.
type XAxis struct {
- Name string
- NameStyle Style
+ Name string
+ NameStyle Style
+
Style Style
ValueFormatter ValueFormatter
Range Range
- Ticks []Tick
+ TickStyle Style
+ Ticks []Tick
TickPosition TickPosition
GridLines []GridLine
@@ -68,20 +70,20 @@ func (xa XAxis) GetGridLines(ticks []Tick) []GridLine {
// Measure returns the bounds of the axis.
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))
tp := xa.GetTickPosition()
+ var ltx, rtx int
+ var tx, ty int
var left, right, bottom = math.MaxInt32, 0, 0
for index, t := range ticks {
v := t.Value
- tickStyle.GetTextOptions().WriteToRenderer(r)
- tb := r.MeasureText(t.Label)
+ tb := Draw.MeasureText(r, t.Label, tickStyle.GetTextOptions())
- var ltx, rtx int
- tx := canvasBox.Left + ra.Translate(v)
- ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
+ tx = canvasBox.Left + ra.Translate(v)
+ ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
switch tp {
case TickPositionUnderTick, TickPositionUnset:
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 {
- tb := r.MeasureText(xa.Name)
+ tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults))
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
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)
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.Stroke()
- tickStyle.GetTextOptions().WriteToRenderer(r)
- tb := r.MeasureText(t.Label)
+ tickWithAxisStyle := xa.TickStyle.InheritFrom(xa.Style.InheritFrom(defaults))
+ tb := Draw.MeasureText(r, t.Label, tickWithAxisStyle)
switch tp {
case TickPositionUnderTick, TickPositionUnset:
- ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
- r.Text(t.Label, tx-tb.Width()>>1, ty)
+ if tickStyle.TextRotationDegrees == 0 {
+ tx = tx - tb.Width()>>1
+ ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
+ } else {
+ ty = canvasBox.Bottom + (2 * DefaultXAxisMargin)
+ }
+ Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle)
maxTextHeight = Math.MaxInt(maxTextHeight, tb.Height())
break
case TickPositionBetweenTicks:
if index > 0 {
llx := ra.Translate(ticks[index-1].Value)
ltx := canvasBox.Left + llx
- finalTickStyle := tickStyle.InheritFrom(Style{TextHorizontalAlign: TextHorizontalAlignCenter})
+ finalTickStyle := tickWithAxisStyle.InheritFrom(Style{TextHorizontalAlign: TextHorizontalAlignCenter})
+
Draw.TextWithin(r, t.Label, Box{
Left: ltx,
Right: tx,
Top: canvasBox.Bottom + DefaultXAxisMargin,
- Bottom: canvasBox.Bottom + DefaultXAxisMargin + tb.Height(),
+ Bottom: canvasBox.Bottom + DefaultXAxisMargin,
}, 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)
if xa.NameStyle.Show && len(xa.Name) > 0 {
- nameStyle.GetTextOptions().WriteToRenderer(r)
- tb := r.MeasureText(xa.Name)
+ tb := Draw.MeasureText(r, xa.Name, nameStyle)
tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1)
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 {
diff --git a/yaxis.go b/yaxis.go
index fe7ab44..0695276 100644
--- a/yaxis.go
+++ b/yaxis.go
@@ -20,9 +20,10 @@ type YAxis struct {
ValueFormatter ValueFormatter
Range Range
+ TickStyle Style
Ticks []Tick
- GridLines []GridLine
+ GridLines []GridLine
GridMajorStyle Style
GridMinorStyle Style
}
@@ -42,6 +43,11 @@ func (ya YAxis) GetStyle() Style {
return ya.Style
}
+// GetTickStyle returns the tick style.
+func (ya YAxis) GetTickStyle() Style {
+ return ya.TickStyle
+}
+
// GetTicks returns the ticks for a series.
// The coalesce priority is:
// - 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.
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))
var tx int
@@ -79,6 +83,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
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 maxTextHeight int
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)
tb := r.MeasureText(t.Label)
+ tbh2 := tb.Height() >> 1
finalTextX := tx
if ya.AxisType == YAxisSecondary {
finalTextX = tx - tb.Width()
}
- if tb.Height() > maxTextHeight {
- maxTextHeight = tb.Height()
- }
+ maxTextHeight = Math.MaxInt(tb.Height(), maxTextHeight)
if ya.AxisType == YAxisPrimary {
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)
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 {
@@ -120,11 +125,12 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
// Render renders the axis.
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))
- sw := ya.Style.GetStrokeWidth(defaults.StrokeWidth)
+ sw := tickStyle.GetStrokeWidth(defaults.StrokeWidth)
var lx int
var tx int
@@ -141,23 +147,30 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, tick
r.Stroke()
var maxTextWidth int
+ var finalTextX, finalTextY int
for _, t := range ticks {
v := t.Value
ly := canvasBox.Bottom - ra.Translate(v)
- tb := r.MeasureText(t.Label)
+ tb := Draw.MeasureText(r, t.Label, tickStyle)
if tb.Width() > maxTextWidth {
maxTextWidth = tb.Width()
}
- finalTextX := tx
- finalTextY := ly + tb.Height()>>1
if ya.AxisType == YAxisSecondary {
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)
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.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 {
nameStyle.GetTextOptions().WriteToRenderer(r)
-
- r.SetTextRotation(Math.DegreesToRadians(90))
-
- tb := r.MeasureText(ya.Name)
+ tb := Draw.MeasureText(r, ya.Name, nameStyle)
var tx int
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)
}
- ty := canvasBox.Bottom - (canvasBox.Height()>>1 + tb.Width()>>1)
+ ty := canvasBox.Top + (canvasBox.Height()>>1 + tb.Width()>>1)
- r.Text(ya.Name, tx, ty)
- r.ClearTextRotation()
+ Draw.Text(r, ya.Name, tx, ty, nameStyle)
}
if ya.Zero.Style.Show {