diff --git a/bar_series_test.go b/bar_series_test.go index 6343c06..8703367 100644 --- a/bar_series_test.go +++ b/bar_series_test.go @@ -72,6 +72,7 @@ func TestBarSeriesGetWidthValues(t *testing.T) { barWidth: 80, }, widthValues) + // 指定margin bs.Margin = 5 widthValues = bs.getWidthValues(300) assert.Equal(barSeriesWidthValues{ @@ -81,6 +82,7 @@ func TestBarSeriesGetWidthValues(t *testing.T) { barWidth: 80, }, widthValues) + // 指定bar的宽度 bs.BarWidth = 60 widthValues = bs.getWidthValues(300) assert.Equal(barSeriesWidthValues{ @@ -89,7 +91,6 @@ func TestBarSeriesGetWidthValues(t *testing.T) { margin: 5, barWidth: 60, }, widthValues) - } func TestBarSeriesRender(t *testing.T) { diff --git a/base_series.go b/base_series.go index ac49d21..e17b4b9 100644 --- a/base_series.go +++ b/base_series.go @@ -51,55 +51,55 @@ type BaseSeries struct { } // GetName returns the name of the time series. -func (cs BaseSeries) GetName() string { - return cs.Name +func (bs BaseSeries) GetName() string { + return bs.Name } // GetStyle returns the line style. -func (cs BaseSeries) GetStyle() chart.Style { - return cs.Style +func (bs BaseSeries) GetStyle() chart.Style { + return bs.Style } // Len returns the number of elements in the series. -func (cs BaseSeries) Len() int { +func (bs BaseSeries) Len() int { offset := 0 - if cs.TickPosition == chart.TickPositionBetweenTicks { + if bs.TickPosition == chart.TickPositionBetweenTicks { offset = -1 } - return len(cs.XValues) + offset + return len(bs.XValues) + offset } // GetValues gets the x,y values at a given index. -func (cs BaseSeries) GetValues(index int) (float64, float64) { - if cs.TickPosition == chart.TickPositionBetweenTicks { +func (bs BaseSeries) GetValues(index int) (float64, float64) { + if bs.TickPosition == chart.TickPositionBetweenTicks { index++ } - return cs.XValues[index], cs.YValues[index] + return bs.XValues[index], bs.YValues[index] } // GetFirstValues gets the first x,y values. -func (cs BaseSeries) GetFirstValues() (float64, float64) { +func (bs BaseSeries) GetFirstValues() (float64, float64) { index := 0 - if cs.TickPosition == chart.TickPositionBetweenTicks { + if bs.TickPosition == chart.TickPositionBetweenTicks { index++ } - return cs.XValues[index], cs.YValues[index] + return bs.XValues[index], bs.YValues[index] } // GetLastValues gets the last x,y values. -func (cs BaseSeries) GetLastValues() (float64, float64) { - return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1] +func (bs BaseSeries) GetLastValues() (float64, float64) { + return bs.XValues[len(bs.XValues)-1], bs.YValues[len(bs.YValues)-1] } // GetValueFormatters returns value formatter defaults for the series. -func (cs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) { - if cs.XValueFormatter != nil { - x = cs.XValueFormatter +func (bs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) { + if bs.XValueFormatter != nil { + x = bs.XValueFormatter } else { x = chart.FloatValueFormatter } - if cs.YValueFormatter != nil { - y = cs.YValueFormatter + if bs.YValueFormatter != nil { + y = bs.YValueFormatter } else { y = chart.FloatValueFormatter } @@ -107,26 +107,26 @@ func (cs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) { } // GetYAxis returns which YAxis the series draws on. -func (cs BaseSeries) GetYAxis() chart.YAxisType { - return cs.YAxis +func (bs BaseSeries) GetYAxis() chart.YAxisType { + return bs.YAxis } // Render renders the series. -func (cs BaseSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) { +func (bs BaseSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) { fmt.Println("should be override the function") } // Validate validates the series. -func (cs BaseSeries) Validate() error { - if len(cs.XValues) == 0 { +func (bs BaseSeries) Validate() error { + if len(bs.XValues) == 0 { return fmt.Errorf("continuous series; must have xvalues set") } - if len(cs.YValues) == 0 { + if len(bs.YValues) == 0 { return fmt.Errorf("continuous series; must have yvalues set") } - if len(cs.XValues) != len(cs.YValues) { + if len(bs.XValues) != len(bs.YValues) { return fmt.Errorf("continuous series; must have same length xvalues as yvalues") } return nil diff --git a/base_series_test.go b/base_series_test.go new file mode 100644 index 0000000..0c9b5d1 --- /dev/null +++ b/base_series_test.go @@ -0,0 +1,94 @@ +// MIT License + +// Copyright (c) 2021 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 ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2" +) + +func TestBaseSeries(t *testing.T) { + assert := assert.New(t) + + bs := BaseSeries{ + XValues: []float64{ + 1, + 2, + 3, + }, + YValues: []float64{ + 10, + 20, + 30, + }, + } + assert.Equal(3, bs.Len()) + bs.TickPosition = chart.TickPositionBetweenTicks + assert.Equal(2, bs.Len()) + + bs.TickPosition = chart.TickPositionUnset + x, y := bs.GetValues(1) + assert.Equal(float64(2), x) + assert.Equal(float64(20), y) + bs.TickPosition = chart.TickPositionBetweenTicks + x, y = bs.GetValues(1) + assert.Equal(float64(3), x) + assert.Equal(float64(30), y) + + bs.TickPosition = chart.TickPositionUnset + x, y = bs.GetFirstValues() + assert.Equal(float64(1), x) + assert.Equal(float64(10), y) + bs.TickPosition = chart.TickPositionBetweenTicks + x, y = bs.GetFirstValues() + assert.Equal(float64(2), x) + assert.Equal(float64(20), y) + + bs.TickPosition = chart.TickPositionUnset + x, y = bs.GetLastValues() + assert.Equal(float64(3), x) + assert.Equal(float64(30), y) + bs.TickPosition = chart.TickPositionBetweenTicks + x, y = bs.GetLastValues() + assert.Equal(float64(3), x) + assert.Equal(float64(30), y) + + xFormater, yFormater := bs.GetValueFormatters() + assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(xFormater).Pointer()) + assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(yFormater).Pointer()) + formater := func(v interface{}) string { + return "" + } + bs.XValueFormatter = formater + bs.YValueFormatter = formater + xFormater, yFormater = bs.GetValueFormatters() + assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(xFormater).Pointer()) + assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(yFormater).Pointer()) + + assert.Equal(chart.YAxisPrimary, bs.GetYAxis()) + + assert.Nil(bs.Validate()) +} diff --git a/charts.go b/charts.go index 1657e6d..cdc4b76 100644 --- a/charts.go +++ b/charts.go @@ -70,10 +70,10 @@ type Graph interface { } func (o *Options) validate() error { - xAxisCount := len(o.XAxis.Data) if len(o.Series) == 0 { return errors.New("series can not be empty") } + xAxisCount := len(o.XAxis.Data) for _, item := range o.Series { if item.Type != SeriesPie && len(item.Data) != xAxisCount { @@ -83,6 +83,29 @@ func (o *Options) validate() error { return nil } +func (o *Options) getWidth() int { + width := o.Width + if width <= 0 { + width = DefaultChartWidth + } + return width +} + +func (o *Options) getHeight() int { + height := o.Height + if height <= 0 { + height = DefaultChartHeight + } + return height +} + +func (o *Options) getBackground() chart.Style { + bg := chart.Style{ + Padding: o.Padding, + } + return bg +} + func render(g Graph, rp chart.RendererProvider) ([]byte, error) { buf := bytes.Buffer{} err := g.Render(rp, &buf) @@ -99,46 +122,32 @@ func ToPNG(g Graph) ([]byte, error) { func ToSVG(g Graph) ([]byte, error) { return render(g, chart.SVG) } -func New(opt Options) (Graph, error) { - err := opt.validate() - if err != nil { - return nil, err - } - tickPosition := opt.TickPosition - width := opt.Width - if width <= 0 { - width = DefaultChartWidth - } - height := opt.Height - if height <= 0 { - height = DefaultChartHeight - } - bg := chart.Style{ - Padding: opt.Padding, - } - if opt.Series[0].Type == SeriesPie { - values := make(chart.Values, len(opt.Series)) - for index, item := range opt.Series { - values[index] = chart.Value{ - Value: item.Data[0].Value, - Label: item.Name, - } + +func newPieChart(opt Options) *chart.PieChart { + values := make(chart.Values, len(opt.Series)) + for index, item := range opt.Series { + values[index] = chart.Value{ + Value: item.Data[0].Value, + Label: item.Name, } - g := &chart.PieChart{ - Background: bg, - Title: opt.Title.Text, - TitleStyle: opt.Title.Style, - Width: width, - Height: height, - Values: values, - ColorPalette: &PieThemeColorPalette{ - ThemeColorPalette: ThemeColorPalette{ - Theme: opt.Theme, - }, + } + return &chart.PieChart{ + Background: opt.getBackground(), + Title: opt.Title.Text, + TitleStyle: opt.Title.Style, + Width: opt.getWidth(), + Height: opt.getHeight(), + Values: values, + ColorPalette: &PieThemeColorPalette{ + ThemeColorPalette: ThemeColorPalette{ + Theme: opt.Theme, }, - } - return g, nil + }, } +} + +func newChart(opt Options) *chart.Chart { + tickPosition := opt.TickPosition xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme) @@ -164,16 +173,16 @@ func New(opt Options) (Graph, error) { yAxisOption = opt.YAxisOptions[1] } - g := &chart.Chart{ + c := &chart.Chart{ Log: opt.Log, - Background: bg, + Background: opt.getBackground(), ColorPalette: &ThemeColorPalette{ Theme: opt.Theme, }, Title: opt.Title.Text, TitleStyle: opt.Title.Style, - Width: width, - Height: height, + Width: opt.getWidth(), + Height: opt.getHeight(), XAxis: xAxis, YAxis: GetYAxis(opt.Theme, yAxisOption), YAxisSecondary: GetSecondaryYAxis(opt.Theme, secondaryYAxisOption), @@ -182,8 +191,8 @@ func New(opt Options) (Graph, error) { // 设置secondary的样式 if legendSize != 0 { - g.Elements = []chart.Renderable{ - LegendCustomize(g, LegendOption{ + c.Elements = []chart.Renderable{ + LegendCustomize(c.Series, LegendOption{ Theme: opt.Theme, TextPosition: LegendTextPositionRight, IconDraw: DefaultLegendIconDraw, @@ -192,5 +201,17 @@ func New(opt Options) (Graph, error) { }), } } - return g, nil + return c +} + +func New(opt Options) (Graph, error) { + err := opt.validate() + if err != nil { + return nil, err + } + if opt.Series[0].Type == SeriesPie { + return newPieChart(opt), nil + } + + return newChart(opt), nil } diff --git a/charts_test.go b/charts_test.go new file mode 100644 index 0000000..3173868 --- /dev/null +++ b/charts_test.go @@ -0,0 +1,143 @@ +// MIT License + +// Copyright (c) 2021 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 ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2" +) + +func TestChartsOptions(t *testing.T) { + assert := assert.New(t) + + o := Options{} + + assert.Equal(errors.New("series can not be empty"), o.validate()) + + o.Series = []Series{ + { + Data: []SeriesData{ + { + Value: 1, + }, + }, + }, + } + assert.Equal(errors.New("series and xAxis is not matched"), o.validate()) + o.XAxis.Data = []string{ + "1", + } + assert.Nil(o.validate()) + + assert.Equal(DefaultChartWidth, o.getWidth()) + o.Width = 10 + assert.Equal(10, o.getWidth()) + + assert.Equal(DefaultChartHeight, o.getHeight()) + o.Height = 10 + assert.Equal(10, o.getHeight()) + + padding := chart.NewBox(10, 10, 10, 10) + o.Padding = padding + assert.Equal(padding, o.getBackground().Padding) +} + +func TestNewPieChart(t *testing.T) { + assert := assert.New(t) + + data := []Series{ + { + Data: []SeriesData{ + { + Value: 10, + }, + }, + Name: "chrome", + }, + { + Data: []SeriesData{ + { + Value: 2, + }, + }, + Name: "edge", + }, + } + pie := newPieChart(Options{ + Series: data, + }) + for index, item := range pie.Values { + assert.Equal(data[index].Name, item.Label) + assert.Equal(data[index].Data[0].Value, item.Value) + } +} + +func TestNewChart(t *testing.T) { + assert := assert.New(t) + + data := []Series{ + { + Data: []SeriesData{ + { + Value: 10, + }, + { + Value: 20, + }, + }, + Name: "chrome", + }, + { + Data: []SeriesData{ + { + Value: 2, + }, + { + Value: 3, + }, + }, + Name: "edge", + }, + } + + c := newChart(Options{ + Series: data, + }) + assert.Empty(c.Elements) + for index, series := range c.Series { + assert.Equal(data[index].Name, series.GetName()) + } + + c = newChart(Options{ + Legend: Legend{ + Data: []string{ + "chrome", + "edge", + }, + }, + }) + assert.Equal(1, len(c.Elements)) +} diff --git a/legend.go b/legend.go index a9f73d1..b519e3e 100644 --- a/legend.go +++ b/legend.go @@ -59,8 +59,8 @@ func DefaultLegendIconDraw(r chart.Renderer, opt LegendIconDrawOption) { return } r.SetStrokeColor(opt.Style.GetStrokeColor()) - stokeWidth := opt.Style.GetStrokeWidth() - r.SetStrokeWidth(stokeWidth) + strokeWidth := opt.Style.GetStrokeWidth() + r.SetStrokeWidth(strokeWidth) height := opt.Box.Bottom - opt.Box.Top ly := opt.Box.Top - (height / 2) + 2 r.MoveTo(opt.Box.Left, ly) @@ -71,7 +71,7 @@ func DefaultLegendIconDraw(r chart.Renderer, opt LegendIconDrawOption) { r.FillStroke() } -func LegendCustomize(c *chart.Chart, opt LegendOption) chart.Renderable { +func LegendCustomize(series []chart.Series, opt LegendOption) chart.Renderable { return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) { legendDefaults := chart.Style{ FontColor: getTextColor(opt.Theme), @@ -87,7 +87,7 @@ func LegendCustomize(c *chart.Chart, opt LegendOption) chart.Renderable { var labels []string var lines []chart.Style - for _, s := range c.Series { + for _, s := range series { if !s.GetStyle().Hidden { if _, isAnnotationSeries := s.(chart.AnnotationSeries); !isAnnotationSeries { labels = append(labels, s.GetName()) diff --git a/legend_test.go b/legend_test.go new file mode 100644 index 0000000..6685243 --- /dev/null +++ b/legend_test.go @@ -0,0 +1,62 @@ +// MIT License + +// Copyright (c) 2021 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 ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/wcharczuk/go-chart/v2" +) + +func TestLegendCustomize(t *testing.T) { + assert := assert.New(t) + + series := GetSeries([]Series{ + { + Name: "chrome", + }, + { + Name: "edge", + }, + }, chart.TickPositionBetweenTicks, "") + r, err := chart.SVG(800, 600) + assert.Nil(err) + fn := LegendCustomize(series, LegendOption{ + TextPosition: LegendTextPositionRight, + IconDraw: DefaultLegendIconDraw, + Align: LegendAlignLeft, + Padding: chart.Box{ + Left: 100, + Top: 100, + }, + }) + fn(r, chart.NewBox(11, 47, 784, 373), chart.Style{ + Font: chart.StyleTextDefaults().Font, + }) + buf := bytes.Buffer{} + err = r.Save(&buf) + assert.Nil(err) + assert.Equal("\\nchromeedge", buf.String()) +}