From 3f245215931296b1de0f5c30fd7363a147a3c79a Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 15 Jun 2022 23:30:37 +0800 Subject: [PATCH] feat: support horizontal bar chart --- alias.go | 4 + axis.go | 25 +++- bar_chart.go | 46 +++--- charts.go | 119 +++++++++++++--- examples/charts/main.go | 195 ++++++++++++++++++++++++++ examples/horizontal_bar_chart/main.go | 94 +++++++++++++ horizontal_bar_chart.go | 152 ++++++++++++++++++++ legend.go | 8 +- line_chart.go | 50 ++++--- mark_line.go | 3 +- mark_point.go | 3 +- series.go | 6 + title.go | 5 + xaxis.go | 33 +++-- yaxis.go | 25 +++- 15 files changed, 677 insertions(+), 91 deletions(-) create mode 100644 examples/horizontal_bar_chart/main.go create mode 100644 horizontal_bar_chart.go diff --git a/alias.go b/alias.go index 0b161e6..a96f50b 100644 --- a/alias.go +++ b/alias.go @@ -31,6 +31,8 @@ type Box = chart.Box type Style = chart.Style type Color = drawing.Color +var BoxZero = chart.BoxZero + type Point struct { X int Y int @@ -42,6 +44,8 @@ const ( ChartTypePie = "pie" ChartTypeRadar = "radar" ChartTypeFunnel = "funnel" + // horizontal bar + ChartTypeHorizontalBar = "horizontalBar" ) const ( diff --git a/axis.go b/axis.go index bd760b6..d069c39 100644 --- a/axis.go +++ b/axis.go @@ -153,6 +153,8 @@ func (a *axisPainter) Render() (Box, error) { padding.Right = top.Width() - width case PositionRight: padding.Left = top.Width() - width + default: + padding.Top = top.Height() - defaultXAxisHeight } p := top.Child(PainterPaddingOption(padding)) @@ -240,7 +242,10 @@ func (a *axisPainter) Render() (Box, error) { x0 = 0 x1 = top.Width() - p.Width() } - for _, y := range autoDivide(height, tickCount) { + for index, y := range autoDivide(height, tickCount) { + if index == 0 { + continue + } top.LineStroke([]Point{ { X: x0, @@ -252,6 +257,24 @@ func (a *axisPainter) Render() (Box, error) { }, }) } + } else { + y0 := p.Height() - defaultXAxisHeight + y1 := top.Height() - defaultXAxisHeight + for index, x := range autoDivide(width, tickCount) { + if index == 0 { + continue + } + top.LineStroke([]Point{ + { + X: x, + Y: y0, + }, + { + X: x, + Y: y1, + }, + }) + } } } diff --git a/bar_chart.go b/bar_chart.go index 9dadb1e..8fae4df 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -60,25 +60,10 @@ type BarChartOption struct { Legend LegendOption } -func (b *barChart) Render() (Box, error) { +func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { p := b.p opt := b.opt - seriesList := opt.SeriesList - seriesList.init() - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: seriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - }) - if err != nil { - return chart.BoxZero, err - } - seriesPainter := renderResult.seriesPainter - seriesList = seriesList.Filter(ChartTypeBar) + seriesPainter := result.seriesPainter xRange := NewRange(AxisRangeOption{ DivideCount: len(opt.XAxis.Data), @@ -112,7 +97,7 @@ func (b *barChart) Render() (Box, error) { } for i := range seriesList { series := seriesList[i] - yRange := renderResult.axisRanges[series.AxisIndex] + yRange := result.axisRanges[series.AxisIndex] index := series.index if index == 0 { index = i @@ -196,10 +181,29 @@ func (b *barChart) Render() (Box, error) { }) } // 最大、最小的mark point - err = doRender(rendererList...) + err := doRender(rendererList...) if err != nil { - return chart.BoxZero, err + return BoxZero, err } - return chart.BoxZero, nil + return p.box, nil +} + +func (b *barChart) Render() (Box, error) { + p := b.p + opt := b.opt + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeLine) + return b.render(renderResult, seriesList) } diff --git a/charts.go b/charts.go index 09dfc3f..ae14c8d 100644 --- a/charts.go +++ b/charts.go @@ -22,6 +22,8 @@ package charts +import "errors" + const labelFontSize = 10 const defaultDotWidth = 2.0 const defaultStrokeWidth = 2.0 @@ -44,6 +46,28 @@ type Renderer interface { Render() (Box, error) } +type renderHandler struct { + list []func() error +} + +func (rh *renderHandler) Add(fn func() error) { + list := rh.list + if len(list) == 0 { + list = make([]func() error, 0) + } + rh.list = append(list, fn) +} + +func (rh *renderHandler) Do() error { + for _, fn := range rh.list { + err := fn() + if err != nil { + return err + } + } + return nil +} + type defaultRenderOption struct { Theme ColorPalette Padding Box @@ -58,6 +82,8 @@ type defaultRenderOption struct { LegendOption LegendOption // background is filled backgroundIsFilled bool + // x y axis is reversed + axisReversed bool } type defaultRenderResult struct { @@ -67,6 +93,8 @@ type defaultRenderResult struct { } func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) { + seriesList := opt.SeriesList + seriesList.init() if !opt.backgroundIsFilled { p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) } @@ -138,7 +166,13 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e if yAxisOption.Theme == nil { yAxisOption.Theme = opt.Theme } - yAxisOption.Data = r.Values() + if !opt.axisReversed { + yAxisOption.Data = r.Values() + } else { + yAxisOption.isCategoryAxis = true + opt.XAxis.Data = r.Values() + opt.XAxis.isValueAxis = true + } reverseStringSlice(yAxisOption.Data) // TODO生成其它位置既yAxis yAxis := NewLeftYAxis(p, yAxisOption) @@ -201,30 +235,71 @@ func Render(opt ChartOption) (*Painter, error) { seriesList := opt.SeriesList seriesList.init() - rendererList := make([]Renderer, 0) - // line chart - lineChartSeriesList := seriesList.Filter(ChartTypeLine) - if len(lineChartSeriesList) != 0 { - renderer := NewLineChart(p, LineChartOption{ - Theme: opt.theme, - Font: opt.font, - SeriesList: lineChartSeriesList, - XAxis: opt.XAxis, - Padding: opt.Padding, - YAxisOptions: opt.YAxisOptions, - Title: opt.Title, - Legend: opt.Legend, - backgroundIsFilled: true, - }) - rendererList = append(rendererList, renderer) + lineSeriesList := seriesList.Filter(ChartTypeLine) + barSeriesList := seriesList.Filter(ChartTypeBar) + horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar) + if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != len(seriesList) { + return nil, errors.New("Horizontal bar can not mix other charts") } - for _, renderer := range rendererList { - _, err := renderer.Render() - if err != nil { - return nil, err - } + axisReversed := len(horizontalBarSeriesList) != 0 + + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + axisReversed: axisReversed, + }) + if err != nil { + return nil, err + } + + handler := renderHandler{} + + if len(lineSeriesList) != 0 { + handler.Add(func() error { + _, err := NewLineChart(p, LineChartOption{ + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + }).render(renderResult, lineSeriesList) + return err + }) + } + + // bar chart + if len(barSeriesList) != 0 { + handler.Add(func() error { + _, err := NewBarChart(p, BarChartOption{ + Theme: opt.theme, + Font: opt.font, + XAxis: opt.XAxis, + }).render(renderResult, barSeriesList) + return err + }) + } + + // horizontal bar chart + if len(horizontalBarSeriesList) != 0 { + handler.Add(func() error { + _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{ + Theme: opt.theme, + Font: opt.font, + YAxisOptions: opt.YAxisOptions, + }).render(renderResult, horizontalBarSeriesList) + return err + }) + } + + err = handler.Do() + + if err != nil { + return nil, err } return p, nil diff --git a/examples/charts/main.go b/examples/charts/main.go index 18f5a95..c7986a4 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -218,6 +218,201 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, + // 柱状图 + { + Title: charts.TitleOption{ + Text: "Bar", + }, + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + Legend: charts.LegendOption{ + Data: []string{ + "Rainfall", + "Evaporation", + }, + Icon: charts.IconRect, + }, + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 120, + 200, + 150, + 80, + 70, + 110, + 130, + }, charts.ChartTypeBar), + { + Type: charts.ChartTypeBar, + Data: []charts.SeriesData{ + { + Value: 100, + }, + { + Value: 190, + Style: charts.Style{ + FillColor: charts.Color{ + R: 169, + G: 0, + B: 0, + A: 255, + }, + }, + }, + { + Value: 230, + }, + { + Value: 140, + }, + { + Value: 100, + }, + { + Value: 200, + }, + { + Value: 180, + }, + }, + }, + }, + }, + // 水平柱状图 + { + Title: charts.TitleOption{ + Text: "World Population", + }, + Padding: charts.Box{ + Top: 20, + Right: 40, + Bottom: 20, + Left: 20, + }, + Legend: charts.NewLegendOption([]string{ + "2011", + "2012", + }), + YAxisOptions: charts.NewYAxisOptions([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }), + }, + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, + }), + }, + }, + }, + { + Title: charts.TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + }, + Padding: charts.Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + XAxis: charts.NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + Legend: charts.NewLegendOption([]string{ + "Rainfall", + "Evaporation", + }, charts.PositionRight), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + }, + }, } handler(w, req, chartOptions, nil) } diff --git a/examples/horizontal_bar_chart/main.go b/examples/horizontal_bar_chart/main.go new file mode 100644 index 0000000..eecd9ec --- /dev/null +++ b/examples/horizontal_bar_chart/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "horizontal-bar-chart.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.NewPainter(charts.PainterOptions{ + Width: 800, + Height: 600, + Type: charts.ChartOutputPNG, + }) + if err != nil { + panic(err) + } + _, err = charts.NewHorizontalBarChart(p, charts.HorizontalBarChartOption{ + Title: charts.TitleOption{ + Text: "World Population", + }, + Padding: charts.Box{ + Top: 20, + Right: 40, + Bottom: 20, + Left: 20, + }, + Legend: charts.NewLegendOption([]string{ + "2011", + "2012", + }), + YAxisOptions: charts.NewYAxisOptions([]string{ + "Brazil", + "Indonesia", + "USA", + "India", + "China", + "World", + }), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 18203, + 23489, + 29034, + 104970, + 131744, + 630230, + }), + }, + { + Type: charts.ChartTypeHorizontalBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 19325, + 23438, + 31000, + 121594, + 134141, + 681807, + }), + }, + }, + }).Render() + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go new file mode 100644 index 0000000..87ca9ae --- /dev/null +++ b/horizontal_bar_chart.go @@ -0,0 +1,152 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +import ( + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" +) + +type horizontalBarChart struct { + p *Painter + opt *HorizontalBarChartOption +} + +type HorizontalBarChartOption struct { + Theme ColorPalette + // The font size + Font *truetype.Font + // The data series list + SeriesList SeriesList + // The x axis option + XAxis XAxisOption + // The padding of line chart + Padding Box + // The y axis option + YAxisOptions []YAxisOption + // The option of title + Title TitleOption + // The legend option + Legend LegendOption +} + +func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart { + if opt.Theme == nil { + opt.Theme = NewTheme("") + } + return &horizontalBarChart{ + p: p, + opt: &opt, + } +} + +func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { + p := h.p + opt := h.opt + seriesPainter := result.seriesPainter + yRange := result.axisRanges[0] + y0, y1 := yRange.GetRange(0) + height := int(y1 - y0) + // 每一块之间的margin + margin := 10 + // 每一个bar之间的margin + barMargin := 5 + if height < 20 { + margin = 2 + barMargin = 2 + } else if height < 50 { + margin = 5 + barMargin = 3 + } + seriesCount := len(seriesList) + // 总的高度-两个margin-(总数-1)的barMargin + barHeight := (height - 2*margin - barMargin*(seriesCount-1)) / len(seriesList) + + theme := opt.Theme + + max, min := seriesList.GetMaxMin(0) + xRange := NewRange(AxisRangeOption{ + Min: min, + Max: max, + DivideCount: defaultAxisDivideCount, + Size: seriesPainter.Width(), + }) + + for i := range seriesList { + series := seriesList[i] + index := series.index + if index == 0 { + index = i + } + seriesColor := theme.GetSeriesColor(index) + divideValues := yRange.AutoDivide() + for j, item := range series.Data { + if j >= yRange.divideCount { + continue + } + // 显示位置切换 + j = yRange.divideCount - j - 1 + y := divideValues[j] + y += margin + if i != 0 { + y += i * (barHeight + barMargin) + } + + w := int(xRange.getHeight(item.Value)) + fillColor := seriesColor + if !item.Style.FillColor.IsZero() { + fillColor = item.Style.FillColor + } + right := w + seriesPainter.OverrideDrawingStyle(Style{ + FillColor: fillColor, + }).Rect(chart.Box{ + Top: y, + Left: 0, + Right: right, + Bottom: y + barHeight, + }) + } + } + return p.box, nil +} + +func (h *horizontalBarChart) Render() (Box, error) { + p := h.p + opt := h.opt + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + axisReversed: true, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeHorizontalBar) + return h.render(renderResult, seriesList) +} diff --git a/legend.go b/legend.go index e645e17..65793c9 100644 --- a/legend.go +++ b/legend.go @@ -155,17 +155,17 @@ func (l *legendPainter) Render() (Box, error) { drawIcon := func(top, left int) int { if opt.Icon == IconRect { p.Rect(Box{ - Top: top - legendHeight + 4, + Top: top - legendHeight + 8, Left: left, Right: left + legendWidth, - Bottom: top - 2, + Bottom: top + 1, }) } else { p.LegendLineDot(Box{ - Top: top, + Top: top + 1, Left: left, Right: left + legendWidth, - Bottom: top + legendHeight, + Bottom: top + legendHeight + 1, }) } return left + legendWidth diff --git a/line_chart.go b/line_chart.go index 451edfe..47a497f 100644 --- a/line_chart.go +++ b/line_chart.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) @@ -63,32 +62,15 @@ type LineChartOption struct { backgroundIsFilled bool } -func (l *lineChart) Render() (Box, error) { +func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { p := l.p opt := l.opt - seriesList := opt.SeriesList - seriesList.init() - renderResult, err := defaultRender(p, defaultRenderOption{ - Theme: opt.Theme, - Padding: opt.Padding, - SeriesList: seriesList, - XAxis: opt.XAxis, - YAxisOptions: opt.YAxisOptions, - TitleOption: opt.Title, - LegendOption: opt.Legend, - backgroundIsFilled: opt.backgroundIsFilled, - }) - if err != nil { - return chart.BoxZero, err - } boundaryGap := true if opt.XAxis.BoundaryGap != nil && !*opt.XAxis.BoundaryGap { boundaryGap = false } - seriesList = seriesList.Filter(ChartTypeLine) - - seriesPainter := renderResult.seriesPainter + seriesPainter := result.seriesPainter xDivideCount := len(opt.XAxis.Data) if !boundaryGap { @@ -118,7 +100,7 @@ func (l *lineChart) Render() (Box, error) { } seriesPainter.SetDrawingStyle(drawingStyle) - yRange := renderResult.axisRanges[series.AxisIndex] + yRange := result.axisRanges[series.AxisIndex] points := make([]Point, 0) for i, item := range series.Data { h := yRange.getRestHeight(item.Value) @@ -156,10 +138,32 @@ func (l *lineChart) Render() (Box, error) { }) } // 最大、最小的mark point - err = doRender(rendererList...) + err := doRender(rendererList...) if err != nil { - return chart.BoxZero, err + return BoxZero, err } return p.box, nil } + +func (l *lineChart) Render() (Box, error) { + p := l.p + opt := l.opt + + renderResult, err := defaultRender(p, defaultRenderOption{ + Theme: opt.Theme, + Padding: opt.Padding, + SeriesList: opt.SeriesList, + XAxis: opt.XAxis, + YAxisOptions: opt.YAxisOptions, + TitleOption: opt.Title, + LegendOption: opt.Legend, + backgroundIsFilled: opt.backgroundIsFilled, + }) + if err != nil { + return BoxZero, err + } + seriesList := opt.SeriesList.Filter(ChartTypeLine) + + return l.render(renderResult, seriesList) +} diff --git a/mark_line.go b/mark_line.go index 9a9d568..bb1b602 100644 --- a/mark_line.go +++ b/mark_line.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) func NewMarkLine(markLineTypes ...string) SeriesMarkLine { @@ -104,7 +103,7 @@ func (m *markLinePainter) Render() (Box, error) { painter.Text(text, width, y+textBox.Height()>>1-2) } } - return chart.BoxZero, nil + return BoxZero, nil } func markLineRender(opt markLineRenderOption) { diff --git a/mark_point.go b/mark_point.go index 07daf57..3d43a73 100644 --- a/mark_point.go +++ b/mark_point.go @@ -24,7 +24,6 @@ package charts import ( "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" ) func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { @@ -99,5 +98,5 @@ func (m *markPointPainter) Render() (Box, error) { painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) } } - return chart.BoxZero, nil + return BoxZero, nil } diff --git a/series.go b/series.go index 2888f30..4808bcb 100644 --- a/series.go +++ b/series.go @@ -132,6 +132,12 @@ func (sl SeriesList) init() { } } +func (sl SeriesList) reverse() { + for i, j := 0, len(sl)-1; i < j; i, j = i+1, j-1 { + sl[i], sl[j] = sl[j], sl[i] + } +} + func (sl SeriesList) Filter(chartType string) SeriesList { arr := make(SeriesList, 0) for index, item := range sl { diff --git a/title.go b/title.go index 30831ac..a805c55 100644 --- a/title.go +++ b/title.go @@ -95,6 +95,11 @@ func (t *titlePainter) Render() (Box, error) { opt := t.opt p := t.p theme := opt.Theme + + if opt.Text == "" && opt.Subtext == "" { + return BoxZero, nil + } + measureOptions := make([]titleMeasureOption, 0) if opt.Font == nil { diff --git a/xaxis.go b/xaxis.go index d8f6700..f06d71f 100644 --- a/xaxis.go +++ b/xaxis.go @@ -47,7 +47,8 @@ type XAxisOption struct { // The line color of axis StrokeColor Color // The color of label - FontColor Color + FontColor Color + isValueAxis bool } const defaultXAxisHeight = 30 @@ -67,22 +68,26 @@ func (opt *XAxisOption) ToAxisOption() AxisOption { if opt.Position == PositionTop { position = PositionTop } - return AxisOption{ - Theme: opt.Theme, - Data: opt.Data, - BoundaryGap: opt.BoundaryGap, - Position: position, - SplitNumber: opt.SplitNumber, - StrokeColor: opt.StrokeColor, - FontSize: opt.FontSize, - Font: opt.Font, - FontColor: opt.FontColor, + axisOpt := AxisOption{ + Theme: opt.Theme, + Data: opt.Data, + BoundaryGap: opt.BoundaryGap, + Position: position, + SplitNumber: opt.SplitNumber, + StrokeColor: opt.StrokeColor, + FontSize: opt.FontSize, + Font: opt.Font, + FontColor: opt.FontColor, + SplitLineColor: opt.Theme.GetAxisSplitLineColor(), } + if opt.isValueAxis { + axisOpt.SplitLineShow = true + axisOpt.StrokeWidth = -1 + axisOpt.BoundaryGap = FalseFlag() + } + return axisOpt } func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter { - p = p.Child(PainterPaddingOption(Box{ - Top: p.Height() - defaultXAxisHeight, - })) return NewAxisPainter(p, opt.ToAxisOption()) } diff --git a/yaxis.go b/yaxis.go index b011a74..609924f 100644 --- a/yaxis.go +++ b/yaxis.go @@ -36,7 +36,22 @@ type YAxisOption struct { // The position of axis, it can be 'left' or 'right' Position string // The color of label - FontColor Color + FontColor Color + isCategoryAxis bool +} + +func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { + arr := [][]string{ + data, + } + arr = append(arr, others...) + opts := make([]YAxisOption, 0) + for _, data := range arr { + opts = append(opts, YAxisOption{ + Data: data, + }) + } + return opts } func (opt *YAxisOption) ToAxisOption() AxisOption { @@ -44,7 +59,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { if opt.Position == PositionRight { position = PositionRight } - return AxisOption{ + axisOpt := AxisOption{ Theme: opt.Theme, Data: opt.Data, Position: position, @@ -56,6 +71,12 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { SplitLineShow: true, SplitLineColor: opt.Theme.GetAxisSplitLineColor(), } + if opt.isCategoryAxis { + axisOpt.BoundaryGap = TrueFlag() + axisOpt.StrokeWidth = 1 + axisOpt.SplitLineShow = false + } + return axisOpt } func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {