From fead23ae5b9a07d8d4940c668d2badfa2d3526eb Mon Sep 17 00:00:00 2001 From: Jamie Isaacs Date: Thu, 7 Mar 2019 00:36:49 -0800 Subject: [PATCH] Add horizontal render option to stacked bar chart (#39) --- _examples/horizontal_stacked_bar/main.go | 222 ++++++++++++++++++++ _examples/horizontal_stacked_bar/output.png | Bin 0 -> 34138 bytes draw.go | 6 +- stacked_bar_chart.go | 196 ++++++++++++++++- text.go | 2 +- 5 files changed, 418 insertions(+), 8 deletions(-) create mode 100644 _examples/horizontal_stacked_bar/main.go create mode 100644 _examples/horizontal_stacked_bar/output.png diff --git a/_examples/horizontal_stacked_bar/main.go b/_examples/horizontal_stacked_bar/main.go new file mode 100644 index 0000000..4447c03 --- /dev/null +++ b/_examples/horizontal_stacked_bar/main.go @@ -0,0 +1,222 @@ +package main + +import ( + "os" + + "github.com/wcharczuk/go-chart" + "github.com/wcharczuk/go-chart/drawing" +) + +func main() { + chart.DefaultBackgroundColor = chart.ColorTransparent + chart.DefaultCanvasColor = chart.ColorTransparent + + barWidth := 80 + + var ( + colorWhite = drawing.Color{R: 241, G: 241, B: 241, A: 255} + colorMariner = drawing.Color{R: 60, G: 100, B: 148, A: 255} + colorLightSteelBlue = drawing.Color{R: 182, G: 195, B: 220, A: 255} + colorPoloBlue = drawing.Color{R: 126, G: 155, B: 200, A: 255} + colorSteelBlue = drawing.Color{R: 73, G: 120, B: 177, A: 255} + ) + + stackedBarChart := chart.StackedBarChart{ + Title: "Quarterly Sales", + TitleStyle: chart.StyleShow(), + Background: chart.Style{ + Padding: chart.Box{ + Top: 75, + }, + }, + Width: 800, + Height: 600, + XAxis: chart.StyleShow(), + YAxis: chart.StyleShow(), + BarSpacing: 40, + IsHorizontal: true, + Bars: []chart.StackedBar{ + { + Name: "Q1", + Width: barWidth, + Values: []chart.Value{ + { + Label: "32K", + Value: 32, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorMariner, + FontColor: colorWhite, + }, + }, + { + Label: "46K", + Value: 46, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorLightSteelBlue, + FontColor: colorWhite, + }, + }, + { + Label: "48K", + Value: 48, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorPoloBlue, + FontColor: colorWhite, + }, + }, + { + Label: "42K", + Value: 42, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorSteelBlue, + FontColor: colorWhite, + }, + }, + }, + }, + { + Name: "Q2", + Width: barWidth, + Values: []chart.Value{ + { + Label: "45K", + Value: 45, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorMariner, + FontColor: colorWhite, + }, + }, + { + Label: "60K", + Value: 60, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorLightSteelBlue, + FontColor: colorWhite, + }, + }, + { + Label: "62K", + Value: 62, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorPoloBlue, + FontColor: colorWhite, + }, + }, + { + Label: "53K", + Value: 53, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorSteelBlue, + FontColor: colorWhite, + }, + }, + }, + }, + { + Name: "Q3", + Width: barWidth, + Values: []chart.Value{ + { + Label: "54K", + Value: 54, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorMariner, + FontColor: colorWhite, + }, + }, + { + Label: "58K", + Value: 58, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorLightSteelBlue, + FontColor: colorWhite, + }, + }, + { + Label: "55K", + Value: 55, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorPoloBlue, + FontColor: colorWhite, + }, + }, + { + Label: "47K", + Value: 47, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorSteelBlue, + FontColor: colorWhite, + }, + }, + }, + }, + { + Name: "Q4", + Width: barWidth, + Values: []chart.Value{ + { + Label: "46K", + Value: 46, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorMariner, + FontColor: colorWhite, + }, + }, + { + Label: "70K", + Value: 70, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorLightSteelBlue, + FontColor: colorWhite, + }, + }, + { + Label: "74K", + Value: 74, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorPoloBlue, + FontColor: colorWhite, + }, + }, + { + Label: "60K", + Value: 60, + Style: chart.Style{ + StrokeWidth: .01, + FillColor: colorSteelBlue, + FontColor: colorWhite, + }, + }, + }, + }, + }, + } + + pngFile, err := os.Create("output.png") + if err != nil { + panic(err) + } + + if err := stackedBarChart.Render(chart.PNG, pngFile); err != nil { + panic(err) + } + + if err := pngFile.Close(); err != nil { + panic(err) + } +} diff --git a/_examples/horizontal_stacked_bar/output.png b/_examples/horizontal_stacked_bar/output.png new file mode 100644 index 0000000000000000000000000000000000000000..8cb0e94776fa4ed6d47e36bed34245a87affe059 GIT binary patch literal 34138 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 1 + case TextVerticalAlignMiddle: + y = y + (box.Height() >> 1) - (linesBox.Height() >> 1) + case TextVerticalAlignMiddleBaseline: + y = y + (box.Height() >> 1) - linesBox.Height() } var tx, ty int diff --git a/stacked_bar_chart.go b/stacked_bar_chart.go index a7740cd..befe99a 100644 --- a/stacked_bar_chart.go +++ b/stacked_bar_chart.go @@ -46,6 +46,8 @@ type StackedBarChart struct { Font *truetype.Font defaultFont *truetype.Font + IsHorizontal bool + Bars []StackedBar Elements []Renderable } @@ -113,11 +115,20 @@ func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error { } 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) + var canvasBox Box + if sbc.IsHorizontal { + canvasBox = sbc.getHorizontalAdjustedCanvasBox(r, sbc.getDefaultCanvasBox()) + sbc.drawCanvas(r, canvasBox) + sbc.drawHorizontalBars(r, canvasBox) + sbc.drawHorizontalXAxis(r, canvasBox) + sbc.drawHorizontalYAxis(r, canvasBox) + } else { + 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 { @@ -139,6 +150,14 @@ func (sbc StackedBarChart) drawBars(r Renderer, canvasBox Box) { } } +func (sbc StackedBarChart) drawHorizontalBars(r Renderer, canvasBox Box) { + yOffset := canvasBox.Top + for _, bar := range sbc.Bars { + sbc.drawHorizontalBar(r, canvasBox, yOffset, bar) + yOffset += sbc.GetBarSpacing() + bar.GetWidth() + } +} + func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar StackedBar) int { barSpacing2 := sbc.GetBarSpacing() >> 1 bxl := xoffset + barSpacing2 @@ -188,6 +207,55 @@ func (sbc StackedBarChart) drawBar(r Renderer, canvasBox Box, xoffset int, bar S return bxr } +func (sbc StackedBarChart) drawHorizontalBar(r Renderer, canvasBox Box, yoffset int, bar StackedBar) { + halfBarSpacing := sbc.GetBarSpacing() >> 1 + + boxTop := yoffset + halfBarSpacing + boxBottom := boxTop + bar.GetWidth() + + normalizedBarComponents := Values(bar.Values).Normalize() + + xOffset := canvasBox.Right + for index, bv := range normalizedBarComponents { + barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Width()))) + barBox := Box{ + Top: boxTop, + Left: util.Math.MinInt(xOffset-barHeight, canvasBox.Left+DefaultStrokeWidth), + Right: xOffset, + Bottom: boxBottom, + } + Draw.Box(r, barBox, bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index))) + xOffset -= barHeight + } + + // draw the labels + xOffset = canvasBox.Right + var lx, ly int + for index, bv := range normalizedBarComponents { + barHeight := int(math.Ceil(bv.Value * float64(canvasBox.Width()))) + + if len(bv.Label) > 0 { + lx = xOffset - (barHeight / 2) + ly = boxTop + ((boxBottom - boxTop) / 2) + + bv.Style.InheritFrom(sbc.styleDefaultsStackedBarValue(index)).WriteToRenderer(r) + tb := r.MeasureText(bv.Label) + lx = lx - (tb.Width() >> 1) + ly = ly + (tb.Height() >> 1) + + if lx < 0 { + lx = 0 + } + if ly < 0 { + lx = 0 + } + + r.Text(bv.Label, lx, ly) + } + xOffset -= barHeight + } +} + func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) { if !sbc.XAxis.Hidden { axisStyle := sbc.XAxis.InheritFrom(sbc.styleDefaultsAxes()) @@ -222,6 +290,37 @@ func (sbc StackedBarChart) drawXAxis(r Renderer, canvasBox Box) { } } +func (sbc StackedBarChart) drawHorizontalXAxis(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() + + ticks := seq.RangeWithStep(0.0, 1.0, 0.2) + for _, t := range ticks { + axisStyle.GetStrokeOptions().WriteToRenderer(r) + tx := canvasBox.Left + int(t*float64(canvasBox.Width())) + r.MoveTo(tx, canvasBox.Bottom) + r.LineTo(tx, canvasBox.Bottom+DefaultVerticalTickHeight) + r.Stroke() + + axisStyle.GetTextOptions().WriteToRenderer(r) + text := fmt.Sprintf("%0.0f%%", t*100) + + textBox := r.MeasureText(text) + textX := tx - (textBox.Width() >> 1) + textY := canvasBox.Bottom + DefaultXAxisMargin + 10 + Draw.Text(r, text, textX, textY, axisStyle) + } + } +} + func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { if !sbc.YAxis.Hidden { axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsAxes()) @@ -248,7 +347,39 @@ func (sbc StackedBarChart) drawYAxis(r Renderer, canvasBox Box) { tb := r.MeasureText(text) Draw.Text(r, text, canvasBox.Right+DefaultYAxisMargin+5, ty+(tb.Height()>>1), axisStyle) } + } +} +func (sbc StackedBarChart) drawHorizontalYAxis(r Renderer, canvasBox Box) { + if sbc.YAxis.Show { + axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsHorizontalAxes()) + axisStyle.WriteToRenderer(r) + + r.MoveTo(canvasBox.Left, canvasBox.Bottom) + r.LineTo(canvasBox.Left, canvasBox.Top) + r.Stroke() + + r.MoveTo(canvasBox.Left, canvasBox.Bottom) + r.LineTo(canvasBox.Left-DefaultHorizontalTickWidth, canvasBox.Bottom) + r.Stroke() + + cursor := canvasBox.Top + for _, bar := range sbc.Bars { + barLabelBox := Box{ + Top: cursor, + Left: 0, + Right: canvasBox.Left + DefaultYAxisMargin, + Bottom: cursor + bar.GetWidth() + sbc.GetBarSpacing(), + } + if len(bar.Name) > 0 { + Draw.TextWithin(r, bar.Name, barLabelBox, axisStyle) + } + axisStyle.WriteToRenderer(r) + r.MoveTo(canvasBox.Left, barLabelBox.Bottom) + r.LineTo(canvasBox.Left-DefaultHorizontalTickWidth, barLabelBox.Bottom) + r.Stroke() + cursor += bar.GetWidth() + sbc.GetBarSpacing() + } } } @@ -338,6 +469,48 @@ func (sbc StackedBarChart) getAdjustedCanvasBox(r Renderer, canvasBox Box) Box { } +func (sbc StackedBarChart) getHorizontalAdjustedCanvasBox(r Renderer, canvasBox Box) Box { + var totalHeight int + for _, bar := range sbc.Bars { + totalHeight += bar.GetWidth() + sbc.GetBarSpacing() + } + + if sbc.YAxis.Show { + yAxisWidth := DefaultHorizontalTickWidth + + axisStyle := sbc.YAxis.InheritFrom(sbc.styleDefaultsHorizontalAxes()) + axisStyle.WriteToRenderer(r) + + cursor := canvasBox.Top + for _, bar := range sbc.Bars { + if len(bar.Name) > 0 { + barLabelBox := Box{ + Top: cursor, + Left: 0, + Right: canvasBox.Left + DefaultYAxisMargin, + Bottom: cursor + bar.GetWidth() + sbc.GetBarSpacing(), + } + lines := Text.WrapFit(r, bar.Name, barLabelBox.Width(), axisStyle) + linesBox := Text.MeasureLines(r, lines, axisStyle) + + yAxisWidth = util.Math.MaxInt(linesBox.Height()+(2*DefaultXAxisMargin), yAxisWidth) + } + } + return Box{ + Top: canvasBox.Top, + Left: canvasBox.Left + yAxisWidth, + Right: canvasBox.Right, + Bottom: canvasBox.Top + totalHeight, + } + } + return Box{ + Top: canvasBox.Top, + Left: canvasBox.Left, + Right: canvasBox.Right, + Bottom: canvasBox.Top + totalHeight, + } +} + // Box returns the chart bounds as a box. func (sbc StackedBarChart) Box() Box { dpr := sbc.Background.Padding.GetRight(10) @@ -412,6 +585,19 @@ func (sbc StackedBarChart) styleDefaultsAxes() Style { TextWrap: TextWrapWord, } } + +func (sbc StackedBarChart) styleDefaultsHorizontalAxes() Style { + return Style{ + StrokeColor: DefaultAxisColor, + Font: sbc.GetFont(), + FontSize: DefaultAxisFontSize, + FontColor: DefaultAxisColor, + TextHorizontalAlign: TextHorizontalAlignCenter, + TextVerticalAlign: TextVerticalAlignMiddle, + TextWrap: TextWrapWord, + } +} + func (sbc StackedBarChart) styleDefaultsElements() Style { return Style{ Font: sbc.GetFont(), diff --git a/text.go b/text.go index 37750ab..0a9dfd0 100644 --- a/text.go +++ b/text.go @@ -46,7 +46,7 @@ const ( 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 aligns the text vertically 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