feat: support horizontal bar chart

This commit is contained in:
vicanso 2022-06-15 23:30:37 +08:00
parent b69728dd12
commit 3f24521593
15 changed files with 677 additions and 91 deletions

View file

@ -31,6 +31,8 @@ type Box = chart.Box
type Style = chart.Style type Style = chart.Style
type Color = drawing.Color type Color = drawing.Color
var BoxZero = chart.BoxZero
type Point struct { type Point struct {
X int X int
Y int Y int
@ -42,6 +44,8 @@ const (
ChartTypePie = "pie" ChartTypePie = "pie"
ChartTypeRadar = "radar" ChartTypeRadar = "radar"
ChartTypeFunnel = "funnel" ChartTypeFunnel = "funnel"
// horizontal bar
ChartTypeHorizontalBar = "horizontalBar"
) )
const ( const (

25
axis.go
View file

@ -153,6 +153,8 @@ func (a *axisPainter) Render() (Box, error) {
padding.Right = top.Width() - width padding.Right = top.Width() - width
case PositionRight: case PositionRight:
padding.Left = top.Width() - width padding.Left = top.Width() - width
default:
padding.Top = top.Height() - defaultXAxisHeight
} }
p := top.Child(PainterPaddingOption(padding)) p := top.Child(PainterPaddingOption(padding))
@ -240,7 +242,10 @@ func (a *axisPainter) Render() (Box, error) {
x0 = 0 x0 = 0
x1 = top.Width() - p.Width() 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{ top.LineStroke([]Point{
{ {
X: x0, 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,
},
})
}
} }
} }

View file

@ -60,25 +60,10 @@ type BarChartOption struct {
Legend LegendOption Legend LegendOption
} }
func (b *barChart) Render() (Box, error) { func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
p := b.p p := b.p
opt := b.opt opt := b.opt
seriesList := opt.SeriesList seriesPainter := result.seriesPainter
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)
xRange := NewRange(AxisRangeOption{ xRange := NewRange(AxisRangeOption{
DivideCount: len(opt.XAxis.Data), DivideCount: len(opt.XAxis.Data),
@ -112,7 +97,7 @@ func (b *barChart) Render() (Box, error) {
} }
for i := range seriesList { for i := range seriesList {
series := seriesList[i] series := seriesList[i]
yRange := renderResult.axisRanges[series.AxisIndex] yRange := result.axisRanges[series.AxisIndex]
index := series.index index := series.index
if index == 0 { if index == 0 {
index = i index = i
@ -196,10 +181,29 @@ func (b *barChart) Render() (Box, error) {
}) })
} }
// 最大、最小的mark point // 最大、最小的mark point
err = doRender(rendererList...) err := doRender(rendererList...)
if err != nil { 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)
} }

119
charts.go
View file

@ -22,6 +22,8 @@
package charts package charts
import "errors"
const labelFontSize = 10 const labelFontSize = 10
const defaultDotWidth = 2.0 const defaultDotWidth = 2.0
const defaultStrokeWidth = 2.0 const defaultStrokeWidth = 2.0
@ -44,6 +46,28 @@ type Renderer interface {
Render() (Box, error) 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 { type defaultRenderOption struct {
Theme ColorPalette Theme ColorPalette
Padding Box Padding Box
@ -58,6 +82,8 @@ type defaultRenderOption struct {
LegendOption LegendOption LegendOption LegendOption
// background is filled // background is filled
backgroundIsFilled bool backgroundIsFilled bool
// x y axis is reversed
axisReversed bool
} }
type defaultRenderResult struct { type defaultRenderResult struct {
@ -67,6 +93,8 @@ type defaultRenderResult struct {
} }
func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) { func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) {
seriesList := opt.SeriesList
seriesList.init()
if !opt.backgroundIsFilled { if !opt.backgroundIsFilled {
p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor()) 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 { if yAxisOption.Theme == nil {
yAxisOption.Theme = opt.Theme 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) reverseStringSlice(yAxisOption.Data)
// TODO生成其它位置既yAxis // TODO生成其它位置既yAxis
yAxis := NewLeftYAxis(p, yAxisOption) yAxis := NewLeftYAxis(p, yAxisOption)
@ -201,30 +235,71 @@ func Render(opt ChartOption) (*Painter, error) {
seriesList := opt.SeriesList seriesList := opt.SeriesList
seriesList.init() seriesList.init()
rendererList := make([]Renderer, 0)
// line chart // line chart
lineChartSeriesList := seriesList.Filter(ChartTypeLine) lineSeriesList := seriesList.Filter(ChartTypeLine)
if len(lineChartSeriesList) != 0 { barSeriesList := seriesList.Filter(ChartTypeBar)
renderer := NewLineChart(p, LineChartOption{ horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar)
Theme: opt.theme, if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != len(seriesList) {
Font: opt.font, return nil, errors.New("Horizontal bar can not mix other charts")
SeriesList: lineChartSeriesList,
XAxis: opt.XAxis,
Padding: opt.Padding,
YAxisOptions: opt.YAxisOptions,
Title: opt.Title,
Legend: opt.Legend,
backgroundIsFilled: true,
})
rendererList = append(rendererList, renderer)
} }
for _, renderer := range rendererList { axisReversed := len(horizontalBarSeriesList) != 0
_, err := renderer.Render()
if err != nil { renderResult, err := defaultRender(p, defaultRenderOption{
return nil, err 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 return p, nil

View file

@ -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) handler(w, req, chartOptions, nil)
} }

View file

@ -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)
}
}

152
horizontal_bar_chart.go Normal file
View file

@ -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)
}

View file

@ -155,17 +155,17 @@ func (l *legendPainter) Render() (Box, error) {
drawIcon := func(top, left int) int { drawIcon := func(top, left int) int {
if opt.Icon == IconRect { if opt.Icon == IconRect {
p.Rect(Box{ p.Rect(Box{
Top: top - legendHeight + 4, Top: top - legendHeight + 8,
Left: left, Left: left,
Right: left + legendWidth, Right: left + legendWidth,
Bottom: top - 2, Bottom: top + 1,
}) })
} else { } else {
p.LegendLineDot(Box{ p.LegendLineDot(Box{
Top: top, Top: top + 1,
Left: left, Left: left,
Right: left + legendWidth, Right: left + legendWidth,
Bottom: top + legendHeight, Bottom: top + legendHeight + 1,
}) })
} }
return left + legendWidth return left + legendWidth

View file

@ -24,7 +24,6 @@ package charts
import ( import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing" "github.com/wcharczuk/go-chart/v2/drawing"
) )
@ -63,32 +62,15 @@ type LineChartOption struct {
backgroundIsFilled bool backgroundIsFilled bool
} }
func (l *lineChart) Render() (Box, error) { func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
p := l.p p := l.p
opt := l.opt 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 boundaryGap := true
if opt.XAxis.BoundaryGap != nil && !*opt.XAxis.BoundaryGap { if opt.XAxis.BoundaryGap != nil && !*opt.XAxis.BoundaryGap {
boundaryGap = false boundaryGap = false
} }
seriesList = seriesList.Filter(ChartTypeLine) seriesPainter := result.seriesPainter
seriesPainter := renderResult.seriesPainter
xDivideCount := len(opt.XAxis.Data) xDivideCount := len(opt.XAxis.Data)
if !boundaryGap { if !boundaryGap {
@ -118,7 +100,7 @@ func (l *lineChart) Render() (Box, error) {
} }
seriesPainter.SetDrawingStyle(drawingStyle) seriesPainter.SetDrawingStyle(drawingStyle)
yRange := renderResult.axisRanges[series.AxisIndex] yRange := result.axisRanges[series.AxisIndex]
points := make([]Point, 0) points := make([]Point, 0)
for i, item := range series.Data { for i, item := range series.Data {
h := yRange.getRestHeight(item.Value) h := yRange.getRestHeight(item.Value)
@ -156,10 +138,32 @@ func (l *lineChart) Render() (Box, error) {
}) })
} }
// 最大、最小的mark point // 最大、最小的mark point
err = doRender(rendererList...) err := doRender(rendererList...)
if err != nil { if err != nil {
return chart.BoxZero, err return BoxZero, err
} }
return p.box, nil 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)
}

View file

@ -24,7 +24,6 @@ package charts
import ( import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
) )
func NewMarkLine(markLineTypes ...string) SeriesMarkLine { func NewMarkLine(markLineTypes ...string) SeriesMarkLine {
@ -104,7 +103,7 @@ func (m *markLinePainter) Render() (Box, error) {
painter.Text(text, width, y+textBox.Height()>>1-2) painter.Text(text, width, y+textBox.Height()>>1-2)
} }
} }
return chart.BoxZero, nil return BoxZero, nil
} }
func markLineRender(opt markLineRenderOption) { func markLineRender(opt markLineRenderOption) {

View file

@ -24,7 +24,6 @@ package charts
import ( import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
) )
func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { 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) painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
} }
} }
return chart.BoxZero, nil return BoxZero, nil
} }

View file

@ -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 { func (sl SeriesList) Filter(chartType string) SeriesList {
arr := make(SeriesList, 0) arr := make(SeriesList, 0)
for index, item := range sl { for index, item := range sl {

View file

@ -95,6 +95,11 @@ func (t *titlePainter) Render() (Box, error) {
opt := t.opt opt := t.opt
p := t.p p := t.p
theme := opt.Theme theme := opt.Theme
if opt.Text == "" && opt.Subtext == "" {
return BoxZero, nil
}
measureOptions := make([]titleMeasureOption, 0) measureOptions := make([]titleMeasureOption, 0)
if opt.Font == nil { if opt.Font == nil {

View file

@ -47,7 +47,8 @@ type XAxisOption struct {
// The line color of axis // The line color of axis
StrokeColor Color StrokeColor Color
// The color of label // The color of label
FontColor Color FontColor Color
isValueAxis bool
} }
const defaultXAxisHeight = 30 const defaultXAxisHeight = 30
@ -67,22 +68,26 @@ func (opt *XAxisOption) ToAxisOption() AxisOption {
if opt.Position == PositionTop { if opt.Position == PositionTop {
position = PositionTop position = PositionTop
} }
return AxisOption{ axisOpt := AxisOption{
Theme: opt.Theme, Theme: opt.Theme,
Data: opt.Data, Data: opt.Data,
BoundaryGap: opt.BoundaryGap, BoundaryGap: opt.BoundaryGap,
Position: position, Position: position,
SplitNumber: opt.SplitNumber, SplitNumber: opt.SplitNumber,
StrokeColor: opt.StrokeColor, StrokeColor: opt.StrokeColor,
FontSize: opt.FontSize, FontSize: opt.FontSize,
Font: opt.Font, Font: opt.Font,
FontColor: opt.FontColor, 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 { func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter {
p = p.Child(PainterPaddingOption(Box{
Top: p.Height() - defaultXAxisHeight,
}))
return NewAxisPainter(p, opt.ToAxisOption()) return NewAxisPainter(p, opt.ToAxisOption())
} }

View file

@ -36,7 +36,22 @@ type YAxisOption struct {
// The position of axis, it can be 'left' or 'right' // The position of axis, it can be 'left' or 'right'
Position string Position string
// The color of label // 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 { func (opt *YAxisOption) ToAxisOption() AxisOption {
@ -44,7 +59,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
if opt.Position == PositionRight { if opt.Position == PositionRight {
position = PositionRight position = PositionRight
} }
return AxisOption{ axisOpt := AxisOption{
Theme: opt.Theme, Theme: opt.Theme,
Data: opt.Data, Data: opt.Data,
Position: position, Position: position,
@ -56,6 +71,12 @@ func (opt *YAxisOption) ToAxisOption() AxisOption {
SplitLineShow: true, SplitLineShow: true,
SplitLineColor: opt.Theme.GetAxisSplitLineColor(), 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 { func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {