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,
Right: 105,
Bottom: 105,
Height: 100,
Width: 100,
}
sd := Style{
FontSize: 10.0,
@ -100,8 +98,6 @@ func TestAnnotationSeriesRender(t *testing.T) {
Left: 5,
Right: 105,
Bottom: 105,
Height: 100,
Width: 100,
}
sd := Style{
FontSize: 10.0,

172
box.go
View File

@ -8,9 +8,6 @@ type Box struct {
Left int
Right int
Bottom int
Height int
Width int
}
// IsZero returns if the box is set or not.
@ -66,3 +63,172 @@ func (b Box) GetBottom(defaults ...int) int {
}
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.drawTitle(r)
return r.Save(w)
}
func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
var globalMinX, globalMaxX float64 = math.MaxFloat64, 0
var globalMinY, globalMaxY float64 = math.MaxFloat64, 0
var globalMinYA, globalMaxYA float64 = math.MaxFloat64, 0
var minx, maxx float64 = math.MaxFloat64, 0
var miny, maxy float64 = math.MaxFloat64, 0
var minya, maxya float64 = math.MaxFloat64, 0
for _, s := range c.Series {
seriesAxis := s.GetYAxis()
@ -113,26 +114,16 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
seriesLength := vp.Len()
for index := 0; index < seriesLength; index++ {
vx, vy := vp.GetValue(index)
if globalMinX > vx {
globalMinX = vx
}
if globalMaxX < vx {
globalMaxX = vx
}
minx = math.Min(minx, vx)
maxx = math.Max(maxx, vx)
if seriesAxis == YAxisPrimary {
if globalMinY > vy {
globalMinY = vy
}
if globalMaxY < vy {
globalMaxY = vy
}
miny = math.Min(miny, vy)
maxy = math.Max(maxy, vy)
} else if seriesAxis == YAxisSecondary {
if globalMinYA > vy {
globalMinYA = vy
}
if globalMaxYA < vy {
globalMaxYA = vy
}
minya = math.Min(minya, vy)
maxya = math.Max(maxya, vy)
}
}
}
@ -142,17 +133,16 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
xrange.Min = c.XAxis.Range.Min
xrange.Max = c.XAxis.Range.Max
} else {
xrange.Min = globalMinX
xrange.Max = globalMaxX
//xrange.Min, xrange.Max = xrange.GetRoundedRangeBounds()
xrange.Min = minx
xrange.Max = maxx
}
if !c.YAxis.Range.IsZero() {
yrange.Min = c.YAxis.Range.Min
yrange.Max = c.YAxis.Range.Max
} else {
yrange.Min = globalMinY
yrange.Max = globalMaxY
yrange.Min = miny
yrange.Max = maxy
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.Max = c.YAxisSecondary.Range.Max
} else {
yrangeAlt.Min = globalMinYA
yrangeAlt.Max = globalMaxYA
yrangeAlt.Min = minya
yrangeAlt.Max = maxya
yrangeAlt.Min, yrangeAlt.Max = yrangeAlt.GetRoundedRangeBounds()
}
@ -169,19 +159,17 @@ func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
}
func (c Chart) getDefaultCanvasBox() Box {
dpt := c.Background.Padding.GetTop(DefaultBackgroundPadding.Top)
dpl := c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left)
dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
cb := Box{
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
return Box{
Top: dpt,
Left: dpl,
Right: c.Width - dpr,
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) {
@ -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 {
axesMinX, axesMaxX, axesMinY, axesMaxY := math.MaxInt32, 0, math.MaxInt32, 0
axesOuterBox := canvasBox.Clone()
if c.XAxis.Style.Show {
axesBounds := c.XAxis.Measure(r, canvasBox, xr, xticks)
axesMinY = MinInt(axesMinX, axesBounds.Top)
axesMinX = MinInt(axesMinY, axesBounds.Left)
axesMaxX = MaxInt(axesMaxX, axesBounds.Right)
axesMaxY = MaxInt(axesMaxY, axesBounds.Bottom)
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
if c.YAxis.Style.Show {
axesBounds := c.YAxis.Measure(r, canvasBox, yr, yticks)
axesMinY = MinInt(axesMinX, axesBounds.Top)
axesMinX = MinInt(axesMinY, axesBounds.Left)
axesMaxX = MaxInt(axesMaxX, axesBounds.Right)
axesMaxY = MaxInt(axesMaxY, axesBounds.Bottom)
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
if c.YAxisSecondary.Style.Show {
axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, yticksAlt)
axesMinY = MinInt(axesMinX, axesBounds.Top)
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,
axesOuterBox = axesOuterBox.Grow(axesBounds)
}
if axesMinY < 0 {
// 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
return canvasBox.OuterConstrain(c.asBox(), axesOuterBox)
}
func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (xr2, yr2, yra2 Range) {
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.Domain = canvasBox.Height
yr2.Domain = canvasBox.Height()
yra2.Min, yra2.Max = yra.Min, yra.Max
yra2.Domain = canvasBox.Height
yra2.Domain = canvasBox.Height()
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 {
annotationMinX, annotationMaxX, annotationMinY, annotationMaxY := math.MaxInt32, 0, math.MaxInt32, 0
annotationSeriesBox := canvasBox.Clone()
for seriesIndex, s := range c.Series {
if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
if as.Style.Show {
style := c.getSeriesStyleDefaults(seriesIndex)
style := c.seriesStyleDefaults(seriesIndex)
var annotationBounds Box
if as.YAxis == YAxisPrimary {
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)
}
annotationMinY = MinInt(annotationMinY, annotationBounds.Top)
annotationMinX = MinInt(annotationMinX, annotationBounds.Left)
annotationMaxX = MaxInt(annotationMaxX, annotationBounds.Right)
annotationMaxY = MaxInt(annotationMaxY, annotationBounds.Bottom)
annotationSeriesBox = annotationSeriesBox.Grow(annotationBounds)
}
}
}
newBox := Box{
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
return canvasBox.OuterConstrain(c.asBox(), annotationSeriesBox)
}
func (c Chart) drawBackground(r Renderer) {
r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor))
r.SetStrokeColor(c.Background.GetStrokeColor(DefaultBackgroundStrokeColor))
r.SetStrokeWidth(c.Background.GetStrokeWidth(DefaultStrokeWidth))
r.MoveTo(0, 0)
r.LineTo(c.Width, 0)
r.LineTo(c.Width, c.Height)
r.LineTo(0, c.Height)
r.LineTo(0, 0)
r.Close()
r.FillStroke()
DrawBox(r, Box{Right: c.Width, Bottom: c.Height}, c.Canvas.WithDefaultsFrom(Style{
FillColor: DefaultBackgroundColor,
StrokeColor: DefaultBackgroundStrokeColor,
StrokeWidth: DefaultStrokeWidth,
}))
}
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
r.SetFillColor(c.Canvas.GetFillColor(DefaultCanvasColor))
r.SetStrokeColor(c.Canvas.GetStrokeColor(DefaultCanvasStrokColor))
r.SetStrokeWidth(c.Canvas.GetStrokeWidth(DefaultStrokeWidth))
r.MoveTo(canvasBox.Left, canvasBox.Top)
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()
DrawBox(r, canvasBox, c.Canvas.WithDefaultsFrom(Style{
FillColor: DefaultCanvasColor,
StrokeColor: DefaultCanvasStrokeColor,
StrokeWidth: DefaultStrokeWidth,
}))
}
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) {
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 {
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)
textWidth := textBox.Width
textHeight := textBox.Height
textWidth := textBox.Width()
textHeight := textBox.Height()
titleX := (c.Width >> 1) - (textWidth >> 1)
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
@ -437,3 +328,17 @@ func (c Chart) drawTitle(r Renderer) {
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}
// DefaultCanvasStrokColor is the default chart canvas stroke color.
// 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.
// It is equivalent to #333333.
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.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
textBox := r.MeasureText(label)
halfTextHeight := textBox.Height >> 1
textWidth := textBox.Width()
textHeight := textBox.Height()
halfTextHeight := textHeight >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
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()
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)
return Box{
@ -69,8 +71,6 @@ func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label str
Left: lx,
Right: right,
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.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
textBox := r.MeasureText(label)
halfTextHeight := textBox.Height >> 1
textWidth := textBox.Width()
halfTextHeight := textBox.Height() >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
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
lty := ly - (pt + halfTextHeight)
rtx := lx + pl + pr + textBox.Width + DefaultAnnotationDeltaWidth
rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rty := ly - (pt + halfTextHeight)
rbx := lx + pl + pr + textBox.Width + DefaultAnnotationDeltaWidth
rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
rby := ly + (pb + halfTextHeight)
lbx := lx + DefaultAnnotationDeltaWidth
@ -131,3 +132,27 @@ func DrawBox(r Renderer, b Box, s Style) {
r.LineTo(b.Left, b.Top)
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)),
Right: int(math.Ceil(r)),
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
import (
"bytes"
"fmt"
"log"
"math/rand"
@ -8,6 +9,7 @@ import (
"time"
"github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
"github.com/wcharczuk/go-web"
)
@ -36,7 +38,7 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
c := chart.Chart{
Title: "A Test Chart",
TitleStyle: chart.Style{
Show: true,
Show: false,
},
Width: 1024,
Height: 400,
@ -91,6 +93,104 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
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() {
app := web.New()
app.SetName("Chart Test Server")
@ -100,5 +200,6 @@ func main() {
app.GET("/favico.ico", func(rc *web.RequestContext) web.ControllerResult {
return rc.Raw([]byte{})
})
app.GET("/box", boxHandler)
log.Fatal(app.Start())
}

View File

@ -114,3 +114,11 @@ func MaxInt(values ...int) int {
}
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.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
box.Width = w
box.Height = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
}
return
}

View File

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

View File

@ -56,7 +56,7 @@ func (xa XAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int {
ll = ln
}
llb := r.MeasureText(ll)
textWidth := llb.Width
textWidth := llb.Width()
width := textWidth + DefaultMinimumTickHorizontalSpacing
count := int(math.Ceil(float64(ra.Domain) / float64(width)))
return count
@ -77,11 +77,11 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
tb := r.MeasureText(t.Label)
tx := canvasBox.Left + lx
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
top = MinInt(top, canvasBox.Bottom)
left = MinInt(left, tx-(tb.Width>>1))
right = MaxInt(right, tx+(tb.Width>>1))
left = MinInt(left, tx-(tb.Width()>>1))
right = MaxInt(right, tx+(tb.Width()>>1))
bottom = MaxInt(bottom, ty)
}
@ -90,8 +90,6 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
Left: left,
Right: right,
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)
tb := r.MeasureText(t.Label)
tx := canvasBox.Left + lx
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height
r.Text(t.Label, tx-tb.Width>>1, ty)
ty := canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
r.Text(t.Label, tx-tb.Width()>>1, ty)
r.MoveTo(tx, canvasBox.Bottom)
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 ...
label := vf(ra.Min)
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.
@ -85,18 +85,18 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
tb := r.MeasureText(t.Label)
finalTextX := tx
if ya.AxisType == YAxisSecondary {
finalTextX = tx - tb.Width
finalTextX = tx - tb.Width()
}
if ya.AxisType == YAxisPrimary {
minx = canvasBox.Right
maxx = MaxInt(maxx, tx+tb.Width)
maxx = MaxInt(maxx, tx+tb.Width())
} else if ya.AxisType == YAxisSecondary {
minx = MinInt(minx, finalTextX)
maxx = MaxInt(maxx, tx)
}
miny = MinInt(miny, ly-tb.Height>>1)
maxy = MaxInt(maxy, ly+tb.Height>>1)
miny = MinInt(miny, ly-tb.Height()>>1)
maxy = MaxInt(maxy, ly+tb.Height()>>1)
}
return Box{
@ -104,8 +104,6 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, ticks []Tick) Box {
Left: minx,
Right: maxx,
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)
finalTextX := tx
finalTextY := ly + tb.Height>>1
finalTextY := ly + tb.Height()>>1
if ya.AxisType == YAxisSecondary {
finalTextX = tx - tb.Width
finalTextX = tx - tb.Width()
}
r.Text(t.Label, finalTextX, finalTextY)