feat: support table render
This commit is contained in:
parent
8c5647f65f
commit
2fb0ebcbf7
6 changed files with 476 additions and 23 deletions
|
|
@ -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
79
examples/table/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
22
painter.go
22
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
201
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
|
||||
}
|
||||
|
|
|
|||
100
table_test.go
Normal file
100
table_test.go
Normal 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))
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue