text options!

This commit is contained in:
Will Charczuk 2016-07-29 16:36:29 -07:00
parent b3386853bb
commit d84d6790c0
25 changed files with 526 additions and 287 deletions

View file

@ -50,7 +50,7 @@ func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Ran
style := a.Style.InheritFrom(seriesStyle)
lx := canvasBox.Left + xrange.Translate(a.XValue)
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
ab := MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
box.Top = MinInt(box.Top, ab.Top)
box.Left = MinInt(box.Left, ab.Left)
box.Right = MaxInt(box.Right, ab.Right)
@ -68,7 +68,7 @@ func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Rang
style := a.Style.InheritFrom(seriesStyle)
lx := canvasBox.Left + xrange.Translate(a.XValue)
ly := canvasBox.Bottom - yrange.Translate(a.YValue)
DrawAnnotation(r, canvasBox, style, lx, ly, a.Label)
Draw.Annotation(r, canvasBox, style, lx, ly, a.Label)
}
}
}

View file

@ -114,7 +114,7 @@ func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrang
FillColor: DefaultAxisColor.WithAlpha(32),
}))
DrawBoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
}
func (bbs BollingerBandsSeries) getAverage(valueBuffer *RingBuffer) float64 {

View file

@ -391,7 +391,7 @@ func (c Chart) getBackgroundStyle() Style {
}
func (c Chart) drawBackground(r Renderer) {
DrawBox(r, Box{
Draw.Box(r, Box{
Right: c.GetWidth(),
Bottom: c.GetHeight(),
}, c.getBackgroundStyle())
@ -402,7 +402,7 @@ func (c Chart) getCanvasStyle() Style {
}
func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
DrawBox(r, canvasBox, c.getCanvasStyle())
Draw.Box(r, canvasBox, c.getCanvasStyle())
}
func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {

View file

@ -51,5 +51,5 @@ func (cs ContinuousSeries) GetYAxis() YAxisType {
// Render renders the series.
func (cs ContinuousSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := cs.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, cs)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs)
}

View file

@ -1,13 +1,16 @@
package chart
import (
"math"
import "math"
"github.com/wcharczuk/go-chart/drawing"
var (
// Draw contains helpers for drawing common objects.
Draw = &draw{}
)
// DrawLineSeries draws a line series with a renderer.
func DrawLineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider) {
type draw struct{}
// LineSeries draws a line series with a renderer.
func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider) {
if vs.Len() == 0 {
return
}
@ -52,8 +55,8 @@ func DrawLineSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs
r.Stroke()
}
// DrawBoundedSeries draws a series that implements BoundedValueProvider.
func DrawBoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
// BoundedSeries draws a series that implements BoundedValueProvider.
func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, bbs BoundedValueProvider, drawOffsetIndexes ...int) {
drawOffsetIndex := 0
if len(drawOffsetIndexes) > 0 {
drawOffsetIndex = drawOffsetIndexes[0]
@ -106,8 +109,8 @@ func DrawBoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style,
r.FillStroke()
}
// DrawHistogramSeries draws a value provider as boxes from 0.
func DrawHistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider, barWidths ...int) {
// HistogramSeries draws a value provider as boxes from 0.
func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Style, vs ValueProvider, barWidths ...int) {
if vs.Len() == 0 {
return
}
@ -129,7 +132,7 @@ func DrawHistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
x := cl + xrange.Translate(vx)
y := yrange.Translate(vy)
DrawBox(r, Box{
d.Box(r, Box{
Top: cb - y0,
Left: x - (barWidth >> 1),
Right: x + (barWidth >> 1),
@ -139,7 +142,7 @@ func DrawHistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, s Styl
}
// MeasureAnnotation measures how big an annotation would be.
func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string) Box {
func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label string) Box {
r.SetFillColor(s.GetFillColor(DefaultAnnotationFillColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
@ -171,8 +174,8 @@ func MeasureAnnotation(r Renderer, canvasBox Box, s Style, lx, ly int, label str
}
}
// DrawAnnotation draws an anotation with a renderer.
func DrawAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
// Annotation draws an anotation with a renderer.
func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
r.SetFillColor(style.GetFillColor())
r.SetStrokeColor(style.GetStrokeColor())
r.SetStrokeWidth(style.GetStrokeWidth())
@ -218,8 +221,8 @@ func DrawAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label st
r.Text(label, textX, textY)
}
// DrawBox draws a box with a given style.
func DrawBox(r Renderer, b Box, s Style) {
// Box draws a box with a given style.
func (d draw) Box(r Renderer, b Box, s Style) {
r.SetFillColor(s.GetFillColor())
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth(DefaultStrokeWidth))
@ -234,7 +237,7 @@ func DrawBox(r Renderer, b Box, s Style) {
}
// DrawText draws text with a given style.
func DrawText(r Renderer, text string, x, y int, s Style) {
func (d draw) Text(r Renderer, text string, x, y int, s Style) {
r.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
@ -244,129 +247,7 @@ func DrawText(r Renderer, text string, x, y int, s Style) {
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.SetFontColor(s.GetFontColor(DefaultTextColor))
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetFont(s.GetFont())
r.SetFontSize(s.GetFontSize())
// TextWithin draws the text within a given box.
func (d draw) TextWithin(r Renderer, text string, box Box, s Style) {
tb := r.MeasureText(text)
tx := x - (tb.Width() >> 1)
ty := y - (tb.Height() >> 1)
r.Text(text, tx, ty)
}
// CreateLegend returns a legend renderable function.
func CreateLegend(c *Chart, userDefaults ...Style) Renderable {
return func(r Renderer, cb Box, chartDefaults Style) {
legendDefaults := Style{
FillColor: drawing.ColorWhite,
FontColor: DefaultTextColor,
FontSize: 8.0,
StrokeColor: DefaultAxisColor,
StrokeWidth: DefaultAxisLineWidth,
}
var legendStyle Style
if len(userDefaults) > 0 {
legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults))
} else {
legendStyle = chartDefaults.InheritFrom(legendDefaults)
}
// DEFAULTS
legendPadding := Box{
Top: 5,
Left: 5,
Right: 5,
Bottom: 5,
}
lineTextGap := 5
lineLengthMinimum := 25
var labels []string
var lines []Style
for index, s := range c.Series {
if s.GetStyle().IsZero() || s.GetStyle().Show {
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
labels = append(labels, s.GetName())
lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index)))
}
}
}
legend := Box{
Top: cb.Top,
Left: cb.Left,
// bottom and right will be sized by the legend content + relevant padding.
}
legendContent := Box{
Top: legend.Top + legendPadding.Top,
Left: legend.Left + legendPadding.Left,
Right: legend.Left + legendPadding.Left,
Bottom: legend.Top + legendPadding.Top,
}
r.SetFont(legendStyle.GetFont())
r.SetFontColor(legendStyle.GetFontColor())
r.SetFontSize(legendStyle.GetFontSize())
// measure
labelCount := 0
for x := 0; x < len(labels); x++ {
if len(labels[x]) > 0 {
tb := r.MeasureText(labels[x])
if labelCount > 0 {
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
}
legendContent.Bottom += tb.Height()
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
legendContent.Right = MaxInt(legendContent.Right, right)
labelCount++
}
}
legend = legend.Grow(legendContent)
legend.Right = legendContent.Right + legendPadding.Right
legend.Bottom = legendContent.Bottom + legendPadding.Bottom
DrawBox(r, legend, legendStyle)
ycursor := legendContent.Top
tx := legendContent.Left
legendCount := 0
for x := 0; x < len(labels); x++ {
if len(labels[x]) > 0 {
if legendCount > 0 {
ycursor += DefaultMinimumTickVerticalSpacing
}
tb := r.MeasureText(labels[x])
ty := ycursor + tb.Height()
r.Text(labels[x], tx, ty)
th2 := tb.Height() >> 1
lx := tx + tb.Width() + lineTextGap
ly := ty - th2
lx2 := legendContent.Right - legendPadding.Right
r.SetStrokeColor(lines[x].GetStrokeColor())
r.SetStrokeWidth(lines[x].GetStrokeWidth())
r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
r.MoveTo(lx, ly)
r.LineTo(lx2, ly)
r.Stroke()
ycursor += tb.Height()
legendCount++
}
}
}
}

View file

@ -1,6 +1,3 @@
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 13/12/2010 by Laurent Le Goff
package drawing
import (

View file

@ -97,5 +97,5 @@ func (ema *EMASeries) ensureCachedValues() {
// Render renders the series.
func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := ema.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, ema)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema)
}

View file

@ -10,11 +10,13 @@ import (
func drawChart(res http.ResponseWriter, req *http.Request) {
pie := chart.PieChart{
Title: "test\nchart",
TitleStyle: chart.Style{
Show: true,
FontSize: 32,
},
Width: 512,
Height: 512,
Canvas: chart.Style{
FillColor: chart.ColorLightGray,
},
Values: []chart.Value{
{Value: 5, Label: "Blue"},
{Value: 5, Label: "Green"},

View file

@ -53,5 +53,5 @@ func (hs HistogramSeries) GetBoundedValue(index int) (x, y1, y2 float64) {
// Render implements Series.Render.
func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := hs.Style.InheritFrom(defaults)
DrawHistogramSeries(r, canvasBox, xrange, yrange, style, hs)
Draw.HistogramSeries(r, canvasBox, xrange, yrange, style, hs)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 32 KiB

116
legend.go Normal file
View file

@ -0,0 +1,116 @@
package chart
import "github.com/wcharczuk/go-chart/drawing"
// Legend returns a legend renderable function.
func Legend(c *Chart, userDefaults ...Style) Renderable {
return func(r Renderer, cb Box, chartDefaults Style) {
legendDefaults := Style{
FillColor: drawing.ColorWhite,
FontColor: DefaultTextColor,
FontSize: 8.0,
StrokeColor: DefaultAxisColor,
StrokeWidth: DefaultAxisLineWidth,
}
var legendStyle Style
if len(userDefaults) > 0 {
legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults))
} else {
legendStyle = chartDefaults.InheritFrom(legendDefaults)
}
// DEFAULTS
legendPadding := Box{
Top: 5,
Left: 5,
Right: 5,
Bottom: 5,
}
lineTextGap := 5
lineLengthMinimum := 25
var labels []string
var lines []Style
for index, s := range c.Series {
if s.GetStyle().IsZero() || s.GetStyle().Show {
if _, isAnnotationSeries := s.(AnnotationSeries); !isAnnotationSeries {
labels = append(labels, s.GetName())
lines = append(lines, s.GetStyle().InheritFrom(c.styleDefaultsSeries(index)))
}
}
}
legend := Box{
Top: cb.Top,
Left: cb.Left,
// bottom and right will be sized by the legend content + relevant padding.
}
legendContent := Box{
Top: legend.Top + legendPadding.Top,
Left: legend.Left + legendPadding.Left,
Right: legend.Left + legendPadding.Left,
Bottom: legend.Top + legendPadding.Top,
}
r.SetFont(legendStyle.GetFont())
r.SetFontColor(legendStyle.GetFontColor())
r.SetFontSize(legendStyle.GetFontSize())
// measure
labelCount := 0
for x := 0; x < len(labels); x++ {
if len(labels[x]) > 0 {
tb := r.MeasureText(labels[x])
if labelCount > 0 {
legendContent.Bottom += DefaultMinimumTickVerticalSpacing
}
legendContent.Bottom += tb.Height()
right := legendContent.Left + tb.Width() + lineTextGap + lineLengthMinimum
legendContent.Right = MaxInt(legendContent.Right, right)
labelCount++
}
}
legend = legend.Grow(legendContent)
legend.Right = legendContent.Right + legendPadding.Right
legend.Bottom = legendContent.Bottom + legendPadding.Bottom
Draw.Box(r, legend, legendStyle)
ycursor := legendContent.Top
tx := legendContent.Left
legendCount := 0
for x := 0; x < len(labels); x++ {
if len(labels[x]) > 0 {
if legendCount > 0 {
ycursor += DefaultMinimumTickVerticalSpacing
}
tb := r.MeasureText(labels[x])
ty := ycursor + tb.Height()
r.Text(labels[x], tx, ty)
th2 := tb.Height() >> 1
lx := tx + tb.Width() + lineTextGap
ly := ty - th2
lx2 := legendContent.Right - legendPadding.Right
r.SetStrokeColor(lines[x].GetStrokeColor())
r.SetStrokeWidth(lines[x].GetStrokeWidth())
r.SetStrokeDashArray(lines[x].GetStrokeDashArray())
r.MoveTo(lx, ly)
r.LineTo(lx2, ly)
r.Stroke()
ycursor += tb.Height()
legendCount++
}
}
}
}

View file

@ -128,5 +128,5 @@ func (lrs *LinearRegressionSeries) computeCoefficients() {
// Render renders the series.
func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := lrs.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, lrs)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs)
}

View file

@ -193,7 +193,7 @@ func (macds *MACDSignalSeries) ensureSignal() {
// Render renders the series.
func (macds *MACDSignalSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := macds.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, macds)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macds)
}
// MACDLineSeries is a series that computes the inner ema1-ema2 value as a series.
@ -285,5 +285,5 @@ func (macdl *MACDLineSeries) ensureEMASeries() {
// Render renders the series.
func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := macdl.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, macdl)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, macdl)
}

View file

@ -99,14 +99,14 @@ func (pc PieChart) Render(rp RendererProvider, w io.Writer) error {
}
func (pc PieChart) drawBackground(r Renderer) {
DrawBox(r, Box{
Draw.Box(r, Box{
Right: pc.GetWidth(),
Bottom: pc.GetHeight(),
}, pc.getBackgroundStyle())
}
func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
DrawBox(r, canvasBox, pc.getCanvasStyle())
Draw.Box(r, canvasBox, pc.getCanvasStyle())
}
func (pc PieChart) drawTitle(r Renderer) {
@ -138,7 +138,7 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
var rads, delta, delta2, total float64
var lx, ly int
for index, v := range values {
v.Style.InheritFrom(pc.stylePieChartValue(index)).PersistToRenderer(r)
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
r.MoveTo(cx, cy)
rads = PercentToRadians(total)
@ -155,7 +155,7 @@ func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
// draw the labels
total = 0
for index, v := range values {
v.Style.InheritFrom(pc.stylePieChartValue(index)).PersistToRenderer(r)
v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
if len(v.Label) > 0 {
delta2 = PercentToRadians(total + (v.Value / 2.0))
delta2 = RadianAdd(delta2, _pi2)

View file

@ -86,5 +86,5 @@ func (sma SMASeries) getAverage(index int) float64 {
// Render renders the series.
func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := sma.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, sma)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, sma)
}

View file

@ -134,7 +134,7 @@ func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar S
for index, bv := range normalizedBarComponents {
barHeight := int(bv.Value * float64(canvasBox.Height()))
barBox := Box{Top: yoffset, Left: bxl, Right: bxr, Bottom: yoffset + barHeight}
DrawBox(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)))
Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)))
yoffset += barHeight
}

143
style.go
View file

@ -21,6 +21,10 @@ type Style struct {
FontSize float64
FontColor drawing.Color
Font *truetype.Font
TextHorizontalAlign textHorizontalAlign
TextVerticalAlign textVerticalAlign
TextWrap textWrap
}
// IsZero returns if the object is set or not.
@ -185,8 +189,41 @@ func (s Style) GetPadding(defaults ...Box) Box {
return s.Padding
}
// PersistToRenderer passes the style onto a renderer.
func (s Style) PersistToRenderer(r Renderer) {
// GetTextHorizontalAlign returns the horizontal alignment.
func (s Style) GetTextHorizontalAlign(defaults ...textHorizontalAlign) textHorizontalAlign {
if s.TextHorizontalAlign == TextHorizontalAlignUnset {
if len(defaults) > 0 {
return defaults[0]
}
return TextHorizontalAlignLeft
}
return s.TextHorizontalAlign
}
// GetTextVerticalAlign returns the vertical alignment.
func (s Style) GetTextVerticalAlign(defaults ...textVerticalAlign) textVerticalAlign {
if s.TextVerticalAlign == TextVerticalAlignUnset {
if len(defaults) > 0 {
return defaults[0]
}
return TextVerticalAlignBaseline
}
return s.TextVerticalAlign
}
// GetTextWrap returns the word wrap.
func (s Style) GetTextWrap(defaults ...textWrap) textWrap {
if s.TextWrap == TextWrapUnset {
if len(defaults) > 0 {
return defaults[0]
}
return TextWrapWord
}
return s.TextWrap
}
// WriteToRenderer passes the style's options to a renderer.
func (s Style) WriteToRenderer(r Renderer) {
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetStrokeDashArray(s.GetStrokeDashArray())
@ -196,6 +233,21 @@ func (s Style) PersistToRenderer(r Renderer) {
r.SetFontSize(s.GetFontSize())
}
// WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer.
func (s Style) WriteDrawingOptionsToRenderer(r Renderer) {
r.SetStrokeColor(s.GetStrokeColor())
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.SetFillColor(s.GetFillColor())
}
// WriteTextOptionsToRenderer passes just the text style options to a renderer.
func (s Style) WriteTextOptionsToRenderer(r Renderer) {
r.SetFont(s.GetFont())
r.SetFontColor(s.GetFontColor())
r.SetFontSize(s.GetFontSize())
}
// InheritFrom coalesces two styles into a new style.
func (s Style) InheritFrom(defaults Style) (final Style) {
final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
@ -206,47 +258,14 @@ func (s Style) InheritFrom(defaults Style) (final Style) {
final.FontSize = s.GetFontSize(defaults.FontSize)
final.Font = s.GetFont(defaults.Font)
final.Padding = s.GetPadding(defaults.Padding)
final.TextHorizontalAlign = s.GetTextHorizontalAlign(defaults.TextHorizontalAlign)
final.TextVerticalAlign = s.GetTextVerticalAlign(defaults.TextVerticalAlign)
final.TextWrap = s.GetTextWrap(defaults.TextWrap)
return
}
// SVG returns the style as a svg style string.
func (s Style) SVG(dpi float64) string {
sw := s.StrokeWidth
sc := s.StrokeColor
fc := s.FillColor
fs := s.FontSize
fnc := s.FontColor
strokeWidthText := "stroke-width:0"
if sw != 0 {
strokeWidthText = "stroke-width:" + fmt.Sprintf("%d", int(sw))
}
strokeText := "stroke:none"
if !sc.IsZero() {
strokeText = "stroke:" + sc.String()
}
fillText := "fill:none"
if !fc.IsZero() {
fillText = "fill:" + fc.String()
}
fontSizeText := ""
if fs != 0 {
fontSizeText = "font-size:" + fmt.Sprintf("%.1fpx", drawing.PointsToPixels(dpi, fs))
}
if !fnc.IsZero() {
fillText = "fill:" + fnc.String()
}
fontText := s.SVGFontFace()
return strings.Join([]string{strokeWidthText, strokeText, fillText, fontSizeText, fontText}, ";")
}
// SVGStroke returns the stroke components.
func (s Style) SVGStroke() Style {
// GetStrokeOptions returns the stroke components.
func (s Style) GetStrokeOptions() Style {
return Style{
StrokeDashArray: s.StrokeDashArray,
StrokeColor: s.StrokeColor,
@ -254,15 +273,15 @@ func (s Style) SVGStroke() Style {
}
}
// SVGFill returns the fill components.
func (s Style) SVGFill() Style {
// GetFillOptions returns the fill components.
func (s Style) GetFillOptions() Style {
return Style{
FillColor: s.FillColor,
}
}
// SVGFillAndStroke returns the fill and stroke components.
func (s Style) SVGFillAndStroke() Style {
// GetFillAndStrokeOptions returns the fill and stroke components.
func (s Style) GetFillAndStrokeOptions() Style {
return Style{
StrokeDashArray: s.StrokeDashArray,
FillColor: s.FillColor,
@ -271,34 +290,14 @@ func (s Style) SVGFillAndStroke() Style {
}
}
// SVGText returns just the text components of the style.
func (s Style) SVGText() Style {
// GetTextOptions returns just the text components of the style.
func (s Style) GetTextOptions() Style {
return Style{
FontColor: s.FontColor,
FontSize: s.FontSize,
FontColor: s.FontColor,
FontSize: s.FontSize,
Font: s.Font,
TextHorizontalAlign: s.TextHorizontalAlign,
TextVerticalAlign: s.TextVerticalAlign,
TextWrap: s.TextWrap,
}
}
// SVGFontFace returns the font face for the style.
func (s Style) SVGFontFace() string {
family := "sans-serif"
if s.GetFont() != nil {
name := s.GetFont().Name(truetype.NameIDFontFamily)
if len(name) != 0 {
family = fmt.Sprintf(`'%s',%s`, name, family)
}
}
return fmt.Sprintf("font-family:%s", family)
}
// SVGStrokeDashArray returns the stroke-dasharray property of a style.
func (s Style) SVGStrokeDashArray() string {
if len(s.StrokeDashArray) > 0 {
var values []string
for _, v := range s.StrokeDashArray {
values = append(values, fmt.Sprintf("%0.1f", v))
}
return "stroke-dasharray=\"" + strings.Join(values, ", ") + "\""
}
return ""
}

View file

@ -1,7 +1,6 @@
package chart
import (
"strings"
"testing"
"github.com/blendlabs/go-assert"
@ -146,29 +145,7 @@ func TestStyleWithDefaultsFrom(t *testing.T) {
assert.Equal(set, coalesced)
}
func TestStyleSVG(t *testing.T) {
assert := assert.New(t)
f, err := GetDefaultFont()
assert.Nil(err)
set := Style{
StrokeColor: drawing.ColorWhite,
StrokeWidth: 5.0,
FillColor: drawing.ColorWhite,
FontColor: drawing.ColorWhite,
Font: f,
Padding: DefaultBackgroundPadding,
}
svgString := set.SVG(DefaultDPI)
assert.NotEmpty(svgString)
assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)"))
assert.True(strings.Contains(svgString, "stroke-width:5"))
assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)"))
}
func TestStyleSVGStroke(t *testing.T) {
func TestStyleGetStrokeOptions(t *testing.T) {
assert := assert.New(t)
set := Style{
@ -178,14 +155,14 @@ func TestStyleSVGStroke(t *testing.T) {
FontColor: drawing.ColorWhite,
Padding: DefaultBackgroundPadding,
}
svgStroke := set.SVGStroke()
svgStroke := set.GetStrokeOptions()
assert.False(svgStroke.StrokeColor.IsZero())
assert.NotZero(svgStroke.StrokeWidth)
assert.True(svgStroke.FillColor.IsZero())
assert.True(svgStroke.FontColor.IsZero())
}
func TestStyleSVGFill(t *testing.T) {
func TestStyleGetFillOptions(t *testing.T) {
assert := assert.New(t)
set := Style{
@ -195,14 +172,14 @@ func TestStyleSVGFill(t *testing.T) {
FontColor: drawing.ColorWhite,
Padding: DefaultBackgroundPadding,
}
svgFill := set.SVGFill()
svgFill := set.GetFillOptions()
assert.False(svgFill.FillColor.IsZero())
assert.Zero(svgFill.StrokeWidth)
assert.True(svgFill.StrokeColor.IsZero())
assert.True(svgFill.FontColor.IsZero())
}
func TestStyleSVGFillAndStroke(t *testing.T) {
func TestStyleGetFillAndStrokeOptions(t *testing.T) {
assert := assert.New(t)
set := Style{
@ -212,14 +189,14 @@ func TestStyleSVGFillAndStroke(t *testing.T) {
FontColor: drawing.ColorWhite,
Padding: DefaultBackgroundPadding,
}
svgFillAndStroke := set.SVGFillAndStroke()
svgFillAndStroke := set.GetFillAndStrokeOptions()
assert.False(svgFillAndStroke.FillColor.IsZero())
assert.NotZero(svgFillAndStroke.StrokeWidth)
assert.False(svgFillAndStroke.StrokeColor.IsZero())
assert.True(svgFillAndStroke.FontColor.IsZero())
}
func TestStyleSVGText(t *testing.T) {
func TestStyleGetTextOptions(t *testing.T) {
assert := assert.New(t)
set := Style{
@ -229,7 +206,7 @@ func TestStyleSVGText(t *testing.T) {
FontColor: drawing.ColorWhite,
Padding: DefaultBackgroundPadding,
}
svgStroke := set.SVGText()
svgStroke := set.GetTextOptions()
assert.True(svgStroke.StrokeColor.IsZero())
assert.Zero(svgStroke.StrokeWidth)
assert.True(svgStroke.FillColor.IsZero())

153
text.go Normal file
View file

@ -0,0 +1,153 @@
package chart
import "strings"
// TextHorizontalAlign is an enum for the horizontal alignment options.
type textHorizontalAlign int
const (
// TextHorizontalAlignUnset is the unset state for text horizontal alignment.
TextHorizontalAlignUnset textHorizontalAlign = 0
// TextHorizontalAlignLeft aligns a string horizontally so that it's left ligature starts at horizontal pixel 0.
TextHorizontalAlignLeft textHorizontalAlign = 1
// TextHorizontalAlignCenter left aligns a string horizontally so that there are equal pixels
// to the left and to the right of a string within a box.
TextHorizontalAlignCenter textHorizontalAlign = 2
// TextHorizontalAlignRight right aligns a string horizontally so that the right ligature ends at the right-most pixel
// of a box.
TextHorizontalAlignRight textHorizontalAlign = 3
)
// TextWrap is an enum for the word wrap options.
type textWrap int
const (
// TextWrapUnset is the unset state for text wrap options.
TextWrapUnset textWrap = 0
// TextWrapNone will spill text past horizontal boundaries.
TextWrapNone textWrap = 1
// TextWrapWord will split a string on words (i.e. spaces) to fit within a horizontal boundary.
TextWrapWord textWrap = 2
// TextWrapRune will split a string on a rune (i.e. utf-8 codepage) to fit within a horizontal boundary.
TextWrapRune textWrap = 3
)
// TextVerticalAlign is an enum for the vertical alignment options.
type textVerticalAlign int
const (
// TextVerticalAlignUnset is the unset state for vertical alignment options.
TextVerticalAlignUnset textVerticalAlign = 0
// TextVerticalAlignBaseline aligns text according to the "baseline" of the string, or where a normal ascender begins.
TextVerticalAlignBaseline textVerticalAlign = 1
// TextVerticalAlignBottom aligns the text according to the lowers pixel of any of the ligatures (ex. g or q both extend below the baseline).
TextVerticalAlignBottom textVerticalAlign = 2
// TextVerticalAlignMiddle aligns the text so that there is an equal amount of space above and below the top and bottom of the ligatures.
TextVerticalAlignMiddle textVerticalAlign = 3
// TextVerticalAlignMiddleBaseline aligns the text veritcally so that there is an equal number of pixels above and below the baseline of the string.
TextVerticalAlignMiddleBaseline textVerticalAlign = 4
// TextVerticalAlignTop alignts the text so that the top of the ligatures are at y-pixel 0 in the container.
TextVerticalAlignTop textVerticalAlign = 5
)
var (
// Text contains utilities for text.
Text = &text{}
)
// TextStyle encapsulates text style options.
type TextStyle struct {
HorizontalAlign textHorizontalAlign
VerticalAlign textVerticalAlign
Wrap textWrap
}
type text struct{}
func (t text) WrapFit(r Renderer, value string, width int, style Style, wrapOption textWrap) []string {
valueBox := r.MeasureText(value)
if valueBox.Width() > width {
switch wrapOption {
case TextWrapRune:
return t.WrapFitRune(r, value, width, style)
case TextWrapWord:
return t.WrapFitWord(r, value, width, style)
}
}
return []string{value}
}
func (t text) WrapFitWord(r Renderer, value string, width int, style Style) []string {
style.WriteToRenderer(r)
var output []string
var line string
var word string
var textBox Box
for _, c := range value {
if c == rune('\n') { // commit the line to output
output = append(output, t.Trim(line+word))
line = ""
word = ""
continue
}
textBox = r.MeasureText(line + word + string(c))
if textBox.Width() >= width {
output = append(output, t.Trim(line))
line = word
word = string(c)
continue
}
if c == rune(' ') || c == rune('\t') {
line = line + word + string(c)
word = ""
continue
}
word = word + string(c)
}
return append(output, t.Trim(line+word))
}
func (t text) WrapFitRune(r Renderer, value string, width int, style Style) []string {
style.WriteToRenderer(r)
var output []string
var line string
var textBox Box
for _, c := range value {
if c == rune('\n') {
output = append(output, line)
line = ""
continue
}
textBox = r.MeasureText(line + string(c))
if textBox.Width() >= width {
output = append(output, line)
line = string(c)
continue
}
line = line + string(c)
}
return t.appendLast(output, line)
}
func (t text) Trim(value string) string {
return strings.Trim(value, " \t\n\r")
}
func (t text) appendLast(lines []string, text string) []string {
if len(lines) == 0 {
return []string{text}
}
lastLine := lines[len(lines)-1]
lines[len(lines)-1] = lastLine + text
return lines
}

32
text_test.go Normal file
View file

@ -0,0 +1,32 @@
package chart
import (
"testing"
assert "github.com/blendlabs/go-assert"
)
func TestTextWrapWord(t *testing.T) {
assert := assert.New(t)
r, err := PNG(1024, 1024)
assert.Nil(err)
f, err := GetDefaultFont()
assert.Nil(err)
basicTextStyle := Style{Font: f, FontSize: 24}
output := Text.WrapFitWord(r, "this is a test string", 100, basicTextStyle)
assert.NotEmpty(output)
assert.Len(output, 3)
for _, line := range output {
basicTextStyle.WriteToRenderer(r)
lineBox := r.MeasureText(line)
assert.True(lineBox.Width() < 100, line+": "+lineBox.String())
}
output = Text.WrapFitWord(r, "foo", 100, basicTextStyle)
assert.Len(output, 1)
assert.Equal("foo", output[0])
}

View file

@ -57,5 +57,5 @@ func (ts TimeSeries) GetYAxis() YAxisType {
// Render renders the series.
func (ts TimeSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
style := ts.Style.InheritFrom(defaults)
DrawLineSeries(r, canvasBox, xrange, yrange, style, ts)
Draw.LineSeries(r, canvasBox, xrange, yrange, style, ts)
}

View file

@ -110,30 +110,28 @@ func (vr *vectorRenderer) Close() {
// Stroke draws the path with no fill.
func (vr *vectorRenderer) Stroke() {
vr.drawPath(vr.s.SVGStroke())
vr.drawPath(vr.s.GetStrokeOptions())
}
// Fill draws the path with no stroke.
func (vr *vectorRenderer) Fill() {
vr.drawPath(vr.s.SVGFill())
vr.drawPath(vr.s.GetFillOptions())
}
// FillStroke draws the path with both fill and stroke.
func (vr *vectorRenderer) FillStroke() {
s := vr.s.SVGFillAndStroke()
vr.drawPath(s)
vr.drawPath(vr.s.GetFillAndStrokeOptions())
}
// drawPath draws a path.
func (vr *vectorRenderer) drawPath(s Style) {
vr.c.Path(strings.Join(vr.p, "\n"), &s)
vr.c.Path(strings.Join(vr.p, "\n"), vr.s.GetFillAndStrokeOptions())
vr.p = []string{} // clear the path
}
// Circle implements the interface method.
func (vr *vectorRenderer) Circle(radius float64, x, y int) {
style := vr.s.SVGFillAndStroke()
vr.c.Circle(x, y, int(radius), &style)
vr.c.Circle(x, y, int(radius), vr.s.GetFillAndStrokeOptions())
}
// SetFont implements the interface method.
@ -153,8 +151,7 @@ func (vr *vectorRenderer) SetFontSize(size float64) {
// Text draws a text blob.
func (vr *vectorRenderer) Text(body string, x, y int) {
style := vr.s.SVGText()
vr.c.Text(x, y, body, &style)
vr.c.Text(x, y, body, vr.s.GetTextOptions())
}
// MeasureText uses the truetype font drawer to measure the width of text.
@ -200,22 +197,82 @@ func (c *canvas) Start(width, height int) {
c.w.Write([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d">\n`, c.width, c.height)))
}
func (c *canvas) Path(d string, style *Style) {
func (c *canvas) Path(d string, style Style) {
var strokeDashArrayProperty string
if len(style.StrokeDashArray) > 0 {
strokeDashArrayProperty = style.SVGStrokeDashArray()
strokeDashArrayProperty = c.getStrokeDashArray(style)
}
c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" style="%s"/>\n`, strokeDashArrayProperty, d, style.SVG(c.dpi))))
c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" style="%s"/>\n`, strokeDashArrayProperty, d, c.styleAsSVG(style))))
}
func (c *canvas) Text(x, y int, body string, style *Style) {
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, style.SVG(c.dpi), body)))
func (c *canvas) Text(x, y int, body string, style Style) {
c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" style="%s">%s</text>`, x, y, c.styleAsSVG(style), body)))
}
func (c *canvas) Circle(x, y, r int, style *Style) {
c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" style="%s">`, x, y, r, style.SVG(c.dpi))))
func (c *canvas) Circle(x, y, r int, style Style) {
c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" style="%s">`, x, y, r, c.styleAsSVG(style))))
}
func (c *canvas) End() {
c.w.Write([]byte("</svg>"))
}
// getStrokeDashArray returns the stroke-dasharray property of a style.
func (c *canvas) getStrokeDashArray(s Style) string {
if len(s.StrokeDashArray) > 0 {
var values []string
for _, v := range s.StrokeDashArray {
values = append(values, fmt.Sprintf("%0.1f", v))
}
return "stroke-dasharray=\"" + strings.Join(values, ", ") + "\""
}
return ""
}
// GetFontFace returns the font face for the style.
func (c *canvas) getFontFace(s Style) string {
family := "sans-serif"
if s.GetFont() != nil {
name := s.GetFont().Name(truetype.NameIDFontFamily)
if len(name) != 0 {
family = fmt.Sprintf(`'%s',%s`, name, family)
}
}
return fmt.Sprintf("font-family:%s", family)
}
// styleAsSVG returns the style as a svg style string.
func (c *canvas) styleAsSVG(s Style) string {
sw := s.StrokeWidth
sc := s.StrokeColor
fc := s.FillColor
fs := s.FontSize
fnc := s.FontColor
strokeWidthText := "stroke-width:0"
if sw != 0 {
strokeWidthText = "stroke-width:" + fmt.Sprintf("%d", int(sw))
}
strokeText := "stroke:none"
if !sc.IsZero() {
strokeText = "stroke:" + sc.String()
}
fillText := "fill:none"
if !fc.IsZero() {
fillText = "fill:" + fc.String()
}
fontSizeText := ""
if fs != 0 {
fontSizeText = "font-size:" + fmt.Sprintf("%.1fpx", drawing.PointsToPixels(c.dpi, fs))
}
if !fnc.IsZero() {
fillText = "fill:" + fnc.String()
}
fontText := c.getFontFace(s)
return strings.Join([]string{strokeWidthText, strokeText, fillText, fontSizeText, fontText}, ";")
}

View file

@ -6,6 +6,7 @@ import (
"testing"
"github.com/blendlabs/go-assert"
"github.com/wcharczuk/go-chart/drawing"
)
func TestVectorRendererPath(t *testing.T) {
@ -50,3 +51,27 @@ func TestVectorRendererMeasureText(t *testing.T) {
assert.Equal(21, tb.Width())
assert.Equal(15, tb.Height())
}
func TestCanvasStyleSVG(t *testing.T) {
assert := assert.New(t)
f, err := GetDefaultFont()
assert.Nil(err)
set := Style{
StrokeColor: drawing.ColorWhite,
StrokeWidth: 5.0,
FillColor: drawing.ColorWhite,
FontColor: drawing.ColorWhite,
Font: f,
Padding: DefaultBackgroundPadding,
}
canvas := &canvas{dpi: DefaultDPI}
svgString := canvas.styleAsSVG(set)
assert.NotEmpty(svgString)
assert.True(strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)"))
assert.True(strings.Contains(svgString, "stroke-width:5"))
assert.True(strings.Contains(svgString, "fill:rgba(255,255,255,1.0)"))
}

View file

@ -54,7 +54,7 @@ func (xa XAxis) GetGridLines(ticks []Tick) []GridLine {
// Measure returns the bounds of the axis.
func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
xa.Style.InheritFrom(defaults).PersistToRenderer(r)
xa.Style.InheritFrom(defaults).WriteToRenderer(r)
sort.Sort(Ticks(ticks))
var left, right, top, bottom = math.MaxInt32, 0, math.MaxInt32, 0
@ -82,7 +82,7 @@ func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
// Render renders the axis
func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
xa.Style.InheritFrom(defaults).PersistToRenderer(r)
xa.Style.InheritFrom(defaults).WriteToRenderer(r)
r.MoveTo(canvasBox.Left, canvasBox.Bottom)
r.LineTo(canvasBox.Right, canvasBox.Bottom)

View file

@ -61,7 +61,7 @@ func (ya YAxis) GetGridLines(ticks []Tick) []GridLine {
// Measure returns the bounds of the axis.
func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
ya.Style.InheritFrom(defaults).PersistToRenderer(r)
ya.Style.InheritFrom(defaults).WriteToRenderer(r)
sort.Sort(Ticks(ticks))
@ -104,7 +104,7 @@ func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, tic
// Render renders the axis.
func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
ya.Style.InheritFrom(defaults).PersistToRenderer(r)
ya.Style.InheritFrom(defaults).WriteToRenderer(r)
sort.Sort(Ticks(ticks))