From a1fb2847977d920cdfa5625e4fff7f21d64c7237 Mon Sep 17 00:00:00 2001 From: Will Charczuk Date: Thu, 28 Jul 2016 18:51:55 -0700 Subject: [PATCH] basics of stacked bar. --- chart.go | 2 +- defaults.go | 8 +- examples/pie_chart/main.go | 14 +-- examples/stacked_bar/main.go | 48 ++++++++ pie_chart.go | 12 +- stacked_bar_chart.go | 209 +++++++++++++++++++++++++++++++++++ util.go | 2 +- value.go | 2 +- 8 files changed, 272 insertions(+), 25 deletions(-) create mode 100644 examples/stacked_bar/main.go create mode 100644 stacked_bar_chart.go diff --git a/chart.go b/chart.go index c837019..c4e7526 100644 --- a/chart.go +++ b/chart.go @@ -463,7 +463,7 @@ func (c Chart) styleDefaultsCanvas() Style { } func (c Chart) styleDefaultsSeries(seriesIndex int) Style { - strokeColor := GetDefaultSeriesStrokeColor(seriesIndex) + strokeColor := GetDefaultColor(seriesIndex) return Style{ StrokeColor: strokeColor, StrokeWidth: DefaultStrokeWidth, diff --git a/defaults.go b/defaults.go index 770958a..a1fb01c 100644 --- a/defaults.go +++ b/defaults.go @@ -159,16 +159,16 @@ var ( DashArrayDashesLarge = []int{10, 10} ) -// GetDefaultSeriesStrokeColor returns a color from the default list by index. +// GetDefaultColor returns a color from the default list by index. // NOTE: the index will wrap around (using a modulo). -func GetDefaultSeriesStrokeColor(index int) drawing.Color { +func GetDefaultColor(index int) drawing.Color { finalIndex := index % len(DefaultColors) return DefaultColors[finalIndex] } -// GetDefaultPieChartValueColor returns a color from the default list by index. +// GetAlternateColor returns a color from the default list by index. // NOTE: the index will wrap around (using a modulo). -func GetDefaultPieChartValueColor(index int) drawing.Color { +func GetAlternateColor(index int) drawing.Color { finalIndex := index % len(DefaultAlternateColors) return DefaultAlternateColors[finalIndex] } diff --git a/examples/pie_chart/main.go b/examples/pie_chart/main.go index 4054ca5..d5c9bcd 100644 --- a/examples/pie_chart/main.go +++ b/examples/pie_chart/main.go @@ -14,13 +14,13 @@ func drawChart(res http.ResponseWriter, req *http.Request) { FillColor: chart.ColorLightGray, }, Values: []chart.Value{ - {Value: 10, Label: "Blue"}, - {Value: 9, Label: "Green"}, - {Value: 8, Label: "Gray"}, - {Value: 7, Label: "Orange"}, - {Value: 6, Label: "HEANG"}, - {Value: 5, Label: "??"}, - {Value: 2, Label: "!!"}, + {Value: 5, Label: "Blue"}, + {Value: 5, Label: "Green"}, + {Value: 4, Label: "Gray"}, + {Value: 4, Label: "Orange"}, + {Value: 3, Label: "Test"}, + {Value: 3, Label: "??"}, + {Value: 1, Label: "!!"}, }, } diff --git a/examples/stacked_bar/main.go b/examples/stacked_bar/main.go new file mode 100644 index 0000000..2bdbb9b --- /dev/null +++ b/examples/stacked_bar/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/wcharczuk/go-chart" +) + +func drawChart(res http.ResponseWriter, req *http.Request) { + sbc := chart.StackedBarChart{ + Background: chart.Style{ + Padding: chart.Box{Top: 50, Left: 50, Right: 50, Bottom: 50}, + }, + Bars: []chart.StackedBar{ + { + Values: []chart.Value{ + {Value: 5, Label: "Blue"}, + {Value: 5, Label: "Green"}, + {Value: 4, Label: "Gray"}, + {Value: 4, Label: "Orange"}, + {Value: 3, Label: "Test"}, + {Value: 3, Label: "??"}, + {Value: 1, Label: "!!"}, + }, + }, + { + Values: []chart.Value{ + {Value: 10, Label: "Blue"}, + {Value: 5, Label: "Green"}, + {Value: 1, Label: "Gray"}, + }, + }, + }, + } + + res.Header().Set("Content-Type", "image/svg+xml") + err := sbc.Render(chart.SVG, res) + if err != nil { + fmt.Printf("Error rendering chart: %v\n", err) + } +} + +func main() { + http.HandleFunc("/", drawChart) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/pie_chart.go b/pie_chart.go index 9203099..614167e 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -217,7 +217,7 @@ func (pc PieChart) stylePieChartValue(index int) Style { return Style{ StrokeColor: ColorWhite, StrokeWidth: 5.0, - FillColor: GetDefaultPieChartValueColor(index), + FillColor: GetAlternateColor(index), FontSize: 24.0, FontColor: ColorWhite, Font: pc.GetFont(), @@ -232,16 +232,6 @@ func (pc PieChart) styleDefaultsBackground() Style { } } -func (pc PieChart) styleDefaultsSeries(seriesIndex int) Style { - strokeColor := GetDefaultSeriesStrokeColor(seriesIndex) - return Style{ - StrokeColor: strokeColor, - StrokeWidth: DefaultStrokeWidth, - Font: pc.GetFont(), - FontSize: DefaultFontSize, - } -} - func (pc PieChart) styleDefaultsElements() Style { return Style{ Font: pc.GetFont(), diff --git a/stacked_bar_chart.go b/stacked_bar_chart.go new file mode 100644 index 0000000..c479cf4 --- /dev/null +++ b/stacked_bar_chart.go @@ -0,0 +1,209 @@ +package chart + +import ( + "errors" + "io" + + "github.com/golang/freetype/truetype" +) + +// 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 20 + } + return sb.Width +} + +// StackedBarChart is a chart that draws sections of a bar based on percentages. +type StackedBarChart struct { + Title string + TitleStyle Style + + Width int + Height int + DPI float64 + + Background Style + Canvas 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(sbc.getDefaultCanvasBox()) + sbc.drawBars(r, canvasBox) + + sbc.drawTitle(r) + for _, a := range sbc.Elements { + a(r, canvasBox, sbc.styleDefaultsElements()) + } + + return r.Save(w) +} + +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() + } +} + +func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar StackedBar) int { + bxl := xoffset + bxr := xoffset + bar.GetWidth() + + normalizedBarComponents := Values(bar.Values).Normalize() + yoffset := canvasBox.Top + for index, bv := range normalizedBarComponents { + barHeight := int(bv.Value * float64(canvasBox.Height())) + barBox := Box{Top: yoffset, Left: bxl, Right: bxr, Bottom: yoffset + barHeight} + DrawBox(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index))) + yoffset += barHeight + } + + return bxr +} + +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(DefaultTextColor)) + 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) getDefaultCanvasBox() Box { + return sbc.Box() +} + +func (sbc StackedBarChart) getAdjustedCanvasBox(canvasBox Box) Box { + var totalWidth int + for index, bar := range sbc.Bars { + totalWidth += bar.GetWidth() + if index < len(sbc.Bars)-1 { + totalWidth += sbc.GetBarSpacing() + } + } + + return canvasBox.OuterConstrain(sbc.Box(), 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(DefaultBackgroundPadding.Right) + dpb := sbc.Background.Padding.GetBottom(DefaultBackgroundPadding.Bottom) + + return Box{ + Top: sbc.Background.Padding.GetTop(DefaultBackgroundPadding.Top), + Left: sbc.Background.Padding.GetLeft(DefaultBackgroundPadding.Left), + Right: sbc.GetWidth() - dpr, + Bottom: sbc.GetHeight() - dpb, + } +} + +func (sbc StackedBarChart) styleDefaultsStackedBarValue(index int) Style { + return Style{ + StrokeColor: GetAlternateColor(index), + StrokeWidth: 3.0, + FillColor: GetAlternateColor(index), + } +} + +func (sbc StackedBarChart) styleDefaultsElements() Style { + return Style{ + Font: sbc.GetFont(), + } +} diff --git a/util.go b/util.go index f391d7e..dfb9877 100644 --- a/util.go +++ b/util.go @@ -109,7 +109,7 @@ func Normalize(values ...float64) []float64 { } output := make([]float64, len(values)) for x, v := range values { - output[x] = RoundDown(v/total, 0.001) + output[x] = RoundDown(v/total, 0.00001) } return output } diff --git a/value.go b/value.go index acae226..a1ac67d 100644 --- a/value.go +++ b/value.go @@ -32,7 +32,7 @@ func (vs Values) Normalize() []Value { output[index] = Value{ Style: v.Style, Label: v.Label, - Value: RoundDown(v.Value/total, 0.001), + Value: RoundDown(v.Value/total, 0.00001), } } return output