From b3a3018ea2eb6f71b126ce053f1d9410225a0689 Mon Sep 17 00:00:00 2001 From: vicanso Date: Sat, 25 Jun 2022 08:21:27 +0800 Subject: [PATCH] feat: support table redner --- README.md | 68 +++++++++++++++ README_zh.md | 64 ++++++++++++++ chart_option.go | 61 ++++++------- examples/charts/main.go | 42 +++++++++ examples/table/main.go | 76 ++++++++-------- painter.go | 4 + table.go | 188 ++++++++++++++++++++++++++++++---------- table_test.go | 46 +++++++++- 8 files changed, 433 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index d7d94f7..d0549f0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ Screenshot of common charts, the left part is light theme, the right part is gra go-charts

+

+ go-table +

## Chart Type These chart types are supported: `line`, `bar`, `pie`, `radar` or `funnel`. @@ -374,6 +377,71 @@ func main() { } ``` +### Table + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + 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", + }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` + ### ECharts Render ```go diff --git a/README_zh.md b/README_zh.md index dbdaaf3..487a365 100644 --- a/README_zh.md +++ b/README_zh.md @@ -373,6 +373,70 @@ func main() { } ``` +### Table + +```go +package main + +import ( + "github.com/vicanso/go-charts/v2" +) + +func main() { + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + 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", + }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, + ) + if err != nil { + panic(err) + } + + buf, err := p.Bytes() + if err != nil { + panic(err) + } + // snip... +} +``` ### ECharts Render ```go diff --git a/chart_option.go b/chart_option.go index b7b4714..39de686 100644 --- a/chart_option.go +++ b/chart_option.go @@ -23,7 +23,6 @@ package charts import ( - "fmt" "sort" "github.com/golang/freetype/truetype" @@ -338,24 +337,44 @@ func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) { }, opts...) } -func TableRender(opt TableChartOption) (*Painter, error) { +// TableRender table chart render +func TableRender(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) { + opt := TableChartOption{ + Header: header, + Data: data, + } + if len(spanMaps) != 0 { + spanMap := spanMaps[0] + spans := make([]int, len(opt.Header)) + for index := range spans { + v, ok := spanMap[index] + if !ok { + v = 1 + } + spans[index] = v + } + opt.Spans = spans + } + return TableOptionRender(opt) +} + +// TableOptionRender table render with option +func TableOptionRender(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, + Type: opt.Type, + Width: opt.Width, + // 仅用于计算表格高度,因此随便设置即可 + Height: 100, Font: opt.Font, }) if err != nil { @@ -365,8 +384,6 @@ func TableRender(opt TableChartOption) (*Painter, error) { if err != nil { return nil, err } - fmt.Println(*info) - fmt.Println(info.Height) p, err = NewPainter(PainterOptions{ Type: opt.Type, @@ -377,31 +394,9 @@ func TableRender(opt TableChartOption) (*Painter, error) { if err != nil { return nil, err } - _, err = NewTableChart(p, opt).Render() + _, err = NewTableChart(p, opt).renderWithInfo(info) 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/charts/main.go b/examples/charts/main.go index 0e1d48e..7b14919 100644 --- a/examples/charts/main.go +++ b/examples/charts/main.go @@ -92,6 +92,48 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha bytesList = append(bytesList, buf) } + p, err := charts.TableOptionRender(charts.TableChartOption{ + Type: charts.ChartOutputSVG, + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + 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) + } + bytesList = append(bytesList, buf) + data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) w.Header().Set("Content-Type", "text/html") w.Write(data) diff --git a/examples/table/main.go b/examples/table/main.go index 650566c..ee8147e 100644 --- a/examples/table/main.go +++ b/examples/table/main.go @@ -24,45 +24,49 @@ func writeFile(buf []byte) error { } func main() { - p, err := charts.TableRender(charts.TableChartOption{ - Header: []string{ - "Name", - "Age", - "Address", - "Tag", - "Action", + charts.SetDefaultWidth(810) + header := []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + } + data := [][]string{ + { + "John Brown", + "32", + "New York No. 1 Lake Park", + "nice, developer", + "Send Mail", }, - Spans: []int{ - 1, - 1, - 2, - 1, - 1, + { + "Jim Green ", + "42", + "London No. 1 Lake Park", + "wow", + "Send Mail", }, - 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", - }, + { + "Joe Black ", + "32", + "Sidney No. 1 Lake Park", + "cool, teacher", + "Send Mail", }, - }, + } + spans := map[int]int{ + 0: 2, + 1: 1, + // 设置第三列的span + 2: 3, + 3: 2, + 4: 2, + } + p, err := charts.TableRender( + header, + data, + spans, ) if err != nil { panic(err) diff --git a/painter.go b/painter.go index c787315..06973b6 100644 --- a/painter.go +++ b/painter.go @@ -38,6 +38,8 @@ type Painter struct { parent *Painter style Style theme ColorPalette + // 类型 + outputType string } type PainterOptions struct { @@ -169,6 +171,8 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { Bottom: opts.Height, }, font: font, + // 类型 + outputType: opts.Type, } p.setOptions(opt...) if p.theme == nil { diff --git a/table.go b/table.go index 34dee67..ffa7013 100644 --- a/table.go +++ b/table.go @@ -35,6 +35,7 @@ type tableChart struct { opt *TableChartOption } +// NewTableChart returns a table chart render func NewTableChart(p *Painter, opt TableChartOption) *tableChart { if opt.Theme == nil { opt.Theme = defaultTheme @@ -50,8 +51,6 @@ type TableChartOption struct { Type string // The width of table Width int - // The height of table - Height int // The theme Theme ColorPalette // The padding of table cell @@ -64,7 +63,9 @@ type TableChartOption struct { Spans []int // The font size of table FontSize float64 - Font *truetype.Font + // The font family, which should be installed first + FontFamily string + Font *truetype.Font // The font color of table FontColor Color // The background color of header @@ -77,26 +78,101 @@ type TableChartOption struct { BackgroundColor Color } -var defaultTableHeaderColor = Color{ - R: 34, - G: 34, - B: 34, - A: 255, +type TableSetting struct { + // The color of header + HeaderColor Color + // The color of heder text + HeaderFontColor Color + // The color of table text + FontColor Color + // The color list of row + RowColors []Color + // The padding of cell + Padding Box } -var defaultTableRowColors = []Color{ - drawing.ColorWhite, - { - R: 242, - G: 242, - B: 242, + +var TableLightThemeSetting = TableSetting{ + HeaderColor: Color{ + R: 240, + G: 240, + B: 240, A: 255, }, + HeaderFontColor: Color{ + R: 98, + G: 105, + B: 118, + A: 255, + }, + FontColor: Color{ + R: 70, + G: 70, + B: 70, + A: 255, + }, + RowColors: []Color{ + drawing.ColorWhite, + { + R: 247, + G: 247, + B: 247, + A: 255, + }, + }, + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, } -var defaultTablePadding = Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, + +var TableDarkThemeSetting = TableSetting{ + HeaderColor: Color{ + R: 38, + G: 38, + B: 42, + A: 255, + }, + HeaderFontColor: Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + FontColor: Color{ + R: 216, + G: 217, + B: 218, + A: 255, + }, + RowColors: []Color{ + { + R: 24, + G: 24, + B: 28, + A: 255, + }, + { + R: 38, + G: 38, + B: 42, + A: 255, + }, + }, + Padding: Box{ + Left: 10, + Top: 10, + Right: 10, + Bottom: 10, + }, +} + +var tableDefaultSetting = TableLightThemeSetting + +// SetDefaultTableSetting sets the default setting for table +func SetDefaultTableSetting(setting TableSetting) { + tableDefaultSetting = setting } type renderInfo struct { @@ -106,12 +182,12 @@ type renderInfo struct { RowHeights []int } -func (c *tableChart) render() (*renderInfo, error) { +func (t *tableChart) render() (*renderInfo, error) { info := renderInfo{ RowHeights: make([]int, 0), } - p := c.p - opt := c.opt + p := t.p + opt := t.opt if len(opt.Header) == 0 { return nil, errors.New("header can not be nil") } @@ -125,7 +201,7 @@ func (c *tableChart) render() (*renderInfo, error) { } fontColor := opt.FontColor if fontColor.IsZero() { - fontColor = theme.GetTextColor() + fontColor = tableDefaultSetting.FontColor } font := opt.Font if font == nil { @@ -133,14 +209,14 @@ func (c *tableChart) render() (*renderInfo, error) { } headerFontColor := opt.HeaderFontColor if opt.HeaderFontColor.IsZero() { - headerFontColor = drawing.ColorWhite + headerFontColor = tableDefaultSetting.HeaderFontColor } spans := opt.Spans - if len(spans) != 0 && len(spans) != len(opt.Header) { + if len(spans) != len(opt.Header) { newSpans := make([]int, len(opt.Header)) for index := range opt.Header { - if len(spans) < index { + if index >= len(spans) { newSpans[index] = 1 } else { newSpans[index] = spans[index] @@ -149,7 +225,8 @@ func (c *tableChart) render() (*renderInfo, error) { spans = newSpans } - values := autoDivideSpans(p.Width(), len(opt.Header)+1, spans) + sum := sumInt(spans) + values := autoDivideSpans(p.Width(), sum, spans) height := 0 textStyle := Style{ FontSize: fontSize, @@ -162,7 +239,7 @@ func (c *tableChart) render() (*renderInfo, error) { headerHeight := 0 padding := opt.Padding if padding.IsZero() { - padding = defaultTablePadding + padding = tableDefaultSetting.Padding } renderTableCells := func(textList []string, currentHeight int, cellPadding Box) int { @@ -201,34 +278,23 @@ func (c *tableChart) render() (*renderInfo, error) { return &info, nil } -func (c *tableChart) Render() (Box, error) { - p := c.p - opt := c.opt +func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) { + p := t.p + opt := t.opt if !opt.BackgroundColor.IsZero() { p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) } headerBGColor := opt.HeaderBackgroundColor if headerBGColor.IsZero() { - headerBGColor = defaultTableHeaderColor + headerBGColor = tableDefaultSetting.HeaderColor } - 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 + rowColors = tableDefaultSetting.RowColors } for index, h := range info.RowHeights { color := rowColors[index%len(rowColors)] @@ -238,7 +304,7 @@ func (c *tableChart) Render() (Box, error) { child.SetBackground(p.Width(), h, color, true) currentHeight += h } - _, err = c.render() + _, err := t.render() if err != nil { return BoxZero, err } @@ -248,3 +314,35 @@ func (c *tableChart) Render() (Box, error) { Bottom: info.Height, }, nil } + +func (t *tableChart) Render() (Box, error) { + p := t.p + opt := t.opt + if !opt.BackgroundColor.IsZero() { + p.SetBackground(p.Width(), p.Height(), opt.BackgroundColor) + } + headerBGColor := opt.HeaderBackgroundColor + if headerBGColor.IsZero() { + headerBGColor = tableDefaultSetting.HeaderColor + } + if opt.Font == nil && opt.FontFamily != "" { + opt.Font, _ = GetFont(opt.FontFamily) + } + + r := p.render + fn := chart.PNG + if p.outputType == ChartOutputSVG { + fn = chart.SVG + } + newRender, err := fn(p.Width(), 100) + if err != nil { + return BoxZero, err + } + p.render = newRender + info, err := t.render() + if err != nil { + return BoxZero, err + } + p.render = r + return t.renderWithInfo(info) +} diff --git a/table_test.go b/table_test.go index c54de25..41a857c 100644 --- a/table_test.go +++ b/table_test.go @@ -51,7 +51,8 @@ func TestTableChart(t *testing.T) { 1, 2, 1, - 1, + // span和header不匹配,最后自动设置为1 + // 1, }, Data: [][]string{ { @@ -82,6 +83,48 @@ func TestTableChart(t *testing.T) { } return p.Bytes() }, + result: "\\nNameAgeAddressTagActionJohnBrown32New York No. 1 Lake Parknice,developerSend MailJim Green42London No. 1 Lake ParkwowSend MailJoe Black32Sidney No. 1 Lake Parkcool,teacherSend Mail", + }, + { + render: func(p *Painter) ([]byte, error) { + _, err := NewTableChart(p, TableChartOption{ + Header: []string{ + "Name", + "Age", + "Address", + "Tag", + "Action", + }, + 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() + }, + result: "\\nNameAgeAddressTagActionJohn Brown32New York No.1 Lake Parknice,developerSend MailJim Green42London No. 1Lake ParkwowSend MailJoe Black32Sidney No. 1Lake Parkcool, teacherSend Mail", }, } for _, tt := range tests { @@ -96,5 +139,4 @@ func TestTableChart(t *testing.T) { fmt.Println(string(data)) assert.Equal(tt.result, string(data)) } - }