slightly more rigorous bounds checking and auto-fit
This commit is contained in:
parent
bba75e5d4c
commit
28f01842de
13 changed files with 475 additions and 195 deletions
|
@ -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
172
box.go
|
@ -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
87
box_test.go
Normal 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
217
chart.go
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
8
util.go
8
util.go
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
14
xaxis.go
14
xaxis.go
|
@ -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)
|
||||
|
|
16
yaxis.go
16
yaxis.go
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue