From 2fb0ebcbf7a5f8efd8c4f14c1b3056e2e29af688 Mon Sep 17 00:00:00 2001 From: vicanso Date: Thu, 23 Jun 2022 23:29:13 +0800 Subject: [PATCH] feat: support table render --- chart_option.go | 69 ++++++++++++++ examples/table/main.go | 79 ++++++++++++++++ painter.go | 22 +++-- painter_test.go | 28 +++--- table.go | 201 ++++++++++++++++++++++++++++++++++++++++- table_test.go | 100 ++++++++++++++++++++ 6 files changed, 476 insertions(+), 23 deletions(-) create mode 100644 examples/table/main.go create mode 100644 table_test.go diff --git a/chart_option.go b/chart_option.go index f2f3b51..b7b4714 100644 --- a/chart_option.go +++ b/chart_option.go @@ -23,6 +23,7 @@ package charts import ( + "fmt" "sort" "github.com/golang/freetype/truetype" @@ -336,3 +337,71 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { SeriesList: seriesList, }, opts...) } + +func TableRender(opt TableChartOption) (*Painter, error) { + if opt.Type == "" { + opt.Type = ChartOutputPNG + } + if opt.Width <= 0 { + opt.Width = defaultChartWidth + } + if opt.Height <= 0 { + opt.Height = defaultChartHeight + } + if opt.Font == nil { + opt.Font, _ = chart.GetDefaultFont() + } + + p, err := NewPainter(PainterOptions{ + Type: opt.Type, + Width: opt.Width, + Height: opt.Height, + Font: opt.Font, + }) + if err != nil { + return nil, err + } + info, err := NewTableChart(p, opt).render() + if err != nil { + return nil, err + } + fmt.Println(*info) + fmt.Println(info.Height) + + p, err = NewPainter(PainterOptions{ + Type: opt.Type, + Width: info.Width, + Height: info.Height, + Font: opt.Font, + }) + if err != nil { + return nil, err + } + _, err = NewTableChart(p, opt).Render() + if err != nil { + return nil, err + } + + // opt := ChartOption{} + // for _, fn := range opts { + // fn(&opt) + // } + // opt.fillDefault() + // p, err := NewPainter(PainterOptions{ + // Type: opt.Type, + // Width: opt.Width, + // Height: opt.Height, + // Font: opt.font, + // }) + // if err != nil { + // return nil, err + // } + // _, err = NewTableChart(p, TableChartOption{ + // Header: header, + // Data: data, + // }).Render() + // if err != nil { + // return nil, err + // } + return p, nil +} diff --git a/examples/table/main.go b/examples/table/main.go new file mode 100644 index 0000000..650566c --- /dev/null +++ b/examples/table/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/vicanso/go-charts/v2" +) + +func writeFile(buf []byte) error { + tmpPath := "./tmp" + err := os.MkdirAll(tmpPath, 0700) + if err != nil { + return err + } + + file := filepath.Join(tmpPath, "table.png") + err = ioutil.WriteFile(file, buf, 0600) + if err != nil { + return err + } + return nil +} + +func main() { + p, err := charts.TableRender(charts.TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Spans: []int{ + 1, + 1, + 2, + 1, + 1, + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + err = writeFile(buf) + if err != nil { + panic(err) + } +} diff --git a/painter.go b/painter.go index 1f9d418..c787315 100644 --- a/painter.go +++ b/painter.go @@ -468,7 +468,7 @@ func (p *Painter) SmoothLineStroke(points []Point) *Painter { return p } -func (p *Painter) SetBackground(width, height int, color Color) *Painter { +func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter { r := p.render s := chart.Style{ FillColor: color, @@ -476,12 +476,20 @@ func (p *Painter) SetBackground(width, height int, color Color) *Painter { // 背景色 p.SetDrawingStyle(s) defer p.ResetStyle() - // 设置背景色不使用box,因此不直接使用Painter - r.MoveTo(0, 0) - r.LineTo(width, 0) - r.LineTo(width, height) - r.LineTo(0, height) - r.LineTo(0, 0) + if len(inside) != 0 && inside[0] { + p.MoveTo(0, 0) + p.LineTo(width, 0) + p.LineTo(width, height) + p.LineTo(0, height) + p.LineTo(0, 0) + } else { + // 设置背景色不使用box,因此不直接使用Painter + r.MoveTo(0, 0) + r.LineTo(width, 0) + r.LineTo(width, height) + r.LineTo(0, height) + r.LineTo(0, 0) + } p.FillStroke() return p } diff --git a/painter_test.go b/painter_test.go index 8892563..96e41ef 100644 --- a/painter_test.go +++ b/painter_test.go @@ -143,13 +143,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -165,13 +165,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -187,13 +187,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -209,13 +209,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -231,13 +231,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -253,13 +253,13 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, A: 255, }, - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, @@ -279,7 +279,7 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.SetStyle(Style{ StrokeWidth: 1, - StrokeColor: drawing.Color{ + StrokeColor: Color{ R: 84, G: 112, B: 198, @@ -297,7 +297,7 @@ func TestPainter(t *testing.T) { { fn: func(p *Painter) { p.SetDrawingStyle(Style{ - FillColor: drawing.Color{ + FillColor: Color{ R: 84, G: 112, B: 198, diff --git a/table.go b/table.go index e47914c..34dee67 100644 --- a/table.go +++ b/table.go @@ -22,6 +22,14 @@ package charts +import ( + "errors" + + "github.com/golang/freetype/truetype" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" +) + type tableChart struct { p *Painter opt *TableChartOption @@ -38,16 +46,205 @@ func NewTableChart(p *Painter, opt TableChartOption) *tableChart { } type TableChartOption struct { + // The output type + Type string + // The width of table + Width int + // The height of table + Height int // The theme Theme ColorPalette // The padding of table cell Padding Box // The header data of table - HeaderData []string + Header []string // The data of table Data [][]string + // The span of table column + Spans []int + // The font size of table + FontSize float64 + Font *truetype.Font + // The font color of table + FontColor Color + // The background color of header + HeaderBackgroundColor Color + // The header font color + HeaderFontColor Color + // The background color of row + RowBackgroundColors []Color + // The background color + BackgroundColor Color +} + +var defaultTableHeaderColor = Color{ + R: 34, + G: 34, + B: 34, + A: 255, +} +var defaultTableRowColors = []Color{ + drawing.ColorWhite, + { + R: 242, + G: 242, + B: 242, + A: 255, + }, +} +var defaultTablePadding = Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, +} + +type renderInfo struct { + Width int + Height int + HeaderHeight int + RowHeights []int +} + +func (c *tableChart) render() (*renderInfo, error) { + info := renderInfo{ + RowHeights: make([]int, 0), + } + p := c.p + opt := c.opt + if len(opt.Header) == 0 { + return nil, errors.New("header can not be nil") + } + theme := opt.Theme + if theme == nil { + theme = p.theme + } + fontSize := opt.FontSize + if fontSize == 0 { + fontSize = 12 + } + fontColor := opt.FontColor + if fontColor.IsZero() { + fontColor = theme.GetTextColor() + } + font := opt.Font + if font == nil { + font = theme.GetFont() + } + headerFontColor := opt.HeaderFontColor + if opt.HeaderFontColor.IsZero() { + headerFontColor = drawing.ColorWhite + } + + spans := opt.Spans + if len(spans) != 0 && len(spans) != len(opt.Header) { + newSpans := make([]int, len(opt.Header)) + for index := range opt.Header { + if len(spans) < index { + newSpans[index] = 1 + } else { + newSpans[index] = spans[index] + } + } + spans = newSpans + } + + values := autoDivideSpans(p.Width(), len(opt.Header)+1, spans) + height := 0 + textStyle := Style{ + FontSize: fontSize, + FontColor: headerFontColor, + FillColor: headerFontColor, + Font: font, + } + p.SetStyle(textStyle) + + headerHeight := 0 + padding := opt.Padding + if padding.IsZero() { + padding = defaultTablePadding + } + + renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { + cellMaxHeight := 0 + paddingHeight := cellPadding.Top + cellPadding.Bottom + paddingWidth := cellPadding.Left + cellPadding.Right + for index, text := range textList { + x := values[index] + y := currentHeight + cellPadding.Top + width := values[index+1] - x + x += cellPadding.Left + width -= paddingWidth + box := p.TextFit(text, x, y+int(fontSize), width) + if box.Height()+paddingHeight > cellMaxHeight { + cellMaxHeight = box.Height() + paddingHeight + } + } + return cellMaxHeight + } + + headerHeight = renderTableCells(opt.Header, height, padding) + height += headerHeight + info.HeaderHeight = headerHeight + + textStyle.FontColor = fontColor + textStyle.FillColor = fontColor + p.SetStyle(textStyle) + for _, textList := range opt.Data { + cellHeight := renderTableCells(textList, height, padding) + info.RowHeights = append(info.RowHeights, cellHeight) + height += cellHeight + } + + info.Width = p.Width() + info.Height = height + return &info, nil } func (c *tableChart) Render() (Box, error) { - return BoxZero, nil + p := c.p + opt := c.opt + if !opt.BackgroundColor.IsZero() { + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + } + headerBGColor := opt.HeaderBackgroundColor + if headerBGColor.IsZero() { + headerBGColor = defaultTableHeaderColor + } + + r := p.render + newRender, err := chart.SVG(p.Width(), 100) + if err != nil { + return BoxZero, err + } + p.render = newRender + info, err := c.render() + if err != nil { + return BoxZero, err + } + p.render = r + // 如果设置表头背景色 + p.SetBackground(info.Width, info.HeaderHeight, headerBGColor, true) + currentHeight := info.HeaderHeight + rowColors := opt.RowBackgroundColors + if len(rowColors) == 0 { + rowColors = defaultTableRowColors + } + for index, h := range info.RowHeights { + color := rowColors[index%len(rowColors)] + child := p.Child(PainterPaddingOption(Box{ + Top: currentHeight, + })) + child.SetBackground(p.Width(), h, color, true) + currentHeight += h + } + _, err = c.render() + if err != nil { + return BoxZero, err + } + + return Box{ + Right: info.Width, + Bottom: info.Height, + }, nil } diff --git a/table_test.go b/table_test.go new file mode 100644 index 0000000..c54de25 --- /dev/null +++ b/table_test.go @@ -0,0 +1,100 @@ +// 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 ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTableChart(t *testing.T) { + assert := assert.New(t) + + tests := []struct { + render func(*Painter) ([]byte, error) + result string + }{ + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTableChart(p, TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + Spans: []int{ + 1, + 1, + 2, + 1, + 1, + }, + Data: [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", + }, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", + }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", + }, + }, + }).Render() + if err != nil { + return nil, err + } + return p.Bytes() + }, + }, + } + 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) + fmt.Println(string(data)) + assert.Equal(tt.result, string(data)) + } + +}