feat: support make point and make line
This commit is contained in:
parent
fd05250305
commit
e558634dda
16 changed files with 308 additions and 51 deletions
25
axis.go
25
axis.go
|
|
@ -68,6 +68,10 @@ type axis struct {
|
||||||
data *AxisDataList
|
data *AxisDataList
|
||||||
style *AxisOption
|
style *AxisOption
|
||||||
}
|
}
|
||||||
|
type axisMeasurement struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
func NewAxis(d *Draw, data AxisDataList, style AxisOption) *axis {
|
func NewAxis(d *Draw, data AxisDataList, style AxisOption) *axis {
|
||||||
return &axis{
|
return &axis{
|
||||||
|
|
@ -379,7 +383,7 @@ func (a *axis) axisTick(opt *axisRenderOption) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *axis) axisMeasureTextMaxWidthHeight() (int, int) {
|
func (a *axis) measureTextMaxWidthHeight() (int, int) {
|
||||||
d := a.d
|
d := a.d
|
||||||
r := d.Render
|
r := d.Render
|
||||||
s := a.style.Style(d.Font)
|
s := a.style.Style(d.Font)
|
||||||
|
|
@ -389,18 +393,21 @@ func (a *axis) axisMeasureTextMaxWidthHeight() (int, int) {
|
||||||
return measureTextMaxWidthHeight(data.TextList(), r)
|
return measureTextMaxWidthHeight(data.TextList(), r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// measureAxis returns the measurement of axis.
|
// measure returns the measurement of axis.
|
||||||
// If the position is left or right, it will be textMaxWidth + labelMargin + tickLength.
|
// Width will be textMaxWidth + labelMargin + tickLength for position left or right.
|
||||||
// If the position is top or bottom, it will be textMaxHeight + labelMargin + tickLength.
|
// Height will be textMaxHeight + labelMargin + tickLength for position top or bottom.
|
||||||
func (a *axis) measureAxis() int {
|
func (a *axis) measure() axisMeasurement {
|
||||||
style := a.style
|
style := a.style
|
||||||
value := style.GetLabelMargin() + style.GetTickLength()
|
value := style.GetLabelMargin() + style.GetTickLength()
|
||||||
textMaxWidth, textMaxHeight := a.axisMeasureTextMaxWidthHeight()
|
textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
|
||||||
|
info := axisMeasurement{}
|
||||||
if style.Position == PositionLeft ||
|
if style.Position == PositionLeft ||
|
||||||
style.Position == PositionRight {
|
style.Position == PositionRight {
|
||||||
return textMaxWidth + value
|
info.Width = textMaxWidth + value
|
||||||
|
} else {
|
||||||
|
info.Height = textMaxHeight + value
|
||||||
}
|
}
|
||||||
return textMaxHeight + value
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render renders the axis for chart
|
// Render renders the axis for chart
|
||||||
|
|
@ -409,7 +416,7 @@ func (a *axis) Render() {
|
||||||
if isFalse(style.Show) {
|
if isFalse(style.Show) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
textMaxWidth, textMaxHeight := a.axisMeasureTextMaxWidthHeight()
|
textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
|
||||||
opt := &axisRenderOption{
|
opt := &axisRenderOption{
|
||||||
textMaxWith: textMaxWidth,
|
textMaxWith: textMaxWidth,
|
||||||
textMaxHeight: textMaxHeight,
|
textMaxHeight: textMaxHeight,
|
||||||
|
|
|
||||||
|
|
@ -247,13 +247,13 @@ func TestMeasureAxis(t *testing.T) {
|
||||||
FontSize: 12,
|
FontSize: 12,
|
||||||
Font: f,
|
Font: f,
|
||||||
Position: PositionLeft,
|
Position: PositionLeft,
|
||||||
}).measureAxis()
|
}).measure().Width
|
||||||
assert.Equal(44, width)
|
assert.Equal(44, width)
|
||||||
|
|
||||||
height := NewAxis(d, data, AxisOption{
|
height := NewAxis(d, data, AxisOption{
|
||||||
FontSize: 12,
|
FontSize: 12,
|
||||||
Font: f,
|
Font: f,
|
||||||
Position: PositionTop,
|
Position: PositionTop,
|
||||||
}).measureAxis()
|
}).measure().Height
|
||||||
assert.Equal(28, height)
|
assert.Equal(28, height)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
bar_chart.go
31
bar_chart.go
|
|
@ -26,7 +26,7 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
func barChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRenderOption, error) {
|
||||||
|
|
||||||
d, err := NewDraw(DrawOption{
|
d, err := NewDraw(DrawOption{
|
||||||
Parent: result.d,
|
Parent: result.d,
|
||||||
|
|
@ -57,10 +57,29 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
|
|
||||||
r := d.Render
|
r := d.Render
|
||||||
|
|
||||||
for i, series := range opt.SeriesList {
|
markPointRenderOptions := make([]*markPointRenderOption, 0)
|
||||||
|
|
||||||
|
for i, s := range opt.SeriesList {
|
||||||
|
// 由于series是for range,为同一个数据,因此需要clone
|
||||||
|
// 后续需要使用,如mark point
|
||||||
|
series := s
|
||||||
yRange := result.getYRange(series.YAxisIndex)
|
yRange := result.getYRange(series.YAxisIndex)
|
||||||
points := make([]Point, len(series.Data))
|
points := make([]Point, len(series.Data))
|
||||||
seriesColor := theme.GetSeriesColor(i)
|
index := series.index
|
||||||
|
if index == 0 {
|
||||||
|
index = i
|
||||||
|
}
|
||||||
|
seriesColor := theme.GetSeriesColor(index)
|
||||||
|
// mark line
|
||||||
|
markLineRender(&markLineRenderOption{
|
||||||
|
Draw: d,
|
||||||
|
FillColor: seriesColor,
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
StrokeColor: seriesColor,
|
||||||
|
Font: opt.Font,
|
||||||
|
Series: &series,
|
||||||
|
Range: yRange,
|
||||||
|
})
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
x0, _ := xRange.GetRange(j)
|
x0, _ := xRange.GetRange(j)
|
||||||
x := int(x0)
|
x := int(x0)
|
||||||
|
|
@ -105,7 +124,9 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
textBox := r.MeasureText(text)
|
textBox := r.MeasureText(text)
|
||||||
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-5)
|
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-5)
|
||||||
}
|
}
|
||||||
markPointRender(d, markPointRenderOption{
|
|
||||||
|
markPointRenderOptions = append(markPointRenderOptions, &markPointRenderOption{
|
||||||
|
Draw: d,
|
||||||
FillColor: seriesColor,
|
FillColor: seriesColor,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
Points: points,
|
Points: points,
|
||||||
|
|
@ -113,5 +134,5 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.d, nil
|
return markPointRenderOptions, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
chart.go
37
chart.go
|
|
@ -80,6 +80,14 @@ func (o *ChartOption) FillDefault(theme string) {
|
||||||
if o.BackgroundColor.IsZero() {
|
if o.BackgroundColor.IsZero() {
|
||||||
o.BackgroundColor = t.GetBackgroundColor()
|
o.BackgroundColor = t.GetBackgroundColor()
|
||||||
}
|
}
|
||||||
|
if o.Padding.IsZero() {
|
||||||
|
o.Padding = chart.Box{
|
||||||
|
Top: 20,
|
||||||
|
Right: 10,
|
||||||
|
Bottom: 10,
|
||||||
|
Left: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 标题的默认值
|
// 标题的默认值
|
||||||
if o.Title.Style.FontColor.IsZero() {
|
if o.Title.Style.FontColor.IsZero() {
|
||||||
|
|
@ -110,7 +118,7 @@ func (o *ChartOption) FillDefault(theme string) {
|
||||||
o.Title.SubtextStyle.Font = o.Font
|
o.Title.SubtextStyle.Font = o.Font
|
||||||
}
|
}
|
||||||
|
|
||||||
o.Legend.Theme = theme
|
o.Legend.theme = theme
|
||||||
if o.Legend.Style.FontSize == 0 {
|
if o.Legend.Style.FontSize == 0 {
|
||||||
o.Legend.Style.FontSize = 10
|
o.Legend.Style.FontSize = 10
|
||||||
}
|
}
|
||||||
|
|
@ -238,13 +246,14 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
markPointRenderOptions := make([]*markPointRenderOption, 0)
|
||||||
fns := []func() error{
|
fns := []func() error{
|
||||||
// pie render
|
// pie render
|
||||||
func() error {
|
func() error {
|
||||||
if !isPieChart {
|
if !isPieChart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
_, err := pieChartRender(opt, result)
|
err := pieChartRender(opt, result)
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
// bar render
|
// bar render
|
||||||
|
|
@ -255,8 +264,12 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
}
|
}
|
||||||
o := opt
|
o := opt
|
||||||
o.SeriesList = barSeries
|
o.SeriesList = barSeries
|
||||||
_, err := barChartRender(o, result)
|
options, err := barChartRender(o, result)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
markPointRenderOptions = append(markPointRenderOptions, options...)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
// line render
|
// line render
|
||||||
func() error {
|
func() error {
|
||||||
|
|
@ -266,14 +279,26 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
}
|
}
|
||||||
o := opt
|
o := opt
|
||||||
o.SeriesList = lineSeries
|
o.SeriesList = lineSeries
|
||||||
_, err := lineChartRender(o, result)
|
options, err := lineChartRender(o, result)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
markPointRenderOptions = append(markPointRenderOptions, options...)
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
// legend需要在顶层,因此最后render
|
// legend需要在顶层,因此此处render
|
||||||
func() error {
|
func() error {
|
||||||
_, err := NewLegend(result.d, opt.Legend).Render()
|
_, err := NewLegend(result.d, opt.Legend).Render()
|
||||||
return err
|
return err
|
||||||
},
|
},
|
||||||
|
// mark point最后render
|
||||||
|
func() error {
|
||||||
|
// mark point render不会出错
|
||||||
|
for _, opt := range markPointRenderOptions {
|
||||||
|
markPointRender(opt)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range fns {
|
for _, fn := range fns {
|
||||||
|
|
@ -296,6 +321,7 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
||||||
|
opt.FillDefault(opt.Theme)
|
||||||
d, err := NewDraw(
|
d, err := NewDraw(
|
||||||
DrawOption{
|
DrawOption{
|
||||||
Type: opt.Type,
|
Type: opt.Type,
|
||||||
|
|
@ -309,7 +335,6 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
opt.FillDefault(opt.Theme)
|
|
||||||
if len(opt.YAxisList) > 2 {
|
if len(opt.YAxisList) > 2 {
|
||||||
return nil, errors.New("y axis should not be gt 2")
|
return nil, errors.New("y axis should not be gt 2")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
draw.go
20
draw.go
|
|
@ -161,7 +161,9 @@ func (d *Draw) pin(x, y, width int) {
|
||||||
delta := 2*math.Pi - 2*angle
|
delta := 2*math.Pi - 2*angle
|
||||||
d.arcTo(x, y, r, r, startAngle, delta)
|
d.arcTo(x, y, r, r, startAngle, delta)
|
||||||
d.lineTo(x, y)
|
d.lineTo(x, y)
|
||||||
d.Render.Fill()
|
d.Render.Close()
|
||||||
|
d.Render.FillStroke()
|
||||||
|
|
||||||
startX := x - int(r)
|
startX := x - int(r)
|
||||||
startY := y
|
startY := y
|
||||||
endX := x + int(r)
|
endX := x + int(r)
|
||||||
|
|
@ -173,7 +175,8 @@ func (d *Draw) pin(x, y, width int) {
|
||||||
cx := x
|
cx := x
|
||||||
cy := y + int(r*2.5)
|
cy := y + int(r*2.5)
|
||||||
d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
|
d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
|
||||||
d.Render.Stroke()
|
d.Render.Close()
|
||||||
|
d.Render.Fill()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Draw) arrowLeft(x, y, width, height int) {
|
func (d *Draw) arrowLeft(x, y, width, height int) {
|
||||||
|
|
@ -226,7 +229,20 @@ func (d *Draw) arrow(x, y, width, height int, direction string) {
|
||||||
d.lineTo(x0+dx, y0+halfHeight)
|
d.lineTo(x0+dx, y0+halfHeight)
|
||||||
d.lineTo(x0, y0)
|
d.lineTo(x0, y0)
|
||||||
}
|
}
|
||||||
|
d.Render.FillStroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Draw) makeLine(x, y, width int) {
|
||||||
|
arrowWidth := 16
|
||||||
|
arrowHeight := 10
|
||||||
|
endX := x + width
|
||||||
|
d.circle(3, x, y)
|
||||||
|
d.Render.Fill()
|
||||||
|
d.moveTo(x+5, y)
|
||||||
|
d.lineTo(endX-arrowWidth, y)
|
||||||
d.Render.Stroke()
|
d.Render.Stroke()
|
||||||
|
d.Render.SetStrokeDashArray([]float64{})
|
||||||
|
d.arrowRight(endX, y, arrowWidth, arrowHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Draw) circle(radius float64, x, y int) {
|
func (d *Draw) circle(radius float64, x, y int) {
|
||||||
|
|
|
||||||
28
draw_test.go
28
draw_test.go
|
|
@ -259,7 +259,7 @@ func TestDraw(t *testing.T) {
|
||||||
}.WriteToRenderer(d.Render)
|
}.WriteToRenderer(d.Render)
|
||||||
d.pin(30, 30, 30)
|
d.pin(30, 30, 30)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 32 47\nA 15 15 330.00 1 1 38 47\nL 35 33\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 20 33\nQ35,70 50,33\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 32 47\nA 15 15 330.00 1 1 38 47\nL 35 33\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 20 33\nQ35,70 50,33\nZ\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
// arrow left
|
// arrow left
|
||||||
{
|
{
|
||||||
|
|
@ -349,6 +349,32 @@ func TestDraw(t *testing.T) {
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 30 24\nL 35 40\nL 40 24\nL 35 30\nL 30 24\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 30 24\nL 35 40\nL 40 24\nL 35 30\nL 30 24\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
|
// mark line
|
||||||
|
{
|
||||||
|
fn: func(d *Draw) {
|
||||||
|
chart.Style{
|
||||||
|
StrokeWidth: 1,
|
||||||
|
StrokeColor: drawing.Color{
|
||||||
|
R: 84,
|
||||||
|
G: 112,
|
||||||
|
B: 198,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
FillColor: drawing.Color{
|
||||||
|
R: 84,
|
||||||
|
G: 112,
|
||||||
|
B: 198,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
StrokeDashArray: []float64{
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
}.WriteToRenderer(d.Render)
|
||||||
|
d.makeLine(0, 20, 300)
|
||||||
|
},
|
||||||
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"5\" cy=\"30\" r=\"3\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path stroke-dasharray=\"4.0, 2.0\" d=\"M 10 30\nL 289 30\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"M 289 25\nL 305 30\nL 289 35\nL 294 30\nL 289 25\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
d, err := NewDraw(DrawOption{
|
d, err := NewDraw(DrawOption{
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type LegendOption struct {
|
type LegendOption struct {
|
||||||
Theme string
|
theme string
|
||||||
// Legend show flag, if nil or true, the legend will be shown
|
// Legend show flag, if nil or true, the legend will be shown
|
||||||
Show *bool
|
Show *bool
|
||||||
// Legend text style
|
// Legend text style
|
||||||
|
|
@ -67,7 +67,7 @@ func (l *legend) Render() (chart.Box, error) {
|
||||||
if len(opt.Data) == 0 || isFalse(opt.Show) {
|
if len(opt.Data) == 0 || isFalse(opt.Show) {
|
||||||
return chart.BoxZero, nil
|
return chart.BoxZero, nil
|
||||||
}
|
}
|
||||||
theme := NewTheme(opt.Theme)
|
theme := NewTheme(opt.theme)
|
||||||
padding := opt.Style.Padding
|
padding := opt.Style.Padding
|
||||||
legendDraw, err := NewDraw(DrawOption{
|
legendDraw, err := NewDraw(DrawOption{
|
||||||
Parent: d,
|
Parent: d,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
func lineChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRenderOption, error) {
|
||||||
|
|
||||||
theme := NewTheme(opt.Theme)
|
theme := NewTheme(opt.Theme)
|
||||||
|
|
||||||
|
|
@ -44,9 +44,29 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
|
|
||||||
r := d.Render
|
r := d.Render
|
||||||
xRange := result.xRange
|
xRange := result.xRange
|
||||||
for i, series := range opt.SeriesList {
|
markPointRenderOptions := make([]*markPointRenderOption, 0)
|
||||||
|
for i, s := range opt.SeriesList {
|
||||||
|
// 由于series是for range,为同一个数据,因此需要clone
|
||||||
|
// 后续需要使用,如mark point
|
||||||
|
series := s
|
||||||
|
index := series.index
|
||||||
|
if index == 0 {
|
||||||
|
index = i
|
||||||
|
}
|
||||||
|
seriesColor := theme.GetSeriesColor(index)
|
||||||
|
|
||||||
yRange := result.getYRange(series.YAxisIndex)
|
yRange := result.getYRange(series.YAxisIndex)
|
||||||
points := make([]Point, len(series.Data))
|
points := make([]Point, len(series.Data))
|
||||||
|
// mark line
|
||||||
|
markLineRender(&markLineRenderOption{
|
||||||
|
Draw: d,
|
||||||
|
FillColor: seriesColor,
|
||||||
|
FontColor: theme.GetTextColor(),
|
||||||
|
StrokeColor: seriesColor,
|
||||||
|
Font: opt.Font,
|
||||||
|
Series: &series,
|
||||||
|
Range: yRange,
|
||||||
|
})
|
||||||
|
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
y := yRange.getRestHeight(item.Value)
|
y := yRange.getRestHeight(item.Value)
|
||||||
|
|
@ -71,11 +91,7 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
textBox := r.MeasureText(text)
|
textBox := r.MeasureText(text)
|
||||||
d.text(text, x-textBox.Width()>>1, y-5)
|
d.text(text, x-textBox.Width()>>1, y-5)
|
||||||
}
|
}
|
||||||
index := series.index
|
|
||||||
if index == 0 {
|
|
||||||
index = i
|
|
||||||
}
|
|
||||||
seriesColor := theme.GetSeriesColor(index)
|
|
||||||
dotFillColor := drawing.ColorWhite
|
dotFillColor := drawing.ColorWhite
|
||||||
if theme.IsDark() {
|
if theme.IsDark() {
|
||||||
dotFillColor = seriesColor
|
dotFillColor = seriesColor
|
||||||
|
|
@ -88,7 +104,8 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
DotFillColor: dotFillColor,
|
DotFillColor: dotFillColor,
|
||||||
})
|
})
|
||||||
// draw mark point
|
// draw mark point
|
||||||
markPointRender(d, markPointRenderOption{
|
markPointRenderOptions = append(markPointRenderOptions, &markPointRenderOption{
|
||||||
|
Draw: d,
|
||||||
FillColor: seriesColor,
|
FillColor: seriesColor,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
Points: points,
|
Points: points,
|
||||||
|
|
@ -96,5 +113,5 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.d, nil
|
return markPointRenderOptions, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
92
mark_line.go
Normal file
92
mark_line.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// 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 (
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMarkLine(markLineTypes ...string) SeriesMarkLine {
|
||||||
|
data := make([]SeriesMarkLineData, len(markLineTypes))
|
||||||
|
for index, t := range markLineTypes {
|
||||||
|
data[index] = SeriesMarkLineData{
|
||||||
|
Type: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SeriesMarkLine{
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type markLineRenderOption struct {
|
||||||
|
Draw *Draw
|
||||||
|
FillColor drawing.Color
|
||||||
|
FontColor drawing.Color
|
||||||
|
StrokeColor drawing.Color
|
||||||
|
Font *truetype.Font
|
||||||
|
Series *Series
|
||||||
|
Range *Range
|
||||||
|
}
|
||||||
|
|
||||||
|
func markLineRender(opt *markLineRenderOption) {
|
||||||
|
d := opt.Draw
|
||||||
|
s := opt.Series
|
||||||
|
if len(s.MarkLine.Data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := d.Render
|
||||||
|
summary := s.Summary()
|
||||||
|
for _, markLine := range s.MarkLine.Data {
|
||||||
|
// 由于mark line会修改style,因此每次重新设置
|
||||||
|
chart.Style{
|
||||||
|
FillColor: opt.FillColor,
|
||||||
|
FontColor: opt.FontColor,
|
||||||
|
FontSize: labelFontSize,
|
||||||
|
StrokeColor: opt.StrokeColor,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
Font: opt.Font,
|
||||||
|
StrokeDashArray: []float64{
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
}.WriteToRenderer(r)
|
||||||
|
value := float64(0)
|
||||||
|
switch markLine.Type {
|
||||||
|
case SeriesMarkDataTypeMax:
|
||||||
|
value = summary.MaxValue
|
||||||
|
case SeriesMarkDataTypeMin:
|
||||||
|
value = summary.MinValue
|
||||||
|
default:
|
||||||
|
value = summary.AverageValue
|
||||||
|
}
|
||||||
|
y := opt.Range.getRestHeight(value)
|
||||||
|
width := d.Box.Width()
|
||||||
|
text := commafWithDigits(value)
|
||||||
|
textBox := r.MeasureText(text)
|
||||||
|
d.makeLine(0, y, width)
|
||||||
|
d.text(text, width, y+textBox.Height()>>1-2)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -28,14 +28,28 @@ import (
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint {
|
||||||
|
data := make([]SeriesMarkPointData, len(markPointTypes))
|
||||||
|
for index, t := range markPointTypes {
|
||||||
|
data[index] = SeriesMarkPointData{
|
||||||
|
Type: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SeriesMarkPoint{
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type markPointRenderOption struct {
|
type markPointRenderOption struct {
|
||||||
|
Draw *Draw
|
||||||
FillColor drawing.Color
|
FillColor drawing.Color
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
Series *Series
|
Series *Series
|
||||||
Points []Point
|
Points []Point
|
||||||
}
|
}
|
||||||
|
|
||||||
func markPointRender(d *Draw, opt markPointRenderOption) {
|
func markPointRender(opt *markPointRenderOption) {
|
||||||
|
d := opt.Draw
|
||||||
s := opt.Series
|
s := opt.Series
|
||||||
if len(s.MarkPoint.Data) == 0 {
|
if len(s.MarkPoint.Data) == 0 {
|
||||||
return
|
return
|
||||||
|
|
@ -54,7 +68,7 @@ func markPointRender(d *Draw, opt markPointRenderOption) {
|
||||||
// 设置文本样式
|
// 设置文本样式
|
||||||
chart.Style{
|
chart.Style{
|
||||||
FontColor: NewTheme(ThemeDark).GetTextColor(),
|
FontColor: NewTheme(ThemeDark).GetTextColor(),
|
||||||
FontSize: 10,
|
FontSize: labelFontSize,
|
||||||
StrokeWidth: 1,
|
StrokeWidth: 1,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}.WriteTextOptionsToRenderer(r)
|
}.WriteTextOptionsToRenderer(r)
|
||||||
|
|
@ -62,7 +76,7 @@ func markPointRender(d *Draw, opt markPointRenderOption) {
|
||||||
p := points[summary.MinIndex]
|
p := points[summary.MinIndex]
|
||||||
value := summary.MinValue
|
value := summary.MinValue
|
||||||
switch markPointData.Type {
|
switch markPointData.Type {
|
||||||
case SeriesMarkPointDataTypeMax:
|
case SeriesMarkDataTypeMax:
|
||||||
p = points[summary.MaxIndex]
|
p = points[summary.MaxIndex]
|
||||||
value = summary.MaxValue
|
value = summary.MaxValue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,14 +41,14 @@ func getPieStyle(theme *Theme, index int) chart.Style {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
func pieChartRender(opt ChartOption, result *basicRenderResult) error {
|
||||||
d, err := NewDraw(DrawOption{
|
d, err := NewDraw(DrawOption{
|
||||||
Parent: result.d,
|
Parent: result.d,
|
||||||
}, PaddingOption(chart.Box{
|
}, PaddingOption(chart.Box{
|
||||||
Top: result.titleBox.Height(),
|
Top: result.titleBox.Height(),
|
||||||
}))
|
}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
values := make([]float64, len(opt.SeriesList))
|
values := make([]float64, len(opt.SeriesList))
|
||||||
|
|
@ -155,5 +155,5 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
d.text(text, x, y)
|
d.text(text, x, y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result.d, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
range.go
4
range.go
|
|
@ -54,9 +54,11 @@ func NewRange(min, max float64, divideCount int) Range {
|
||||||
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
|
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
|
||||||
|
|
||||||
if min != 0 {
|
if min != 0 {
|
||||||
|
isLessThanZero := min < 0
|
||||||
min = float64(int(min/float64(unit)) * unit)
|
min = float64(int(min/float64(unit)) * unit)
|
||||||
// 如果是小于0,int的时候向上取整了,因此调整
|
// 如果是小于0,int的时候向上取整了,因此调整
|
||||||
if min < 0 {
|
if min < 0 ||
|
||||||
|
(isLessThanZero && min == 0) {
|
||||||
min -= float64(unit)
|
min -= float64(unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
series.go
12
series.go
|
|
@ -63,8 +63,9 @@ type SeriesLabel struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SeriesMarkPointDataTypeMax = "max"
|
SeriesMarkDataTypeMax = "max"
|
||||||
SeriesMarkPointDataTypeMin = "min"
|
SeriesMarkDataTypeMin = "min"
|
||||||
|
SeriesMarkDataTypeAverage = "average"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SeriesMarkPointData struct {
|
type SeriesMarkPointData struct {
|
||||||
|
|
@ -74,6 +75,12 @@ type SeriesMarkPoint struct {
|
||||||
SymbolSize int
|
SymbolSize int
|
||||||
Data []SeriesMarkPointData
|
Data []SeriesMarkPointData
|
||||||
}
|
}
|
||||||
|
type SeriesMarkLineData struct {
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
type SeriesMarkLine struct {
|
||||||
|
Data []SeriesMarkLineData
|
||||||
|
}
|
||||||
type Series struct {
|
type Series struct {
|
||||||
index int
|
index int
|
||||||
Type string
|
Type string
|
||||||
|
|
@ -85,6 +92,7 @@ type Series struct {
|
||||||
// Radius of Pie chart, e.g.: 40%
|
// Radius of Pie chart, e.g.: 40%
|
||||||
Radius string
|
Radius string
|
||||||
MarkPoint SeriesMarkPoint
|
MarkPoint SeriesMarkPoint
|
||||||
|
MarkLine SeriesMarkLine
|
||||||
}
|
}
|
||||||
|
|
||||||
type seriesSummary struct {
|
type seriesSummary struct {
|
||||||
|
|
|
||||||
3
xaxis.go
3
xaxis.go
|
|
@ -77,8 +77,7 @@ func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) {
|
||||||
}
|
}
|
||||||
axis := NewAxis(dXAxis, data, style)
|
axis := NewAxis(dXAxis, data, style)
|
||||||
axis.Render()
|
axis.Render()
|
||||||
|
return axis.measure().Height, &Range{
|
||||||
return axis.measureAxis(), &Range{
|
|
||||||
divideCount: len(opt.Data),
|
divideCount: len(opt.Data),
|
||||||
Min: 0,
|
Min: 0,
|
||||||
Max: max,
|
Max: max,
|
||||||
|
|
|
||||||
2
yaxis.go
2
yaxis.go
|
|
@ -62,7 +62,7 @@ func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding ch
|
||||||
SplitLineColor: theme.GetAxisSplitLineColor(),
|
SplitLineColor: theme.GetAxisSplitLineColor(),
|
||||||
SplitLineShow: true,
|
SplitLineShow: true,
|
||||||
}
|
}
|
||||||
width := NewAxis(p, data, style).measureAxis()
|
width := NewAxis(p, data, style).measure().Width
|
||||||
|
|
||||||
yAxisCount := len(opt.YAxisList)
|
yAxisCount := len(opt.YAxisList)
|
||||||
boxWidth := p.Box.Width()
|
boxWidth := p.Box.Width()
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ func TestDrawYAxis(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
newDraw func() *Draw
|
newDraw func() *Draw
|
||||||
newOption func() *ChartOption
|
newOption func() *ChartOption
|
||||||
|
axisIndex int
|
||||||
xAxisHeight int
|
xAxisHeight int
|
||||||
result string
|
result string
|
||||||
}{
|
}{
|
||||||
|
|
@ -70,11 +71,40 @@ func TestDrawYAxis(t *testing.T) {
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 50 10\nL 50 290\" style=\"stroke-width:1;stroke:none;fill:none\"/><path d=\"M 50 10\nL 390 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 57\nL 390 57\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 104\nL 390 104\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 151\nL 390 151\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 198\nL 390 198\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 244\nL 390 244\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><text x=\"36\" y=\"294\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0</text><text x=\"18\" y=\"248\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3.33</text><text x=\"18\" y=\"202\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6.66</text><text x=\"29\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10</text><text x=\"11\" y=\"108\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">13.33</text><text x=\"11\" y=\"61\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">16.66</text><text x=\"29\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20</text></svg>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 50 10\nL 50 290\" style=\"stroke-width:1;stroke:none;fill:none\"/><path d=\"M 50 10\nL 390 10\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 57\nL 390 57\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 104\nL 390 104\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 151\nL 390 151\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 198\nL 390 198\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><path d=\"M 50 244\nL 390 244\" style=\"stroke-width:1;stroke:rgba(224,230,242,1.0);fill:none\"/><text x=\"36\" y=\"294\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0</text><text x=\"18\" y=\"248\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3.33</text><text x=\"18\" y=\"202\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6.66</text><text x=\"29\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10</text><text x=\"11\" y=\"108\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">13.33</text><text x=\"11\" y=\"61\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">16.66</text><text x=\"29\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20</text></svg>",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
newDraw: newDraw,
|
||||||
|
newOption: func() *ChartOption {
|
||||||
|
return &ChartOption{
|
||||||
|
YAxisList: []YAxisOption{
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
Max: NewFloatPoint(20),
|
||||||
|
Formatter: "{value} C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SeriesList: []Series{
|
||||||
|
{
|
||||||
|
YAxisIndex: 1,
|
||||||
|
Data: []SeriesData{
|
||||||
|
{
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisIndex: 1,
|
||||||
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 337 10\nL 337 290\" style=\"stroke-width:1;stroke:none;fill:none\"/><text x=\"345\" y=\"294\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">0 C</text><text x=\"345\" y=\"248\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">3.33 C</text><text x=\"345\" y=\"202\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">6.66 C</text><text x=\"345\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">10 C</text><text x=\"345\" y=\"108\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">13.33 C</text><text x=\"345\" y=\"61\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">16.66 C</text><text x=\"345\" y=\"14\" style=\"stroke-width:0;stroke:none;fill:rgba(110,112,121,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">20 C</text></svg>",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
d := tt.newDraw()
|
d := tt.newDraw()
|
||||||
r, err := drawYAxis(d, tt.newOption(), 0, tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
|
r, err := drawYAxis(d, tt.newOption(), tt.axisIndex, tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal(&Range{
|
assert.Equal(&Range{
|
||||||
divideCount: 6,
|
divideCount: 6,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue