diff --git a/bar_chart.go b/bar_chart.go index 7582294..6db1714 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -28,7 +28,7 @@ func NewBarChartOptionWithData(data [][]float64) BarChartOption { Padding: defaultPadding, Theme: GetDefaultTheme(), Font: GetDefaultFont(), - YAxis: make([]YAxisOption, sl.getYAxisCount()), + YAxis: make([]YAxisOption, getSeriesYAxisCount(sl)), ValueFormatter: defaultValueFormatter, } } @@ -40,8 +40,8 @@ type BarChartOption struct { Padding Box // Font is the font used to render the chart. Font *truetype.Font - // SeriesList provides the data series. - SeriesList SeriesList + // SeriesList provides the data population for the chart, typically constructed using NewSeriesListBar. + SeriesList BarSeriesList // StackSeries if set to *true a single bar with the colored series stacked together will be rendered. // This feature will result in some options being ignored, including BarMargin and SeriesLabelPosition. // MarkLine is also interpreted differently, only the first Series will have the MarkLine rendered (as it's the @@ -101,7 +101,7 @@ func calculateBarMarginsAndSize(seriesCount, space int, configuredBarSize int, c return margin, barMargin, barSize } -func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { +func (b *barChart) render(result *defaultRenderResult, seriesList BarSeriesList) (Box, error) { p := b.p opt := b.opt seriesCount := len(seriesList) @@ -115,13 +115,13 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B x0, x1 := xRange.GetRange(0) width := int(x1 - x0) barMaxHeight := seriesPainter.Height() // total vertical space for bars - seriesNames := seriesList.Names() + seriesNames := seriesList.names() divideValues := xRange.AutoDivide() stackedSeries := flagIs(true, opt.StackSeries) var margin, barMargin, barWidth int var accumulatedHeights []int // prior heights for stacking to avoid recalculating the heights if stackedSeries { - barCount := seriesList.getYAxisCount() // only two bars if two y-axis + barCount := getSeriesYAxisCount(seriesList) // only two bars if two y-axis configuredMargin := opt.BarMargin if barCount == 1 { configuredMargin = nil // no margin needed with a single bar @@ -148,8 +148,8 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B rendererList = append(rendererList, labelPainter) } - points := make([]Point, len(series.Data)) // used for mark points - for j, item := range series.Data { + points := make([]Point, len(series.Values)) // used for mark points + for j, item := range series.Values { if j >= xRange.divideCount { continue } @@ -230,7 +230,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B var globalSeriesData []float64 // lazily initialized if series.MarkLine.GlobalLine && stackSeries && index == seriesCount-1 { if globalSeriesData == nil { - globalSeriesData = seriesList.makeSumSeries(ChartTypeBar, series.YAxisIndex).Data + globalSeriesData = sumSeriesData(seriesList, series.YAxisIndex) } markLinePainter.add(markLineRenderOption{ fillColor: defaultGlobalMarkFillColor, @@ -238,7 +238,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B strokeColor: defaultGlobalMarkFillColor, font: opt.Font, markline: series.MarkLine, - seriesData: globalSeriesData, + seriesValues: globalSeriesData, axisRange: yRange, valueFormatterDefault: markLineValueFormatter, }) @@ -250,20 +250,20 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B strokeColor: seriesColor, font: opt.Font, markline: series.MarkLine, - seriesData: series.Data, + seriesValues: series.Values, axisRange: yRange, valueFormatterDefault: markLineValueFormatter, }) } if series.MarkPoint.GlobalPoint && stackSeries && index == seriesCount-1 { if globalSeriesData == nil { - globalSeriesData = seriesList.makeSumSeries(ChartTypeBar, series.YAxisIndex).Data + globalSeriesData = sumSeriesData(seriesList, series.YAxisIndex) } markPointPainter.add(markPointRenderOption{ fillColor: defaultGlobalMarkFillColor, font: opt.Font, markpoint: series.MarkPoint, - seriesData: globalSeriesData, + seriesValues: globalSeriesData, points: points, valueFormatterDefault: markPointValueFormatter, seriesLabelPainter: labelPainter, @@ -273,7 +273,7 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B fillColor: seriesColor, font: opt.Font, markpoint: series.MarkPoint, - seriesData: series.Data, + seriesValues: series.Values, points: points, valueFormatterDefault: markPointValueFormatter, seriesLabelPainter: labelPainter, @@ -307,6 +307,5 @@ func (b *barChart) Render() (Box, error) { if err != nil { return BoxZero, err } - seriesList := opt.SeriesList.Filter(ChartTypeBar) - return b.render(renderResult, seriesList) + return b.render(renderResult, opt.SeriesList) } diff --git a/bar_chart_test.go b/bar_chart_test.go index d0b27f9..cc3842b 100644 --- a/bar_chart_test.go +++ b/bar_chart_test.go @@ -84,7 +84,7 @@ func TestNewBarChartOptionWithData(t *testing.T) { }) assert.Len(t, opt.SeriesList, 2) - assert.Equal(t, ChartTypeBar, opt.SeriesList[0].Type) + assert.Equal(t, ChartTypeBar, opt.SeriesList[0].getType()) assert.Len(t, opt.YAxis, 1) assert.Equal(t, defaultPadding, opt.Padding) diff --git a/benchmark_test.go b/benchmark_test.go index 1dbac3f..7763ddd 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -20,7 +20,6 @@ func makeDefaultMultiChartOptions() ChartOption { }, YAxis: []YAxisOption{ { - Min: Ptr(0.0), Max: Ptr(90.0), }, @@ -29,11 +28,11 @@ func makeDefaultMultiChartOptions() ChartOption { NewSeriesListLine([][]float64{ {56.5, 82.1, 88.7, 70.1, 53.4, 85.1}, {51.1, 51.4, 55.1, 53.3, 73.8, 68.7}, - }), + }).ToGenericSeriesList(), NewSeriesListBar([][]float64{ {40.1, 62.2, 69.5, 36.4, 45.2, 32.5}, {25.2, 37.1, 41.2, 18, 33.9, 49.1}, - })...), + }).ToGenericSeriesList()...), Children: []ChartOption{ { Legend: LegendOption{ @@ -50,7 +49,7 @@ func makeDefaultMultiChartOptions() ChartOption { 435.9, 354.3, 285.9, 204.5, }, PieSeriesOption{ Radius: "35%", - }), + }).ToGenericSeriesList(), }, }, } diff --git a/chart_option.go b/chart_option.go index ea69194..92a2b1a 100644 --- a/chart_option.go +++ b/chart_option.go @@ -30,8 +30,8 @@ type ChartOption struct { Font *truetype.Font // Box specifies the canvas box for the chart. Box Box - // SeriesList provides the data series. - SeriesList SeriesList + // SeriesList provides the population data for the charts, constructed through NewSeriesListGeneric. + SeriesList GenericSeriesList // StackSeries if set to *true the lines will be layered or stacked. This option significantly changes the chart // visualization, please see the specific chart docs for full details. StackSeries *bool @@ -228,7 +228,7 @@ func (o *ChartOption) fillDefault() error { o.Width = getDefaultInt(o.Width, defaultChartWidth) o.Height = getDefaultInt(o.Height, defaultChartHeight) - yaxisCount := o.SeriesList.getYAxisCount() + yaxisCount := getSeriesYAxisCount(o.SeriesList) if yaxisCount < 0 { return errors.New("series specified invalid y-axis index") } @@ -267,41 +267,41 @@ func fillThemeDefaults(defaultTheme ColorPalette, title *TitleOption, legend *Le // LineRender line chart render. func LineRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { return Render(ChartOption{ - SeriesList: NewSeriesListLine(values), + SeriesList: NewSeriesListGeneric(values, ChartTypeLine), }, opts...) } // BarRender bar chart render. func BarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { return Render(ChartOption{ - SeriesList: NewSeriesListBar(values), + SeriesList: NewSeriesListGeneric(values, ChartTypeBar), }, opts...) } // HorizontalBarRender horizontal bar chart render. func HorizontalBarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { return Render(ChartOption{ - SeriesList: NewSeriesListHorizontalBar(values), + SeriesList: NewSeriesListGeneric(values, ChartTypeHorizontalBar), }, opts...) } // PieRender pie chart render. func PieRender(values []float64, opts ...OptionFunc) (*Painter, error) { return Render(ChartOption{ - SeriesList: NewSeriesListPie(values), + SeriesList: NewSeriesListPie(values).ToGenericSeriesList(), }, opts...) } // RadarRender radar chart render. func RadarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) { return Render(ChartOption{ - SeriesList: NewSeriesListRadar(values), + SeriesList: NewSeriesListGeneric(values, ChartTypeRadar), }, opts...) } // FunnelRender funnel chart render. func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { return Render(ChartOption{ - SeriesList: NewSeriesListFunnel(values), + SeriesList: NewSeriesListFunnel(values).ToGenericSeriesList(), }, opts...) } diff --git a/chart_option_test.go b/chart_option_test.go index 709e7f0..8118f17 100644 --- a/chart_option_test.go +++ b/chart_option_test.go @@ -54,7 +54,7 @@ func TestChartOptionSeriesShowLabel(t *testing.T) { t.Parallel() opt := ChartOption{ - SeriesList: NewSeriesListPie([]float64{1, 2}), + SeriesList: NewSeriesListPie([]float64{1, 2}).ToGenericSeriesList(), } SeriesShowLabel(true)(&opt) assert.True(t, flagIs(true, opt.SeriesList[0].Label.Show)) @@ -63,9 +63,8 @@ func TestChartOptionSeriesShowLabel(t *testing.T) { assert.True(t, flagIs(false, opt.SeriesList[0].Label.Show)) } -func newNoTypeSeriesListFromValues(values [][]float64) SeriesList { - return newSeriesListFromValues(values, "", - SeriesLabel{}, nil, "", SeriesMarkPoint{}, SeriesMarkLine{}) +func newNoTypeSeriesListFromValues(values [][]float64) GenericSeriesList { + return NewSeriesListGeneric(values, "") } func TestChartOptionMarkLine(t *testing.T) { @@ -273,7 +272,7 @@ func TestChildRender(t *testing.T) { SeriesList: NewSeriesListHorizontalBar([][]float64{ {70, 90, 110, 130}, {80, 100, 120, 140}, - }), + }).ToGenericSeriesList(), Legend: LegendOption{ SeriesNames: []string{"2011", "2012"}, }, diff --git a/charts.go b/charts.go index 418ad9e..20ee154 100644 --- a/charts.go +++ b/charts.go @@ -3,7 +3,6 @@ package charts import ( "errors" "math" - "sort" "github.com/go-analyze/charts/chartdraw" ) @@ -70,7 +69,7 @@ type defaultRenderOption struct { // padding specifies the padding of chart. padding Box // seriesList provides the data series. - seriesList SeriesList + seriesList seriesList // stackSeries can be set to true if the series data will be stacked (summed). stackSeries bool // xAxis are options for the x-axis. @@ -99,15 +98,12 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e fillThemeDefaults(getPreferredTheme(opt.theme, p.theme), &opt.title, opt.legend, opt.xAxis) // TODO - this is a hack, we need to update the yaxis based on the markpoint state - for _, sl := range opt.seriesList { - if len(sl.MarkPoint.Data) > 0 { // if graph has markpoint - // adjust padding scale to give space for mark point (if not specified by user) - for i := range opt.yAxis { - if opt.yAxis[i].RangeValuePaddingScale == nil { - opt.yAxis[i].RangeValuePaddingScale = Ptr(2.5) - } + if opt.seriesList.hasMarkPoint() { + // adjust padding scale to give space for mark point (if not specified by user) + for i := range opt.yAxis { + if opt.yAxis[i].RangeValuePaddingScale == nil { + opt.yAxis[i].RangeValuePaddingScale = Ptr(2.5) } - break } } @@ -120,12 +116,14 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e // association between legend and series name if len(opt.legend.SeriesNames) == 0 { - opt.legend.SeriesNames = opt.seriesList.Names() + opt.legend.SeriesNames = opt.seriesList.names() } else { - seriesCount := len(opt.seriesList) + seriesCount := opt.seriesList.len() for index, name := range opt.legend.SeriesNames { - if index < seriesCount && len(opt.seriesList[index].Name) == 0 { - opt.seriesList[index].Name = name + if index >= seriesCount { + break + } else if opt.seriesList.getSeriesName(index) == "" { + opt.seriesList.setSeriesName(index, name) } } nameIndexDict := map[string]int{} @@ -133,9 +131,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e nameIndexDict[name] = index } // ensure order of series is consistent with legend - sort.Slice(opt.seriesList, func(i, j int) bool { - return nameIndexDict[opt.seriesList[i].Name] < nameIndexDict[opt.seriesList[j].Name] - }) + opt.seriesList.sortByNameIndex(nameIndexDict) } const legendTitlePadding = 15 @@ -188,7 +184,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e axisRanges: make(map[int]axisRange), } - axisIndexList := make([]int, opt.seriesList.getYAxisCount()) + axisIndexList := make([]int, getSeriesYAxisCount(opt.seriesList)) for i := range axisIndexList { axisIndexList[i] = i } @@ -208,7 +204,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e minPadRange = *yAxisOption.RangeValuePaddingScale maxPadRange = *yAxisOption.RangeValuePaddingScale } - min, max, sumMax := opt.seriesList.getMinMaxSumMax(index, opt.stackSeries) + min, max, sumMax := getSeriesMinMaxSumMax(opt.seriesList, index, opt.stackSeries) decimalData := min != math.Floor(min) || (max-min) != math.Floor(max-min) if yAxisOption.Min != nil && *yAxisOption.Min < min { min = *yAxisOption.Min @@ -270,7 +266,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e } else { yAxisOption.isCategoryAxis = true // we need to update the range labels or the bars won't be aligned to the Y axis - r.divideCount = opt.seriesList.getMaxDataCount("") + r.divideCount = getSeriesMaxDataCount(opt.seriesList) result.axisRanges[index] = r // since the x-axis is the value part, it's label is calculated and processed separately opt.xAxis.Labels = r.Values() @@ -350,12 +346,12 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) { } seriesList := opt.SeriesList - lineSeriesList := seriesList.Filter(ChartTypeLine) - barSeriesList := seriesList.Filter(ChartTypeBar) - horizontalBarSeriesList := seriesList.Filter(ChartTypeHorizontalBar) - pieSeriesList := seriesList.Filter(ChartTypePie) - radarSeriesList := seriesList.Filter(ChartTypeRadar) - funnelSeriesList := seriesList.Filter(ChartTypeFunnel) + lineSeriesList := filterSeriesList[LineSeriesList](opt.SeriesList, ChartTypeLine) + barSeriesList := filterSeriesList[BarSeriesList](opt.SeriesList, ChartTypeBar) + horizontalBarSeriesList := filterSeriesList[HorizontalBarSeriesList](opt.SeriesList, ChartTypeHorizontalBar) + pieSeriesList := filterSeriesList[PieSeriesList](opt.SeriesList, ChartTypePie) + radarSeriesList := filterSeriesList[RadarSeriesList](opt.SeriesList, ChartTypeRadar) + funnelSeriesList := filterSeriesList[FunnelSeriesList](opt.SeriesList, ChartTypeFunnel) seriesCount := len(seriesList) if len(horizontalBarSeriesList) != 0 && len(horizontalBarSeriesList) != seriesCount { diff --git a/echarts.go b/echarts.go index 9f47876..42ab296 100644 --- a/echarts.go +++ b/echarts.go @@ -253,20 +253,20 @@ type EChartsSeries struct { } type EChartsSeriesList []EChartsSeries -func (esList EChartsSeriesList) ToSeriesList() SeriesList { - seriesList := make(SeriesList, 0, len(esList)) +func (esList EChartsSeriesList) ToSeriesList() GenericSeriesList { + seriesList := make([]GenericSeries, 0, len(esList)) for _, item := range esList { // if pie, each sub-recommendation generates a series if item.Type == ChartTypePie { for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ + seriesList = append(seriesList, GenericSeries{ Type: item.Type, Name: dataItem.Name, Label: SeriesLabel{ Show: Ptr(true), }, Radius: item.Radius, - Data: []float64{dataItem.Value.First()}, + Values: []float64{dataItem.Value.First()}, }) } continue @@ -274,10 +274,10 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList { if item.Type == ChartTypeRadar || item.Type == ChartTypeFunnel { for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ - Name: dataItem.Name, - Type: item.Type, - Data: dataItem.Value.values, + seriesList = append(seriesList, GenericSeries{ + Name: dataItem.Name, + Type: item.Type, + Values: dataItem.Value.values, Label: SeriesLabel{ FontStyle: FontStyle{ FontColor: ParseColor(item.Label.Color), @@ -289,9 +289,9 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList { } continue } - seriesList = append(seriesList, Series{ + seriesList = append(seriesList, GenericSeries{ Type: item.Type, - Data: sliceConversion(item.Data, func(dataItem EChartsSeriesData) float64 { + Values: sliceConversion(item.Data, func(dataItem EChartsSeriesData) float64 { return dataItem.Value.First() }), YAxisIndex: item.YAxisIndex, diff --git a/examples/multiple_charts-3/main.go b/examples/multiple_charts-3/main.go index 56aac98..a3a6c09 100644 --- a/examples/multiple_charts-3/main.go +++ b/examples/multiple_charts-3/main.go @@ -60,7 +60,7 @@ func main() { SeriesList: charts.NewSeriesListHorizontalBar([][]float64{ {70, 90, 110, 130}, {80, 100, 120, 140}, - }), + }).ToGenericSeriesList(), Legend: charts.LegendOption{ SeriesNames: []string{ "2011", "2012", diff --git a/examples/web-1/main.go b/examples/web-1/main.go index 1d6359d..ae014b3 100644 --- a/examples/web-1/main.go +++ b/examples/web-1/main.go @@ -157,13 +157,13 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", }, }, - SeriesList: charts.NewSeriesListLine([][]float64{ + SeriesList: charts.NewSeriesListGeneric([][]float64{ {120, 132, 101, 134, 90, 230, 210}, {220, 182, 191, 234, 290, 330, 310}, {150, 232, 201, 154, 190, 330, 410}, {320, 332, 301, 334, 390, 330, 320}, {820, 932, 901, 934, 1290, 1330, 1320}, - }), + }, charts.ChartTypeLine), }, // temperature line chart { @@ -187,9 +187,9 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, BoundaryGap: charts.Ptr(false), }, - SeriesList: []charts.Series{ + SeriesList: []charts.GenericSeries{ { - Data: []float64{ + Values: []float64{ 14, 11, 13, 11, 12, 12, 7, }, Type: charts.ChartTypeLine, @@ -197,7 +197,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), }, { - Data: []float64{ + Values: []float64{ 1, -2, 2, 5, 3, 2, 0, }, Type: charts.ChartTypeLine, @@ -221,9 +221,9 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { YAxis: []charts.YAxisOption{{ Min: charts.Ptr(0.0), // ensure y-axis starts at 0 }}, - SeriesList: charts.NewSeriesListLine([][]float64{ + SeriesList: charts.NewSeriesListGeneric([][]float64{ {120, 132, 101, 134, 90, 230, 210}, - }), + }, charts.ChartTypeLine), FillArea: charts.Ptr(true), }, // histogram @@ -243,15 +243,15 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, Icon: charts.IconRect, }, - SeriesList: []charts.Series{ + SeriesList: []charts.GenericSeries{ { - Data: []float64{ + Values: []float64{ 120, 200, 150, 80, 70, 110, 130, }, Type: charts.ChartTypeBar, }, { - Data: []float64{ + Values: []float64{ 100, 190, 230, 140, 100, 200, 180, }, Type: charts.ChartTypeBar, @@ -285,10 +285,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, }, - SeriesList: charts.NewSeriesListHorizontalBar([][]float64{ + SeriesList: charts.NewSeriesListGeneric([][]float64{ {18203, 23489, 29034, 104970, 131744, 630230}, {19325, 23438, 31000, 121594, 134141, 681807}, - }), + }, charts.ChartTypeHorizontalBar), }, // histogram+marker { @@ -309,10 +309,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { "Rainfall", "Evaporation", }, }, - SeriesList: []charts.Series{ + SeriesList: []charts.GenericSeries{ { Type: charts.ChartTypeBar, - Data: []float64{ + Values: []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( @@ -325,7 +325,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, { Type: charts.ChartTypeBar, - Data: []float64{ + Values: []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( @@ -365,12 +365,12 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { AxisColor: charts.ColorRGB(250, 200, 88), }, }, - SeriesList: append(charts.NewSeriesListBar([][]float64{ + SeriesList: append(charts.NewSeriesListGeneric([][]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}, {2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3}, - }), charts.Series{ + }, charts.ChartTypeBar), charts.GenericSeries{ Type: charts.ChartTypeLine, - Data: []float64{ + Values: []float64{ 2.0, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, 23.4, 23.0, 16.5, 12.0, 6.2, }, YAxisIndex: 1, @@ -396,7 +396,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { 1048, 735, 580, 484, 300, }, charts.PieSeriesOption{ Radius: "35%", - }), + }).ToGenericSeriesList(), }, // radar chart { @@ -436,10 +436,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { Max: 25000, }, }, - SeriesList: charts.NewSeriesListRadar([][]float64{ + SeriesList: charts.NewSeriesListGeneric([][]float64{ {4200, 3000, 20000, 35000, 50000, 18000}, {5000, 14000, 28000, 26000, 42000, 21000}, - }), + }, charts.ChartTypeRadar), }, // funnel chart { @@ -451,31 +451,31 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { "Show", "Click", "Visit", "Inquiry", "Order", }, }, - SeriesList: []charts.Series{ + SeriesList: []charts.GenericSeries{ { - Type: charts.ChartTypeFunnel, - Name: "Show", - Data: []float64{100}, + Type: charts.ChartTypeFunnel, + Name: "Show", + Values: []float64{100}, }, { - Type: charts.ChartTypeFunnel, - Name: "Click", - Data: []float64{80}, + Type: charts.ChartTypeFunnel, + Name: "Click", + Values: []float64{80}, }, { - Type: charts.ChartTypeFunnel, - Name: "Visit", - Data: []float64{60}, + Type: charts.ChartTypeFunnel, + Name: "Visit", + Values: []float64{60}, }, { - Type: charts.ChartTypeFunnel, - Name: "Inquiry", - Data: []float64{40}, + Type: charts.ChartTypeFunnel, + Name: "Inquiry", + Values: []float64{40}, }, { - Type: charts.ChartTypeFunnel, - Name: "Order", - Data: []float64{20}, + Type: charts.ChartTypeFunnel, + Name: "Order", + Values: []float64{20}, }, }, }, @@ -507,14 +507,14 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { }, }, SeriesList: append( - charts.NewSeriesListLine([][]float64{ + charts.NewSeriesListGeneric([][]float64{ {56.5, 82.1, 88.7, 70.1, 53.4, 85.1}, {51.1, 51.4, 55.1, 53.3, 73.8, 68.7}, - }), - charts.NewSeriesListBar([][]float64{ + }, charts.ChartTypeLine), + charts.NewSeriesListGeneric([][]float64{ {40.1, 62.2, 69.5, 36.4, 45.2, 32.5}, {25.2, 37.1, 41.2, 18, 33.9, 49.1}, - })...), + }, charts.ChartTypeBar)...), Children: []charts.ChartOption{ { Legend: charts.LegendOption{ @@ -533,7 +533,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) { 435.9, 354.3, 285.9, 204.5, }, charts.PieSeriesOption{ Radius: "35%", - }), + }).ToGenericSeriesList(), }, }, }, diff --git a/funnel_chart.go b/funnel_chart.go index 4e42e42..ee61313 100644 --- a/funnel_chart.go +++ b/funnel_chart.go @@ -36,8 +36,8 @@ type FunnelChartOption struct { Padding Box // Font is the font used to render the chart. Font *truetype.Font - // SeriesList provides the data series. - SeriesList SeriesList + // SeriesList provides the data population for the chart, typically constructed using NewSeriesListFunnel. + SeriesList FunnelSeriesList // Title are options for rendering the title. Title TitleOption // Legend are options for the data legend. @@ -46,14 +46,14 @@ type FunnelChartOption struct { ValueFormatter ValueFormatter } -func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { +func (f *funnelChart) render(result *defaultRenderResult, seriesList FunnelSeriesList) (Box, error) { opt := f.opt count := len(seriesList) if count == 0 { return BoxZero, errors.New("empty series list") } seriesPainter := result.seriesPainter - max := seriesList[0].Data[0] + max := seriesList[0].Value var min float64 theme := opt.Theme gap := 2 @@ -65,27 +65,26 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) var y int widthList := make([]int, len(seriesList)) textList := make([]string, len(seriesList)) - seriesNames := seriesList.Names() + seriesNames := seriesList.names() offset := max - min for index, item := range seriesList { - value := item.Data[0] // if the maximum and minimum are consistent it's 100% widthPercent := 100.0 if offset != 0 { - widthPercent = (value - min) / offset + widthPercent = (item.Value - min) / offset } w := int(widthPercent * float64(width)) widthList[index] = w // if the maximum value is 0, the proportion is 100% percent := 1.0 if max != 0 { - percent = value / max + percent = item.Value / max } if !flagIs(false, item.Label.Show) { if item.Label.ValueFormatter != nil || opt.ValueFormatter != nil { - textList[index] = getPreferredValueFormatter(item.Label.ValueFormatter, opt.ValueFormatter)(value) + textList[index] = getPreferredValueFormatter(item.Label.ValueFormatter, opt.ValueFormatter)(item.Value) } else { - textList[index] = labelFormatFunnel(seriesNames, item.Label.FormatTemplate, index, value, percent) + textList[index] = labelFormatFunnel(seriesNames, item.Label.FormatTemplate, index, item.Value, percent) } } } @@ -165,6 +164,5 @@ func (f *funnelChart) Render() (Box, error) { if err != nil { return BoxZero, err } - seriesList := opt.SeriesList.Filter(ChartTypeFunnel) - return f.render(renderResult, seriesList) + return f.render(renderResult, opt.SeriesList) } diff --git a/funnel_chart_test.go b/funnel_chart_test.go index 673ab1f..40e54fa 100644 --- a/funnel_chart_test.go +++ b/funnel_chart_test.go @@ -28,7 +28,7 @@ func TestNewFunnelChartOptionWithData(t *testing.T) { opt := NewFunnelChartOptionWithData([]float64{12, 24, 48}) assert.Len(t, opt.SeriesList, 3) - assert.Equal(t, ChartTypeFunnel, opt.SeriesList[0].Type) + assert.Equal(t, ChartTypeFunnel, opt.SeriesList[0].getType()) assert.Equal(t, defaultPadding, opt.Padding) p := NewPainter(PainterOptions{}) diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go index de03e66..b5d85fd 100644 --- a/horizontal_bar_chart.go +++ b/horizontal_bar_chart.go @@ -30,8 +30,8 @@ type HorizontalBarChartOption struct { Padding Box // Font is the font used to render the chart. Font *truetype.Font - // SeriesList provides the data series. - SeriesList SeriesList + // SeriesList provides the data population for the chart, typically constructed using NewSeriesListHorizontalBar. + SeriesList HorizontalBarSeriesList // StackSeries if set to *true a single bar with the colored series stacked together will be rendered. // This feature will result in some options being ignored, including BarMargin and SeriesLabelPosition. // MarkLine is also interpreted differently, only the first Series will have the MarkLine rendered (as it's the @@ -64,7 +64,7 @@ func newHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontal } } -func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { +func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList HorizontalBarSeriesList) (Box, error) { p := h.p opt := h.opt seriesCount := len(seriesList) @@ -76,7 +76,7 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri y0, y1 := yRange.GetRange(0) height := int(y1 - y0) stackedSeries := flagIs(true, opt.StackSeries) - min, max, sumMax := seriesList.getMinMaxSumMax(0, stackedSeries) + min, max, sumMax := getSeriesMinMaxSumMax(seriesList, 0, stackedSeries) // If stacking, keep track of accumulated widths for each data index (after the “reverse” logic). var accumulatedWidths []int var margin, barMargin, barHeight int @@ -89,10 +89,10 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri margin, barMargin, barHeight = calculateBarMarginsAndSize(seriesCount, height, opt.BarHeight, opt.BarMargin) } - seriesNames := seriesList.Names() + seriesNames := seriesList.names() // xRange is used to convert data values into horizontal bar widths xRange := newRange(p, getPreferredValueFormatter(opt.XAxis.ValueFormatter, opt.ValueFormatter), - seriesPainter.Width(), len(seriesList[0].Data), min, max, 1.0, 1.0) + seriesPainter.Width(), len(seriesList[0].Values), min, max, 1.0, 1.0) divideValues := yRange.AutoDivide() var rendererList []renderer @@ -106,7 +106,7 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri rendererList = append(rendererList, labelPainter) } - for j, item := range series.Data { + for j, item := range series.Values { if j >= yRange.divideCount { continue } @@ -201,6 +201,5 @@ func (h *horizontalBarChart) Render() (Box, error) { if err != nil { return BoxZero, err } - seriesList := opt.SeriesList.Filter(ChartTypeHorizontalBar) - return h.render(renderResult, seriesList) + return h.render(renderResult, opt.SeriesList) } diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go index f624184..35beacd 100644 --- a/horizontal_bar_chart_test.go +++ b/horizontal_bar_chart_test.go @@ -76,7 +76,7 @@ func TestNewHorizontalBarChartOptionWithData(t *testing.T) { }) assert.Len(t, opt.SeriesList, 2) - assert.Equal(t, ChartTypeHorizontalBar, opt.SeriesList[0].Type) + assert.Equal(t, ChartTypeHorizontalBar, opt.SeriesList[0].getType()) assert.Equal(t, defaultPadding, opt.Padding) p := NewPainter(PainterOptions{}) diff --git a/line_chart.go b/line_chart.go index 8d39c55..b4cecbe 100644 --- a/line_chart.go +++ b/line_chart.go @@ -29,9 +29,9 @@ func NewLineChartOptionWithData(data [][]float64) LineChartOption { Theme: GetDefaultTheme(), Font: GetDefaultFont(), XAxis: XAxisOption{ - Labels: make([]string, sl.getMaxDataCount(ChartTypeLine)), + Labels: make([]string, getSeriesMaxDataCount(sl)), }, - YAxis: make([]YAxisOption, sl.getYAxisCount()), + YAxis: make([]YAxisOption, getSeriesYAxisCount(sl)), ValueFormatter: defaultValueFormatter, } } @@ -43,8 +43,8 @@ type LineChartOption struct { Padding Box // Font is the font used to render the chart. Font *truetype.Font - // SeriesList provides the data series. - SeriesList SeriesList + // SeriesList provides the data population for the chart, typically constructed using NewSeriesListLine. + SeriesList LineSeriesList // StackSeries if set to *true the lines will be layered over each other, with the last series value representing // the sum of all the values. Enabling this will also enable FillArea (which until v0.5 can't be disabled). // Some options will be ignored when StackedSeries is enabled, this includes StrokeSmoothingTension. @@ -80,7 +80,7 @@ type LineChartOption struct { const showSymbolDefaultThreshold = 100 -func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { +func (l *lineChart) render(result *defaultRenderResult, seriesList LineSeriesList) (Box, error) { p := l.p opt := l.opt seriesCount := len(seriesList) @@ -111,7 +111,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( } xDivideValues := autoDivide(seriesPainter.Width(), xDivideCount) xValues := make([]int, len(xDivideValues)-1) - dataCount := seriesList.getMaxDataCount(ChartTypeLine) + dataCount := getSeriesMaxDataCount(seriesList) // accumulatedValues is used for stacking: it holds the summed data values at each X index var accumulatedValues []float64 if stackedSeries { @@ -141,21 +141,21 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( markLinePainter := newMarkLinePainter(seriesPainter) rendererList := []renderer{markPointPainter, markLinePainter} - seriesNames := seriesList.Names() + seriesNames := seriesList.names() var priorSeriesPoints []Point for index := range seriesList { series := seriesList[index] stackSeries := stackedSeries && series.YAxisIndex == 0 seriesColor := opt.Theme.GetSeriesColor(index) yRange := result.axisRanges[series.YAxisIndex] - points := make([]Point, len(series.Data)) + points := make([]Point, len(series.Values)) var labelPainter *seriesLabelPainter if flagIs(true, series.Label.Show) { labelPainter = newSeriesLabelPainter(seriesPainter, seriesNames, series.Label, opt.Theme, opt.Font) rendererList = append(rendererList, labelPainter) } - for i, item := range series.Data { + for i, item := range series.Values { if item == GetNullValue() { points[i] = Point{X: xValues[i], Y: math.MaxInt32} } else if stackSeries { @@ -251,7 +251,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( var globalSeriesData []float64 // lazily initialized if series.MarkLine.GlobalLine && stackSeries && index == seriesCount-1 { if globalSeriesData == nil { - globalSeriesData = seriesList.makeSumSeries(ChartTypeLine, series.YAxisIndex).Data + globalSeriesData = sumSeriesData(seriesList, series.YAxisIndex) } markLinePainter.add(markLineRenderOption{ fillColor: defaultGlobalMarkFillColor, @@ -259,7 +259,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( strokeColor: defaultGlobalMarkFillColor, font: opt.Font, markline: series.MarkLine, - seriesData: globalSeriesData, + seriesValues: globalSeriesData, axisRange: yRange, valueFormatterDefault: markLineValueFormatter, }) @@ -271,21 +271,21 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( strokeColor: seriesColor, font: opt.Font, markline: series.MarkLine, - seriesData: series.Data, + seriesValues: series.Values, axisRange: yRange, valueFormatterDefault: markLineValueFormatter, }) } if series.MarkPoint.GlobalPoint && stackSeries && index == seriesCount-1 { if globalSeriesData == nil { - globalSeriesData = seriesList.makeSumSeries(ChartTypeLine, series.YAxisIndex).Data + globalSeriesData = sumSeriesData(seriesList, series.YAxisIndex) } markPointPainter.add(markPointRenderOption{ fillColor: defaultGlobalMarkFillColor, font: opt.Font, points: points, markpoint: series.MarkPoint, - seriesData: globalSeriesData, + seriesValues: globalSeriesData, valueFormatterDefault: markPointValueFormatter, seriesLabelPainter: labelPainter, }) @@ -295,7 +295,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( font: opt.Font, points: points, markpoint: series.MarkPoint, - seriesData: series.Data, + seriesValues: series.Values, valueFormatterDefault: markPointValueFormatter, seriesLabelPainter: labelPainter, }) @@ -342,6 +342,5 @@ func (l *lineChart) Render() (Box, error) { if err != nil { return BoxZero, err } - seriesList := opt.SeriesList.Filter(ChartTypeLine) - return l.render(renderResult, seriesList) + return l.render(renderResult, opt.SeriesList) } diff --git a/line_chart_test.go b/line_chart_test.go index c8c12d1..e936c9b 100644 --- a/line_chart_test.go +++ b/line_chart_test.go @@ -120,7 +120,7 @@ func TestNewLineChartOptionWithData(t *testing.T) { }) assert.Len(t, opt.SeriesList, 2) - assert.Equal(t, ChartTypeLine, opt.SeriesList[0].Type) + assert.Equal(t, ChartTypeLine, opt.SeriesList[0].getType()) assert.Len(t, opt.YAxis, 1) assert.Equal(t, defaultPadding, opt.Padding) @@ -502,7 +502,7 @@ func TestLineChart(t *testing.T) { name: "line_gap", makeOptions: func() LineChartOption { opt := makeMinimalLineChartOption() - opt.SeriesList[0].Data[3] = GetNullValue() + opt.SeriesList[0].Values[3] = GetNullValue() return opt }, result: "1.44k1.28k1.12k9608006404803201600", @@ -511,8 +511,8 @@ func TestLineChart(t *testing.T) { name: "line_gap_dot", makeOptions: func() LineChartOption { opt := makeMinimalLineChartOption() - opt.SeriesList[0].Data[3] = GetNullValue() - opt.SeriesList[0].Data[5] = GetNullValue() + opt.SeriesList[0].Values[3] = GetNullValue() + opt.SeriesList[0].Values[5] = GetNullValue() return opt }, result: "1.44k1.28k1.12k9608006404803201600", @@ -521,7 +521,7 @@ func TestLineChart(t *testing.T) { name: "line_gap_fill_area", makeOptions: func() LineChartOption { opt := makeMinimalLineChartOption() - opt.SeriesList[0].Data[3] = GetNullValue() + opt.SeriesList[0].Values[3] = GetNullValue() opt.FillArea = Ptr(true) return opt }, @@ -531,9 +531,9 @@ func TestLineChart(t *testing.T) { name: "line_gap_start_fill_area", makeOptions: func() LineChartOption { opt := makeMinimalLineChartOption() - opt.SeriesList[0].Data[0] = GetNullValue() - opt.SeriesList[0].Data[1] = GetNullValue() - opt.SeriesList[1].Data[0] = GetNullValue() + opt.SeriesList[0].Values[0] = GetNullValue() + opt.SeriesList[0].Values[1] = GetNullValue() + opt.SeriesList[1].Values[0] = GetNullValue() opt.FillArea = Ptr(true) return opt }, @@ -544,7 +544,7 @@ func TestLineChart(t *testing.T) { makeOptions: func() LineChartOption { opt := makeMinimalLineChartOption() opt.StrokeSmoothingTension = 0.8 - opt.SeriesList[0].Data[3] = GetNullValue() + opt.SeriesList[0].Values[3] = GetNullValue() return opt }, result: "1.44k1.28k1.12k9608006404803201600", @@ -554,7 +554,7 @@ func TestLineChart(t *testing.T) { makeOptions: func() LineChartOption { opt := makeMinimalLineChartOption() opt.StrokeSmoothingTension = 0.8 - opt.SeriesList[0].Data[3] = GetNullValue() + opt.SeriesList[0].Values[3] = GetNullValue() opt.FillArea = Ptr(true) return opt }, diff --git a/mark_line.go b/mark_line.go index 706bc63..7525497 100644 --- a/mark_line.go +++ b/mark_line.go @@ -40,7 +40,7 @@ type markLineRenderOption struct { fontColor Color strokeColor Color font *truetype.Font - seriesData []float64 + seriesValues []float64 markline SeriesMarkLine axisRange axisRange valueFormatterDefault ValueFormatter @@ -52,7 +52,7 @@ func (m *markLinePainter) Render() (Box, error) { if len(opt.markline.Data) == 0 { continue } - summary := summarizePopulationData(opt.seriesData) + summary := summarizePopulationData(opt.seriesValues) fontStyle := FontStyle{ Font: getPreferredFont(opt.font), FontColor: opt.fontColor, diff --git a/mark_line_test.go b/mark_line_test.go index 615076f..5814d2b 100644 --- a/mark_line_test.go +++ b/mark_line_test.go @@ -18,10 +18,10 @@ func TestMarkLine(t *testing.T) { render: func(p *Painter) ([]byte, error) { markLine := newMarkLinePainter(p) markLine.add(markLineRenderOption{ - fillColor: ColorBlack, - fontColor: ColorBlack, - strokeColor: ColorBlack, - seriesData: []float64{1, 2, 3}, + fillColor: ColorBlack, + fontColor: ColorBlack, + strokeColor: ColorBlack, + seriesValues: []float64{1, 2, 3}, markline: NewMarkLine( SeriesMarkDataTypeMax, SeriesMarkDataTypeAverage, diff --git a/mark_point.go b/mark_point.go index a408fd8..7002f94 100644 --- a/mark_point.go +++ b/mark_point.go @@ -33,7 +33,7 @@ func (m *markPointPainter) add(opt markPointRenderOption) { type markPointRenderOption struct { fillColor Color font *truetype.Font - seriesData []float64 + seriesValues []float64 markpoint SeriesMarkPoint seriesLabelPainter *seriesLabelPainter points []Point @@ -54,7 +54,7 @@ func (m *markPointPainter) Render() (Box, error) { continue } points := opt.points - summary := summarizePopulationData(opt.seriesData) + summary := summarizePopulationData(opt.seriesValues) symbolSize := opt.markpoint.SymbolSize if symbolSize == 0 { symbolSize = 28 diff --git a/mark_point_test.go b/mark_point_test.go index 565b917..3f71fd2 100644 --- a/mark_point_test.go +++ b/mark_point_test.go @@ -18,9 +18,9 @@ func TestMarkPoint(t *testing.T) { render: func(p *Painter) ([]byte, error) { markPoint := newMarkPointPainter(p) markPoint.add(markPointRenderOption{ - fillColor: ColorBlack, - seriesData: []float64{1, 2, 3}, - markpoint: NewMarkPoint(SeriesMarkDataTypeMax), + fillColor: ColorBlack, + seriesValues: []float64{1, 2, 3}, + markpoint: NewMarkPoint(SeriesMarkDataTypeMax), points: []Point{ {X: 10, Y: 10}, {X: 30, Y: 30}, diff --git a/pie_chart.go b/pie_chart.go index 6f2e270..b21555c 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -32,8 +32,8 @@ type PieChartOption struct { Padding Box // Font is the font used to render the chart. Font *truetype.Font - // SeriesList provides the data series. - SeriesList SeriesList + // SeriesList provides the data population for the chart, typically constructed using NewSeriesListPie. + SeriesList PieSeriesList // Title are options for rendering the title. Title TitleOption // Legend are options for the data legend. @@ -149,26 +149,23 @@ func (s *sector) calculateTextXY(textBox Box) (x int, y int) { return } -func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { +func (p *pieChart) render(result *defaultRenderResult, seriesList PieSeriesList) (Box, error) { opt := p.opt seriesCount := len(seriesList) if seriesCount == 0 { return BoxZero, errors.New("empty series list") } - values := make([]float64, seriesCount) var total float64 var radiusValue string for index, series := range seriesList { if series.Radius != "" { radiusValue = series.Radius } - value := chartdraw.SumFloat64(series.Data...) - values[index] = value - total += value - if value < 0 { + if series.Value < 0 { return BoxZero, fmt.Errorf("unsupported negative value for series index %d", index) } + total += series.Value } if total <= 0 { return BoxZero, errors.New("the sum value of pie chart should greater than 0") @@ -187,25 +184,25 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B labelRadius := radius + float64(labelLineWidth) seriesNames := opt.Legend.SeriesNames if len(seriesNames) == 0 { - seriesNames = seriesList.Names() + seriesNames = seriesList.names() } theme := opt.Theme var currentValue float64 var quadrant1, quadrant2, quadrant3, quadrant4 []sector - for index, v := range values { - series := seriesList[index] + seriesLen := len(seriesList) + for index, series := range seriesList { seriesRadius := radius if series.Radius != "" { seriesRadius = getRadius(float64(diameter), series.Radius) } color := theme.GetSeriesColor(index) - if index == len(values)-1 { + if index == seriesLen-1 { if color == theme.GetSeriesColor(0) { color = theme.GetSeriesColor(1) } } - s := newSector(cx, cy, seriesRadius, labelRadius, v, currentValue, total, labelLineWidth, + s := newSector(cx, cy, seriesRadius, labelRadius, series.Value, currentValue, total, labelLineWidth, seriesNames[index], series.Label, opt.ValueFormatter, color) switch quadrant := s.quadrant; quadrant { case 1: @@ -217,7 +214,7 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B case 4: quadrant4 = append(quadrant4, s) } - currentValue += v + currentValue += series.Value } sectors := append(quadrant1, quadrant4...) sectors = append(sectors, quadrant3...) @@ -304,6 +301,5 @@ func (p *pieChart) Render() (Box, error) { if err != nil { return BoxZero, err } - seriesList := opt.SeriesList.Filter(ChartTypePie) - return p.render(renderResult, seriesList) + return p.render(renderResult, opt.SeriesList) } diff --git a/pie_chart_test.go b/pie_chart_test.go index 31a4e3c..9fc644a 100644 --- a/pie_chart_test.go +++ b/pie_chart_test.go @@ -34,7 +34,7 @@ func TestNewPieChartOptionWithData(t *testing.T) { opt := NewPieChartOptionWithData([]float64{12, 24, 48}) assert.Len(t, opt.SeriesList, 3) - assert.Equal(t, ChartTypePie, opt.SeriesList[0].Type) + assert.Equal(t, ChartTypePie, opt.SeriesList[0].getType()) assert.Equal(t, defaultPadding, opt.Padding) p := NewPainter(PainterOptions{}) diff --git a/radar_chart.go b/radar_chart.go index 1c678c2..abb6f29 100644 --- a/radar_chart.go +++ b/radar_chart.go @@ -45,14 +45,16 @@ type RadarChartOption struct { Padding Box // Font is the font used to render the chart. Font *truetype.Font - // SeriesList provides the data series. - SeriesList SeriesList + // SeriesList provides the data population for the chart, typically constructed using NewSeriesListRadar. + SeriesList RadarSeriesList // Title are options for rendering the title. Title TitleOption // Legend are options for the data legend. Legend LegendOption // RadarIndicators provides the radar indicator list. RadarIndicators []RadarIndicator + // Radius for radar e.g.: 40%, default is "40%" + Radius string // ValueFormatter defines how float values should be rendered to strings, notably for series labels. ValueFormatter ValueFormatter // backgroundIsFilled is set to true if the background is filled. @@ -82,7 +84,7 @@ func newRadarChart(p *Painter, opt RadarChartOption) *radarChart { } } -func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) { +func (r *radarChart) render(result *defaultRenderResult, seriesList RadarSeriesList) (Box, error) { opt := r.opt indicators := opt.RadarIndicators sides := len(indicators) @@ -93,7 +95,7 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) } maxValues := make([]float64, len(indicators)) for _, series := range seriesList { - for index, item := range series.Data { + for index, item := range series.Values { if index < len(maxValues) && item > maxValues[index] { maxValues[index] = item } @@ -105,20 +107,13 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) } } - var radiusValue string - for _, series := range seriesList { - if series.Radius != "" { - radiusValue = series.Radius - } - } - seriesPainter := result.seriesPainter theme := opt.Theme cx := seriesPainter.Width() >> 1 cy := seriesPainter.Height() >> 1 diameter := chartdraw.MinInt(seriesPainter.Width(), seriesPainter.Height()) - radius := getRadius(float64(diameter), radiusValue) + radius := getRadius(float64(diameter), opt.Radius) divideCount := 5 divideRadius := float64(int(radius / float64(divideCount))) @@ -185,7 +180,7 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) valueFormatter := getPreferredValueFormatter(series.Label.ValueFormatter, opt.ValueFormatter, radarDefaultValueFormatter) linePoints := make([]Point, 0, maxCount) - for j, item := range series.Data { + for j, item := range series.Values { if j >= maxCount { continue } @@ -210,8 +205,8 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList) dotWith := defaultDotWidth for index, point := range linePoints { seriesPainter.Circle(dotWith, point.X, point.Y, dotFillColor, color, defaultStrokeWidth) - if flagIs(true, series.Label.Show) && index < len(series.Data) { - valueStr := valueFormatter(series.Data[index]) + if flagIs(true, series.Label.Show) && index < len(series.Values) { + valueStr := valueFormatter(series.Values[index]) b := seriesPainter.MeasureText(valueStr, 0, fontStyle) seriesPainter.Text(valueStr, point.X-b.Width()/2, point.Y, 0, fontStyle) } @@ -247,6 +242,5 @@ func (r *radarChart) Render() (Box, error) { if err != nil { return BoxZero, err } - seriesList := opt.SeriesList.Filter(ChartTypeRadar) - return r.render(renderResult, seriesList) + return r.render(renderResult, opt.SeriesList) } diff --git a/radar_chart_test.go b/radar_chart_test.go index 721813b..7a23f79 100644 --- a/radar_chart_test.go +++ b/radar_chart_test.go @@ -52,7 +52,7 @@ func TestNewRadarChartOptionWithData(t *testing.T) { }) assert.Len(t, opt.SeriesList, 2) - assert.Equal(t, ChartTypeRadar, opt.SeriesList[0].Type) + assert.Equal(t, ChartTypeRadar, opt.SeriesList[0].getType()) assert.Equal(t, defaultPadding, opt.Padding) p := NewPainter(PainterOptions{}) diff --git a/series.go b/series.go index fdfe557..65e3581 100644 --- a/series.go +++ b/series.go @@ -6,28 +6,9 @@ import ( "strings" "github.com/dustin/go-humanize" -) -// newSeriesListFromValues returns a series list for the given values and chart type. -func newSeriesListFromValues(values [][]float64, chartType string, label SeriesLabel, names []string, - radius string, markPoint SeriesMarkPoint, markLine SeriesMarkLine) SeriesList { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - s := Series{ - Data: value, - Type: chartType, - Label: label, - Radius: radius, - MarkPoint: markPoint, - MarkLine: markLine, - } - if index < len(names) { - s.Name = names[index] - } - seriesList[index] = s - } - return seriesList -} + "github.com/go-analyze/charts/chartdraw" +) type SeriesLabel struct { // FormatTemplate is a string template for formatting the data label. @@ -80,12 +61,13 @@ type SeriesMarkLine struct { Data []SeriesMarkData } -// Series references a population of data. -type Series struct { +// GenericSeries references a population of data for any type of charts. The chart specific fields will only be active +// for chart types which support them. +type GenericSeries struct { // Type is the type of series, it can be "line", "bar" or "pie". Default value is "line". Type string - // Data provides the series data list. - Data []float64 + // Values provides the series data values. + Values []float64 // YAxisIndex is the index for the axis, it must be 0 or 1. YAxisIndex int // Label provides the series labels. @@ -102,53 +84,778 @@ type Series struct { MarkLine SeriesMarkLine } -// SeriesList is a list of series to be rendered on the chart, typically constructed using NewSeriesListLine, -// NewSeriesListBar, NewSeriesListHorizontalBar, NewSeriesListPie, NewSeriesListRadar, or NewSeriesListFunnel. -// These Series can be appended to each other if multiple chart types should be rendered to the same axis. -type SeriesList []Series +func (g *GenericSeries) getYAxisIndex() int { + return g.YAxisIndex +} + +func (g *GenericSeries) getValues() []float64 { + return g.Values +} + +func (g *GenericSeries) getType() string { + return g.Type +} + +// GenericSeriesList provides the data populations for any chart type configured through ChartOption. +type GenericSeriesList []GenericSeries + +func (g GenericSeriesList) names() []string { + return seriesNames(g) +} + +func (g GenericSeriesList) len() int { + return len(g) +} + +func (g GenericSeriesList) getSeries(index int) series { + return &g[index] +} + +func (g GenericSeriesList) getSeriesName(index int) string { + return g[index].Name +} + +func (g GenericSeriesList) getSeriesValues(index int) []float64 { + return g[index].Values +} + +func (g GenericSeriesList) hasMarkPoint() bool { + for _, s := range g { + if len(s.MarkPoint.Data) > 0 { + return true + } + } + return false +} + +func (g GenericSeriesList) setSeriesName(index int, name string) { + g[index].Name = name +} + +func (g GenericSeriesList) sortByNameIndex(dict map[string]int) { + sort.Slice(g, func(i, j int) bool { + return dict[g[i].Name] < dict[g[j].Name] + }) +} + +// LineSeries references a population of data for line charts. +type LineSeries struct { + // Values provides the series data values. + Values []float64 + // YAxisIndex is the index for the axis, it must be 0 or 1. + YAxisIndex int + // Label provides the series labels. + Label SeriesLabel + // Name specifies a name for the series. + Name string + // MarkPoint provides a series for mark points. If Label is also enabled, the MarkPoint will replace the label + // where rendered. + MarkPoint SeriesMarkPoint + // MarkLine provides a series for mark lines. When using a MarkLine, you will want to configure padding to the + // chart on the right for the values. + MarkLine SeriesMarkLine +} + +func (l *LineSeries) getYAxisIndex() int { + return l.YAxisIndex +} + +func (l *LineSeries) getValues() []float64 { + return l.Values +} + +func (l *LineSeries) getType() string { + return ChartTypeLine +} + +func (l *LineSeries) Summary() populationSummary { + return summarizePopulationData(l.Values) +} + +// LineSeriesList provides the data populations for line charts (LineChartOption). +type LineSeriesList []LineSeries + +func (l LineSeriesList) names() []string { + return seriesNames(l) +} + +func (l LineSeriesList) SumSeries() []float64 { + return sumSeriesData(l, -1) +} + +func (l LineSeriesList) len() int { + return len(l) +} + +func (l LineSeriesList) getSeries(index int) series { + return &l[index] +} + +func (l LineSeriesList) getSeriesName(index int) string { + return l[index].Name +} + +func (l LineSeriesList) getSeriesValues(index int) []float64 { + return l[index].Values +} + +func (l LineSeriesList) hasMarkPoint() bool { + for _, s := range l { + if len(s.MarkPoint.Data) > 0 { + return true + } + } + return false +} + +func (l LineSeriesList) setSeriesName(index int, name string) { + l[index].Name = name +} + +func (l LineSeriesList) sortByNameIndex(dict map[string]int) { + sort.Slice(l, func(i, j int) bool { + return dict[l[i].Name] < dict[l[j].Name] + }) +} + +func (l LineSeriesList) ToGenericSeriesList() GenericSeriesList { + result := make([]GenericSeries, len(l)) + for i, s := range l { + result[i] = GenericSeries{ + Values: s.Values, + YAxisIndex: s.YAxisIndex, + Label: s.Label, + Name: s.Name, + Type: ChartTypeLine, + MarkLine: s.MarkLine, + MarkPoint: s.MarkPoint, + } + } + return result +} + +// BarSeries references a population of data for bar charts. +type BarSeries struct { + // Values provides the series data values. + Values []float64 + // YAxisIndex is the index for the axis, it must be 0 or 1. + YAxisIndex int + // Label provides the series labels. + Label SeriesLabel + // Name specifies a name for the series. + Name string + // MarkPoint provides a series for mark points. If Label is also enabled, the MarkPoint will replace the label + // where rendered. + MarkPoint SeriesMarkPoint + // MarkLine provides a series for mark lines. When using a MarkLine, you will want to configure padding to the + // chart on the right for the values. + MarkLine SeriesMarkLine +} + +func (b *BarSeries) getYAxisIndex() int { + return b.YAxisIndex +} + +func (b *BarSeries) getValues() []float64 { + return b.Values +} + +func (b *BarSeries) getType() string { + return ChartTypeBar +} + +func (b *BarSeries) Summary() populationSummary { + return summarizePopulationData(b.Values) +} + +// BarSeriesList provides the data populations for line charts (BarChartOption). +type BarSeriesList []BarSeries + +func (b BarSeriesList) names() []string { + return seriesNames(b) +} + +func (b BarSeriesList) SumSeries() []float64 { + return sumSeriesData(b, -1) +} + +func (b BarSeriesList) len() int { + return len(b) +} + +func (b BarSeriesList) getSeries(index int) series { + return &b[index] +} + +func (b BarSeriesList) getSeriesName(index int) string { + return b[index].Name +} + +func (b BarSeriesList) getSeriesValues(index int) []float64 { + return b[index].Values +} + +func (b BarSeriesList) hasMarkPoint() bool { + for _, s := range b { + if len(s.MarkPoint.Data) > 0 { + return true + } + } + return false +} + +func (b BarSeriesList) setSeriesName(index int, name string) { + b[index].Name = name +} + +func (b BarSeriesList) sortByNameIndex(dict map[string]int) { + sort.Slice(b, func(i, j int) bool { + return dict[b[i].Name] < dict[b[j].Name] + }) +} + +func (b BarSeriesList) ToGenericSeriesList() GenericSeriesList { + result := make([]GenericSeries, len(b)) + for i, s := range b { + result[i] = GenericSeries{ + Values: s.Values, + YAxisIndex: s.YAxisIndex, + Label: s.Label, + Name: s.Name, + Type: ChartTypeBar, + MarkLine: s.MarkLine, + MarkPoint: s.MarkPoint, + } + } + return result +} + +// HorizontalBarSeries references a population of data for horizontal bar charts. +type HorizontalBarSeries struct { + // Values provides the series data values. + Values []float64 + // Label provides the series labels. + Label SeriesLabel + // Name specifies a name for the series. + Name string +} + +func (h *HorizontalBarSeries) getYAxisIndex() int { + return 0 +} + +func (h *HorizontalBarSeries) getValues() []float64 { + return h.Values +} + +func (h *HorizontalBarSeries) getType() string { + return ChartTypeHorizontalBar +} + +func (h *HorizontalBarSeries) Summary() populationSummary { + return summarizePopulationData(h.Values) +} + +// HorizontalBarSeriesList provides the data populations for horizontal bar charts (HorizontalBarChartOption). +type HorizontalBarSeriesList []HorizontalBarSeries + +func (h HorizontalBarSeriesList) names() []string { + return seriesNames(h) +} + +func (h HorizontalBarSeriesList) SumSeries() []float64 { + return sumSeriesData(h, -1) +} + +func (h HorizontalBarSeriesList) len() int { + return len(h) +} + +func (h HorizontalBarSeriesList) getSeries(index int) series { + return &h[index] +} + +func (h HorizontalBarSeriesList) getSeriesName(index int) string { + return h[index].Name +} + +func (h HorizontalBarSeriesList) getSeriesValues(index int) []float64 { + return h[index].Values +} + +func (h HorizontalBarSeriesList) hasMarkPoint() bool { + return false // not currently supported on this chart type +} + +func (h HorizontalBarSeriesList) setSeriesName(index int, name string) { + h[index].Name = name +} + +func (h HorizontalBarSeriesList) sortByNameIndex(dict map[string]int) { + sort.Slice(h, func(i, j int) bool { + return dict[h[i].Name] < dict[h[j].Name] + }) +} + +func (h HorizontalBarSeriesList) ToGenericSeriesList() GenericSeriesList { + result := make([]GenericSeries, len(h)) + for i, s := range h { + result[i] = GenericSeries{ + Values: s.Values, + Label: s.Label, + Name: s.Name, + Type: ChartTypeHorizontalBar, + } + } + return result +} + +// FunnelSeries references a population of data for funnel charts. +type FunnelSeries struct { + // Value provides the value for the funnel section. + Value float64 + // Label provides the series labels. + Label SeriesLabel + // Name specifies a name for the series. + Name string +} + +func (f *FunnelSeries) getYAxisIndex() int { + return 0 +} + +func (f *FunnelSeries) getValues() []float64 { + return []float64{f.Value} +} + +func (f *FunnelSeries) getType() string { + return ChartTypeFunnel +} + +// FunnelSeriesList provides the data populations for funnel charts (FunnelChartOption). +type FunnelSeriesList []FunnelSeries + +func (f FunnelSeriesList) names() []string { + return seriesNames(f) +} + +func (f FunnelSeriesList) len() int { + return len(f) +} + +func (f FunnelSeriesList) getSeries(index int) series { + return &f[index] +} + +func (f FunnelSeriesList) getSeriesName(index int) string { + return f[index].Name +} + +func (f FunnelSeriesList) getSeriesValues(index int) []float64 { + return []float64{f[index].Value} +} -// Deprecated: Filter is deprecated, this function is not expected to be used outside the internal chart -// implementation. If you make use of this function open a GitHub issue to mention its use. -func (sl SeriesList) Filter(chartType string) SeriesList { - arr := make(SeriesList, 0, len(sl)) - for index, item := range sl { - if chartTypeMatch(chartType, item.Type) { - arr = append(arr, sl[index]) +func (f FunnelSeriesList) hasMarkPoint() bool { + return false // not supported on this chart type +} + +func (f FunnelSeriesList) setSeriesName(index int, name string) { + f[index].Name = name +} + +func (f FunnelSeriesList) sortByNameIndex(dict map[string]int) { + sort.Slice(f, func(i, j int) bool { + return dict[f[i].Name] < dict[f[j].Name] + }) +} + +func (f FunnelSeriesList) ToGenericSeriesList() GenericSeriesList { + result := make([]GenericSeries, len(f)) + for i, s := range f { + result[i] = GenericSeries{ + Values: []float64{s.Value}, + Label: s.Label, + Name: s.Name, + Type: ChartTypeFunnel, } } - return arr + return result +} + +// PieSeries references a population of data for pie charts. +type PieSeries struct { + // Value provides the value for the pie section. + Value float64 + // Label provides the series labels. + Label SeriesLabel + // Name specifies a name for the series. + Name string + // Radius for Pie chart, e.g.: 40%, default is "40%" + Radius string +} + +func (p *PieSeries) getYAxisIndex() int { + return 0 +} + +func (p *PieSeries) getValues() []float64 { + return []float64{p.Value} +} + +func (p *PieSeries) getType() string { + return ChartTypePie +} + +// PieSeriesList provides the data populations for pie charts (PieChartOption). +type PieSeriesList []PieSeries + +func (p PieSeriesList) names() []string { + return seriesNames(p) +} + +func (p PieSeriesList) len() int { + return len(p) +} + +func (p PieSeriesList) getSeries(index int) series { + return &p[index] +} + +func (p PieSeriesList) getSeriesName(index int) string { + return p[index].Name +} + +func (p PieSeriesList) getSeriesValues(index int) []float64 { + return []float64{p[index].Value} +} + +func (p PieSeriesList) hasMarkPoint() bool { + return false // not supported on this chart type +} + +func (p PieSeriesList) setSeriesName(index int, name string) { + p[index].Name = name +} + +func (p PieSeriesList) sortByNameIndex(dict map[string]int) { + sort.Slice(p, func(i, j int) bool { + return dict[p[i].Name] < dict[p[j].Name] + }) +} + +func (p PieSeriesList) ToGenericSeriesList() GenericSeriesList { + result := make([]GenericSeries, len(p)) + for i, s := range p { + result[i] = GenericSeries{ + Values: []float64{s.Value}, + Label: s.Label, + Name: s.Name, + Type: ChartTypePie, + Radius: s.Radius, + } + } + return result +} + +// RadarSeries references a population of data for radar charts. +type RadarSeries struct { + // Values provides the series data list. + Values []float64 + // Label provides the series labels. + Label SeriesLabel + // Name specifies a name for the series. + Name string +} + +func (r *RadarSeries) getYAxisIndex() int { + return 0 +} + +func (r *RadarSeries) getValues() []float64 { + return r.Values +} + +func (r *RadarSeries) getType() string { + return ChartTypeRadar +} + +// RadarSeriesList provides the data populations for line charts (RadarChartOption). +type RadarSeriesList []RadarSeries + +func (r RadarSeriesList) names() []string { + return seriesNames(r) +} + +func (r RadarSeriesList) len() int { + return len(r) +} + +func (r RadarSeriesList) getSeries(index int) series { + return &r[index] +} + +func (r RadarSeriesList) getSeriesName(index int) string { + return r[index].Name +} + +func (r RadarSeriesList) getSeriesValues(index int) []float64 { + return r[index].Values +} + +func (r RadarSeriesList) hasMarkPoint() bool { + return false // not supported on this chart type +} + +func (r RadarSeriesList) setSeriesName(index int, name string) { + r[index].Name = name +} + +func (r RadarSeriesList) sortByNameIndex(dict map[string]int) { + sort.Slice(r, func(i, j int) bool { + return dict[r[i].Name] < dict[r[j].Name] + }) +} + +func (r RadarSeriesList) ToGenericSeriesList() GenericSeriesList { + result := make([]GenericSeries, len(r)) + for i, s := range r { + result[i] = GenericSeries{ + Values: s.Values, + Label: s.Label, + Name: s.Name, + Type: ChartTypeRadar, + } + } + return result +} + +// seriesList contains internal functions for operations that occur across chart types. Most of this interface usage +// is within `series.go` and `charts.go`. +type seriesList interface { + len() int + getSeries(index int) series + getSeriesName(index int) string + getSeriesValues(index int) []float64 + names() []string + hasMarkPoint() bool + setSeriesName(index int, name string) + sortByNameIndex(dict map[string]int) +} + +// series interface is used to provide the raw series struct to callers of seriesList, allowing direct type checks. +type series interface { + getType() string + getYAxisIndex() int + getValues() []float64 +} + +func filterSeriesList[T any](sl seriesList, chartType string) T { + switch chartType { + case ChartTypeLine: + result := make(LineSeriesList, 0, sl.len()) + for i := 0; i < sl.len(); i++ { + s := sl.getSeries(i) + if chartTypeMatch(chartType, s.getType()) { + switch v := s.(type) { + case *LineSeries: + result = append(result, *v) + case *GenericSeries: + result = append(result, LineSeries{ + Values: v.Values, + YAxisIndex: v.YAxisIndex, + Label: v.Label, + Name: v.Name, + MarkLine: v.MarkLine, + MarkPoint: v.MarkPoint, + }) + } + } + } + return any(result).(T) + case ChartTypeBar: + result := make(BarSeriesList, 0, sl.len()) + for i := 0; i < sl.len(); i++ { + s := sl.getSeries(i) + if chartTypeMatch(chartType, s.getType()) { + switch v := s.(type) { + case *BarSeries: + result = append(result, *v) + case *GenericSeries: + result = append(result, BarSeries{ + Values: v.Values, + YAxisIndex: v.YAxisIndex, + Label: v.Label, + Name: v.Name, + MarkLine: v.MarkLine, + MarkPoint: v.MarkPoint, + }) + } + } + } + return any(result).(T) + case ChartTypeHorizontalBar: + result := make(HorizontalBarSeriesList, 0, sl.len()) + for i := 0; i < sl.len(); i++ { + s := sl.getSeries(i) + if chartTypeMatch(chartType, s.getType()) { + switch v := s.(type) { + case *HorizontalBarSeries: + result = append(result, *v) + case *GenericSeries: + result = append(result, HorizontalBarSeries{ + Values: v.Values, + Label: v.Label, + Name: v.Name, + }) + } + } + } + return any(result).(T) + case ChartTypePie: + result := make(PieSeriesList, 0, sl.len()) + for i := 0; i < sl.len(); i++ { + s := sl.getSeries(i) + if chartTypeMatch(chartType, s.getType()) { + switch v := s.(type) { + case *PieSeries: + result = append(result, *v) + case *GenericSeries: + result = append(result, PieSeries{ + Value: chartdraw.SumFloat64(v.Values...), + Label: v.Label, + Name: v.Name, + Radius: v.Radius, + }) + } + } + } + return any(result).(T) + case ChartTypeRadar: + result := make(RadarSeriesList, 0, sl.len()) + for i := 0; i < sl.len(); i++ { + s := sl.getSeries(i) + if chartTypeMatch(chartType, s.getType()) { + switch v := s.(type) { + case *RadarSeries: + result = append(result, *v) + case *GenericSeries: + result = append(result, RadarSeries{ + Values: v.Values, + Label: v.Label, + Name: v.Name, + }) + } + } + } + return any(result).(T) + case ChartTypeFunnel: + result := make(FunnelSeriesList, 0, sl.len()) + for i := 0; i < sl.len(); i++ { + s := sl.getSeries(i) + if chartTypeMatch(chartType, s.getType()) { + switch v := s.(type) { + case *FunnelSeries: + result = append(result, *v) + case *GenericSeries: + result = append(result, FunnelSeries{ + Value: chartdraw.SumFloat64(v.Values...), + Label: v.Label, + Name: v.Name, + }) + } + } + } + return any(result).(T) + default: + result := make(GenericSeriesList, 0, sl.len()) + for i := 0; i < sl.len(); i++ { + s := sl.getSeries(i) + if chartTypeMatch(chartType, s.getType()) { + switch v := s.(type) { + case *LineSeries: + result = append(result, GenericSeries{ + Values: v.Values, + YAxisIndex: v.YAxisIndex, + Label: v.Label, + Name: v.Name, + MarkLine: v.MarkLine, + MarkPoint: v.MarkPoint, + }) + case *BarSeries: + result = append(result, GenericSeries{ + Values: v.Values, + YAxisIndex: v.YAxisIndex, + Label: v.Label, + Name: v.Name, + MarkLine: v.MarkLine, + MarkPoint: v.MarkPoint, + }) + case *HorizontalBarSeries: + result = append(result, GenericSeries{ + Values: v.Values, + Label: v.Label, + Name: v.Name, + }) + case *PieSeries: + result = append(result, GenericSeries{ + Values: []float64{v.Value}, + Label: v.Label, + Name: v.Name, + Radius: v.Radius, + }) + case *RadarSeries: + result = append(result, GenericSeries{ + Values: v.Values, + Label: v.Label, + Name: v.Name, + }) + case *FunnelSeries: + result = append(result, GenericSeries{ + Values: []float64{v.Value}, + Label: v.Label, + Name: v.Name, + }) + case *GenericSeries: + result = append(result, *v) + } + } + } + return any(result).(T) + } } func chartTypeMatch(expected, actual string) bool { return expected == "" || expected == actual || (expected == ChartTypeLine && actual == "") } -func (sl SeriesList) getYAxisCount() int { - for _, series := range sl { - if series.YAxisIndex == 1 { +func getSeriesYAxisCount(sl seriesList) int { + for i := 0; i < sl.len(); i++ { + axis := sl.getSeries(i).getYAxisIndex() + if axis == 1 { return 2 - } else if series.YAxisIndex != 0 { + } else if axis != 0 { return -1 } } return 1 } -// getMinMaxSumMax returns the min, max, and maximum sum of the series for a given y-axis index (either 0 or 1). +// getSeriesMinMaxSumMax returns the min, max, and maximum sum of the series for a given y-axis index (either 0 or 1). // This is a higher performance option for internal use. calcSum provides an optimization to // only calculate the sumMax if it will be used. -func (sl SeriesList) getMinMaxSumMax(yaxisIndex int, calcSum bool) (float64, float64, float64) { +func getSeriesMinMaxSumMax(sl seriesList, yaxisIndex int, calcSum bool) (float64, float64, float64) { min := math.MaxFloat64 max := -math.MaxFloat64 var sums []float64 if calcSum { - sums = make([]float64, sl.getMaxDataCount("")) + sums = make([]float64, getSeriesMaxDataCount(sl)) } - for _, series := range sl { - if series.YAxisIndex != yaxisIndex { + for i := 0; i < sl.len(); i++ { + series := sl.getSeries(i) + if series.getYAxisIndex() != yaxisIndex { continue } - for i, item := range series.Data { + for i, item := range series.getValues() { if item == GetNullValue() { continue } @@ -174,6 +881,18 @@ func (sl SeriesList) getMinMaxSumMax(yaxisIndex int, calcSum bool) (float64, flo return min, max, maxSum } +// NewSeriesListGeneric returns a Generic series list for the given values and chart type (used in ChartOption). +func NewSeriesListGeneric(values [][]float64, chartType string) GenericSeriesList { + seriesList := make([]GenericSeries, len(values)) + for index, v := range values { + seriesList[index] = GenericSeries{ + Values: v, + Type: chartType, + } + } + return seriesList +} + // LineSeriesOption provides series customization for NewSeriesListLine. type LineSeriesOption struct { Label SeriesLabel @@ -184,13 +903,26 @@ type LineSeriesOption struct { // NewSeriesListLine builds a SeriesList for a line chart. The first dimension of the values indicates the population // of the data, while the second dimension provides the samples for the population. -func NewSeriesListLine(values [][]float64, opts ...LineSeriesOption) SeriesList { +func NewSeriesListLine(values [][]float64, opts ...LineSeriesOption) LineSeriesList { var opt LineSeriesOption if len(opts) != 0 { opt = opts[0] } - return newSeriesListFromValues(values, ChartTypeLine, - opt.Label, opt.Names, "", opt.MarkPoint, opt.MarkLine) + + seriesList := make([]LineSeries, len(values)) + for index, v := range values { + s := LineSeries{ + Values: v, + Label: opt.Label, + MarkPoint: opt.MarkPoint, + MarkLine: opt.MarkLine, + } + if index < len(opt.Names) { + s.Name = opt.Names[index] + } + seriesList[index] = s + } + return seriesList } // BarSeriesOption provides series customization for NewSeriesListBar or NewSeriesListHorizontalBar. @@ -203,24 +935,48 @@ type BarSeriesOption struct { // NewSeriesListBar builds a SeriesList for a bar chart. The first dimension of the values indicates the population // of the data, while the second dimension provides the samples for the population (on the X-Axis). -func NewSeriesListBar(values [][]float64, opts ...BarSeriesOption) SeriesList { +func NewSeriesListBar(values [][]float64, opts ...BarSeriesOption) BarSeriesList { var opt BarSeriesOption if len(opts) != 0 { opt = opts[0] } - return newSeriesListFromValues(values, ChartTypeBar, - opt.Label, opt.Names, "", opt.MarkPoint, opt.MarkLine) + + seriesList := make([]BarSeries, len(values)) + for index, v := range values { + s := BarSeries{ + Values: v, + Label: opt.Label, + MarkPoint: opt.MarkPoint, + MarkLine: opt.MarkLine, + } + if index < len(opt.Names) { + s.Name = opt.Names[index] + } + seriesList[index] = s + } + return seriesList } // NewSeriesListHorizontalBar builds a SeriesList for a horizontal bar chart. Horizontal bar charts are unique in that // these Series can not be combined with any other chart type. -func NewSeriesListHorizontalBar(values [][]float64, opts ...BarSeriesOption) SeriesList { +func NewSeriesListHorizontalBar(values [][]float64, opts ...BarSeriesOption) HorizontalBarSeriesList { var opt BarSeriesOption if len(opts) != 0 { opt = opts[0] } - return newSeriesListFromValues(values, ChartTypeHorizontalBar, - opt.Label, opt.Names, "", opt.MarkPoint, opt.MarkLine) + + seriesList := make([]HorizontalBarSeries, len(values)) + for index, v := range values { + s := HorizontalBarSeries{ + Values: v, + Label: opt.Label, + } + if index < len(opt.Names) { + s.Name = opt.Names[index] + } + seriesList[index] = s + } + return seriesList } // PieSeriesOption provides series customization for NewSeriesListPie. @@ -231,23 +987,21 @@ type PieSeriesOption struct { } // NewSeriesListPie builds a SeriesList for a pie chart. -func NewSeriesListPie(values []float64, opts ...PieSeriesOption) SeriesList { - result := make([]Series, len(values)) +func NewSeriesListPie(values []float64, opts ...PieSeriesOption) PieSeriesList { var opt PieSeriesOption if len(opts) != 0 { opt = opts[0] } + + result := make([]PieSeries, len(values)) for index, v := range values { - var name string - if index < len(opt.Names) { - name = opt.Names[index] - } - s := Series{ - Type: ChartTypePie, - Data: []float64{v}, - Radius: opt.Radius, + s := PieSeries{ + Value: v, Label: opt.Label, - Name: name, + Radius: opt.Radius, + } + if index < len(opt.Names) { + s.Name = opt.Names[index] } result[index] = s } @@ -261,13 +1015,24 @@ type RadarSeriesOption struct { } // NewSeriesListRadar builds a SeriesList for a Radar chart. -func NewSeriesListRadar(values [][]float64, opts ...RadarSeriesOption) SeriesList { +func NewSeriesListRadar(values [][]float64, opts ...RadarSeriesOption) RadarSeriesList { var opt RadarSeriesOption if len(opts) != 0 { opt = opts[0] } - return newSeriesListFromValues(values, ChartTypeRadar, - opt.Label, opt.Names, "", SeriesMarkPoint{}, SeriesMarkLine{}) + + result := make([]RadarSeries, len(values)) + for index, v := range values { + s := RadarSeries{ + Values: v, + Label: opt.Label, + } + if index < len(opt.Names) { + s.Name = opt.Names[index] + } + result[index] = s + } + return result } // FunnelSeriesOption provides series customization for NewSeriesListFunnel. @@ -277,23 +1042,22 @@ type FunnelSeriesOption struct { } // NewSeriesListFunnel builds a series list for funnel charts. -func NewSeriesListFunnel(values []float64, opts ...FunnelSeriesOption) SeriesList { +func NewSeriesListFunnel(values []float64, opts ...FunnelSeriesOption) FunnelSeriesList { var opt FunnelSeriesOption if len(opts) != 0 { opt = opts[0] } - seriesList := make(SeriesList, len(values)) + + seriesList := make([]FunnelSeries, len(values)) for index, value := range values { - var name string - if index < len(opt.Names) { - name = opt.Names[index] - } - seriesList[index] = Series{ - Data: []float64{value}, - Type: ChartTypeFunnel, + s := FunnelSeries{ + Value: value, Label: opt.Label, - Name: name, } + if index < len(opt.Names) { + s.Name = opt.Names[index] + } + seriesList[index] = s } return seriesList } @@ -325,12 +1089,7 @@ type populationSummary struct { Kurtosis float64 } -// Summary returns numeric summary of series values (population statistics). -func (s *Series) Summary() populationSummary { - return summarizePopulationData(s.Data) -} - -// summarizePopulationData returns numeric summary of series values (population statistics). +// summarizePopulationData returns numeric summary of the values (population statistics). func summarizePopulationData(data []float64) populationSummary { n := float64(len(data)) if n == 0 { @@ -422,61 +1181,49 @@ func summarizePopulationData(data []float64) populationSummary { } } -// Deprecated: Names is deprecated, this is expected to be used internally only, if you use this function please open -// a GitHub issue to let us know it's useful to you. -func (sl SeriesList) Names() []string { - names := make([]string, len(sl)) - for index, s := range sl { - names[index] = s.Name +// seriesNames returns the names of series list. +func seriesNames(sl seriesList) []string { + names := make([]string, sl.len()) + for index := range names { + names[index] = sl.getSeriesName(index) } return names } -// SumSeries will return a single Series which represents the sum of the entire SeriesList. This is useful for -// providing global statistics through Series.Summary(). -func (sl SeriesList) SumSeries() Series { - return sl.makeSumSeries("", -1) -} - -func (sl SeriesList) makeSumSeries(chartType string, yaxisIndex int) Series { - result := Series{ - Type: chartType, - } +func sumSeriesData(sl seriesList, yaxisIndex int) []float64 { + seriesLen := sl.len() // check for fast path result - switch len(sl) { + switch seriesLen { case 0: - return result + return make([]float64, 0) case 1: - if chartTypeMatch(chartType, sl[0].Type) && (yaxisIndex < 0 || sl[0].YAxisIndex == yaxisIndex) { - return sl[0] - } else { - return result + s := sl.getSeries(0) + if yaxisIndex < 0 || s.getYAxisIndex() == yaxisIndex { + return s.getValues() } } - sumValues := make([]float64, sl.getMaxDataCount(chartType)) - for _, s := range sl { - if chartTypeMatch(chartType, s.Type) && (yaxisIndex < 0 || s.YAxisIndex == yaxisIndex) { - result = s // ensure other series values are set into the result - for i, f := range s.Data { - if f != GetNullValue() { - sumValues[i] += f - } + sumValues := make([]float64, getSeriesMaxDataCount(sl)) + for i1 := 0; i1 < seriesLen; i1++ { + s := sl.getSeries(i1) + if yaxisIndex > -1 && s.getYAxisIndex() != yaxisIndex { + continue + } + for i2, f := range s.getValues() { + if f != GetNullValue() { + sumValues[i2] += f } } } - result.Data = sumValues - return result + return sumValues } -func (sl SeriesList) getMaxDataCount(chartType string) int { +func getSeriesMaxDataCount(sl seriesList) int { result := 0 - for _, s := range sl { - if chartTypeMatch(chartType, s.Type) { - count := len(s.Data) - if count > result { - result = count - } + for i := 0; i < sl.len(); i++ { + count := len(sl.getSeriesValues(i)) + if count > result { + result = count } } return result diff --git a/series_test.go b/series_test.go index d5fd667..aa8e359 100644 --- a/series_test.go +++ b/series_test.go @@ -1,92 +1,129 @@ package charts import ( + "strconv" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestNewSeriesListDataFromValues(t *testing.T) { - t.Parallel() - - assert.Equal(t, SeriesList{ - { - Type: ChartTypeBar, - Data: []float64{1.0}, - }, - }, NewSeriesListBar([][]float64{ - {1}, - })) -} - func TestSeriesLists(t *testing.T) { t.Parallel() - seriesList := NewSeriesListBar([][]float64{ + values := [][]float64{ {1, 2}, {10}, {1, 2, 3, 4, 5, 6, 7, 8, 9}, - }) + } + for i, tc := range []string{ChartTypeLine, ChartTypeBar, ChartTypeHorizontalBar} { + t.Run(strconv.Itoa(i)+"-"+tc, func(t *testing.T) { + var seriesList seriesList + switch tc { // switch case to ensure chart type and generic type match expectations + case ChartTypeLine: + seriesList = NewSeriesListLine(values) + assert.Len(t, filterSeriesList[LineSeriesList](seriesList, ChartTypeLine), 3) + assert.Empty(t, filterSeriesList[BarSeriesList](seriesList, ChartTypeBar)) + case ChartTypeBar: + seriesList = NewSeriesListBar(values) + assert.Len(t, filterSeriesList[BarSeriesList](seriesList, ChartTypeBar), 3) + assert.Empty(t, filterSeriesList[LineSeriesList](seriesList, ChartTypeLine)) + case ChartTypeHorizontalBar: + seriesList = NewSeriesListHorizontalBar(values) + assert.Len(t, filterSeriesList[HorizontalBarSeriesList](seriesList, ChartTypeHorizontalBar), 3) + assert.Empty(t, filterSeriesList[LineSeriesList](seriesList, ChartTypeLine)) + default: + require.Fail(t, "Need to implement chart type test") + } - assert.Len(t, seriesList.Filter(ChartTypeBar), 3) - assert.Empty(t, seriesList.Filter(ChartTypeLine)) - - min, max, maxSum := seriesList.getMinMaxSumMax(0, true) - assert.InDelta(t, float64(12), maxSum, 0) - assert.InDelta(t, float64(10), max, 0) - assert.InDelta(t, float64(1), min, 0) + min, max, maxSum := getSeriesMinMaxSumMax(seriesList, 0, true) + assert.InDelta(t, float64(12), maxSum, 0) + assert.InDelta(t, float64(10), max, 0) + assert.InDelta(t, float64(1), min, 0) + }) + } } func TestSumSeries(t *testing.T) { t.Parallel() - t.Run("empty", func(t *testing.T) { - var sl SeriesList - result := sl.SumSeries() - - assert.Equal(t, "", result.Type) - assert.Empty(t, result.Data) - }) - - t.Run("single", func(t *testing.T) { - sl := SeriesList{ - { - Type: ChartTypeLine, - Data: []float64{1.5, 2.5}, - Name: "SingleLine", - YAxisIndex: 1, - Radius: "50%", + type summableSeries interface { + SumSeries() []float64 + } + testTypes := []struct { + name string + seriesFact func([][]float64) summableSeries + }{ + { + name: "line", + seriesFact: func(values [][]float64) summableSeries { + return NewSeriesListLine(values) }, - } - - result := sl.SumSeries() - - assert.Equal(t, sl[0], result) - }) - - t.Run("multiple", func(t *testing.T) { - sl := NewSeriesListLine([][]float64{ - {1, 2, 3}, - {4, 5, 6}, - }) - - result := sl.SumSeries() - - assert.Equal(t, ChartTypeLine, result.Type) - assert.Equal(t, []float64{5, 7, 9}, result.Data) - }) - - t.Run("unequal_data_length", func(t *testing.T) { - sl := NewSeriesListLine([][]float64{ - {1, 2}, - {3, 4, 5}, - }) + }, + { + name: "bar", + seriesFact: func(values [][]float64) summableSeries { + return NewSeriesListBar(values) + }, + }, + { + name: "horizontal_bar", + seriesFact: func(values [][]float64) summableSeries { + return NewSeriesListHorizontalBar(values) + }, + }, + } + tests := []struct { + name string + values [][]float64 + expected []float64 + }{ + { + name: "empty", + values: [][]float64{}, + expected: []float64{}, + }, + { + name: "single", + values: [][]float64{{1.5, 2.5}}, + expected: []float64{1.5, 2.5}, + }, + { + name: "multiple", + values: [][]float64{ + {1, 2, 3}, + {4, 5, 6}, + }, + expected: []float64{5, 7, 9}, + }, + { + name: "unequal_data_length", + values: [][]float64{ + {1, 2}, + {3, 4, 5}, + }, + expected: []float64{4, 6, 5}, + }, + { + name: "null_values", + values: [][]float64{ + {GetNullValue(), 2, 3}, + {4, GetNullValue(), 6}, + }, + expected: []float64{4, 2, 9}, + }, + } - result := sl.SumSeries() + for _, typeCase := range testTypes { + for _, tc := range tests { + t.Run(typeCase.name+"-"+tc.name, func(t *testing.T) { + series := typeCase.seriesFact(tc.values) + result := series.SumSeries() - assert.Equal(t, ChartTypeLine, result.Type) - assert.Equal(t, []float64{4, 6, 5}, result.Data) - }) + assert.Equal(t, tc.expected, result) + }) + } + } } func TestSeriesSummary(t *testing.T) { @@ -104,7 +141,7 @@ func TestSeriesSummary(t *testing.T) { assert.Equal(t, populationSummary{ MaxIndex: -1, MinIndex: -1, - }, (&Series{}).Summary()) + }, summarizePopulationData(nil)) }) t.Run("one_value", func(t *testing.T) { assert.Equal(t, populationSummary{ @@ -182,3 +219,81 @@ func TestFormatter(t *testing.T) { assert.Equal(t, "10", labelFormatValue([]string{"a", "b"}, "", 0, 10, 0.12)) } + +func BenchmarkGetSeriesYAxisCount(b *testing.B) { // benchmark used to evaluate methods for iterating the series + nameCount := 100 + seriesList := make(LineSeriesList, nameCount) + for i := 0; i < nameCount; i++ { + seriesList[i] = LineSeries{} + } + + for i := 0; i < b.N; i++ { + _ = getSeriesYAxisCount(seriesList) + } +} + +func BenchmarkGetSeriesMinMaxSumMax(b *testing.B) { // benchmark used to evaluate methods for iterating the series + seriesCount := 100 + seriesSize := 100 + seriesList := make(LineSeriesList, seriesCount) + for i := 0; i < seriesCount; i++ { + data := make([]float64, seriesSize) + for si := 0; si < seriesSize; si++ { + if si+1%10 == 0 { + data[si] = GetNullValue() + } else { + data[si] = float64(si) + } + } + seriesList[i] = LineSeries{ + Values: data, + } + } + + for i := 0; i < b.N; i++ { + _, _, _ = getSeriesMinMaxSumMax(seriesList, 0, true) + } +} + +func BenchmarkSumSeries(b *testing.B) { // benchmark used to evaluate methods for iterating the series + seriesCount := 100 + seriesSize := 100 + seriesList := make(LineSeriesList, seriesCount) + for i := 0; i < seriesCount; i++ { + seriesList[i] = LineSeries{ + Values: make([]float64, seriesSize), + } + } + + for i := 0; i < b.N; i++ { + _ = seriesList.SumSeries() + } +} + +func BenchmarkSeriesNames(b *testing.B) { // benchmark used to evaluate methods for iterating the series + nameCount := 100 + seriesList := make(LineSeriesList, nameCount) + for i := 0; i < nameCount; i++ { + seriesList[i] = LineSeries{ + Name: strconv.Itoa(i), + } + } + + for i := 0; i < b.N; i++ { + _ = seriesList.names() + } +} + +func BenchmarkGetSeriesMaxDataCount(b *testing.B) { // benchmark used to evaluate methods for iterating the series + seriesCount := 100 + seriesList := make(LineSeriesList, seriesCount) + for i := 0; i < seriesCount; i++ { + seriesList[i] = LineSeries{ + Values: make([]float64, i), + } + } + + for i := 0; i < b.N; i++ { + _ = getSeriesMaxDataCount(seriesList) + } +}