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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"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.
|
||||
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)
|
||||
if c.hasText() {
|
||||
font, err := c.GetFont()
|
||||
|
@ -60,7 +64,15 @@ func (c *Chart) Render(provider RendererProvider, w io.Writer) error {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -69,7 +81,11 @@ func (c Chart) calculateCanvasBox(r Renderer) Box {
|
|||
if finalLabelWidth > dpr {
|
||||
dpr = finalLabelWidth
|
||||
}
|
||||
axisBottomHeight := c.calculateBottomLabelHeight()
|
||||
dpb := DefaultBackgroundPadding.Bottom
|
||||
if dpb < axisBottomHeight {
|
||||
dpb = axisBottomHeight
|
||||
}
|
||||
|
||||
cb := Box{
|
||||
Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
|
||||
|
@ -88,7 +104,8 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int {
|
|||
}
|
||||
var finalLabelText string
|
||||
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) {
|
||||
finalLabelText = ll
|
||||
}
|
||||
|
@ -96,11 +113,10 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int {
|
|||
|
||||
r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize))
|
||||
textWidth := r.MeasureText(finalLabelText)
|
||||
asw := c.getAxisWidth()
|
||||
|
||||
pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left)
|
||||
pr := c.FinalValueLabel.Padding.GetRight(DefaultFinalLabelPadding.Right)
|
||||
|
||||
asw := int(c.Axes.GetStrokeWidth(DefaultAxisLineWidth))
|
||||
lsw := int(c.FinalValueLabel.GetStrokeWidth(DefaultAxisLineWidth))
|
||||
|
||||
return DefaultFinalLabelDeltaWidth +
|
||||
|
@ -108,6 +124,13 @@ func (c Chart) calculateFinalLabelWidth(r Renderer) int {
|
|||
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) {
|
||||
//iterate over each series, pull out the min/max for x,y
|
||||
var didSetFirstValues bool
|
||||
|
@ -136,6 +159,8 @@ func (c Chart) initRanges(canvasBox Box) (xrange Range, yrange Range) {
|
|||
didSetFirstValues = true
|
||||
}
|
||||
}
|
||||
xrange.Formatter = s.GetXFormatter()
|
||||
yrange.Formatter = s.GetYFormatter()
|
||||
}
|
||||
|
||||
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.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) {
|
||||
// do x axis
|
||||
// do y axis
|
||||
func (c Chart) drawYAxisLabels(r Renderer, canvasBox Box, yrange Range) {
|
||||
tickFontSize := c.Axes.GetFontSize(DefaultAxisFontSize)
|
||||
|
||||
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) {
|
||||
|
@ -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) {
|
||||
if c.FinalValueLabel.Show {
|
||||
_, lv := s.GetValue(s.Len() - 1)
|
||||
_, ll := s.GetLabel(s.Len() - 1)
|
||||
ll := s.GetYFormatter()(lv)
|
||||
|
||||
py := canvasBox.Top
|
||||
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))
|
||||
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)
|
||||
pl := c.FinalValueLabel.Padding.GetLeft(DefaultFinalLabelPadding.Left)
|
||||
|
|
10
defaults.go
10
defaults.go
|
@ -28,10 +28,20 @@ const (
|
|||
DefaultFinalLabelDeltaWidth = 10
|
||||
// DefaultFinalLabelFontSize is the font size of the final label.
|
||||
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 = 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 = "2006-01-02"
|
||||
// DefaultMaxTickCount is the maximum number of ticks to draw
|
||||
DefaultMaxTickCount = 7
|
||||
)
|
||||
|
||||
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
|
17
range.go
17
range.go
|
@ -3,13 +3,16 @@ package chart
|
|||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/blendlabs/go-util"
|
||||
)
|
||||
|
||||
// Range represents a continuous range,
|
||||
type Range struct {
|
||||
Min float64
|
||||
Max float64
|
||||
Domain int
|
||||
Min float64
|
||||
Max float64
|
||||
Domain int
|
||||
Formatter Formatter
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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
|
||||
|
|
54
series.go
54
series.go
|
@ -3,6 +3,8 @@ package chart
|
|||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/blendlabs/go-util"
|
||||
)
|
||||
|
||||
// Series is a entity data set.
|
||||
|
@ -10,8 +12,11 @@ type Series interface {
|
|||
GetName() string
|
||||
GetStyle() Style
|
||||
Len() int
|
||||
|
||||
GetValue(index int) (float64, float64)
|
||||
GetLabel(index int) (string, string)
|
||||
|
||||
GetXFormatter() Formatter
|
||||
GetYFormatter() Formatter
|
||||
}
|
||||
|
||||
// TimeSeries is a line on a chart.
|
||||
|
@ -45,11 +50,30 @@ func (ts TimeSeries) GetValue(index int) (x float64, y float64) {
|
|||
return
|
||||
}
|
||||
|
||||
// GetLabel gets a label for the values at a given index.
|
||||
func (ts TimeSeries) GetLabel(index int) (xLabel string, yLabel string) {
|
||||
xLabel = ts.XValues[index].Format(DefaultDateFormat)
|
||||
yLabel = fmt.Sprintf("%0.2f", ts.YValues[index])
|
||||
return
|
||||
// GetXFormatter returns the x value formatter.
|
||||
func (ts TimeSeries) GetXFormatter() Formatter {
|
||||
return func(v interface{}) string {
|
||||
if typed, isTyped := v.(time.Time); isTyped {
|
||||
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.
|
||||
|
@ -81,9 +105,17 @@ func (cs ContinousSeries) GetValue(index int) (interface{}, float64) {
|
|||
return cs.XValues[index], cs.YValues[index]
|
||||
}
|
||||
|
||||
// GetLabel gets a label for the values at a given index.
|
||||
func (cs ContinousSeries) GetLabel(index int) (xLabel string, yLabel string) {
|
||||
xLabel = fmt.Sprintf("%0.2f", cs.XValues[index])
|
||||
yLabel = fmt.Sprintf("%0.2f", cs.YValues[index])
|
||||
return
|
||||
// GetXFormatter returns the xs value formatter.
|
||||
func (cs ContinousSeries) GetXFormatter() Formatter {
|
||||
return func(v interface{}) string {
|
||||
if typed, isTyped := v.(float64); isTyped {
|
||||
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{
|
||||
Show: true,
|
||||
},
|
||||
Width: 800,
|
||||
Height: 380,
|
||||
Width: 640,
|
||||
Height: 480,
|
||||
Axes: chart.Style{
|
||||
Show: true,
|
||||
StrokeWidth: 1.0,
|
||||
|
|
9
util.go
9
util.go
|
@ -3,7 +3,6 @@ package chart
|
|||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -58,10 +57,10 @@ func MinAndMaxOfTime(values ...time.Time) (min time.Time, max time.Time) {
|
|||
|
||||
// Slices generates N slices that span the total.
|
||||
// The resulting array will be intermediate indexes until total.
|
||||
func Slices(count, total int) []int {
|
||||
var values []int
|
||||
sliceWidth := int(math.Floor(float64(total) / float64(count)))
|
||||
for cursor := 0; cursor < total; cursor += sliceWidth {
|
||||
func Slices(count int, total float64) []float64 {
|
||||
var values []float64
|
||||
sliceWidth := float64(total) / float64(count)
|
||||
for cursor := 0.0; cursor < total; cursor += sliceWidth {
|
||||
values = append(values, cursor)
|
||||
}
|
||||
return values
|
||||
|
|
Loading…
Reference in a new issue