diff --git a/grid.go b/grid.go index fb5dad6..0ebd226 100644 --- a/grid.go +++ b/grid.go @@ -32,6 +32,8 @@ type GridPainterOption struct { StrokeWidth float64 // The stroke color StrokeColor Color + // The spans of column + ColumnSpans []int // The column of grid Column int // The row of grid @@ -81,6 +83,7 @@ func (g *gridPainter) Render() (Box, error) { }) g.p.Grid(GridOption{ Column: opt.Column, + ColumnSpans: opt.ColumnSpans, Row: opt.Row, IgnoreColumnLines: ignoreColumnLines, IgnoreRowLines: ignoreRowLines, diff --git a/grid_test.go b/grid_test.go index f6880dc..3110a2b 100644 --- a/grid_test.go +++ b/grid_test.go @@ -54,6 +54,24 @@ func TestGrid(t *testing.T) { }, result: "\\n", }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewGridPainter(p, GridPainterOption{ + StrokeColor: drawing.ColorBlack, + ColumnSpans: []int{ + 2, + 5, + 3, + }, + Row: 6, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\n", + }, } for _, tt := range tests { p, err := NewPainter(PainterOptions{ diff --git a/painter.go b/painter.go index c250369..1f9d418 100644 --- a/painter.go +++ b/painter.go @@ -69,8 +69,9 @@ type MultiTextOption struct { } type GridOption struct { - Column int - Row int + Column int + Row int + ColumnSpans []int // 忽略不展示的column IgnoreColumnLines []int // 忽略不展示的row @@ -542,6 +543,9 @@ func (p *Painter) TextFit(body string, x, y, width int) chart.Box { var output chart.Box for index, line := range lines { + if line == "" { + continue + } x0 := x y0 := y + output.Height() p.Text(line, x0, y0) @@ -690,8 +694,12 @@ func (p *Painter) Grid(opt GridOption) *Painter { }) } } - if opt.Column > 0 { - values := autoDivide(width, opt.Column) + columnCount := sumInt(opt.ColumnSpans) + if columnCount == 0 { + columnCount = opt.Column + } + if columnCount > 0 { + values := autoDivideSpans(width, columnCount, opt.ColumnSpans) drawLines(values, opt.IgnoreColumnLines, true) } if opt.Row > 0 { diff --git a/series_test.go b/series_test.go new file mode 100644 index 0000000..40d2f91 --- /dev/null +++ b/series_test.go @@ -0,0 +1,89 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +package charts + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSeriesListDataFromValues(t *testing.T) { + assert := assert.New(t) + + assert.Equal(SeriesList{ + { + Type: ChartTypeBar, + Data: []SeriesData{ + { + Value: 1.0, + }, + }, + }, + }, NewSeriesListDataFromValues([][]float64{ + { + 1, + }, + }, ChartTypeBar)) +} + +func TestSeriesLists(t *testing.T) { + assert := assert.New(t) + seriesList := NewSeriesListDataFromValues([][]float64{ + { + 1, + 2, + }, + { + 10, + }, + }, ChartTypeBar) + + assert.Equal(2, len(seriesList.Filter(ChartTypeBar))) + assert.Equal(0, len(seriesList.Filter(ChartTypeLine))) + + max, min := seriesList.GetMaxMin(0) + assert.Equal(float64(10), max) + assert.Equal(float64(1), min) + + assert.Equal(seriesSummary{ + MaxIndex: 1, + MaxValue: 2, + MinIndex: 0, + MinValue: 1, + AverageValue: 1.5, + }, seriesList[0].Summary()) +} + +func TestFormatter(t *testing.T) { + assert := assert.New(t) + + assert.Equal("a: 12%", NewPieLabelFormatter([]string{ + "a", + "b", + }, "")(0, 10, 0.12)) + + assert.Equal("10", NewValueLabelFormatter([]string{ + "a", + "b", + }, "")(0, 10, 0.12)) +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..e47914c --- /dev/null +++ b/table.go @@ -0,0 +1,53 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +type tableChart struct { + p *Painter + opt *TableChartOption +} + +func NewTableChart(p *Painter, opt TableChartOption) *tableChart { + if opt.Theme == nil { + opt.Theme = defaultTheme + } + return &tableChart{ + p: p, + opt: &opt, + } +} + +type TableChartOption struct { + // The theme + Theme ColorPalette + // The padding of table cell + Padding Box + // The header data of table + HeaderData []string + // The data of table + Data [][]string +} + +func (c *tableChart) Render() (Box, error) { + return BoxZero, nil +} diff --git a/title.go b/title.go index 5af4c39..5cdd161 100644 --- a/title.go +++ b/title.go @@ -97,6 +97,9 @@ func (t *titlePainter) Render() (Box, error) { p := t.p theme := opt.Theme + if theme == nil { + theme = p.theme + } if opt.Text == "" && opt.Subtext == "" { return BoxZero, nil } diff --git a/title_test.go b/title_test.go new file mode 100644 index 0000000..add8163 --- /dev/null +++ b/title_test.go @@ -0,0 +1,93 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTitleRenderer(t *testing.T) { + assert := assert.New(t) + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: "20", + Top: "20", + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: "20%", + Top: "20", + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTitlePainter(p, TitleOption{ + Text: "title", + Subtext: "subTitle", + Left: PositionRight, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\ntitlesubTitle", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme)) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +} diff --git a/util.go b/util.go index adfa9fd..a33c6d2 100644 --- a/util.go +++ b/util.go @@ -90,6 +90,30 @@ func autoDivide(max, size int) []int { return values } +func autoDivideSpans(max, size int, spans []int) []int { + values := autoDivide(max, size) + // 重新合并 + if len(spans) != 0 { + newValues := make([]int, len(spans)+1) + newValues[0] = 0 + end := 0 + for index, v := range spans { + end += v + newValues[index+1] = values[end] + } + values = newValues + } + return values +} + +func sumInt(values []int) int { + sum := 0 + for _, v := range values { + sum += v + } + return sum +} + // measureTextMaxWidthHeight returns maxWidth and maxHeight of text list func measureTextMaxWidthHeight(textList []string, p *Painter) (int, int) { maxWidth := 0 diff --git a/yaxis.go b/yaxis.go index b0bfa86..eb9034c 100644 --- a/yaxis.go +++ b/yaxis.go @@ -65,14 +65,18 @@ func NewYAxisOptions(data []string, others ...[]string) []YAxisOption { return opts } -func (opt *YAxisOption) ToAxisOption() AxisOption { +func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption { position := PositionLeft if opt.Position == PositionRight { position = PositionRight } + theme := opt.Theme + if theme == nil { + theme = p.theme + } axisOpt := AxisOption{ Formatter: opt.Formatter, - Theme: opt.Theme, + Theme: theme, Data: opt.Data, Position: position, FontSize: opt.FontSize, @@ -81,7 +85,7 @@ func (opt *YAxisOption) ToAxisOption() AxisOption { FontColor: opt.FontColor, BoundaryGap: FalseFlag(), SplitLineShow: true, - SplitLineColor: opt.Theme.GetAxisSplitLineColor(), + SplitLineColor: theme.GetAxisSplitLineColor(), Show: opt.Show, } if !opt.Color.IsZero() { @@ -101,7 +105,7 @@ func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - return NewAxisPainter(p, opt.ToAxisOption()) + return NewAxisPainter(p, opt.ToAxisOption(p)) } // NewRightYAxis returns a right y axis renderer @@ -109,7 +113,7 @@ func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter { p = p.Child(PainterPaddingOption(Box{ Bottom: defaultXAxisHeight, })) - axisOpt := opt.ToAxisOption() + axisOpt := opt.ToAxisOption(p) axisOpt.Position = PositionRight axisOpt.SplitLineShow = false return NewAxisPainter(p, axisOpt) diff --git a/yaxis_test.go b/yaxis_test.go new file mode 100644 index 0000000..0f565ac --- /dev/null +++ b/yaxis_test.go @@ -0,0 +1,70 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRightYAxis(t *testing.T) { + assert := assert.New(t) + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + opt := NewYAxisOptions([]string{ + "a", + "b", + "c", + "d", + })[0] + _, err := NewRightYAxis(p, opt).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + result: "\\nabcd", + }, + } + for _, tt := range tests { + p, err := NewPainter(PainterOptions{ + Type: ChartOutputSVG, + Width: 600, + Height: 400, + }, PainterThemeOption(defaultTheme), PainterPaddingOption(Box{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + })) + assert.Nil(err) + data, err := tt.render(p) + assert.Nil(err) + assert.Equal(tt.result, string(data)) + } +}