axis labels!
This commit is contained in:
parent
5c8836f9bd
commit
4bbc7978a2
7 changed files with 178 additions and 31 deletions
111
chart.go
111
chart.go
|
@ -1,6 +1,7 @@
|
||||||
package chart
|
package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
@ -37,6 +38,9 @@ func (c Chart) GetFont() (*truetype.Font, error) {
|
||||||
|
|
||||||
// Render renders the chart with the given renderer to the given io.Writer.
|
// Render renders the chart with the given renderer to the given io.Writer.
|
||||||
func (c *Chart) Render(provider RendererProvider, w io.Writer) error {
|
func (c *Chart) Render(provider RendererProvider, w io.Writer) error {
|
||||||
|
if len(c.Series) == 0 {
|
||||||
|
return errors.New("Please provide at least one series")
|
||||||
|
}
|
||||||
r := provider(c.Width, c.Height)
|
r := provider(c.Width, c.Height)
|
||||||
if c.hasText() {
|
if c.hasText() {
|
||||||
font, err := c.GetFont()
|
font, err := c.GetFont()
|
||||||
|
@ -60,7 +64,15 @@ func (c *Chart) Render(provider RendererProvider, w io.Writer) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) hasText() bool {
|
func (c Chart) hasText() bool {
|
||||||
return c.TitleStyle.Show || c.Axes.Show
|
return c.TitleStyle.Show || c.Axes.Show || c.FinalValueLabel.Show
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) getAxisWidth() int {
|
||||||
|
asw := 0
|
||||||
|
if c.Axes.Show {
|
||||||
|
asw = int(c.Axes.GetStrokeWidth(DefaultAxisLineWidth))
|
||||||
|
}
|
||||||
|
return asw
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) calculateCanvasBox(r Renderer) Box {
|
func (c Chart) calculateCanvasBox(r Renderer) Box {
|
||||||
|
@ -69,7 +81,11 @@ func (c Chart) calculateCanvasBox(r Renderer) Box {
|
||||||
if finalLabelWidth > dpr {
|
if finalLabelWidth > dpr {
|
||||||
dpr = finalLabelWidth
|
dpr = finalLabelWidth
|
||||||
}
|
}
|
||||||
|
axisBottomHeight := c.calculateBottomLabelHeight()
|
||||||
dpb := DefaultBackgroundPadding.Bottom
|
dpb := DefaultBackgroundPadding.Bottom
|
||||||
|
if dpb < axisBottomHeight {
|
||||||
|
dpb = axisBottomHeight
|
||||||
|
}
|
||||||
|
|
||||||
cb := Box{
|
cb := Box{
|
||||||
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
||||||
|
@ -88,7 +104,8 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int {
|
||||||
}
|
}
|
||||||
var finalLabelText string
|
var finalLabelText string
|
||||||
for _, s := range c.Series {
|
for _, s := range c.Series {
|
||||||
_, ll := s.GetLabel(s.Len() - 1)
|
_, lv := s.GetValue(s.Len() - 1)
|
||||||
|
ll := s.GetYFormatter()(lv)
|
||||||
if len(finalLabelText) < len(ll) {
|
if len(finalLabelText) < len(ll) {
|
||||||
finalLabelText = ll
|
finalLabelText = ll
|
||||||
}
|
}
|
||||||
|
@ -96,11 +113,10 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int {
|
||||||
|
|
||||||
r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize))
|
r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize))
|
||||||
textWidth := r.MeasureText(finalLabelText)
|
textWidth := r.MeasureText(finalLabelText)
|
||||||
|
asw := c.getAxisWidth()
|
||||||
|
|
||||||
pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left)
|
pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left)
|
||||||
pr := c.FinalValueLabel.Padding.GetRight(DefaultFinalLabelPadding.Right)
|
pr := c.FinalValueLabel.Padding.GetRight(DefaultFinalLabelPadding.Right)
|
||||||
|
|
||||||
asw := int(c.Axes.GetStrokeWidth(DefaultAxisLineWidth))
|
|
||||||
lsw := int(c.FinalValueLabel.GetStrokeWidth(DefaultAxisLineWidth))
|
lsw := int(c.FinalValueLabel.GetStrokeWidth(DefaultAxisLineWidth))
|
||||||
|
|
||||||
return DefaultFinalLabelDeltaWidth +
|
return DefaultFinalLabelDeltaWidth +
|
||||||
|
@ -108,6 +124,13 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int {
|
||||||
textWidth + asw + 2*lsw
|
textWidth + asw + 2*lsw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Chart) calculateBottomLabelHeight() int {
|
||||||
|
if c.Axes.Show {
|
||||||
|
return c.getAxisWidth() + int(math.Ceil(c.Axes.GetFontSize(DefaultAxisFontSize))) + DefaultXAxisMargin
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (c Chart) initRanges(canvasBox Box) (xrange Range, yrange Range) {
|
func (c Chart) initRanges(canvasBox Box) (xrange Range, yrange Range) {
|
||||||
//iterate over each series, pull out the min/max for x,y
|
//iterate over each series, pull out the min/max for x,y
|
||||||
var didSetFirstValues bool
|
var didSetFirstValues bool
|
||||||
|
@ -136,6 +159,8 @@ func (c Chart) initRanges(canvasBox Box) (xrange Range, yrange Range) {
|
||||||
didSetFirstValues = true
|
didSetFirstValues = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
xrange.Formatter = s.GetXFormatter()
|
||||||
|
yrange.Formatter = s.GetYFormatter()
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.XRange.IsZero() {
|
if c.XRange.IsZero() {
|
||||||
|
@ -194,13 +219,74 @@ func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange Range) {
|
||||||
r.LineTo(canvasBox.Right, canvasBox.Top)
|
r.LineTo(canvasBox.Right, canvasBox.Top)
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
|
|
||||||
c.drawAxesLabels(r, xrange, yrange)
|
c.drawXAxisLabels(r, canvasBox, xrange)
|
||||||
|
c.drawYAxisLabels(r, canvasBox, yrange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) drawAxesLabels(r Renderer, xrange, yrange Range) {
|
func (c Chart) drawYAxisLabels(r Renderer, canvasBox Box, yrange Range) {
|
||||||
// do x axis
|
tickFontSize := c.Axes.GetFontSize(DefaultAxisFontSize)
|
||||||
// do y axis
|
|
||||||
|
r.SetFontColor(c.Axes.GetFontColor(DefaultAxisColor))
|
||||||
|
r.SetFontSize(tickFontSize)
|
||||||
|
|
||||||
|
minimumTickHeight := tickFontSize + DefaultMinimumTickVerticalSpacing
|
||||||
|
tickCount := int(math.Floor(float64(yrange.Domain) / float64(minimumTickHeight)))
|
||||||
|
|
||||||
|
if tickCount > DefaultMaxTickCount {
|
||||||
|
tickCount = DefaultMaxTickCount
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeTicks := Slices(tickCount, yrange.Max-yrange.Min)
|
||||||
|
domainTicks := Slices(tickCount, float64(yrange.Domain))
|
||||||
|
|
||||||
|
asw := c.getAxisWidth()
|
||||||
|
tx := canvasBox.Right + DefaultFinalLabelDeltaWidth + asw
|
||||||
|
|
||||||
|
count := len(rangeTicks)
|
||||||
|
if len(domainTicks) < count {
|
||||||
|
count = len(domainTicks) //guard against mismatched array sizes.
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := 0; index < count; index++ {
|
||||||
|
v := rangeTicks[index]
|
||||||
|
y := domainTicks[index]
|
||||||
|
ty := canvasBox.Bottom - int(y)
|
||||||
|
r.Text(yrange.Format(v), tx, ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Chart) drawXAxisLabels(r Renderer, canvasBox Box, xrange Range) {
|
||||||
|
tickFontSize := c.Axes.GetFontSize(DefaultAxisFontSize)
|
||||||
|
|
||||||
|
r.SetFontColor(c.Axes.GetFontColor(DefaultAxisColor))
|
||||||
|
r.SetFontSize(tickFontSize)
|
||||||
|
|
||||||
|
maxLabelWidth := 60
|
||||||
|
|
||||||
|
minimumTickWidth := maxLabelWidth + DefaultMinimumTickHorizontalSpacing
|
||||||
|
tickCount := int(math.Floor(float64(xrange.Domain) / float64(minimumTickWidth)))
|
||||||
|
|
||||||
|
if tickCount > DefaultMaxTickCount {
|
||||||
|
tickCount = DefaultMaxTickCount
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeTicks := Slices(tickCount, xrange.Max-xrange.Min)
|
||||||
|
domainTicks := Slices(tickCount, float64(xrange.Domain))
|
||||||
|
|
||||||
|
ty := canvasBox.Bottom + DefaultXAxisMargin + int(tickFontSize)
|
||||||
|
|
||||||
|
count := len(rangeTicks)
|
||||||
|
if len(domainTicks) < count {
|
||||||
|
count = len(domainTicks) //guard against mismatched array sizes.
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := 0; index < count; index++ {
|
||||||
|
v := rangeTicks[index] + xrange.Min
|
||||||
|
x := domainTicks[index]
|
||||||
|
tx := canvasBox.Left + int(x)
|
||||||
|
r.Text(xrange.Format(v), tx, ty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Chart) drawSeries(r Renderer, canvasBox Box, index int, s Series, xrange, yrange Range) {
|
func (c Chart) drawSeries(r Renderer, canvasBox Box, index int, s Series, xrange, yrange Range) {
|
||||||
|
@ -236,7 +322,7 @@ func (c Chart) drawSeries(r Renderer, canvasBox Box, index int, s Series, xrange
|
||||||
func (c Chart) drawFinalValueLabel(r Renderer, canvasBox Box, index int, s Series, yrange Range) {
|
func (c Chart) drawFinalValueLabel(r Renderer, canvasBox Box, index int, s Series, yrange Range) {
|
||||||
if c.FinalValueLabel.Show {
|
if c.FinalValueLabel.Show {
|
||||||
_, lv := s.GetValue(s.Len() - 1)
|
_, lv := s.GetValue(s.Len() - 1)
|
||||||
_, ll := s.GetLabel(s.Len() - 1)
|
ll := s.GetYFormatter()(lv)
|
||||||
|
|
||||||
py := canvasBox.Top
|
py := canvasBox.Top
|
||||||
ly := yrange.Translate(lv) + py
|
ly := yrange.Translate(lv) + py
|
||||||
|
@ -246,7 +332,12 @@ func (c Chart) drawFinalValueLabel(r Renderer, canvasBox Box, index int, s Serie
|
||||||
textHeight := int(math.Floor(DefaultFinalLabelFontSize))
|
textHeight := int(math.Floor(DefaultFinalLabelFontSize))
|
||||||
halfTextHeight := textHeight >> 1
|
halfTextHeight := textHeight >> 1
|
||||||
|
|
||||||
cx := canvasBox.Right + int(c.Axes.GetStrokeWidth(DefaultAxisLineWidth))
|
asw := 0
|
||||||
|
if c.Axes.Show {
|
||||||
|
asw = int(c.Axes.GetStrokeWidth(DefaultAxisLineWidth))
|
||||||
|
}
|
||||||
|
|
||||||
|
cx := canvasBox.Right + asw
|
||||||
|
|
||||||
pt := c.FinalValueLabel.Padding.GetTop(DefaultFinalLabelPadding.Top)
|
pt := c.FinalValueLabel.Padding.GetTop(DefaultFinalLabelPadding.Top)
|
||||||
pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left)
|
pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left)
|
||||||
|
|
10
defaults.go
10
defaults.go
|
@ -28,10 +28,20 @@ const (
|
||||||
DefaultFinalLabelDeltaWidth = 10
|
DefaultFinalLabelDeltaWidth = 10
|
||||||
// DefaultFinalLabelFontSize is the font size of the final label.
|
// DefaultFinalLabelFontSize is the font size of the final label.
|
||||||
DefaultFinalLabelFontSize = 10.0
|
DefaultFinalLabelFontSize = 10.0
|
||||||
|
// DefaultAxisFontSize is the font size of the axis labels.
|
||||||
|
DefaultAxisFontSize = 10.0
|
||||||
// DefaultTitleTop is the default distance from the top of the chart to put the title.
|
// DefaultTitleTop is the default distance from the top of the chart to put the title.
|
||||||
DefaultTitleTop = 10
|
DefaultTitleTop = 10
|
||||||
|
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
|
||||||
|
DefaultXAxisMargin = 10
|
||||||
|
// DefaultMinimumTickHorizontalSpacing is the minimum distance between horizontal ticks.
|
||||||
|
DefaultMinimumTickHorizontalSpacing = 20
|
||||||
|
// DefaultMinimumTickVerticalSpacing is the minimum distance between vertical ticks.
|
||||||
|
DefaultMinimumTickVerticalSpacing = 20
|
||||||
// DefaultDateFormat is the default date format.
|
// DefaultDateFormat is the default date format.
|
||||||
DefaultDateFormat = "2006-01-02"
|
DefaultDateFormat = "2006-01-02"
|
||||||
|
// DefaultMaxTickCount is the maximum number of ticks to draw
|
||||||
|
DefaultMaxTickCount = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
4
formatter.go
Normal file
4
formatter.go
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
package chart
|
||||||
|
|
||||||
|
// Formatter is a function that takes a value and produces a string.
|
||||||
|
type Formatter func(v interface{}) string
|
11
range.go
11
range.go
|
@ -3,6 +3,8 @@ package chart
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
"github.com/blendlabs/go-util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Range represents a continuous range,
|
// Range represents a continuous range,
|
||||||
|
@ -10,6 +12,7 @@ type Range struct {
|
||||||
Min float64
|
Min float64
|
||||||
Max float64
|
Max float64
|
||||||
Domain int
|
Domain int
|
||||||
|
Formatter Formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsZero returns if the range has been set or not.
|
// IsZero returns if the range has been set or not.
|
||||||
|
@ -27,6 +30,14 @@ func (r Range) String() string {
|
||||||
return fmt.Sprintf("Range [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
|
return fmt.Sprintf("Range [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format formats the value based on the range's formatter.
|
||||||
|
func (r Range) Format(v interface{}) string {
|
||||||
|
if r.Formatter != nil {
|
||||||
|
return r.Formatter(v)
|
||||||
|
}
|
||||||
|
return util.StringEmpty
|
||||||
|
}
|
||||||
|
|
||||||
// Translate maps a given value into the range space.
|
// Translate maps a given value into the range space.
|
||||||
// An example would be a 600 px image, with a min of 10 and a max of 100.
|
// An example would be a 600 px image, with a min of 10 and a max of 100.
|
||||||
// Translate(50) would yield (50.0/90.0)*600 ~= 333.33
|
// Translate(50) would yield (50.0/90.0)*600 ~= 333.33
|
||||||
|
|
54
series.go
54
series.go
|
@ -3,6 +3,8 @@ package chart
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/blendlabs/go-util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Series is a entity data set.
|
// Series is a entity data set.
|
||||||
|
@ -10,8 +12,11 @@ type Series interface {
|
||||||
GetName() string
|
GetName() string
|
||||||
GetStyle() Style
|
GetStyle() Style
|
||||||
Len() int
|
Len() int
|
||||||
|
|
||||||
GetValue(index int) (float64, float64)
|
GetValue(index int) (float64, float64)
|
||||||
GetLabel(index int) (string, string)
|
|
||||||
|
GetXFormatter() Formatter
|
||||||
|
GetYFormatter() Formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimeSeries is a line on a chart.
|
// TimeSeries is a line on a chart.
|
||||||
|
@ -45,11 +50,30 @@ func (ts TimeSeries) GetValue(index int) (x float64, y float64) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLabel gets a label for the values at a given index.
|
// GetXFormatter returns the x value formatter.
|
||||||
func (ts TimeSeries) GetLabel(index int) (xLabel string, yLabel string) {
|
func (ts TimeSeries) GetXFormatter() Formatter {
|
||||||
xLabel = ts.XValues[index].Format(DefaultDateFormat)
|
return func(v interface{}) string {
|
||||||
yLabel = fmt.Sprintf("%0.2f", ts.YValues[index])
|
if typed, isTyped := v.(time.Time); isTyped {
|
||||||
return
|
return typed.Format(DefaultDateFormat)
|
||||||
|
}
|
||||||
|
if typed, isTyped := v.(int64); isTyped {
|
||||||
|
return time.Unix(typed, 0).Format(DefaultDateFormat)
|
||||||
|
}
|
||||||
|
if typed, isTyped := v.(float64); isTyped {
|
||||||
|
return time.Unix(int64(typed), 0).Format(DefaultDateFormat)
|
||||||
|
}
|
||||||
|
return util.StringEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYFormatter returns the y value formatter.
|
||||||
|
func (ts TimeSeries) GetYFormatter() Formatter {
|
||||||
|
return func(v interface{}) string {
|
||||||
|
if typed, isTyped := v.(float64); isTyped {
|
||||||
|
return fmt.Sprintf("%0.2f", typed)
|
||||||
|
}
|
||||||
|
return util.StringEmpty
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContinousSeries represents a line on a chart.
|
// ContinousSeries represents a line on a chart.
|
||||||
|
@ -81,9 +105,17 @@ func (cs ContinousSeries) GetValue(index int) (interface{}, float64) {
|
||||||
return cs.XValues[index], cs.YValues[index]
|
return cs.XValues[index], cs.YValues[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLabel gets a label for the values at a given index.
|
// GetXFormatter returns the xs value formatter.
|
||||||
func (cs ContinousSeries) GetLabel(index int) (xLabel string, yLabel string) {
|
func (cs ContinousSeries) GetXFormatter() Formatter {
|
||||||
xLabel = fmt.Sprintf("%0.2f", cs.XValues[index])
|
return func(v interface{}) string {
|
||||||
yLabel = fmt.Sprintf("%0.2f", cs.YValues[index])
|
if typed, isTyped := v.(float64); isTyped {
|
||||||
return
|
return fmt.Sprintf("%0.2f", typed)
|
||||||
|
}
|
||||||
|
return util.StringEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetYFormatter returns the y value formatter.
|
||||||
|
func (cs ContinousSeries) GetYFormatter() Formatter {
|
||||||
|
return cs.GetXFormatter()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ func main() {
|
||||||
TitleStyle: chart.Style{
|
TitleStyle: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
},
|
},
|
||||||
Width: 800,
|
Width: 640,
|
||||||
Height: 380,
|
Height: 480,
|
||||||
Axes: chart.Style{
|
Axes: chart.Style{
|
||||||
Show: true,
|
Show: true,
|
||||||
StrokeWidth: 1.0,
|
StrokeWidth: 1.0,
|
||||||
|
|
9
util.go
9
util.go
|
@ -3,7 +3,6 @@ package chart
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,10 +57,10 @@ func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
|
||||||
|
|
||||||
// Slices generates N slices that span the total.
|
// Slices generates N slices that span the total.
|
||||||
// The resulting array will be intermediate indexes until total.
|
// The resulting array will be intermediate indexes until total.
|
||||||
func Slices(count, total int) []int {
|
func Slices(count int, total float64) []float64 {
|
||||||
var values []int
|
var values []float64
|
||||||
sliceWidth := int(math.Floor(float64(total) / float64(count)))
|
sliceWidth := float64(total) / float64(count)
|
||||||
for cursor := 0; cursor < total; cursor += sliceWidth {
|
for cursor := 0.0; cursor < total; cursor += sliceWidth {
|
||||||
values = append(values, cursor)
|
values = append(values, cursor)
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
|
|
Loading…
Reference in a new issue