diff --git a/bar_chart.go b/bar_chart.go index ae5adfb..6809a26 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -23,10 +23,17 @@ package charts import ( + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" ) -func barChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRenderOption, error) { +type barChartOption struct { + SeriesList SeriesList + Theme string + Font *truetype.Font +} + +func barChartRender(opt barChartOption, result *basicRenderResult) ([]*markPointRenderOption, error) { d, err := NewDraw(DrawOption{ Parent: result.d, @@ -56,7 +63,7 @@ func barChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRen barMaxHeight := result.getYRange(0).Size theme := NewTheme(opt.Theme) - seriesNames := opt.Legend.Data + seriesNames := opt.SeriesList.Names() r := d.Render diff --git a/chart.go b/chart.go index 69c2aba..91c9cae 100644 --- a/chart.go +++ b/chart.go @@ -57,7 +57,7 @@ type ChartOption struct { Parent *Draw Padding chart.Box Box chart.Box - SeriesList []Series + SeriesList SeriesList BackgroundColor drawing.Color Children []ChartOption } @@ -126,12 +126,16 @@ func (o *ChartOption) FillDefault(theme string) { if o.Legend.Left == "" { o.Legend.Left = PositionCenter } + // legend与series name的关联 if len(o.Legend.Data) == 0 { - names := make([]string, len(o.SeriesList)) - for index, item := range o.SeriesList { - names[index] = item.Name + o.Legend.Data = o.SeriesList.Names() + } else { + seriesCount := len(o.SeriesList) + for index, name := range o.Legend.Data { + if index < seriesCount { + o.SeriesList[index].Name = name + } } - o.Legend.Data = names } if o.Legend.Style.Font == nil { o.Legend.Style.Font = o.Font @@ -262,7 +266,11 @@ func Render(opt ChartOption) (*Draw, error) { if !isPieChart { return nil } - err := pieChartRender(opt, result) + err := pieChartRender(pieChartOption{ + SeriesList: opt.SeriesList, + Theme: opt.Theme, + Font: opt.Font, + }, result) return err }, // bar render @@ -271,9 +279,11 @@ func Render(opt ChartOption) (*Draw, error) { if isPieChart || len(barSeries) == 0 { return nil } - o := opt - o.SeriesList = barSeries - options, err := barChartRender(o, result) + options, err := barChartRender(barChartOption{ + SeriesList: barSeries, + Theme: opt.Theme, + Font: opt.Font, + }, result) if err != nil { return err } @@ -286,9 +296,11 @@ func Render(opt ChartOption) (*Draw, error) { if isPieChart || len(lineSeries) == 0 { return nil } - o := opt - o.SeriesList = lineSeries - options, err := lineChartRender(o, result) + options, err := lineChartRender(lineChartOption{ + Theme: opt.Theme, + SeriesList: lineSeries, + Font: opt.Font, + }, result) if err != nil { return err } diff --git a/line_chart.go b/line_chart.go index af87cc9..53fde07 100644 --- a/line_chart.go +++ b/line_chart.go @@ -23,11 +23,18 @@ package charts import ( + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) -func lineChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRenderOption, error) { +type lineChartOption struct { + Theme string + SeriesList SeriesList + Font *truetype.Font +} + +func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]*markPointRenderOption, error) { theme := NewTheme(opt.Theme) @@ -40,7 +47,7 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRe if err != nil { return nil, err } - seriesNames := opt.Legend.Data + seriesNames := opt.SeriesList.Names() r := d.Render xRange := result.xRange diff --git a/pie_chart.go b/pie_chart.go index ff24642..7c62e8f 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -26,6 +26,7 @@ import ( "math" "strconv" + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" ) @@ -40,7 +41,13 @@ func getPieStyle(theme *Theme, index int) chart.Style { } } -func pieChartRender(opt ChartOption, result *basicRenderResult) error { +type pieChartOption struct { + Theme string + Font *truetype.Font + SeriesList SeriesList +} + +func pieChartRender(opt pieChartOption, result *basicRenderResult) error { d, err := NewDraw(DrawOption{ Parent: result.d, }, PaddingOption(chart.Box{ @@ -91,7 +98,7 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) error { } labelRadius := radius + float64(labelLineWidth) - seriesNames := opt.Legend.Data + seriesNames := opt.SeriesList.Names() if len(values) == 1 { getPieStyle(theme, 0).WriteToRenderer(r) diff --git a/series.go b/series.go index b1d72a2..f8a7444 100644 --- a/series.go +++ b/series.go @@ -94,6 +94,7 @@ type Series struct { MarkPoint SeriesMarkPoint MarkLine SeriesMarkLine } +type SeriesList []Series type PieSeriesOption struct { Radius string @@ -158,6 +159,14 @@ func (s *Series) Summary() seriesSummary { } } +func (sl SeriesList) Names() []string { + names := make([]string, len(sl)) + for index, s := range sl { + names[index] = s.Name + } + return names +} + type LabelFormatter func(index int, value float64, percent float64) string func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { diff --git a/series_test.go b/series_test.go new file mode 100644 index 0000000..cab14c3 --- /dev/null +++ b/series_test.go @@ -0,0 +1,158 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSeriesFromValues(t *testing.T) { + assert := assert.New(t) + + assert.Equal(Series{ + Data: []SeriesData{ + { + Value: 1, + }, + { + Value: 2, + }, + }, + Type: ChartTypeBar, + }, NewSeriesFromValues([]float64{ + 1, + 2, + }, ChartTypeBar)) +} + +func TestNewSeriesDataFromValues(t *testing.T) { + assert := assert.New(t) + + assert.Equal([]SeriesData{ + { + Value: 1, + }, + { + Value: 2, + }, + }, NewSeriesDataFromValues([]float64{ + 1, + 2, + })) +} + +func TestNewPieSeriesList(t *testing.T) { + assert := assert.New(t) + + assert.Equal([]Series{ + { + Type: ChartTypePie, + Label: SeriesLabel{ + Show: true, + }, + Radius: "30%", + Data: []SeriesData{ + { + Value: 1, + }, + }, + }, + { + Type: ChartTypePie, + Label: SeriesLabel{ + Show: true, + }, + Radius: "30%", + Data: []SeriesData{ + { + Value: 2, + }, + }, + }, + }, NewPieSeriesList([]float64{ + 1, + 2, + }, PieSeriesOption{ + Radius: "30%", + LabelShow: true, + })) +} + +func TestSeriesSummary(t *testing.T) { + assert := assert.New(t) + + s := Series{ + Data: NewSeriesDataFromValues([]float64{ + 1, + 3, + 5, + 7, + 9, + }), + } + assert.Equal(seriesSummary{ + MaxIndex: 4, + MaxValue: 9, + MinIndex: 0, + MinValue: 1, + AverageValue: 5, + }, s.Summary()) +} + +func TestGetSeriesNames(t *testing.T) { + assert := assert.New(t) + + sl := SeriesList{ + { + Name: "a", + }, + { + Name: "b", + }, + } + assert.Equal([]string{ + "a", + "b", + }, sl.Names()) +} + +func TestNewPieLabelFormatter(t *testing.T) { + assert := assert.New(t) + + fn := NewPieLabelFormatter([]string{ + "a", + "b", + }, "") + assert.Equal("a: 35%", fn(0, 1.2, 0.35)) +} + +func TestNewValueLabelFormater(t *testing.T) { + assert := assert.New(t) + fn := NewValueLabelFormater([]string{ + "a", + "b", + }, "") + assert.Equal("1.2", fn(0, 1.2, 0.35)) +} diff --git a/theme_test.go b/theme_test.go new file mode 100644 index 0000000..7caf273 --- /dev/null +++ b/theme_test.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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +func TestTheme(t *testing.T) { + assert := assert.New(t) + + darkTheme := NewTheme(ThemeDark) + lightTheme := NewTheme(ThemeLight) + + assert.True(darkTheme.IsDark()) + assert.False(lightTheme.IsDark()) + + assert.Equal(drawing.Color{ + R: 185, + G: 184, + B: 206, + A: 255, + }, darkTheme.GetAxisStrokeColor()) + assert.Equal(drawing.Color{ + R: 110, + G: 112, + B: 121, + A: 255, + }, lightTheme.GetAxisStrokeColor()) + + assert.Equal(drawing.Color{ + R: 72, + G: 71, + B: 83, + A: 255, + }, darkTheme.GetAxisSplitLineColor()) + assert.Equal(drawing.Color{ + R: 224, + G: 230, + B: 242, + A: 255, + }, lightTheme.GetAxisSplitLineColor()) + + assert.Equal([]drawing.Color{ + { + R: 84, + G: 112, + B: 198, + A: 255, + }, + { + R: 145, + G: 204, + B: 117, + A: 255, + }, + { + R: 250, + G: 200, + B: 88, + A: 255, + }, + { + R: 238, + G: 102, + B: 102, + A: 255, + }, + { + R: 115, + G: 192, + B: 222, + A: 255, + }, + }, darkTheme.GetSeriesColors()) + assert.Equal([]drawing.Color{ + { + R: 84, + G: 112, + B: 198, + A: 255, + }, + { + R: 145, + G: 204, + B: 117, + A: 255, + }, + { + R: 250, + G: 200, + B: 88, + A: 255, + }, + { + R: 238, + G: 102, + B: 102, + A: 255, + }, + { + R: 115, + G: 192, + B: 222, + A: 255, + }, + }, lightTheme.GetSeriesColors()) + + assert.Equal(drawing.Color{ + R: 16, + G: 12, + B: 42, + A: 255, + }, darkTheme.GetBackgroundColor()) + assert.Equal(drawing.ColorWhite, lightTheme.GetBackgroundColor()) + + assert.Equal(drawing.Color{ + R: 238, + G: 238, + B: 238, + A: 255, + }, darkTheme.GetTextColor()) + assert.Equal(drawing.Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, lightTheme.GetTextColor()) +}