slightly more rigorous bounds checking and auto-fit

This commit is contained in:
Will Charczuk 2016-07-12 16:47:52 -07:00
parent bba75e5d4c
commit 28f01842de
13 changed files with 475 additions and 195 deletions

View File

@ -45,8 +45,6 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
Left: 5, Left: 5,
Right: 105, Right: 105,
Bottom: 105, Bottom: 105,
Height: 100,
Width: 100,
} }
sd := Style{ sd := Style{
FontSize: 10.0, FontSize: 10.0,
@ -100,8 +98,6 @@ func TestAnnotationSeriesRender(t *testing.T) {
Left: 5, Left: 5,
Right: 105, Right: 105,
Bottom: 105, Bottom: 105,
Height: 100,
Width: 100,
} }
sd := Style{ sd := Style{
FontSize: 10.0, FontSize: 10.0,

172
box.go
View File

@ -8,9 +8,6 @@ type Box struct {
Left int Left int
Right int Right int
Bottom int Bottom int
Height int
Width int
} }
// IsZero returns if the box is set or not. // IsZero returns if the box is set or not.
@ -66,3 +63,172 @@ func (b Box) GetBottom(defaults ...int) int {
} }
return b.Bottom return b.Bottom
} }
// Width returns the width
func (b Box) Width() int {
return AbsInt(b.Right - b.Left)
}
// Height returns the height
func (b Box) Height() int {
return AbsInt(b.Bottom - b.Top)
}
// 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
}
// Aspect returns the aspect ratio of the box.
func (b Box) Aspect() float64 {
return float64(b.Width()) / float64(b.Height())
}
// Clone returns a new copy of the box.
func (b Box) Clone() Box {
return Box{
Top: b.Top,
Left: b.Left,
Right: b.Right,
Bottom: b.Bottom,
}
}
// IsBiggerThan returns if a box is bigger than another box.
func (b Box) IsBiggerThan(other Box) bool {
return b.Top < other.Top ||
b.Bottom > other.Bottom ||
b.Left < other.Left ||
b.Right > other.Right
}
// IsSmallerThan returns if a box is smaller than another box.
func (b Box) IsSmallerThan(other Box) bool {
return b.Top > other.Top &&
b.Bottom < other.Bottom &&
b.Left > other.Left &&
b.Right < other.Right
}
// Equals returns if the box equals another box.
func (b Box) Equals(other Box) bool {
return b.Top == other.Top &&
b.Left == other.Left &&
b.Right == other.Right &&
b.Bottom == other.Bottom
}
// Grow grows a box based on another box.
func (b Box) Grow(other Box) Box {
return Box{
Top: MinInt(b.Top, other.Top),
Left: MinInt(b.Left, other.Left),
Right: MaxInt(b.Right, other.Right),
Bottom: MaxInt(b.Bottom, other.Bottom),
}
}
// Shift pushes a box by x,y.
func (b Box) Shift(x, y int) Box {
return Box{
Top: b.Top + y,
Left: b.Left + x,
Right: b.Right + x,
Bottom: b.Bottom + y,
}
}
// 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.
func (b Box) Fit(other Box) Box {
ba := b.Aspect()
oa := other.Aspect()
if oa == ba {
return b.Clone()
}
bw, bh := float64(b.Width()), float64(b.Height())
bw2 := int(bw) >> 1
bh2 := int(bh) >> 1
if oa > ba { // ex. 16:9 vs. 4:3
var noh2 int
if oa > 1.0 {
noh2 = int(bw/oa) >> 1
} else {
noh2 = int(bh*oa) >> 1
}
return Box{
Top: (b.Top + bh2) - noh2,
Left: b.Left,
Right: b.Right,
Bottom: (b.Top + bh2) + noh2,
}
}
var now2 int
if oa > 1.0 {
now2 = int(bh/oa) >> 1
} else {
now2 = int(bw*oa) >> 1
}
return Box{
Top: b.Top,
Left: (b.Left + bw2) - now2,
Right: (b.Left + bw2) + now2,
Bottom: b.Bottom,
}
}
// Constrain is similar to `Fit` except that it will work
// more literally like the opposite of grow.
func (b Box) Constrain(other Box) Box {
newBox := b.Clone()
if other.Top < b.Top {
delta := b.Top - other.Top
newBox.Top = other.Top + delta
}
if other.Left < b.Left {
delta := b.Left - other.Left
newBox.Left = other.Left + delta
}
if other.Right > b.Right {
delta := other.Right - b.Right
newBox.Right = other.Right - delta
}
if other.Bottom > b.Bottom {
delta := other.Bottom - b.Bottom
newBox.Bottom = other.Bottom - delta
}
return newBox
}
// OuterConstrain is similar to `Constraint` with the difference
// that it applies corrections
func (b Box) OuterConstrain(bounds, other Box) Box {
newBox := b.Clone()
if other.Top < bounds.Top {
delta := bounds.Top - other.Top
newBox.Top = b.Top + delta
}
if other.Left < bounds.Left {
delta := bounds.Left - other.Left
newBox.Left = b.Left + delta
}
if other.Right > bounds.Right {
delta := other.Right - bounds.Right
newBox.Right = b.Right - delta
}
if other.Bottom > bounds.Bottom {
delta := other.Bottom - bounds.Bottom
newBox.Bottom = b.Bottom - delta
}
return newBox
}

87
box_test.go Normal file
View File

@ -0,0 +1,87 @@
package chart
import (
"math"
"testing"
"github.com/blendlabs/go-assert"
)
func TestBoxClone(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
b := a.Clone()
assert.True(a.Equals(b))
assert.True(b.Equals(a))
}
func TestBoxEquals(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
b := Box{Top: 10, Left: 10, Right: 30, Bottom: 30}
c := Box{Top: 5, Left: 5, Right: 15, Bottom: 15}
assert.True(a.Equals(a))
assert.True(a.Equals(c))
assert.True(c.Equals(a))
assert.False(a.Equals(b))
assert.False(c.Equals(b))
assert.False(b.Equals(a))
assert.False(b.Equals(c))
}
func TestBoxIsBiggerThan(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
assert.True(a.IsBiggerThan(b))
assert.False(a.IsBiggerThan(c))
assert.True(c.IsBiggerThan(a))
}
func TestBoxIsSmallerThan(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 5, Left: 5, Right: 25, Bottom: 25}
b := Box{Top: 10, Left: 10, Right: 20, Bottom: 20} // only half bigger
c := Box{Top: 1, Left: 1, Right: 30, Bottom: 30} //bigger
assert.False(a.IsSmallerThan(b))
assert.True(a.IsSmallerThan(c))
assert.False(c.IsSmallerThan(a))
}
func TestBoxGrow(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 1, Left: 2, Right: 15, Bottom: 15}
b := Box{Top: 4, Left: 5, Right: 30, Bottom: 35}
c := a.Grow(b)
assert.False(c.Equals(b))
assert.False(c.Equals(a))
assert.Equal(1, c.Top)
assert.Equal(2, c.Left)
assert.Equal(30, c.Right)
assert.Equal(35, c.Bottom)
}
func TestBoxFit(t *testing.T) {
assert := assert.New(t)
a := Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
b := Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
c := Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
fab := a.Fit(b)
assert.Equal(a.Left, fab.Left)
assert.Equal(a.Right, fab.Right)
assert.True(fab.Top < fab.Bottom)
assert.True(fab.Left < fab.Right)
assert.True(math.Abs(b.Aspect()-fab.Aspect()) < 0.02)
fac := a.Fit(c)
assert.Equal(a.Top, fac.Top)
assert.Equal(a.Bottom, fac.Bottom)
assert.True(math.Abs(c.Aspect()-fac.Aspect()) < 0.02)
}

217
chart.go
View File

@ -99,13 +99,14 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
c.drawSeries(r, canvasBox, xr, yr, yra, series, index) c.drawSeries(r, canvasBox, xr, yr, yra, series, index)
} }
c.drawTitle(r) c.drawTitle(r)
return r.Save(w) return r.Save(w)
} }
func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) { func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
var globalMinX, globalMaxX float64 = math.MaxFloat64, 0 var minx, maxx float64 = math.MaxFloat64, 0
var globalMinY, globalMaxY float64 = math.MaxFloat64, 0 var miny, maxy float64 = math.MaxFloat64, 0
var globalMinYA, globalMaxYA float64 = math.MaxFloat64, 0 var minya, maxya float64 = math.MaxFloat64, 0
for _, s := range c.Series { for _, s := range c.Series {
seriesAxis := s.GetYAxis() seriesAxis := s.GetYAxis()
@ -113,26 +114,16 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
seriesLength := vp.Len() seriesLength := vp.Len()
for index := 0; index < seriesLength; index++ { for index := 0; index < seriesLength; index++ {
vx, vy := vp.GetValue(index) vx, vy := vp.GetValue(index)
if globalMinX > vx {
globalMinX = vx minx = math.Min(minx, vx)
} maxx = math.Max(maxx, vx)
if globalMaxX < vx {
globalMaxX = vx
}
if seriesAxis == YAxisPrimary { if seriesAxis == YAxisPrimary {
if globalMinY > vy { miny = math.Min(miny, vy)
globalMinY = vy maxy = math.Max(maxy, vy)
}
if globalMaxY < vy {
globalMaxY = vy
}
} else if seriesAxis == YAxisSecondary { } else if seriesAxis == YAxisSecondary {
if globalMinYA > vy { minya = math.Min(minya, vy)
globalMinYA = vy maxya = math.Max(maxya, vy)
}
if globalMaxYA < vy {
globalMaxYA = vy
}
} }
} }
} }
@ -142,17 +133,16 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
xrange.Min = c.XAxis.Range.Min xrange.Min = c.XAxis.Range.Min
xrange.Max = c.XAxis.Range.Max xrange.Max = c.XAxis.Range.Max
} else { } else {
xrange.Min = globalMinX xrange.Min = minx
xrange.Max = globalMaxX xrange.Max = maxx
//xrange.Min, xrange.Max = xrange.GetRoundedRangeBounds()
} }
if !c.YAxis.Range.IsZero() { if !c.YAxis.Range.IsZero() {
yrange.Min = c.YAxis.Range.Min yrange.Min = c.YAxis.Range.Min
yrange.Max = c.YAxis.Range.Max yrange.Max = c.YAxis.Range.Max
} else { } else {
yrange.Min = globalMinY yrange.Min = miny
yrange.Max = globalMaxY yrange.Max = maxy
yrange.Min, yrange.Max = yrange.GetRoundedRangeBounds() yrange.Min, yrange.Max = yrange.GetRoundedRangeBounds()
} }
@ -160,8 +150,8 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
yrangeAlt.Min = c.YAxisSecondary.Range.Min yrangeAlt.Min = c.YAxisSecondary.Range.Min
yrangeAlt.Max = c.YAxisSecondary.Range.Max yrangeAlt.Max = c.YAxisSecondary.Range.Max
} else { } else {
yrangeAlt.Min = globalMinYA yrangeAlt.Min = minya
yrangeAlt.Max = globalMaxYA yrangeAlt.Max = maxya
yrangeAlt.Min, yrangeAlt.Max = yrangeAlt.GetRoundedRangeBounds() yrangeAlt.Min, yrangeAlt.Max = yrangeAlt.GetRoundedRangeBounds()
} }
@ -169,19 +159,17 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
} }
func (c Chart) getDefaultCanvasBox() Box { func (c Chart) getDefaultCanvasBox() Box {
dpt := c.Background.Padding.GetTop(DefaultBackgroundPadding.Top)
dpl := c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left) dpl := c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left)
dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right) dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom) dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
cb := Box{ return Box{
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top), Top: dpt,
Left: dpl, Left: dpl,
Right: c.Width - dpr, Right: c.Width - dpr,
Bottom: c.Height - dpb, Bottom: c.Height - dpb,
} }
cb.Height = cb.Bottom - cb.Top
cb.Width = cb.Right - cb.Left
return cb
} }
func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) { func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
@ -227,71 +215,30 @@ func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueForm
} }
func (c Chart) getAxisAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box { func (c Chart) getAxisAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
axesMinX, axesMaxX, axesMinY, axesMaxY := math.MaxInt32, 0, math.MaxInt32, 0 axesOuterBox := canvasBox.Clone()
if c.XAxis.Style.Show { if c.XAxis.Style.Show {
axesBounds := c.XAxis.Measure(r, canvasBox, xr, xticks) axesBounds := c.XAxis.Measure(r, canvasBox, xr, xticks)
axesMinY = MinInt(axesMinX, axesBounds.Top) axesOuterBox = axesOuterBox.Grow(axesBounds)
axesMinX = MinInt(axesMinY, axesBounds.Left)
axesMaxX = MaxInt(axesMaxX, axesBounds.Right)
axesMaxY = MaxInt(axesMaxY, axesBounds.Bottom)
} }
if c.YAxis.Style.Show { if c.YAxis.Style.Show {
axesBounds := c.YAxis.Measure(r, canvasBox, yr, yticks) axesBounds := c.YAxis.Measure(r, canvasBox, yr, yticks)
axesMinY = MinInt(axesMinX, axesBounds.Top) axesOuterBox = axesOuterBox.Grow(axesBounds)
axesMinX = MinInt(axesMinY, axesBounds.Left)
axesMaxX = MaxInt(axesMaxX, axesBounds.Right)
axesMaxY = MaxInt(axesMaxY, axesBounds.Bottom)
} }
if c.YAxisSecondary.Style.Show { if c.YAxisSecondary.Style.Show {
axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, yticksAlt) axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, yticksAlt)
axesMinY = MinInt(axesMinX, axesBounds.Top) axesOuterBox = axesOuterBox.Grow(axesBounds)
axesMinX = MinInt(axesMinY, axesBounds.Left)
axesMaxX = MaxInt(axesMaxX, axesBounds.Right)
axesMaxY = MaxInt(axesMaxY, axesBounds.Bottom)
}
newBox := Box{
Top: canvasBox.Top,
Left: canvasBox.Left,
Right: canvasBox.Right,
Bottom: canvasBox.Bottom,
} }
if axesMinY < 0 { return canvasBox.OuterConstrain(c.asBox(), axesOuterBox)
// figure out how much top padding to add
delta := -1 * axesMinY
newBox.Top = canvasBox.Top + delta
}
if axesMinX < 0 {
// figure out how much left padding to add
delta := -1 * axesMinX
newBox.Left = canvasBox.Left + delta
}
if axesMaxX > c.Width {
// figure out how much right padding to add
delta := axesMaxX - c.Width
newBox.Right = canvasBox.Right - delta
}
if axesMaxY > c.Height {
//figure out how much bottom padding to add
delta := axesMaxY - c.Height
newBox.Bottom = canvasBox.Bottom - delta
}
newBox.Height = newBox.Bottom - newBox.Top
newBox.Width = newBox.Right - newBox.Left
return newBox
} }
func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (xr2, yr2, yra2 Range) { func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (xr2, yr2, yra2 Range) {
xr2.Min, xr2.Max = xr.Min, xr.Max xr2.Min, xr2.Max = xr.Min, xr.Max
xr2.Domain = canvasBox.Width xr2.Domain = canvasBox.Width()
yr2.Min, yr2.Max = yr.Min, yr.Max yr2.Min, yr2.Max = yr.Min, yr.Max
yr2.Domain = canvasBox.Height yr2.Domain = canvasBox.Height()
yra2.Min, yra2.Max = yra.Min, yra.Max yra2.Min, yra2.Max = yra.Min, yra.Max
yra2.Domain = canvasBox.Height yra2.Domain = canvasBox.Height()
return return
} }
@ -307,11 +254,11 @@ func (c Chart) hasAnnotationSeries() bool {
} }
func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xf, yf, yfa ValueFormatter) Box { func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xf, yf, yfa ValueFormatter) Box {
annotationMinX, annotationMaxX, annotationMinY, annotationMaxY := math.MaxInt32, 0, math.MaxInt32, 0 annotationSeriesBox := canvasBox.Clone()
for seriesIndex, s := range c.Series { for seriesIndex, s := range c.Series {
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries { if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
if as.Style.Show { if as.Style.Show {
style := c.getSeriesStyleDefaults(seriesIndex) style := c.seriesStyleDefaults(seriesIndex)
var annotationBounds Box var annotationBounds Box
if as.YAxis == YAxisPrimary { if as.YAxis == YAxisPrimary {
annotationBounds = as.Measure(r, canvasBox, xr, yr, style) annotationBounds = as.Measure(r, canvasBox, xr, yr, style)
@ -319,74 +266,28 @@ func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr,
annotationBounds = as.Measure(r, canvasBox, xr, yra, style) annotationBounds = as.Measure(r, canvasBox, xr, yra, style)
} }
annotationMinY = MinInt(annotationMinY, annotationBounds.Top) annotationSeriesBox = annotationSeriesBox.Grow(annotationBounds)
annotationMinX = MinInt(annotationMinX, annotationBounds.Left)
annotationMaxX = MaxInt(annotationMaxX, annotationBounds.Right)
annotationMaxY = MaxInt(annotationMaxY, annotationBounds.Bottom)
} }
} }
} }
newBox := Box{ return canvasBox.OuterConstrain(c.asBox(), annotationSeriesBox)
Top: canvasBox.Top,
Left: canvasBox.Left,
Right: canvasBox.Right,
Bottom: canvasBox.Bottom,
}
if annotationMinY < 0 {
// figure out how much top padding to add
delta := -1 * annotationMinY
newBox.Top = canvasBox.Top + delta
}
if annotationMinX < 0 {
// figure out how much left padding to add
delta := -1 * annotationMinX
newBox.Left = canvasBox.Left + delta
}
if annotationMaxX > c.Width {
// figure out how much right padding to add
delta := annotationMaxX - c.Width
newBox.Right = canvasBox.Right - delta
}
if annotationMaxY > c.Height {
//figure out how much bottom padding to add
delta := annotationMaxY - c.Height
newBox.Bottom = canvasBox.Bottom - delta
}
newBox.Height = newBox.Bottom - newBox.Top
newBox.Width = newBox.Right - newBox.Left
return newBox
} }
func (c Chart) drawBackground(r Renderer) { func (c Chart) drawBackground(r Renderer) {
r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor)) DrawBox(r, Box{Right: c.Width, Bottom: c.Height}, c.Canvas.WithDefaultsFrom(Style{
r.SetStrokeColor(c.Background.GetStrokeColor(DefaultBackgroundStrokeColor)) FillColor: DefaultBackgroundColor,
r.SetStrokeWidth(c.Background.GetStrokeWidth(DefaultStrokeWidth)) StrokeColor: DefaultBackgroundStrokeColor,
r.MoveTo(0, 0) StrokeWidth: DefaultStrokeWidth,
r.LineTo(c.Width, 0) }))
r.LineTo(c.Width, c.Height)
r.LineTo(0, c.Height)
r.LineTo(0, 0)
r.Close()
r.FillStroke()
} }
func (c Chart) drawCanvas(r Renderer, canvasBox Box) { func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor)) DrawBox(r, canvasBox, c.Canvas.WithDefaultsFrom(Style{
r.SetStrokeColor(c.Canvas.GetStrokeColor(DefaultCanvasStrokColor)) FillColor: DefaultCanvasColor,
r.SetStrokeWidth(c.Canvas.GetStrokeWidth(DefaultStrokeWidth)) StrokeColor: DefaultCanvasStrokeColor,
r.MoveTo(canvasBox.Left, canvasBox.Top) StrokeWidth: DefaultStrokeWidth,
r.LineTo(canvasBox.Right, canvasBox.Top) }))
r.LineTo(canvasBox.Right, canvasBox.Bottom)
r.LineTo(canvasBox.Left, canvasBox.Bottom)
r.LineTo(canvasBox.Left, canvasBox.Top)
r.Close()
r.FillStroke()
} }
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) { func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
@ -401,21 +302,11 @@ func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Ran
} }
} }
func (c Chart) getSeriesStyleDefaults(seriesIndex int) Style {
strokeColor := GetDefaultSeriesStrokeColor(seriesIndex)
return Style{
StrokeColor: strokeColor,
StrokeWidth: DefaultStrokeWidth,
Font: c.Font,
FontSize: DefaultFontSize,
}
}
func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) { func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) {
if s.GetYAxis() == YAxisPrimary { if s.GetYAxis() == YAxisPrimary {
s.Render(r, canvasBox, xrange, yrange, c.getSeriesStyleDefaults(seriesIndex)) s.Render(r, canvasBox, xrange, yrange, c.seriesStyleDefaults(seriesIndex))
} else if s.GetYAxis() == YAxisSecondary { } else if s.GetYAxis() == YAxisSecondary {
s.Render(r, canvasBox, xrange, yrangeAlt, c.getSeriesStyleDefaults(seriesIndex)) s.Render(r, canvasBox, xrange, yrangeAlt, c.seriesStyleDefaults(seriesIndex))
} }
} }
@ -428,8 +319,8 @@ func (c Chart) drawTitle(r Renderer) {
textBox := r.MeasureText(c.Title) textBox := r.MeasureText(c.Title)
textWidth := textBox.Width textWidth := textBox.Width()
textHeight := textBox.Height textHeight := textBox.Height()
titleX := (c.Width >> 1) - (textWidth >> 1) titleX := (c.Width >> 1) - (textWidth >> 1)
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
@ -437,3 +328,17 @@ func (c Chart) drawTitle(r Renderer) {
r.Text(c.Title, titleX, titleY) r.Text(c.Title, titleX, titleY)
} }
} }
func (c Chart) seriesStyleDefaults(seriesIndex int) Style {
strokeColor := GetDefaultSeriesStrokeColor(seriesIndex)
return Style{
StrokeColor: strokeColor,
StrokeWidth: DefaultStrokeWidth,
Font: c.Font,
FontSize: DefaultFontSize,
}
}
func (c Chart) asBox() Box {
return Box{Right: c.Width, Bottom: c.Height}
}

View File

@ -67,7 +67,7 @@ var (
DefaultCanvasColor = drawing.Color{R: 255, G: 255, B: 255, A: 255} DefaultCanvasColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// DefaultCanvasStrokColor is the default chart canvas stroke color. // DefaultCanvasStrokColor is the default chart canvas stroke color.
// It is equivalent to css color:white. // It is equivalent to css color:white.
DefaultCanvasStrokColor = drawing.Color{R: 255, G: 255, B: 255, A: 255} DefaultCanvasStrokeColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
// DefaultTextColor is the default chart text color. // DefaultTextColor is the default chart text color.
// It is equivalent to #333333. // It is equivalent to #333333.
DefaultTextColor = drawing.Color{R: 51, G: 51, B: 51, A: 255} DefaultTextColor = drawing.Color{R: 51, G: 51, B: 51, A: 255}

View File

@ -51,7 +51,9 @@ func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label str
r.SetFont(s.GetFont()) r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize)) r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
textBox := r.MeasureText(label) textBox := r.MeasureText(label)
halfTextHeight := textBox.Height >> 1 textWidth := textBox.Width()
textHeight := textBox.Height()
halfTextHeight := textHeight >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top) pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left) pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
@ -61,7 +63,7 @@ func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label str
strokeWidth := s.GetStrokeWidth() strokeWidth := s.GetStrokeWidth()
top := ly - (pt + halfTextHeight) top := ly - (pt + halfTextHeight)
right := lx + pl + pr + textBox.Width + DefaultAnnotationDeltaWidth + int(strokeWidth) right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
bottom := ly + (pb + halfTextHeight) bottom := ly + (pb + halfTextHeight)
return Box{ return Box{
@ -69,8 +71,6 @@ func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label str
Left: lx, Left: lx,
Right: right, Right: right,
Bottom: bottom, Bottom: bottom,
Width: right - lx,
Height: bottom - top,
} }
} }
@ -79,7 +79,8 @@ func DrawAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string
r.SetFont(s.GetFont()) r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize)) r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
textBox := r.MeasureText(label) textBox := r.MeasureText(label)
halfTextHeight := textBox.Height >> 1 textWidth := textBox.Width()
halfTextHeight := textBox.Height() >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top) pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left) pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
@ -92,10 +93,10 @@ func DrawAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string
ltx := lx + DefaultAnnotationDeltaWidth ltx := lx + DefaultAnnotationDeltaWidth
lty := ly - (pt + halfTextHeight) lty := ly - (pt + halfTextHeight)
rtx := lx + pl + pr + textBox.Width + DefaultAnnotationDeltaWidth rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rty := ly - (pt + halfTextHeight) rty := ly - (pt + halfTextHeight)
rbx := lx + pl + pr + textBox.Width + DefaultAnnotationDeltaWidth rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rby := ly + (pb + halfTextHeight) rby := ly + (pb + halfTextHeight)
lbx := lx + DefaultAnnotationDeltaWidth lbx := lx + DefaultAnnotationDeltaWidth
@ -131,3 +132,27 @@ func DrawBox(r Renderer, b Box, s Style) {
r.LineTo(b.Left, b.Top) r.LineTo(b.Left, b.Top)
r.FillStroke() r.FillStroke()
} }
// DrawText draws text with a given style.
func DrawText(r Renderer, text string, x, y int, s Style) {
r.SetFillColor(s.GetFillColor())
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize())
r.Text(text, x, y)
}
// DrawTextCentered draws text with a given style centered.
func DrawTextCentered(r Renderer, text string, x, y int, s Style) {
r.SetFillColor(s.GetFillColor())
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize())
tb := r.MeasureText(text)
tx := x - (tb.Width() >> 1)
ty := y - (tb.Height() >> 1)
r.Text(text, tx, ty)
}

View File

@ -162,8 +162,6 @@ func (rr *rasterRenderer) MeasureText(body string) Box {
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)),
Width: int(math.Ceil(r - l)),
Height: int(math.Ceil(b - t)),
} }
} }

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
@ -8,6 +9,7 @@ import (
"time" "time"
"github.com/wcharczuk/go-chart" "github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
"github.com/wcharczuk/go-web" "github.com/wcharczuk/go-web"
) )
@ -36,7 +38,7 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
c := chart.Chart{ c := chart.Chart{
Title: "A Test Chart", Title: "A Test Chart",
TitleStyle: chart.Style{ TitleStyle: chart.Style{
Show: true, Show: false,
}, },
Width: 1024, Width: 1024,
Height: 400, Height: 400,
@ -91,6 +93,104 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
return nil return nil
} }
func boxHandler(rc *web.RequestContext) web.ControllerResult {
r, err := chart.PNG(1024, 1024)
if err != nil {
rc.API().InternalError(err)
}
f, err := chart.GetDefaultFont()
if err != nil {
return rc.API().InternalError(err)
}
//1:1 128wx128h @ 64,64
a := chart.Box{Top: 64, Left: 64, Right: 192, Bottom: 192}
// 3:2 256x170 @ 16, 16
//b := chart.Box{Top: 16, Left: 16, Right: 256, Bottom: 170}
// 2:3 170x256 @ 16, 16
c := chart.Box{Top: 16, Left: 16, Right: 170, Bottom: 256}
//fitb := a.Fit(b)
fitc := a.Fit(c)
//growb := a.Grow(b)
//growc := a.Grow(c)
//grow := a.Grow(b).Grow(c)
conc := a.Constrain(c)
boxStyle := chart.Style{
StrokeColor: drawing.ColorBlack,
StrokeWidth: 1.0,
Font: f,
FontSize: 18.0,
}
computedBoxStyle := chart.Style{
StrokeColor: drawing.ColorRed,
StrokeWidth: 1.0,
Font: f,
FontSize: 18.0,
}
chart.DrawBox(r, a, boxStyle)
//chart.DrawBox(r, b, boxStyle)
chart.DrawBox(r, c, boxStyle)
//chart.DrawBox(r, fitb, computedBoxStyle)
chart.DrawBox(r, fitc, computedBoxStyle)
/*chart.DrawBox(r, growb, computedBoxStyle)
chart.DrawBox(r, growc, computedBoxStyle)
chart.DrawBox(r, grow, computedBoxStyle)*/
chart.DrawBox(r, conc, computedBoxStyle)
ax, ay := a.Center()
chart.DrawTextCentered(r, "a", ax, ay, boxStyle.WithDefaultsFrom(chart.Style{
FillColor: boxStyle.StrokeColor,
}))
/*bx, by := b.Center()
chart.DrawTextCentered(r, "b", bx, by, boxStyle.WithDefaultsFrom(chart.Style{
FillColor: boxStyle.StrokeColor,
}))*/
cx, cy := c.Center()
chart.DrawTextCentered(r, "c", cx, cy, boxStyle.WithDefaultsFrom(chart.Style{
FillColor: boxStyle.StrokeColor,
}))
/*fbx, fby := fitb.Center()
chart.DrawTextCentered(r, "a fit b", fbx, fby, computedBoxStyle.WithDefaultsFrom(chart.Style{
FillColor: computedBoxStyle.StrokeColor,
}))*/
fcx, fcy := fitc.Center()
chart.DrawTextCentered(r, "a fit c", fcx, fcy, computedBoxStyle.WithDefaultsFrom(chart.Style{
FillColor: computedBoxStyle.StrokeColor,
}))
/*gbx, gby := growb.Center()
chart.DrawTextCentered(r, "a grow b", gbx, gby, computedBoxStyle.WithDefaultsFrom(chart.Style{
FillColor: computedBoxStyle.StrokeColor,
}))
gcx, gcy := growc.Center()
chart.DrawTextCentered(r, "a grow c", gcx, gcy, computedBoxStyle.WithDefaultsFrom(chart.Style{
FillColor: computedBoxStyle.StrokeColor,
}))*/
ccx, ccy := conc.Center()
chart.DrawTextCentered(r, "a const c", ccx, ccy, computedBoxStyle.WithDefaultsFrom(chart.Style{
FillColor: computedBoxStyle.StrokeColor,
}))
rc.Response.Header().Set("Content-Type", "image/png")
buffer := bytes.NewBuffer([]byte{})
err = r.Save(buffer)
return rc.Raw(buffer.Bytes())
}
func main() { func main() {
app := web.New() app := web.New()
app.SetName("Chart Test Server") app.SetName("Chart Test Server")
@ -100,5 +200,6 @@ func main() {
app.GET("/favico.ico", func(rc *web.RequestContext) web.ControllerResult { app.GET("/favico.ico", func(rc *web.RequestContext) web.ControllerResult {
return rc.Raw([]byte{}) return rc.Raw([]byte{})
}) })
app.GET("/box", boxHandler)
log.Fatal(app.Start()) log.Fatal(app.Start())
} }

View File

@ -114,3 +114,11 @@ func MaxInt(values ...int) int {
} }
return max return max
} }
// AbsInt returns the absolute value of an integer.
func AbsInt(value int) int {
if value < 0 {
return -value
}
return value
}

View File

@ -153,8 +153,6 @@ 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))
box.Width = w
box.Height = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
} }
return return
} }

View File

@ -47,6 +47,6 @@ func TestVectorRendererMeasureText(t *testing.T) {
vr.SetFontSize(12.0) vr.SetFontSize(12.0)
tb := vr.MeasureText("Ljp") tb := vr.MeasureText("Ljp")
assert.Equal(21, tb.Width) assert.Equal(21, tb.Width())
assert.Equal(15, tb.Height) assert.Equal(15, tb.Height())
} }

View File

@ -56,7 +56,7 @@ func (xa XAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int {
ll = ln ll = ln
} }
llb := r.MeasureText(ll) llb := r.MeasureText(ll)
textWidth := llb.Width textWidth := llb.Width()
width := textWidth + DefaultMinimumTickHorizontalSpacing width := textWidth + DefaultMinimumTickHorizontalSpacing
count := int(math.Ceil(float64(ra.Domain) / float64(width))) count := int(math.Ceil(float64(ra.Domain) / float64(width)))
return count return count
@ -77,11 +77,11 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
tb := r.MeasureText(t.Label) tb := r.MeasureText(t.Label)
tx := canvasBox.Left + lx tx := canvasBox.Left + lx
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
top = MinInt(top, canvasBox.Bottom) top = MinInt(top, canvasBox.Bottom)
left = MinInt(left, tx-(tb.Width>>1)) left = MinInt(left, tx-(tb.Width()>>1))
right = MaxInt(right, tx+(tb.Width>>1)) right = MaxInt(right, tx+(tb.Width()>>1))
bottom = MaxInt(bottom, ty) bottom = MaxInt(bottom, ty)
} }
@ -90,8 +90,6 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
Left: left, Left: left,
Right: right, Right: right,
Bottom: bottom, Bottom: bottom,
Width: right - left,
Height: bottom - top,
} }
} }
@ -116,8 +114,8 @@ func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, ticks []Tick) {
lx := ra.Translate(v) lx := ra.Translate(v)
tb := r.MeasureText(t.Label) tb := r.MeasureText(t.Label)
tx := canvasBox.Left + lx tx := canvasBox.Left + lx
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
r.Text(t.Label, tx-tb.Width>>1, ty) r.Text(t.Label, tx-tb.Width()>>1, ty)
r.MoveTo(tx, canvasBox.Bottom) r.MoveTo(tx, canvasBox.Bottom)
r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight) r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight)

View File

@ -59,7 +59,7 @@ func (ya YAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int {
//given the domain, figure out how many ticks we can draw ... //given the domain, figure out how many ticks we can draw ...
label := vf(ra.Min) label := vf(ra.Min)
tb := r.MeasureText(label) tb := r.MeasureText(label)
return int(math.Ceil(float64(ra.Domain) / float64(tb.Height+DefaultMinimumTickVerticalSpacing))) return int(math.Ceil(float64(ra.Domain) / float64(tb.Height()+DefaultMinimumTickVerticalSpacing)))
} }
// Measure returns the bounds of the axis. // Measure returns the bounds of the axis.
@ -85,18 +85,18 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
tb := r.MeasureText(t.Label) tb := r.MeasureText(t.Label)
finalTextX := tx finalTextX := tx
if ya.AxisType == YAxisSecondary { if ya.AxisType == YAxisSecondary {
finalTextX = tx - tb.Width finalTextX = tx - tb.Width()
} }
if ya.AxisType == YAxisPrimary { if ya.AxisType == YAxisPrimary {
minx = canvasBox.Right minx = canvasBox.Right
maxx = MaxInt(maxx, tx+tb.Width) maxx = MaxInt(maxx, tx+tb.Width())
} else if ya.AxisType == YAxisSecondary { } else if ya.AxisType == YAxisSecondary {
minx = MinInt(minx, finalTextX) minx = MinInt(minx, finalTextX)
maxx = MaxInt(maxx, tx) maxx = MaxInt(maxx, tx)
} }
miny = MinInt(miny, ly-tb.Height>>1) miny = MinInt(miny, ly-tb.Height()>>1)
maxy = MaxInt(maxy, ly+tb.Height>>1) maxy = MaxInt(maxy, ly+tb.Height()>>1)
} }
return Box{ return Box{
@ -104,8 +104,6 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
Left: minx, Left: minx,
Right: maxx, Right: maxx,
Bottom: maxy, Bottom: maxy,
Width: maxx - minx,
Height: maxy - miny,
} }
} }
@ -142,9 +140,9 @@ func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, ticks []Tick) {
tb := r.MeasureText(t.Label) tb := r.MeasureText(t.Label)
finalTextX := tx finalTextX := tx
finalTextY := ly + tb.Height>>1 finalTextY := ly + tb.Height()>>1
if ya.AxisType == YAxisSecondary { if ya.AxisType == YAxisSecondary {
finalTextX = tx - tb.Width finalTextX = tx - tb.Width()
} }
r.Text(t.Label, finalTextX, finalTextY) r.Text(t.Label, finalTextX, finalTextY)