feat: support table render

This commit is contained in:
vicanso 2022-06-23 23:29:13 +08:00
parent 8c5647f65f
commit 2fb0ebcbf7
6 changed files with 476 additions and 23 deletions

View file

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

79
examples/table/main.go Normal file
View file

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

View file

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

View file

@ -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,

201
table.go
View file

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

100
table_test.go Normal file
View file

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