Merge pull request #1 from wcharczuk/render

The March Towards v1.0
This commit is contained in:
Will Charczuk 2016-07-10 11:25:53 -07:00 committed by GitHub
commit 04c3cbdcf9
26 changed files with 1023 additions and 514 deletions

48
annotation_series.go Normal file
View File

@ -0,0 +1,48 @@
package chart
// Annotation is a label on the chart.
type Annotation struct {
X, Y float64
Label string
}
// AnnotationSeries is a series of labels on the chart.
type AnnotationSeries struct {
Name string
Style Style
YAxis YAxisType
Annotations []Annotation
}
// GetName returns the name of the time series.
func (as AnnotationSeries) GetName() string {
return as.Name
}
// GetStyle returns the line style.
func (as AnnotationSeries) GetStyle() Style {
return as.Style
}
// GetYAxis returns which YAxis the series draws on.
func (as AnnotationSeries) GetYAxis() YAxisType {
return as.YAxis
}
// Render draws the series.
func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
if as.Style.Show {
style := as.Style.WithDefaultsFrom(Style{
FillColor: DefaultAnnotationFillColor,
FontSize: DefaultAnnotationFontSize,
StrokeColor: defaults.StrokeColor,
StrokeWidth: defaults.StrokeWidth,
Padding: DefaultAnnotationPadding,
})
for _, a := range as.Annotations {
lx := canvasBox.Right - xrange.Translate(a.X)
ly := yrange.Translate(a.Y) + canvasBox.Top
DrawAnnotation(r, canvasBox, xrange, yrange, style, lx, ly, a.Label)
}
}
}

19
axis.go Normal file
View File

@ -0,0 +1,19 @@
package chart
// YAxisType is a type of y-axis; it can either be primary or secondary.
type YAxisType int
const (
// YAxisPrimary is the primary axis.
YAxisPrimary YAxisType = 0
// YAxisSecondary is the secondary axis.
YAxisSecondary YAxisType = 1
)
// Axis is a chart feature detailing what values happen where.
type Axis interface {
GetName() string
GetStyle() Style
GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick
Render(c *Chart, r Renderer, canvasBox Box, ra Range, ticks []Tick)
}

2
box.go
View File

@ -20,7 +20,7 @@ func (b Box) IsZero() bool {
// String returns a string representation of the box. // String returns a string representation of the box.
func (b Box) String() string { func (b Box) String() string {
return fmt.Sprintf("Box(%d,%d,%d,%d)", b.Top, b.Left, b.Right, b.Bottom) return fmt.Sprintf("box(%d,%d,%d,%d)", b.Top, b.Left, b.Right, b.Bottom)
} }
// GetTop returns a coalesced value with a default. // GetTop returns a coalesced value with a default.

577
chart.go
View File

@ -3,9 +3,9 @@ package chart
import ( import (
"errors" "errors"
"io" "io"
"math"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/drawing"
) )
// Chart is what we're drawing. // Chart is what we're drawing.
@ -17,13 +17,12 @@ type Chart struct {
Height int Height int
DPI float64 DPI float64
Background Style Background Style
Canvas Style Canvas Style
Axes Style
FinalValueLabel Style
XRange Range XAxis XAxis
YRange Range YAxis YAxis
YAxisSecondary YAxis
Font *truetype.Font Font *truetype.Font
Series []Series Series []Series
@ -53,179 +52,257 @@ 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(rp RendererProvider, w io.Writer) error {
if len(c.Series) == 0 { if len(c.Series) == 0 {
return errors.New("Please provide at least one series") return errors.New("Please provide at least one series")
} }
r, err := provider(c.Width, c.Height) r, err := rp(c.Width, c.Height)
if err != nil { if err != nil {
return err return err
} }
if c.hasText() {
font, err := c.GetFont() font, err := c.GetFont()
if err != nil { if err != nil {
return err return err
}
r.SetFont(font)
} }
c.Font = font
r.SetFont(font)
r.SetDPI(c.GetDPI(DefaultDPI)) r.SetDPI(c.GetDPI(DefaultDPI))
canvasBox := c.calculateCanvasBox(r) xrange, yrange, yrangeAlt := c.getRanges()
xrange, yrange := c.initRanges(canvasBox) canvasBox := c.getDefaultCanvasBox()
xf, yf, yfa := c.getValueFormatters()
xrange, yrange, yrangeAlt = c.setRangeDomains(canvasBox, xrange, yrange, yrangeAlt)
xticks, yticks, yticksAlt := c.getAxesTicks(r, xrange, yrange, yrangeAlt, xf, yf, yfa)
canvasBox = c.getAdjustedCanvasBox(r, canvasBox, xticks, yticks, yticksAlt)
// we do a second pass to take the updated domains into account
xrange, yrange, yrangeAlt = c.setRangeDomains(canvasBox, xrange, yrange, yrangeAlt)
xticks, yticks, yticksAlt = c.getAxesTicks(r, xrange, yrange, yrangeAlt, xf, yf, yfa)
canvasBox = c.getAdjustedCanvasBox(r, canvasBox, xticks, yticks, yticksAlt)
c.drawBackground(r) c.drawBackground(r)
c.drawCanvas(r, canvasBox) c.drawCanvas(r, canvasBox)
c.drawAxes(r, canvasBox, xrange, yrange) c.drawAxes(r, canvasBox, xrange, yrange, yrangeAlt, xticks, yticks, yticksAlt)
for index, series := range c.Series { for index, series := range c.Series {
c.drawSeries(r, canvasBox, index, series, xrange, yrange) c.drawSeries(r, canvasBox, xrange, yrange, yrangeAlt, series, index)
} }
c.drawTitle(r) c.drawTitle(r)
return r.Save(w) return r.Save(w)
} }
func (c Chart) hasText() bool { func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
return c.TitleStyle.Show || c.Axes.Show || c.FinalValueLabel.Show //iterate over each series, pull out the min/max for x,y
var didSetFirstValues bool
var globalMinX, globalMaxX float64
var globalMinY, globalMaxY float64
var globalMinYA, globalMaxYA float64
for _, s := range c.Series {
if vp, isValueProvider := s.(ValueProvider); isValueProvider {
seriesLength := vp.Len()
for index := 0; index < seriesLength; index++ {
vx, vy := vp.GetValue(index)
if didSetFirstValues {
if globalMinX > vx {
globalMinX = vx
}
if globalMaxX < vx {
globalMaxX = vx
}
if s.GetYAxis() == YAxisPrimary {
if globalMinY > vy {
globalMinY = vy
}
if globalMaxY < vy {
globalMaxY = vy
}
} else if s.GetYAxis() == YAxisSecondary {
if globalMinYA > vy {
globalMinYA = vy
}
if globalMaxYA < vy {
globalMaxYA = vy
}
}
} else {
globalMinX, globalMaxX = vx, vx
if s.GetYAxis() == YAxisPrimary {
globalMinY, globalMaxY = vy, vy
} else if s.GetYAxis() == YAxisSecondary {
globalMinYA, globalMaxYA = vy, vy
}
didSetFirstValues = true
}
}
}
}
if !c.XAxis.Range.IsZero() {
xrange.Min = c.XAxis.Range.Min
xrange.Max = c.XAxis.Range.Max
} else {
xrange.Min = globalMinX
xrange.Max = globalMaxX
}
if !c.YAxis.Range.IsZero() {
yrange.Min = c.YAxis.Range.Min
yrange.Max = c.YAxis.Range.Max
} else {
yrange.Min = globalMinY
yrange.Max = globalMaxY
}
if !c.YAxisSecondary.Range.IsZero() {
yrangeAlt.Min = c.YAxisSecondary.Range.Min
yrangeAlt.Max = c.YAxisSecondary.Range.Max
} else {
yrangeAlt.Min = globalMinYA
yrangeAlt.Max = globalMaxYA
}
return
} }
func (c Chart) getAxisWidth() int { func (c Chart) getDefaultCanvasBox() Box {
asw := 0 dpl := c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left)
if c.Axes.Show { dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
asw = int(c.Axes.GetStrokeWidth(DefaultAxisLineWidth)) dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
}
return asw
}
func (c Chart) calculateCanvasBox(r Renderer) Box {
dpr := DefaultBackgroundPadding.Right
finalLabelWidth := c.calculateFinalLabelWidth(r)
if finalLabelWidth > dpr {
dpr = finalLabelWidth
}
axisBottomHeight := c.calculateBottomLabelHeight()
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),
Left: c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left), Left: dpl,
Right: c.Width - c.Background.Padding.GetRight(dpr), Right: c.Width - dpr,
Bottom: c.Height - c.Background.Padding.GetBottom(dpb), Bottom: c.Height - dpb,
} }
cb.Height = cb.Bottom - cb.Top cb.Height = cb.Bottom - cb.Top
cb.Width = cb.Right - cb.Left cb.Width = cb.Right - cb.Left
return cb return cb
} }
func (c Chart) calculateFinalLabelWidth(r Renderer) int { func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
if !c.FinalValueLabel.Show {
return 0
}
var finalLabelText string
for _, s := range c.Series { for _, s := range c.Series {
_, lv := s.GetValue(s.Len() - 1) if vfp, isVfp := s.(ValueFormatterProvider); isVfp {
var ll string sx, sy := vfp.GetValueFormatters()
if c.YRange.Formatter != nil { if s.GetYAxis() == YAxisPrimary {
ll = c.YRange.Formatter(lv) x = sx
} else { y = sy
ll = s.GetYFormatter()(lv) } else if s.GetYAxis() == YAxisSecondary {
} x = sx
if len(finalLabelText) < len(ll) { ya = sy
finalLabelText = ll
}
}
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)
lsw := int(c.FinalValueLabel.GetStrokeWidth(DefaultAxisLineWidth))
return DefaultFinalLabelDeltaWidth +
pl + pr +
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
var globalMinY, globalMinX float64
var globalMaxY, globalMaxX float64
for _, s := range c.Series {
seriesLength := s.Len()
for index := 0; index < seriesLength; index++ {
vx, vy := s.GetValue(index)
if didSetFirstValues {
if globalMinX > vx {
globalMinX = vx
}
if globalMinY > vy {
globalMinY = vy
}
if globalMaxX < vx {
globalMaxX = vx
}
if globalMaxY < vy {
globalMaxY = vy
}
} else {
globalMinX, globalMaxX = vx, vx
globalMinY, globalMaxY = vy, vy
didSetFirstValues = true
} }
} }
if xrange.Formatter == nil {
xrange.Formatter = s.GetXFormatter()
}
if yrange.Formatter == nil {
yrange.Formatter = s.GetYFormatter()
}
} }
if c.XAxis.ValueFormatter != nil {
if c.XRange.IsZero() { x = c.XAxis.ValueFormatter
xrange.Min = globalMinX
xrange.Max = globalMaxX
} else {
xrange.Min = c.XRange.Min
xrange.Max = c.XRange.Max
} }
if c.XRange.Formatter != nil { if c.YAxis.ValueFormatter != nil {
xrange.Formatter = c.XRange.Formatter y = c.YAxis.ValueFormatter
} }
if c.XRange.Ticks != nil { if c.YAxisSecondary.ValueFormatter != nil {
xrange.Ticks = c.XRange.Ticks ya = c.YAxisSecondary.ValueFormatter
} }
xrange.Domain = canvasBox.Width
if c.YRange.IsZero() {
yrange.Min = globalMinY
yrange.Max = globalMaxY
} else {
yrange.Min = c.YRange.Min
yrange.Max = c.YRange.Max
}
if c.YRange.Formatter != nil {
yrange.Formatter = c.YRange.Formatter
}
if c.YRange.Ticks != nil {
yrange.Ticks = c.YRange.Ticks
}
yrange.Domain = canvasBox.Height
return return
} }
func (c Chart) getAxesTicks(r Renderer, xr, yr, yar Range, xf, yf, yfa ValueFormatter) (xticks, yticks, yticksAlt []Tick) {
if c.XAxis.Style.Show {
xticks = c.XAxis.GetTicks(r, xr, xf)
}
if c.YAxis.Style.Show {
yticks = c.YAxis.GetTicks(r, yr, yf)
}
if c.YAxisSecondary.Style.Show {
yticksAlt = c.YAxisSecondary.GetTicks(r, yar, yf)
}
return
}
func (c Chart) getAdjustedCanvasBox(r Renderer, defaults Box, xticks, yticks, yticksAlt []Tick) Box {
canvasBox := Box{}
var dpl, dpr, dpb int
if c.XAxis.Style.Show {
dpb = c.getXAxisHeight(r, xticks)
}
if c.YAxis.Style.Show {
dpr = c.getYAxisWidth(r, yticks)
}
if c.YAxisSecondary.Style.Show {
dpl = c.getYAxisSecondaryWidth(r, yticksAlt)
}
canvasBox.Top = defaults.Top
if dpl != 0 {
canvasBox.Left = c.Canvas.Padding.GetLeft(dpl)
} else {
canvasBox.Left = defaults.Left
}
if dpr != 0 {
canvasBox.Right = c.Width - c.Canvas.Padding.GetRight(dpr)
} else {
canvasBox.Right = defaults.Right
}
if dpb != 0 {
canvasBox.Bottom = c.Height - c.Canvas.Padding.GetBottom(dpb)
} else {
canvasBox.Bottom = defaults.Bottom
}
canvasBox.Width = canvasBox.Right - canvasBox.Left
canvasBox.Height = canvasBox.Bottom - canvasBox.Top
return canvasBox
}
func (c Chart) getXAxisHeight(r Renderer, ticks []Tick) int {
r.SetFontSize(c.XAxis.Style.GetFontSize(DefaultFontSize))
r.SetFont(c.XAxis.Style.GetFont(c.Font))
var tl int
for _, t := range ticks {
_, lh := r.MeasureText(t.Label)
if lh > tl {
tl = lh
}
}
return tl + DefaultXAxisMargin
}
func (c Chart) getYAxisWidth(r Renderer, ticks []Tick) int {
var ll string
for _, t := range ticks {
if len(t.Label) > len(ll) {
ll = t.Label
}
}
r.SetFontSize(c.YAxis.Style.GetFontSize(DefaultFontSize))
r.SetFont(c.YAxis.Style.GetFont(c.Font))
tw, _ := r.MeasureText(ll)
return tw + DefaultYAxisMargin
}
func (c Chart) getYAxisSecondaryWidth(r Renderer, ticks []Tick) int {
var ll string
for _, t := range ticks {
if len(t.Label) > len(ll) {
ll = t.Label
}
}
r.SetFontSize(c.YAxisSecondary.Style.GetFontSize(DefaultFontSize))
r.SetFont(c.YAxisSecondary.Style.GetFont(c.Font))
tw, _ := r.MeasureText(ll)
return tw + DefaultYAxisMargin
}
func (c Chart) setRangeDomains(canvasBox Box, xrange, yrange, yrangeAlt Range) (Range, Range, Range) {
xrange.Domain = canvasBox.Width
yrange.Domain = canvasBox.Height
yrangeAlt.Domain = canvasBox.Height
return xrange, yrange, yrangeAlt
}
func (c Chart) drawBackground(r Renderer) { func (c Chart) drawBackground(r Renderer) {
r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor)) r.SetFillColor(c.Background.GetFillColor(DefaultBackgroundColor))
r.SetStrokeColor(c.Background.GetStrokeColor(DefaultBackgroundStrokeColor)) r.SetStrokeColor(c.Background.GetStrokeColor(DefaultBackgroundStrokeColor))
@ -252,202 +329,52 @@ func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
r.FillStroke() r.FillStroke()
} }
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange Range) { func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
if c.Axes.Show { if c.XAxis.Style.Show {
r.SetStrokeColor(c.Axes.GetStrokeColor(DefaultAxisColor)) c.XAxis.Render(r, canvasBox, xrange, xticks)
r.SetStrokeWidth(c.Axes.GetStrokeWidth(DefaultStrokeWidth)) }
r.MoveTo(canvasBox.Left, canvasBox.Bottom) if c.YAxis.Style.Show {
r.LineTo(canvasBox.Right, canvasBox.Bottom) c.YAxis.Render(r, canvasBox, yrange, YAxisPrimary, yticks)
r.LineTo(canvasBox.Right, canvasBox.Top) }
r.Stroke() if c.YAxisSecondary.Style.Show {
c.YAxisSecondary.Render(r, canvasBox, yrangeAlt, YAxisSecondary, yticksAlt)
c.drawXAxisLabels(r, canvasBox, xrange)
c.drawYAxisLabels(r, canvasBox, yrange)
} }
} }
func (c Chart) generateRangeTicks(r Range, tickCount int, offset float64) []Tick { func (c Chart) getSeriesStyleDefaults(seriesIndex int) Style {
var ticks []Tick strokeColor := GetDefaultSeriesStrokeColor(seriesIndex)
rangeTicks := Slices(tickCount, r.Max-r.Min) return Style{
for _, rv := range rangeTicks { StrokeColor: strokeColor,
ticks = append(ticks, Tick{ StrokeWidth: DefaultStrokeWidth,
RangeValue: rv + offset, FillColor: strokeColor.WithAlpha(100),
Label: r.Format(rv + offset), Font: c.Font,
}) FontSize: DefaultFontSize,
}
return ticks
}
func (c Chart) drawYAxisLabels(r Renderer, canvasBox Box, yrange Range) {
tickFontSize := c.Axes.GetFontSize(DefaultAxisFontSize)
asw := c.getAxisWidth()
tx := canvasBox.Right + DefaultFinalLabelDeltaWidth + asw
r.SetFontColor(c.Axes.GetFontColor(DefaultAxisColor))
r.SetFontSize(tickFontSize)
ticks := yrange.Ticks
if ticks == nil {
minimumTickHeight := tickFontSize + DefaultMinimumTickVerticalSpacing
tickCount := int(math.Floor(float64(yrange.Domain) / float64(minimumTickHeight)))
if tickCount > DefaultMaxTickCount {
tickCount = DefaultMaxTickCount
}
ticks = c.generateRangeTicks(yrange, tickCount, yrange.Min)
}
for _, t := range ticks {
v := t.RangeValue
y := yrange.Translate(v)
ty := int(y)
r.Text(t.Label, tx, ty)
} }
} }
func (c Chart) drawXAxisLabels(r Renderer, canvasBox Box, xrange Range) { func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) {
tickFontSize := c.Axes.GetFontSize(DefaultAxisFontSize) if s.GetYAxis() == YAxisPrimary {
ty := canvasBox.Bottom + DefaultXAxisMargin + int(tickFontSize) s.Render(r, canvasBox, xrange, yrange, c.getSeriesStyleDefaults(seriesIndex))
} else if s.GetYAxis() == YAxisSecondary {
r.SetFontColor(c.Axes.GetFontColor(DefaultAxisColor)) s.Render(r, canvasBox, xrange, yrangeAlt, c.getSeriesStyleDefaults(seriesIndex))
r.SetFontSize(tickFontSize)
ticks := xrange.Ticks
if ticks == nil {
maxLabelWidth := 60
minimumTickWidth := maxLabelWidth + DefaultMinimumTickHorizontalSpacing
tickCount := int(math.Floor(float64(xrange.Domain) / float64(minimumTickWidth)))
if tickCount > DefaultMaxTickCount {
tickCount = DefaultMaxTickCount
}
ticks = c.generateRangeTicks(xrange, tickCount, xrange.Min)
}
for _, t := range ticks {
v := t.RangeValue
x := xrange.Translate(v)
tx := canvasBox.Left + int(x)
r.Text(t.Label, tx, ty)
} }
} }
func (c Chart) drawSeries(r Renderer, canvasBox Box, index int, s Series, xrange, yrange Range) { func (c Chart) drawTitle(r Renderer) {
if s.Len() == 0 {
return
}
cx := canvasBox.Left
cy := canvasBox.Top
cb := canvasBox.Bottom
cw := canvasBox.Width
v0x, v0y := s.GetValue(0)
x0 := cw - xrange.Translate(v0x)
y0 := yrange.Translate(v0y)
var vx, vy float64
var x, y int
fill := s.GetStyle().GetFillColor()
if !fill.IsZero() {
r.SetFillColor(fill)
r.MoveTo(x0+cx, y0+cy)
for i := 1; i < s.Len(); i++ {
vx, vy = s.GetValue(i)
x = cw - xrange.Translate(vx)
y = yrange.Translate(vy)
r.LineTo(x+cx, y+cy)
}
r.LineTo(x+cx, cb)
r.LineTo(x0+cx, cb)
r.Close()
r.Fill()
}
stroke := s.GetStyle().GetStrokeColor(GetDefaultSeriesStrokeColor(index))
r.SetStrokeColor(stroke)
r.SetStrokeWidth(s.GetStyle().GetStrokeWidth(DefaultStrokeWidth))
r.MoveTo(x0+cx, y0+cy)
for i := 1; i < s.Len(); i++ {
vx, vy = s.GetValue(i)
x = cw - xrange.Translate(vx)
y = yrange.Translate(vy)
r.LineTo(x+cx, y+cy)
}
r.Stroke()
c.drawFinalValueLabel(r, canvasBox, index, s, yrange)
}
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 := yrange.Format(lv)
py := canvasBox.Top
ly := yrange.Translate(lv) + py
r.SetFontSize(c.FinalValueLabel.GetFontSize(DefaultFinalLabelFontSize))
textWidth, _ := r.MeasureText(ll)
textHeight := int(math.Floor(DefaultFinalLabelFontSize))
halfTextHeight := textHeight >> 1
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)
pr := c.FinalValueLabel.Padding.GetRight(DefaultFinalLabelPadding.Right)
pb := c.FinalValueLabel.Padding.GetBottom(DefaultFinalLabelPadding.Bottom)
textX := cx + pl + DefaultFinalLabelDeltaWidth
textY := ly + halfTextHeight
ltlx := cx + pl + DefaultFinalLabelDeltaWidth
ltly := ly - (pt + halfTextHeight)
ltrx := cx + pl + pr + textWidth
ltry := ly - (pt + halfTextHeight)
lbrx := cx + pl + pr + textWidth
lbry := ly + (pb + halfTextHeight)
lblx := cx + DefaultFinalLabelDeltaWidth
lbly := ly + (pb + halfTextHeight)
//draw the shape...
r.SetFillColor(c.FinalValueLabel.GetFillColor(DefaultFinalLabelBackgroundColor))
r.SetStrokeColor(c.FinalValueLabel.GetStrokeColor(s.GetStyle().GetStrokeColor(GetDefaultSeriesStrokeColor(index))))
r.SetStrokeWidth(c.FinalValueLabel.GetStrokeWidth(DefaultAxisLineWidth))
r.MoveTo(cx, ly)
r.LineTo(ltlx, ltly)
r.LineTo(ltrx, ltry)
r.LineTo(lbrx, lbry)
r.LineTo(lblx, lbly)
r.LineTo(cx, ly)
r.Close()
r.FillStroke()
r.SetFontColor(c.FinalValueLabel.GetFontColor(DefaultTextColor))
r.Text(ll, textX, textY)
}
}
func (c Chart) drawTitle(r Renderer) error {
if len(c.Title) > 0 && c.TitleStyle.Show { if len(c.Title) > 0 && c.TitleStyle.Show {
r.SetFontColor(c.Canvas.GetFontColor(DefaultTextColor)) r.SetFont(c.TitleStyle.GetFont(c.Font))
titleFontSize := c.Canvas.GetFontSize(DefaultTitleFontSize) r.SetFontColor(c.TitleStyle.GetFontColor(DefaultTextColor))
titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)
r.SetFontSize(titleFontSize) r.SetFontSize(titleFontSize)
textWidth, _ := r.MeasureText(c.Title)
textWidthPoints, textHeightPoints := r.MeasureText(c.Title)
textWidth := int(drawing.PointsToPixels(r.GetDPI(), float64(textWidthPoints)))
textHeight := int(drawing.PointsToPixels(r.GetDPI(), float64(textHeightPoints)))
titleX := (c.Width >> 1) - (textWidth >> 1) titleX := (c.Width >> 1) - (textWidth >> 1)
titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + int(titleFontSize) titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
r.Text(c.Title, titleX, titleY) r.Text(c.Title, titleX, titleY)
} }
return nil
} }

50
continuous_series.go Normal file
View File

@ -0,0 +1,50 @@
package chart
// ContinuousSeries represents a line on a chart.
type ContinuousSeries struct {
Name string
Style Style
YAxis YAxisType
XValues []float64
YValues []float64
}
// GetName returns the name of the time series.
func (cs ContinuousSeries) GetName() string {
return cs.Name
}
// GetStyle returns the line style.
func (cs ContinuousSeries) GetStyle() Style {
return cs.Style
}
// Len returns the number of elements in the series.
func (cs ContinuousSeries) Len() int {
return len(cs.XValues)
}
// GetValue gets a value at a given index.
func (cs ContinuousSeries) GetValue(index int) (float64, float64) {
return cs.XValues[index], cs.YValues[index]
}
// GetValueFormatters returns value formatter defaults for the series.
func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) {
x = FloatValueFormatter
y = FloatValueFormatter
return
}
// GetYAxis returns which YAxis the series draws on.
func (cs ContinuousSeries) GetYAxis() YAxisType {
return cs.YAxis
}
// Render renders the series.
func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := cs.Style.WithDefaultsFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, cs)
}

View File

@ -24,24 +24,33 @@ const (
DefaultFontSize = 10.0 DefaultFontSize = 10.0
// DefaultTitleFontSize is the default title font size. // DefaultTitleFontSize is the default title font size.
DefaultTitleFontSize = 18.0 DefaultTitleFontSize = 18.0
// DefaultFinalLabelDeltaWidth is the width of the left triangle out of the final label. // DefaultAnnotationDeltaWidth is the width of the left triangle out of annotations.
DefaultFinalLabelDeltaWidth = 10 DefaultAnnotationDeltaWidth = 10
// DefaultFinalLabelFontSize is the font size of the final label. // DefaultAnnotationFontSize is the font size of annotations.
DefaultFinalLabelFontSize = 10.0 DefaultAnnotationFontSize = 10.0
// DefaultAxisFontSize is the font size of the axis labels. // DefaultAxisFontSize is the font size of the axis labels.
DefaultAxisFontSize = 10.0 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
// DefaultYAxisMargin is the default distance from the right of the canvas to the y axis labels.
DefaultYAxisMargin = 10
// DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels. // DefaultXAxisMargin is the default distance from bottom of the canvas to the x axis labels.
DefaultXAxisMargin = 10 DefaultXAxisMargin = 10
//DefaultVerticalTickWidth is half the margin.
DefaultVerticalTickWidth = DefaultYAxisMargin >> 1
//DefaultHorizontalTickWidth is half the margin.
DefaultHorizontalTickWidth = DefaultXAxisMargin >> 1
// DefaultMinimumTickHorizontalSpacing is the minimum distance between horizontal ticks. // DefaultMinimumTickHorizontalSpacing is the minimum distance between horizontal ticks.
DefaultMinimumTickHorizontalSpacing = 20 DefaultMinimumTickHorizontalSpacing = 20
// DefaultMinimumTickVerticalSpacing is the minimum distance between vertical ticks. // DefaultMinimumTickVerticalSpacing is the minimum distance between vertical ticks.
DefaultMinimumTickVerticalSpacing = 20 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 (
@ -69,8 +78,8 @@ var (
// DefaultFillColor is the default fill color. // DefaultFillColor is the default fill color.
// It is equivalent to #0074d9. // It is equivalent to #0074d9.
DefaultFillColor = drawing.Color{R: 0, G: 217, B: 116, A: 255} DefaultFillColor = drawing.Color{R: 0, G: 217, B: 116, A: 255}
// DefaultFinalLabelBackgroundColor is the default final label background color. // DefaultAnnotationFillColor is the default annotation background color.
DefaultFinalLabelBackgroundColor = drawing.Color{R: 255, G: 255, B: 255, A: 255} DefaultAnnotationFillColor = drawing.Color{R: 255, G: 255, B: 255, A: 255}
) )
var ( var (
@ -90,8 +99,8 @@ func GetDefaultSeriesStrokeColor(index int) drawing.Color {
} }
var ( var (
// DefaultFinalLabelPadding is the padding around the final label. // DefaultAnnotationPadding is the padding around an annotation.
DefaultFinalLabelPadding = Box{Top: 5, Left: 0, Right: 7, Bottom: 5} DefaultAnnotationPadding = Box{Top: 5, Left: 0, Right: 7, Bottom: 5}
// DefaultBackgroundPadding is the default canvas padding config. // DefaultBackgroundPadding is the default canvas padding config.
DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5} DefaultBackgroundPadding = Box{Top: 5, Left: 5, Right: 5, Bottom: 5}
) )

95
drawing_helpers.go Normal file
View File

@ -0,0 +1,95 @@
package chart
import "math"
// DrawLineSeries draws a line series with a renderer.
func DrawLineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider) {
if vs.Len() == 0 {
return
}
ct := canvasBox.Top
cb := canvasBox.Bottom
cr := canvasBox.Right
v0x, v0y := vs.GetValue(0)
x0 := cr - xrange.Translate(v0x)
y0 := yrange.Translate(v0y) + ct
var vx, vy float64
var x, y int
fill := s.GetFillColor()
if !fill.IsZero() {
r.SetFillColor(fill)
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValue(i)
x = cr - xrange.Translate(vx)
y = yrange.Translate(vy) + ct
r.LineTo(x, y)
}
r.LineTo(x, cb)
r.LineTo(x0, cb)
r.Close()
r.Fill()
}
stroke := s.GetStrokeColor()
r.SetStrokeColor(stroke)
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
r.MoveTo(x0, y0)
for i := 1; i < vs.Len(); i++ {
vx, vy = vs.GetValue(i)
x = cr - xrange.Translate(vx)
y = yrange.Translate(vy) + ct
r.LineTo(x, y)
}
r.Stroke()
}
// DrawAnnotation draws an anotation with a renderer.
func DrawAnnotation(r Renderer, canvasBox Box, xrange, yrange Range, s Style, lx, ly int, label string) {
r.SetFontSize(s.GetFontSize(DefaultAnnotationFontSize))
textWidth, _ := r.MeasureText(label)
textHeight := int(math.Floor(DefaultAnnotationFontSize))
halfTextHeight := textHeight >> 1
pt := s.Padding.GetTop(DefaultAnnotationPadding.Top)
pl := s.Padding.GetLeft(DefaultAnnotationPadding.Left)
pr := s.Padding.GetRight(DefaultAnnotationPadding.Right)
pb := s.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
textX := lx + pl + DefaultAnnotationDeltaWidth
textY := ly + halfTextHeight
ltlx := lx + pl + DefaultAnnotationDeltaWidth
ltly := ly - (pt + halfTextHeight)
ltrx := lx + pl + pr + textWidth
ltry := ly - (pt + halfTextHeight)
lbrx := lx + pl + pr + textWidth
lbry := ly + (pb + halfTextHeight)
lblx := lx + DefaultAnnotationDeltaWidth
lbly := ly + (pb + halfTextHeight)
//draw the shape...
r.SetFillColor(s.GetFillColor(DefaultAnnotationFillColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.MoveTo(lx, ly)
r.LineTo(ltlx, ltly)
r.LineTo(ltrx, ltry)
r.LineTo(lbrx, lbry)
r.LineTo(lblx, lbly)
r.LineTo(lx, ly)
r.Close()
r.FillStroke()
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.Text(label, textX, textY)
}

View File

@ -1,4 +0,0 @@
package chart
// Formatter is a function that takes a value and produces a string.
type Formatter func(v interface{}) string

View File

@ -3,23 +3,13 @@ package chart
import ( import (
"fmt" "fmt"
"math" "math"
"github.com/blendlabs/go-util"
) )
// Tick represents a label on an axis. // Range represents a boundary for a set of numbers.
type Tick struct {
RangeValue float64
Label string
}
// Range represents a continuous range,
type Range struct { type Range struct {
Min float64 Min float64
Max float64 Max float64
Domain int Domain int
Ticks []Tick
Formatter Formatter
} }
// IsZero returns if the range has been set or not. // IsZero returns if the range has been set or not.
@ -37,14 +27,6 @@ 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

View File

@ -33,6 +33,11 @@ type rasterRenderer struct {
f *truetype.Font f *truetype.Font
} }
// GetDPI returns the dpi.
func (rr *rasterRenderer) GetDPI() float64 {
return rr.gc.GetDPI()
}
// SetDPI implements the interface method. // SetDPI implements the interface method.
func (rr *rasterRenderer) SetDPI(dpi float64) { func (rr *rasterRenderer) SetDPI(dpi float64) {
rr.gc.SetDPI(dpi) rr.gc.SetDPI(dpi)

6
renderable.go Normal file
View File

@ -0,0 +1,6 @@
package chart
// Renderable is a type that can be rendered onto a chart.
type Renderable interface {
Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style)
}

View File

@ -7,11 +7,11 @@ import (
"github.com/wcharczuk/go-chart/drawing" "github.com/wcharczuk/go-chart/drawing"
) )
// RendererProvider is a function that returns a renderer.
type RendererProvider func(int, int) (Renderer, error)
// Renderer represents the basic methods required to draw a chart. // Renderer represents the basic methods required to draw a chart.
type Renderer interface { type Renderer interface {
// GetDPI gets the DPI for the renderer.
GetDPI() float64
// SetDPI sets the DPI for the renderer. // SetDPI sets the DPI for the renderer.
SetDPI(dpi float64) SetDPI(dpi float64)

4
renderer_provider.go Normal file
View File

@ -0,0 +1,4 @@
package chart
// RendererProvider is a function that returns a renderer.
type RendererProvider func(int, int) (Renderer, error)

120
series.go
View File

@ -1,121 +1,7 @@
package chart package chart
import ( // Series is an alias to Renderable.
"fmt"
"time"
"github.com/blendlabs/go-util"
)
// Series is a entity data set.
type Series interface { type Series interface {
GetName() string GetYAxis() YAxisType
GetStyle() Style Renderable
Len() int
GetValue(index int) (float64, float64)
GetXFormatter() Formatter
GetYFormatter() Formatter
}
// TimeSeries is a line on a chart.
type TimeSeries struct {
Name string
Style Style
XValues []time.Time
YValues []float64
}
// GetName returns the name of the time series.
func (ts TimeSeries) GetName() string {
return ts.Name
}
// GetStyle returns the line style.
func (ts TimeSeries) GetStyle() Style {
return ts.Style
}
// Len returns the number of elements in the series.
func (ts TimeSeries) Len() int {
return len(ts.XValues)
}
// GetValue gets a value at a given index.
func (ts TimeSeries) GetValue(index int) (x float64, y float64) {
x = float64(ts.XValues[index].Unix())
y = 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
}
}
// ContinuousSeries represents a line on a chart.
type ContinuousSeries struct {
Name string
Style Style
XValues []float64
YValues []float64
}
// GetName returns the name of the time series.
func (cs ContinuousSeries) GetName() string {
return cs.Name
}
// GetStyle returns the line style.
func (cs ContinuousSeries) GetStyle() Style {
return cs.Style
}
// Len returns the number of elements in the series.
func (cs ContinuousSeries) Len() int {
return len(cs.XValues)
}
// GetValue gets a value at a given index.
func (cs ContinuousSeries) GetValue(index int) (float64, float64) {
return cs.XValues[index], cs.YValues[index]
}
// GetXFormatter returns the xs value formatter.
func (cs ContinuousSeries) 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 ContinuousSeries) GetYFormatter() Formatter {
return cs.GetXFormatter()
} }

View File

@ -4,23 +4,28 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/drawing" "github.com/wcharczuk/go-chart/drawing"
) )
// Style is a simple style set. // Style is a simple style set.
type Style struct { type Style struct {
Show bool Show bool
StrokeColor drawing.Color Padding Box
FillColor drawing.Color
StrokeWidth float64 StrokeWidth float64
FontSize float64 StrokeColor drawing.Color
FontColor drawing.Color
Padding Box FillColor drawing.Color
FontSize float64
FontColor drawing.Color
Font *truetype.Font
} }
// IsZero returns if the object is set or not. // IsZero returns if the object is set or not.
func (s Style) IsZero() bool { func (s Style) IsZero() bool {
return s.StrokeColor.IsZero() && s.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontSize == 0 return s.StrokeColor.IsZero() && s.FillColor.IsZero() && s.StrokeWidth == 0 && s.FontSize == 0 && s.Font == nil
} }
// GetStrokeColor returns the stroke color. // GetStrokeColor returns the stroke color.
@ -78,6 +83,39 @@ func (s Style) GetFontColor(defaults ...drawing.Color) drawing.Color {
return s.FontColor return s.FontColor
} }
// GetFont returns the font face.
func (s Style) GetFont(defaults ...*truetype.Font) *truetype.Font {
if s.Font == nil {
if len(defaults) > 0 {
return defaults[0]
}
return nil
}
return s.Font
}
// GetPadding returns the padding.
func (s Style) GetPadding(defaults ...Box) Box {
if s.Padding.IsZero() {
if len(defaults) > 0 {
return defaults[0]
}
return Box{}
}
return s.Padding
}
// WithDefaultsFrom coalesces two styles into a new style.
func (s Style) WithDefaultsFrom(defaults Style) (final Style) {
final.FillColor = s.GetFillColor(defaults.FillColor)
final.FontColor = s.GetFontColor(defaults.FontColor)
final.Font = s.GetFont(defaults.Font)
final.Padding = s.GetPadding(defaults.Padding)
final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth)
return
}
// SVG returns the style as a svg style string. // SVG returns the style as a svg style string.
func (s Style) SVG(dpi float64) string { func (s Style) SVG(dpi float64) string {
sw := s.StrokeWidth sw := s.StrokeWidth

View File

@ -5,7 +5,6 @@ import (
"net/http" "net/http"
"github.com/wcharczuk/go-chart" "github.com/wcharczuk/go-chart"
"github.com/wcharczuk/go-chart/drawing"
"github.com/wcharczuk/go-web" "github.com/wcharczuk/go-web"
) )
@ -21,38 +20,82 @@ func chartHandler(rc *web.RequestContext) web.ControllerResult {
rc.Response.Header().Set("Content-Type", "image/svg+xml") rc.Response.Header().Set("Content-Type", "image/svg+xml")
} }
s1x := []float64{2.0, 3.0, 4.0, 5.0}
s1y := []float64{2.5, 5.0, 2.0, 3.3}
s2x := []float64{0.0, 0.5, 1.0, 1.5}
s2y := []float64{1.1, 1.2, 1.0, 1.3}
c := chart.Chart{ c := chart.Chart{
Title: "A Test Chart", Title: "A Test Chart",
TitleStyle: chart.Style{ TitleStyle: chart.Style{
Show: true,
FontSize: 26.0,
},
Width: 640,
Height: 480,
Axes: chart.Style{
Show: true,
StrokeWidth: 1.0,
},
YRange: chart.Range{
Min: 0.0,
Max: 7.0,
},
FinalValueLabel: chart.Style{
Show: true, Show: true,
}, },
Width: 1024,
Height: 400,
XAxis: chart.XAxis{
Style: chart.Style{
Show: true,
},
},
YAxis: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: chart.Range{
Min: 0.0,
Max: 7.0,
},
},
YAxisSecondary: chart.YAxis{
Style: chart.Style{
Show: true,
},
Range: chart.Range{
Min: 0.8,
Max: 1.5,
},
},
Series: []chart.Series{ Series: []chart.Series{
chart.ContinuousSeries{ chart.ContinuousSeries{
Name: "a", Name: "a",
XValues: []float64{1.0, 2.0, 3.0, 4.0}, XValues: s1x,
YValues: []float64{2.5, 5.0, 2.0, 3.3}, YValues: s1y,
Style: chart.Style{
FillColor: drawing.Color{R: 0, G: 116, B: 217, A: 128},
},
}, },
chart.ContinuousSeries{ chart.ContinuousSeries{
Name: "b", Name: "b",
XValues: []float64{3.0, 4.0, 5.0, 6.0}, YAxis: chart.YAxisSecondary,
YValues: []float64{6.0, 5.0, 4.0, 1.0}, XValues: s2x,
YValues: s2y,
},
chart.AnnotationSeries{
Name: "a - last value",
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultSeriesStrokeColor(0),
},
Annotations: []chart.Annotation{
chart.Annotation{
X: s1x[len(s1x)-1],
Y: s1y[len(s1y)-1],
Label: chart.FloatValueFormatter(s1y[len(s1y)-1]),
},
},
},
chart.AnnotationSeries{
Name: "b - last value",
YAxis: chart.YAxisSecondary,
Style: chart.Style{
Show: true,
StrokeColor: chart.GetDefaultSeriesStrokeColor(1),
},
Annotations: []chart.Annotation{
chart.Annotation{
X: s2x[len(s2x)-1],
Y: s2y[len(s2y)-1],
Label: chart.FloatValueFormatter(s2y[len(s2y)-1]),
},
},
}, },
}, },
} }
@ -74,6 +117,9 @@ func main() {
app.SetName("Chart Test Server") app.SetName("Chart Test Server")
app.SetLogger(web.NewStandardOutputLogger()) app.SetLogger(web.NewStandardOutputLogger())
app.GET("/", chartHandler) app.GET("/", chartHandler)
app.GET("/:format", chartHandler) app.GET("/format/:format", chartHandler)
app.GET("/favico.ico", func(rc *web.RequestContext) web.ControllerResult {
return rc.Raw([]byte{})
})
log.Fatal(app.Start()) log.Fatal(app.Start())
} }

25
tick.go Normal file
View File

@ -0,0 +1,25 @@
package chart
// Tick represents a label on an axis.
type Tick struct {
Value float64
Label string
}
// Ticks is an array of ticks.
type Ticks []Tick
// Len returns the length of the ticks set.
func (t Ticks) Len() int {
return len(t)
}
// Swap swaps two elements.
func (t Ticks) Swap(i, j int) {
t[i], t[j] = t[j], t[i]
}
// Less returns if i's value is less than j's value.
func (t Ticks) Less(i, j int) bool {
return t[i].Value < t[j].Value
}

54
time_series.go Normal file
View File

@ -0,0 +1,54 @@
package chart
import "time"
// TimeSeries is a line on a chart.
type TimeSeries struct {
Name string
Style Style
YAxis YAxisType
XValues []time.Time
YValues []float64
}
// GetName returns the name of the time series.
func (ts TimeSeries) GetName() string {
return ts.Name
}
// GetStyle returns the line style.
func (ts TimeSeries) GetStyle() Style {
return ts.Style
}
// Len returns the number of elements in the series.
func (ts TimeSeries) Len() int {
return len(ts.XValues)
}
// GetValue gets a value at a given index.
func (ts TimeSeries) GetValue(index int) (x float64, y float64) {
x = float64(ts.XValues[index].Unix())
y = ts.YValues[index]
return
}
// GetValueFormatters returns value formatter defaults for the series.
func (ts TimeSeries) GetValueFormatters() (x, y ValueFormatter) {
x = TimeValueFormatter
y = FloatValueFormatter
return
}
// GetYAxis returns which YAxis the series draws on.
func (ts TimeSeries) GetYAxis() YAxisType {
return ts.YAxis
}
// Render renders the series.
func (ts TimeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := ts.Style.WithDefaultsFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, ts)
}

30
time_series_test.go Normal file
View File

@ -0,0 +1,30 @@
package chart
import (
"testing"
"time"
"github.com/blendlabs/go-assert"
)
func TestTimeSeriesGetValue(t *testing.T) {
assert := assert.New(t)
ts := TimeSeries{
Name: "Test",
XValues: []time.Time{
time.Now().AddDate(0, 0, -5),
time.Now().AddDate(0, 0, -4),
time.Now().AddDate(0, 0, -3),
time.Now().AddDate(0, 0, -2),
time.Now().AddDate(0, 0, -1),
},
YValues: []float64{
1.0, 2.0, 3.0, 4.0, 5.0,
},
}
x0, y0 := ts.GetValue(0)
assert.NotZero(x0)
assert.Equal(1.0, y0)
}

View File

@ -55,6 +55,10 @@ func Slices(count int, total float64) []float64 {
return values return values
} }
func flf(v float64) string { // Float is an alias for float64 that provides a better .String() method.
return fmt.Sprintf("%.2f", v) type Float float64
// String returns the string representation of a float.
func (f Float) String() string {
return fmt.Sprintf("%.2f", f)
} }

41
value_formatter.go Normal file
View File

@ -0,0 +1,41 @@
package chart
import (
"fmt"
"time"
)
// ValueFormatter is a function that takes a value and produces a string.
type ValueFormatter func(v interface{}) string
// TimeValueFormatter is a ValueFormatter for timestamps.
func TimeValueFormatter(v interface{}) string {
return TimeValueFormatterWithFormat(v, DefaultDateFormat)
}
// TimeValueFormatterWithFormat is a ValueFormatter for timestamps with a given format.
func TimeValueFormatterWithFormat(v interface{}, dateFormat string) string {
if typed, isTyped := v.(time.Time); isTyped {
return typed.Format(dateFormat)
}
if typed, isTyped := v.(int64); isTyped {
return time.Unix(typed, 0).Format(dateFormat)
}
if typed, isTyped := v.(float64); isTyped {
return time.Unix(int64(typed), 0).Format(dateFormat)
}
return ""
}
// FloatValueFormatter is a ValueFormatter for float64.
func FloatValueFormatter(v interface{}) string {
return FloatValueFormatterWithFormat(v, "%.2f")
}
// FloatValueFormatterWithFormat is a ValueFormatter for float64 with a given format.
func FloatValueFormatterWithFormat(v interface{}, floatFormat string) string {
if typed, isTyped := v.(float64); isTyped {
return fmt.Sprintf(floatFormat, typed)
}
return ""
}

View File

@ -0,0 +1,6 @@
package chart
// ValueFormatterProvider is a series that has custom formatters.
type ValueFormatterProvider interface {
GetValueFormatters() (x, y ValueFormatter)
}

7
value_provider.go Normal file
View File

@ -0,0 +1,7 @@
package chart
// ValueProvider is a type that produces values.
type ValueProvider interface {
Len() int
GetValue(index int) (float64, float64)
}

View File

@ -36,6 +36,11 @@ type vectorRenderer struct {
fc *font.Drawer fc *font.Drawer
} }
// GetDPI returns the dpi.
func (vr *vectorRenderer) GetDPI() float64 {
return vr.dpi
}
// SetDPI implements the interface method. // SetDPI implements the interface method.
func (vr *vectorRenderer) SetDPI(dpi float64) { func (vr *vectorRenderer) SetDPI(dpi float64) {
vr.dpi = dpi vr.dpi = dpi

101
xaxis.go Normal file
View File

@ -0,0 +1,101 @@
package chart
import (
"math"
"sort"
"github.com/wcharczuk/go-chart/drawing"
)
// XAxis represents the horizontal axis.
type XAxis struct {
Name string
Style Style
ValueFormatter ValueFormatter
Range Range
Ticks []Tick
}
// GetName returns the name.
func (xa XAxis) GetName() string {
return xa.Name
}
// GetStyle returns the style.
func (xa XAxis) GetStyle() Style {
return xa.Style
}
// GetTicks returns the ticks for a series. It coalesces between user provided ticks and
// generated ticks.
func (xa XAxis) GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick {
if len(xa.Ticks) > 0 {
return xa.Ticks
}
return xa.generateTicks(r, ra, vf)
}
func (xa XAxis) generateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick {
step := xa.getTickStep(r, ra, vf)
return xa.generateTicksWithStep(ra, step, vf)
}
func (xa XAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int {
fontSize := xa.Style.GetFontSize(DefaultFontSize)
r.SetFontSize(fontSize)
// take a cut at determining the 'widest' value.
l0 := vf(ra.Min)
ln := vf(ra.Max)
ll := l0
if len(ln) > len(l0) {
ll = ln
}
llw, _ := r.MeasureText(ll)
textWidth := drawing.PointsToPixels(r.GetDPI(), float64(llw))
width := textWidth + DefaultMinimumTickHorizontalSpacing
count := int(math.Ceil(float64(ra.Domain) / float64(width)))
return count
}
func (xa XAxis) getTickStep(r Renderer, ra Range, vf ValueFormatter) float64 {
tickCount := xa.getTickCount(r, ra, vf)
step := ra.Delta() / float64(tickCount)
return step
}
func (xa XAxis) generateTicksWithStep(ra Range, step float64, vf ValueFormatter) []Tick {
var ticks []Tick
for cursor := ra.Min; cursor < ra.Max; cursor += step {
ticks = append(ticks, Tick{
Value: cursor,
Label: vf(cursor),
})
}
return ticks
}
// Render renders the axis
func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, ticks []Tick) {
tickFontSize := xa.Style.GetFontSize(DefaultFontSize)
tickHeight := drawing.PointsToPixels(r.GetDPI(), tickFontSize)
ty := canvasBox.Bottom + DefaultXAxisMargin + int(tickHeight)
r.SetStrokeColor(xa.Style.GetStrokeColor(DefaultAxisColor))
r.SetStrokeWidth(xa.Style.GetStrokeWidth(DefaultAxisLineWidth))
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
r.LineTo(canvasBox.Right, canvasBox.Bottom)
r.Stroke()
r.SetFontColor(xa.Style.GetFontColor(DefaultAxisColor))
r.SetFontSize(tickFontSize)
sort.Sort(Ticks(ticks))
for _, t := range ticks {
v := t.Value
x := ra.Translate(v)
tx := canvasBox.Right - x
r.Text(t.Label, tx, ty)
}
}

125
yaxis.go Normal file
View File

@ -0,0 +1,125 @@
package chart
import (
"math"
"sort"
)
// YAxis is a veritcal rule of the range.
// There can be (2) y-axes; a primary and secondary.
type YAxis struct {
Name string
Style Style
ValueFormatter ValueFormatter
Range Range
Ticks []Tick
}
// GetName returns the name.
func (ya YAxis) GetName() string {
return ya.Name
}
// GetStyle returns the style.
func (ya YAxis) GetStyle() Style {
return ya.Style
}
// GetTicks returns the ticks for a series. It coalesces between user provided ticks and
// generated ticks.
func (ya YAxis) GetTicks(r Renderer, ra Range, vf ValueFormatter) []Tick {
if len(ya.Ticks) > 0 {
return ya.Ticks
}
return ya.generateTicks(r, ra, vf)
}
func (ya YAxis) generateTicks(r Renderer, ra Range, vf ValueFormatter) []Tick {
step := ya.getTickStep(r, ra, vf)
return ya.generateTicksWithStep(ra, step, vf)
}
func (ya YAxis) getTickCount(r Renderer, ra Range, vf ValueFormatter) int {
textHeight := int(ya.Style.GetFontSize(DefaultFontSize))
height := textHeight + DefaultMinimumTickVerticalSpacing
count := int(math.Ceil(float64(ra.Domain) / float64(height)))
return count
}
func (ya YAxis) getTickStep(r Renderer, ra Range, vf ValueFormatter) float64 {
tickCount := ya.getTickCount(r, ra, vf)
return ra.Delta() / float64(tickCount)
}
func (ya YAxis) generateTicksWithStep(ra Range, step float64, vf ValueFormatter) []Tick {
var ticks []Tick
for cursor := ra.Min; cursor < ra.Max; cursor += step {
ticks = append(ticks, Tick{
Value: cursor,
Label: vf(cursor),
})
}
return ticks
}
// Render renders the axis.
func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, axisType YAxisType, ticks []Tick) {
r.SetStrokeColor(ya.Style.GetStrokeColor(DefaultAxisColor))
r.SetStrokeWidth(ya.Style.GetStrokeWidth(DefaultAxisLineWidth))
r.SetFontColor(ya.Style.GetFontColor(DefaultAxisColor))
fontSize := ya.Style.GetFontSize(DefaultFontSize)
r.SetFontSize(fontSize)
sort.Sort(Ticks(ticks))
var lx int
var tx int
if axisType == YAxisPrimary {
lx = canvasBox.Right
tx = canvasBox.Right + DefaultYAxisMargin
r.MoveTo(lx, canvasBox.Bottom)
r.LineTo(lx, canvasBox.Top)
r.Stroke()
for _, t := range ticks {
v := t.Value
ly := ra.Translate(v) + canvasBox.Top
th := int(fontSize) >> 1
ty := ly + th
r.Text(t.Label, tx, ty)
r.MoveTo(lx, ly)
r.LineTo(lx+DefaultVerticalTickWidth, ly)
r.Stroke()
}
} else if axisType == YAxisSecondary {
lx = canvasBox.Left
r.MoveTo(lx, canvasBox.Bottom)
r.LineTo(lx, canvasBox.Top)
r.Stroke()
for _, t := range ticks {
v := t.Value
ly := ra.Translate(v) + canvasBox.Top
ptw, _ := r.MeasureText(t.Label)
tw := ptw
th := int(fontSize)
tx = lx - (int(tw) + (DefaultYAxisMargin >> 1))
ty := ly + th>>1
r.Text(t.Label, tx, ty)
r.MoveTo(lx, ly)
r.LineTo(lx-DefaultVerticalTickWidth, ly)
r.Stroke()
}
}
}