diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..e69de29
diff --git a/COVERAGE b/COVERAGE
new file mode 100644
index 0000000..f309e21
--- /dev/null
+++ b/COVERAGE
@@ -0,0 +1 @@
+70.89
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 0f8d424..fb421e7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,11 @@
all: test
-tools:
- @go get -u github.com/blend/go-sdk/_bin/coverage
- @go get -u github.com/blend/go-sdk/_bin/profanity
+ci: profanity coverage
+
+new-install:
+ @go get -v -u ./...
+ @go get -v -u github.com/blend/go-sdk/cmd/coverage
+ @go get -v -u github.com/blend/go-sdk/cmd/profanity
test:
@go test ./...
diff --git a/coverage.html b/coverage.html
new file mode 100644
index 0000000..9957897
--- /dev/null
+++ b/coverage.html
@@ -0,0 +1,11486 @@
+
+
+
+
+
+
+
package chart
+
+import (
+ "fmt"
+ "math"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*AnnotationSeries)(nil)
+)
+
+// AnnotationSeries is a series of labels on the chart.
+type AnnotationSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+ Annotations []Value2
+}
+
+// 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
+}
+
+func (as AnnotationSeries) annotationStyleDefaults(defaults Style) Style {
+ return Style{
+ FontColor: DefaultTextColor,
+ Font: defaults.Font,
+ FillColor: DefaultAnnotationFillColor,
+ FontSize: DefaultAnnotationFontSize,
+ StrokeColor: defaults.StrokeColor,
+ StrokeWidth: defaults.StrokeWidth,
+ Padding: DefaultAnnotationPadding,
+ }
+}
+
+// Measure returns a bounds box of the series.
+func (as AnnotationSeries) Measure(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) Box {
+ box := Box{
+ Top: math.MaxInt32,
+ Left: math.MaxInt32,
+ Right: 0,
+ Bottom: 0,
+ }
+ if as.Style.IsZero() || as.Style.Show {
+ seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
+ for _, a := range as.Annotations {
+ style := a.Style.InheritFrom(seriesStyle)
+ lx := canvasBox.Left + xrange.Translate(a.XValue)
+ ly := canvasBox.Bottom - yrange.Translate(a.YValue)
+ ab := Draw.MeasureAnnotation(r, canvasBox, style, lx, ly, a.Label)
+ box.Top = util.Math.MinInt(box.Top, ab.Top)
+ box.Left = util.Math.MinInt(box.Left, ab.Left)
+ box.Right = util.Math.MaxInt(box.Right, ab.Right)
+ box.Bottom = util.Math.MaxInt(box.Bottom, ab.Bottom)
+ }
+ }
+ return box
+}
+
+// Render draws the series.
+func (as AnnotationSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ if as.Style.IsZero() || as.Style.Show {
+ seriesStyle := as.Style.InheritFrom(as.annotationStyleDefaults(defaults))
+ for _, a := range as.Annotations {
+ style := a.Style.InheritFrom(seriesStyle)
+ lx := canvasBox.Left + xrange.Translate(a.XValue)
+ ly := canvasBox.Bottom - yrange.Translate(a.YValue)
+ Draw.Annotation(r, canvasBox, style, lx, ly, a.Label)
+ }
+ }
+}
+
+// Validate validates the series.
+func (as AnnotationSeries) Validate() error {
+ if len(as.Annotations) == 0 {
+ return fmt.Errorf("annotation series requires annotations to be set and not empty")
+ }
+ return nil
+}
+
+
+
package chart
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "math"
+
+ "github.com/golang/freetype/truetype"
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// BarChart is a chart that draws bars on a range.
+type BarChart struct {
+ Title string
+ TitleStyle Style
+
+ ColorPalette ColorPalette
+
+ Width int
+ Height int
+ DPI float64
+
+ BarWidth int
+
+ Background Style
+ Canvas Style
+
+ XAxis Style
+ YAxis YAxis
+
+ BarSpacing int
+
+ UseBaseValue bool
+ BaseValue float64
+
+ Font *truetype.Font
+ defaultFont *truetype.Font
+
+ Bars []Value
+ Elements []Renderable
+}
+
+// GetDPI returns the dpi for the chart.
+func (bc BarChart) GetDPI() float64 {
+ if bc.DPI == 0 {
+ return DefaultDPI
+ }
+ return bc.DPI
+}
+
+// GetFont returns the text font.
+func (bc BarChart) GetFont() *truetype.Font {
+ if bc.Font == nil {
+ return bc.defaultFont
+ }
+ return bc.Font
+}
+
+// GetWidth returns the chart width or the default value.
+func (bc BarChart) GetWidth() int {
+ if bc.Width == 0 {
+ return DefaultChartWidth
+ }
+ return bc.Width
+}
+
+// GetHeight returns the chart height or the default value.
+func (bc BarChart) GetHeight() int {
+ if bc.Height == 0 {
+ return DefaultChartHeight
+ }
+ return bc.Height
+}
+
+// GetBarSpacing returns the spacing between bars.
+func (bc BarChart) GetBarSpacing() int {
+ if bc.BarSpacing == 0 {
+ return DefaultBarSpacing
+ }
+ return bc.BarSpacing
+}
+
+// GetBarWidth returns the default bar width.
+func (bc BarChart) GetBarWidth() int {
+ if bc.BarWidth == 0 {
+ return DefaultBarWidth
+ }
+ return bc.BarWidth
+}
+
+// Render renders the chart with the given renderer to the given io.Writer.
+func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
+ if len(bc.Bars) == 0 {
+ return errors.New("please provide at least one bar")
+ }
+
+ r, err := rp(bc.GetWidth(), bc.GetHeight())
+ if err != nil {
+ return err
+ }
+
+ if bc.Font == nil {
+ defaultFont, err := GetDefaultFont()
+ if err != nil {
+ return err
+ }
+ bc.defaultFont = defaultFont
+ }
+ r.SetDPI(bc.GetDPI())
+
+ bc.drawBackground(r)
+
+ var canvasBox Box
+ var yt []Tick
+ var yr Range
+ var yf ValueFormatter
+
+ canvasBox = bc.getDefaultCanvasBox()
+ yr = bc.getRanges()
+ if yr.GetMax()-yr.GetMin() == 0 {
+ return fmt.Errorf("invalid data range; cannot be zero")
+ }
+ yr = bc.setRangeDomains(canvasBox, yr)
+ yf = bc.getValueFormatters()
+
+ if bc.hasAxes() {
+ yt = bc.getAxesTicks(r, yr, yf)
+ canvasBox = bc.getAdjustedCanvasBox(r, canvasBox, yr, yt)
+ yr = bc.setRangeDomains(canvasBox, yr)
+ }
+ bc.drawCanvas(r, canvasBox)
+ bc.drawBars(r, canvasBox, yr)
+ bc.drawXAxis(r, canvasBox)
+ bc.drawYAxis(r, canvasBox, yr, yt)
+
+ bc.drawTitle(r)
+ for _, a := range bc.Elements {
+ a(r, canvasBox, bc.styleDefaultsElements())
+ }
+
+ return r.Save(w)
+}
+
+func (bc BarChart) drawCanvas(r Renderer, canvasBox Box) {
+ Draw.Box(r, canvasBox, bc.getCanvasStyle())
+}
+
+func (bc BarChart) getRanges() Range {
+ var yrange Range
+ if bc.YAxis.Range != nil && !bc.YAxis.Range.IsZero() {
+ yrange = bc.YAxis.Range
+ } else {
+ yrange = &ContinuousRange{}
+ }
+
+ if !yrange.IsZero() {
+ return yrange
+ }
+
+ if len(bc.YAxis.Ticks) > 0 {
+ tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
+ for _, t := range bc.YAxis.Ticks {
+ tickMin = math.Min(tickMin, t.Value)
+ tickMax = math.Max(tickMax, t.Value)
+ }
+ yrange.SetMin(tickMin)
+ yrange.SetMax(tickMax)
+ return yrange
+ }
+
+ min, max := math.MaxFloat64, -math.MaxFloat64
+ for _, b := range bc.Bars {
+ min = math.Min(b.Value, min)
+ max = math.Max(b.Value, max)
+ }
+
+ yrange.SetMin(min)
+ yrange.SetMax(max)
+
+ return yrange
+}
+
+func (bc BarChart) drawBackground(r Renderer) {
+ Draw.Box(r, Box{
+ Right: bc.GetWidth(),
+ Bottom: bc.GetHeight(),
+ }, bc.getBackgroundStyle())
+}
+
+func (bc BarChart) drawBars(r Renderer, canvasBox Box, yr Range) {
+ xoffset := canvasBox.Left
+
+ width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
+ bs2 := spacing >> 1
+
+ var barBox Box
+ var bxl, bxr, by int
+ for index, bar := range bc.Bars {
+ bxl = xoffset + bs2
+ bxr = bxl + width
+
+ by = canvasBox.Bottom - yr.Translate(bar.Value)
+
+ if bc.UseBaseValue {
+ barBox = Box{
+ Top: by,
+ Left: bxl,
+ Right: bxr,
+ Bottom: canvasBox.Bottom - yr.Translate(bc.BaseValue),
+ }
+ } else {
+ barBox = Box{
+ Top: by,
+ Left: bxl,
+ Right: bxr,
+ Bottom: canvasBox.Bottom,
+ }
+ }
+
+ Draw.Box(r, barBox, bar.Style.InheritFrom(bc.styleDefaultsBar(index)))
+
+ xoffset += width + spacing
+ }
+}
+
+func (bc BarChart) drawXAxis(r Renderer, canvasBox Box) {
+ if bc.XAxis.Show {
+ axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
+ axisStyle.WriteToRenderer(r)
+
+ width, spacing, _ := bc.calculateScaledTotalWidth(canvasBox)
+
+ r.MoveTo(canvasBox.Left, canvasBox.Bottom)
+ r.LineTo(canvasBox.Right, canvasBox.Bottom)
+ r.Stroke()
+
+ r.MoveTo(canvasBox.Left, canvasBox.Bottom)
+ r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight)
+ r.Stroke()
+
+ cursor := canvasBox.Left
+ for index, bar := range bc.Bars {
+ barLabelBox := Box{
+ Top: canvasBox.Bottom + DefaultXAxisMargin,
+ Left: cursor,
+ Right: cursor + width + spacing,
+ Bottom: bc.GetHeight(),
+ }
+
+ if len(bar.Label) > 0 {
+ Draw.TextWithin(r, bar.Label, barLabelBox, axisStyle)
+ }
+
+ axisStyle.WriteToRenderer(r)
+ if index < len(bc.Bars)-1 {
+ r.MoveTo(barLabelBox.Right, canvasBox.Bottom)
+ r.LineTo(barLabelBox.Right, canvasBox.Bottom+DefaultVerticalTickHeight)
+ r.Stroke()
+ }
+ cursor += width + spacing
+ }
+ }
+}
+
+func (bc BarChart) drawYAxis(r Renderer, canvasBox Box, yr Range, ticks []Tick) {
+ if bc.YAxis.Style.Show {
+ axisStyle := bc.YAxis.Style.InheritFrom(bc.styleDefaultsAxes())
+ axisStyle.WriteToRenderer(r)
+
+ r.MoveTo(canvasBox.Right, canvasBox.Top)
+ r.LineTo(canvasBox.Right, canvasBox.Bottom)
+ r.Stroke()
+
+ r.MoveTo(canvasBox.Right, canvasBox.Bottom)
+ r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
+ r.Stroke()
+
+ var ty int
+ var tb Box
+ for _, t := range ticks {
+ ty = canvasBox.Bottom - yr.Translate(t.Value)
+
+ axisStyle.GetStrokeOptions().WriteToRenderer(r)
+ r.MoveTo(canvasBox.Right, ty)
+ r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
+ r.Stroke()
+
+ axisStyle.GetTextOptions().WriteToRenderer(r)
+ tb = r.MeasureText(t.Label)
+ Draw.Text(r, t.Label, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
+ }
+
+ }
+}
+
+func (bc BarChart) drawTitle(r Renderer) {
+ if len(bc.Title) > 0 && bc.TitleStyle.Show {
+ r.SetFont(bc.TitleStyle.GetFont(bc.GetFont()))
+ r.SetFontColor(bc.TitleStyle.GetFontColor(bc.GetColorPalette().TextColor()))
+ titleFontSize := bc.TitleStyle.GetFontSize(bc.getTitleFontSize())
+ r.SetFontSize(titleFontSize)
+
+ textBox := r.MeasureText(bc.Title)
+
+ textWidth := textBox.Width()
+ textHeight := textBox.Height()
+
+ titleX := (bc.GetWidth() >> 1) - (textWidth >> 1)
+ titleY := bc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
+
+ r.Text(bc.Title, titleX, titleY)
+ }
+}
+
+func (bc BarChart) getCanvasStyle() Style {
+ return bc.Canvas.InheritFrom(bc.styleDefaultsCanvas())
+}
+
+func (bc BarChart) styleDefaultsCanvas() Style {
+ return Style{
+ FillColor: bc.GetColorPalette().CanvasColor(),
+ StrokeColor: bc.GetColorPalette().CanvasStrokeColor(),
+ StrokeWidth: DefaultCanvasStrokeWidth,
+ }
+}
+
+func (bc BarChart) hasAxes() bool {
+ return bc.YAxis.Style.Show
+}
+
+func (bc BarChart) setRangeDomains(canvasBox Box, yr Range) Range {
+ yr.SetDomain(canvasBox.Height())
+ return yr
+}
+
+func (bc BarChart) getDefaultCanvasBox() Box {
+ return bc.box()
+}
+
+func (bc BarChart) getValueFormatters() ValueFormatter {
+ if bc.YAxis.ValueFormatter != nil {
+ return bc.YAxis.ValueFormatter
+ }
+ return FloatValueFormatter
+}
+
+func (bc BarChart) getAxesTicks(r Renderer, yr Range, yf ValueFormatter) (yticks []Tick) {
+ if bc.YAxis.Style.Show {
+ yticks = bc.YAxis.GetTicks(r, yr, bc.styleDefaultsAxes(), yf)
+ }
+ return
+}
+
+func (bc BarChart) calculateEffectiveBarSpacing(canvasBox Box) int {
+ totalWithBaseSpacing := bc.calculateTotalBarWidth(bc.GetBarWidth(), bc.GetBarSpacing())
+ if totalWithBaseSpacing > canvasBox.Width() {
+ lessBarWidths := canvasBox.Width() - (len(bc.Bars) * bc.GetBarWidth())
+ if lessBarWidths > 0 {
+ return int(math.Ceil(float64(lessBarWidths) / float64(len(bc.Bars))))
+ }
+ return 0
+ }
+ return bc.GetBarSpacing()
+}
+
+func (bc BarChart) calculateEffectiveBarWidth(canvasBox Box, spacing int) int {
+ totalWithBaseWidth := bc.calculateTotalBarWidth(bc.GetBarWidth(), spacing)
+ if totalWithBaseWidth > canvasBox.Width() {
+ totalLessBarSpacings := canvasBox.Width() - (len(bc.Bars) * spacing)
+ if totalLessBarSpacings > 0 {
+ return int(math.Ceil(float64(totalLessBarSpacings) / float64(len(bc.Bars))))
+ }
+ return 0
+ }
+ return bc.GetBarWidth()
+}
+
+func (bc BarChart) calculateTotalBarWidth(barWidth, spacing int) int {
+ return len(bc.Bars) * (barWidth + spacing)
+}
+
+func (bc BarChart) calculateScaledTotalWidth(canvasBox Box) (width, spacing, total int) {
+ spacing = bc.calculateEffectiveBarSpacing(canvasBox)
+ width = bc.calculateEffectiveBarWidth(canvasBox, spacing)
+ total = bc.calculateTotalBarWidth(width, spacing)
+ return
+}
+
+func (bc BarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box, yrange Range, yticks []Tick) Box {
+ axesOuterBox := canvasBox.Clone()
+
+ _, _, totalWidth := bc.calculateScaledTotalWidth(canvasBox)
+
+ if bc.XAxis.Show {
+ xaxisHeight := DefaultVerticalTickHeight
+
+ axisStyle := bc.XAxis.InheritFrom(bc.styleDefaultsAxes())
+ axisStyle.WriteToRenderer(r)
+
+ cursor := canvasBox.Left
+ for _, bar := range bc.Bars {
+ if len(bar.Label) > 0 {
+ barLabelBox := Box{
+ Top: canvasBox.Bottom + DefaultXAxisMargin,
+ Left: cursor,
+ Right: cursor + bc.GetBarWidth() + bc.GetBarSpacing(),
+ Bottom: bc.GetHeight(),
+ }
+ lines := Text.WrapFit(r, bar.Label, barLabelBox.Width(), axisStyle)
+ linesBox := Text.MeasureLines(r, lines, axisStyle)
+
+ xaxisHeight = util.Math.MinInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
+ }
+ }
+
+ xbox := Box{
+ Top: canvasBox.Top,
+ Left: canvasBox.Left,
+ Right: canvasBox.Left + totalWidth,
+ Bottom: bc.GetHeight() - xaxisHeight,
+ }
+
+ axesOuterBox = axesOuterBox.Grow(xbox)
+ }
+
+ if bc.YAxis.Style.Show {
+ axesBounds := bc.YAxis.Measure(r, canvasBox, yrange, bc.styleDefaultsAxes(), yticks)
+ axesOuterBox = axesOuterBox.Grow(axesBounds)
+ }
+
+ return canvasBox.OuterConstrain(bc.box(), axesOuterBox)
+}
+
+// box returns the chart bounds as a box.
+func (bc BarChart) box() Box {
+ dpr := bc.Background.Padding.GetRight(10)
+ dpb := bc.Background.Padding.GetBottom(50)
+
+ return Box{
+ Top: bc.Background.Padding.GetTop(20),
+ Left: bc.Background.Padding.GetLeft(20),
+ Right: bc.GetWidth() - dpr,
+ Bottom: bc.GetHeight() - dpb,
+ }
+}
+
+func (bc BarChart) getBackgroundStyle() Style {
+ return bc.Background.InheritFrom(bc.styleDefaultsBackground())
+}
+
+func (bc BarChart) styleDefaultsBackground() Style {
+ return Style{
+ FillColor: bc.GetColorPalette().BackgroundColor(),
+ StrokeColor: bc.GetColorPalette().BackgroundStrokeColor(),
+ StrokeWidth: DefaultStrokeWidth,
+ }
+}
+
+func (bc BarChart) styleDefaultsBar(index int) Style {
+ return Style{
+ StrokeColor: bc.GetColorPalette().GetSeriesColor(index),
+ StrokeWidth: 3.0,
+ FillColor: bc.GetColorPalette().GetSeriesColor(index),
+ }
+}
+
+func (bc BarChart) styleDefaultsTitle() Style {
+ return bc.TitleStyle.InheritFrom(Style{
+ FontColor: bc.GetColorPalette().TextColor(),
+ Font: bc.GetFont(),
+ FontSize: bc.getTitleFontSize(),
+ TextHorizontalAlign: TextHorizontalAlignCenter,
+ TextVerticalAlign: TextVerticalAlignTop,
+ TextWrap: TextWrapWord,
+ })
+}
+
+func (bc BarChart) getTitleFontSize() float64 {
+ effectiveDimension := util.Math.MinInt(bc.GetWidth(), bc.GetHeight())
+ if effectiveDimension >= 2048 {
+ return 48
+ } else if effectiveDimension >= 1024 {
+ return 24
+ } else if effectiveDimension >= 512 {
+ return 18
+ } else if effectiveDimension >= 256 {
+ return 12
+ }
+ return 10
+}
+
+func (bc BarChart) styleDefaultsAxes() Style {
+ return Style{
+ StrokeColor: bc.GetColorPalette().AxisStrokeColor(),
+ Font: bc.GetFont(),
+ FontSize: DefaultAxisFontSize,
+ FontColor: bc.GetColorPalette().TextColor(),
+ TextHorizontalAlign: TextHorizontalAlignCenter,
+ TextVerticalAlign: TextVerticalAlignTop,
+ TextWrap: TextWrapWord,
+ }
+}
+
+func (bc BarChart) styleDefaultsElements() Style {
+ return Style{
+ Font: bc.GetFont(),
+ }
+}
+
+// GetColorPalette returns the color palette for the chart.
+func (bc BarChart) GetColorPalette() ColorPalette {
+ if bc.ColorPalette != nil {
+ return bc.ColorPalette
+ }
+ return AlternateColorPalette
+}
+
+
+
package chart
+
+import (
+ "fmt"
+
+ "github.com/wcharczuk/go-chart/seq"
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*BollingerBandsSeries)(nil)
+)
+
+// BollingerBandsSeries draws bollinger bands for an inner series.
+// Bollinger bands are defined by two lines, one at SMA+k*stddev, one at SMA-k*stdev.
+type BollingerBandsSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+
+ Period int
+ K float64
+ InnerSeries ValuesProvider
+
+ valueBuffer *seq.Buffer
+}
+
+// GetName returns the name of the time series.
+func (bbs BollingerBandsSeries) GetName() string {
+ return bbs.Name
+}
+
+// GetStyle returns the line style.
+func (bbs BollingerBandsSeries) GetStyle() Style {
+ return bbs.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (bbs BollingerBandsSeries) GetYAxis() YAxisType {
+ return bbs.YAxis
+}
+
+// GetPeriod returns the window size.
+func (bbs BollingerBandsSeries) GetPeriod() int {
+ if bbs.Period == 0 {
+ return DefaultSimpleMovingAveragePeriod
+ }
+ return bbs.Period
+}
+
+// GetK returns the K value, or the number of standard deviations above and below
+// to band the simple moving average with.
+// Typical K value is 2.0.
+func (bbs BollingerBandsSeries) GetK(defaults ...float64) float64 {
+ if bbs.K == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return 2.0
+ }
+ return bbs.K
+}
+
+// Len returns the number of elements in the series.
+func (bbs BollingerBandsSeries) Len() int {
+ return bbs.InnerSeries.Len()
+}
+
+// GetBoundedValues gets the bounded value for the series.
+func (bbs *BollingerBandsSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
+ if bbs.InnerSeries == nil {
+ return
+ }
+ if bbs.valueBuffer == nil || index == 0 {
+ bbs.valueBuffer = seq.NewBufferWithCapacity(bbs.GetPeriod())
+ }
+ if bbs.valueBuffer.Len() >= bbs.GetPeriod() {
+ bbs.valueBuffer.Dequeue()
+ }
+ px, py := bbs.InnerSeries.GetValues(index)
+ bbs.valueBuffer.Enqueue(py)
+ x = px
+
+ ay := seq.New(bbs.valueBuffer).Average()
+ std := seq.New(bbs.valueBuffer).StdDev()
+
+ y1 = ay + (bbs.GetK() * std)
+ y2 = ay - (bbs.GetK() * std)
+ return
+}
+
+// GetBoundedLastValues returns the last bounded value for the series.
+func (bbs *BollingerBandsSeries) GetBoundedLastValues() (x, y1, y2 float64) {
+ if bbs.InnerSeries == nil {
+ return
+ }
+ period := bbs.GetPeriod()
+ seriesLength := bbs.InnerSeries.Len()
+ startAt := seriesLength - period
+ if startAt < 0 {
+ startAt = 0
+ }
+
+ vb := seq.NewBufferWithCapacity(period)
+ for index := startAt; index < seriesLength; index++ {
+ xn, yn := bbs.InnerSeries.GetValues(index)
+ vb.Enqueue(yn)
+ x = xn
+ }
+
+ ay := seq.Seq{Provider: vb}.Average()
+ std := seq.Seq{Provider: vb}.StdDev()
+
+ y1 = ay + (bbs.GetK() * std)
+ y2 = ay - (bbs.GetK() * std)
+
+ return
+}
+
+// Render renders the series.
+func (bbs *BollingerBandsSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ s := bbs.Style.InheritFrom(defaults.InheritFrom(Style{
+ StrokeWidth: 1.0,
+ StrokeColor: DefaultAxisColor.WithAlpha(64),
+ FillColor: DefaultAxisColor.WithAlpha(32),
+ }))
+
+ Draw.BoundedSeries(r, canvasBox, xrange, yrange, s, bbs, bbs.GetPeriod())
+}
+
+// Validate validates the series.
+func (bbs BollingerBandsSeries) Validate() error {
+ if bbs.InnerSeries == nil {
+ return fmt.Errorf("bollinger bands series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "math"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+var (
+ // BoxZero is a preset box that represents an intentional zero value.
+ BoxZero = Box{IsSet: true}
+)
+
+// NewBox returns a new (set) box.
+func NewBox(top, left, right, bottom int) Box {
+ return Box{
+ IsSet: true,
+ Top: top,
+ Left: left,
+ Right: right,
+ Bottom: bottom,
+ }
+}
+
+// Box represents the main 4 dimensions of a box.
+type Box struct {
+ Top int
+ Left int
+ Right int
+ Bottom int
+ IsSet bool
+}
+
+// IsZero returns if the box is set or not.
+func (b Box) IsZero() bool {
+ if b.IsSet {
+ return false
+ }
+ return b.Top == 0 && b.Left == 0 && b.Right == 0 && b.Bottom == 0
+}
+
+// String returns a string representation of the box.
+func (b Box) String() string {
+ return fmt.Sprintf("box(%d,%d,%d,%d)", b.Top, b.Left, b.Right, b.Bottom)
+}
+
+// GetTop returns a coalesced value with a default.
+func (b Box) GetTop(defaults ...int) int {
+ if !b.IsSet && b.Top == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return 0
+ }
+ return b.Top
+}
+
+// GetLeft returns a coalesced value with a default.
+func (b Box) GetLeft(defaults ...int) int {
+ if !b.IsSet && b.Left == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return 0
+ }
+ return b.Left
+}
+
+// GetRight returns a coalesced value with a default.
+func (b Box) GetRight(defaults ...int) int {
+ if !b.IsSet && b.Right == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return 0
+ }
+ return b.Right
+}
+
+// GetBottom returns a coalesced value with a default.
+func (b Box) GetBottom(defaults ...int) int {
+ if !b.IsSet && b.Bottom == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return 0
+ }
+ return b.Bottom
+}
+
+// Width returns the width
+func (b Box) Width() int {
+ return util.Math.AbsInt(b.Right - b.Left)
+}
+
+// Height returns the height
+func (b Box) Height() int {
+ return util.Math.AbsInt(b.Bottom - b.Top)
+}
+
+// Center returns the center of the box
+func (b Box) Center() (x, y int) {
+ w2, h2 := b.Width()>>1, b.Height()>>1
+ return b.Left + w2, b.Top + h2
+}
+
+// Aspect returns the aspect ratio of the box.
+func (b Box) Aspect() float64 {
+ return float64(b.Width()) / float64(b.Height())
+}
+
+// Clone returns a new copy of the box.
+func (b Box) Clone() Box {
+ return Box{
+ IsSet: b.IsSet,
+ Top: b.Top,
+ Left: b.Left,
+ Right: b.Right,
+ Bottom: b.Bottom,
+ }
+}
+
+// IsBiggerThan returns if a box is bigger than another box.
+func (b Box) IsBiggerThan(other Box) bool {
+ return b.Top < other.Top ||
+ b.Bottom > other.Bottom ||
+ b.Left < other.Left ||
+ b.Right > other.Right
+}
+
+// IsSmallerThan returns if a box is smaller than another box.
+func (b Box) IsSmallerThan(other Box) bool {
+ return b.Top > other.Top &&
+ b.Bottom < other.Bottom &&
+ b.Left > other.Left &&
+ b.Right < other.Right
+}
+
+// Equals returns if the box equals another box.
+func (b Box) Equals(other Box) bool {
+ return b.Top == other.Top &&
+ b.Left == other.Left &&
+ b.Right == other.Right &&
+ b.Bottom == other.Bottom
+}
+
+// Grow grows a box based on another box.
+func (b Box) Grow(other Box) Box {
+ return Box{
+ Top: util.Math.MinInt(b.Top, other.Top),
+ Left: util.Math.MinInt(b.Left, other.Left),
+ Right: util.Math.MaxInt(b.Right, other.Right),
+ Bottom: util.Math.MaxInt(b.Bottom, other.Bottom),
+ }
+}
+
+// Shift pushes a box by x,y.
+func (b Box) Shift(x, y int) Box {
+ return Box{
+ Top: b.Top + y,
+ Left: b.Left + x,
+ Right: b.Right + x,
+ Bottom: b.Bottom + y,
+ }
+}
+
+// Corners returns the box as a set of corners.
+func (b Box) Corners() BoxCorners {
+ return BoxCorners{
+ TopLeft: Point{b.Left, b.Top},
+ TopRight: Point{b.Right, b.Top},
+ BottomRight: Point{b.Right, b.Bottom},
+ BottomLeft: Point{b.Left, b.Bottom},
+ }
+}
+
+// Fit is functionally the inverse of grow.
+// Fit maintains the original aspect ratio of the `other` box,
+// but constrains it to the bounds of the target box.
+func (b Box) Fit(other Box) Box {
+ ba := b.Aspect()
+ oa := other.Aspect()
+
+ if oa == ba {
+ return b.Clone()
+ }
+
+ bw, bh := float64(b.Width()), float64(b.Height())
+ bw2 := int(bw) >> 1
+ bh2 := int(bh) >> 1
+ if oa > ba { // ex. 16:9 vs. 4:3
+ var noh2 int
+ if oa > 1.0 {
+ noh2 = int(bw/oa) >> 1
+ } else {
+ noh2 = int(bh*oa) >> 1
+ }
+ return Box{
+ Top: (b.Top + bh2) - noh2,
+ Left: b.Left,
+ Right: b.Right,
+ Bottom: (b.Top + bh2) + noh2,
+ }
+ }
+ var now2 int
+ if oa > 1.0 {
+ now2 = int(bh/oa) >> 1
+ } else {
+ now2 = int(bw*oa) >> 1
+ }
+ return Box{
+ Top: b.Top,
+ Left: (b.Left + bw2) - now2,
+ Right: (b.Left + bw2) + now2,
+ Bottom: b.Bottom,
+ }
+}
+
+// Constrain is similar to `Fit` except that it will work
+// more literally like the opposite of grow.
+func (b Box) Constrain(other Box) Box {
+ newBox := b.Clone()
+
+ newBox.Top = util.Math.MaxInt(newBox.Top, other.Top)
+ newBox.Left = util.Math.MaxInt(newBox.Left, other.Left)
+ newBox.Right = util.Math.MinInt(newBox.Right, other.Right)
+ newBox.Bottom = util.Math.MinInt(newBox.Bottom, other.Bottom)
+
+ return newBox
+}
+
+// OuterConstrain is similar to `Constraint` with the difference
+// that it applies corrections
+func (b Box) OuterConstrain(bounds, other Box) Box {
+ newBox := b.Clone()
+ if other.Top < bounds.Top {
+ delta := bounds.Top - other.Top
+ newBox.Top = b.Top + delta
+ }
+
+ if other.Left < bounds.Left {
+ delta := bounds.Left - other.Left
+ newBox.Left = b.Left + delta
+ }
+
+ if other.Right > bounds.Right {
+ delta := other.Right - bounds.Right
+ newBox.Right = b.Right - delta
+ }
+
+ if other.Bottom > bounds.Bottom {
+ delta := other.Bottom - bounds.Bottom
+ newBox.Bottom = b.Bottom - delta
+ }
+ return newBox
+}
+
+// BoxCorners is a box with independent corners.
+type BoxCorners struct {
+ TopLeft, TopRight, BottomRight, BottomLeft Point
+}
+
+// Box return the BoxCorners as a regular box.
+func (bc BoxCorners) Box() Box {
+ return Box{
+ Top: util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y),
+ Left: util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X),
+ Right: util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X),
+ Bottom: util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y),
+ }
+}
+
+// Width returns the width
+func (bc BoxCorners) Width() int {
+ minLeft := util.Math.MinInt(bc.TopLeft.X, bc.BottomLeft.X)
+ maxRight := util.Math.MaxInt(bc.TopRight.X, bc.BottomRight.X)
+ return maxRight - minLeft
+}
+
+// Height returns the height
+func (bc BoxCorners) Height() int {
+ minTop := util.Math.MinInt(bc.TopLeft.Y, bc.TopRight.Y)
+ maxBottom := util.Math.MaxInt(bc.BottomLeft.Y, bc.BottomRight.Y)
+ return maxBottom - minTop
+}
+
+// Center returns the center of the box
+func (bc BoxCorners) Center() (x, y int) {
+
+ left := util.Math.MeanInt(bc.TopLeft.X, bc.BottomLeft.X)
+ right := util.Math.MeanInt(bc.TopRight.X, bc.BottomRight.X)
+ x = ((right - left) >> 1) + left
+
+ top := util.Math.MeanInt(bc.TopLeft.Y, bc.TopRight.Y)
+ bottom := util.Math.MeanInt(bc.BottomLeft.Y, bc.BottomRight.Y)
+ y = ((bottom - top) >> 1) + top
+
+ return
+}
+
+// Rotate rotates the box.
+func (bc BoxCorners) Rotate(thetaDegrees float64) BoxCorners {
+ cx, cy := bc.Center()
+
+ thetaRadians := util.Math.DegreesToRadians(thetaDegrees)
+
+ tlx, tly := util.Math.RotateCoordinate(cx, cy, bc.TopLeft.X, bc.TopLeft.Y, thetaRadians)
+ trx, try := util.Math.RotateCoordinate(cx, cy, bc.TopRight.X, bc.TopRight.Y, thetaRadians)
+ brx, bry := util.Math.RotateCoordinate(cx, cy, bc.BottomRight.X, bc.BottomRight.Y, thetaRadians)
+ blx, bly := util.Math.RotateCoordinate(cx, cy, bc.BottomLeft.X, bc.BottomLeft.Y, thetaRadians)
+
+ return BoxCorners{
+ TopLeft: Point{tlx, tly},
+ TopRight: Point{trx, try},
+ BottomRight: Point{brx, bry},
+ BottomLeft: Point{blx, bly},
+ }
+}
+
+// Equals returns if the box equals another box.
+func (bc BoxCorners) Equals(other BoxCorners) bool {
+ return bc.TopLeft.Equals(other.TopLeft) &&
+ bc.TopRight.Equals(other.TopRight) &&
+ bc.BottomRight.Equals(other.BottomRight) &&
+ bc.BottomLeft.Equals(other.BottomLeft)
+}
+
+func (bc BoxCorners) String() string {
+ return fmt.Sprintf("BoxC{%s,%s,%s,%s}", bc.TopLeft.String(), bc.TopRight.String(), bc.BottomRight.String(), bc.BottomLeft.String())
+}
+
+// Point is an X,Y pair
+type Point struct {
+ X, Y int
+}
+
+// DistanceTo calculates the distance to another point.
+func (p Point) DistanceTo(other Point) float64 {
+ dx := math.Pow(float64(p.X-other.X), 2)
+ dy := math.Pow(float64(p.Y-other.Y), 2)
+ return math.Pow(dx+dy, 0.5)
+}
+
+// Equals returns if a point equals another point.
+func (p Point) Equals(other Point) bool {
+ return p.X == other.X && p.Y == other.Y
+}
+
+// String returns a string representation of the point.
+func (p Point) String() string {
+ return fmt.Sprintf("P{%d,%d}", p.X, p.Y)
+}
+
+
+
package chart
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "math"
+
+ "github.com/golang/freetype/truetype"
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// Chart is what we're drawing.
+type Chart struct {
+ Title string
+ TitleStyle Style
+
+ ColorPalette ColorPalette
+
+ Width int
+ Height int
+ DPI float64
+
+ Background Style
+ Canvas Style
+
+ XAxis XAxis
+ YAxis YAxis
+ YAxisSecondary YAxis
+
+ Font *truetype.Font
+ defaultFont *truetype.Font
+
+ Series []Series
+ Elements []Renderable
+}
+
+// GetDPI returns the dpi for the chart.
+func (c Chart) GetDPI(defaults ...float64) float64 {
+ if c.DPI == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultDPI
+ }
+ return c.DPI
+}
+
+// GetFont returns the text font.
+func (c Chart) GetFont() *truetype.Font {
+ if c.Font == nil {
+ return c.defaultFont
+ }
+ return c.Font
+}
+
+// GetWidth returns the chart width or the default value.
+func (c Chart) GetWidth() int {
+ if c.Width == 0 {
+ return DefaultChartWidth
+ }
+ return c.Width
+}
+
+// GetHeight returns the chart height or the default value.
+func (c Chart) GetHeight() int {
+ if c.Height == 0 {
+ return DefaultChartHeight
+ }
+ return c.Height
+}
+
+// Render renders the chart with the given renderer to the given io.Writer.
+func (c Chart) Render(rp RendererProvider, w io.Writer) error {
+ if len(c.Series) == 0 {
+ return errors.New("please provide at least one series")
+ }
+ if visibleSeriesErr := c.checkHasVisibleSeries(); visibleSeriesErr != nil {
+ return visibleSeriesErr
+ }
+
+ c.YAxisSecondary.AxisType = YAxisSecondary
+
+ r, err := rp(c.GetWidth(), c.GetHeight())
+ if err != nil {
+ return err
+ }
+
+ if c.Font == nil {
+ defaultFont, err := GetDefaultFont()
+ if err != nil {
+ return err
+ }
+ c.defaultFont = defaultFont
+ }
+ r.SetDPI(c.GetDPI(DefaultDPI))
+
+ c.drawBackground(r)
+
+ var xt, yt, yta []Tick
+ xr, yr, yra := c.getRanges()
+ canvasBox := c.getDefaultCanvasBox()
+ xf, yf, yfa := c.getValueFormatters()
+
+ xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
+
+ err = c.checkRanges(xr, yr, yra)
+ if err != nil {
+ r.Save(w)
+ return err
+ }
+
+ if c.hasAxes() {
+ xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
+ canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
+ xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
+
+ // do a second pass in case things haven't settled yet.
+ xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
+ canvasBox = c.getAxesAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xt, yt, yta)
+ xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
+ }
+
+ if c.hasAnnotationSeries() {
+ canvasBox = c.getAnnotationAdjustedCanvasBox(r, canvasBox, xr, yr, yra, xf, yf, yfa)
+ xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
+ xt, yt, yta = c.getAxesTicks(r, xr, yr, yra, xf, yf, yfa)
+ }
+
+ c.drawCanvas(r, canvasBox)
+ c.drawAxes(r, canvasBox, xr, yr, yra, xt, yt, yta)
+ for index, series := range c.Series {
+ c.drawSeries(r, canvasBox, xr, yr, yra, series, index)
+ }
+
+ c.drawTitle(r)
+
+ for _, a := range c.Elements {
+ a(r, canvasBox, c.styleDefaultsElements())
+ }
+
+ return r.Save(w)
+}
+
+func (c Chart) checkHasVisibleSeries() error {
+ hasVisibleSeries := false
+ var style Style
+ for _, s := range c.Series {
+ style = s.GetStyle()
+ hasVisibleSeries = hasVisibleSeries || (style.IsZero() || style.Show)
+ }
+ if !hasVisibleSeries {
+ return fmt.Errorf("must have (1) visible series; make sure if you set a style, you set .Show = true")
+ }
+ return nil
+}
+
+func (c Chart) validateSeries() error {
+ var err error
+ for _, s := range c.Series {
+ err = s.Validate()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (c Chart) getRanges() (xrange, yrange, yrangeAlt Range) {
+ var minx, maxx float64 = math.MaxFloat64, -math.MaxFloat64
+ var miny, maxy float64 = math.MaxFloat64, -math.MaxFloat64
+ var minya, maxya float64 = math.MaxFloat64, -math.MaxFloat64
+
+ seriesMappedToSecondaryAxis := false
+
+ // note: a possible future optimization is to not scan the series values if
+ // all axis are represented by either custom ticks or custom ranges.
+ for _, s := range c.Series {
+ if s.GetStyle().IsZero() || s.GetStyle().Show {
+ seriesAxis := s.GetYAxis()
+ if bvp, isBoundedValuesProvider := s.(BoundedValuesProvider); isBoundedValuesProvider {
+ seriesLength := bvp.Len()
+ for index := 0; index < seriesLength; index++ {
+ vx, vy1, vy2 := bvp.GetBoundedValues(index)
+
+ minx = math.Min(minx, vx)
+ maxx = math.Max(maxx, vx)
+
+ if seriesAxis == YAxisPrimary {
+ miny = math.Min(miny, vy1)
+ miny = math.Min(miny, vy2)
+ maxy = math.Max(maxy, vy1)
+ maxy = math.Max(maxy, vy2)
+ } else if seriesAxis == YAxisSecondary {
+ minya = math.Min(minya, vy1)
+ minya = math.Min(minya, vy2)
+ maxya = math.Max(maxya, vy1)
+ maxya = math.Max(maxya, vy2)
+ seriesMappedToSecondaryAxis = true
+ }
+ }
+ } else if vp, isValuesProvider := s.(ValuesProvider); isValuesProvider {
+ seriesLength := vp.Len()
+ for index := 0; index < seriesLength; index++ {
+ vx, vy := vp.GetValues(index)
+
+ minx = math.Min(minx, vx)
+ maxx = math.Max(maxx, vx)
+
+ if seriesAxis == YAxisPrimary {
+ miny = math.Min(miny, vy)
+ maxy = math.Max(maxy, vy)
+ } else if seriesAxis == YAxisSecondary {
+ minya = math.Min(minya, vy)
+ maxya = math.Max(maxya, vy)
+ seriesMappedToSecondaryAxis = true
+ }
+ }
+ }
+ }
+ }
+
+ if c.XAxis.Range == nil {
+ xrange = &ContinuousRange{}
+ } else {
+ xrange = c.XAxis.Range
+ }
+
+ if c.YAxis.Range == nil {
+ yrange = &ContinuousRange{}
+ } else {
+ yrange = c.YAxis.Range
+ }
+
+ if c.YAxisSecondary.Range == nil {
+ yrangeAlt = &ContinuousRange{}
+ } else {
+ yrangeAlt = c.YAxisSecondary.Range
+ }
+
+ if len(c.XAxis.Ticks) > 0 {
+ tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
+ for _, t := range c.XAxis.Ticks {
+ tickMin = math.Min(tickMin, t.Value)
+ tickMax = math.Max(tickMax, t.Value)
+ }
+ xrange.SetMin(tickMin)
+ xrange.SetMax(tickMax)
+ } else if xrange.IsZero() {
+ xrange.SetMin(minx)
+ xrange.SetMax(maxx)
+ }
+
+ if len(c.YAxis.Ticks) > 0 {
+ tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
+ for _, t := range c.YAxis.Ticks {
+ tickMin = math.Min(tickMin, t.Value)
+ tickMax = math.Max(tickMax, t.Value)
+ }
+ yrange.SetMin(tickMin)
+ yrange.SetMax(tickMax)
+ } else if yrange.IsZero() {
+ yrange.SetMin(miny)
+ yrange.SetMax(maxy)
+
+ // only round if we're showing the axis
+ if c.YAxis.Style.Show {
+ delta := yrange.GetDelta()
+ roundTo := util.Math.GetRoundToForDelta(delta)
+ rmin, rmax := util.Math.RoundDown(yrange.GetMin(), roundTo), util.Math.RoundUp(yrange.GetMax(), roundTo)
+
+ yrange.SetMin(rmin)
+ yrange.SetMax(rmax)
+ }
+ }
+
+ if len(c.YAxisSecondary.Ticks) > 0 {
+ tickMin, tickMax := math.MaxFloat64, -math.MaxFloat64
+ for _, t := range c.YAxis.Ticks {
+ tickMin = math.Min(tickMin, t.Value)
+ tickMax = math.Max(tickMax, t.Value)
+ }
+ yrangeAlt.SetMin(tickMin)
+ yrangeAlt.SetMax(tickMax)
+ } else if seriesMappedToSecondaryAxis && yrangeAlt.IsZero() {
+ yrangeAlt.SetMin(minya)
+ yrangeAlt.SetMax(maxya)
+
+ if c.YAxisSecondary.Style.Show {
+ delta := yrangeAlt.GetDelta()
+ roundTo := util.Math.GetRoundToForDelta(delta)
+ rmin, rmax := util.Math.RoundDown(yrangeAlt.GetMin(), roundTo), util.Math.RoundUp(yrangeAlt.GetMax(), roundTo)
+ yrangeAlt.SetMin(rmin)
+ yrangeAlt.SetMax(rmax)
+ }
+ }
+
+ return
+}
+
+func (c Chart) checkRanges(xr, yr, yra Range) error {
+ xDelta := xr.GetDelta()
+ if math.IsInf(xDelta, 0) {
+ return errors.New("infinite x-range delta")
+ }
+ if math.IsNaN(xDelta) {
+ return errors.New("nan x-range delta")
+ }
+ if xDelta == 0 {
+ return errors.New("zero x-range delta; there needs to be at least (2) values")
+ }
+
+ yDelta := yr.GetDelta()
+ if math.IsInf(yDelta, 0) {
+ return errors.New("infinite y-range delta")
+ }
+ if math.IsNaN(yDelta) {
+ return errors.New("nan y-range delta")
+ }
+
+ if c.hasSecondarySeries() {
+ yraDelta := yra.GetDelta()
+ if math.IsInf(yraDelta, 0) {
+ return errors.New("infinite secondary y-range delta")
+ }
+ if math.IsNaN(yraDelta) {
+ return errors.New("nan secondary y-range delta")
+ }
+ }
+
+ return nil
+}
+
+func (c Chart) getDefaultCanvasBox() Box {
+ return c.Box()
+}
+
+func (c Chart) getValueFormatters() (x, y, ya ValueFormatter) {
+ for _, s := range c.Series {
+ if vfp, isVfp := s.(ValueFormatterProvider); isVfp {
+ sx, sy := vfp.GetValueFormatters()
+ if s.GetYAxis() == YAxisPrimary {
+ x = sx
+ y = sy
+ } else if s.GetYAxis() == YAxisSecondary {
+ x = sx
+ ya = sy
+ }
+ }
+ }
+ if c.XAxis.ValueFormatter != nil {
+ x = c.XAxis.GetValueFormatter()
+ }
+ if c.YAxis.ValueFormatter != nil {
+ y = c.YAxis.GetValueFormatter()
+ }
+ if c.YAxisSecondary.ValueFormatter != nil {
+ ya = c.YAxisSecondary.GetValueFormatter()
+ }
+ return
+}
+
+func (c Chart) hasAxes() bool {
+ return c.XAxis.Style.Show || c.YAxis.Style.Show || c.YAxisSecondary.Style.Show
+}
+
+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, c.styleDefaultsAxes(), xf)
+ }
+ if c.YAxis.Style.Show {
+ yticks = c.YAxis.GetTicks(r, yr, c.styleDefaultsAxes(), yf)
+ }
+ if c.YAxisSecondary.Style.Show {
+ yticksAlt = c.YAxisSecondary.GetTicks(r, yar, c.styleDefaultsAxes(), yfa)
+ }
+ return
+}
+
+func (c Chart) getAxesAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xticks, yticks, yticksAlt []Tick) Box {
+ axesOuterBox := canvasBox.Clone()
+ if c.XAxis.Style.Show {
+ axesBounds := c.XAxis.Measure(r, canvasBox, xr, c.styleDefaultsAxes(), xticks)
+ axesOuterBox = axesOuterBox.Grow(axesBounds)
+ }
+ if c.YAxis.Style.Show {
+ axesBounds := c.YAxis.Measure(r, canvasBox, yr, c.styleDefaultsAxes(), yticks)
+ axesOuterBox = axesOuterBox.Grow(axesBounds)
+ }
+ if c.YAxisSecondary.Style.Show {
+ axesBounds := c.YAxisSecondary.Measure(r, canvasBox, yra, c.styleDefaultsAxes(), yticksAlt)
+ axesOuterBox = axesOuterBox.Grow(axesBounds)
+ }
+
+ return canvasBox.OuterConstrain(c.Box(), axesOuterBox)
+}
+
+func (c Chart) setRangeDomains(canvasBox Box, xr, yr, yra Range) (Range, Range, Range) {
+ xr.SetDomain(canvasBox.Width())
+ yr.SetDomain(canvasBox.Height())
+ yra.SetDomain(canvasBox.Height())
+ return xr, yr, yra
+}
+
+func (c Chart) hasAnnotationSeries() bool {
+ for _, s := range c.Series {
+ if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
+ if as.Style.IsZero() || as.Style.Show {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (c Chart) hasSecondarySeries() bool {
+ for _, s := range c.Series {
+ if s.GetYAxis() == YAxisSecondary {
+ return true
+ }
+ }
+ return false
+}
+
+func (c Chart) getAnnotationAdjustedCanvasBox(r Renderer, canvasBox Box, xr, yr, yra Range, xf, yf, yfa ValueFormatter) Box {
+ annotationSeriesBox := canvasBox.Clone()
+ for seriesIndex, s := range c.Series {
+ if as, isAnnotationSeries := s.(AnnotationSeries); isAnnotationSeries {
+ if as.Style.IsZero() || as.Style.Show {
+ style := c.styleDefaultsSeries(seriesIndex)
+ var annotationBounds Box
+ if as.YAxis == YAxisPrimary {
+ annotationBounds = as.Measure(r, canvasBox, xr, yr, style)
+ } else if as.YAxis == YAxisSecondary {
+ annotationBounds = as.Measure(r, canvasBox, xr, yra, style)
+ }
+
+ annotationSeriesBox = annotationSeriesBox.Grow(annotationBounds)
+ }
+ }
+ }
+
+ return canvasBox.OuterConstrain(c.Box(), annotationSeriesBox)
+}
+
+func (c Chart) getBackgroundStyle() Style {
+ return c.Background.InheritFrom(c.styleDefaultsBackground())
+}
+
+func (c Chart) drawBackground(r Renderer) {
+ Draw.Box(r, Box{
+ Right: c.GetWidth(),
+ Bottom: c.GetHeight(),
+ }, c.getBackgroundStyle())
+}
+
+func (c Chart) getCanvasStyle() Style {
+ return c.Canvas.InheritFrom(c.styleDefaultsCanvas())
+}
+
+func (c Chart) drawCanvas(r Renderer, canvasBox Box) {
+ Draw.Box(r, canvasBox, c.getCanvasStyle())
+}
+
+func (c Chart) drawAxes(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, xticks, yticks, yticksAlt []Tick) {
+ if c.XAxis.Style.Show {
+ c.XAxis.Render(r, canvasBox, xrange, c.styleDefaultsAxes(), xticks)
+ }
+ if c.YAxis.Style.Show {
+ c.YAxis.Render(r, canvasBox, yrange, c.styleDefaultsAxes(), yticks)
+ }
+ if c.YAxisSecondary.Style.Show {
+ c.YAxisSecondary.Render(r, canvasBox, yrangeAlt, c.styleDefaultsAxes(), yticksAlt)
+ }
+}
+
+func (c Chart) drawSeries(r Renderer, canvasBox Box, xrange, yrange, yrangeAlt Range, s Series, seriesIndex int) {
+ if s.GetStyle().IsZero() || s.GetStyle().Show {
+ if s.GetYAxis() == YAxisPrimary {
+ s.Render(r, canvasBox, xrange, yrange, c.styleDefaultsSeries(seriesIndex))
+ } else if s.GetYAxis() == YAxisSecondary {
+ s.Render(r, canvasBox, xrange, yrangeAlt, c.styleDefaultsSeries(seriesIndex))
+ }
+ }
+}
+
+func (c Chart) drawTitle(r Renderer) {
+ if len(c.Title) > 0 && c.TitleStyle.Show {
+ r.SetFont(c.TitleStyle.GetFont(c.GetFont()))
+ r.SetFontColor(c.TitleStyle.GetFontColor(c.GetColorPalette().TextColor()))
+ titleFontSize := c.TitleStyle.GetFontSize(DefaultTitleFontSize)
+ r.SetFontSize(titleFontSize)
+
+ textBox := r.MeasureText(c.Title)
+
+ textWidth := textBox.Width()
+ textHeight := textBox.Height()
+
+ titleX := (c.GetWidth() >> 1) - (textWidth >> 1)
+ titleY := c.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
+
+ r.Text(c.Title, titleX, titleY)
+ }
+}
+
+func (c Chart) styleDefaultsBackground() Style {
+ return Style{
+ FillColor: c.GetColorPalette().BackgroundColor(),
+ StrokeColor: c.GetColorPalette().BackgroundStrokeColor(),
+ StrokeWidth: DefaultBackgroundStrokeWidth,
+ }
+}
+
+func (c Chart) styleDefaultsCanvas() Style {
+ return Style{
+ FillColor: c.GetColorPalette().CanvasColor(),
+ StrokeColor: c.GetColorPalette().CanvasStrokeColor(),
+ StrokeWidth: DefaultCanvasStrokeWidth,
+ }
+}
+
+func (c Chart) styleDefaultsSeries(seriesIndex int) Style {
+ return Style{
+ DotColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
+ StrokeColor: c.GetColorPalette().GetSeriesColor(seriesIndex),
+ StrokeWidth: DefaultSeriesLineWidth,
+ Font: c.GetFont(),
+ FontSize: DefaultFontSize,
+ }
+}
+
+func (c Chart) styleDefaultsAxes() Style {
+ return Style{
+ Font: c.GetFont(),
+ FontColor: c.GetColorPalette().TextColor(),
+ FontSize: DefaultAxisFontSize,
+ StrokeColor: c.GetColorPalette().AxisStrokeColor(),
+ StrokeWidth: DefaultAxisLineWidth,
+ }
+}
+
+func (c Chart) styleDefaultsElements() Style {
+ return Style{
+ Font: c.GetFont(),
+ }
+}
+
+// GetColorPalette returns the color palette for the chart.
+func (c Chart) GetColorPalette() ColorPalette {
+ if c.ColorPalette != nil {
+ return c.ColorPalette
+ }
+ return DefaultColorPalette
+}
+
+// Box returns the chart bounds as a box.
+func (c Chart) Box() Box {
+ dpr := c.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
+ dpb := c.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
+
+ return Box{
+ Top: c.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
+ Left: c.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
+ Right: c.GetWidth() - dpr,
+ Bottom: c.GetHeight() - dpb,
+ }
+}
+
+
+
package chart
+
+import "github.com/wcharczuk/go-chart/drawing"
+
+var (
+ // ColorWhite is white.
+ ColorWhite = drawing.Color{R: 255, G: 255, B: 255, A: 255}
+ // ColorBlue is the basic theme blue color.
+ ColorBlue = drawing.Color{R: 0, G: 116, B: 217, A: 255}
+ // ColorCyan is the basic theme cyan color.
+ ColorCyan = drawing.Color{R: 0, G: 217, B: 210, A: 255}
+ // ColorGreen is the basic theme green color.
+ ColorGreen = drawing.Color{R: 0, G: 217, B: 101, A: 255}
+ // ColorRed is the basic theme red color.
+ ColorRed = drawing.Color{R: 217, G: 0, B: 116, A: 255}
+ // ColorOrange is the basic theme orange color.
+ ColorOrange = drawing.Color{R: 217, G: 101, B: 0, A: 255}
+ // ColorYellow is the basic theme yellow color.
+ ColorYellow = drawing.Color{R: 217, G: 210, B: 0, A: 255}
+ // ColorBlack is the basic theme black color.
+ ColorBlack = drawing.Color{R: 51, G: 51, B: 51, A: 255}
+ // ColorLightGray is the basic theme light gray color.
+ ColorLightGray = drawing.Color{R: 239, G: 239, B: 239, A: 255}
+
+ // ColorAlternateBlue is a alternate theme color.
+ ColorAlternateBlue = drawing.Color{R: 106, G: 195, B: 203, A: 255}
+ // ColorAlternateGreen is a alternate theme color.
+ ColorAlternateGreen = drawing.Color{R: 42, G: 190, B: 137, A: 255}
+ // ColorAlternateGray is a alternate theme color.
+ ColorAlternateGray = drawing.Color{R: 110, G: 128, B: 139, A: 255}
+ // ColorAlternateYellow is a alternate theme color.
+ ColorAlternateYellow = drawing.Color{R: 240, G: 174, B: 90, A: 255}
+ // ColorAlternateLightGray is a alternate theme color.
+ ColorAlternateLightGray = drawing.Color{R: 187, G: 190, B: 191, A: 255}
+
+ // ColorTransparent is a transparent (alpha zero) color.
+ ColorTransparent = drawing.Color{R: 1, G: 1, B: 1, A: 0}
+)
+
+var (
+ // DefaultBackgroundColor is the default chart background color.
+ // It is equivalent to css color:white.
+ DefaultBackgroundColor = ColorWhite
+ // DefaultBackgroundStrokeColor is the default chart border color.
+ // It is equivalent to color:white.
+ DefaultBackgroundStrokeColor = ColorWhite
+ // DefaultCanvasColor is the default chart canvas color.
+ // It is equivalent to css color:white.
+ DefaultCanvasColor = ColorWhite
+ // DefaultCanvasStrokeColor is the default chart canvas stroke color.
+ // It is equivalent to css color:white.
+ DefaultCanvasStrokeColor = ColorWhite
+ // DefaultTextColor is the default chart text color.
+ // It is equivalent to #333333.
+ DefaultTextColor = ColorBlack
+ // DefaultAxisColor is the default chart axis line color.
+ // It is equivalent to #333333.
+ DefaultAxisColor = ColorBlack
+ // DefaultStrokeColor is the default chart border color.
+ // It is equivalent to #efefef.
+ DefaultStrokeColor = ColorLightGray
+ // DefaultFillColor is the default fill color.
+ // It is equivalent to #0074d9.
+ DefaultFillColor = ColorBlue
+ // DefaultAnnotationFillColor is the default annotation background color.
+ DefaultAnnotationFillColor = ColorWhite
+ // DefaultGridLineColor is the default grid line color.
+ DefaultGridLineColor = ColorLightGray
+)
+
+var (
+ // DefaultColors are a couple default series colors.
+ DefaultColors = []drawing.Color{
+ ColorBlue,
+ ColorGreen,
+ ColorRed,
+ ColorCyan,
+ ColorOrange,
+ }
+
+ // DefaultAlternateColors are a couple alternate colors.
+ DefaultAlternateColors = []drawing.Color{
+ ColorAlternateBlue,
+ ColorAlternateGreen,
+ ColorAlternateGray,
+ ColorAlternateYellow,
+ ColorBlue,
+ ColorGreen,
+ ColorRed,
+ ColorCyan,
+ ColorOrange,
+ }
+)
+
+// GetDefaultColor returns a color from the default list by index.
+// NOTE: the index will wrap around (using a modulo).
+func GetDefaultColor(index int) drawing.Color {
+ finalIndex := index % len(DefaultColors)
+ return DefaultColors[finalIndex]
+}
+
+// GetAlternateColor returns a color from the default list by index.
+// NOTE: the index will wrap around (using a modulo).
+func GetAlternateColor(index int) drawing.Color {
+ finalIndex := index % len(DefaultAlternateColors)
+ return DefaultAlternateColors[finalIndex]
+}
+
+// ColorPalette is a set of colors that.
+type ColorPalette interface {
+ BackgroundColor() drawing.Color
+ BackgroundStrokeColor() drawing.Color
+ CanvasColor() drawing.Color
+ CanvasStrokeColor() drawing.Color
+ AxisStrokeColor() drawing.Color
+ TextColor() drawing.Color
+ GetSeriesColor(index int) drawing.Color
+}
+
+// DefaultColorPalette represents the default palatte.
+var DefaultColorPalette defaultColorPalette
+
+type defaultColorPalette struct{}
+
+func (dp defaultColorPalette) BackgroundColor() drawing.Color {
+ return DefaultBackgroundColor
+}
+
+func (dp defaultColorPalette) BackgroundStrokeColor() drawing.Color {
+ return DefaultBackgroundStrokeColor
+}
+
+func (dp defaultColorPalette) CanvasColor() drawing.Color {
+ return DefaultCanvasColor
+}
+
+func (dp defaultColorPalette) CanvasStrokeColor() drawing.Color {
+ return DefaultCanvasStrokeColor
+}
+
+func (dp defaultColorPalette) AxisStrokeColor() drawing.Color {
+ return DefaultAxisColor
+}
+
+func (dp defaultColorPalette) TextColor() drawing.Color {
+ return DefaultTextColor
+}
+
+func (dp defaultColorPalette) GetSeriesColor(index int) drawing.Color {
+ return GetDefaultColor(index)
+}
+
+// AlternateColorPalette represents the default palatte.
+var AlternateColorPalette alternateColorPalette
+
+type alternateColorPalette struct{}
+
+func (ap alternateColorPalette) BackgroundColor() drawing.Color {
+ return DefaultBackgroundColor
+}
+
+func (ap alternateColorPalette) BackgroundStrokeColor() drawing.Color {
+ return DefaultBackgroundStrokeColor
+}
+
+func (ap alternateColorPalette) CanvasColor() drawing.Color {
+ return DefaultCanvasColor
+}
+
+func (ap alternateColorPalette) CanvasStrokeColor() drawing.Color {
+ return DefaultCanvasStrokeColor
+}
+
+func (ap alternateColorPalette) AxisStrokeColor() drawing.Color {
+ return DefaultAxisColor
+}
+
+func (ap alternateColorPalette) TextColor() drawing.Color {
+ return DefaultTextColor
+}
+
+func (ap alternateColorPalette) GetSeriesColor(index int) drawing.Color {
+ return GetAlternateColor(index)
+}
+
+
+
package chart
+
+// ConcatSeries is a special type of series that concatenates its `InnerSeries`.
+type ConcatSeries []Series
+
+// Len returns the length of the concatenated set of series.
+func (cs ConcatSeries) Len() int {
+ total := 0
+ for _, s := range cs {
+ if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
+ total += typed.Len()
+ }
+ }
+
+ return total
+}
+
+// GetValue returns the value at the (meta) index (i.e 0 => totalLen-1)
+func (cs ConcatSeries) GetValue(index int) (x, y float64) {
+ cursor := 0
+ for _, s := range cs {
+ if typed, isValuesProvider := s.(ValuesProvider); isValuesProvider {
+ len := typed.Len()
+ if index < cursor+len {
+ x, y = typed.GetValues(index - cursor) //FENCEPOSTS.
+ return
+ }
+ cursor += typed.Len()
+ }
+ }
+ return
+}
+
+// Validate validates the series.
+func (cs ConcatSeries) Validate() error {
+ var err error
+ for _, s := range cs {
+ err = s.Validate()
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "math"
+)
+
+// ContinuousRange represents a boundary for a set of numbers.
+type ContinuousRange struct {
+ Min float64
+ Max float64
+ Domain int
+ Descending bool
+}
+
+// IsDescending returns if the range is descending.
+func (r ContinuousRange) IsDescending() bool {
+ return r.Descending
+}
+
+// IsZero returns if the ContinuousRange has been set or not.
+func (r ContinuousRange) IsZero() bool {
+ return (r.Min == 0 || math.IsNaN(r.Min)) &&
+ (r.Max == 0 || math.IsNaN(r.Max)) &&
+ r.Domain == 0
+}
+
+// GetMin gets the min value for the continuous range.
+func (r ContinuousRange) GetMin() float64 {
+ return r.Min
+}
+
+// SetMin sets the min value for the continuous range.
+func (r *ContinuousRange) SetMin(min float64) {
+ r.Min = min
+}
+
+// GetMax returns the max value for the continuous range.
+func (r ContinuousRange) GetMax() float64 {
+ return r.Max
+}
+
+// SetMax sets the max value for the continuous range.
+func (r *ContinuousRange) SetMax(max float64) {
+ r.Max = max
+}
+
+// GetDelta returns the difference between the min and max value.
+func (r ContinuousRange) GetDelta() float64 {
+ return r.Max - r.Min
+}
+
+// GetDomain returns the range domain.
+func (r ContinuousRange) GetDomain() int {
+ return r.Domain
+}
+
+// SetDomain sets the range domain.
+func (r *ContinuousRange) SetDomain(domain int) {
+ r.Domain = domain
+}
+
+// String returns a simple string for the ContinuousRange.
+func (r ContinuousRange) String() string {
+ return fmt.Sprintf("ContinuousRange [%.2f,%.2f] => %d", r.Min, r.Max, r.Domain)
+}
+
+// Translate maps a given value into the ContinuousRange space.
+func (r ContinuousRange) Translate(value float64) int {
+ normalized := value - r.Min
+ ratio := normalized / r.GetDelta()
+
+ if r.IsDescending() {
+ return r.Domain - int(math.Ceil(ratio*float64(r.Domain)))
+ }
+
+ return int(math.Ceil(ratio * float64(r.Domain)))
+}
+
+
+
package chart
+
+import "fmt"
+
+// Interface Assertions.
+var (
+ _ Series = (*ContinuousSeries)(nil)
+ _ FirstValuesProvider = (*ContinuousSeries)(nil)
+ _ LastValuesProvider = (*ContinuousSeries)(nil)
+)
+
+// ContinuousSeries represents a line on a chart.
+type ContinuousSeries struct {
+ Name string
+ Style Style
+
+ YAxis YAxisType
+
+ XValueFormatter ValueFormatter
+ YValueFormatter ValueFormatter
+
+ 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)
+}
+
+// GetValues gets the x,y values at a given index.
+func (cs ContinuousSeries) GetValues(index int) (float64, float64) {
+ return cs.XValues[index], cs.YValues[index]
+}
+
+// GetFirstValues gets the first x,y values.
+func (cs ContinuousSeries) GetFirstValues() (float64, float64) {
+ return cs.XValues[0], cs.YValues[0]
+}
+
+// GetLastValues gets the last x,y values.
+func (cs ContinuousSeries) GetLastValues() (float64, float64) {
+ return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1]
+}
+
+// GetValueFormatters returns value formatter defaults for the series.
+func (cs ContinuousSeries) GetValueFormatters() (x, y ValueFormatter) {
+ if cs.XValueFormatter != nil {
+ x = cs.XValueFormatter
+ } else {
+ x = FloatValueFormatter
+ }
+ if cs.YValueFormatter != nil {
+ y = cs.YValueFormatter
+ } else {
+ 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.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, cs)
+}
+
+// Validate validates the series.
+func (cs ContinuousSeries) Validate() error {
+ if len(cs.XValues) == 0 {
+ return fmt.Errorf("continuous series must have xvalues set")
+ }
+
+ if len(cs.YValues) == 0 {
+ return fmt.Errorf("continuous series must have yvalues set")
+ }
+ return nil
+}
+
+
+
package chart
+
+import (
+ "math"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+var (
+ // Draw contains helpers for drawing common objects.
+ Draw = &draw{}
+)
+
+type draw struct{}
+
+// LineSeries draws a line series with a renderer.
+func (d draw) LineSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider) {
+ if vs.Len() == 0 {
+ return
+ }
+
+ cb := canvasBox.Bottom
+ cl := canvasBox.Left
+
+ v0x, v0y := vs.GetValues(0)
+ x0 := cl + xrange.Translate(v0x)
+ y0 := cb - yrange.Translate(v0y)
+
+ yv0 := yrange.Translate(0)
+
+ var vx, vy float64
+ var x, y int
+
+ if style.ShouldDrawStroke() && style.ShouldDrawFill() {
+ style.GetFillOptions().WriteDrawingOptionsToRenderer(r)
+ r.MoveTo(x0, y0)
+ for i := 1; i < vs.Len(); i++ {
+ vx, vy = vs.GetValues(i)
+ x = cl + xrange.Translate(vx)
+ y = cb - yrange.Translate(vy)
+ r.LineTo(x, y)
+ }
+ r.LineTo(x, util.Math.MinInt(cb, cb-yv0))
+ r.LineTo(x0, util.Math.MinInt(cb, cb-yv0))
+ r.LineTo(x0, y0)
+ r.Fill()
+ }
+
+ if style.ShouldDrawStroke() {
+ style.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
+
+ r.MoveTo(x0, y0)
+ for i := 1; i < vs.Len(); i++ {
+ vx, vy = vs.GetValues(i)
+ x = cl + xrange.Translate(vx)
+ y = cb - yrange.Translate(vy)
+ r.LineTo(x, y)
+ }
+ r.Stroke()
+ }
+
+ if style.ShouldDrawDot() {
+ defaultDotWidth := style.GetDotWidth()
+
+ style.GetDotOptions().WriteDrawingOptionsToRenderer(r)
+ for i := 0; i < vs.Len(); i++ {
+ vx, vy = vs.GetValues(i)
+ x = cl + xrange.Translate(vx)
+ y = cb - yrange.Translate(vy)
+
+ dotWidth := defaultDotWidth
+ if style.DotWidthProvider != nil {
+ dotWidth = style.DotWidthProvider(xrange, yrange, i, vx, vy)
+ }
+
+ if style.DotColorProvider != nil {
+ dotColor := style.DotColorProvider(xrange, yrange, i, vx, vy)
+
+ r.SetFillColor(dotColor)
+ r.SetStrokeColor(dotColor)
+ }
+
+ r.Circle(dotWidth, x, y)
+ r.FillStroke()
+ }
+ }
+}
+
+// BoundedSeries draws a series that implements BoundedValuesProvider.
+func (d draw) BoundedSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, bbs BoundedValuesProvider, drawOffsetIndexes ...int) {
+ drawOffsetIndex := 0
+ if len(drawOffsetIndexes) > 0 {
+ drawOffsetIndex = drawOffsetIndexes[0]
+ }
+
+ cb := canvasBox.Bottom
+ cl := canvasBox.Left
+
+ v0x, v0y1, v0y2 := bbs.GetBoundedValues(0)
+ x0 := cl + xrange.Translate(v0x)
+ y0 := cb - yrange.Translate(v0y1)
+
+ var vx, vy1, vy2 float64
+ var x, y int
+
+ xvalues := make([]float64, bbs.Len())
+ xvalues[0] = v0x
+ y2values := make([]float64, bbs.Len())
+ y2values[0] = v0y2
+
+ style.GetFillAndStrokeOptions().WriteToRenderer(r)
+ r.MoveTo(x0, y0)
+ for i := 1; i < bbs.Len(); i++ {
+ vx, vy1, vy2 = bbs.GetBoundedValues(i)
+
+ xvalues[i] = vx
+ y2values[i] = vy2
+
+ x = cl + xrange.Translate(vx)
+ y = cb - yrange.Translate(vy1)
+ if i > drawOffsetIndex {
+ r.LineTo(x, y)
+ } else {
+ r.MoveTo(x, y)
+ }
+ }
+ y = cb - yrange.Translate(vy2)
+ r.LineTo(x, y)
+ for i := bbs.Len() - 1; i >= drawOffsetIndex; i-- {
+ vx, vy2 = xvalues[i], y2values[i]
+ x = cl + xrange.Translate(vx)
+ y = cb - yrange.Translate(vy2)
+ r.LineTo(x, y)
+ }
+ r.Close()
+ r.FillStroke()
+}
+
+// HistogramSeries draws a value provider as boxes from 0.
+func (d draw) HistogramSeries(r Renderer, canvasBox Box, xrange, yrange Range, style Style, vs ValuesProvider, barWidths ...int) {
+ if vs.Len() == 0 {
+ return
+ }
+
+ //calculate bar width?
+ seriesLength := vs.Len()
+ barWidth := int(math.Floor(float64(xrange.GetDomain()) / float64(seriesLength)))
+ if len(barWidths) > 0 {
+ barWidth = barWidths[0]
+ }
+
+ cb := canvasBox.Bottom
+ cl := canvasBox.Left
+
+ //foreach datapoint, draw a box.
+ for index := 0; index < seriesLength; index++ {
+ vx, vy := vs.GetValues(index)
+ y0 := yrange.Translate(0)
+ x := cl + xrange.Translate(vx)
+ y := yrange.Translate(vy)
+
+ d.Box(r, Box{
+ Top: cb - y0,
+ Left: x - (barWidth >> 1),
+ Right: x + (barWidth >> 1),
+ Bottom: cb - y,
+ }, style)
+ }
+}
+
+// MeasureAnnotation measures how big an annotation would be.
+func (d draw) MeasureAnnotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) Box {
+ style.WriteToRenderer(r)
+ defer r.ResetStyle()
+
+ textBox := r.MeasureText(label)
+ textWidth := textBox.Width()
+ textHeight := textBox.Height()
+ halfTextHeight := textHeight >> 1
+
+ pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
+ pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
+ pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
+ pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
+
+ strokeWidth := style.GetStrokeWidth()
+
+ top := ly - (pt + halfTextHeight)
+ right := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth + int(strokeWidth)
+ bottom := ly + (pb + halfTextHeight)
+
+ return Box{
+ Top: top,
+ Left: lx,
+ Right: right,
+ Bottom: bottom,
+ }
+}
+
+// Annotation draws an anotation with a renderer.
+func (d draw) Annotation(r Renderer, canvasBox Box, style Style, lx, ly int, label string) {
+ style.GetTextOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
+ textBox := r.MeasureText(label)
+ textWidth := textBox.Width()
+ halfTextHeight := textBox.Height() >> 1
+
+ style.GetFillAndStrokeOptions().WriteToRenderer(r)
+
+ pt := style.Padding.GetTop(DefaultAnnotationPadding.Top)
+ pl := style.Padding.GetLeft(DefaultAnnotationPadding.Left)
+ pr := style.Padding.GetRight(DefaultAnnotationPadding.Right)
+ pb := style.Padding.GetBottom(DefaultAnnotationPadding.Bottom)
+
+ textX := lx + pl + DefaultAnnotationDeltaWidth
+ textY := ly + halfTextHeight
+
+ ltx := lx + DefaultAnnotationDeltaWidth
+ lty := ly - (pt + halfTextHeight)
+
+ rtx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
+ rty := ly - (pt + halfTextHeight)
+
+ rbx := lx + pl + pr + textWidth + DefaultAnnotationDeltaWidth
+ rby := ly + (pb + halfTextHeight)
+
+ lbx := lx + DefaultAnnotationDeltaWidth
+ lby := ly + (pb + halfTextHeight)
+
+ r.MoveTo(lx, ly)
+ r.LineTo(ltx, lty)
+ r.LineTo(rtx, rty)
+ r.LineTo(rbx, rby)
+ r.LineTo(lbx, lby)
+ r.LineTo(lx, ly)
+ r.Close()
+ r.FillStroke()
+
+ style.GetTextOptions().WriteToRenderer(r)
+ r.Text(label, textX, textY)
+}
+
+// Box draws a box with a given style.
+func (d draw) Box(r Renderer, b Box, s Style) {
+ s.GetFillAndStrokeOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
+ r.MoveTo(b.Left, b.Top)
+ r.LineTo(b.Right, b.Top)
+ r.LineTo(b.Right, b.Bottom)
+ r.LineTo(b.Left, b.Bottom)
+ r.LineTo(b.Left, b.Top)
+ r.FillStroke()
+}
+
+func (d draw) BoxRotated(r Renderer, b Box, thetaDegrees float64, s Style) {
+ d.BoxCorners(r, b.Corners().Rotate(thetaDegrees), s)
+}
+
+func (d draw) BoxCorners(r Renderer, bc BoxCorners, s Style) {
+ s.GetFillAndStrokeOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
+ r.MoveTo(bc.TopLeft.X, bc.TopLeft.Y)
+ r.LineTo(bc.TopRight.X, bc.TopRight.Y)
+ r.LineTo(bc.BottomRight.X, bc.BottomRight.Y)
+ r.LineTo(bc.BottomLeft.X, bc.BottomLeft.Y)
+ r.Close()
+ r.FillStroke()
+}
+
+// DrawText draws text with a given style.
+func (d draw) Text(r Renderer, text string, x, y int, style Style) {
+ style.GetTextOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
+ r.Text(text, x, y)
+}
+
+func (d draw) MeasureText(r Renderer, text string, style Style) Box {
+ style.GetTextOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
+ return r.MeasureText(text)
+}
+
+// TextWithin draws the text within a given box.
+func (d draw) TextWithin(r Renderer, text string, box Box, style Style) {
+ style.GetTextOptions().WriteToRenderer(r)
+ defer r.ResetStyle()
+
+ lines := Text.WrapFit(r, text, box.Width(), style)
+ linesBox := Text.MeasureLines(r, lines, style)
+
+ y := box.Top
+
+ switch style.GetTextVerticalAlign() {
+ case TextVerticalAlignBottom, TextVerticalAlignBaseline: // i have to build better baseline handling into measure text
+ y = y - linesBox.Height()
+ case TextVerticalAlignMiddle, TextVerticalAlignMiddleBaseline:
+ y = (y - linesBox.Height()) >> 1
+ }
+
+ var tx, ty int
+ for _, line := range lines {
+ lineBox := r.MeasureText(line)
+ switch style.GetTextHorizontalAlign() {
+ case TextHorizontalAlignCenter:
+ tx = box.Left + ((box.Width() - lineBox.Width()) >> 1)
+ case TextHorizontalAlignRight:
+ tx = box.Right - lineBox.Width()
+ default:
+ tx = box.Left
+ }
+ if style.TextRotationDegrees == 0 {
+ ty = y + lineBox.Height()
+ } else {
+ ty = y
+ }
+
+ r.Text(line, tx, ty)
+ y += lineBox.Height() + style.GetTextLineSpacing()
+ }
+}
+
+
+
package drawing
+
+import (
+ "fmt"
+ "strconv"
+)
+
+var (
+ // ColorTransparent is a fully transparent color.
+ ColorTransparent = Color{}
+
+ // ColorWhite is white.
+ ColorWhite = Color{R: 255, G: 255, B: 255, A: 255}
+
+ // ColorBlack is black.
+ ColorBlack = Color{R: 0, G: 0, B: 0, A: 255}
+
+ // ColorRed is red.
+ ColorRed = Color{R: 255, G: 0, B: 0, A: 255}
+
+ // ColorGreen is green.
+ ColorGreen = Color{R: 0, G: 255, B: 0, A: 255}
+
+ // ColorBlue is blue.
+ ColorBlue = Color{R: 0, G: 0, B: 255, A: 255}
+)
+
+func parseHex(hex string) uint8 {
+ v, _ := strconv.ParseInt(hex, 16, 16)
+ return uint8(v)
+}
+
+// ColorFromHex returns a color from a css hex code.
+func ColorFromHex(hex string) Color {
+ var c Color
+ if len(hex) == 3 {
+ c.R = parseHex(string(hex[0])) * 0x11
+ c.G = parseHex(string(hex[1])) * 0x11
+ c.B = parseHex(string(hex[2])) * 0x11
+ } else {
+ c.R = parseHex(string(hex[0:2]))
+ c.G = parseHex(string(hex[2:4]))
+ c.B = parseHex(string(hex[4:6]))
+ }
+ c.A = 255
+ return c
+}
+
+// ColorFromAlphaMixedRGBA returns the system alpha mixed rgba values.
+func ColorFromAlphaMixedRGBA(r, g, b, a uint32) Color {
+ fa := float64(a) / 255.0
+ var c Color
+ c.R = uint8(float64(r) / fa)
+ c.G = uint8(float64(g) / fa)
+ c.B = uint8(float64(b) / fa)
+ c.A = uint8(a | (a >> 8))
+ return c
+}
+
+// ColorChannelFromFloat returns a normalized byte from a given float value.
+func ColorChannelFromFloat(v float64) uint8 {
+ return uint8(v * 255)
+}
+
+// Color is our internal color type because color.Color is bullshit.
+type Color struct {
+ R, G, B, A uint8
+}
+
+// RGBA returns the color as a pre-alpha mixed color set.
+func (c Color) RGBA() (r, g, b, a uint32) {
+ fa := float64(c.A) / 255.0
+ r = uint32(float64(uint32(c.R)) * fa)
+ r |= r << 8
+ g = uint32(float64(uint32(c.G)) * fa)
+ g |= g << 8
+ b = uint32(float64(uint32(c.B)) * fa)
+ b |= b << 8
+ a = uint32(c.A)
+ a |= a << 8
+ return
+}
+
+// IsZero returns if the color has been set or not.
+func (c Color) IsZero() bool {
+ return c.R == 0 && c.G == 0 && c.B == 0 && c.A == 0
+}
+
+// IsTransparent returns if the colors alpha channel is zero.
+func (c Color) IsTransparent() bool {
+ return c.A == 0
+}
+
+// WithAlpha returns a copy of the color with a given alpha.
+func (c Color) WithAlpha(a uint8) Color {
+ return Color{
+ R: c.R,
+ G: c.G,
+ B: c.B,
+ A: a,
+ }
+}
+
+// Equals returns true if the color equals another.
+func (c Color) Equals(other Color) bool {
+ return c.R == other.R &&
+ c.G == other.G &&
+ c.B == other.B &&
+ c.A == other.A
+}
+
+// AverageWith averages two colors.
+func (c Color) AverageWith(other Color) Color {
+ return Color{
+ R: (c.R + other.R) >> 1,
+ G: (c.G + other.G) >> 1,
+ B: (c.B + other.B) >> 1,
+ A: c.A,
+ }
+}
+
+// String returns a css string representation of the color.
+func (c Color) String() string {
+ fa := float64(c.A) / float64(255)
+ return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, fa)
+}
+
+
+
package drawing
+
+import "math"
+
+const (
+ // CurveRecursionLimit represents the maximum recursion that is really necessary to subsivide a curve into straight lines
+ CurveRecursionLimit = 32
+)
+
+// Cubic
+// x1, y1, cpx1, cpy1, cpx2, cpy2, x2, y2 float64
+
+// SubdivideCubic a Bezier cubic curve in 2 equivalents Bezier cubic curves.
+// c1 and c2 parameters are the resulting curves
+func SubdivideCubic(c, c1, c2 []float64) {
+ // First point of c is the first point of c1
+ c1[0], c1[1] = c[0], c[1]
+ // Last point of c is the last point of c2
+ c2[6], c2[7] = c[6], c[7]
+
+ // Subdivide segment using midpoints
+ c1[2] = (c[0] + c[2]) / 2
+ c1[3] = (c[1] + c[3]) / 2
+
+ midX := (c[2] + c[4]) / 2
+ midY := (c[3] + c[5]) / 2
+
+ c2[4] = (c[4] + c[6]) / 2
+ c2[5] = (c[5] + c[7]) / 2
+
+ c1[4] = (c1[2] + midX) / 2
+ c1[5] = (c1[3] + midY) / 2
+
+ c2[2] = (midX + c2[4]) / 2
+ c2[3] = (midY + c2[5]) / 2
+
+ c1[6] = (c1[4] + c2[2]) / 2
+ c1[7] = (c1[5] + c2[3]) / 2
+
+ // Last Point of c1 is equal to the first point of c2
+ c2[0], c2[1] = c1[6], c1[7]
+}
+
+// TraceCubic generate lines subdividing the cubic curve using a Liner
+// flattening_threshold helps determines the flattening expectation of the curve
+func TraceCubic(t Liner, cubic []float64, flatteningThreshold float64) {
+ // Allocation curves
+ var curves [CurveRecursionLimit * 8]float64
+ copy(curves[0:8], cubic[0:8])
+ i := 0
+
+ // current curve
+ var c []float64
+
+ var dx, dy, d2, d3 float64
+
+ for i >= 0 {
+ c = curves[i*8:]
+ dx = c[6] - c[0]
+ dy = c[7] - c[1]
+
+ d2 = math.Abs((c[2]-c[6])*dy - (c[3]-c[7])*dx)
+ d3 = math.Abs((c[4]-c[6])*dy - (c[5]-c[7])*dx)
+
+ // if it's flat then trace a line
+ if (d2+d3)*(d2+d3) < flatteningThreshold*(dx*dx+dy*dy) || i == len(curves)-1 {
+ t.LineTo(c[6], c[7])
+ i--
+ } else {
+ // second half of bezier go lower onto the stack
+ SubdivideCubic(c, curves[(i+1)*8:], curves[i*8:])
+ i++
+ }
+ }
+}
+
+// Quad
+// x1, y1, cpx1, cpy2, x2, y2 float64
+
+// SubdivideQuad a Bezier quad curve in 2 equivalents Bezier quad curves.
+// c1 and c2 parameters are the resulting curves
+func SubdivideQuad(c, c1, c2 []float64) {
+ // First point of c is the first point of c1
+ c1[0], c1[1] = c[0], c[1]
+ // Last point of c is the last point of c2
+ c2[4], c2[5] = c[4], c[5]
+
+ // Subdivide segment using midpoints
+ c1[2] = (c[0] + c[2]) / 2
+ c1[3] = (c[1] + c[3]) / 2
+ c2[2] = (c[2] + c[4]) / 2
+ c2[3] = (c[3] + c[5]) / 2
+ c1[4] = (c1[2] + c2[2]) / 2
+ c1[5] = (c1[3] + c2[3]) / 2
+ c2[0], c2[1] = c1[4], c1[5]
+ return
+}
+
+func traceWindowIndices(i int) (startAt, endAt int) {
+ startAt = i * 6
+ endAt = startAt + 6
+ return
+}
+
+func traceCalcDeltas(c []float64) (dx, dy, d float64) {
+ dx = c[4] - c[0]
+ dy = c[5] - c[1]
+ d = math.Abs(((c[2]-c[4])*dy - (c[3]-c[5])*dx))
+ return
+}
+
+func traceIsFlat(dx, dy, d, threshold float64) bool {
+ return (d * d) < threshold*(dx*dx+dy*dy)
+}
+
+func traceGetWindow(curves []float64, i int) []float64 {
+ startAt, endAt := traceWindowIndices(i)
+ return curves[startAt:endAt]
+}
+
+// TraceQuad generate lines subdividing the curve using a Liner
+// flattening_threshold helps determines the flattening expectation of the curve
+func TraceQuad(t Liner, quad []float64, flatteningThreshold float64) {
+ const curveLen = CurveRecursionLimit * 6
+ const curveEndIndex = curveLen - 1
+ const lastIteration = CurveRecursionLimit - 1
+
+ // Allocates curves stack
+ curves := make([]float64, curveLen)
+
+ // copy 6 elements from the quad path to the stack
+ copy(curves[0:6], quad[0:6])
+
+ var i int
+ var c []float64
+ var dx, dy, d float64
+
+ for i >= 0 {
+ c = traceGetWindow(curves, i)
+ dx, dy, d = traceCalcDeltas(c)
+
+ // bail early if the distance is 0
+ if d == 0 {
+ return
+ }
+
+ // if it's flat then trace a line
+ if traceIsFlat(dx, dy, d, flatteningThreshold) || i == lastIteration {
+ t.LineTo(c[4], c[5])
+ i--
+ } else {
+ SubdivideQuad(c, traceGetWindow(curves, i+1), traceGetWindow(curves, i))
+ i++
+ }
+ }
+}
+
+// TraceArc trace an arc using a Liner
+func TraceArc(t Liner, x, y, rx, ry, start, angle, scale float64) (lastX, lastY float64) {
+ end := start + angle
+ clockWise := true
+ if angle < 0 {
+ clockWise = false
+ }
+ ra := (math.Abs(rx) + math.Abs(ry)) / 2
+ da := math.Acos(ra/(ra+0.125/scale)) * 2
+ //normalize
+ if !clockWise {
+ da = -da
+ }
+ angle = start + da
+ var curX, curY float64
+ for {
+ if (angle < end-da/4) != clockWise {
+ curX = x + math.Cos(end)*rx
+ curY = y + math.Sin(end)*ry
+ return curX, curY
+ }
+ curX = x + math.Cos(angle)*rx
+ curY = y + math.Sin(angle)*ry
+
+ angle += da
+ t.LineTo(curX, curY)
+ }
+}
+
+
+
package drawing
+
+// NewDashVertexConverter creates a new dash converter.
+func NewDashVertexConverter(dash []float64, dashOffset float64, flattener Flattener) *DashVertexConverter {
+ var dasher DashVertexConverter
+ dasher.dash = dash
+ dasher.currentDash = 0
+ dasher.dashOffset = dashOffset
+ dasher.next = flattener
+ return &dasher
+}
+
+// DashVertexConverter is a converter for dash vertexes.
+type DashVertexConverter struct {
+ next Flattener
+ x, y, distance float64
+ dash []float64
+ currentDash int
+ dashOffset float64
+}
+
+// LineTo implements the pathbuilder interface.
+func (dasher *DashVertexConverter) LineTo(x, y float64) {
+ dasher.lineTo(x, y)
+}
+
+// MoveTo implements the pathbuilder interface.
+func (dasher *DashVertexConverter) MoveTo(x, y float64) {
+ dasher.next.MoveTo(x, y)
+ dasher.x, dasher.y = x, y
+ dasher.distance = dasher.dashOffset
+ dasher.currentDash = 0
+}
+
+// LineJoin implements the pathbuilder interface.
+func (dasher *DashVertexConverter) LineJoin() {
+ dasher.next.LineJoin()
+}
+
+// Close implements the pathbuilder interface.
+func (dasher *DashVertexConverter) Close() {
+ dasher.next.Close()
+}
+
+// End implements the pathbuilder interface.
+func (dasher *DashVertexConverter) End() {
+ dasher.next.End()
+}
+
+func (dasher *DashVertexConverter) lineTo(x, y float64) {
+ rest := dasher.dash[dasher.currentDash] - dasher.distance
+ for rest < 0 {
+ dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
+ dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
+ rest = dasher.dash[dasher.currentDash] - dasher.distance
+ }
+ d := distance(dasher.x, dasher.y, x, y)
+ for d >= rest {
+ k := rest / d
+ lx := dasher.x + k*(x-dasher.x)
+ ly := dasher.y + k*(y-dasher.y)
+ if dasher.currentDash%2 == 0 {
+ // line
+ dasher.next.LineTo(lx, ly)
+ } else {
+ // gap
+ dasher.next.End()
+ dasher.next.MoveTo(lx, ly)
+ }
+ d = d - rest
+ dasher.x, dasher.y = lx, ly
+ dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
+ rest = dasher.dash[dasher.currentDash]
+ }
+ dasher.distance = d
+ if dasher.currentDash%2 == 0 {
+ // line
+ dasher.next.LineTo(x, y)
+ } else {
+ // gap
+ dasher.next.End()
+ dasher.next.MoveTo(x, y)
+ }
+ if dasher.distance >= dasher.dash[dasher.currentDash] {
+ dasher.distance = dasher.distance - dasher.dash[dasher.currentDash]
+ dasher.currentDash = (dasher.currentDash + 1) % len(dasher.dash)
+ }
+ dasher.x, dasher.y = x, y
+}
+
+
+
package drawing
+
+// DemuxFlattener is a flattener
+type DemuxFlattener struct {
+ Flatteners []Flattener
+}
+
+// MoveTo implements the path builder interface.
+func (dc DemuxFlattener) MoveTo(x, y float64) {
+ for _, flattener := range dc.Flatteners {
+ flattener.MoveTo(x, y)
+ }
+}
+
+// LineTo implements the path builder interface.
+func (dc DemuxFlattener) LineTo(x, y float64) {
+ for _, flattener := range dc.Flatteners {
+ flattener.LineTo(x, y)
+ }
+}
+
+// LineJoin implements the path builder interface.
+func (dc DemuxFlattener) LineJoin() {
+ for _, flattener := range dc.Flatteners {
+ flattener.LineJoin()
+ }
+}
+
+// Close implements the path builder interface.
+func (dc DemuxFlattener) Close() {
+ for _, flattener := range dc.Flatteners {
+ flattener.Close()
+ }
+}
+
+// End implements the path builder interface.
+func (dc DemuxFlattener) End() {
+ for _, flattener := range dc.Flatteners {
+ flattener.End()
+ }
+}
+
+
+
package drawing
+
+// Liner receive segment definition
+type Liner interface {
+ // LineTo Draw a line from the current position to the point (x, y)
+ LineTo(x, y float64)
+}
+
+// Flattener receive segment definition
+type Flattener interface {
+ // MoveTo Start a New line from the point (x, y)
+ MoveTo(x, y float64)
+ // LineTo Draw a line from the current position to the point (x, y)
+ LineTo(x, y float64)
+ // LineJoin add the most recent starting point to close the path to create a polygon
+ LineJoin()
+ // Close add the most recent starting point to close the path to create a polygon
+ Close()
+ // End mark the current line as finished so we can draw caps
+ End()
+}
+
+// Flatten convert curves into straight segments keeping join segments info
+func Flatten(path *Path, flattener Flattener, scale float64) {
+ // First Point
+ var startX, startY float64
+ // Current Point
+ var x, y float64
+ var i int
+ for _, cmp := range path.Components {
+ switch cmp {
+ case MoveToComponent:
+ x, y = path.Points[i], path.Points[i+1]
+ startX, startY = x, y
+ if i != 0 {
+ flattener.End()
+ }
+ flattener.MoveTo(x, y)
+ i += 2
+ case LineToComponent:
+ x, y = path.Points[i], path.Points[i+1]
+ flattener.LineTo(x, y)
+ flattener.LineJoin()
+ i += 2
+ case QuadCurveToComponent:
+ // we include the previous point for the start of the curve
+ TraceQuad(flattener, path.Points[i-2:], 0.5)
+ x, y = path.Points[i+2], path.Points[i+3]
+ flattener.LineTo(x, y)
+ i += 4
+ case CubicCurveToComponent:
+ TraceCubic(flattener, path.Points[i-2:], 0.5)
+ x, y = path.Points[i+4], path.Points[i+5]
+ flattener.LineTo(x, y)
+ i += 6
+ case ArcToComponent:
+ x, y = TraceArc(flattener, path.Points[i], path.Points[i+1], path.Points[i+2], path.Points[i+3], path.Points[i+4], path.Points[i+5], scale)
+ flattener.LineTo(x, y)
+ i += 6
+ case CloseComponent:
+ flattener.LineTo(startX, startY)
+ flattener.Close()
+ }
+ }
+ flattener.End()
+}
+
+// SegmentedPath is a path of disparate point sectinos.
+type SegmentedPath struct {
+ Points []float64
+}
+
+// MoveTo implements the path interface.
+func (p *SegmentedPath) MoveTo(x, y float64) {
+ p.Points = append(p.Points, x, y)
+ // TODO need to mark this point as moveto
+}
+
+// LineTo implements the path interface.
+func (p *SegmentedPath) LineTo(x, y float64) {
+ p.Points = append(p.Points, x, y)
+}
+
+// LineJoin implements the path interface.
+func (p *SegmentedPath) LineJoin() {
+ // TODO need to mark the current point as linejoin
+}
+
+// Close implements the path interface.
+func (p *SegmentedPath) Close() {
+ // TODO Close
+}
+
+// End implements the path interface.
+func (p *SegmentedPath) End() {
+ // Nothing to do
+}
+
+
+
package drawing
+
+import (
+ "github.com/golang/freetype/raster"
+ "golang.org/x/image/math/fixed"
+)
+
+// FtLineBuilder is a builder for freetype raster glyphs.
+type FtLineBuilder struct {
+ Adder raster.Adder
+}
+
+// MoveTo implements the path builder interface.
+func (liner FtLineBuilder) MoveTo(x, y float64) {
+ liner.Adder.Start(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
+}
+
+// LineTo implements the path builder interface.
+func (liner FtLineBuilder) LineTo(x, y float64) {
+ liner.Adder.Add1(fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)})
+}
+
+// LineJoin implements the path builder interface.
+func (liner FtLineBuilder) LineJoin() {}
+
+// Close implements the path builder interface.
+func (liner FtLineBuilder) Close() {}
+
+// End implements the path builder interface.
+func (liner FtLineBuilder) End() {}
+
+
+
package drawing
+
+import (
+ "image/color"
+ "image/draw"
+)
+
+// PolylineBresenham draws a polyline to an image
+func PolylineBresenham(img draw.Image, c color.Color, s ...float64) {
+ for i := 2; i < len(s); i += 2 {
+ Bresenham(img, c, int(s[i-2]+0.5), int(s[i-1]+0.5), int(s[i]+0.5), int(s[i+1]+0.5))
+ }
+}
+
+// Bresenham draws a line between (x0, y0) and (x1, y1)
+func Bresenham(img draw.Image, color color.Color, x0, y0, x1, y1 int) {
+ dx := abs(x1 - x0)
+ dy := abs(y1 - y0)
+ var sx, sy int
+ if x0 < x1 {
+ sx = 1
+ } else {
+ sx = -1
+ }
+ if y0 < y1 {
+ sy = 1
+ } else {
+ sy = -1
+ }
+ err := dx - dy
+
+ var e2 int
+ for {
+ img.Set(x0, y0, color)
+ if x0 == x1 && y0 == y1 {
+ return
+ }
+ e2 = 2 * err
+ if e2 > -dy {
+ err = err - dy
+ x0 = x0 + sx
+ }
+ if e2 < dx {
+ err = err + dx
+ y0 = y0 + sy
+ }
+ }
+}
+
+
+
package drawing
+
+import (
+ "math"
+)
+
+// Matrix represents an affine transformation
+type Matrix [6]float64
+
+const (
+ epsilon = 1e-6
+)
+
+// Determinant compute the determinant of the matrix
+func (tr Matrix) Determinant() float64 {
+ return tr[0]*tr[3] - tr[1]*tr[2]
+}
+
+// Transform applies the transformation matrix to points. It modify the points passed in parameter.
+func (tr Matrix) Transform(points []float64) {
+ for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
+ x := points[i]
+ y := points[j]
+ points[i] = x*tr[0] + y*tr[2] + tr[4]
+ points[j] = x*tr[1] + y*tr[3] + tr[5]
+ }
+}
+
+// TransformPoint applies the transformation matrix to point. It returns the point the transformed point.
+func (tr Matrix) TransformPoint(x, y float64) (xres, yres float64) {
+ xres = x*tr[0] + y*tr[2] + tr[4]
+ yres = x*tr[1] + y*tr[3] + tr[5]
+ return xres, yres
+}
+
+func minMax(x, y float64) (min, max float64) {
+ if x > y {
+ return y, x
+ }
+ return x, y
+}
+
+// TransformRectangle applies the transformation matrix to the rectangle represented by the min and the max point of the rectangle
+func (tr Matrix) TransformRectangle(x0, y0, x2, y2 float64) (nx0, ny0, nx2, ny2 float64) {
+ points := []float64{x0, y0, x2, y0, x2, y2, x0, y2}
+ tr.Transform(points)
+ points[0], points[2] = minMax(points[0], points[2])
+ points[4], points[6] = minMax(points[4], points[6])
+ points[1], points[3] = minMax(points[1], points[3])
+ points[5], points[7] = minMax(points[5], points[7])
+
+ nx0 = math.Min(points[0], points[4])
+ ny0 = math.Min(points[1], points[5])
+ nx2 = math.Max(points[2], points[6])
+ ny2 = math.Max(points[3], points[7])
+ return nx0, ny0, nx2, ny2
+}
+
+// InverseTransform applies the transformation inverse matrix to the rectangle represented by the min and the max point of the rectangle
+func (tr Matrix) InverseTransform(points []float64) {
+ d := tr.Determinant() // matrix determinant
+ for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
+ x := points[i]
+ y := points[j]
+ points[i] = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
+ points[j] = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
+ }
+}
+
+// InverseTransformPoint applies the transformation inverse matrix to point. It returns the point the transformed point.
+func (tr Matrix) InverseTransformPoint(x, y float64) (xres, yres float64) {
+ d := tr.Determinant() // matrix determinant
+ xres = ((x-tr[4])*tr[3] - (y-tr[5])*tr[2]) / d
+ yres = ((y-tr[5])*tr[0] - (x-tr[4])*tr[1]) / d
+ return xres, yres
+}
+
+// VectorTransform applies the transformation matrix to points without using the translation parameter of the affine matrix.
+// It modify the points passed in parameter.
+func (tr Matrix) VectorTransform(points []float64) {
+ for i, j := 0, 1; j < len(points); i, j = i+2, j+2 {
+ x := points[i]
+ y := points[j]
+ points[i] = x*tr[0] + y*tr[2]
+ points[j] = x*tr[1] + y*tr[3]
+ }
+}
+
+// NewIdentityMatrix creates an identity transformation matrix.
+func NewIdentityMatrix() Matrix {
+ return Matrix{1, 0, 0, 1, 0, 0}
+}
+
+// NewTranslationMatrix creates a transformation matrix with a translation tx and ty translation parameter
+func NewTranslationMatrix(tx, ty float64) Matrix {
+ return Matrix{1, 0, 0, 1, tx, ty}
+}
+
+// NewScaleMatrix creates a transformation matrix with a sx, sy scale factor
+func NewScaleMatrix(sx, sy float64) Matrix {
+ return Matrix{sx, 0, 0, sy, 0, 0}
+}
+
+// NewRotationMatrix creates a rotation transformation matrix. angle is in radian
+func NewRotationMatrix(angle float64) Matrix {
+ c := math.Cos(angle)
+ s := math.Sin(angle)
+ return Matrix{c, s, -s, c, 0, 0}
+}
+
+// NewMatrixFromRects creates a transformation matrix, combining a scale and a translation, that transform rectangle1 into rectangle2.
+func NewMatrixFromRects(rectangle1, rectangle2 [4]float64) Matrix {
+ xScale := (rectangle2[2] - rectangle2[0]) / (rectangle1[2] - rectangle1[0])
+ yScale := (rectangle2[3] - rectangle2[1]) / (rectangle1[3] - rectangle1[1])
+ xOffset := rectangle2[0] - (rectangle1[0] * xScale)
+ yOffset := rectangle2[1] - (rectangle1[1] * yScale)
+ return Matrix{xScale, 0, 0, yScale, xOffset, yOffset}
+}
+
+// Inverse computes the inverse matrix
+func (tr *Matrix) Inverse() {
+ d := tr.Determinant() // matrix determinant
+ tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
+ tr[0] = tr3 / d
+ tr[1] = -tr1 / d
+ tr[2] = -tr2 / d
+ tr[3] = tr0 / d
+ tr[4] = (tr2*tr5 - tr3*tr4) / d
+ tr[5] = (tr1*tr4 - tr0*tr5) / d
+}
+
+// Copy copies the matrix.
+func (tr Matrix) Copy() Matrix {
+ var result Matrix
+ copy(result[:], tr[:])
+ return result
+}
+
+// Compose multiplies trToConcat x tr
+func (tr *Matrix) Compose(trToCompose Matrix) {
+ tr0, tr1, tr2, tr3, tr4, tr5 := tr[0], tr[1], tr[2], tr[3], tr[4], tr[5]
+ tr[0] = trToCompose[0]*tr0 + trToCompose[1]*tr2
+ tr[1] = trToCompose[1]*tr3 + trToCompose[0]*tr1
+ tr[2] = trToCompose[2]*tr0 + trToCompose[3]*tr2
+ tr[3] = trToCompose[3]*tr3 + trToCompose[2]*tr1
+ tr[4] = trToCompose[4]*tr0 + trToCompose[5]*tr2 + tr4
+ tr[5] = trToCompose[5]*tr3 + trToCompose[4]*tr1 + tr5
+}
+
+// Scale adds a scale to the matrix
+func (tr *Matrix) Scale(sx, sy float64) {
+ tr[0] = sx * tr[0]
+ tr[1] = sx * tr[1]
+ tr[2] = sy * tr[2]
+ tr[3] = sy * tr[3]
+}
+
+// Translate adds a translation to the matrix
+func (tr *Matrix) Translate(tx, ty float64) {
+ tr[4] = tx*tr[0] + ty*tr[2] + tr[4]
+ tr[5] = ty*tr[3] + tx*tr[1] + tr[5]
+}
+
+// Rotate adds a rotation to the matrix.
+func (tr *Matrix) Rotate(radians float64) {
+ c := math.Cos(radians)
+ s := math.Sin(radians)
+ t0 := c*tr[0] + s*tr[2]
+ t1 := s*tr[3] + c*tr[1]
+ t2 := c*tr[2] - s*tr[0]
+ t3 := c*tr[3] - s*tr[1]
+ tr[0] = t0
+ tr[1] = t1
+ tr[2] = t2
+ tr[3] = t3
+}
+
+// GetTranslation gets the matrix traslation.
+func (tr Matrix) GetTranslation() (x, y float64) {
+ return tr[4], tr[5]
+}
+
+// GetScaling gets the matrix scaling.
+func (tr Matrix) GetScaling() (x, y float64) {
+ return tr[0], tr[3]
+}
+
+// GetScale computes a scale for the matrix
+func (tr Matrix) GetScale() float64 {
+ x := 0.707106781*tr[0] + 0.707106781*tr[1]
+ y := 0.707106781*tr[2] + 0.707106781*tr[3]
+ return math.Sqrt(x*x + y*y)
+}
+
+// ******************** Testing ********************
+
+// Equals tests if a two transformation are equal. A tolerance is applied when comparing matrix elements.
+func (tr Matrix) Equals(tr2 Matrix) bool {
+ for i := 0; i < 6; i = i + 1 {
+ if !fequals(tr[i], tr2[i]) {
+ return false
+ }
+ }
+ return true
+}
+
+// IsIdentity tests if a transformation is the identity transformation. A tolerance is applied when comparing matrix elements.
+func (tr Matrix) IsIdentity() bool {
+ return fequals(tr[4], 0) && fequals(tr[5], 0) && tr.IsTranslation()
+}
+
+// IsTranslation tests if a transformation is is a pure translation. A tolerance is applied when comparing matrix elements.
+func (tr Matrix) IsTranslation() bool {
+ return fequals(tr[0], 1) && fequals(tr[1], 0) && fequals(tr[2], 0) && fequals(tr[3], 1)
+}
+
+// fequals compares two floats. return true if the distance between the two floats is less than epsilon, false otherwise
+func fequals(float1, float2 float64) bool {
+ return math.Abs(float1-float2) <= epsilon
+}
+
+
+
package drawing
+
+import (
+ "image"
+ "image/color"
+
+ "golang.org/x/image/draw"
+ "golang.org/x/image/math/f64"
+
+ "github.com/golang/freetype/raster"
+)
+
+// Painter implements the freetype raster.Painter and has a SetColor method like the RGBAPainter
+type Painter interface {
+ raster.Painter
+ SetColor(color color.Color)
+}
+
+// DrawImage draws an image into dest using an affine transformation matrix, an op and a filter
+func DrawImage(src image.Image, dest draw.Image, tr Matrix, op draw.Op, filter ImageFilter) {
+ var transformer draw.Transformer
+ switch filter {
+ case LinearFilter:
+ transformer = draw.NearestNeighbor
+ case BilinearFilter:
+ transformer = draw.BiLinear
+ case BicubicFilter:
+ transformer = draw.CatmullRom
+ }
+ transformer.Transform(dest, f64.Aff3{tr[0], tr[1], tr[4], tr[2], tr[3], tr[5]}, src, src.Bounds(), op, nil)
+}
+
+
+
package drawing
+
+import (
+ "fmt"
+ "math"
+)
+
+// PathBuilder describes the interface for path drawing.
+type PathBuilder interface {
+ // LastPoint returns the current point of the current sub path
+ LastPoint() (x, y float64)
+ // MoveTo creates a new subpath that start at the specified point
+ MoveTo(x, y float64)
+ // LineTo adds a line to the current subpath
+ LineTo(x, y float64)
+ // QuadCurveTo adds a quadratic Bézier curve to the current subpath
+ QuadCurveTo(cx, cy, x, y float64)
+ // CubicCurveTo adds a cubic Bézier curve to the current subpath
+ CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64)
+ // ArcTo adds an arc to the current subpath
+ ArcTo(cx, cy, rx, ry, startAngle, angle float64)
+ // Close creates a line from the current point to the last MoveTo
+ // point (if not the same) and mark the path as closed so the
+ // first and last lines join nicely.
+ Close()
+}
+
+// PathComponent represents component of a path
+type PathComponent int
+
+const (
+ // MoveToComponent is a MoveTo component in a Path
+ MoveToComponent PathComponent = iota
+ // LineToComponent is a LineTo component in a Path
+ LineToComponent
+ // QuadCurveToComponent is a QuadCurveTo component in a Path
+ QuadCurveToComponent
+ // CubicCurveToComponent is a CubicCurveTo component in a Path
+ CubicCurveToComponent
+ // ArcToComponent is a ArcTo component in a Path
+ ArcToComponent
+ // CloseComponent is a ArcTo component in a Path
+ CloseComponent
+)
+
+// Path stores points
+type Path struct {
+ // Components is a slice of PathComponent in a Path and mark the role of each points in the Path
+ Components []PathComponent
+ // Points are combined with Components to have a specific role in the path
+ Points []float64
+ // Last Point of the Path
+ x, y float64
+}
+
+func (p *Path) appendToPath(cmd PathComponent, points ...float64) {
+ p.Components = append(p.Components, cmd)
+ p.Points = append(p.Points, points...)
+}
+
+// LastPoint returns the current point of the current path
+func (p *Path) LastPoint() (x, y float64) {
+ return p.x, p.y
+}
+
+// MoveTo starts a new path at (x, y) position
+func (p *Path) MoveTo(x, y float64) {
+ p.appendToPath(MoveToComponent, x, y)
+ p.x = x
+ p.y = y
+}
+
+// LineTo adds a line to the current path
+func (p *Path) LineTo(x, y float64) {
+ if len(p.Components) == 0 { //special case when no move has been done
+ p.MoveTo(0, 0)
+ }
+ p.appendToPath(LineToComponent, x, y)
+ p.x = x
+ p.y = y
+}
+
+// QuadCurveTo adds a quadratic bezier curve to the current path
+func (p *Path) QuadCurveTo(cx, cy, x, y float64) {
+ if len(p.Components) == 0 { //special case when no move has been done
+ p.MoveTo(0, 0)
+ }
+ p.appendToPath(QuadCurveToComponent, cx, cy, x, y)
+ p.x = x
+ p.y = y
+}
+
+// CubicCurveTo adds a cubic bezier curve to the current path
+func (p *Path) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
+ if len(p.Components) == 0 { //special case when no move has been done
+ p.MoveTo(0, 0)
+ }
+ p.appendToPath(CubicCurveToComponent, cx1, cy1, cx2, cy2, x, y)
+ p.x = x
+ p.y = y
+}
+
+// ArcTo adds an arc to the path
+func (p *Path) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
+ endAngle := startAngle + delta
+ clockWise := true
+ if delta < 0 {
+ clockWise = false
+ }
+ // normalize
+ if clockWise {
+ for endAngle < startAngle {
+ endAngle += math.Pi * 2.0
+ }
+ } else {
+ for startAngle < endAngle {
+ startAngle += math.Pi * 2.0
+ }
+ }
+ startX := cx + math.Cos(startAngle)*rx
+ startY := cy + math.Sin(startAngle)*ry
+ if len(p.Components) > 0 {
+ p.LineTo(startX, startY)
+ } else {
+ p.MoveTo(startX, startY)
+ }
+ p.appendToPath(ArcToComponent, cx, cy, rx, ry, startAngle, delta)
+ p.x = cx + math.Cos(endAngle)*rx
+ p.y = cy + math.Sin(endAngle)*ry
+}
+
+// Close closes the current path
+func (p *Path) Close() {
+ p.appendToPath(CloseComponent)
+}
+
+// Copy make a clone of the current path and return it
+func (p *Path) Copy() (dest *Path) {
+ dest = new(Path)
+ dest.Components = make([]PathComponent, len(p.Components))
+ copy(dest.Components, p.Components)
+ dest.Points = make([]float64, len(p.Points))
+ copy(dest.Points, p.Points)
+ dest.x, dest.y = p.x, p.y
+ return dest
+}
+
+// Clear reset the path
+func (p *Path) Clear() {
+ p.Components = p.Components[0:0]
+ p.Points = p.Points[0:0]
+ return
+}
+
+// IsEmpty returns true if the path is empty
+func (p *Path) IsEmpty() bool {
+ return len(p.Components) == 0
+}
+
+// String returns a debug text view of the path
+func (p *Path) String() string {
+ s := ""
+ j := 0
+ for _, cmd := range p.Components {
+ switch cmd {
+ case MoveToComponent:
+ s += fmt.Sprintf("MoveTo: %f, %f\n", p.Points[j], p.Points[j+1])
+ j = j + 2
+ case LineToComponent:
+ s += fmt.Sprintf("LineTo: %f, %f\n", p.Points[j], p.Points[j+1])
+ j = j + 2
+ case QuadCurveToComponent:
+ s += fmt.Sprintf("QuadCurveTo: %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3])
+ j = j + 4
+ case CubicCurveToComponent:
+ s += fmt.Sprintf("CubicCurveTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
+ j = j + 6
+ case ArcToComponent:
+ s += fmt.Sprintf("ArcTo: %f, %f, %f, %f, %f, %f\n", p.Points[j], p.Points[j+1], p.Points[j+2], p.Points[j+3], p.Points[j+4], p.Points[j+5])
+ j = j + 6
+ case CloseComponent:
+ s += "Close\n"
+ }
+ }
+ return s
+}
+
+
+
package drawing
+
+import (
+ "errors"
+ "image"
+ "image/color"
+ "math"
+
+ "github.com/golang/freetype/raster"
+ "github.com/golang/freetype/truetype"
+ "golang.org/x/image/draw"
+ "golang.org/x/image/font"
+ "golang.org/x/image/math/fixed"
+)
+
+// NewRasterGraphicContext creates a new Graphic context from an image.
+func NewRasterGraphicContext(img draw.Image) (*RasterGraphicContext, error) {
+ var painter Painter
+ switch selectImage := img.(type) {
+ case *image.RGBA:
+ painter = raster.NewRGBAPainter(selectImage)
+ default:
+ return nil, errors.New("NewRasterGraphicContext() :: invalid image type")
+ }
+ return NewRasterGraphicContextWithPainter(img, painter), nil
+}
+
+// NewRasterGraphicContextWithPainter creates a new Graphic context from an image and a Painter (see Freetype-go)
+func NewRasterGraphicContextWithPainter(img draw.Image, painter Painter) *RasterGraphicContext {
+ width, height := img.Bounds().Dx(), img.Bounds().Dy()
+ return &RasterGraphicContext{
+ NewStackGraphicContext(),
+ img,
+ painter,
+ raster.NewRasterizer(width, height),
+ raster.NewRasterizer(width, height),
+ &truetype.GlyphBuf{},
+ DefaultDPI,
+ }
+}
+
+// RasterGraphicContext is the implementation of GraphicContext for a raster image
+type RasterGraphicContext struct {
+ *StackGraphicContext
+ img draw.Image
+ painter Painter
+ fillRasterizer *raster.Rasterizer
+ strokeRasterizer *raster.Rasterizer
+ glyphBuf *truetype.GlyphBuf
+ DPI float64
+}
+
+// SetDPI sets the screen resolution in dots per inch.
+func (rgc *RasterGraphicContext) SetDPI(dpi float64) {
+ rgc.DPI = dpi
+ rgc.recalc()
+}
+
+// GetDPI returns the resolution of the Image GraphicContext
+func (rgc *RasterGraphicContext) GetDPI() float64 {
+ return rgc.DPI
+}
+
+// Clear fills the current canvas with a default transparent color
+func (rgc *RasterGraphicContext) Clear() {
+ width, height := rgc.img.Bounds().Dx(), rgc.img.Bounds().Dy()
+ rgc.ClearRect(0, 0, width, height)
+}
+
+// ClearRect fills the current canvas with a default transparent color at the specified rectangle
+func (rgc *RasterGraphicContext) ClearRect(x1, y1, x2, y2 int) {
+ imageColor := image.NewUniform(rgc.current.FillColor)
+ draw.Draw(rgc.img, image.Rect(x1, y1, x2, y2), imageColor, image.ZP, draw.Over)
+}
+
+// DrawImage draws the raster image in the current canvas
+func (rgc *RasterGraphicContext) DrawImage(img image.Image) {
+ DrawImage(img, rgc.img, rgc.current.Tr, draw.Over, BilinearFilter)
+}
+
+// FillString draws the text at point (0, 0)
+func (rgc *RasterGraphicContext) FillString(text string) (cursor float64, err error) {
+ cursor, err = rgc.FillStringAt(text, 0, 0)
+ return
+}
+
+// FillStringAt draws the text at the specified point (x, y)
+func (rgc *RasterGraphicContext) FillStringAt(text string, x, y float64) (cursor float64, err error) {
+ cursor, err = rgc.CreateStringPath(text, x, y)
+ rgc.Fill()
+ return
+}
+
+// StrokeString draws the contour of the text at point (0, 0)
+func (rgc *RasterGraphicContext) StrokeString(text string) (cursor float64, err error) {
+ cursor, err = rgc.StrokeStringAt(text, 0, 0)
+ return
+}
+
+// StrokeStringAt draws the contour of the text at point (x, y)
+func (rgc *RasterGraphicContext) StrokeStringAt(text string, x, y float64) (cursor float64, err error) {
+ cursor, err = rgc.CreateStringPath(text, x, y)
+ rgc.Stroke()
+ return
+}
+
+func (rgc *RasterGraphicContext) drawGlyph(glyph truetype.Index, dx, dy float64) error {
+ if err := rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), glyph, font.HintingNone); err != nil {
+ return err
+ }
+ e0 := 0
+ for _, e1 := range rgc.glyphBuf.Ends {
+ DrawContour(rgc, rgc.glyphBuf.Points[e0:e1], dx, dy)
+ e0 = e1
+ }
+ return nil
+}
+
+// CreateStringPath creates a path from the string s at x, y, and returns the string width.
+// The text is placed so that the left edge of the em square of the first character of s
+// and the baseline intersect at x, y. The majority of the affected pixels will be
+// above and to the right of the point, but some may be below or to the left.
+// For example, drawing a string that starts with a 'J' in an italic font may
+// affect pixels below and left of the point.
+func (rgc *RasterGraphicContext) CreateStringPath(s string, x, y float64) (cursor float64, err error) {
+ f := rgc.GetFont()
+ if f == nil {
+ err = errors.New("No font loaded, cannot continue")
+ return
+ }
+ rgc.recalc()
+
+ startx := x
+ prev, hasPrev := truetype.Index(0), false
+ for _, rc := range s {
+ index := f.Index(rc)
+ if hasPrev {
+ x += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
+ }
+ err = rgc.drawGlyph(index, x, y)
+ if err != nil {
+ cursor = x - startx
+ return
+ }
+ x += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
+ prev, hasPrev = index, true
+ }
+ cursor = x - startx
+ return
+}
+
+// GetStringBounds returns the approximate pixel bounds of a string.
+func (rgc *RasterGraphicContext) GetStringBounds(s string) (left, top, right, bottom float64, err error) {
+ f := rgc.GetFont()
+ if f == nil {
+ err = errors.New("No font loaded, cannot continue")
+ return
+ }
+ rgc.recalc()
+
+ left = math.MaxFloat64
+ top = math.MaxFloat64
+
+ cursor := 0.0
+ prev, hasPrev := truetype.Index(0), false
+ for _, rc := range s {
+ index := f.Index(rc)
+ if hasPrev {
+ cursor += fUnitsToFloat64(f.Kern(fixed.Int26_6(rgc.current.Scale), prev, index))
+ }
+
+ if err = rgc.glyphBuf.Load(rgc.current.Font, fixed.Int26_6(rgc.current.Scale), index, font.HintingNone); err != nil {
+ return
+ }
+ e0 := 0
+ for _, e1 := range rgc.glyphBuf.Ends {
+ ps := rgc.glyphBuf.Points[e0:e1]
+ for _, p := range ps {
+ x, y := pointToF64Point(p)
+ top = math.Min(top, y)
+ bottom = math.Max(bottom, y)
+ left = math.Min(left, x+cursor)
+ right = math.Max(right, x+cursor)
+ }
+ e0 = e1
+ }
+ cursor += fUnitsToFloat64(f.HMetric(fixed.Int26_6(rgc.current.Scale), index).AdvanceWidth)
+ prev, hasPrev = index, true
+ }
+ return
+}
+
+// recalc recalculates scale and bounds values from the font size, screen
+// resolution and font metrics, and invalidates the glyph cache.
+func (rgc *RasterGraphicContext) recalc() {
+ rgc.current.Scale = rgc.current.FontSizePoints * float64(rgc.DPI)
+}
+
+// SetFont sets the font used to draw text.
+func (rgc *RasterGraphicContext) SetFont(font *truetype.Font) {
+ rgc.current.Font = font
+}
+
+// GetFont returns the font used to draw text.
+func (rgc *RasterGraphicContext) GetFont() *truetype.Font {
+ return rgc.current.Font
+}
+
+// SetFontSize sets the font size in points (as in ``a 12 point font'').
+func (rgc *RasterGraphicContext) SetFontSize(fontSizePoints float64) {
+ rgc.current.FontSizePoints = fontSizePoints
+ rgc.recalc()
+}
+
+func (rgc *RasterGraphicContext) paint(rasterizer *raster.Rasterizer, color color.Color) {
+ rgc.painter.SetColor(color)
+ rasterizer.Rasterize(rgc.painter)
+ rasterizer.Clear()
+ rgc.current.Path.Clear()
+}
+
+// Stroke strokes the paths with the color specified by SetStrokeColor
+func (rgc *RasterGraphicContext) Stroke(paths ...*Path) {
+ paths = append(paths, rgc.current.Path)
+ rgc.strokeRasterizer.UseNonZeroWinding = true
+
+ stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
+ stroker.HalfLineWidth = rgc.current.LineWidth / 2
+
+ var liner Flattener
+ if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
+ liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
+ } else {
+ liner = stroker
+ }
+ for _, p := range paths {
+ Flatten(p, liner, rgc.current.Tr.GetScale())
+ }
+
+ rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
+}
+
+// Fill fills the paths with the color specified by SetFillColor
+func (rgc *RasterGraphicContext) Fill(paths ...*Path) {
+ paths = append(paths, rgc.current.Path)
+ rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
+
+ flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
+ for _, p := range paths {
+ Flatten(p, flattener, rgc.current.Tr.GetScale())
+ }
+
+ rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
+}
+
+// FillStroke first fills the paths and than strokes them
+func (rgc *RasterGraphicContext) FillStroke(paths ...*Path) {
+ paths = append(paths, rgc.current.Path)
+ rgc.fillRasterizer.UseNonZeroWinding = rgc.current.FillRule == FillRuleWinding
+ rgc.strokeRasterizer.UseNonZeroWinding = true
+
+ flattener := Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.fillRasterizer}}
+
+ stroker := NewLineStroker(rgc.current.Cap, rgc.current.Join, Transformer{Tr: rgc.current.Tr, Flattener: FtLineBuilder{Adder: rgc.strokeRasterizer}})
+ stroker.HalfLineWidth = rgc.current.LineWidth / 2
+
+ var liner Flattener
+ if rgc.current.Dash != nil && len(rgc.current.Dash) > 0 {
+ liner = NewDashVertexConverter(rgc.current.Dash, rgc.current.DashOffset, stroker)
+ } else {
+ liner = stroker
+ }
+
+ demux := DemuxFlattener{Flatteners: []Flattener{flattener, liner}}
+ for _, p := range paths {
+ Flatten(p, demux, rgc.current.Tr.GetScale())
+ }
+
+ // Fill
+ rgc.paint(rgc.fillRasterizer, rgc.current.FillColor)
+ // Stroke
+ rgc.paint(rgc.strokeRasterizer, rgc.current.StrokeColor)
+}
+
+
+
package drawing
+
+import (
+ "image"
+ "image/color"
+
+ "github.com/golang/freetype/truetype"
+)
+
+// StackGraphicContext is a context that does thngs.
+type StackGraphicContext struct {
+ current *ContextStack
+}
+
+// ContextStack is a graphic context implementation.
+type ContextStack struct {
+ Tr Matrix
+ Path *Path
+ LineWidth float64
+ Dash []float64
+ DashOffset float64
+ StrokeColor color.Color
+ FillColor color.Color
+ FillRule FillRule
+ Cap LineCap
+ Join LineJoin
+
+ FontSizePoints float64
+ Font *truetype.Font
+
+ Scale float64
+
+ Previous *ContextStack
+}
+
+// NewStackGraphicContext Create a new Graphic context from an image
+func NewStackGraphicContext() *StackGraphicContext {
+ gc := &StackGraphicContext{}
+ gc.current = new(ContextStack)
+ gc.current.Tr = NewIdentityMatrix()
+ gc.current.Path = new(Path)
+ gc.current.LineWidth = 1.0
+ gc.current.StrokeColor = image.Black
+ gc.current.FillColor = image.White
+ gc.current.Cap = RoundCap
+ gc.current.FillRule = FillRuleEvenOdd
+ gc.current.Join = RoundJoin
+ gc.current.FontSizePoints = 10
+ return gc
+}
+
+// GetMatrixTransform returns the matrix transform.
+func (gc *StackGraphicContext) GetMatrixTransform() Matrix {
+ return gc.current.Tr
+}
+
+// SetMatrixTransform sets the matrix transform.
+func (gc *StackGraphicContext) SetMatrixTransform(tr Matrix) {
+ gc.current.Tr = tr
+}
+
+// ComposeMatrixTransform composes a transform into the current transform.
+func (gc *StackGraphicContext) ComposeMatrixTransform(tr Matrix) {
+ gc.current.Tr.Compose(tr)
+}
+
+// Rotate rotates the matrix transform by an angle in degrees.
+func (gc *StackGraphicContext) Rotate(angle float64) {
+ gc.current.Tr.Rotate(angle)
+}
+
+// Translate translates a transform.
+func (gc *StackGraphicContext) Translate(tx, ty float64) {
+ gc.current.Tr.Translate(tx, ty)
+}
+
+// Scale scales a transform.
+func (gc *StackGraphicContext) Scale(sx, sy float64) {
+ gc.current.Tr.Scale(sx, sy)
+}
+
+// SetStrokeColor sets the stroke color.
+func (gc *StackGraphicContext) SetStrokeColor(c color.Color) {
+ gc.current.StrokeColor = c
+}
+
+// SetFillColor sets the fill color.
+func (gc *StackGraphicContext) SetFillColor(c color.Color) {
+ gc.current.FillColor = c
+}
+
+// SetFillRule sets the fill rule.
+func (gc *StackGraphicContext) SetFillRule(f FillRule) {
+ gc.current.FillRule = f
+}
+
+// SetLineWidth sets the line width.
+func (gc *StackGraphicContext) SetLineWidth(lineWidth float64) {
+ gc.current.LineWidth = lineWidth
+}
+
+// SetLineCap sets the line cap.
+func (gc *StackGraphicContext) SetLineCap(cap LineCap) {
+ gc.current.Cap = cap
+}
+
+// SetLineJoin sets the line join.
+func (gc *StackGraphicContext) SetLineJoin(join LineJoin) {
+ gc.current.Join = join
+}
+
+// SetLineDash sets the line dash.
+func (gc *StackGraphicContext) SetLineDash(dash []float64, dashOffset float64) {
+ gc.current.Dash = dash
+ gc.current.DashOffset = dashOffset
+}
+
+// SetFontSize sets the font size.
+func (gc *StackGraphicContext) SetFontSize(fontSizePoints float64) {
+ gc.current.FontSizePoints = fontSizePoints
+}
+
+// GetFontSize gets the font size.
+func (gc *StackGraphicContext) GetFontSize() float64 {
+ return gc.current.FontSizePoints
+}
+
+// SetFont sets the current font.
+func (gc *StackGraphicContext) SetFont(f *truetype.Font) {
+ gc.current.Font = f
+}
+
+// GetFont returns the font.
+func (gc *StackGraphicContext) GetFont() *truetype.Font {
+ return gc.current.Font
+}
+
+// BeginPath starts a new path.
+func (gc *StackGraphicContext) BeginPath() {
+ gc.current.Path.Clear()
+}
+
+// IsEmpty returns if the path is empty.
+func (gc *StackGraphicContext) IsEmpty() bool {
+ return gc.current.Path.IsEmpty()
+}
+
+// LastPoint returns the last point on the path.
+func (gc *StackGraphicContext) LastPoint() (x float64, y float64) {
+ return gc.current.Path.LastPoint()
+}
+
+// MoveTo moves the cursor for a path.
+func (gc *StackGraphicContext) MoveTo(x, y float64) {
+ gc.current.Path.MoveTo(x, y)
+}
+
+// LineTo draws a line.
+func (gc *StackGraphicContext) LineTo(x, y float64) {
+ gc.current.Path.LineTo(x, y)
+}
+
+// QuadCurveTo draws a quad curve.
+func (gc *StackGraphicContext) QuadCurveTo(cx, cy, x, y float64) {
+ gc.current.Path.QuadCurveTo(cx, cy, x, y)
+}
+
+// CubicCurveTo draws a cubic curve.
+func (gc *StackGraphicContext) CubicCurveTo(cx1, cy1, cx2, cy2, x, y float64) {
+ gc.current.Path.CubicCurveTo(cx1, cy1, cx2, cy2, x, y)
+}
+
+// ArcTo draws an arc.
+func (gc *StackGraphicContext) ArcTo(cx, cy, rx, ry, startAngle, delta float64) {
+ gc.current.Path.ArcTo(cx, cy, rx, ry, startAngle, delta)
+}
+
+// Close closes a path.
+func (gc *StackGraphicContext) Close() {
+ gc.current.Path.Close()
+}
+
+// Save pushes a context onto the stack.
+func (gc *StackGraphicContext) Save() {
+ context := new(ContextStack)
+ context.FontSizePoints = gc.current.FontSizePoints
+ context.Font = gc.current.Font
+ context.LineWidth = gc.current.LineWidth
+ context.StrokeColor = gc.current.StrokeColor
+ context.FillColor = gc.current.FillColor
+ context.FillRule = gc.current.FillRule
+ context.Dash = gc.current.Dash
+ context.DashOffset = gc.current.DashOffset
+ context.Cap = gc.current.Cap
+ context.Join = gc.current.Join
+ context.Path = gc.current.Path.Copy()
+ context.Font = gc.current.Font
+ context.Scale = gc.current.Scale
+ copy(context.Tr[:], gc.current.Tr[:])
+ context.Previous = gc.current
+ gc.current = context
+}
+
+// Restore restores the previous context.
+func (gc *StackGraphicContext) Restore() {
+ if gc.current.Previous != nil {
+ oldContext := gc.current
+ gc.current = gc.current.Previous
+ oldContext.Previous = nil
+ }
+}
+
+
+
// Copyright 2010 The draw2d Authors. All rights reserved.
+// created: 13/12/2010 by Laurent Le Goff
+
+package drawing
+
+// NewLineStroker creates a new line stroker.
+func NewLineStroker(c LineCap, j LineJoin, flattener Flattener) *LineStroker {
+ l := new(LineStroker)
+ l.Flattener = flattener
+ l.HalfLineWidth = 0.5
+ l.Cap = c
+ l.Join = j
+ return l
+}
+
+// LineStroker draws the stroke portion of a line.
+type LineStroker struct {
+ Flattener Flattener
+ HalfLineWidth float64
+ Cap LineCap
+ Join LineJoin
+ vertices []float64
+ rewind []float64
+ x, y, nx, ny float64
+}
+
+// MoveTo implements the path builder interface.
+func (l *LineStroker) MoveTo(x, y float64) {
+ l.x, l.y = x, y
+}
+
+// LineTo implements the path builder interface.
+func (l *LineStroker) LineTo(x, y float64) {
+ l.line(l.x, l.y, x, y)
+}
+
+// LineJoin implements the path builder interface.
+func (l *LineStroker) LineJoin() {}
+
+func (l *LineStroker) line(x1, y1, x2, y2 float64) {
+ dx := (x2 - x1)
+ dy := (y2 - y1)
+ d := vectorDistance(dx, dy)
+ if d != 0 {
+ nx := dy * l.HalfLineWidth / d
+ ny := -(dx * l.HalfLineWidth / d)
+ l.appendVertex(x1+nx, y1+ny, x2+nx, y2+ny, x1-nx, y1-ny, x2-nx, y2-ny)
+ l.x, l.y, l.nx, l.ny = x2, y2, nx, ny
+ }
+}
+
+// Close implements the path builder interface.
+func (l *LineStroker) Close() {
+ if len(l.vertices) > 1 {
+ l.appendVertex(l.vertices[0], l.vertices[1], l.rewind[0], l.rewind[1])
+ }
+}
+
+// End implements the path builder interface.
+func (l *LineStroker) End() {
+ if len(l.vertices) > 1 {
+ l.Flattener.MoveTo(l.vertices[0], l.vertices[1])
+ for i, j := 2, 3; j < len(l.vertices); i, j = i+2, j+2 {
+ l.Flattener.LineTo(l.vertices[i], l.vertices[j])
+ }
+ }
+ for i, j := len(l.rewind)-2, len(l.rewind)-1; j > 0; i, j = i-2, j-2 {
+ l.Flattener.LineTo(l.rewind[i], l.rewind[j])
+ }
+ if len(l.vertices) > 1 {
+ l.Flattener.LineTo(l.vertices[0], l.vertices[1])
+ }
+ l.Flattener.End()
+ // reinit vertices
+ l.vertices = l.vertices[0:0]
+ l.rewind = l.rewind[0:0]
+ l.x, l.y, l.nx, l.ny = 0, 0, 0, 0
+
+}
+
+func (l *LineStroker) appendVertex(vertices ...float64) {
+ s := len(vertices) / 2
+ l.vertices = append(l.vertices, vertices[:s]...)
+ l.rewind = append(l.rewind, vertices[s:]...)
+}
+
+
+
package drawing
+
+import (
+ "github.com/golang/freetype/truetype"
+ "golang.org/x/image/math/fixed"
+)
+
+// DrawContour draws the given closed contour at the given sub-pixel offset.
+func DrawContour(path PathBuilder, ps []truetype.Point, dx, dy float64) {
+ if len(ps) == 0 {
+ return
+ }
+ startX, startY := pointToF64Point(ps[0])
+ path.MoveTo(startX+dx, startY+dy)
+ q0X, q0Y, on0 := startX, startY, true
+ for _, p := range ps[1:] {
+ qX, qY := pointToF64Point(p)
+ on := p.Flags&0x01 != 0
+ if on {
+ if on0 {
+ path.LineTo(qX+dx, qY+dy)
+ } else {
+ path.QuadCurveTo(q0X+dx, q0Y+dy, qX+dx, qY+dy)
+ }
+ } else if !on0 {
+ midX := (q0X + qX) / 2
+ midY := (q0Y + qY) / 2
+ path.QuadCurveTo(q0X+dx, q0Y+dy, midX+dx, midY+dy)
+ }
+ q0X, q0Y, on0 = qX, qY, on
+ }
+ // Close the curve.
+ if on0 {
+ path.LineTo(startX+dx, startY+dy)
+ } else {
+ path.QuadCurveTo(q0X+dx, q0Y+dy, startX+dx, startY+dy)
+ }
+}
+
+// FontExtents contains font metric information.
+type FontExtents struct {
+ // Ascent is the distance that the text
+ // extends above the baseline.
+ Ascent float64
+
+ // Descent is the distance that the text
+ // extends below the baseline. The descent
+ // is given as a negative value.
+ Descent float64
+
+ // Height is the distance from the lowest
+ // descending point to the highest ascending
+ // point.
+ Height float64
+}
+
+// Extents returns the FontExtents for a font.
+// TODO needs to read this https://developer.apple.com/fonts/TrueType-Reference-Manual/RM02/Chap2.html#intro
+func Extents(font *truetype.Font, size float64) FontExtents {
+ bounds := font.Bounds(fixed.Int26_6(font.FUnitsPerEm()))
+ scale := size / float64(font.FUnitsPerEm())
+ return FontExtents{
+ Ascent: float64(bounds.Max.Y) * scale,
+ Descent: float64(bounds.Min.Y) * scale,
+ Height: float64(bounds.Max.Y-bounds.Min.Y) * scale,
+ }
+}
+
+
+
// Copyright 2010 The draw2d Authors. All rights reserved.
+// created: 13/12/2010 by Laurent Le Goff
+
+package drawing
+
+// Transformer apply the Matrix transformation tr
+type Transformer struct {
+ Tr Matrix
+ Flattener Flattener
+}
+
+// MoveTo implements the path builder interface.
+func (t Transformer) MoveTo(x, y float64) {
+ u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
+ v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
+ t.Flattener.MoveTo(u, v)
+}
+
+// LineTo implements the path builder interface.
+func (t Transformer) LineTo(x, y float64) {
+ u := x*t.Tr[0] + y*t.Tr[2] + t.Tr[4]
+ v := x*t.Tr[1] + y*t.Tr[3] + t.Tr[5]
+ t.Flattener.LineTo(u, v)
+}
+
+// LineJoin implements the path builder interface.
+func (t Transformer) LineJoin() {
+ t.Flattener.LineJoin()
+}
+
+// Close implements the path builder interface.
+func (t Transformer) Close() {
+ t.Flattener.Close()
+}
+
+// End implements the path builder interface.
+func (t Transformer) End() {
+ t.Flattener.End()
+}
+
+
+
package drawing
+
+import (
+ "math"
+
+ "golang.org/x/image/math/fixed"
+
+ "github.com/golang/freetype/raster"
+ "github.com/golang/freetype/truetype"
+)
+
+// PixelsToPoints returns the points for a given number of pixels at a DPI.
+func PixelsToPoints(dpi, pixels float64) (points float64) {
+ points = (pixels * 72.0) / dpi
+ return
+}
+
+// PointsToPixels returns the pixels for a given number of points at a DPI.
+func PointsToPixels(dpi, points float64) (pixels float64) {
+ pixels = (points * dpi) / 72.0
+ return
+}
+
+func abs(i int) int {
+ if i < 0 {
+ return -i
+ }
+ return i
+}
+
+func distance(x1, y1, x2, y2 float64) float64 {
+ return vectorDistance(x2-x1, y2-y1)
+}
+
+func vectorDistance(dx, dy float64) float64 {
+ return float64(math.Sqrt(dx*dx + dy*dy))
+}
+
+func toFtCap(c LineCap) raster.Capper {
+ switch c {
+ case RoundCap:
+ return raster.RoundCapper
+ case ButtCap:
+ return raster.ButtCapper
+ case SquareCap:
+ return raster.SquareCapper
+ }
+ return raster.RoundCapper
+}
+
+func toFtJoin(j LineJoin) raster.Joiner {
+ switch j {
+ case RoundJoin:
+ return raster.RoundJoiner
+ case BevelJoin:
+ return raster.BevelJoiner
+ }
+ return raster.RoundJoiner
+}
+
+func pointToF64Point(p truetype.Point) (x, y float64) {
+ return fUnitsToFloat64(p.X), -fUnitsToFloat64(p.Y)
+}
+
+func fUnitsToFloat64(x fixed.Int26_6) float64 {
+ scaled := x << 2
+ return float64(scaled/256) + float64(scaled%256)/256.0
+}
+
+
+
package chart
+
+import "fmt"
+
+const (
+ // DefaultEMAPeriod is the default EMA period used in the sigma calculation.
+ DefaultEMAPeriod = 12
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*EMASeries)(nil)
+ _ FirstValuesProvider = (*EMASeries)(nil)
+ _ LastValuesProvider = (*EMASeries)(nil)
+)
+
+// EMASeries is a computed series.
+type EMASeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+
+ Period int
+ InnerSeries ValuesProvider
+
+ cache []float64
+}
+
+// GetName returns the name of the time series.
+func (ema EMASeries) GetName() string {
+ return ema.Name
+}
+
+// GetStyle returns the line style.
+func (ema EMASeries) GetStyle() Style {
+ return ema.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (ema EMASeries) GetYAxis() YAxisType {
+ return ema.YAxis
+}
+
+// GetPeriod returns the window size.
+func (ema EMASeries) GetPeriod() int {
+ if ema.Period == 0 {
+ return DefaultEMAPeriod
+ }
+ return ema.Period
+}
+
+// Len returns the number of elements in the series.
+func (ema EMASeries) Len() int {
+ return ema.InnerSeries.Len()
+}
+
+// GetSigma returns the smoothing factor for the serise.
+func (ema EMASeries) GetSigma() float64 {
+ return 2.0 / (float64(ema.GetPeriod()) + 1)
+}
+
+// GetValues gets a value at a given index.
+func (ema *EMASeries) GetValues(index int) (x, y float64) {
+ if ema.InnerSeries == nil {
+ return
+ }
+ if len(ema.cache) == 0 {
+ ema.ensureCachedValues()
+ }
+ vx, _ := ema.InnerSeries.GetValues(index)
+ x = vx
+ y = ema.cache[index]
+ return
+}
+
+// GetFirstValues computes the first moving average value.
+func (ema *EMASeries) GetFirstValues() (x, y float64) {
+ if ema.InnerSeries == nil {
+ return
+ }
+ if len(ema.cache) == 0 {
+ ema.ensureCachedValues()
+ }
+ x, _ = ema.InnerSeries.GetValues(0)
+ y = ema.cache[0]
+ return
+}
+
+// GetLastValues computes the last moving average value but walking back window size samples,
+// and recomputing the last moving average chunk.
+func (ema *EMASeries) GetLastValues() (x, y float64) {
+ if ema.InnerSeries == nil {
+ return
+ }
+ if len(ema.cache) == 0 {
+ ema.ensureCachedValues()
+ }
+ lastIndex := ema.InnerSeries.Len() - 1
+ x, _ = ema.InnerSeries.GetValues(lastIndex)
+ y = ema.cache[lastIndex]
+ return
+}
+
+func (ema *EMASeries) ensureCachedValues() {
+ seriesLength := ema.InnerSeries.Len()
+ ema.cache = make([]float64, seriesLength)
+ sigma := ema.GetSigma()
+ for x := 0; x < seriesLength; x++ {
+ _, y := ema.InnerSeries.GetValues(x)
+ if x == 0 {
+ ema.cache[x] = y
+ continue
+ }
+ previousEMA := ema.cache[x-1]
+ ema.cache[x] = ((y - previousEMA) * sigma) + previousEMA
+ }
+}
+
+// Render renders the series.
+func (ema *EMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := ema.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, ema)
+}
+
+// Validate validates the series.
+func (ema *EMASeries) Validate() error {
+ if ema.InnerSeries == nil {
+ return fmt.Errorf("ema series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+
+
package chart
+
+import "fmt"
+
+// FirstValueAnnotation returns an annotation series of just the first value of a value provider as an annotation.
+func FirstValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
+ var vf ValueFormatter
+ if len(vfs) > 0 {
+ vf = vfs[0]
+ } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
+ _, vf = typed.GetValueFormatters()
+ } else {
+ vf = FloatValueFormatter
+ }
+
+ var firstValue Value2
+ if typed, isTyped := innerSeries.(FirstValuesProvider); isTyped {
+ firstValue.XValue, firstValue.YValue = typed.GetFirstValues()
+ firstValue.Label = vf(firstValue.YValue)
+ } else {
+ firstValue.XValue, firstValue.YValue = innerSeries.GetValues(0)
+ firstValue.Label = vf(firstValue.YValue)
+ }
+
+ var seriesName string
+ var seriesStyle Style
+ if typed, isTyped := innerSeries.(Series); isTyped {
+ seriesName = fmt.Sprintf("%s - First Value", typed.GetName())
+ seriesStyle = typed.GetStyle()
+ }
+
+ return AnnotationSeries{
+ Name: seriesName,
+ Style: seriesStyle,
+ Annotations: []Value2{firstValue},
+ }
+}
+
+
+
package chart
+
+import (
+ "sync"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/roboto"
+)
+
+var (
+ _defaultFontLock sync.Mutex
+ _defaultFont *truetype.Font
+)
+
+// GetDefaultFont returns the default font (Roboto-Medium).
+func GetDefaultFont() (*truetype.Font, error) {
+ if _defaultFont == nil {
+ _defaultFontLock.Lock()
+ defer _defaultFontLock.Unlock()
+ if _defaultFont == nil {
+ font, err := truetype.Parse(roboto.Roboto)
+ if err != nil {
+ return nil, err
+ }
+ _defaultFont = font
+ }
+ }
+ return _defaultFont, nil
+}
+
+
+
package chart
+
+// GridLineProvider is a type that provides grid lines.
+type GridLineProvider interface {
+ GetGridLines(ticks []Tick, isVertical bool, majorStyle, minorStyle Style) []GridLine
+}
+
+// GridLine is a line on a graph canvas.
+type GridLine struct {
+ IsMinor bool
+ Style Style
+ Value float64
+}
+
+// Major returns if the gridline is a `major` line.
+func (gl GridLine) Major() bool {
+ return !gl.IsMinor
+}
+
+// Minor returns if the gridline is a `minor` line.
+func (gl GridLine) Minor() bool {
+ return gl.IsMinor
+}
+
+// Render renders the gridline
+func (gl GridLine) Render(r Renderer, canvasBox Box, ra Range, isVertical bool, defaults Style) {
+ r.SetStrokeColor(gl.Style.GetStrokeColor(defaults.GetStrokeColor()))
+ r.SetStrokeWidth(gl.Style.GetStrokeWidth(defaults.GetStrokeWidth()))
+ r.SetStrokeDashArray(gl.Style.GetStrokeDashArray(defaults.GetStrokeDashArray()))
+
+ if isVertical {
+ lineLeft := canvasBox.Left + ra.Translate(gl.Value)
+ lineBottom := canvasBox.Bottom
+ lineTop := canvasBox.Top
+
+ r.MoveTo(lineLeft, lineBottom)
+ r.LineTo(lineLeft, lineTop)
+ r.Stroke()
+ } else {
+ lineLeft := canvasBox.Left
+ lineRight := canvasBox.Right
+ lineHeight := canvasBox.Bottom - ra.Translate(gl.Value)
+
+ r.MoveTo(lineLeft, lineHeight)
+ r.LineTo(lineRight, lineHeight)
+ r.Stroke()
+ }
+}
+
+// GenerateGridLines generates grid lines.
+func GenerateGridLines(ticks []Tick, majorStyle, minorStyle Style) []GridLine {
+ var gl []GridLine
+ isMinor := false
+
+ if len(ticks) < 3 {
+ return gl
+ }
+
+ for _, t := range ticks[1 : len(ticks)-1] {
+ s := majorStyle
+ if isMinor {
+ s = minorStyle
+ }
+ gl = append(gl, GridLine{
+ Style: s,
+ IsMinor: isMinor,
+ Value: t.Value,
+ })
+ isMinor = !isMinor
+ }
+ return gl
+}
+
+
+
package chart
+
+import "fmt"
+
+// HistogramSeries is a special type of series that draws as a histogram.
+// Some peculiarities; it will always be lower bounded at 0 (at the very least).
+// This may alter ranges a bit and generally you want to put a histogram series on it's own y-axis.
+type HistogramSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+ InnerSeries ValuesProvider
+}
+
+// GetName implements Series.GetName.
+func (hs HistogramSeries) GetName() string {
+ return hs.Name
+}
+
+// GetStyle implements Series.GetStyle.
+func (hs HistogramSeries) GetStyle() Style {
+ return hs.Style
+}
+
+// GetYAxis returns which yaxis the series is mapped to.
+func (hs HistogramSeries) GetYAxis() YAxisType {
+ return hs.YAxis
+}
+
+// Len implements BoundedValuesProvider.Len.
+func (hs HistogramSeries) Len() int {
+ return hs.InnerSeries.Len()
+}
+
+// GetValues implements ValuesProvider.GetValues.
+func (hs HistogramSeries) GetValues(index int) (x, y float64) {
+ return hs.InnerSeries.GetValues(index)
+}
+
+// GetBoundedValues implements BoundedValuesProvider.GetBoundedValue
+func (hs HistogramSeries) GetBoundedValues(index int) (x, y1, y2 float64) {
+ vx, vy := hs.InnerSeries.GetValues(index)
+
+ x = vx
+
+ if vy > 0 {
+ y1 = vy
+ return
+ }
+
+ y2 = vy
+ return
+}
+
+// Render implements Series.Render.
+func (hs HistogramSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := hs.Style.InheritFrom(defaults)
+ Draw.HistogramSeries(r, canvasBox, xrange, yrange, style, hs)
+}
+
+// Validate validates the series.
+func (hs HistogramSeries) Validate() error {
+ if hs.InnerSeries == nil {
+ return fmt.Errorf("histogram series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+
+
package chart
+
+import (
+ "bytes"
+ "errors"
+ "image"
+ "image/png"
+)
+
+// RGBACollector is a render target for a chart.
+type RGBACollector interface {
+ SetRGBA(i *image.RGBA)
+}
+
+// ImageWriter is a special type of io.Writer that produces a final image.
+type ImageWriter struct {
+ rgba *image.RGBA
+ contents *bytes.Buffer
+}
+
+func (ir *ImageWriter) Write(buffer []byte) (int, error) {
+ if ir.contents == nil {
+ ir.contents = bytes.NewBuffer([]byte{})
+ }
+ return ir.contents.Write(buffer)
+}
+
+// SetRGBA sets a raw version of the image.
+func (ir *ImageWriter) SetRGBA(i *image.RGBA) {
+ ir.rgba = i
+}
+
+// Image returns an *image.Image for the result.
+func (ir *ImageWriter) Image() (image.Image, error) {
+ if ir.rgba != nil {
+ return ir.rgba, nil
+ }
+ if ir.contents != nil && ir.contents.Len() > 0 {
+ return png.Decode(ir.contents)
+ }
+ return nil, errors.New("no valid sources for image data, cannot continue")
+}
+
+
+
package chart
+
+import "github.com/wcharczuk/go-chart/drawing"
+
+// Jet is a color map provider based on matlab's jet color map.
+func Jet(v, vmin, vmax float64) drawing.Color {
+ c := drawing.Color{R: 0xff, G: 0xff, B: 0xff, A: 0xff} // white
+ var dv float64
+
+ if v < vmin {
+ v = vmin
+ }
+ if v > vmax {
+ v = vmax
+ }
+ dv = vmax - vmin
+
+ if v < (vmin + 0.25*dv) {
+ c.R = 0
+ c.G = drawing.ColorChannelFromFloat(4 * (v - vmin) / dv)
+ } else if v < (vmin + 0.5*dv) {
+ c.R = 0
+ c.B = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.25*dv-v)/dv)
+ } else if v < (vmin + 0.75*dv) {
+ c.R = drawing.ColorChannelFromFloat(4 * (v - vmin - 0.5*dv) / dv)
+ c.B = 0
+ } else {
+ c.G = drawing.ColorChannelFromFloat(1 + 4*(vmin+0.75*dv-v)/dv)
+ c.B = 0
+ }
+
+ return c
+}
+
+
+
package chart
+
+import "fmt"
+
+// LastValueAnnotation returns an annotation series of just the last value of a value provider.
+func LastValueAnnotation(innerSeries ValuesProvider, vfs ...ValueFormatter) AnnotationSeries {
+ var vf ValueFormatter
+ if len(vfs) > 0 {
+ vf = vfs[0]
+ } else if typed, isTyped := innerSeries.(ValueFormatterProvider); isTyped {
+ _, vf = typed.GetValueFormatters()
+ } else {
+ vf = FloatValueFormatter
+ }
+
+ var lastValue Value2
+ if typed, isTyped := innerSeries.(LastValuesProvider); isTyped {
+ lastValue.XValue, lastValue.YValue = typed.GetLastValues()
+ lastValue.Label = vf(lastValue.YValue)
+ } else {
+ lastValue.XValue, lastValue.YValue = innerSeries.GetValues(innerSeries.Len() - 1)
+ lastValue.Label = vf(lastValue.YValue)
+ }
+
+ var seriesName string
+ var seriesStyle Style
+ if typed, isTyped := innerSeries.(Series); isTyped {
+ seriesName = fmt.Sprintf("%s - Last Value", typed.GetName())
+ seriesStyle = typed.GetStyle()
+ }
+
+ return AnnotationSeries{
+ Name: seriesName,
+ Style: seriesStyle,
+ Annotations: []Value2{lastValue},
+ }
+}
+
+
+
package chart
+
+import (
+ "github.com/wcharczuk/go-chart/drawing"
+ "github.com/wcharczuk/go-chart/util"
+)
+
+// 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,
+ }
+
+ legendStyle.GetTextOptions().WriteToRenderer(r)
+
+ // 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 = util.Math.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)
+
+ legendStyle.GetTextOptions().WriteToRenderer(r)
+
+ ycursor := legendContent.Top
+ tx := legendContent.Left
+ legendCount := 0
+ var label string
+ for x := 0; x < len(labels); x++ {
+ label = labels[x]
+ if len(label) > 0 {
+ if legendCount > 0 {
+ ycursor += DefaultMinimumTickVerticalSpacing
+ }
+
+ tb := r.MeasureText(label)
+
+ ty := ycursor + tb.Height()
+ r.Text(label, 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++
+ }
+ }
+ }
+}
+
+// LegendThin is a legend that doesn't obscure the chart area.
+func LegendThin(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,
+ Padding: Box{
+ Top: 2,
+ Left: 7,
+ Right: 7,
+ Bottom: 5,
+ },
+ }
+
+ var legendStyle Style
+ if len(userDefaults) > 0 {
+ legendStyle = userDefaults[0].InheritFrom(chartDefaults.InheritFrom(legendDefaults))
+ } else {
+ legendStyle = chartDefaults.InheritFrom(legendDefaults)
+ }
+
+ r.SetFont(legendStyle.GetFont())
+ r.SetFontColor(legendStyle.GetFontColor())
+ r.SetFontSize(legendStyle.GetFontSize())
+
+ 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)))
+ }
+ }
+ }
+
+ var textHeight int
+ var textWidth int
+ var textBox Box
+ for x := 0; x < len(labels); x++ {
+ if len(labels[x]) > 0 {
+ textBox = r.MeasureText(labels[x])
+ textHeight = util.Math.MaxInt(textBox.Height(), textHeight)
+ textWidth = util.Math.MaxInt(textBox.Width(), textWidth)
+ }
+ }
+
+ legendBoxHeight := textHeight + legendStyle.Padding.Top + legendStyle.Padding.Bottom
+ chartPadding := cb.Top
+ legendYMargin := (chartPadding - legendBoxHeight) >> 1
+
+ legendBox := Box{
+ Left: cb.Left,
+ Right: cb.Right,
+ Top: legendYMargin,
+ Bottom: legendYMargin + legendBoxHeight,
+ }
+
+ Draw.Box(r, legendBox, legendDefaults)
+
+ r.SetFont(legendStyle.GetFont())
+ r.SetFontColor(legendStyle.GetFontColor())
+ r.SetFontSize(legendStyle.GetFontSize())
+
+ lineTextGap := 5
+ lineLengthMinimum := 25
+
+ tx := legendBox.Left + legendStyle.Padding.Left
+ ty := legendYMargin + legendStyle.Padding.Top + textHeight
+ var label string
+ var lx, ly int
+ th2 := textHeight >> 1
+ for index := range labels {
+ label = labels[index]
+ if len(label) > 0 {
+ textBox = r.MeasureText(label)
+ r.Text(label, tx, ty)
+
+ lx = tx + textBox.Width() + lineTextGap
+ ly = ty - th2
+
+ r.SetStrokeColor(lines[index].GetStrokeColor())
+ r.SetStrokeWidth(lines[index].GetStrokeWidth())
+ r.SetStrokeDashArray(lines[index].GetStrokeDashArray())
+
+ r.MoveTo(lx, ly)
+ r.LineTo(lx+lineLengthMinimum, ly)
+ r.Stroke()
+
+ tx += textBox.Width() + DefaultMinimumTickHorizontalSpacing + lineTextGap + lineLengthMinimum
+ }
+ }
+ }
+}
+
+// LegendLeft is a legend that is designed for longer series lists.
+func LegendLeft(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: 5,
+ Left: 5,
+ // 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,
+ }
+
+ legendStyle.GetTextOptions().WriteToRenderer(r)
+
+ // 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 = util.Math.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)
+
+ legendStyle.GetTextOptions().WriteToRenderer(r)
+
+ ycursor := legendContent.Top
+ tx := legendContent.Left
+ legendCount := 0
+ var label string
+ for x := 0; x < len(labels); x++ {
+ label = labels[x]
+ if len(label) > 0 {
+ if legendCount > 0 {
+ ycursor += DefaultMinimumTickVerticalSpacing
+ }
+
+ tb := r.MeasureText(label)
+
+ ty := ycursor + tb.Height()
+ r.Text(label, 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++
+ }
+ }
+ }
+}
+
+
+
package chart
+
+// LinearCoefficientProvider is a type that returns linear cofficients.
+type LinearCoefficientProvider interface {
+ Coefficients() (m, b, stdev, avg float64)
+}
+
+// LinearCoefficients returns a fixed linear coefficient pair.
+func LinearCoefficients(m, b float64) LinearCoefficientSet {
+ return LinearCoefficientSet{
+ M: m,
+ B: b,
+ }
+}
+
+// NormalizedLinearCoefficients returns a fixed linear coefficient pair.
+func NormalizedLinearCoefficients(m, b, stdev, avg float64) LinearCoefficientSet {
+ return LinearCoefficientSet{
+ M: m,
+ B: b,
+ StdDev: stdev,
+ Avg: avg,
+ }
+}
+
+// LinearCoefficientSet is the m and b values for the linear equation in the form:
+// y = (m*x) + b
+type LinearCoefficientSet struct {
+ M float64
+ B float64
+ StdDev float64
+ Avg float64
+}
+
+// Coefficients returns the coefficients.
+func (lcs LinearCoefficientSet) Coefficients() (m, b, stdev, avg float64) {
+ m = lcs.M
+ b = lcs.B
+ stdev = lcs.StdDev
+ avg = lcs.Avg
+ return
+}
+
+
+
package chart
+
+import (
+ "fmt"
+
+ "github.com/wcharczuk/go-chart/seq"
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*LinearRegressionSeries)(nil)
+ _ FirstValuesProvider = (*LinearRegressionSeries)(nil)
+ _ LastValuesProvider = (*LinearRegressionSeries)(nil)
+ _ LinearCoefficientProvider = (*LinearRegressionSeries)(nil)
+)
+
+// LinearRegressionSeries is a series that plots the n-nearest neighbors
+// linear regression for the values.
+type LinearRegressionSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+
+ Limit int
+ Offset int
+ InnerSeries ValuesProvider
+
+ m float64
+ b float64
+ avgx float64
+ stddevx float64
+}
+
+// Coefficients returns the linear coefficients for the series.
+func (lrs LinearRegressionSeries) Coefficients() (m, b, stdev, avg float64) {
+ if lrs.IsZero() {
+ lrs.computeCoefficients()
+ }
+
+ m = lrs.m
+ b = lrs.b
+ stdev = lrs.stddevx
+ avg = lrs.avgx
+ return
+}
+
+// GetName returns the name of the time series.
+func (lrs LinearRegressionSeries) GetName() string {
+ return lrs.Name
+}
+
+// GetStyle returns the line style.
+func (lrs LinearRegressionSeries) GetStyle() Style {
+ return lrs.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (lrs LinearRegressionSeries) GetYAxis() YAxisType {
+ return lrs.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (lrs LinearRegressionSeries) Len() int {
+ return util.Math.MinInt(lrs.GetLimit(), lrs.InnerSeries.Len()-lrs.GetOffset())
+}
+
+// GetLimit returns the window size.
+func (lrs LinearRegressionSeries) GetLimit() int {
+ if lrs.Limit == 0 {
+ return lrs.InnerSeries.Len()
+ }
+ return lrs.Limit
+}
+
+// GetEndIndex returns the effective limit end.
+func (lrs LinearRegressionSeries) GetEndIndex() int {
+ windowEnd := lrs.GetOffset() + lrs.GetLimit()
+ innerSeriesLastIndex := lrs.InnerSeries.Len() - 1
+ return util.Math.MinInt(windowEnd, innerSeriesLastIndex)
+}
+
+// GetOffset returns the data offset.
+func (lrs LinearRegressionSeries) GetOffset() int {
+ if lrs.Offset == 0 {
+ return 0
+ }
+ return lrs.Offset
+}
+
+// GetValues gets a value at a given index.
+func (lrs *LinearRegressionSeries) GetValues(index int) (x, y float64) {
+ if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
+ return
+ }
+ if lrs.IsZero() {
+ lrs.computeCoefficients()
+ }
+ offset := lrs.GetOffset()
+ effectiveIndex := util.Math.MinInt(index+offset, lrs.InnerSeries.Len())
+ x, y = lrs.InnerSeries.GetValues(effectiveIndex)
+ y = (lrs.m * lrs.normalize(x)) + lrs.b
+ return
+}
+
+// GetFirstValues computes the first linear regression value.
+func (lrs *LinearRegressionSeries) GetFirstValues() (x, y float64) {
+ if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
+ return
+ }
+ if lrs.IsZero() {
+ lrs.computeCoefficients()
+ }
+ x, y = lrs.InnerSeries.GetValues(0)
+ y = (lrs.m * lrs.normalize(x)) + lrs.b
+ return
+}
+
+// GetLastValues computes the last linear regression value.
+func (lrs *LinearRegressionSeries) GetLastValues() (x, y float64) {
+ if lrs.InnerSeries == nil || lrs.InnerSeries.Len() == 0 {
+ return
+ }
+ if lrs.IsZero() {
+ lrs.computeCoefficients()
+ }
+ endIndex := lrs.GetEndIndex()
+ x, y = lrs.InnerSeries.GetValues(endIndex)
+ y = (lrs.m * lrs.normalize(x)) + lrs.b
+ return
+}
+
+// Render renders the series.
+func (lrs *LinearRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := lrs.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, lrs)
+}
+
+// Validate validates the series.
+func (lrs *LinearRegressionSeries) Validate() error {
+ if lrs.InnerSeries == nil {
+ return fmt.Errorf("linear regression series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+// IsZero returns if we've computed the coefficients or not.
+func (lrs *LinearRegressionSeries) IsZero() bool {
+ return lrs.m == 0 && lrs.b == 0
+}
+
+//
+// internal helpers
+//
+
+func (lrs *LinearRegressionSeries) normalize(xvalue float64) float64 {
+ return (xvalue - lrs.avgx) / lrs.stddevx
+}
+
+// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`.
+func (lrs *LinearRegressionSeries) computeCoefficients() {
+ startIndex := lrs.GetOffset()
+ endIndex := lrs.GetEndIndex()
+
+ p := float64(endIndex - startIndex)
+
+ xvalues := seq.NewBufferWithCapacity(lrs.Len())
+ for index := startIndex; index < endIndex; index++ {
+ x, _ := lrs.InnerSeries.GetValues(index)
+ xvalues.Enqueue(x)
+ }
+
+ lrs.avgx = seq.Seq{Provider: xvalues}.Average()
+ lrs.stddevx = seq.Seq{Provider: xvalues}.StdDev()
+
+ var sumx, sumy, sumxx, sumxy float64
+ for index := startIndex; index < endIndex; index++ {
+ x, y := lrs.InnerSeries.GetValues(index)
+
+ x = lrs.normalize(x)
+
+ sumx += x
+ sumy += y
+ sumxx += x * x
+ sumxy += x * y
+ }
+
+ lrs.m = (p*sumxy - sumx*sumy) / (p*sumxx - sumx*sumx)
+ lrs.b = (sumy / p) - (lrs.m * sumx / p)
+}
+
+
+
package chart
+
+import (
+ "fmt"
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*LinearSeries)(nil)
+ _ FirstValuesProvider = (*LinearSeries)(nil)
+ _ LastValuesProvider = (*LinearSeries)(nil)
+)
+
+// LinearSeries is a series that plots a line in a given domain.
+type LinearSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+
+ XValues []float64
+ InnerSeries LinearCoefficientProvider
+
+ m float64
+ b float64
+ stdev float64
+ avg float64
+}
+
+// GetName returns the name of the time series.
+func (ls LinearSeries) GetName() string {
+ return ls.Name
+}
+
+// GetStyle returns the line style.
+func (ls LinearSeries) GetStyle() Style {
+ return ls.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (ls LinearSeries) GetYAxis() YAxisType {
+ return ls.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (ls LinearSeries) Len() int {
+ return len(ls.XValues)
+}
+
+// GetEndIndex returns the effective limit end.
+func (ls LinearSeries) GetEndIndex() int {
+ return len(ls.XValues) - 1
+}
+
+// GetValues gets a value at a given index.
+func (ls *LinearSeries) GetValues(index int) (x, y float64) {
+ if ls.InnerSeries == nil || len(ls.XValues) == 0 {
+ return
+ }
+ if ls.IsZero() {
+ ls.computeCoefficients()
+ }
+ x = ls.XValues[index]
+ y = (ls.m * ls.normalize(x)) + ls.b
+ return
+}
+
+// GetFirstValues computes the first linear regression value.
+func (ls *LinearSeries) GetFirstValues() (x, y float64) {
+ if ls.InnerSeries == nil || len(ls.XValues) == 0 {
+ return
+ }
+ if ls.IsZero() {
+ ls.computeCoefficients()
+ }
+ x, y = ls.GetValues(0)
+ return
+}
+
+// GetLastValues computes the last linear regression value.
+func (ls *LinearSeries) GetLastValues() (x, y float64) {
+ if ls.InnerSeries == nil || len(ls.XValues) == 0 {
+ return
+ }
+ if ls.IsZero() {
+ ls.computeCoefficients()
+ }
+ x, y = ls.GetValues(ls.GetEndIndex())
+ return
+}
+
+// Render renders the series.
+func (ls *LinearSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ Draw.LineSeries(r, canvasBox, xrange, yrange, ls.Style.InheritFrom(defaults), ls)
+}
+
+// Validate validates the series.
+func (ls LinearSeries) Validate() error {
+ if ls.InnerSeries == nil {
+ return fmt.Errorf("linear regression series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+// IsZero returns if the linear series has computed coefficients or not.
+func (ls LinearSeries) IsZero() bool {
+ return ls.m == 0 && ls.b == 0
+}
+
+// computeCoefficients computes the `m` and `b` terms in the linear formula given by `y = mx+b`.
+func (ls *LinearSeries) computeCoefficients() {
+ ls.m, ls.b, ls.stdev, ls.avg = ls.InnerSeries.Coefficients()
+}
+
+func (ls *LinearSeries) normalize(xvalue float64) float64 {
+ if ls.avg > 0 && ls.stdev > 0 {
+ return (xvalue - ls.avg) / ls.stdev
+ }
+ return xvalue
+}
+
+
+
package chart
+
+import "fmt"
+
+const (
+ // DefaultMACDPeriodPrimary is the long window.
+ DefaultMACDPeriodPrimary = 26
+ // DefaultMACDPeriodSecondary is the short window.
+ DefaultMACDPeriodSecondary = 12
+ // DefaultMACDSignalPeriod is the signal period to compute for the MACD.
+ DefaultMACDSignalPeriod = 9
+)
+
+// MACDSeries computes the difference between the MACD line and the MACD Signal line.
+// It is used in technical analysis and gives a lagging indicator of momentum.
+type MACDSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+ InnerSeries ValuesProvider
+
+ PrimaryPeriod int
+ SecondaryPeriod int
+ SignalPeriod int
+
+ signal *MACDSignalSeries
+ macdl *MACDLineSeries
+}
+
+// Validate validates the series.
+func (macd MACDSeries) Validate() error {
+ var err error
+ if macd.signal != nil {
+ err = macd.signal.Validate()
+ }
+ if err != nil {
+ return err
+ }
+ if macd.macdl != nil {
+ err = macd.macdl.Validate()
+ }
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// GetPeriods returns the primary and secondary periods.
+func (macd MACDSeries) GetPeriods() (w1, w2, sig int) {
+ if macd.PrimaryPeriod == 0 {
+ w1 = DefaultMACDPeriodPrimary
+ } else {
+ w1 = macd.PrimaryPeriod
+ }
+ if macd.SecondaryPeriod == 0 {
+ w2 = DefaultMACDPeriodSecondary
+ } else {
+ w2 = macd.SecondaryPeriod
+ }
+ if macd.SignalPeriod == 0 {
+ sig = DefaultMACDSignalPeriod
+ } else {
+ sig = macd.SignalPeriod
+ }
+ return
+}
+
+// GetName returns the name of the time series.
+func (macd MACDSeries) GetName() string {
+ return macd.Name
+}
+
+// GetStyle returns the line style.
+func (macd MACDSeries) GetStyle() Style {
+ return macd.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (macd MACDSeries) GetYAxis() YAxisType {
+ return macd.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (macd MACDSeries) Len() int {
+ if macd.InnerSeries == nil {
+ return 0
+ }
+
+ return macd.InnerSeries.Len()
+}
+
+// GetValues gets a value at a given index. For MACD it is the signal value.
+func (macd *MACDSeries) GetValues(index int) (x float64, y float64) {
+ if macd.InnerSeries == nil {
+ return
+ }
+
+ if macd.signal == nil || macd.macdl == nil {
+ macd.ensureChildSeries()
+ }
+
+ _, lv := macd.macdl.GetValues(index)
+ _, sv := macd.signal.GetValues(index)
+
+ x, _ = macd.InnerSeries.GetValues(index)
+ y = lv - sv
+
+ return
+}
+
+func (macd *MACDSeries) ensureChildSeries() {
+ w1, w2, sig := macd.GetPeriods()
+
+ macd.signal = &MACDSignalSeries{
+ InnerSeries: macd.InnerSeries,
+ PrimaryPeriod: w1,
+ SecondaryPeriod: w2,
+ SignalPeriod: sig,
+ }
+
+ macd.macdl = &MACDLineSeries{
+ InnerSeries: macd.InnerSeries,
+ PrimaryPeriod: w1,
+ SecondaryPeriod: w2,
+ }
+}
+
+// MACDSignalSeries computes the EMA of the MACDLineSeries.
+type MACDSignalSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+ InnerSeries ValuesProvider
+
+ PrimaryPeriod int
+ SecondaryPeriod int
+ SignalPeriod int
+
+ signal *EMASeries
+}
+
+// Validate validates the series.
+func (macds MACDSignalSeries) Validate() error {
+ if macds.signal != nil {
+ return macds.signal.Validate()
+ }
+ return nil
+}
+
+// GetPeriods returns the primary and secondary periods.
+func (macds MACDSignalSeries) GetPeriods() (w1, w2, sig int) {
+ if macds.PrimaryPeriod == 0 {
+ w1 = DefaultMACDPeriodPrimary
+ } else {
+ w1 = macds.PrimaryPeriod
+ }
+ if macds.SecondaryPeriod == 0 {
+ w2 = DefaultMACDPeriodSecondary
+ } else {
+ w2 = macds.SecondaryPeriod
+ }
+ if macds.SignalPeriod == 0 {
+ sig = DefaultMACDSignalPeriod
+ } else {
+ sig = macds.SignalPeriod
+ }
+ return
+}
+
+// GetName returns the name of the time series.
+func (macds MACDSignalSeries) GetName() string {
+ return macds.Name
+}
+
+// GetStyle returns the line style.
+func (macds MACDSignalSeries) GetStyle() Style {
+ return macds.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (macds MACDSignalSeries) GetYAxis() YAxisType {
+ return macds.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (macds *MACDSignalSeries) Len() int {
+ if macds.InnerSeries == nil {
+ return 0
+ }
+
+ return macds.InnerSeries.Len()
+}
+
+// GetValues gets a value at a given index. For MACD it is the signal value.
+func (macds *MACDSignalSeries) GetValues(index int) (x float64, y float64) {
+ if macds.InnerSeries == nil {
+ return
+ }
+
+ if macds.signal == nil {
+ macds.ensureSignal()
+ }
+ x, _ = macds.InnerSeries.GetValues(index)
+ _, y = macds.signal.GetValues(index)
+ return
+}
+
+func (macds *MACDSignalSeries) ensureSignal() {
+ w1, w2, sig := macds.GetPeriods()
+
+ macds.signal = &EMASeries{
+ InnerSeries: &MACDLineSeries{
+ InnerSeries: macds.InnerSeries,
+ PrimaryPeriod: w1,
+ SecondaryPeriod: w2,
+ },
+ Period: sig,
+ }
+}
+
+// Render renders the series.
+func (macds *MACDSignalSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := macds.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, macds)
+}
+
+// MACDLineSeries is a series that computes the inner ema1-ema2 value as a series.
+type MACDLineSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+ InnerSeries ValuesProvider
+
+ PrimaryPeriod int
+ SecondaryPeriod int
+
+ ema1 *EMASeries
+ ema2 *EMASeries
+
+ Sigma float64
+}
+
+// Validate validates the series.
+func (macdl MACDLineSeries) Validate() error {
+ var err error
+ if macdl.ema1 != nil {
+ err = macdl.ema1.Validate()
+ }
+ if err != nil {
+ return err
+ }
+ if macdl.ema2 != nil {
+ err = macdl.ema2.Validate()
+ }
+ if err != nil {
+ return err
+ }
+ if macdl.InnerSeries == nil {
+ return fmt.Errorf("MACDLineSeries: must provide an inner series")
+ }
+ return nil
+}
+
+// GetName returns the name of the time series.
+func (macdl MACDLineSeries) GetName() string {
+ return macdl.Name
+}
+
+// GetStyle returns the line style.
+func (macdl MACDLineSeries) GetStyle() Style {
+ return macdl.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (macdl MACDLineSeries) GetYAxis() YAxisType {
+ return macdl.YAxis
+}
+
+// GetPeriods returns the primary and secondary periods.
+func (macdl MACDLineSeries) GetPeriods() (w1, w2 int) {
+ if macdl.PrimaryPeriod == 0 {
+ w1 = DefaultMACDPeriodPrimary
+ } else {
+ w1 = macdl.PrimaryPeriod
+ }
+ if macdl.SecondaryPeriod == 0 {
+ w2 = DefaultMACDPeriodSecondary
+ } else {
+ w2 = macdl.SecondaryPeriod
+ }
+ return
+}
+
+// Len returns the number of elements in the series.
+func (macdl *MACDLineSeries) Len() int {
+ if macdl.InnerSeries == nil {
+ return 0
+ }
+
+ return macdl.InnerSeries.Len()
+}
+
+// GetValues gets a value at a given index. For MACD it is the signal value.
+func (macdl *MACDLineSeries) GetValues(index int) (x float64, y float64) {
+ if macdl.InnerSeries == nil {
+ return
+ }
+ if macdl.ema1 == nil && macdl.ema2 == nil {
+ macdl.ensureEMASeries()
+ }
+
+ x, _ = macdl.InnerSeries.GetValues(index)
+
+ _, emav1 := macdl.ema1.GetValues(index)
+ _, emav2 := macdl.ema2.GetValues(index)
+
+ y = emav2 - emav1
+ return
+}
+
+func (macdl *MACDLineSeries) ensureEMASeries() {
+ w1, w2 := macdl.GetPeriods()
+
+ macdl.ema1 = &EMASeries{
+ InnerSeries: macdl.InnerSeries,
+ Period: w1,
+ }
+ macdl.ema2 = &EMASeries{
+ InnerSeries: macdl.InnerSeries,
+ Period: w2,
+ }
+}
+
+// Render renders the series.
+func (macdl *MACDLineSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := macdl.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, macdl)
+}
+
+
+
package matrix
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "math"
+)
+
+const (
+ // DefaultEpsilon represents the minimum precision for matrix math operations.
+ DefaultEpsilon = 0.000001
+)
+
+var (
+ // ErrDimensionMismatch is a typical error.
+ ErrDimensionMismatch = errors.New("dimension mismatch")
+
+ // ErrSingularValue is a typical error.
+ ErrSingularValue = errors.New("singular value")
+)
+
+// New returns a new matrix.
+func New(rows, cols int, values ...float64) *Matrix {
+ if len(values) == 0 {
+ return &Matrix{
+ stride: cols,
+ epsilon: DefaultEpsilon,
+ elements: make([]float64, rows*cols),
+ }
+ }
+ elems := make([]float64, rows*cols)
+ copy(elems, values)
+ return &Matrix{
+ stride: cols,
+ epsilon: DefaultEpsilon,
+ elements: elems,
+ }
+}
+
+// Identity returns the identity matrix of a given order.
+func Identity(order int) *Matrix {
+ m := New(order, order)
+ for i := 0; i < order; i++ {
+ m.Set(i, i, 1)
+ }
+ return m
+}
+
+// Zero returns a matrix of a given size zeroed.
+func Zero(rows, cols int) *Matrix {
+ return New(rows, cols)
+}
+
+// Ones returns an matrix of ones.
+func Ones(rows, cols int) *Matrix {
+ ones := make([]float64, rows*cols)
+ for i := 0; i < (rows * cols); i++ {
+ ones[i] = 1
+ }
+
+ return &Matrix{
+ stride: cols,
+ epsilon: DefaultEpsilon,
+ elements: ones,
+ }
+}
+
+// Eye returns the eye matrix.
+func Eye(n int) *Matrix {
+ m := Zero(n, n)
+ for i := 0; i < len(m.elements); i += n + 1 {
+ m.elements[i] = 1
+ }
+ return m
+}
+
+// NewFromArrays creates a matrix from a jagged array set.
+func NewFromArrays(a [][]float64) *Matrix {
+ rows := len(a)
+ if rows == 0 {
+ return nil
+ }
+ cols := len(a[0])
+ m := New(rows, cols)
+ for row := 0; row < rows; row++ {
+ for col := 0; col < cols; col++ {
+ m.Set(row, col, a[row][col])
+ }
+ }
+ return m
+}
+
+// Matrix represents a 2d dense array of floats.
+type Matrix struct {
+ epsilon float64
+ elements []float64
+ stride int
+}
+
+// String returns a string representation of the matrix.
+func (m *Matrix) String() string {
+ buffer := bytes.NewBuffer(nil)
+ rows, cols := m.Size()
+
+ for row := 0; row < rows; row++ {
+ for col := 0; col < cols; col++ {
+ buffer.WriteString(f64s(m.Get(row, col)))
+ buffer.WriteRune(' ')
+ }
+ buffer.WriteRune('\n')
+ }
+ return buffer.String()
+}
+
+// Epsilon returns the maximum precision for math operations.
+func (m *Matrix) Epsilon() float64 {
+ return m.epsilon
+}
+
+// WithEpsilon sets the epsilon on the matrix and returns a reference to the matrix.
+func (m *Matrix) WithEpsilon(epsilon float64) *Matrix {
+ m.epsilon = epsilon
+ return m
+}
+
+// Each applies the action to each element of the matrix in
+// rows => cols order.
+func (m *Matrix) Each(action func(row, col int, value float64)) {
+ rows, cols := m.Size()
+ for row := 0; row < rows; row++ {
+ for col := 0; col < cols; col++ {
+ action(row, col, m.Get(row, col))
+ }
+ }
+}
+
+// Round rounds all the values in a matrix to it epsilon,
+// returning a reference to the original
+func (m *Matrix) Round() *Matrix {
+ rows, cols := m.Size()
+ for row := 0; row < rows; row++ {
+ for col := 0; col < cols; col++ {
+ m.Set(row, col, roundToEpsilon(m.Get(row, col), m.epsilon))
+ }
+ }
+ return m
+}
+
+// Arrays returns the matrix as a two dimensional jagged array.
+func (m *Matrix) Arrays() [][]float64 {
+ rows, cols := m.Size()
+ a := make([][]float64, rows)
+
+ for row := 0; row < rows; row++ {
+ a[row] = make([]float64, cols)
+
+ for col := 0; col < cols; col++ {
+ a[row][col] = m.Get(row, col)
+ }
+ }
+ return a
+}
+
+// Size returns the dimensions of the matrix.
+func (m *Matrix) Size() (rows, cols int) {
+ rows = len(m.elements) / m.stride
+ cols = m.stride
+ return
+}
+
+// IsSquare returns if the row count is equal to the column count.
+func (m *Matrix) IsSquare() bool {
+ return m.stride == (len(m.elements) / m.stride)
+}
+
+// IsSymmetric returns if the matrix is symmetric about its diagonal.
+func (m *Matrix) IsSymmetric() bool {
+ rows, cols := m.Size()
+
+ if rows != cols {
+ return false
+ }
+
+ for i := 0; i < rows; i++ {
+ for j := 0; j < i; j++ {
+ if m.Get(i, j) != m.Get(j, i) {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+// Get returns the element at the given row, col.
+func (m *Matrix) Get(row, col int) float64 {
+ index := (m.stride * row) + col
+ return m.elements[index]
+}
+
+// Set sets a value.
+func (m *Matrix) Set(row, col int, val float64) {
+ index := (m.stride * row) + col
+ m.elements[index] = val
+}
+
+// Col returns a column of the matrix as a vector.
+func (m *Matrix) Col(col int) Vector {
+ rows, _ := m.Size()
+ values := make([]float64, rows)
+ for row := 0; row < rows; row++ {
+ values[row] = m.Get(row, col)
+ }
+ return Vector(values)
+}
+
+// Row returns a row of the matrix as a vector.
+func (m *Matrix) Row(row int) Vector {
+ _, cols := m.Size()
+ values := make([]float64, cols)
+ for col := 0; col < cols; col++ {
+ values[col] = m.Get(row, col)
+ }
+ return Vector(values)
+}
+
+// SubMatrix returns a sub matrix from a given outer matrix.
+func (m *Matrix) SubMatrix(i, j, rows, cols int) *Matrix {
+ return &Matrix{
+ elements: m.elements[i*m.stride+j : i*m.stride+j+(rows-1)*m.stride+cols],
+ stride: m.stride,
+ epsilon: m.epsilon,
+ }
+}
+
+// ScaleRow applies a scale to an entire row.
+func (m *Matrix) ScaleRow(row int, scale float64) {
+ startIndex := row * m.stride
+ for i := startIndex; i < m.stride; i++ {
+ m.elements[i] = m.elements[i] * scale
+ }
+}
+
+func (m *Matrix) scaleAddRow(rd int, rs int, f float64) {
+ indexd := rd * m.stride
+ indexs := rs * m.stride
+ for col := 0; col < m.stride; col++ {
+ m.elements[indexd] += f * m.elements[indexs]
+ indexd++
+ indexs++
+ }
+}
+
+// SwapRows swaps a row in the matrix in place.
+func (m *Matrix) SwapRows(i, j int) {
+ var vi, vj float64
+ for col := 0; col < m.stride; col++ {
+ vi = m.Get(i, col)
+ vj = m.Get(j, col)
+ m.Set(i, col, vj)
+ m.Set(j, col, vi)
+ }
+}
+
+// Augment concatenates two matrices about the horizontal.
+func (m *Matrix) Augment(m2 *Matrix) (*Matrix, error) {
+ mr, mc := m.Size()
+ m2r, m2c := m2.Size()
+ if mr != m2r {
+ return nil, ErrDimensionMismatch
+ }
+
+ m3 := Zero(mr, mc+m2c)
+ for row := 0; row < mr; row++ {
+ for col := 0; col < mc; col++ {
+ m3.Set(row, col, m.Get(row, col))
+ }
+ for col := 0; col < m2c; col++ {
+ m3.Set(row, mc+col, m2.Get(row, col))
+ }
+ }
+ return m3, nil
+}
+
+// Copy returns a duplicate of a given matrix.
+func (m *Matrix) Copy() *Matrix {
+ m2 := &Matrix{stride: m.stride, epsilon: m.epsilon, elements: make([]float64, len(m.elements))}
+ copy(m2.elements, m.elements)
+ return m2
+}
+
+// DiagonalVector returns a vector from the diagonal of a matrix.
+func (m *Matrix) DiagonalVector() Vector {
+ rows, cols := m.Size()
+ rank := minInt(rows, cols)
+ values := make([]float64, rank)
+
+ for index := 0; index < rank; index++ {
+ values[index] = m.Get(index, index)
+ }
+ return Vector(values)
+}
+
+// Diagonal returns a matrix from the diagonal of a matrix.
+func (m *Matrix) Diagonal() *Matrix {
+ rows, cols := m.Size()
+ rank := minInt(rows, cols)
+ m2 := New(rank, rank)
+
+ for index := 0; index < rank; index++ {
+ m2.Set(index, index, m.Get(index, index))
+ }
+ return m2
+}
+
+// Equals returns if a matrix equals another matrix.
+func (m *Matrix) Equals(other *Matrix) bool {
+ if other == nil && m != nil {
+ return false
+ } else if other == nil {
+ return true
+ }
+
+ if m.stride != other.stride {
+ return false
+ }
+
+ msize := len(m.elements)
+ m2size := len(other.elements)
+
+ if msize != m2size {
+ return false
+ }
+
+ for i := 0; i < msize; i++ {
+ if m.elements[i] != other.elements[i] {
+ return false
+ }
+ }
+ return true
+}
+
+// L returns the matrix with zeros below the diagonal.
+func (m *Matrix) L() *Matrix {
+ rows, cols := m.Size()
+ m2 := New(rows, cols)
+ for row := 0; row < rows; row++ {
+ for col := row; col < cols; col++ {
+ m2.Set(row, col, m.Get(row, col))
+ }
+ }
+ return m2
+}
+
+// U returns the matrix with zeros above the diagonal.
+// Does not include the diagonal.
+func (m *Matrix) U() *Matrix {
+ rows, cols := m.Size()
+ m2 := New(rows, cols)
+ for row := 0; row < rows; row++ {
+ for col := 0; col < row && col < cols; col++ {
+ m2.Set(row, col, m.Get(row, col))
+ }
+ }
+ return m2
+}
+
+// math operations
+
+// Multiply multiplies two matrices.
+func (m *Matrix) Multiply(m2 *Matrix) (m3 *Matrix, err error) {
+ if m.stride*m2.stride != len(m2.elements) {
+ return nil, ErrDimensionMismatch
+ }
+
+ m3 = &Matrix{epsilon: m.epsilon, stride: m2.stride, elements: make([]float64, (len(m.elements)/m.stride)*m2.stride)}
+ for m1c0, m3x := 0, 0; m1c0 < len(m.elements); m1c0 += m.stride {
+ for m2r0 := 0; m2r0 < m2.stride; m2r0++ {
+ for m1x, m2x := m1c0, m2r0; m2x < len(m2.elements); m2x += m2.stride {
+ m3.elements[m3x] += m.elements[m1x] * m2.elements[m2x]
+ m1x++
+ }
+ m3x++
+ }
+ }
+ return
+}
+
+// Pivotize does something i'm not sure what.
+func (m *Matrix) Pivotize() *Matrix {
+ pv := make([]int, m.stride)
+
+ for i := range pv {
+ pv[i] = i
+ }
+
+ for j, dx := 0, 0; j < m.stride; j++ {
+ row := j
+ max := m.elements[dx]
+ for i, ixcj := j, dx; i < m.stride; i++ {
+ if m.elements[ixcj] > max {
+ max = m.elements[ixcj]
+ row = i
+ }
+ ixcj += m.stride
+ }
+ if j != row {
+ pv[row], pv[j] = pv[j], pv[row]
+ }
+ dx += m.stride + 1
+ }
+ p := Zero(m.stride, m.stride)
+ for r, c := range pv {
+ p.elements[r*m.stride+c] = 1
+ }
+ return p
+}
+
+// Times returns the product of a matrix and another.
+func (m *Matrix) Times(m2 *Matrix) (*Matrix, error) {
+ mr, mc := m.Size()
+ m2r, m2c := m2.Size()
+
+ if mc != m2r {
+ return nil, fmt.Errorf("cannot multiply (%dx%d) and (%dx%d)", mr, mc, m2r, m2c)
+ //return nil, ErrDimensionMismatch
+ }
+
+ c := Zero(mr, m2c)
+
+ for i := 0; i < mr; i++ {
+ sums := c.elements[i*c.stride : (i+1)*c.stride]
+ for k, a := range m.elements[i*m.stride : i*m.stride+m.stride] {
+ for j, b := range m2.elements[k*m2.stride : k*m2.stride+m2.stride] {
+ sums[j] += a * b
+ }
+ }
+ }
+
+ return c, nil
+}
+
+// Decompositions
+
+// LU performs the LU decomposition.
+func (m *Matrix) LU() (l, u, p *Matrix) {
+ l = Zero(m.stride, m.stride)
+ u = Zero(m.stride, m.stride)
+ p = m.Pivotize()
+ m, _ = p.Multiply(m)
+ for j, jxc0 := 0, 0; j < m.stride; j++ {
+ l.elements[jxc0+j] = 1
+ for i, ixc0 := 0, 0; ixc0 <= jxc0; i++ {
+ sum := 0.
+ for k, kxcj := 0, j; k < i; k++ {
+ sum += u.elements[kxcj] * l.elements[ixc0+k]
+ kxcj += m.stride
+ }
+ u.elements[ixc0+j] = m.elements[ixc0+j] - sum
+ ixc0 += m.stride
+ }
+ for ixc0 := jxc0; ixc0 < len(m.elements); ixc0 += m.stride {
+ sum := 0.
+ for k, kxcj := 0, j; k < j; k++ {
+ sum += u.elements[kxcj] * l.elements[ixc0+k]
+ kxcj += m.stride
+ }
+ l.elements[ixc0+j] = (m.elements[ixc0+j] - sum) / u.elements[jxc0+j]
+ }
+ jxc0 += m.stride
+ }
+ return
+}
+
+// QR performs the qr decomposition.
+func (m *Matrix) QR() (q, r *Matrix) {
+ defer func() {
+ q = q.Round()
+ r = r.Round()
+ }()
+
+ rows, cols := m.Size()
+ qr := m.Copy()
+ q = New(rows, cols)
+ r = New(rows, cols)
+
+ var i, j, k int
+ var norm, s float64
+
+ for k = 0; k < cols; k++ {
+ norm = 0
+ for i = k; i < rows; i++ {
+ norm = math.Hypot(norm, qr.Get(i, k))
+ }
+
+ if norm != 0 {
+ if qr.Get(k, k) < 0 {
+ norm = -norm
+ }
+
+ for i = k; i < rows; i++ {
+ qr.Set(i, k, qr.Get(i, k)/norm)
+ }
+ qr.Set(k, k, qr.Get(k, k)+1.0)
+
+ for j = k + 1; j < cols; j++ {
+ s = 0
+ for i = k; i < rows; i++ {
+ s += qr.Get(i, k) * qr.Get(i, j)
+ }
+ s = -s / qr.Get(k, k)
+ for i = k; i < rows; i++ {
+ qr.Set(i, j, qr.Get(i, j)+s*qr.Get(i, k))
+
+ if i < j {
+ r.Set(i, j, qr.Get(i, j))
+ }
+ }
+
+ }
+ }
+
+ r.Set(k, k, -norm)
+
+ }
+
+ //Q Matrix:
+ i, j, k = 0, 0, 0
+
+ for k = cols - 1; k >= 0; k-- {
+ q.Set(k, k, 1.0)
+ for j = k; j < cols; j++ {
+ if qr.Get(k, k) != 0 {
+ s = 0
+ for i = k; i < rows; i++ {
+ s += qr.Get(i, k) * q.Get(i, j)
+ }
+ s = -s / qr.Get(k, k)
+ for i = k; i < rows; i++ {
+ q.Set(i, j, q.Get(i, j)+s*qr.Get(i, k))
+ }
+ }
+ }
+ }
+
+ return
+}
+
+// Transpose flips a matrix about its diagonal, returning a new copy.
+func (m *Matrix) Transpose() *Matrix {
+ rows, cols := m.Size()
+ m2 := Zero(cols, rows)
+ for i := 0; i < rows; i++ {
+ for j := 0; j < cols; j++ {
+ m2.Set(j, i, m.Get(i, j))
+ }
+ }
+ return m2
+}
+
+// Inverse returns a matrix such that M*I==1.
+func (m *Matrix) Inverse() (*Matrix, error) {
+ if !m.IsSymmetric() {
+ return nil, ErrDimensionMismatch
+ }
+
+ rows, cols := m.Size()
+
+ aug, _ := m.Augment(Eye(rows))
+ for i := 0; i < rows; i++ {
+ j := i
+ for k := i; k < rows; k++ {
+ if math.Abs(aug.Get(k, i)) > math.Abs(aug.Get(j, i)) {
+ j = k
+ }
+ }
+ if j != i {
+ aug.SwapRows(i, j)
+ }
+ if aug.Get(i, i) == 0 {
+ return nil, ErrSingularValue
+ }
+ aug.ScaleRow(i, 1.0/aug.Get(i, i))
+ for k := 0; k < rows; k++ {
+ if k == i {
+ continue
+ }
+ aug.scaleAddRow(k, i, -aug.Get(k, i))
+ }
+ }
+ return aug.SubMatrix(0, cols, rows, cols), nil
+}
+
+
+
package matrix
+
+import "errors"
+
+var (
+ // ErrPolyRegArraysSameLength is a common error.
+ ErrPolyRegArraysSameLength = errors.New("polynomial array inputs must be the same length")
+)
+
+// Poly returns the polynomial regress of a given degree over the given values.
+func Poly(xvalues, yvalues []float64, degree int) ([]float64, error) {
+ if len(xvalues) != len(yvalues) {
+ return nil, ErrPolyRegArraysSameLength
+ }
+
+ m := len(yvalues)
+ n := degree + 1
+ y := New(m, 1, yvalues...)
+ x := Zero(m, n)
+
+ for i := 0; i < m; i++ {
+ ip := float64(1)
+ for j := 0; j < n; j++ {
+ x.Set(i, j, ip)
+ ip *= xvalues[i]
+ }
+ }
+
+ q, r := x.QR()
+ qty, err := q.Transpose().Times(y)
+ if err != nil {
+ return nil, err
+ }
+
+ c := make([]float64, n)
+ for i := n - 1; i >= 0; i-- {
+ c[i] = qty.Get(i, 0)
+ for j := i + 1; j < n; j++ {
+ c[i] -= c[j] * r.Get(i, j)
+ }
+ c[i] /= r.Get(i, i)
+ }
+
+ return c, nil
+}
+
+
+
package matrix
+
+import (
+ "math"
+ "strconv"
+)
+
+func minInt(values ...int) int {
+ min := math.MaxInt32
+
+ for x := 0; x < len(values); x++ {
+ if values[x] < min {
+ min = values[x]
+ }
+ }
+ return min
+}
+
+func maxInt(values ...int) int {
+ max := math.MinInt32
+
+ for x := 0; x < len(values); x++ {
+ if values[x] > max {
+ max = values[x]
+ }
+ }
+ return max
+}
+
+func f64s(v float64) string {
+ return strconv.FormatFloat(v, 'f', -1, 64)
+}
+
+func roundToEpsilon(value, epsilon float64) float64 {
+ return math.Nextafter(value, value)
+}
+
+
+
package matrix
+
+// Vector is just an array of values.
+type Vector []float64
+
+// DotProduct returns the dot product of two vectors.
+func (v Vector) DotProduct(v2 Vector) (result float64, err error) {
+ if len(v) != len(v2) {
+ err = ErrDimensionMismatch
+ return
+ }
+
+ for i := 0; i < len(v); i++ {
+ result = result + (v[i] * v2[i])
+ }
+ return
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "math"
+)
+
+// MinSeries draws a horizontal line at the minimum value of the inner series.
+type MinSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+ InnerSeries ValuesProvider
+
+ minValue *float64
+}
+
+// GetName returns the name of the time series.
+func (ms MinSeries) GetName() string {
+ return ms.Name
+}
+
+// GetStyle returns the line style.
+func (ms MinSeries) GetStyle() Style {
+ return ms.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (ms MinSeries) GetYAxis() YAxisType {
+ return ms.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (ms MinSeries) Len() int {
+ return ms.InnerSeries.Len()
+}
+
+// GetValues gets a value at a given index.
+func (ms *MinSeries) GetValues(index int) (x, y float64) {
+ ms.ensureMinValue()
+ x, _ = ms.InnerSeries.GetValues(index)
+ y = *ms.minValue
+ return
+}
+
+// Render renders the series.
+func (ms *MinSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := ms.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, ms)
+}
+
+func (ms *MinSeries) ensureMinValue() {
+ if ms.minValue == nil {
+ minValue := math.MaxFloat64
+ var y float64
+ for x := 0; x < ms.InnerSeries.Len(); x++ {
+ _, y = ms.InnerSeries.GetValues(x)
+ if y < minValue {
+ minValue = y
+ }
+ }
+ ms.minValue = &minValue
+ }
+}
+
+// Validate validates the series.
+func (ms *MinSeries) Validate() error {
+ if ms.InnerSeries == nil {
+ return fmt.Errorf("min series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+// MaxSeries draws a horizontal line at the maximum value of the inner series.
+type MaxSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+ InnerSeries ValuesProvider
+
+ maxValue *float64
+}
+
+// GetName returns the name of the time series.
+func (ms MaxSeries) GetName() string {
+ return ms.Name
+}
+
+// GetStyle returns the line style.
+func (ms MaxSeries) GetStyle() Style {
+ return ms.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (ms MaxSeries) GetYAxis() YAxisType {
+ return ms.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (ms MaxSeries) Len() int {
+ return ms.InnerSeries.Len()
+}
+
+// GetValues gets a value at a given index.
+func (ms *MaxSeries) GetValues(index int) (x, y float64) {
+ ms.ensureMaxValue()
+ x, _ = ms.InnerSeries.GetValues(index)
+ y = *ms.maxValue
+ return
+}
+
+// Render renders the series.
+func (ms *MaxSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := ms.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, ms)
+}
+
+func (ms *MaxSeries) ensureMaxValue() {
+ if ms.maxValue == nil {
+ maxValue := -math.MaxFloat64
+ var y float64
+ for x := 0; x < ms.InnerSeries.Len(); x++ {
+ _, y = ms.InnerSeries.GetValues(x)
+ if y > maxValue {
+ maxValue = y
+ }
+ }
+ ms.maxValue = &maxValue
+ }
+}
+
+// Validate validates the series.
+func (ms *MaxSeries) Validate() error {
+ if ms.InnerSeries == nil {
+ return fmt.Errorf("max series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+
+
package chart
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "math"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/util"
+)
+
+const (
+ _pi = math.Pi
+ _pi2 = math.Pi / 2.0
+ _pi4 = math.Pi / 4.0
+)
+
+// PieChart is a chart that draws sections of a circle based on percentages.
+type PieChart struct {
+ Title string
+ TitleStyle Style
+
+ ColorPalette ColorPalette
+
+ Width int
+ Height int
+ DPI float64
+
+ Background Style
+ Canvas Style
+ SliceStyle Style
+
+ Font *truetype.Font
+ defaultFont *truetype.Font
+
+ Values []Value
+ Elements []Renderable
+}
+
+// GetDPI returns the dpi for the chart.
+func (pc PieChart) GetDPI(defaults ...float64) float64 {
+ if pc.DPI == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultDPI
+ }
+ return pc.DPI
+}
+
+// GetFont returns the text font.
+func (pc PieChart) GetFont() *truetype.Font {
+ if pc.Font == nil {
+ return pc.defaultFont
+ }
+ return pc.Font
+}
+
+// GetWidth returns the chart width or the default value.
+func (pc PieChart) GetWidth() int {
+ if pc.Width == 0 {
+ return DefaultChartWidth
+ }
+ return pc.Width
+}
+
+// GetHeight returns the chart height or the default value.
+func (pc PieChart) GetHeight() int {
+ if pc.Height == 0 {
+ return DefaultChartWidth
+ }
+ return pc.Height
+}
+
+// Render renders the chart with the given renderer to the given io.Writer.
+func (pc PieChart) Render(rp RendererProvider, w io.Writer) error {
+ if len(pc.Values) == 0 {
+ return errors.New("please provide at least one value")
+ }
+
+ r, err := rp(pc.GetWidth(), pc.GetHeight())
+ if err != nil {
+ return err
+ }
+
+ if pc.Font == nil {
+ defaultFont, err := GetDefaultFont()
+ if err != nil {
+ return err
+ }
+ pc.defaultFont = defaultFont
+ }
+ r.SetDPI(pc.GetDPI(DefaultDPI))
+
+ canvasBox := pc.getDefaultCanvasBox()
+ canvasBox = pc.getCircleAdjustedCanvasBox(canvasBox)
+
+ pc.drawBackground(r)
+ pc.drawCanvas(r, canvasBox)
+
+ finalValues, err := pc.finalizeValues(pc.Values)
+ if err != nil {
+ return err
+ }
+ pc.drawSlices(r, canvasBox, finalValues)
+ pc.drawTitle(r)
+ for _, a := range pc.Elements {
+ a(r, canvasBox, pc.styleDefaultsElements())
+ }
+
+ return r.Save(w)
+}
+
+func (pc PieChart) drawBackground(r Renderer) {
+ Draw.Box(r, Box{
+ Right: pc.GetWidth(),
+ Bottom: pc.GetHeight(),
+ }, pc.getBackgroundStyle())
+}
+
+func (pc PieChart) drawCanvas(r Renderer, canvasBox Box) {
+ Draw.Box(r, canvasBox, pc.getCanvasStyle())
+}
+
+func (pc PieChart) drawTitle(r Renderer) {
+ if len(pc.Title) > 0 && pc.TitleStyle.Show {
+ Draw.TextWithin(r, pc.Title, pc.Box(), pc.styleDefaultsTitle())
+ }
+}
+
+func (pc PieChart) drawSlices(r Renderer, canvasBox Box, values []Value) {
+ cx, cy := canvasBox.Center()
+ diameter := util.Math.MinInt(canvasBox.Width(), canvasBox.Height())
+ radius := float64(diameter >> 1)
+ labelRadius := (radius * 2.0) / 3.0
+
+ // draw the pie slices
+ var rads, delta, delta2, total float64
+ var lx, ly int
+
+ if len(values) == 1 {
+ pc.stylePieChartValue(0).WriteToRenderer(r)
+ r.MoveTo(cx, cy)
+ r.Circle(radius, cx, cy)
+ } else {
+ for index, v := range values {
+ v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
+
+ r.MoveTo(cx, cy)
+ rads = util.Math.PercentToRadians(total)
+ delta = util.Math.PercentToRadians(v.Value)
+
+ r.ArcTo(cx, cy, radius, radius, rads, delta)
+
+ r.LineTo(cx, cy)
+ r.Close()
+ r.FillStroke()
+ total = total + v.Value
+ }
+ }
+
+ // draw the labels
+ total = 0
+ for index, v := range values {
+ v.Style.InheritFrom(pc.stylePieChartValue(index)).WriteToRenderer(r)
+ if len(v.Label) > 0 {
+ delta2 = util.Math.PercentToRadians(total + (v.Value / 2.0))
+ delta2 = util.Math.RadianAdd(delta2, _pi2)
+ lx, ly = util.Math.CirclePoint(cx, cy, labelRadius, delta2)
+
+ tb := r.MeasureText(v.Label)
+ lx = lx - (tb.Width() >> 1)
+ ly = ly + (tb.Height() >> 1)
+
+ if lx < 0 {
+ lx = 0
+ }
+ if ly < 0 {
+ lx = 0
+ }
+
+ r.Text(v.Label, lx, ly)
+ }
+ total = total + v.Value
+ }
+}
+
+func (pc PieChart) finalizeValues(values []Value) ([]Value, error) {
+ finalValues := Values(values).Normalize()
+ if len(finalValues) == 0 {
+ return nil, fmt.Errorf("pie chart must contain at least (1) non-zero value")
+ }
+ return finalValues, nil
+}
+
+func (pc PieChart) getDefaultCanvasBox() Box {
+ return pc.Box()
+}
+
+func (pc PieChart) getCircleAdjustedCanvasBox(canvasBox Box) Box {
+ circleDiameter := util.Math.MinInt(canvasBox.Width(), canvasBox.Height())
+
+ square := Box{
+ Right: circleDiameter,
+ Bottom: circleDiameter,
+ }
+
+ return canvasBox.Fit(square)
+}
+
+func (pc PieChart) getBackgroundStyle() Style {
+ return pc.Background.InheritFrom(pc.styleDefaultsBackground())
+}
+
+func (pc PieChart) getCanvasStyle() Style {
+ return pc.Canvas.InheritFrom(pc.styleDefaultsCanvas())
+}
+
+func (pc PieChart) styleDefaultsCanvas() Style {
+ return Style{
+ FillColor: pc.GetColorPalette().CanvasColor(),
+ StrokeColor: pc.GetColorPalette().CanvasStrokeColor(),
+ StrokeWidth: DefaultStrokeWidth,
+ }
+}
+
+func (pc PieChart) styleDefaultsPieChartValue() Style {
+ return Style{
+ StrokeColor: pc.GetColorPalette().TextColor(),
+ StrokeWidth: 5.0,
+ FillColor: pc.GetColorPalette().TextColor(),
+ }
+}
+
+func (pc PieChart) stylePieChartValue(index int) Style {
+ return pc.SliceStyle.InheritFrom(Style{
+ StrokeColor: ColorWhite,
+ StrokeWidth: 5.0,
+ FillColor: pc.GetColorPalette().GetSeriesColor(index),
+ FontSize: pc.getScaledFontSize(),
+ FontColor: pc.GetColorPalette().TextColor(),
+ Font: pc.GetFont(),
+ })
+}
+
+func (pc PieChart) getScaledFontSize() float64 {
+ effectiveDimension := util.Math.MinInt(pc.GetWidth(), pc.GetHeight())
+ if effectiveDimension >= 2048 {
+ return 48.0
+ } else if effectiveDimension >= 1024 {
+ return 24.0
+ } else if effectiveDimension > 512 {
+ return 18.0
+ } else if effectiveDimension > 256 {
+ return 12.0
+ }
+ return 10.0
+}
+
+func (pc PieChart) styleDefaultsBackground() Style {
+ return Style{
+ FillColor: pc.GetColorPalette().BackgroundColor(),
+ StrokeColor: pc.GetColorPalette().BackgroundStrokeColor(),
+ StrokeWidth: DefaultStrokeWidth,
+ }
+}
+
+func (pc PieChart) styleDefaultsElements() Style {
+ return Style{
+ Font: pc.GetFont(),
+ }
+}
+
+func (pc PieChart) styleDefaultsTitle() Style {
+ return pc.TitleStyle.InheritFrom(Style{
+ FontColor: pc.GetColorPalette().TextColor(),
+ Font: pc.GetFont(),
+ FontSize: pc.getTitleFontSize(),
+ TextHorizontalAlign: TextHorizontalAlignCenter,
+ TextVerticalAlign: TextVerticalAlignTop,
+ TextWrap: TextWrapWord,
+ })
+}
+
+func (pc PieChart) getTitleFontSize() float64 {
+ effectiveDimension := util.Math.MinInt(pc.GetWidth(), pc.GetHeight())
+ if effectiveDimension >= 2048 {
+ return 48
+ } else if effectiveDimension >= 1024 {
+ return 24
+ } else if effectiveDimension >= 512 {
+ return 18
+ } else if effectiveDimension >= 256 {
+ return 12
+ }
+ return 10
+}
+
+// GetColorPalette returns the color palette for the chart.
+func (pc PieChart) GetColorPalette() ColorPalette {
+ if pc.ColorPalette != nil {
+ return pc.ColorPalette
+ }
+ return AlternateColorPalette
+}
+
+// Box returns the chart bounds as a box.
+func (pc PieChart) Box() Box {
+ dpr := pc.Background.Padding.GetRight(DefaultBackgroundPadding.Right)
+ dpb := pc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom)
+
+ return Box{
+ Top: pc.Background.Padding.GetTop(DefaultBackgroundPadding.Top),
+ Left: pc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left),
+ Right: pc.GetWidth() - dpr,
+ Bottom: pc.GetHeight() - dpb,
+ }
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "math"
+
+ "github.com/wcharczuk/go-chart/matrix"
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*PolynomialRegressionSeries)(nil)
+ _ FirstValuesProvider = (*PolynomialRegressionSeries)(nil)
+ _ LastValuesProvider = (*PolynomialRegressionSeries)(nil)
+)
+
+// PolynomialRegressionSeries implements a polynomial regression over a given
+// inner series.
+type PolynomialRegressionSeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+
+ Limit int
+ Offset int
+ Degree int
+ InnerSeries ValuesProvider
+
+ coeffs []float64
+}
+
+// GetName returns the name of the time series.
+func (prs PolynomialRegressionSeries) GetName() string {
+ return prs.Name
+}
+
+// GetStyle returns the line style.
+func (prs PolynomialRegressionSeries) GetStyle() Style {
+ return prs.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (prs PolynomialRegressionSeries) GetYAxis() YAxisType {
+ return prs.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (prs PolynomialRegressionSeries) Len() int {
+ return util.Math.MinInt(prs.GetLimit(), prs.InnerSeries.Len()-prs.GetOffset())
+}
+
+// GetLimit returns the window size.
+func (prs PolynomialRegressionSeries) GetLimit() int {
+ if prs.Limit == 0 {
+ return prs.InnerSeries.Len()
+ }
+ return prs.Limit
+}
+
+// GetEndIndex returns the effective limit end.
+func (prs PolynomialRegressionSeries) GetEndIndex() int {
+ windowEnd := prs.GetOffset() + prs.GetLimit()
+ innerSeriesLastIndex := prs.InnerSeries.Len() - 1
+ return util.Math.MinInt(windowEnd, innerSeriesLastIndex)
+}
+
+// GetOffset returns the data offset.
+func (prs PolynomialRegressionSeries) GetOffset() int {
+ if prs.Offset == 0 {
+ return 0
+ }
+ return prs.Offset
+}
+
+// Validate validates the series.
+func (prs *PolynomialRegressionSeries) Validate() error {
+ if prs.InnerSeries == nil {
+ return fmt.Errorf("linear regression series requires InnerSeries to be set")
+ }
+
+ endIndex := prs.GetEndIndex()
+ if endIndex >= prs.InnerSeries.Len() {
+ return fmt.Errorf("invalid window; inner series has length %d but end index is %d", prs.InnerSeries.Len(), endIndex)
+ }
+
+ return nil
+}
+
+// GetValues returns the series value for a given index.
+func (prs *PolynomialRegressionSeries) GetValues(index int) (x, y float64) {
+ if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
+ return
+ }
+
+ if prs.coeffs == nil {
+ coeffs, err := prs.computeCoefficients()
+ if err != nil {
+ panic(err)
+ }
+ prs.coeffs = coeffs
+ }
+
+ offset := prs.GetOffset()
+ effectiveIndex := util.Math.MinInt(index+offset, prs.InnerSeries.Len())
+ x, y = prs.InnerSeries.GetValues(effectiveIndex)
+ y = prs.apply(x)
+ return
+}
+
+// GetFirstValues computes the first poly regression value.
+func (prs *PolynomialRegressionSeries) GetFirstValues() (x, y float64) {
+ if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
+ return
+ }
+ if prs.coeffs == nil {
+ coeffs, err := prs.computeCoefficients()
+ if err != nil {
+ panic(err)
+ }
+ prs.coeffs = coeffs
+ }
+ x, y = prs.InnerSeries.GetValues(0)
+ y = prs.apply(x)
+ return
+}
+
+// GetLastValues computes the last poly regression value.
+func (prs *PolynomialRegressionSeries) GetLastValues() (x, y float64) {
+ if prs.InnerSeries == nil || prs.InnerSeries.Len() == 0 {
+ return
+ }
+ if prs.coeffs == nil {
+ coeffs, err := prs.computeCoefficients()
+ if err != nil {
+ panic(err)
+ }
+ prs.coeffs = coeffs
+ }
+ endIndex := prs.GetEndIndex()
+ x, y = prs.InnerSeries.GetValues(endIndex)
+ y = prs.apply(x)
+ return
+}
+
+func (prs *PolynomialRegressionSeries) apply(v float64) (out float64) {
+ for index, coeff := range prs.coeffs {
+ out = out + (coeff * math.Pow(v, float64(index)))
+ }
+ return
+}
+
+func (prs *PolynomialRegressionSeries) computeCoefficients() ([]float64, error) {
+ xvalues, yvalues := prs.values()
+ return matrix.Poly(xvalues, yvalues, prs.Degree)
+}
+
+func (prs *PolynomialRegressionSeries) values() (xvalues, yvalues []float64) {
+ startIndex := prs.GetOffset()
+ endIndex := prs.GetEndIndex()
+
+ xvalues = make([]float64, endIndex-startIndex)
+ yvalues = make([]float64, endIndex-startIndex)
+
+ for index := startIndex; index < endIndex; index++ {
+ x, y := prs.InnerSeries.GetValues(index)
+ xvalues[index-startIndex] = x
+ yvalues[index-startIndex] = y
+ }
+
+ return
+}
+
+// Render renders the series.
+func (prs *PolynomialRegressionSeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := prs.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, prs)
+}
+
+
+
package chart
+
+import (
+ "image"
+ "image/png"
+ "io"
+ "math"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/drawing"
+ "github.com/wcharczuk/go-chart/util"
+)
+
+// PNG returns a new png/raster renderer.
+func PNG(width, height int) (Renderer, error) {
+ i := image.NewRGBA(image.Rect(0, 0, width, height))
+ gc, err := drawing.NewRasterGraphicContext(i)
+ if err == nil {
+ return &rasterRenderer{
+ i: i,
+ gc: gc,
+ }, nil
+ }
+ return nil, err
+}
+
+// rasterRenderer renders chart commands to a bitmap.
+type rasterRenderer struct {
+ i *image.RGBA
+ gc *drawing.RasterGraphicContext
+
+ rotateRadians *float64
+
+ s Style
+}
+
+func (rr *rasterRenderer) ResetStyle() {
+ rr.s = Style{Font: rr.s.Font}
+ rr.ClearTextRotation()
+}
+
+// GetDPI returns the dpi.
+func (rr *rasterRenderer) GetDPI() float64 {
+ return rr.gc.GetDPI()
+}
+
+// SetDPI implements the interface method.
+func (rr *rasterRenderer) SetDPI(dpi float64) {
+ rr.gc.SetDPI(dpi)
+}
+
+// SetClassName implements the interface method. However, PNGs have no classes.
+func (vr *rasterRenderer) SetClassName(_ string) {
+}
+
+// SetStrokeColor implements the interface method.
+func (rr *rasterRenderer) SetStrokeColor(c drawing.Color) {
+ rr.s.StrokeColor = c
+}
+
+// SetLineWidth implements the interface method.
+func (rr *rasterRenderer) SetStrokeWidth(width float64) {
+ rr.s.StrokeWidth = width
+}
+
+// StrokeDashArray sets the stroke dash array.
+func (rr *rasterRenderer) SetStrokeDashArray(dashArray []float64) {
+ rr.s.StrokeDashArray = dashArray
+}
+
+// SetFillColor implements the interface method.
+func (rr *rasterRenderer) SetFillColor(c drawing.Color) {
+ rr.s.FillColor = c
+}
+
+// MoveTo implements the interface method.
+func (rr *rasterRenderer) MoveTo(x, y int) {
+ rr.gc.MoveTo(float64(x), float64(y))
+}
+
+// LineTo implements the interface method.
+func (rr *rasterRenderer) LineTo(x, y int) {
+ rr.gc.LineTo(float64(x), float64(y))
+}
+
+// QuadCurveTo implements the interface method.
+func (rr *rasterRenderer) QuadCurveTo(cx, cy, x, y int) {
+ rr.gc.QuadCurveTo(float64(cx), float64(cy), float64(x), float64(y))
+}
+
+// ArcTo implements the interface method.
+func (rr *rasterRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
+ rr.gc.ArcTo(float64(cx), float64(cy), rx, ry, startAngle, delta)
+}
+
+// Close implements the interface method.
+func (rr *rasterRenderer) Close() {
+ rr.gc.Close()
+}
+
+// Stroke implements the interface method.
+func (rr *rasterRenderer) Stroke() {
+ rr.gc.SetStrokeColor(rr.s.StrokeColor)
+ rr.gc.SetLineWidth(rr.s.StrokeWidth)
+ rr.gc.SetLineDash(rr.s.StrokeDashArray, 0)
+ rr.gc.Stroke()
+}
+
+// Fill implements the interface method.
+func (rr *rasterRenderer) Fill() {
+ rr.gc.SetFillColor(rr.s.FillColor)
+ rr.gc.Fill()
+}
+
+// FillStroke implements the interface method.
+func (rr *rasterRenderer) FillStroke() {
+ rr.gc.SetFillColor(rr.s.FillColor)
+ rr.gc.SetStrokeColor(rr.s.StrokeColor)
+ rr.gc.SetLineWidth(rr.s.StrokeWidth)
+ rr.gc.SetLineDash(rr.s.StrokeDashArray, 0)
+ rr.gc.FillStroke()
+}
+
+// Circle fully draws a circle at a given point but does not apply the fill or stroke.
+func (rr *rasterRenderer) Circle(radius float64, x, y int) {
+ xf := float64(x)
+ yf := float64(y)
+
+ rr.gc.MoveTo(xf-radius, yf) //9
+ rr.gc.QuadCurveTo(xf-radius, yf-radius, xf, yf-radius) //12
+ rr.gc.QuadCurveTo(xf+radius, yf-radius, xf+radius, yf) //3
+ rr.gc.QuadCurveTo(xf+radius, yf+radius, xf, yf+radius) //6
+ rr.gc.QuadCurveTo(xf-radius, yf+radius, xf-radius, yf) //9
+}
+
+// SetFont implements the interface method.
+func (rr *rasterRenderer) SetFont(f *truetype.Font) {
+ rr.s.Font = f
+}
+
+// SetFontSize implements the interface method.
+func (rr *rasterRenderer) SetFontSize(size float64) {
+ rr.s.FontSize = size
+}
+
+// SetFontColor implements the interface method.
+func (rr *rasterRenderer) SetFontColor(c drawing.Color) {
+ rr.s.FontColor = c
+}
+
+// Text implements the interface method.
+func (rr *rasterRenderer) Text(body string, x, y int) {
+ xf, yf := rr.getCoords(x, y)
+ rr.gc.SetFont(rr.s.Font)
+ rr.gc.SetFontSize(rr.s.FontSize)
+ rr.gc.SetFillColor(rr.s.FontColor)
+ rr.gc.CreateStringPath(body, float64(xf), float64(yf))
+ rr.gc.Fill()
+}
+
+// MeasureText returns the height and width in pixels of a string.
+func (rr *rasterRenderer) MeasureText(body string) Box {
+ rr.gc.SetFont(rr.s.Font)
+ rr.gc.SetFontSize(rr.s.FontSize)
+ rr.gc.SetFillColor(rr.s.FontColor)
+ l, t, r, b, err := rr.gc.GetStringBounds(body)
+ if err != nil {
+ return Box{}
+ }
+ if l < 0 {
+ r = r - l // equivalent to r+(-1*l)
+ l = 0
+ }
+ if t < 0 {
+ b = b - t
+ t = 0
+ }
+
+ if l > 0 {
+ r = r + l
+ l = 0
+ }
+
+ if t > 0 {
+ b = b + t
+ t = 0
+ }
+
+ textBox := Box{
+ Top: int(math.Ceil(t)),
+ Left: int(math.Ceil(l)),
+ Right: int(math.Ceil(r)),
+ Bottom: int(math.Ceil(b)),
+ }
+ if rr.rotateRadians == nil {
+ return textBox
+ }
+
+ return textBox.Corners().Rotate(util.Math.RadiansToDegrees(*rr.rotateRadians)).Box()
+}
+
+// SetTextRotation sets a text rotation.
+func (rr *rasterRenderer) SetTextRotation(radians float64) {
+ rr.rotateRadians = &radians
+}
+
+func (rr *rasterRenderer) getCoords(x, y int) (xf, yf int) {
+ if rr.rotateRadians == nil {
+ xf = x
+ yf = y
+ return
+ }
+
+ rr.gc.Translate(float64(x), float64(y))
+ rr.gc.Rotate(*rr.rotateRadians)
+ return
+}
+
+// ClearTextRotation clears text rotation.
+func (rr *rasterRenderer) ClearTextRotation() {
+ rr.gc.SetMatrixTransform(drawing.NewIdentityMatrix())
+ rr.rotateRadians = nil
+}
+
+// Save implements the interface method.
+func (rr *rasterRenderer) Save(w io.Writer) error {
+ if typed, isTyped := w.(RGBACollector); isTyped {
+ typed.SetRGBA(rr.i)
+ return nil
+ }
+ return png.Encode(w, rr.i)
+}
+
+
+
package seq
+
+// NewArray creates a new array.
+func NewArray(values ...float64) Array {
+ return Array(values)
+}
+
+// Array is a wrapper for an array of floats that implements `ValuesProvider`.
+type Array []float64
+
+// Len returns the value provider length.
+func (a Array) Len() int {
+ return len(a)
+}
+
+// GetValue returns the value at a given index.
+func (a Array) GetValue(index int) float64 {
+ return a[index]
+}
+
+
+
package seq
+
+import (
+ "fmt"
+ "strings"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+const (
+ bufferMinimumGrow = 4
+ bufferShrinkThreshold = 32
+ bufferGrowFactor = 200
+ bufferDefaultCapacity = 4
+)
+
+var (
+ emptyArray = make([]float64, 0)
+)
+
+// NewBuffer creates a new value buffer with an optional set of values.
+func NewBuffer(values ...float64) *Buffer {
+ var tail int
+ array := make([]float64, util.Math.MaxInt(len(values), bufferDefaultCapacity))
+ if len(values) > 0 {
+ copy(array, values)
+ tail = len(values)
+ }
+ return &Buffer{
+ array: array,
+ head: 0,
+ tail: tail,
+ size: len(values),
+ }
+}
+
+// NewBufferWithCapacity creates a new ValueBuffer pre-allocated with the given capacity.
+func NewBufferWithCapacity(capacity int) *Buffer {
+ return &Buffer{
+ array: make([]float64, capacity),
+ head: 0,
+ tail: 0,
+ size: 0,
+ }
+}
+
+// Buffer is a fifo datastructure that is backed by a pre-allocated array.
+// Instead of allocating a whole new node object for each element, array elements are re-used (which saves GC churn).
+// Enqueue can be O(n), Dequeue is generally O(1).
+// Buffer implements `seq.Provider`
+type Buffer struct {
+ array []float64
+ head int
+ tail int
+ size int
+}
+
+// Len returns the length of the Buffer (as it is currently populated).
+// Actual memory footprint may be different.
+func (b *Buffer) Len() int {
+ return b.size
+}
+
+// GetValue implements seq provider.
+func (b *Buffer) GetValue(index int) float64 {
+ effectiveIndex := (b.head + index) % len(b.array)
+ return b.array[effectiveIndex]
+}
+
+// Capacity returns the total size of the Buffer, including empty elements.
+func (b *Buffer) Capacity() int {
+ return len(b.array)
+}
+
+// SetCapacity sets the capacity of the Buffer.
+func (b *Buffer) SetCapacity(capacity int) {
+ newArray := make([]float64, capacity)
+ if b.size > 0 {
+ if b.head < b.tail {
+ arrayCopy(b.array, b.head, newArray, 0, b.size)
+ } else {
+ arrayCopy(b.array, b.head, newArray, 0, len(b.array)-b.head)
+ arrayCopy(b.array, 0, newArray, len(b.array)-b.head, b.tail)
+ }
+ }
+ b.array = newArray
+ b.head = 0
+ if b.size == capacity {
+ b.tail = 0
+ } else {
+ b.tail = b.size
+ }
+}
+
+// Clear removes all objects from the Buffer.
+func (b *Buffer) Clear() {
+ b.array = make([]float64, bufferDefaultCapacity)
+ b.head = 0
+ b.tail = 0
+ b.size = 0
+}
+
+// Enqueue adds an element to the "back" of the Buffer.
+func (b *Buffer) Enqueue(value float64) {
+ if b.size == len(b.array) {
+ newCapacity := int(len(b.array) * int(bufferGrowFactor/100))
+ if newCapacity < (len(b.array) + bufferMinimumGrow) {
+ newCapacity = len(b.array) + bufferMinimumGrow
+ }
+ b.SetCapacity(newCapacity)
+ }
+
+ b.array[b.tail] = value
+ b.tail = (b.tail + 1) % len(b.array)
+ b.size++
+}
+
+// Dequeue removes the first element from the RingBuffer.
+func (b *Buffer) Dequeue() float64 {
+ if b.size == 0 {
+ return 0
+ }
+
+ removed := b.array[b.head]
+ b.head = (b.head + 1) % len(b.array)
+ b.size--
+ return removed
+}
+
+// Peek returns but does not remove the first element.
+func (b *Buffer) Peek() float64 {
+ if b.size == 0 {
+ return 0
+ }
+ return b.array[b.head]
+}
+
+// PeekBack returns but does not remove the last element.
+func (b *Buffer) PeekBack() float64 {
+ if b.size == 0 {
+ return 0
+ }
+ if b.tail == 0 {
+ return b.array[len(b.array)-1]
+ }
+ return b.array[b.tail-1]
+}
+
+// TrimExcess resizes the capacity of the buffer to better fit the contents.
+func (b *Buffer) TrimExcess() {
+ threshold := float64(len(b.array)) * 0.9
+ if b.size < int(threshold) {
+ b.SetCapacity(b.size)
+ }
+}
+
+// Array returns the ring buffer, in order, as an array.
+func (b *Buffer) Array() Array {
+ newArray := make([]float64, b.size)
+
+ if b.size == 0 {
+ return newArray
+ }
+
+ if b.head < b.tail {
+ arrayCopy(b.array, b.head, newArray, 0, b.size)
+ } else {
+ arrayCopy(b.array, b.head, newArray, 0, len(b.array)-b.head)
+ arrayCopy(b.array, 0, newArray, len(b.array)-b.head, b.tail)
+ }
+
+ return Array(newArray)
+}
+
+// Each calls the consumer for each element in the buffer.
+func (b *Buffer) Each(mapfn func(int, float64)) {
+ if b.size == 0 {
+ return
+ }
+
+ var index int
+ if b.head < b.tail {
+ for cursor := b.head; cursor < b.tail; cursor++ {
+ mapfn(index, b.array[cursor])
+ index++
+ }
+ } else {
+ for cursor := b.head; cursor < len(b.array); cursor++ {
+ mapfn(index, b.array[cursor])
+ index++
+ }
+ for cursor := 0; cursor < b.tail; cursor++ {
+ mapfn(index, b.array[cursor])
+ index++
+ }
+ }
+}
+
+// String returns a string representation for value buffers.
+func (b *Buffer) String() string {
+ var values []string
+ for _, elem := range b.Array() {
+ values = append(values, fmt.Sprintf("%v", elem))
+ }
+ return strings.Join(values, " <= ")
+}
+
+// --------------------------------------------------------------------------------
+// Util methods
+// --------------------------------------------------------------------------------
+
+func arrayClear(source []float64, index, length int) {
+ for x := 0; x < length; x++ {
+ absoluteIndex := x + index
+ source[absoluteIndex] = 0
+ }
+}
+
+func arrayCopy(source []float64, sourceIndex int, destination []float64, destinationIndex, length int) {
+ for x := 0; x < length; x++ {
+ from := sourceIndex + x
+ to := destinationIndex + x
+
+ destination[to] = source[from]
+ }
+}
+
+
+
package seq
+
+// Range returns the array values of a linear seq with a given start, end and optional step.
+func Range(start, end float64) []float64 {
+ return Seq{NewLinear().WithStart(start).WithEnd(end).WithStep(1.0)}.Array()
+}
+
+// RangeWithStep returns the array values of a linear seq with a given start, end and optional step.
+func RangeWithStep(start, end, step float64) []float64 {
+ return Seq{NewLinear().WithStart(start).WithEnd(end).WithStep(step)}.Array()
+}
+
+// NewLinear returns a new linear generator.
+func NewLinear() *Linear {
+ return &Linear{step: 1.0}
+}
+
+// Linear is a stepwise generator.
+type Linear struct {
+ start float64
+ end float64
+ step float64
+}
+
+// Start returns the start value.
+func (lg Linear) Start() float64 {
+ return lg.start
+}
+
+// End returns the end value.
+func (lg Linear) End() float64 {
+ return lg.end
+}
+
+// Step returns the step value.
+func (lg Linear) Step() float64 {
+ return lg.step
+}
+
+// Len returns the number of elements in the seq.
+func (lg Linear) Len() int {
+ if lg.start < lg.end {
+ return int((lg.end-lg.start)/lg.step) + 1
+ }
+ return int((lg.start-lg.end)/lg.step) + 1
+}
+
+// GetValue returns the value at a given index.
+func (lg Linear) GetValue(index int) float64 {
+ fi := float64(index)
+ if lg.start < lg.end {
+ return lg.start + (fi * lg.step)
+ }
+ return lg.start - (fi * lg.step)
+}
+
+// WithStart sets the start and returns the linear generator.
+func (lg *Linear) WithStart(start float64) *Linear {
+ lg.start = start
+ return lg
+}
+
+// WithEnd sets the end and returns the linear generator.
+func (lg *Linear) WithEnd(end float64) *Linear {
+ lg.end = end
+ return lg
+}
+
+// WithStep sets the step and returns the linear generator.
+func (lg *Linear) WithStep(step float64) *Linear {
+ lg.step = step
+ return lg
+}
+
+
+
package seq
+
+import (
+ "math"
+ "math/rand"
+ "time"
+)
+
+// RandomValues returns an array of random values.
+func RandomValues(count int) []float64 {
+ return Seq{NewRandom().WithLen(count)}.Array()
+}
+
+// RandomValuesWithMax returns an array of random values with a given average.
+func RandomValuesWithMax(count int, max float64) []float64 {
+ return Seq{NewRandom().WithMax(max).WithLen(count)}.Array()
+}
+
+// NewRandom creates a new random seq.
+func NewRandom() *Random {
+ return &Random{
+ rnd: rand.New(rand.NewSource(time.Now().Unix())),
+ }
+}
+
+// Random is a random number seq generator.
+type Random struct {
+ rnd *rand.Rand
+ max *float64
+ min *float64
+ len *int
+}
+
+// Len returns the number of elements that will be generated.
+func (r *Random) Len() int {
+ if r.len != nil {
+ return *r.len
+ }
+ return math.MaxInt32
+}
+
+// GetValue returns the value.
+func (r *Random) GetValue(_ int) float64 {
+ if r.min != nil && r.max != nil {
+ var delta float64
+
+ if *r.max > *r.min {
+ delta = *r.max - *r.min
+ } else {
+ delta = *r.min - *r.max
+ }
+
+ return *r.min + (r.rnd.Float64() * delta)
+ } else if r.max != nil {
+ return r.rnd.Float64() * *r.max
+ } else if r.min != nil {
+ return *r.min + (r.rnd.Float64())
+ }
+ return r.rnd.Float64()
+}
+
+// WithLen sets a maximum len
+func (r *Random) WithLen(length int) *Random {
+ r.len = &length
+ return r
+}
+
+// Min returns the minimum value.
+func (r Random) Min() *float64 {
+ return r.min
+}
+
+// WithMin sets the scale and returns the Random.
+func (r *Random) WithMin(min float64) *Random {
+ r.min = &min
+ return r
+}
+
+// Max returns the maximum value.
+func (r Random) Max() *float64 {
+ return r.max
+}
+
+// WithMax sets the average and returns the Random.
+func (r *Random) WithMax(max float64) *Random {
+ r.max = &max
+ return r
+}
+
+
+
package seq
+
+import (
+ "math"
+ "sort"
+)
+
+// New wraps a provider with a seq.
+func New(provider Provider) Seq {
+ return Seq{Provider: provider}
+}
+
+// Values returns a new seq composed of a given set of values.
+func Values(values ...float64) Seq {
+ return Seq{Provider: Array(values)}
+}
+
+// Provider is a provider for values for a seq.
+type Provider interface {
+ Len() int
+ GetValue(int) float64
+}
+
+// Seq is a utility wrapper for seq providers.
+type Seq struct {
+ Provider
+}
+
+// Array enumerates the seq into a slice.
+func (s Seq) Array() (output []float64) {
+ if s.Len() == 0 {
+ return
+ }
+
+ output = make([]float64, s.Len())
+ for i := 0; i < s.Len(); i++ {
+ output[i] = s.GetValue(i)
+ }
+ return
+}
+
+// Each applies the `mapfn` to all values in the value provider.
+func (s Seq) Each(mapfn func(int, float64)) {
+ for i := 0; i < s.Len(); i++ {
+ mapfn(i, s.GetValue(i))
+ }
+}
+
+// Map applies the `mapfn` to all values in the value provider,
+// returning a new seq.
+func (s Seq) Map(mapfn func(i int, v float64) float64) Seq {
+ output := make([]float64, s.Len())
+ for i := 0; i < s.Len(); i++ {
+ mapfn(i, s.GetValue(i))
+ }
+ return Seq{Array(output)}
+}
+
+// FoldLeft collapses a seq from left to right.
+func (s Seq) FoldLeft(mapfn func(i int, v0, v float64) float64) (v0 float64) {
+ if s.Len() == 0 {
+ return 0
+ }
+
+ if s.Len() == 1 {
+ return s.GetValue(0)
+ }
+
+ v0 = s.GetValue(0)
+ for i := 1; i < s.Len(); i++ {
+ v0 = mapfn(i, v0, s.GetValue(i))
+ }
+ return
+}
+
+// FoldRight collapses a seq from right to left.
+func (s Seq) FoldRight(mapfn func(i int, v0, v float64) float64) (v0 float64) {
+ if s.Len() == 0 {
+ return 0
+ }
+
+ if s.Len() == 1 {
+ return s.GetValue(0)
+ }
+
+ v0 = s.GetValue(s.Len() - 1)
+ for i := s.Len() - 2; i >= 0; i-- {
+ v0 = mapfn(i, v0, s.GetValue(i))
+ }
+ return
+}
+
+// Min returns the minimum value in the seq.
+func (s Seq) Min() float64 {
+ if s.Len() == 0 {
+ return 0
+ }
+ min := s.GetValue(0)
+ var value float64
+ for i := 1; i < s.Len(); i++ {
+ value = s.GetValue(i)
+ if value < min {
+ min = value
+ }
+ }
+ return min
+}
+
+// Max returns the maximum value in the seq.
+func (s Seq) Max() float64 {
+ if s.Len() == 0 {
+ return 0
+ }
+ max := s.GetValue(0)
+ var value float64
+ for i := 1; i < s.Len(); i++ {
+ value = s.GetValue(i)
+ if value > max {
+ max = value
+ }
+ }
+ return max
+}
+
+// MinMax returns the minimum and the maximum in one pass.
+func (s Seq) MinMax() (min, max float64) {
+ if s.Len() == 0 {
+ return
+ }
+ min = s.GetValue(0)
+ max = min
+ var value float64
+ for i := 1; i < s.Len(); i++ {
+ value = s.GetValue(i)
+ if value < min {
+ min = value
+ }
+ if value > max {
+ max = value
+ }
+ }
+ return
+}
+
+// Sort returns the seq sorted in ascending order.
+// This fully enumerates the seq.
+func (s Seq) Sort() Seq {
+ if s.Len() == 0 {
+ return s
+ }
+ values := s.Array()
+ sort.Float64s(values)
+ return Seq{Provider: Array(values)}
+}
+
+// Median returns the median or middle value in the sorted seq.
+func (s Seq) Median() (median float64) {
+ l := s.Len()
+ if l == 0 {
+ return
+ }
+
+ sorted := s.Sort()
+ if l%2 == 0 {
+ v0 := sorted.GetValue(l/2 - 1)
+ v1 := sorted.GetValue(l/2 + 1)
+ median = (v0 + v1) / 2
+ } else {
+ median = float64(sorted.GetValue(l << 1))
+ }
+
+ return
+}
+
+// Sum adds all the elements of a series together.
+func (s Seq) Sum() (accum float64) {
+ if s.Len() == 0 {
+ return 0
+ }
+
+ for i := 0; i < s.Len(); i++ {
+ accum += s.GetValue(i)
+ }
+ return
+}
+
+// Average returns the float average of the values in the buffer.
+func (s Seq) Average() float64 {
+ if s.Len() == 0 {
+ return 0
+ }
+
+ return s.Sum() / float64(s.Len())
+}
+
+// Variance computes the variance of the buffer.
+func (s Seq) Variance() float64 {
+ if s.Len() == 0 {
+ return 0
+ }
+
+ m := s.Average()
+ var variance, v float64
+ for i := 0; i < s.Len(); i++ {
+ v = s.GetValue(i)
+ variance += (v - m) * (v - m)
+ }
+
+ return variance / float64(s.Len())
+}
+
+// StdDev returns the standard deviation.
+func (s Seq) StdDev() float64 {
+ if s.Len() == 0 {
+ return 0
+ }
+
+ return math.Pow(s.Variance(), 0.5)
+}
+
+//Percentile finds the relative standing in a slice of floats.
+// `percent` should be given on the interval [0,1.0).
+func (s Seq) Percentile(percent float64) (percentile float64) {
+ l := s.Len()
+ if l == 0 {
+ return 0
+ }
+
+ if percent < 0 || percent > 1.0 {
+ panic("percent out of range [0.0, 1.0)")
+ }
+
+ sorted := s.Sort()
+ index := percent * float64(l)
+ if index == float64(int64(index)) {
+ i := f64i(index)
+ ci := sorted.GetValue(i - 1)
+ c := sorted.GetValue(i)
+ percentile = (ci + c) / 2.0
+ } else {
+ i := f64i(index)
+ percentile = sorted.GetValue(i)
+ }
+
+ return percentile
+}
+
+// Normalize maps every value to the interval [0, 1.0].
+func (s Seq) Normalize() Seq {
+ min, max := s.MinMax()
+
+ delta := max - min
+ output := make([]float64, s.Len())
+ for i := 0; i < s.Len(); i++ {
+ output[i] = (s.GetValue(i) - min) / delta
+ }
+
+ return Seq{Provider: Array(output)}
+}
+
+
+
package seq
+
+import (
+ "time"
+
+ "github.com/wcharczuk/go-chart/util"
+)
+
+// Time is a utility singleton with helper functions for time seq generation.
+var Time timeSequence
+
+type timeSequence struct{}
+
+// Days generates a seq of timestamps by day, from -days to today.
+func (ts timeSequence) Days(days int) []time.Time {
+ var values []time.Time
+ for day := days; day >= 0; day-- {
+ values = append(values, time.Now().AddDate(0, 0, -day))
+ }
+ return values
+}
+
+func (ts timeSequence) Hours(start time.Time, totalHours int) []time.Time {
+ times := make([]time.Time, totalHours)
+
+ last := start
+ for i := 0; i < totalHours; i++ {
+ times[i] = last
+ last = last.Add(time.Hour)
+ }
+
+ return times
+}
+
+// HoursFilled adds zero values for the data bounded by the start and end of the xdata array.
+func (ts timeSequence) HoursFilled(xdata []time.Time, ydata []float64) ([]time.Time, []float64) {
+ start, end := util.Time.StartAndEnd(xdata...)
+ totalHours := util.Time.DiffHours(start, end)
+
+ finalTimes := ts.Hours(start, totalHours+1)
+ finalValues := make([]float64, totalHours+1)
+
+ var hoursFromStart int
+ for i, xd := range xdata {
+ hoursFromStart = util.Time.DiffHours(start, xd)
+ finalValues[hoursFromStart] = ydata[i]
+ }
+
+ return finalTimes, finalValues
+}
+
+
+
package seq
+
+import (
+ "time"
+
+ "github.com/wcharczuk/go-chart/util"
+)
+
+// Assert types implement interfaces.
+var (
+ _ Provider = (*Times)(nil)
+)
+
+// Times are an array of times.
+// It wraps the array with methods that implement `seq.Provider`.
+type Times []time.Time
+
+// Array returns the times to an array.
+func (t Times) Array() []time.Time {
+ return []time.Time(t)
+}
+
+// Len returns the length of the array.
+func (t Times) Len() int {
+ return len(t)
+}
+
+// GetValue returns a value at an index as a time.
+func (t Times) GetValue(index int) float64 {
+ return util.Time.ToFloat64(t[index])
+}
+
+
+
package seq
+
+import "math"
+
+func round(input float64, places int) (rounded float64) {
+ if math.IsNaN(input) {
+ return 0.0
+ }
+
+ sign := 1.0
+ if input < 0 {
+ sign = -1
+ input *= -1
+ }
+
+ precision := math.Pow(10, float64(places))
+ digit := input * precision
+ _, decimal := math.Modf(digit)
+
+ if decimal >= 0.5 {
+ rounded = math.Ceil(digit)
+ } else {
+ rounded = math.Floor(digit)
+ }
+
+ return rounded / precision * sign
+}
+
+func f64i(value float64) int {
+ r := round(value, 0)
+ return int(r)
+}
+
+
+
package chart
+
+import (
+ "fmt"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+const (
+ // DefaultSimpleMovingAveragePeriod is the default number of values to average.
+ DefaultSimpleMovingAveragePeriod = 16
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*SMASeries)(nil)
+ _ FirstValuesProvider = (*SMASeries)(nil)
+ _ LastValuesProvider = (*SMASeries)(nil)
+)
+
+// SMASeries is a computed series.
+type SMASeries struct {
+ Name string
+ Style Style
+ YAxis YAxisType
+
+ Period int
+ InnerSeries ValuesProvider
+}
+
+// GetName returns the name of the time series.
+func (sma SMASeries) GetName() string {
+ return sma.Name
+}
+
+// GetStyle returns the line style.
+func (sma SMASeries) GetStyle() Style {
+ return sma.Style
+}
+
+// GetYAxis returns which YAxis the series draws on.
+func (sma SMASeries) GetYAxis() YAxisType {
+ return sma.YAxis
+}
+
+// Len returns the number of elements in the series.
+func (sma SMASeries) Len() int {
+ return sma.InnerSeries.Len()
+}
+
+// GetPeriod returns the window size.
+func (sma SMASeries) GetPeriod(defaults ...int) int {
+ if sma.Period == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultSimpleMovingAveragePeriod
+ }
+ return sma.Period
+}
+
+// GetValues gets a value at a given index.
+func (sma SMASeries) GetValues(index int) (x, y float64) {
+ if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
+ return
+ }
+ px, _ := sma.InnerSeries.GetValues(index)
+ x = px
+ y = sma.getAverage(index)
+ return
+}
+
+// GetFirstValues computes the first moving average value.
+func (sma SMASeries) GetFirstValues() (x, y float64) {
+ if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
+ return
+ }
+ px, _ := sma.InnerSeries.GetValues(0)
+ x = px
+ y = sma.getAverage(0)
+ return
+}
+
+// GetLastValues computes the last moving average value but walking back window size samples,
+// and recomputing the last moving average chunk.
+func (sma SMASeries) GetLastValues() (x, y float64) {
+ if sma.InnerSeries == nil || sma.InnerSeries.Len() == 0 {
+ return
+ }
+ seriesLen := sma.InnerSeries.Len()
+ px, _ := sma.InnerSeries.GetValues(seriesLen - 1)
+ x = px
+ y = sma.getAverage(seriesLen - 1)
+ return
+}
+
+func (sma SMASeries) getAverage(index int) float64 {
+ period := sma.GetPeriod()
+ floor := util.Math.MaxInt(0, index-period)
+ var accum float64
+ var count float64
+ for x := index; x >= floor; x-- {
+ _, vy := sma.InnerSeries.GetValues(x)
+ accum += vy
+ count += 1.0
+ }
+ return accum / count
+}
+
+// Render renders the series.
+func (sma SMASeries) Render(r Renderer, canvasBox Box, xrange, yrange Range, defaults Style) {
+ style := sma.Style.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, sma)
+}
+
+// Validate validates the series.
+func (sma SMASeries) Validate() error {
+ if sma.InnerSeries == nil {
+ return fmt.Errorf("sma series requires InnerSeries to be set")
+ }
+ return nil
+}
+
+
+
package chart
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "math"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/seq"
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// StackedBar is a bar within a StackedBarChart.
+type StackedBar struct {
+ Name string
+ Width int
+ Values []Value
+}
+
+// GetWidth returns the width of the bar.
+func (sb StackedBar) GetWidth() int {
+ if sb.Width == 0 {
+ return 50
+ }
+ return sb.Width
+}
+
+// StackedBarChart is a chart that draws sections of a bar based on percentages.
+type StackedBarChart struct {
+ Title string
+ TitleStyle Style
+
+ ColorPalette ColorPalette
+
+ Width int
+ Height int
+ DPI float64
+
+ Background Style
+ Canvas Style
+
+ XAxis Style
+ YAxis Style
+
+ BarSpacing int
+
+ Font *truetype.Font
+ defaultFont *truetype.Font
+
+ Bars []StackedBar
+ Elements []Renderable
+}
+
+// GetDPI returns the dpi for the chart.
+func (sbc StackedBarChart) GetDPI(defaults ...float64) float64 {
+ if sbc.DPI == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultDPI
+ }
+ return sbc.DPI
+}
+
+// GetFont returns the text font.
+func (sbc StackedBarChart) GetFont() *truetype.Font {
+ if sbc.Font == nil {
+ return sbc.defaultFont
+ }
+ return sbc.Font
+}
+
+// GetWidth returns the chart width or the default value.
+func (sbc StackedBarChart) GetWidth() int {
+ if sbc.Width == 0 {
+ return DefaultChartWidth
+ }
+ return sbc.Width
+}
+
+// GetHeight returns the chart height or the default value.
+func (sbc StackedBarChart) GetHeight() int {
+ if sbc.Height == 0 {
+ return DefaultChartWidth
+ }
+ return sbc.Height
+}
+
+// GetBarSpacing returns the spacing between bars.
+func (sbc StackedBarChart) GetBarSpacing() int {
+ if sbc.BarSpacing == 0 {
+ return 100
+ }
+ return sbc.BarSpacing
+}
+
+// Render renders the chart with the given renderer to the given io.Writer.
+func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error {
+ if len(sbc.Bars) == 0 {
+ return errors.New("please provide at least one bar")
+ }
+
+ r, err := rp(sbc.GetWidth(), sbc.GetHeight())
+ if err != nil {
+ return err
+ }
+
+ if sbc.Font == nil {
+ defaultFont, err := GetDefaultFont()
+ if err != nil {
+ return err
+ }
+ sbc.defaultFont = defaultFont
+ }
+ r.SetDPI(sbc.GetDPI(DefaultDPI))
+
+ canvasBox := sbc.getAdjustedCanvasBox(r, sbc.getDefaultCanvasBox())
+ sbc.drawCanvas(r, canvasBox)
+ sbc.drawBars(r, canvasBox)
+ sbc.drawXAxis(r, canvasBox)
+ sbc.drawYAxis(r, canvasBox)
+
+ sbc.drawTitle(r)
+ for _, a := range sbc.Elements {
+ a(r, canvasBox, sbc.styleDefaultsElements())
+ }
+
+ return r.Save(w)
+}
+
+func (sbc StackedBarChart) drawCanvas(r Renderer, canvasBox Box) {
+ Draw.Box(r, canvasBox, sbc.getCanvasStyle())
+}
+
+func (sbc StackedBarChart) drawBars(r Renderer, canvasBox Box) {
+ xoffset := canvasBox.Left
+ for _, bar := range sbc.Bars {
+ sbc.drawBar(r, canvasBox, xoffset, bar)
+ xoffset += (sbc.GetBarSpacing() + bar.GetWidth())
+ }
+}
+
+func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar StackedBar) int {
+ barSpacing2 := sbc.GetBarSpacing() >> 1
+ bxl := xoffset + barSpacing2
+ bxr := bxl + bar.GetWidth()
+
+ normalizedBarComponents := Values(bar.Values).Normalize()
+ yoffset := canvasBox.Top
+ for index, bv := range normalizedBarComponents {
+ barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Height())))
+ barBox := Box{
+ Top: yoffset,
+ Left: bxl,
+ Right: bxr,
+ Bottom: util.Math.MinInt(yoffset+barHeight, canvasBox.Bottom-DefaultStrokeWidth),
+ }
+ Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)))
+ yoffset += barHeight
+ }
+
+ return bxr
+}
+
+func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) {
+ if sbc.XAxis.Show {
+ axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes())
+ axisStyle.WriteToRenderer(r)
+
+ r.MoveTo(canvasBox.Left, canvasBox.Bottom)
+ r.LineTo(canvasBox.Right, canvasBox.Bottom)
+ r.Stroke()
+
+ r.MoveTo(canvasBox.Left, canvasBox.Bottom)
+ r.LineTo(canvasBox.Left, canvasBox.Bottom+DefaultVerticalTickHeight)
+ r.Stroke()
+
+ cursor := canvasBox.Left
+ for _, bar := range sbc.Bars {
+
+ barLabelBox := Box{
+ Top: canvasBox.Bottom + DefaultXAxisMargin,
+ Left: cursor,
+ Right: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
+ Bottom: sbc.GetHeight(),
+ }
+ if len(bar.Name) > 0 {
+ Draw.TextWithin(r, bar.Name, barLabelBox, axisStyle)
+ }
+ axisStyle.WriteToRenderer(r)
+ r.MoveTo(barLabelBox.Right, canvasBox.Bottom)
+ r.LineTo(barLabelBox.Right, canvasBox.Bottom+DefaultVerticalTickHeight)
+ r.Stroke()
+ cursor += bar.GetWidth() + sbc.GetBarSpacing()
+ }
+ }
+}
+
+func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) {
+ if sbc.YAxis.Show {
+ axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsAxes())
+ axisStyle.WriteToRenderer(r)
+ r.MoveTo(canvasBox.Right, canvasBox.Top)
+ r.LineTo(canvasBox.Right, canvasBox.Bottom)
+ r.Stroke()
+
+ r.MoveTo(canvasBox.Right, canvasBox.Bottom)
+ r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, canvasBox.Bottom)
+ r.Stroke()
+
+ ticks := seq.RangeWithStep(0.0, 1.0, 0.2)
+ for _, t := range ticks {
+ axisStyle.GetStrokeOptions().WriteToRenderer(r)
+ ty := canvasBox.Bottom - int(t*float64(canvasBox.Height()))
+ r.MoveTo(canvasBox.Right, ty)
+ r.LineTo(canvasBox.Right+DefaultHorizontalTickWidth, ty)
+ r.Stroke()
+
+ axisStyle.GetTextOptions().WriteToRenderer(r)
+ text := fmt.Sprintf("%0.0f%%", t*100)
+
+ tb := r.MeasureText(text)
+ Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle)
+ }
+
+ }
+}
+
+func (sbc StackedBarChart) drawTitle(r Renderer) {
+ if len(sbc.Title) > 0 && sbc.TitleStyle.Show {
+ r.SetFont(sbc.TitleStyle.GetFont(sbc.GetFont()))
+ r.SetFontColor(sbc.TitleStyle.GetFontColor(sbc.GetColorPalette().TextColor()))
+ titleFontSize := sbc.TitleStyle.GetFontSize(DefaultTitleFontSize)
+ r.SetFontSize(titleFontSize)
+
+ textBox := r.MeasureText(sbc.Title)
+
+ textWidth := textBox.Width()
+ textHeight := textBox.Height()
+
+ titleX := (sbc.GetWidth() >> 1) - (textWidth >> 1)
+ titleY := sbc.TitleStyle.Padding.GetTop(DefaultTitleTop) + textHeight
+
+ r.Text(sbc.Title, titleX, titleY)
+ }
+}
+
+func (sbc StackedBarChart) getCanvasStyle() Style {
+ return sbc.Canvas.InheritFrom(sbc.styleDefaultsCanvas())
+}
+
+func (sbc StackedBarChart) styleDefaultsCanvas() Style {
+ return Style{
+ FillColor: sbc.GetColorPalette().CanvasColor(),
+ StrokeColor: sbc.GetColorPalette().CanvasStrokeColor(),
+ StrokeWidth: DefaultCanvasStrokeWidth,
+ }
+}
+
+// GetColorPalette returns the color palette for the chart.
+func (sbc StackedBarChart) GetColorPalette() ColorPalette {
+ if sbc.ColorPalette != nil {
+ return sbc.ColorPalette
+ }
+ return AlternateColorPalette
+}
+
+func (sbc StackedBarChart) getDefaultCanvasBox() Box {
+ return sbc.Box()
+}
+
+func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box {
+ var totalWidth int
+ for _, bar := range sbc.Bars {
+ totalWidth += bar.GetWidth() + sbc.GetBarSpacing()
+ }
+
+ if sbc.XAxis.Show {
+ xaxisHeight := DefaultVerticalTickHeight
+
+ axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes())
+ axisStyle.WriteToRenderer(r)
+
+ cursor := canvasBox.Left
+ for _, bar := range sbc.Bars {
+ if len(bar.Name) > 0 {
+ barLabelBox := Box{
+ Top: canvasBox.Bottom + DefaultXAxisMargin,
+ Left: cursor,
+ Right: cursor + bar.GetWidth() + sbc.GetBarSpacing(),
+ Bottom: sbc.GetHeight(),
+ }
+ lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle)
+ linesBox := Text.MeasureLines(r, lines, axisStyle)
+
+ xaxisHeight = util.Math.MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), xaxisHeight)
+ }
+ }
+ return Box{
+ Top: canvasBox.Top,
+ Left: canvasBox.Left,
+ Right: canvasBox.Left + totalWidth,
+ Bottom: sbc.GetHeight() - xaxisHeight,
+ }
+ }
+ return Box{
+ Top: canvasBox.Top,
+ Left: canvasBox.Left,
+ Right: canvasBox.Left + totalWidth,
+ Bottom: canvasBox.Bottom,
+ }
+
+}
+
+// Box returns the chart bounds as a box.
+func (sbc StackedBarChart) Box() Box {
+ dpr := sbc.Background.Padding.GetRight(10)
+ dpb := sbc.Background.Padding.GetBottom(50)
+
+ return Box{
+ Top: sbc.Background.Padding.GetTop(20),
+ Left: sbc.Background.Padding.GetLeft(20),
+ Right: sbc.GetWidth() - dpr,
+ Bottom: sbc.GetHeight() - dpb,
+ }
+}
+
+func (sbc StackedBarChart) styleDefaultsStackedBarValue(index int) Style {
+ return Style{
+ StrokeColor: sbc.GetColorPalette().GetSeriesColor(index),
+ StrokeWidth: 3.0,
+ FillColor: sbc.GetColorPalette().GetSeriesColor(index),
+ }
+}
+
+func (sbc StackedBarChart) styleDefaultsTitle() Style {
+ return sbc.TitleStyle.InheritFrom(Style{
+ FontColor: DefaultTextColor,
+ Font: sbc.GetFont(),
+ FontSize: sbc.getTitleFontSize(),
+ TextHorizontalAlign: TextHorizontalAlignCenter,
+ TextVerticalAlign: TextVerticalAlignTop,
+ TextWrap: TextWrapWord,
+ })
+}
+
+func (sbc StackedBarChart) getTitleFontSize() float64 {
+ effectiveDimension := util.Math.MinInt(sbc.GetWidth(), sbc.GetHeight())
+ if effectiveDimension >= 2048 {
+ return 48
+ } else if effectiveDimension >= 1024 {
+ return 24
+ } else if effectiveDimension >= 512 {
+ return 18
+ } else if effectiveDimension >= 256 {
+ return 12
+ }
+ return 10
+}
+
+func (sbc StackedBarChart) styleDefaultsAxes() Style {
+ return Style{
+ StrokeColor: DefaultAxisColor,
+ Font: sbc.GetFont(),
+ FontSize: DefaultAxisFontSize,
+ FontColor: DefaultAxisColor,
+ TextHorizontalAlign: TextHorizontalAlignCenter,
+ TextVerticalAlign: TextVerticalAlignTop,
+ TextWrap: TextWrapWord,
+ }
+}
+func (sbc StackedBarChart) styleDefaultsElements() Style {
+ return Style{
+ Font: sbc.GetFont(),
+ }
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/drawing"
+ "github.com/wcharczuk/go-chart/util"
+)
+
+const (
+ // Disabled indicates if the value should be interpreted as set intentionally to zero.
+ // this is because golang optionals aren't here yet.
+ Disabled = -1
+)
+
+// StyleShow is a prebuilt style with the `Show` property set to true.
+func StyleShow() Style {
+ return Style{
+ Show: true,
+ }
+}
+
+// StyleTextDefaults returns a style for drawing outside a
+// chart context.
+func StyleTextDefaults() Style {
+ font, _ := GetDefaultFont()
+ return Style{
+ Show: true,
+ Font: font,
+ FontColor: DefaultTextColor,
+ FontSize: DefaultTitleFontSize,
+ }
+}
+
+// Style is a simple style set.
+type Style struct {
+ Show bool
+ Padding Box
+
+ ClassName string
+
+ StrokeWidth float64
+ StrokeColor drawing.Color
+ StrokeDashArray []float64
+
+ DotColor drawing.Color
+ DotWidth float64
+
+ DotWidthProvider SizeProvider
+ DotColorProvider DotColorProvider
+
+ FillColor drawing.Color
+
+ FontSize float64
+ FontColor drawing.Color
+ Font *truetype.Font
+
+ TextHorizontalAlign TextHorizontalAlign
+ TextVerticalAlign TextVerticalAlign
+ TextWrap TextWrap
+ TextLineSpacing int
+ TextRotationDegrees float64 //0 is unset or normal
+}
+
+// IsZero returns if the object is set or not.
+func (s Style) IsZero() bool {
+ return s.StrokeColor.IsZero() &&
+ s.StrokeWidth == 0 &&
+ s.DotColor.IsZero() &&
+ s.DotWidth == 0 &&
+ s.FillColor.IsZero() &&
+ s.FontColor.IsZero() &&
+ s.FontSize == 0 &&
+ s.Font == nil &&
+ s.ClassName == ""
+}
+
+// String returns a text representation of the style.
+func (s Style) String() string {
+ if s.IsZero() {
+ return "{}"
+ }
+
+ var output []string
+ if s.Show {
+ output = []string{"\"show\": true"}
+ } else {
+ output = []string{"\"show\": false"}
+ }
+
+ if s.ClassName != "" {
+ output = append(output, fmt.Sprintf("\"class_name\": %s", s.ClassName))
+ } else {
+ output = append(output, "\"class_name\": null")
+ }
+
+ if !s.Padding.IsZero() {
+ output = append(output, fmt.Sprintf("\"padding\": %s", s.Padding.String()))
+ } else {
+ output = append(output, "\"padding\": null")
+ }
+
+ if s.StrokeWidth >= 0 {
+ output = append(output, fmt.Sprintf("\"stroke_width\": %0.2f", s.StrokeWidth))
+ } else {
+ output = append(output, "\"stroke_width\": null")
+ }
+
+ if !s.StrokeColor.IsZero() {
+ output = append(output, fmt.Sprintf("\"stroke_color\": %s", s.StrokeColor.String()))
+ } else {
+ output = append(output, "\"stroke_color\": null")
+ }
+
+ if len(s.StrokeDashArray) > 0 {
+ var elements []string
+ for _, v := range s.StrokeDashArray {
+ elements = append(elements, fmt.Sprintf("%.2f", v))
+ }
+ dashArray := strings.Join(elements, ", ")
+ output = append(output, fmt.Sprintf("\"stroke_dash_array\": [%s]", dashArray))
+ } else {
+ output = append(output, "\"stroke_dash_array\": null")
+ }
+
+ if s.DotWidth >= 0 {
+ output = append(output, fmt.Sprintf("\"dot_width\": %0.2f", s.DotWidth))
+ } else {
+ output = append(output, "\"dot_width\": null")
+ }
+
+ if !s.DotColor.IsZero() {
+ output = append(output, fmt.Sprintf("\"dot_color\": %s", s.DotColor.String()))
+ } else {
+ output = append(output, "\"dot_color\": null")
+ }
+
+ if !s.FillColor.IsZero() {
+ output = append(output, fmt.Sprintf("\"fill_color\": %s", s.FillColor.String()))
+ } else {
+ output = append(output, "\"fill_color\": null")
+ }
+
+ if s.FontSize != 0 {
+ output = append(output, fmt.Sprintf("\"font_size\": \"%0.2fpt\"", s.FontSize))
+ } else {
+ output = append(output, "\"font_size\": null")
+ }
+
+ if !s.FontColor.IsZero() {
+ output = append(output, fmt.Sprintf("\"font_color\": %s", s.FontColor.String()))
+ } else {
+ output = append(output, "\"font_color\": null")
+ }
+
+ if s.Font != nil {
+ output = append(output, fmt.Sprintf("\"font\": \"%s\"", s.Font.Name(truetype.NameIDFontFamily)))
+ } else {
+ output = append(output, "\"font_color\": null")
+ }
+
+ return "{" + strings.Join(output, ", ") + "}"
+}
+
+func (s Style) GetClassName(defaults ...string) string {
+ if s.ClassName == "" {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return ""
+ }
+ return s.ClassName
+}
+
+// GetStrokeColor returns the stroke color.
+func (s Style) GetStrokeColor(defaults ...drawing.Color) drawing.Color {
+ if s.StrokeColor.IsZero() {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return drawing.ColorTransparent
+ }
+ return s.StrokeColor
+}
+
+// GetFillColor returns the fill color.
+func (s Style) GetFillColor(defaults ...drawing.Color) drawing.Color {
+ if s.FillColor.IsZero() {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return drawing.ColorTransparent
+ }
+ return s.FillColor
+}
+
+// GetDotColor returns the stroke color.
+func (s Style) GetDotColor(defaults ...drawing.Color) drawing.Color {
+ if s.DotColor.IsZero() {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return drawing.ColorTransparent
+ }
+ return s.DotColor
+}
+
+// GetStrokeWidth returns the stroke width.
+func (s Style) GetStrokeWidth(defaults ...float64) float64 {
+ if s.StrokeWidth == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultStrokeWidth
+ }
+ return s.StrokeWidth
+}
+
+// GetDotWidth returns the dot width for scatter plots.
+func (s Style) GetDotWidth(defaults ...float64) float64 {
+ if s.DotWidth == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultDotWidth
+ }
+ return s.DotWidth
+}
+
+// GetStrokeDashArray returns the stroke dash array.
+func (s Style) GetStrokeDashArray(defaults ...[]float64) []float64 {
+ if len(s.StrokeDashArray) == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return nil
+ }
+ return s.StrokeDashArray
+}
+
+// GetFontSize gets the font size.
+func (s Style) GetFontSize(defaults ...float64) float64 {
+ if s.FontSize == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultFontSize
+ }
+ return s.FontSize
+}
+
+// GetFontColor gets the font size.
+func (s Style) GetFontColor(defaults ...drawing.Color) drawing.Color {
+ if s.FontColor.IsZero() {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return drawing.ColorTransparent
+ }
+ 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
+}
+
+// GetTextHorizontalAlign returns the horizontal alignment.
+func (s Style) GetTextHorizontalAlign(defaults ...TextHorizontalAlign) TextHorizontalAlign {
+ if s.TextHorizontalAlign == TextHorizontalAlignUnset {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return TextHorizontalAlignUnset
+ }
+ 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 TextVerticalAlignUnset
+ }
+ 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 TextWrapUnset
+ }
+ return s.TextWrap
+}
+
+// GetTextLineSpacing returns the spacing in pixels between lines of text (vertically).
+func (s Style) GetTextLineSpacing(defaults ...int) int {
+ if s.TextLineSpacing == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return DefaultLineSpacing
+ }
+ return s.TextLineSpacing
+}
+
+// GetTextRotationDegrees returns the text rotation in degrees.
+func (s Style) GetTextRotationDegrees(defaults ...float64) float64 {
+ if s.TextRotationDegrees == 0 {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ }
+ return s.TextRotationDegrees
+}
+
+// WriteToRenderer passes the style's options to a renderer.
+func (s Style) WriteToRenderer(r Renderer) {
+ r.SetClassName(s.GetClassName())
+ r.SetStrokeColor(s.GetStrokeColor())
+ r.SetStrokeWidth(s.GetStrokeWidth())
+ r.SetStrokeDashArray(s.GetStrokeDashArray())
+ r.SetFillColor(s.GetFillColor())
+ r.SetFont(s.GetFont())
+ r.SetFontColor(s.GetFontColor())
+ r.SetFontSize(s.GetFontSize())
+
+ r.ClearTextRotation()
+ if s.GetTextRotationDegrees() != 0 {
+ r.SetTextRotation(util.Math.DegreesToRadians(s.GetTextRotationDegrees()))
+ }
+}
+
+// WriteDrawingOptionsToRenderer passes just the drawing style options to a renderer.
+func (s Style) WriteDrawingOptionsToRenderer(r Renderer) {
+ r.SetClassName(s.GetClassName())
+ 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.SetClassName(s.GetClassName())
+ 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.ClassName = s.GetClassName(defaults.ClassName)
+
+ final.StrokeColor = s.GetStrokeColor(defaults.StrokeColor)
+ final.StrokeWidth = s.GetStrokeWidth(defaults.StrokeWidth)
+ final.StrokeDashArray = s.GetStrokeDashArray(defaults.StrokeDashArray)
+
+ final.DotColor = s.GetDotColor(defaults.DotColor)
+ final.DotWidth = s.GetDotWidth(defaults.DotWidth)
+
+ final.DotWidthProvider = s.DotWidthProvider
+ final.DotColorProvider = s.DotColorProvider
+
+ final.FillColor = s.GetFillColor(defaults.FillColor)
+ final.FontColor = s.GetFontColor(defaults.FontColor)
+ 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)
+ final.TextLineSpacing = s.GetTextLineSpacing(defaults.TextLineSpacing)
+ final.TextRotationDegrees = s.GetTextRotationDegrees(defaults.TextRotationDegrees)
+
+ return
+}
+
+// GetStrokeOptions returns the stroke components.
+func (s Style) GetStrokeOptions() Style {
+ return Style{
+ ClassName: s.ClassName,
+ StrokeDashArray: s.StrokeDashArray,
+ StrokeColor: s.StrokeColor,
+ StrokeWidth: s.StrokeWidth,
+ }
+}
+
+// GetFillOptions returns the fill components.
+func (s Style) GetFillOptions() Style {
+ return Style{
+ ClassName: s.ClassName,
+ FillColor: s.FillColor,
+ }
+}
+
+// GetDotOptions returns the dot components.
+func (s Style) GetDotOptions() Style {
+ return Style{
+ ClassName: s.ClassName,
+ StrokeDashArray: nil,
+ FillColor: s.DotColor,
+ StrokeColor: s.DotColor,
+ StrokeWidth: 1.0,
+ }
+}
+
+// GetFillAndStrokeOptions returns the fill and stroke components.
+func (s Style) GetFillAndStrokeOptions() Style {
+ return Style{
+ ClassName: s.ClassName,
+ StrokeDashArray: s.StrokeDashArray,
+ FillColor: s.FillColor,
+ StrokeColor: s.StrokeColor,
+ StrokeWidth: s.StrokeWidth,
+ }
+}
+
+// GetTextOptions returns just the text components of the style.
+func (s Style) GetTextOptions() Style {
+ return Style{
+ ClassName: s.ClassName,
+ FontColor: s.FontColor,
+ FontSize: s.FontSize,
+ Font: s.Font,
+ TextHorizontalAlign: s.TextHorizontalAlign,
+ TextVerticalAlign: s.TextVerticalAlign,
+ TextWrap: s.TextWrap,
+ TextLineSpacing: s.TextLineSpacing,
+ TextRotationDegrees: s.TextRotationDegrees,
+ }
+}
+
+// ShouldDrawStroke tells drawing functions if they should draw the stroke.
+func (s Style) ShouldDrawStroke() bool {
+ return !s.StrokeColor.IsZero() && s.StrokeWidth > 0
+}
+
+// ShouldDrawDot tells drawing functions if they should draw the dot.
+func (s Style) ShouldDrawDot() bool {
+ return (!s.DotColor.IsZero() && s.DotWidth > 0) || s.DotColorProvider != nil || s.DotWidthProvider != nil
+}
+
+// ShouldDrawFill tells drawing functions if they should draw the stroke.
+func (s Style) ShouldDrawFill() bool {
+ return !s.FillColor.IsZero()
+}
+
+
+
package chart
+
+import (
+ "strings"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// 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) []string {
+ switch style.TextWrap {
+ 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) MeasureLines(r Renderer, lines []string, style Style) Box {
+ style.WriteTextOptionsToRenderer(r)
+ var output Box
+ for index, line := range lines {
+ lineBox := r.MeasureText(line)
+ output.Right = util.Math.MaxInt(lineBox.Right, output.Right)
+ output.Bottom += lineBox.Height()
+ if index < len(lines)-1 {
+ output.Bottom += +style.GetTextLineSpacing()
+ }
+ }
+ return output
+}
+
+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
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "math"
+ "strings"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// TicksProvider is a type that provides ticks.
+type TicksProvider interface {
+ GetTicks(r Renderer, defaults Style, vf ValueFormatter) []Tick
+}
+
+// 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
+}
+
+// String returns a string representation of the set of ticks.
+func (t Ticks) String() string {
+ var values []string
+ for i, tick := range t {
+ values = append(values, fmt.Sprintf("[%d: %s]", i, tick.Label))
+ }
+ return strings.Join(values, ", ")
+}
+
+// GenerateContinuousTicks generates a set of ticks.
+func GenerateContinuousTicks(r Renderer, ra Range, isVertical bool, style Style, vf ValueFormatter) []Tick {
+ if vf == nil {
+ vf = FloatValueFormatter
+ }
+
+ var ticks []Tick
+ min, max := ra.GetMin(), ra.GetMax()
+
+ if ra.IsDescending() {
+ ticks = append(ticks, Tick{
+ Value: max,
+ Label: vf(max),
+ })
+ } else {
+ ticks = append(ticks, Tick{
+ Value: min,
+ Label: vf(min),
+ })
+ }
+
+ minLabel := vf(min)
+ style.GetTextOptions().WriteToRenderer(r)
+ labelBox := r.MeasureText(minLabel)
+
+ var tickSize float64
+ if isVertical {
+ tickSize = float64(labelBox.Height() + DefaultMinimumTickVerticalSpacing)
+ } else {
+ tickSize = float64(labelBox.Width() + DefaultMinimumTickHorizontalSpacing)
+ }
+
+ domain := float64(ra.GetDomain())
+ domainRemainder := domain - (tickSize * 2)
+ intermediateTickCount := int(math.Floor(float64(domainRemainder) / float64(tickSize)))
+
+ rangeDelta := math.Abs(max - min)
+ tickStep := rangeDelta / float64(intermediateTickCount)
+
+ roundTo := util.Math.GetRoundToForDelta(rangeDelta) / 10
+ intermediateTickCount = util.Math.MinInt(intermediateTickCount, DefaultTickCountSanityCheck)
+
+ for x := 1; x < intermediateTickCount; x++ {
+ var tickValue float64
+ if ra.IsDescending() {
+ tickValue = max - util.Math.RoundUp(tickStep*float64(x), roundTo)
+ } else {
+ tickValue = min + util.Math.RoundUp(tickStep*float64(x), roundTo)
+ }
+ ticks = append(ticks, Tick{
+ Value: tickValue,
+ Label: vf(tickValue),
+ })
+ }
+
+ if ra.IsDescending() {
+ ticks = append(ticks, Tick{
+ Value: min,
+ Label: vf(min),
+ })
+ } else {
+ ticks = append(ticks, Tick{
+ Value: max,
+ Label: vf(max),
+ })
+ }
+
+ return ticks
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "time"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// Interface Assertions.
+var (
+ _ Series = (*TimeSeries)(nil)
+ _ FirstValuesProvider = (*TimeSeries)(nil)
+ _ LastValuesProvider = (*TimeSeries)(nil)
+ _ ValueFormatterProvider = (*TimeSeries)(nil)
+)
+
+// 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)
+}
+
+// GetValues gets x, y values at a given index.
+func (ts TimeSeries) GetValues(index int) (x, y float64) {
+ x = util.Time.ToFloat64(ts.XValues[index])
+ y = ts.YValues[index]
+ return
+}
+
+// GetFirstValues gets the first values.
+func (ts TimeSeries) GetFirstValues() (x, y float64) {
+ x = util.Time.ToFloat64(ts.XValues[0])
+ y = ts.YValues[0]
+ return
+}
+
+// GetLastValues gets the last values.
+func (ts TimeSeries) GetLastValues() (x, y float64) {
+ x = util.Time.ToFloat64(ts.XValues[len(ts.XValues)-1])
+ y = ts.YValues[len(ts.YValues)-1]
+ 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.InheritFrom(defaults)
+ Draw.LineSeries(r, canvasBox, xrange, yrange, style, ts)
+}
+
+// Validate validates the series.
+func (ts TimeSeries) Validate() error {
+ if len(ts.XValues) == 0 {
+ return fmt.Errorf("time series must have xvalues set")
+ }
+
+ if len(ts.YValues) == 0 {
+ return fmt.Errorf("time series must have yvalues set")
+ }
+ return nil
+}
+
+
+
package util
+
+import (
+ "time"
+)
+
+const (
+ // AllDaysMask is a bitmask of all the days of the week.
+ AllDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday) | 1<<uint(time.Saturday)
+ // WeekDaysMask is a bitmask of all the weekdays of the week.
+ WeekDaysMask = 1<<uint(time.Monday) | 1<<uint(time.Tuesday) | 1<<uint(time.Wednesday) | 1<<uint(time.Thursday) | 1<<uint(time.Friday)
+ //WeekendDaysMask is a bitmask of the weekend days of the week.
+ WeekendDaysMask = 1<<uint(time.Sunday) | 1<<uint(time.Saturday)
+)
+
+var (
+ // DaysOfWeek are all the time.Weekday in an array for utility purposes.
+ DaysOfWeek = []time.Weekday{
+ time.Sunday,
+ time.Monday,
+ time.Tuesday,
+ time.Wednesday,
+ time.Thursday,
+ time.Friday,
+ time.Saturday,
+ }
+
+ // WeekDays are the business time.Weekday in an array.
+ WeekDays = []time.Weekday{
+ time.Monday,
+ time.Tuesday,
+ time.Wednesday,
+ time.Thursday,
+ time.Friday,
+ }
+
+ // WeekendDays are the weekend time.Weekday in an array.
+ WeekendDays = []time.Weekday{
+ time.Sunday,
+ time.Saturday,
+ }
+
+ //Epoch is unix epoc saved for utility purposes.
+ Epoch = time.Unix(0, 0)
+)
+
+// Date contains utility functions that operate on dates.
+var Date date
+
+type date struct{}
+
+func (d date) MustEastern() *time.Location {
+ if eastern, err := d.Eastern(); err != nil {
+ panic(err)
+ } else {
+ return eastern
+ }
+}
+
+// Eastern returns the eastern timezone.
+func (d date) Eastern() (*time.Location, error) {
+ // Try POSIX
+ est, err := time.LoadLocation("America/New_York")
+ if err != nil {
+ // Try Windows
+ est, err = time.LoadLocation("EST")
+ if err != nil {
+ return nil, err
+ }
+ }
+ return est, nil
+}
+
+func (d date) MustPacific() *time.Location {
+ if pst, err := d.Pacific(); err != nil {
+ panic(err)
+ } else {
+ return pst
+ }
+}
+
+// Pacific returns the pacific timezone.
+func (d date) Pacific() (*time.Location, error) {
+ // Try POSIX
+ pst, err := time.LoadLocation("America/Los_Angeles")
+ if err != nil {
+ // Try Windows
+ pst, err = time.LoadLocation("PST")
+ if err != nil {
+ return nil, err
+ }
+ }
+ return pst, nil
+}
+
+// TimeUTC returns a new time.Time for the given clock components in UTC.
+// It is meant to be used with the `OnDate` function.
+func (d date) TimeUTC(hour, min, sec, nsec int) time.Time {
+ return time.Date(0, 0, 0, hour, min, sec, nsec, time.UTC)
+}
+
+// Time returns a new time.Time for the given clock components.
+// It is meant to be used with the `OnDate` function.
+func (d date) Time(hour, min, sec, nsec int, loc *time.Location) time.Time {
+ return time.Date(0, 0, 0, hour, min, sec, nsec, loc)
+}
+
+// DateUTC returns a new time.Time for the given date comonents at (noon) in UTC.
+func (d date) DateUTC(year, month, day int) time.Time {
+ return time.Date(year, time.Month(month), day, 12, 0, 0, 0, time.UTC)
+}
+
+// DateUTC returns a new time.Time for the given date comonents at (noon) in a given location.
+func (d date) Date(year, month, day int, loc *time.Location) time.Time {
+ return time.Date(year, time.Month(month), day, 12, 0, 0, 0, loc)
+}
+
+// OnDate returns the clock components of clock (hour,minute,second) on the date components of d.
+func (d date) OnDate(clock, date time.Time) time.Time {
+ tzAdjusted := date.In(clock.Location())
+ return time.Date(tzAdjusted.Year(), tzAdjusted.Month(), tzAdjusted.Day(), clock.Hour(), clock.Minute(), clock.Second(), clock.Nanosecond(), clock.Location())
+}
+
+// NoonOnDate is a shortcut for On(Time(12,0,0), cd) a.k.a. noon on a given date.
+func (d date) NoonOnDate(cd time.Time) time.Time {
+ return time.Date(cd.Year(), cd.Month(), cd.Day(), 12, 0, 0, 0, cd.Location())
+}
+
+// IsWeekDay returns if the day is a monday->friday.
+func (d date) IsWeekDay(day time.Weekday) bool {
+ return !d.IsWeekendDay(day)
+}
+
+// IsWeekendDay returns if the day is a monday->friday.
+func (d date) IsWeekendDay(day time.Weekday) bool {
+ return day == time.Saturday || day == time.Sunday
+}
+
+// Before returns if a timestamp is strictly before another date (ignoring hours, minutes etc.)
+func (d date) Before(before, reference time.Time) bool {
+ tzAdjustedBefore := before.In(reference.Location())
+ if tzAdjustedBefore.Year() < reference.Year() {
+ return true
+ }
+ if tzAdjustedBefore.Month() < reference.Month() {
+ return true
+ }
+ return tzAdjustedBefore.Year() == reference.Year() && tzAdjustedBefore.Month() == reference.Month() && tzAdjustedBefore.Day() < reference.Day()
+}
+
+const (
+ _secondsPerHour = 60 * 60
+ _secondsPerDay = 60 * 60 * 24
+)
+
+// NextDay returns the timestamp advanced a day.
+func (d date) NextDay(ts time.Time) time.Time {
+ return ts.AddDate(0, 0, 1)
+}
+
+// NextHour returns the next timestamp on the hour.
+func (d date) NextHour(ts time.Time) time.Time {
+ //advance a full hour ...
+ advanced := ts.Add(time.Hour)
+ minutes := time.Duration(advanced.Minute()) * time.Minute
+ final := advanced.Add(-minutes)
+ return time.Date(final.Year(), final.Month(), final.Day(), final.Hour(), 0, 0, 0, final.Location())
+}
+
+// NextDayOfWeek returns the next instance of a given weekday after a given timestamp.
+func (d date) NextDayOfWeek(after time.Time, dayOfWeek time.Weekday) time.Time {
+ afterWeekday := after.Weekday()
+ if afterWeekday == dayOfWeek {
+ return after.AddDate(0, 0, 7)
+ }
+
+ // 1 vs 5 ~ add 4 days
+ if afterWeekday < dayOfWeek {
+ dayDelta := int(dayOfWeek - afterWeekday)
+ return after.AddDate(0, 0, dayDelta)
+ }
+
+ // 5 vs 1, add 7-(5-1) ~ 3 days
+ dayDelta := 7 - int(afterWeekday-dayOfWeek)
+ return after.AddDate(0, 0, dayDelta)
+}
+
+
+
package util
+
+import (
+ "bufio"
+ "io"
+ "os"
+)
+
+var (
+ // File contains file utility functions
+ File = fileUtil{}
+)
+
+type fileUtil struct{}
+
+// ReadByLines reads a file and calls the handler for each line.
+func (fu fileUtil) ReadByLines(filePath string, handler func(line string) error) error {
+ var f *os.File
+ var err error
+ if f, err = os.Open(filePath); err == nil {
+ defer f.Close()
+ var line string
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line = scanner.Text()
+ err = handler(line)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return err
+
+}
+
+// ReadByChunks reads a file in `chunkSize` pieces, dispatched to the handler.
+func (fu fileUtil) ReadByChunks(filePath string, chunkSize int, handler func(line []byte) error) error {
+ var f *os.File
+ var err error
+ if f, err = os.Open(filePath); err == nil {
+ defer f.Close()
+
+ chunk := make([]byte, chunkSize)
+ for {
+ readBytes, err := f.Read(chunk)
+ if err == io.EOF {
+ break
+ }
+ readData := chunk[:readBytes]
+ err = handler(readData)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return err
+}
+
+
+
package util
+
+import (
+ "math"
+)
+
+const (
+ _pi = math.Pi
+ _2pi = 2 * math.Pi
+ _3pi4 = (3 * math.Pi) / 4.0
+ _4pi3 = (4 * math.Pi) / 3.0
+ _3pi2 = (3 * math.Pi) / 2.0
+ _5pi4 = (5 * math.Pi) / 4.0
+ _7pi4 = (7 * math.Pi) / 4.0
+ _pi2 = math.Pi / 2.0
+ _pi4 = math.Pi / 4.0
+ _d2r = (math.Pi / 180.0)
+ _r2d = (180.0 / math.Pi)
+)
+
+var (
+ // Math contains helper methods for common math operations.
+ Math = &mathUtil{}
+)
+
+type mathUtil struct{}
+
+// Max returns the maximum value of a group of floats.
+func (m mathUtil) Max(values ...float64) float64 {
+ if len(values) == 0 {
+ return 0
+ }
+ max := values[0]
+ for _, v := range values {
+ if max < v {
+ max = v
+ }
+ }
+ return max
+}
+
+// MinAndMax returns both the min and max in one pass.
+func (m mathUtil) MinAndMax(values ...float64) (min float64, max float64) {
+ if len(values) == 0 {
+ return
+ }
+ min = values[0]
+ max = values[0]
+ for _, v := range values[1:] {
+ if max < v {
+ max = v
+ }
+ if min > v {
+ min = v
+ }
+ }
+ return
+}
+
+// GetRoundToForDelta returns a `roundTo` value for a given delta.
+func (m mathUtil) GetRoundToForDelta(delta float64) float64 {
+ startingDeltaBound := math.Pow(10.0, 10.0)
+ for cursor := startingDeltaBound; cursor > 0; cursor /= 10.0 {
+ if delta > cursor {
+ return cursor / 10.0
+ }
+ }
+
+ return 0.0
+}
+
+// RoundUp rounds up to a given roundTo value.
+func (m mathUtil) RoundUp(value, roundTo float64) float64 {
+ if roundTo < 0.000000000000001 {
+ return value
+ }
+ d1 := math.Ceil(value / roundTo)
+ return d1 * roundTo
+}
+
+// RoundDown rounds down to a given roundTo value.
+func (m mathUtil) RoundDown(value, roundTo float64) float64 {
+ if roundTo < 0.000000000000001 {
+ return value
+ }
+ d1 := math.Floor(value / roundTo)
+ return d1 * roundTo
+}
+
+// Normalize returns a set of numbers on the interval [0,1] for a given set of inputs.
+// An example: 4,3,2,1 => 0.4, 0.3, 0.2, 0.1
+// Caveat; the total may be < 1.0; there are going to be issues with irrational numbers etc.
+func (m mathUtil) Normalize(values ...float64) []float64 {
+ var total float64
+ for _, v := range values {
+ total += v
+ }
+ output := make([]float64, len(values))
+ for x, v := range values {
+ output[x] = m.RoundDown(v/total, 0.0001)
+ }
+ return output
+}
+
+// MinInt returns the minimum of a set of integers.
+func (m mathUtil) MinInt(values ...int) int {
+ min := math.MaxInt32
+ for _, v := range values {
+ if v < min {
+ min = v
+ }
+ }
+ return min
+}
+
+// MaxInt returns the maximum of a set of integers.
+func (m mathUtil) MaxInt(values ...int) int {
+ max := math.MinInt32
+ for _, v := range values {
+ if v > max {
+ max = v
+ }
+ }
+ return max
+}
+
+// AbsInt returns the absolute value of an integer.
+func (m mathUtil) AbsInt(value int) int {
+ if value < 0 {
+ return -value
+ }
+ return value
+}
+
+// AbsInt64 returns the absolute value of a long.
+func (m mathUtil) AbsInt64(value int64) int64 {
+ if value < 0 {
+ return -value
+ }
+ return value
+}
+
+// Mean returns the mean of a set of values
+func (m mathUtil) Mean(values ...float64) float64 {
+ return m.Sum(values...) / float64(len(values))
+}
+
+// MeanInt returns the mean of a set of integer values.
+func (m mathUtil) MeanInt(values ...int) int {
+ return m.SumInt(values...) / len(values)
+}
+
+// Sum sums a set of values.
+func (m mathUtil) Sum(values ...float64) float64 {
+ var total float64
+ for _, v := range values {
+ total += v
+ }
+ return total
+}
+
+// SumInt sums a set of values.
+func (m mathUtil) SumInt(values ...int) int {
+ var total int
+ for _, v := range values {
+ total += v
+ }
+ return total
+}
+
+// PercentDifference computes the percentage difference between two values.
+// The formula is (v2-v1)/v1.
+func (m mathUtil) PercentDifference(v1, v2 float64) float64 {
+ if v1 == 0 {
+ return 0
+ }
+ return (v2 - v1) / v1
+}
+
+// DegreesToRadians returns degrees as radians.
+func (m mathUtil) DegreesToRadians(degrees float64) float64 {
+ return degrees * _d2r
+}
+
+// RadiansToDegrees translates a radian value to a degree value.
+func (m mathUtil) RadiansToDegrees(value float64) float64 {
+ return math.Mod(value, _2pi) * _r2d
+}
+
+// PercentToRadians converts a normalized value (0,1) to radians.
+func (m mathUtil) PercentToRadians(pct float64) float64 {
+ return m.DegreesToRadians(360.0 * pct)
+}
+
+// RadianAdd adds a delta to a base in radians.
+func (m mathUtil) RadianAdd(base, delta float64) float64 {
+ value := base + delta
+ if value > _2pi {
+ return math.Mod(value, _2pi)
+ } else if value < 0 {
+ return math.Mod(_2pi+value, _2pi)
+ }
+ return value
+}
+
+// DegreesAdd adds a delta to a base in radians.
+func (m mathUtil) DegreesAdd(baseDegrees, deltaDegrees float64) float64 {
+ value := baseDegrees + deltaDegrees
+ if value > _2pi {
+ return math.Mod(value, 360.0)
+ } else if value < 0 {
+ return math.Mod(360.0+value, 360.0)
+ }
+ return value
+}
+
+// DegreesToCompass returns the degree value in compass / clock orientation.
+func (m mathUtil) DegreesToCompass(deg float64) float64 {
+ return m.DegreesAdd(deg, -90.0)
+}
+
+// CirclePoint returns the absolute position of a circle diameter point given
+// by the radius and the theta.
+func (m mathUtil) CirclePoint(cx, cy int, radius, thetaRadians float64) (x, y int) {
+ x = cx + int(radius*math.Sin(thetaRadians))
+ y = cy - int(radius*math.Cos(thetaRadians))
+ return
+}
+
+func (m mathUtil) RotateCoordinate(cx, cy, x, y int, thetaRadians float64) (rx, ry int) {
+ tempX, tempY := float64(x-cx), float64(y-cy)
+ rotatedX := tempX*math.Cos(thetaRadians) - tempY*math.Sin(thetaRadians)
+ rotatedY := tempX*math.Sin(thetaRadians) + tempY*math.Cos(thetaRadians)
+ rx = int(rotatedX) + cx
+ ry = int(rotatedY) + cy
+ return
+}
+
+
+
package util
+
+import "time"
+
+var (
+ // Time contains time utility functions.
+ Time = timeUtil{}
+)
+
+type timeUtil struct{}
+
+// Millis returns the duration as milliseconds.
+func (tu timeUtil) Millis(d time.Duration) float64 {
+ return float64(d) / float64(time.Millisecond)
+}
+
+// TimeToFloat64 returns a float64 representation of a time.
+func (tu timeUtil) ToFloat64(t time.Time) float64 {
+ return float64(t.UnixNano())
+}
+
+// Float64ToTime returns a time from a float64.
+func (tu timeUtil) FromFloat64(tf float64) time.Time {
+ return time.Unix(0, int64(tf))
+}
+
+func (tu timeUtil) DiffDays(t1, t2 time.Time) (days int) {
+ t1n := t1.Unix()
+ t2n := t2.Unix()
+ var diff int64
+ if t1n > t2n {
+ diff = t1n - t2n //yields seconds
+ } else {
+ diff = t2n - t1n //yields seconds
+ }
+ return int(diff / (_secondsPerDay))
+}
+
+func (tu timeUtil) DiffHours(t1, t2 time.Time) (hours int) {
+ t1n := t1.Unix()
+ t2n := t2.Unix()
+ var diff int64
+ if t1n > t2n {
+ diff = t1n - t2n
+ } else {
+ diff = t2n - t1n
+ }
+ return int(diff / (_secondsPerHour))
+}
+
+// Start returns the earliest (min) time in a list of times.
+func (tu timeUtil) Start(times ...time.Time) time.Time {
+ if len(times) == 0 {
+ return time.Time{}
+ }
+
+ start := times[0]
+ for _, t := range times[1:] {
+ if t.Before(start) {
+ start = t
+ }
+ }
+ return start
+}
+
+// Start returns the earliest (min) time in a list of times.
+func (tu timeUtil) End(times ...time.Time) time.Time {
+ if len(times) == 0 {
+ return time.Time{}
+ }
+
+ end := times[0]
+ for _, t := range times[1:] {
+ if t.After(end) {
+ end = t
+ }
+ }
+ return end
+}
+
+// StartAndEnd returns the start and end of a given set of time in one pass.
+func (tu timeUtil) StartAndEnd(values ...time.Time) (start time.Time, end time.Time) {
+ if len(values) == 0 {
+ return
+ }
+
+ start = values[0]
+ end = values[0]
+
+ for _, v := range values[1:] {
+ if end.Before(v) {
+ end = v
+ }
+ if start.After(v) {
+ start = v
+ }
+ }
+ return
+}
+
+
+
package chart
+
+import util "github.com/wcharczuk/go-chart/util"
+
+// Value is a chart value.
+type Value struct {
+ Style Style
+ Label string
+ Value float64
+}
+
+// Values is an array of Value.
+type Values []Value
+
+// Values returns the values.
+func (vs Values) Values() []float64 {
+ values := make([]float64, len(vs))
+ for index, v := range vs {
+ values[index] = v.Value
+ }
+ return values
+}
+
+// ValuesNormalized returns normalized values.
+func (vs Values) ValuesNormalized() []float64 {
+ return util.Math.Normalize(vs.Values()...)
+}
+
+// Normalize returns the values normalized.
+func (vs Values) Normalize() []Value {
+ var output []Value
+ var total float64
+
+ for _, v := range vs {
+ total += v.Value
+ }
+
+ for _, v := range vs {
+ if v.Value > 0 {
+ output = append(output, Value{
+ Style: v.Style,
+ Label: v.Label,
+ Value: util.Math.RoundDown(v.Value/total, 0.0001),
+ })
+ }
+ }
+ return output
+}
+
+// Value2 is a two axis value.
+type Value2 struct {
+ Style Style
+ Label string
+ XValue, YValue float64
+}
+
+
+
package chart
+
+import (
+ "fmt"
+ "strconv"
+ "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 formatTime(v, DefaultDateFormat)
+}
+
+// TimeHourValueFormatter is a ValueFormatter for timestamps.
+func TimeHourValueFormatter(v interface{}) string {
+ return formatTime(v, DefaultDateHourFormat)
+}
+
+// TimeMinuteValueFormatter is a ValueFormatter for timestamps.
+func TimeMinuteValueFormatter(v interface{}) string {
+ return formatTime(v, DefaultDateMinuteFormat)
+}
+
+// TimeDateValueFormatter is a ValueFormatter for timestamps.
+func TimeDateValueFormatter(v interface{}) string {
+ return formatTime(v, "2006-01-02")
+}
+
+// TimeValueFormatterWithFormat returns a time formatter with a given format.
+func TimeValueFormatterWithFormat(format string) ValueFormatter {
+ return func(v interface{}) string {
+ return formatTime(v, format)
+ }
+}
+
+// TimeValueFormatterWithFormat is a ValueFormatter for timestamps with a given format.
+func formatTime(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(0, typed).Format(dateFormat)
+ }
+ if typed, isTyped := v.(float64); isTyped {
+ return time.Unix(0, int64(typed)).Format(dateFormat)
+ }
+ return ""
+}
+
+// IntValueFormatter is a ValueFormatter for float64.
+func IntValueFormatter(v interface{}) string {
+ switch v.(type) {
+ case int:
+ return strconv.Itoa(v.(int))
+ case int64:
+ return strconv.FormatInt(v.(int64), 10)
+ case float32:
+ return strconv.FormatInt(int64(v.(float32)), 10)
+ case float64:
+ return strconv.FormatInt(int64(v.(float64)), 10)
+ default:
+ return ""
+ }
+}
+
+// FloatValueFormatter is a ValueFormatter for float64.
+func FloatValueFormatter(v interface{}) string {
+ return FloatValueFormatterWithFormat(v, DefaultFloatFormat)
+}
+
+// PercentValueFormatter is a formatter for percent values.
+// NOTE: it normalizes the values, i.e. multiplies by 100.0.
+func PercentValueFormatter(v interface{}) string {
+ if typed, isTyped := v.(float64); isTyped {
+ return FloatValueFormatterWithFormat(typed*100.0, DefaultPercentValueFormat)
+ }
+ return ""
+}
+
+// FloatValueFormatterWithFormat is a ValueFormatter for float64 with a given format.
+func FloatValueFormatterWithFormat(v interface{}, floatFormat string) string {
+ if typed, isTyped := v.(int); isTyped {
+ return fmt.Sprintf(floatFormat, float64(typed))
+ }
+ if typed, isTyped := v.(int64); isTyped {
+ return fmt.Sprintf(floatFormat, float64(typed))
+ }
+ if typed, isTyped := v.(float32); isTyped {
+ return fmt.Sprintf(floatFormat, typed)
+ }
+ if typed, isTyped := v.(float64); isTyped {
+ return fmt.Sprintf(floatFormat, typed)
+ }
+ return ""
+}
+
+
+
package chart
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "math"
+ "strings"
+
+ "golang.org/x/image/font"
+
+ "github.com/golang/freetype/truetype"
+ "github.com/wcharczuk/go-chart/drawing"
+ "github.com/wcharczuk/go-chart/util"
+)
+
+// SVG returns a new png/raster renderer.
+func SVG(width, height int) (Renderer, error) {
+ buffer := bytes.NewBuffer([]byte{})
+ canvas := newCanvas(buffer)
+ canvas.Start(width, height)
+ return &vectorRenderer{
+ b: buffer,
+ c: canvas,
+ s: &Style{},
+ p: []string{},
+ dpi: DefaultDPI,
+ }, nil
+}
+
+// vectorRenderer renders chart commands to a bitmap.
+type vectorRenderer struct {
+ dpi float64
+ b *bytes.Buffer
+ c *canvas
+ s *Style
+ p []string
+ fc *font.Drawer
+}
+
+func (vr *vectorRenderer) ResetStyle() {
+ vr.s = &Style{Font: vr.s.Font}
+ vr.fc = nil
+}
+
+// GetDPI returns the dpi.
+func (vr *vectorRenderer) GetDPI() float64 {
+ return vr.dpi
+}
+
+// SetDPI implements the interface method.
+func (vr *vectorRenderer) SetDPI(dpi float64) {
+ vr.dpi = dpi
+ vr.c.dpi = dpi
+}
+
+// SetClassName implements the interface method.
+func (vr *vectorRenderer) SetClassName(classname string) {
+ vr.s.ClassName = classname
+}
+
+// SetStrokeColor implements the interface method.
+func (vr *vectorRenderer) SetStrokeColor(c drawing.Color) {
+ vr.s.StrokeColor = c
+}
+
+// SetFillColor implements the interface method.
+func (vr *vectorRenderer) SetFillColor(c drawing.Color) {
+ vr.s.FillColor = c
+}
+
+// SetLineWidth implements the interface method.
+func (vr *vectorRenderer) SetStrokeWidth(width float64) {
+ vr.s.StrokeWidth = width
+}
+
+// StrokeDashArray sets the stroke dash array.
+func (vr *vectorRenderer) SetStrokeDashArray(dashArray []float64) {
+ vr.s.StrokeDashArray = dashArray
+}
+
+// MoveTo implements the interface method.
+func (vr *vectorRenderer) MoveTo(x, y int) {
+ vr.p = append(vr.p, fmt.Sprintf("M %d %d", x, y))
+}
+
+// LineTo implements the interface method.
+func (vr *vectorRenderer) LineTo(x, y int) {
+ vr.p = append(vr.p, fmt.Sprintf("L %d %d", x, y))
+}
+
+// QuadCurveTo draws a quad curve.
+func (vr *vectorRenderer) QuadCurveTo(cx, cy, x, y int) {
+ vr.p = append(vr.p, fmt.Sprintf("Q%d,%d %d,%d", cx, cy, x, y))
+}
+
+func (vr *vectorRenderer) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) {
+ startAngle = util.Math.RadianAdd(startAngle, _pi2)
+ endAngle := util.Math.RadianAdd(startAngle, delta)
+
+ startx := cx + int(rx*math.Sin(startAngle))
+ starty := cy - int(ry*math.Cos(startAngle))
+
+ if len(vr.p) > 0 {
+ vr.p = append(vr.p, fmt.Sprintf("L %d %d", startx, starty))
+ } else {
+ vr.p = append(vr.p, fmt.Sprintf("M %d %d", startx, starty))
+ }
+
+ endx := cx + int(rx*math.Sin(endAngle))
+ endy := cy - int(ry*math.Cos(endAngle))
+
+ dd := util.Math.RadiansToDegrees(delta)
+
+ largeArcFlag := 0
+ if delta > _pi {
+ largeArcFlag = 1
+ }
+
+ vr.p = append(vr.p, fmt.Sprintf("A %d %d %0.2f %d 1 %d %d", int(rx), int(ry), dd, largeArcFlag, endx, endy))
+}
+
+// Close closes a shape.
+func (vr *vectorRenderer) Close() {
+ vr.p = append(vr.p, fmt.Sprintf("Z"))
+}
+
+// Stroke draws the path with no fill.
+func (vr *vectorRenderer) Stroke() {
+ vr.drawPath(vr.s.GetStrokeOptions())
+}
+
+// Fill draws the path with no stroke.
+func (vr *vectorRenderer) Fill() {
+ vr.drawPath(vr.s.GetFillOptions())
+}
+
+// FillStroke draws the path with both fill and stroke.
+func (vr *vectorRenderer) FillStroke() {
+ vr.drawPath(vr.s.GetFillAndStrokeOptions())
+}
+
+// drawPath draws a path.
+func (vr *vectorRenderer) drawPath(s Style) {
+ 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) {
+ vr.c.Circle(x, y, int(radius), vr.s.GetFillAndStrokeOptions())
+}
+
+// SetFont implements the interface method.
+func (vr *vectorRenderer) SetFont(f *truetype.Font) {
+ vr.s.Font = f
+}
+
+// SetFontColor implements the interface method.
+func (vr *vectorRenderer) SetFontColor(c drawing.Color) {
+ vr.s.FontColor = c
+}
+
+// SetFontSize implements the interface method.
+func (vr *vectorRenderer) SetFontSize(size float64) {
+ vr.s.FontSize = size
+}
+
+// Text draws a text blob.
+func (vr *vectorRenderer) Text(body string, x, y int) {
+ vr.c.Text(x, y, body, vr.s.GetTextOptions())
+}
+
+// MeasureText uses the truetype font drawer to measure the width of text.
+func (vr *vectorRenderer) MeasureText(body string) (box Box) {
+ if vr.s.GetFont() != nil {
+ vr.fc = &font.Drawer{
+ Face: truetype.NewFace(vr.s.GetFont(), &truetype.Options{
+ DPI: vr.dpi,
+ Size: vr.s.FontSize,
+ }),
+ }
+ w := vr.fc.MeasureString(body).Ceil()
+
+ box.Right = w
+ box.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
+ if vr.c.textTheta == nil {
+ return
+ }
+ box = box.Corners().Rotate(util.Math.RadiansToDegrees(*vr.c.textTheta)).Box()
+ }
+ return
+}
+
+// SetTextRotation sets the text rotation.
+func (vr *vectorRenderer) SetTextRotation(radians float64) {
+ vr.c.textTheta = &radians
+}
+
+// ClearTextRotation clears the text rotation.
+func (vr *vectorRenderer) ClearTextRotation() {
+ vr.c.textTheta = nil
+}
+
+// Save saves the renderer's contents to a writer.
+func (vr *vectorRenderer) Save(w io.Writer) error {
+ vr.c.End()
+ _, err := w.Write(vr.b.Bytes())
+ return err
+}
+
+func newCanvas(w io.Writer) *canvas {
+ return &canvas{
+ w: w,
+ dpi: DefaultDPI,
+ }
+}
+
+type canvas struct {
+ w io.Writer
+ dpi float64
+ textTheta *float64
+ width int
+ height int
+}
+
+func (c *canvas) Start(width, height int) {
+ c.width = width
+ c.height = height
+ 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) {
+ var strokeDashArrayProperty string
+ if len(style.StrokeDashArray) > 0 {
+ strokeDashArrayProperty = c.getStrokeDashArray(style)
+ }
+ c.w.Write([]byte(fmt.Sprintf(`<path %s d="%s" %s/>`, strokeDashArrayProperty, d, c.styleAsSVG(style))))
+}
+
+func (c *canvas) Text(x, y int, body string, style Style) {
+ if c.textTheta == nil {
+ c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" %s>%s</text>`, x, y, c.styleAsSVG(style), body)))
+ } else {
+ transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, util.Math.RadiansToDegrees(*c.textTheta), x, y)
+ c.w.Write([]byte(fmt.Sprintf(`<text x="%d" y="%d" %s%s>%s</text>`, x, y, c.styleAsSVG(style), transform, body)))
+ }
+}
+
+func (c *canvas) Circle(x, y, r int, style Style) {
+ c.w.Write([]byte(fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" %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 or class string.
+func (c *canvas) styleAsSVG(s Style) string {
+ if s.ClassName != "" {
+ return fmt.Sprintf("class=\"%s\"", s.ClassName)
+ }
+ sw := s.StrokeWidth
+ sc := s.StrokeColor
+ fc := s.FillColor
+ fs := s.FontSize
+ fnc := s.FontColor
+
+ var pieces []string
+
+ if sw != 0 {
+ pieces = append(pieces, "stroke-width:"+fmt.Sprintf("%d", int(sw)))
+ } else {
+ pieces = append(pieces, "stroke-width:0")
+ }
+
+ if !sc.IsZero() {
+ pieces = append(pieces, "stroke:"+sc.String())
+ } else {
+ pieces = append(pieces, "stroke:none")
+ }
+
+ if !fnc.IsZero() {
+ pieces = append(pieces, "fill:"+fnc.String())
+ } else if !fc.IsZero() {
+ pieces = append(pieces, "fill:"+fc.String())
+ } else {
+ pieces = append(pieces, "fill:none")
+ }
+
+ if fs != 0 {
+ pieces = append(pieces, "font-size:"+fmt.Sprintf("%.1fpx", drawing.PointsToPixels(c.dpi, fs)))
+ }
+
+ if s.Font != nil {
+ pieces = append(pieces, c.getFontFace(s))
+ }
+ return fmt.Sprintf("style=\"%s\"", strings.Join(pieces, ";"))
+}
+
+
+
package chart
+
+import "github.com/wcharczuk/go-chart/drawing"
+
+var viridisColors = [256]drawing.Color{
+ drawing.Color{R: 0x44, G: 0x1, B: 0x54, A: 0xff},
+ drawing.Color{R: 0x44, G: 0x2, B: 0x55, A: 0xff},
+ drawing.Color{R: 0x45, G: 0x3, B: 0x57, A: 0xff},
+ drawing.Color{R: 0x45, G: 0x5, B: 0x58, A: 0xff},
+ drawing.Color{R: 0x45, G: 0x6, B: 0x5a, A: 0xff},
+ drawing.Color{R: 0x46, G: 0x8, B: 0x5b, A: 0xff},
+ drawing.Color{R: 0x46, G: 0x9, B: 0x5d, A: 0xff},
+ drawing.Color{R: 0x46, G: 0xb, B: 0x5e, A: 0xff},
+ drawing.Color{R: 0x46, G: 0xc, B: 0x60, A: 0xff},
+ drawing.Color{R: 0x47, G: 0xe, B: 0x61, A: 0xff},
+ drawing.Color{R: 0x47, G: 0xf, B: 0x62, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x11, B: 0x64, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x12, B: 0x65, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x14, B: 0x66, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x15, B: 0x68, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x16, B: 0x69, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x18, B: 0x6a, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x19, B: 0x6c, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x1a, B: 0x6d, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x1c, B: 0x6e, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x1d, B: 0x6f, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x1e, B: 0x70, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x20, B: 0x71, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x21, B: 0x73, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x22, B: 0x74, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x24, B: 0x75, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x25, B: 0x76, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x26, B: 0x77, A: 0xff},
+ drawing.Color{R: 0x48, G: 0x27, B: 0x78, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x29, B: 0x79, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x2a, B: 0x79, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x2b, B: 0x7a, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x2c, B: 0x7b, A: 0xff},
+ drawing.Color{R: 0x47, G: 0x2e, B: 0x7c, A: 0xff},
+ drawing.Color{R: 0x46, G: 0x2f, B: 0x7d, A: 0xff},
+ drawing.Color{R: 0x46, G: 0x30, B: 0x7e, A: 0xff},
+ drawing.Color{R: 0x46, G: 0x31, B: 0x7e, A: 0xff},
+ drawing.Color{R: 0x46, G: 0x33, B: 0x7f, A: 0xff},
+ drawing.Color{R: 0x45, G: 0x34, B: 0x80, A: 0xff},
+ drawing.Color{R: 0x45, G: 0x35, B: 0x81, A: 0xff},
+ drawing.Color{R: 0x45, G: 0x36, B: 0x81, A: 0xff},
+ drawing.Color{R: 0x44, G: 0x38, B: 0x82, A: 0xff},
+ drawing.Color{R: 0x44, G: 0x39, B: 0x83, A: 0xff},
+ drawing.Color{R: 0x44, G: 0x3a, B: 0x83, A: 0xff},
+ drawing.Color{R: 0x43, G: 0x3b, B: 0x84, A: 0xff},
+ drawing.Color{R: 0x43, G: 0x3c, B: 0x84, A: 0xff},
+ drawing.Color{R: 0x43, G: 0x3e, B: 0x85, A: 0xff},
+ drawing.Color{R: 0x42, G: 0x3f, B: 0x85, A: 0xff},
+ drawing.Color{R: 0x42, G: 0x40, B: 0x86, A: 0xff},
+ drawing.Color{R: 0x41, G: 0x41, B: 0x86, A: 0xff},
+ drawing.Color{R: 0x41, G: 0x42, B: 0x87, A: 0xff},
+ drawing.Color{R: 0x41, G: 0x43, B: 0x87, A: 0xff},
+ drawing.Color{R: 0x40, G: 0x45, B: 0x88, A: 0xff},
+ drawing.Color{R: 0x40, G: 0x46, B: 0x88, A: 0xff},
+ drawing.Color{R: 0x3f, G: 0x47, B: 0x88, A: 0xff},
+ drawing.Color{R: 0x3f, G: 0x48, B: 0x89, A: 0xff},
+ drawing.Color{R: 0x3e, G: 0x49, B: 0x89, A: 0xff},
+ drawing.Color{R: 0x3e, G: 0x4a, B: 0x89, A: 0xff},
+ drawing.Color{R: 0x3d, G: 0x4b, B: 0x8a, A: 0xff},
+ drawing.Color{R: 0x3d, G: 0x4d, B: 0x8a, A: 0xff},
+ drawing.Color{R: 0x3c, G: 0x4e, B: 0x8a, A: 0xff},
+ drawing.Color{R: 0x3c, G: 0x4f, B: 0x8a, A: 0xff},
+ drawing.Color{R: 0x3b, G: 0x50, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x3b, G: 0x51, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x3a, G: 0x52, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x3a, G: 0x53, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x39, G: 0x54, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x39, G: 0x55, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x38, G: 0x56, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x38, G: 0x57, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x37, G: 0x58, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x37, G: 0x59, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x36, G: 0x5b, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x36, G: 0x5c, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x35, G: 0x5d, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x35, G: 0x5e, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x34, G: 0x5f, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x34, G: 0x60, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x33, G: 0x61, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x33, G: 0x62, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x33, G: 0x63, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x32, G: 0x64, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x32, G: 0x65, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x31, G: 0x66, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x31, G: 0x67, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x30, G: 0x68, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x30, G: 0x69, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2f, G: 0x6a, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2f, G: 0x6b, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2f, G: 0x6c, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2e, G: 0x6d, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2e, G: 0x6e, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2d, G: 0x6f, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2d, G: 0x70, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2d, G: 0x70, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2c, G: 0x71, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2c, G: 0x72, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2b, G: 0x73, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2b, G: 0x74, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2b, G: 0x75, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2a, G: 0x76, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x2a, G: 0x77, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x29, G: 0x78, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x29, G: 0x79, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x29, G: 0x7a, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x28, G: 0x7b, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x28, G: 0x7c, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x28, G: 0x7d, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x27, G: 0x7e, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x27, G: 0x7f, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x26, G: 0x80, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x26, G: 0x81, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x26, G: 0x82, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x25, G: 0x83, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x25, G: 0x83, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x25, G: 0x84, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x24, G: 0x85, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x24, G: 0x86, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x23, G: 0x87, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x23, G: 0x88, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x23, G: 0x89, B: 0x8e, A: 0xff},
+ drawing.Color{R: 0x22, G: 0x8a, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x22, G: 0x8b, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x22, G: 0x8c, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x21, G: 0x8d, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x21, G: 0x8e, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x21, G: 0x8f, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x20, G: 0x90, B: 0x8d, A: 0xff},
+ drawing.Color{R: 0x20, G: 0x91, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x20, G: 0x92, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x20, G: 0x93, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0x93, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0x94, B: 0x8c, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0x95, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0x96, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0x97, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x98, B: 0x8b, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x99, B: 0x8a, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x9a, B: 0x8a, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x9b, B: 0x8a, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x9c, B: 0x89, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x9d, B: 0x89, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x9e, B: 0x89, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0x9f, B: 0x88, A: 0xff},
+ drawing.Color{R: 0x1e, G: 0xa0, B: 0x88, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0xa1, B: 0x88, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0xa2, B: 0x87, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0xa3, B: 0x87, A: 0xff},
+ drawing.Color{R: 0x1f, G: 0xa3, B: 0x86, A: 0xff},
+ drawing.Color{R: 0x20, G: 0xa4, B: 0x86, A: 0xff},
+ drawing.Color{R: 0x20, G: 0xa5, B: 0x86, A: 0xff},
+ drawing.Color{R: 0x21, G: 0xa6, B: 0x85, A: 0xff},
+ drawing.Color{R: 0x21, G: 0xa7, B: 0x85, A: 0xff},
+ drawing.Color{R: 0x22, G: 0xa8, B: 0x84, A: 0xff},
+ drawing.Color{R: 0x23, G: 0xa9, B: 0x83, A: 0xff},
+ drawing.Color{R: 0x23, G: 0xaa, B: 0x83, A: 0xff},
+ drawing.Color{R: 0x24, G: 0xab, B: 0x82, A: 0xff},
+ drawing.Color{R: 0x25, G: 0xac, B: 0x82, A: 0xff},
+ drawing.Color{R: 0x26, G: 0xad, B: 0x81, A: 0xff},
+ drawing.Color{R: 0x27, G: 0xae, B: 0x81, A: 0xff},
+ drawing.Color{R: 0x28, G: 0xaf, B: 0x80, A: 0xff},
+ drawing.Color{R: 0x29, G: 0xaf, B: 0x7f, A: 0xff},
+ drawing.Color{R: 0x2a, G: 0xb0, B: 0x7f, A: 0xff},
+ drawing.Color{R: 0x2b, G: 0xb1, B: 0x7e, A: 0xff},
+ drawing.Color{R: 0x2c, G: 0xb2, B: 0x7d, A: 0xff},
+ drawing.Color{R: 0x2e, G: 0xb3, B: 0x7c, A: 0xff},
+ drawing.Color{R: 0x2f, G: 0xb4, B: 0x7c, A: 0xff},
+ drawing.Color{R: 0x30, G: 0xb5, B: 0x7b, A: 0xff},
+ drawing.Color{R: 0x32, G: 0xb6, B: 0x7a, A: 0xff},
+ drawing.Color{R: 0x33, G: 0xb7, B: 0x79, A: 0xff},
+ drawing.Color{R: 0x35, G: 0xb7, B: 0x79, A: 0xff},
+ drawing.Color{R: 0x36, G: 0xb8, B: 0x78, A: 0xff},
+ drawing.Color{R: 0x38, G: 0xb9, B: 0x77, A: 0xff},
+ drawing.Color{R: 0x39, G: 0xba, B: 0x76, A: 0xff},
+ drawing.Color{R: 0x3b, G: 0xbb, B: 0x75, A: 0xff},
+ drawing.Color{R: 0x3d, G: 0xbc, B: 0x74, A: 0xff},
+ drawing.Color{R: 0x3e, G: 0xbd, B: 0x73, A: 0xff},
+ drawing.Color{R: 0x40, G: 0xbe, B: 0x72, A: 0xff},
+ drawing.Color{R: 0x42, G: 0xbe, B: 0x71, A: 0xff},
+ drawing.Color{R: 0x44, G: 0xbf, B: 0x70, A: 0xff},
+ drawing.Color{R: 0x46, G: 0xc0, B: 0x6f, A: 0xff},
+ drawing.Color{R: 0x48, G: 0xc1, B: 0x6e, A: 0xff},
+ drawing.Color{R: 0x49, G: 0xc2, B: 0x6d, A: 0xff},
+ drawing.Color{R: 0x4b, G: 0xc2, B: 0x6c, A: 0xff},
+ drawing.Color{R: 0x4d, G: 0xc3, B: 0x6b, A: 0xff},
+ drawing.Color{R: 0x4f, G: 0xc4, B: 0x6a, A: 0xff},
+ drawing.Color{R: 0x51, G: 0xc5, B: 0x69, A: 0xff},
+ drawing.Color{R: 0x53, G: 0xc6, B: 0x68, A: 0xff},
+ drawing.Color{R: 0x55, G: 0xc6, B: 0x66, A: 0xff},
+ drawing.Color{R: 0x58, G: 0xc7, B: 0x65, A: 0xff},
+ drawing.Color{R: 0x5a, G: 0xc8, B: 0x64, A: 0xff},
+ drawing.Color{R: 0x5c, G: 0xc9, B: 0x63, A: 0xff},
+ drawing.Color{R: 0x5e, G: 0xc9, B: 0x62, A: 0xff},
+ drawing.Color{R: 0x60, G: 0xca, B: 0x60, A: 0xff},
+ drawing.Color{R: 0x62, G: 0xcb, B: 0x5f, A: 0xff},
+ drawing.Color{R: 0x65, G: 0xcc, B: 0x5e, A: 0xff},
+ drawing.Color{R: 0x67, G: 0xcc, B: 0x5c, A: 0xff},
+ drawing.Color{R: 0x69, G: 0xcd, B: 0x5b, A: 0xff},
+ drawing.Color{R: 0x6c, G: 0xce, B: 0x5a, A: 0xff},
+ drawing.Color{R: 0x6e, G: 0xce, B: 0x58, A: 0xff},
+ drawing.Color{R: 0x70, G: 0xcf, B: 0x57, A: 0xff},
+ drawing.Color{R: 0x73, G: 0xd0, B: 0x55, A: 0xff},
+ drawing.Color{R: 0x75, G: 0xd0, B: 0x54, A: 0xff},
+ drawing.Color{R: 0x77, G: 0xd1, B: 0x52, A: 0xff},
+ drawing.Color{R: 0x7a, G: 0xd2, B: 0x51, A: 0xff},
+ drawing.Color{R: 0x7c, G: 0xd2, B: 0x4f, A: 0xff},
+ drawing.Color{R: 0x7f, G: 0xd3, B: 0x4e, A: 0xff},
+ drawing.Color{R: 0x81, G: 0xd4, B: 0x4c, A: 0xff},
+ drawing.Color{R: 0x84, G: 0xd4, B: 0x4b, A: 0xff},
+ drawing.Color{R: 0x86, G: 0xd5, B: 0x49, A: 0xff},
+ drawing.Color{R: 0x89, G: 0xd5, B: 0x48, A: 0xff},
+ drawing.Color{R: 0x8b, G: 0xd6, B: 0x46, A: 0xff},
+ drawing.Color{R: 0x8e, G: 0xd7, B: 0x44, A: 0xff},
+ drawing.Color{R: 0x90, G: 0xd7, B: 0x43, A: 0xff},
+ drawing.Color{R: 0x93, G: 0xd8, B: 0x41, A: 0xff},
+ drawing.Color{R: 0x95, G: 0xd8, B: 0x3f, A: 0xff},
+ drawing.Color{R: 0x98, G: 0xd9, B: 0x3e, A: 0xff},
+ drawing.Color{R: 0x9b, G: 0xd9, B: 0x3c, A: 0xff},
+ drawing.Color{R: 0x9d, G: 0xda, B: 0x3a, A: 0xff},
+ drawing.Color{R: 0xa0, G: 0xda, B: 0x39, A: 0xff},
+ drawing.Color{R: 0xa3, G: 0xdb, B: 0x37, A: 0xff},
+ drawing.Color{R: 0xa5, G: 0xdb, B: 0x35, A: 0xff},
+ drawing.Color{R: 0xa8, G: 0xdc, B: 0x33, A: 0xff},
+ drawing.Color{R: 0xab, G: 0xdc, B: 0x32, A: 0xff},
+ drawing.Color{R: 0xad, G: 0xdd, B: 0x30, A: 0xff},
+ drawing.Color{R: 0xb0, G: 0xdd, B: 0x2e, A: 0xff},
+ drawing.Color{R: 0xb3, G: 0xdd, B: 0x2d, A: 0xff},
+ drawing.Color{R: 0xb5, G: 0xde, B: 0x2b, A: 0xff},
+ drawing.Color{R: 0xb8, G: 0xde, B: 0x29, A: 0xff},
+ drawing.Color{R: 0xbb, G: 0xdf, B: 0x27, A: 0xff},
+ drawing.Color{R: 0xbd, G: 0xdf, B: 0x26, A: 0xff},
+ drawing.Color{R: 0xc0, G: 0xdf, B: 0x24, A: 0xff},
+ drawing.Color{R: 0xc3, G: 0xe0, B: 0x23, A: 0xff},
+ drawing.Color{R: 0xc5, G: 0xe0, B: 0x21, A: 0xff},
+ drawing.Color{R: 0xc8, G: 0xe1, B: 0x20, A: 0xff},
+ drawing.Color{R: 0xcb, G: 0xe1, B: 0x1e, A: 0xff},
+ drawing.Color{R: 0xcd, G: 0xe1, B: 0x1d, A: 0xff},
+ drawing.Color{R: 0xd0, G: 0xe2, B: 0x1c, A: 0xff},
+ drawing.Color{R: 0xd3, G: 0xe2, B: 0x1b, A: 0xff},
+ drawing.Color{R: 0xd5, G: 0xe2, B: 0x1a, A: 0xff},
+ drawing.Color{R: 0xd8, G: 0xe3, B: 0x19, A: 0xff},
+ drawing.Color{R: 0xdb, G: 0xe3, B: 0x18, A: 0xff},
+ drawing.Color{R: 0xdd, G: 0xe3, B: 0x18, A: 0xff},
+ drawing.Color{R: 0xe0, G: 0xe4, B: 0x18, A: 0xff},
+ drawing.Color{R: 0xe2, G: 0xe4, B: 0x18, A: 0xff},
+ drawing.Color{R: 0xe5, G: 0xe4, B: 0x18, A: 0xff},
+ drawing.Color{R: 0xe8, G: 0xe5, B: 0x19, A: 0xff},
+ drawing.Color{R: 0xea, G: 0xe5, B: 0x19, A: 0xff},
+ drawing.Color{R: 0xed, G: 0xe5, B: 0x1a, A: 0xff},
+ drawing.Color{R: 0xef, G: 0xe6, B: 0x1b, A: 0xff},
+ drawing.Color{R: 0xf2, G: 0xe6, B: 0x1c, A: 0xff},
+ drawing.Color{R: 0xf4, G: 0xe6, B: 0x1e, A: 0xff},
+ drawing.Color{R: 0xf7, G: 0xe6, B: 0x1f, A: 0xff},
+ drawing.Color{R: 0xf9, G: 0xe7, B: 0x21, A: 0xff},
+ drawing.Color{R: 0xfb, G: 0xe7, B: 0x23, A: 0xff},
+ drawing.Color{R: 0xfe, G: 0xe7, B: 0x24, A: 0xff},
+}
+
+// Viridis creates a color map provider.
+func Viridis(v, vmin, vmax float64) drawing.Color {
+ normalized := (v - vmin) / (vmax - vmin)
+ index := uint8(normalized * 255)
+ return viridisColors[index]
+}
+
+
+
package chart
+
+import (
+ "math"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// XAxis represents the horizontal axis.
+type XAxis struct {
+ Name string
+ NameStyle Style
+
+ Style Style
+ ValueFormatter ValueFormatter
+ Range Range
+
+ TickStyle Style
+ Ticks []Tick
+ TickPosition TickPosition
+
+ GridLines []GridLine
+ GridMajorStyle Style
+ GridMinorStyle Style
+}
+
+// GetName returns the name.
+func (xa XAxis) GetName() string {
+ return xa.Name
+}
+
+// GetStyle returns the style.
+func (xa XAxis) GetStyle() Style {
+ return xa.Style
+}
+
+// GetValueFormatter returns the value formatter for the axis.
+func (xa XAxis) GetValueFormatter() ValueFormatter {
+ if xa.ValueFormatter != nil {
+ return xa.ValueFormatter
+ }
+ return FloatValueFormatter
+}
+
+// GetTickPosition returns the tick position option for the axis.
+func (xa XAxis) GetTickPosition(defaults ...TickPosition) TickPosition {
+ if xa.TickPosition == TickPositionUnset {
+ if len(defaults) > 0 {
+ return defaults[0]
+ }
+ return TickPositionUnderTick
+ }
+ return xa.TickPosition
+}
+
+// GetTicks returns the ticks for a series.
+// The coalesce priority is:
+// - User Supplied Ticks (i.e. Ticks array on the axis itself).
+// - Range ticks (i.e. if the range provides ticks).
+// - Generating continuous ticks based on minimum spacing and canvas width.
+func (xa XAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
+ if len(xa.Ticks) > 0 {
+ return xa.Ticks
+ }
+ if tp, isTickProvider := ra.(TicksProvider); isTickProvider {
+ return tp.GetTicks(r, defaults, vf)
+ }
+ tickStyle := xa.Style.InheritFrom(defaults)
+ return GenerateContinuousTicks(r, ra, false, tickStyle, vf)
+}
+
+// GetGridLines returns the gridlines for the axis.
+func (xa XAxis) GetGridLines(ticks []Tick) []GridLine {
+ if len(xa.GridLines) > 0 {
+ return xa.GridLines
+ }
+ return GenerateGridLines(ticks, xa.GridMajorStyle, xa.GridMinorStyle)
+}
+
+// Measure returns the bounds of the axis.
+func (xa XAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
+ tickStyle := xa.TickStyle.InheritFrom(xa.Style.InheritFrom(defaults))
+
+ tp := xa.GetTickPosition()
+
+ var ltx, rtx int
+ var tx, ty int
+ var left, right, bottom = math.MaxInt32, 0, 0
+ for index, t := range ticks {
+ v := t.Value
+ tb := Draw.MeasureText(r, t.Label, tickStyle.GetTextOptions())
+
+ tx = canvasBox.Left + ra.Translate(v)
+ ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
+ switch tp {
+ case TickPositionUnderTick, TickPositionUnset:
+ ltx = tx - tb.Width()>>1
+ rtx = tx + tb.Width()>>1
+ break
+ case TickPositionBetweenTicks:
+ if index > 0 {
+ ltx = ra.Translate(ticks[index-1].Value)
+ rtx = tx
+ }
+ break
+ }
+
+ left = util.Math.MinInt(left, ltx)
+ right = util.Math.MaxInt(right, rtx)
+ bottom = util.Math.MaxInt(bottom, ty)
+ }
+
+ if xa.NameStyle.Show && len(xa.Name) > 0 {
+ tb := Draw.MeasureText(r, xa.Name, xa.NameStyle.InheritFrom(defaults))
+ bottom += DefaultXAxisMargin + tb.Height()
+ }
+
+ return Box{
+ Top: canvasBox.Bottom,
+ Left: left,
+ Right: right,
+ Bottom: bottom,
+ }
+}
+
+// Render renders the axis
+func (xa XAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
+ tickStyle := xa.TickStyle.InheritFrom(xa.Style.InheritFrom(defaults))
+
+ tickStyle.GetStrokeOptions().WriteToRenderer(r)
+ r.MoveTo(canvasBox.Left, canvasBox.Bottom)
+ r.LineTo(canvasBox.Right, canvasBox.Bottom)
+ r.Stroke()
+
+ tp := xa.GetTickPosition()
+
+ var tx, ty int
+ var maxTextHeight int
+ for index, t := range ticks {
+ v := t.Value
+ lx := ra.Translate(v)
+
+ tx = canvasBox.Left + lx
+
+ tickStyle.GetStrokeOptions().WriteToRenderer(r)
+ r.MoveTo(tx, canvasBox.Bottom)
+ r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight)
+ r.Stroke()
+
+ tickWithAxisStyle := xa.TickStyle.InheritFrom(xa.Style.InheritFrom(defaults))
+ tb := Draw.MeasureText(r, t.Label, tickWithAxisStyle)
+
+ switch tp {
+ case TickPositionUnderTick, TickPositionUnset:
+ if tickStyle.TextRotationDegrees == 0 {
+ tx = tx - tb.Width()>>1
+ ty = canvasBox.Bottom + DefaultXAxisMargin + tb.Height()
+ } else {
+ ty = canvasBox.Bottom + (2 * DefaultXAxisMargin)
+ }
+ Draw.Text(r, t.Label, tx, ty, tickWithAxisStyle)
+ maxTextHeight = util.Math.MaxInt(maxTextHeight, tb.Height())
+ break
+ case TickPositionBetweenTicks:
+ if index > 0 {
+ llx := ra.Translate(ticks[index-1].Value)
+ ltx := canvasBox.Left + llx
+ finalTickStyle := tickWithAxisStyle.InheritFrom(Style{TextHorizontalAlign: TextHorizontalAlignCenter})
+
+ Draw.TextWithin(r, t.Label, Box{
+ Left: ltx,
+ Right: tx,
+ Top: canvasBox.Bottom + DefaultXAxisMargin,
+ Bottom: canvasBox.Bottom + DefaultXAxisMargin,
+ }, finalTickStyle)
+
+ ftb := Text.MeasureLines(r, Text.WrapFit(r, t.Label, tx-ltx, finalTickStyle), finalTickStyle)
+ maxTextHeight = util.Math.MaxInt(maxTextHeight, ftb.Height())
+ }
+ break
+ }
+ }
+
+ nameStyle := xa.NameStyle.InheritFrom(defaults)
+ if xa.NameStyle.Show && len(xa.Name) > 0 {
+ tb := Draw.MeasureText(r, xa.Name, nameStyle)
+ tx := canvasBox.Right - (canvasBox.Width()>>1 + tb.Width()>>1)
+ ty := canvasBox.Bottom + DefaultXAxisMargin + maxTextHeight + DefaultXAxisMargin + tb.Height()
+ Draw.Text(r, xa.Name, tx, ty, nameStyle)
+ }
+
+ if xa.GridMajorStyle.Show || xa.GridMinorStyle.Show {
+ for _, gl := range xa.GetGridLines(ticks) {
+ if (gl.IsMinor && xa.GridMinorStyle.Show) || (!gl.IsMinor && xa.GridMajorStyle.Show) {
+ defaults := xa.GridMajorStyle
+ if gl.IsMinor {
+ defaults = xa.GridMinorStyle
+ }
+ gl.Render(r, canvasBox, ra, true, gl.Style.InheritFrom(defaults))
+ }
+ }
+ }
+}
+
+
+
package chart
+
+import (
+ "math"
+
+ util "github.com/wcharczuk/go-chart/util"
+)
+
+// YAxis is a veritcal rule of the range.
+// There can be (2) y-axes; a primary and secondary.
+type YAxis struct {
+ Name string
+ NameStyle Style
+
+ Style Style
+
+ Zero GridLine
+
+ AxisType YAxisType
+ Ascending bool
+
+ ValueFormatter ValueFormatter
+ Range Range
+
+ TickStyle Style
+ Ticks []Tick
+
+ GridLines []GridLine
+ GridMajorStyle Style
+ GridMinorStyle Style
+}
+
+// GetName returns the name.
+func (ya YAxis) GetName() string {
+ return ya.Name
+}
+
+// GetNameStyle returns the name style.
+func (ya YAxis) GetNameStyle() Style {
+ return ya.NameStyle
+}
+
+// GetStyle returns the style.
+func (ya YAxis) GetStyle() Style {
+ return ya.Style
+}
+
+// GetValueFormatter returns the value formatter for the axis.
+func (ya YAxis) GetValueFormatter() ValueFormatter {
+ if ya.ValueFormatter != nil {
+ return ya.ValueFormatter
+ }
+ return FloatValueFormatter
+}
+
+// GetTickStyle returns the tick style.
+func (ya YAxis) GetTickStyle() Style {
+ return ya.TickStyle
+}
+
+// GetTicks returns the ticks for a series.
+// The coalesce priority is:
+// - User Supplied Ticks (i.e. Ticks array on the axis itself).
+// - Range ticks (i.e. if the range provides ticks).
+// - Generating continuous ticks based on minimum spacing and canvas width.
+func (ya YAxis) GetTicks(r Renderer, ra Range, defaults Style, vf ValueFormatter) []Tick {
+ if len(ya.Ticks) > 0 {
+ return ya.Ticks
+ }
+ if tp, isTickProvider := ra.(TicksProvider); isTickProvider {
+ return tp.GetTicks(r, defaults, vf)
+ }
+ tickStyle := ya.Style.InheritFrom(defaults)
+ return GenerateContinuousTicks(r, ra, true, tickStyle, vf)
+}
+
+// GetGridLines returns the gridlines for the axis.
+func (ya YAxis) GetGridLines(ticks []Tick) []GridLine {
+ if len(ya.GridLines) > 0 {
+ return ya.GridLines
+ }
+ return GenerateGridLines(ticks, ya.GridMajorStyle, ya.GridMinorStyle)
+}
+
+// Measure returns the bounds of the axis.
+func (ya YAxis) Measure(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) Box {
+ var tx int
+ if ya.AxisType == YAxisPrimary {
+ tx = canvasBox.Right + DefaultYAxisMargin
+ } else if ya.AxisType == YAxisSecondary {
+ tx = canvasBox.Left - DefaultYAxisMargin
+ }
+
+ ya.TickStyle.InheritFrom(ya.Style.InheritFrom(defaults)).WriteToRenderer(r)
+ var minx, maxx, miny, maxy = math.MaxInt32, 0, math.MaxInt32, 0
+ var maxTextHeight int
+ for _, t := range ticks {
+ v := t.Value
+ ly := canvasBox.Bottom - ra.Translate(v)
+
+ tb := r.MeasureText(t.Label)
+ tbh2 := tb.Height() >> 1
+ finalTextX := tx
+ if ya.AxisType == YAxisSecondary {
+ finalTextX = tx - tb.Width()
+ }
+
+ maxTextHeight = util.Math.MaxInt(tb.Height(), maxTextHeight)
+
+ if ya.AxisType == YAxisPrimary {
+ minx = canvasBox.Right
+ maxx = util.Math.MaxInt(maxx, tx+tb.Width())
+ } else if ya.AxisType == YAxisSecondary {
+ minx = util.Math.MinInt(minx, finalTextX)
+ maxx = util.Math.MaxInt(maxx, tx)
+ }
+
+ miny = util.Math.MinInt(miny, ly-tbh2)
+ maxy = util.Math.MaxInt(maxy, ly+tbh2)
+ }
+
+ if ya.NameStyle.Show && len(ya.Name) > 0 {
+ maxx += (DefaultYAxisMargin + maxTextHeight)
+ }
+
+ return Box{
+ Top: miny,
+ Left: minx,
+ Right: maxx,
+ Bottom: maxy,
+ }
+}
+
+// Render renders the axis.
+func (ya YAxis) Render(r Renderer, canvasBox Box, ra Range, defaults Style, ticks []Tick) {
+ tickStyle := ya.TickStyle.InheritFrom(ya.Style.InheritFrom(defaults))
+ tickStyle.WriteToRenderer(r)
+
+ sw := tickStyle.GetStrokeWidth(defaults.StrokeWidth)
+
+ var lx int
+ var tx int
+ if ya.AxisType == YAxisPrimary {
+ lx = canvasBox.Right + int(sw)
+ tx = lx + DefaultYAxisMargin
+ } else if ya.AxisType == YAxisSecondary {
+ lx = canvasBox.Left - int(sw)
+ tx = lx - DefaultYAxisMargin
+ }
+
+ r.MoveTo(lx, canvasBox.Bottom)
+ r.LineTo(lx, canvasBox.Top)
+ r.Stroke()
+
+ var maxTextWidth int
+ var finalTextX, finalTextY int
+ for _, t := range ticks {
+ v := t.Value
+ ly := canvasBox.Bottom - ra.Translate(v)
+
+ tb := Draw.MeasureText(r, t.Label, tickStyle)
+
+ if tb.Width() > maxTextWidth {
+ maxTextWidth = tb.Width()
+ }
+
+ if ya.AxisType == YAxisSecondary {
+ finalTextX = tx - tb.Width()
+ } else {
+ finalTextX = tx
+ }
+
+ if tickStyle.TextRotationDegrees == 0 {
+ finalTextY = ly + tb.Height()>>1
+ } else {
+ finalTextY = ly
+ }
+
+ tickStyle.WriteToRenderer(r)
+
+ r.MoveTo(lx, ly)
+ if ya.AxisType == YAxisPrimary {
+ r.LineTo(lx+DefaultHorizontalTickWidth, ly)
+ } else if ya.AxisType == YAxisSecondary {
+ r.LineTo(lx-DefaultHorizontalTickWidth, ly)
+ }
+ r.Stroke()
+
+ Draw.Text(r, t.Label, finalTextX, finalTextY, tickStyle)
+ }
+
+ nameStyle := ya.NameStyle.InheritFrom(defaults.InheritFrom(Style{TextRotationDegrees: 90}))
+ if ya.NameStyle.Show && len(ya.Name) > 0 {
+ nameStyle.GetTextOptions().WriteToRenderer(r)
+ tb := Draw.MeasureText(r, ya.Name, nameStyle)
+
+ var tx int
+ if ya.AxisType == YAxisPrimary {
+ tx = canvasBox.Right + int(sw) + DefaultYAxisMargin + maxTextWidth + DefaultYAxisMargin
+ } else if ya.AxisType == YAxisSecondary {
+ tx = canvasBox.Left - (DefaultYAxisMargin + int(sw) + maxTextWidth + DefaultYAxisMargin)
+ }
+
+ var ty int
+ if nameStyle.TextRotationDegrees == 0 {
+ ty = canvasBox.Top + (canvasBox.Height()>>1 - tb.Width()>>1)
+ } else {
+ ty = canvasBox.Top + (canvasBox.Height()>>1 - tb.Height()>>1)
+ }
+
+ Draw.Text(r, ya.Name, tx, ty, nameStyle)
+ }
+
+ if ya.Zero.Style.Show {
+ ya.Zero.Render(r, canvasBox, ra, false, Style{})
+ }
+
+ if ya.GridMajorStyle.Show || ya.GridMinorStyle.Show {
+ for _, gl := range ya.GetGridLines(ticks) {
+ if (gl.IsMinor && ya.GridMinorStyle.Show) || (!gl.IsMinor && ya.GridMajorStyle.Show) {
+ defaults := ya.GridMajorStyle
+ if gl.IsMinor {
+ defaults = ya.GridMinorStyle
+ }
+ gl.Render(r, canvasBox, ra, false, gl.Style.InheritFrom(defaults))
+ }
+ }
+ }
+}
+
+
+
+
+
+