test: add series and legend test

This commit is contained in:
vicanso 2021-12-21 23:43:36 +08:00
parent acc758cb9a
commit a128a2513c
7 changed files with 398 additions and 77 deletions

View file

@ -72,6 +72,7 @@ func TestBarSeriesGetWidthValues(t *testing.T) {
barWidth: 80, barWidth: 80,
}, widthValues) }, widthValues)
// 指定margin
bs.Margin = 5 bs.Margin = 5
widthValues = bs.getWidthValues(300) widthValues = bs.getWidthValues(300)
assert.Equal(barSeriesWidthValues{ assert.Equal(barSeriesWidthValues{
@ -81,6 +82,7 @@ func TestBarSeriesGetWidthValues(t *testing.T) {
barWidth: 80, barWidth: 80,
}, widthValues) }, widthValues)
// 指定bar的宽度
bs.BarWidth = 60 bs.BarWidth = 60
widthValues = bs.getWidthValues(300) widthValues = bs.getWidthValues(300)
assert.Equal(barSeriesWidthValues{ assert.Equal(barSeriesWidthValues{
@ -89,7 +91,6 @@ func TestBarSeriesGetWidthValues(t *testing.T) {
margin: 5, margin: 5,
barWidth: 60, barWidth: 60,
}, widthValues) }, widthValues)
} }
func TestBarSeriesRender(t *testing.T) { func TestBarSeriesRender(t *testing.T) {

View file

@ -51,55 +51,55 @@ type BaseSeries struct {
} }
// GetName returns the name of the time series. // GetName returns the name of the time series.
func (cs BaseSeries) GetName() string { func (bs BaseSeries) GetName() string {
return cs.Name return bs.Name
} }
// GetStyle returns the line style. // GetStyle returns the line style.
func (cs BaseSeries) GetStyle() chart.Style { func (bs BaseSeries) GetStyle() chart.Style {
return cs.Style return bs.Style
} }
// Len returns the number of elements in the series. // Len returns the number of elements in the series.
func (cs BaseSeries) Len() int { func (bs BaseSeries) Len() int {
offset := 0 offset := 0
if cs.TickPosition == chart.TickPositionBetweenTicks { if bs.TickPosition == chart.TickPositionBetweenTicks {
offset = -1 offset = -1
} }
return len(cs.XValues) + offset return len(bs.XValues) + offset
} }
// GetValues gets the x,y values at a given index. // GetValues gets the x,y values at a given index.
func (cs BaseSeries) GetValues(index int) (float64, float64) { func (bs BaseSeries) GetValues(index int) (float64, float64) {
if cs.TickPosition == chart.TickPositionBetweenTicks { if bs.TickPosition == chart.TickPositionBetweenTicks {
index++ index++
} }
return cs.XValues[index], cs.YValues[index] return bs.XValues[index], bs.YValues[index]
} }
// GetFirstValues gets the first x,y values. // GetFirstValues gets the first x,y values.
func (cs BaseSeries) GetFirstValues() (float64, float64) { func (bs BaseSeries) GetFirstValues() (float64, float64) {
index := 0 index := 0
if cs.TickPosition == chart.TickPositionBetweenTicks { if bs.TickPosition == chart.TickPositionBetweenTicks {
index++ index++
} }
return cs.XValues[index], cs.YValues[index] return bs.XValues[index], bs.YValues[index]
} }
// GetLastValues gets the last x,y values. // GetLastValues gets the last x,y values.
func (cs BaseSeries) GetLastValues() (float64, float64) { func (bs BaseSeries) GetLastValues() (float64, float64) {
return cs.XValues[len(cs.XValues)-1], cs.YValues[len(cs.YValues)-1] return bs.XValues[len(bs.XValues)-1], bs.YValues[len(bs.YValues)-1]
} }
// GetValueFormatters returns value formatter defaults for the series. // GetValueFormatters returns value formatter defaults for the series.
func (cs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) { func (bs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) {
if cs.XValueFormatter != nil { if bs.XValueFormatter != nil {
x = cs.XValueFormatter x = bs.XValueFormatter
} else { } else {
x = chart.FloatValueFormatter x = chart.FloatValueFormatter
} }
if cs.YValueFormatter != nil { if bs.YValueFormatter != nil {
y = cs.YValueFormatter y = bs.YValueFormatter
} else { } else {
y = chart.FloatValueFormatter y = chart.FloatValueFormatter
} }
@ -107,26 +107,26 @@ func (cs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) {
} }
// GetYAxis returns which YAxis the series draws on. // GetYAxis returns which YAxis the series draws on.
func (cs BaseSeries) GetYAxis() chart.YAxisType { func (bs BaseSeries) GetYAxis() chart.YAxisType {
return cs.YAxis return bs.YAxis
} }
// Render renders the series. // 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") fmt.Println("should be override the function")
} }
// Validate validates the series. // Validate validates the series.
func (cs BaseSeries) Validate() error { func (bs BaseSeries) Validate() error {
if len(cs.XValues) == 0 { if len(bs.XValues) == 0 {
return fmt.Errorf("continuous series; must have xvalues set") 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") 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 fmt.Errorf("continuous series; must have same length xvalues as yvalues")
} }
return nil return nil

94
base_series_test.go Normal file
View file

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

View file

@ -70,10 +70,10 @@ type Graph interface {
} }
func (o *Options) validate() error { func (o *Options) validate() error {
xAxisCount := len(o.XAxis.Data)
if len(o.Series) == 0 { if len(o.Series) == 0 {
return errors.New("series can not be empty") return errors.New("series can not be empty")
} }
xAxisCount := len(o.XAxis.Data)
for _, item := range o.Series { for _, item := range o.Series {
if item.Type != SeriesPie && len(item.Data) != xAxisCount { if item.Type != SeriesPie && len(item.Data) != xAxisCount {
@ -83,6 +83,29 @@ func (o *Options) validate() error {
return nil 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) { func render(g Graph, rp chart.RendererProvider) ([]byte, error) {
buf := bytes.Buffer{} buf := bytes.Buffer{}
err := g.Render(rp, &buf) err := g.Render(rp, &buf)
@ -99,24 +122,8 @@ func ToPNG(g Graph) ([]byte, error) {
func ToSVG(g Graph) ([]byte, error) { func ToSVG(g Graph) ([]byte, error) {
return render(g, chart.SVG) return render(g, chart.SVG)
} }
func New(opt Options) (Graph, error) {
err := opt.validate() func newPieChart(opt Options) *chart.PieChart {
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)) values := make(chart.Values, len(opt.Series))
for index, item := range opt.Series { for index, item := range opt.Series {
values[index] = chart.Value{ values[index] = chart.Value{
@ -124,12 +131,12 @@ func New(opt Options) (Graph, error) {
Label: item.Name, Label: item.Name,
} }
} }
g := &chart.PieChart{ return &chart.PieChart{
Background: bg, Background: opt.getBackground(),
Title: opt.Title.Text, Title: opt.Title.Text,
TitleStyle: opt.Title.Style, TitleStyle: opt.Title.Style,
Width: width, Width: opt.getWidth(),
Height: height, Height: opt.getHeight(),
Values: values, Values: values,
ColorPalette: &PieThemeColorPalette{ ColorPalette: &PieThemeColorPalette{
ThemeColorPalette: ThemeColorPalette{ ThemeColorPalette: ThemeColorPalette{
@ -137,9 +144,11 @@ func New(opt Options) (Graph, error) {
}, },
}, },
} }
return g, nil
} }
func newChart(opt Options) *chart.Chart {
tickPosition := opt.TickPosition
xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme) xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme)
legendSize := len(opt.Legend.Data) legendSize := len(opt.Legend.Data)
@ -164,16 +173,16 @@ func New(opt Options) (Graph, error) {
yAxisOption = opt.YAxisOptions[1] yAxisOption = opt.YAxisOptions[1]
} }
g := &chart.Chart{ c := &chart.Chart{
Log: opt.Log, Log: opt.Log,
Background: bg, Background: opt.getBackground(),
ColorPalette: &ThemeColorPalette{ ColorPalette: &ThemeColorPalette{
Theme: opt.Theme, Theme: opt.Theme,
}, },
Title: opt.Title.Text, Title: opt.Title.Text,
TitleStyle: opt.Title.Style, TitleStyle: opt.Title.Style,
Width: width, Width: opt.getWidth(),
Height: height, Height: opt.getHeight(),
XAxis: xAxis, XAxis: xAxis,
YAxis: GetYAxis(opt.Theme, yAxisOption), YAxis: GetYAxis(opt.Theme, yAxisOption),
YAxisSecondary: GetSecondaryYAxis(opt.Theme, secondaryYAxisOption), YAxisSecondary: GetSecondaryYAxis(opt.Theme, secondaryYAxisOption),
@ -182,8 +191,8 @@ func New(opt Options) (Graph, error) {
// 设置secondary的样式 // 设置secondary的样式
if legendSize != 0 { if legendSize != 0 {
g.Elements = []chart.Renderable{ c.Elements = []chart.Renderable{
LegendCustomize(g, LegendOption{ LegendCustomize(c.Series, LegendOption{
Theme: opt.Theme, Theme: opt.Theme,
TextPosition: LegendTextPositionRight, TextPosition: LegendTextPositionRight,
IconDraw: DefaultLegendIconDraw, 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
} }

143
charts_test.go Normal file
View file

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

View file

@ -59,8 +59,8 @@ func DefaultLegendIconDraw(r chart.Renderer, opt LegendIconDrawOption) {
return return
} }
r.SetStrokeColor(opt.Style.GetStrokeColor()) r.SetStrokeColor(opt.Style.GetStrokeColor())
stokeWidth := opt.Style.GetStrokeWidth() strokeWidth := opt.Style.GetStrokeWidth()
r.SetStrokeWidth(stokeWidth) r.SetStrokeWidth(strokeWidth)
height := opt.Box.Bottom - opt.Box.Top height := opt.Box.Bottom - opt.Box.Top
ly := opt.Box.Top - (height / 2) + 2 ly := opt.Box.Top - (height / 2) + 2
r.MoveTo(opt.Box.Left, ly) r.MoveTo(opt.Box.Left, ly)
@ -71,7 +71,7 @@ func DefaultLegendIconDraw(r chart.Renderer, opt LegendIconDrawOption) {
r.FillStroke() 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) { return func(r chart.Renderer, cb chart.Box, chartDefaults chart.Style) {
legendDefaults := chart.Style{ legendDefaults := chart.Style{
FontColor: getTextColor(opt.Theme), FontColor: getTextColor(opt.Theme),
@ -87,7 +87,7 @@ func LegendCustomize(c *chart.Chart, opt LegendOption) chart.Renderable {
var labels []string var labels []string
var lines []chart.Style var lines []chart.Style
for _, s := range c.Series { for _, s := range series {
if !s.GetStyle().Hidden { if !s.GetStyle().Hidden {
if _, isAnnotationSeries := s.(chart.AnnotationSeries); !isAnnotationSeries { if _, isAnnotationSeries := s.(chart.AnnotationSeries); !isAnnotationSeries {
labels = append(labels, s.GetName()) labels = append(labels, s.GetName())

62
legend_test.go Normal file
View file

@ -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("<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"800\" height=\"600\">\\n<path d=\"M 100 100\nL 208 100\nL 208 110\nL 100 110\nL 100 100\" style=\"stroke-width:0;stroke:rgba(51,51,51,1.0);fill:none\"/><path d=\"M 100 107\nL 125 107\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:none\"/><circle cx=\"112\" cy=\"107\" r=\"5\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"130\" y=\"110\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">chrome</text><path d=\"M 185 107\nL 210 107\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"197\" cy=\"107\" r=\"5\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:2;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"215\" y=\"110\" style=\"stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:10.2px;font-family:'Roboto Medium',sans-serif\">edge</text></svg>", buf.String())
}