feat: support mulit y axis
This commit is contained in:
parent
c0bb1654c2
commit
fd05250305
16 changed files with 393 additions and 96 deletions
5
axis.go
5
axis.go
|
|
@ -78,14 +78,17 @@ func NewAxis(d *Draw, data AxisDataList, style AxisOption) *axis {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetLabelMargin returns the label margin value
|
||||||
func (as *AxisOption) GetLabelMargin() int {
|
func (as *AxisOption) GetLabelMargin() int {
|
||||||
return getDefaultInt(as.LabelMargin, 8)
|
return getDefaultInt(as.LabelMargin, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTickLength returns the tick length value
|
||||||
func (as *AxisOption) GetTickLength() int {
|
func (as *AxisOption) GetTickLength() int {
|
||||||
return getDefaultInt(as.TickLength, 5)
|
return getDefaultInt(as.TickLength, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Style returns the style of axis
|
||||||
func (as *AxisOption) Style(f *truetype.Font) chart.Style {
|
func (as *AxisOption) Style(f *truetype.Font) chart.Style {
|
||||||
s := chart.Style{
|
s := chart.Style{
|
||||||
ClassName: as.ClassName,
|
ClassName: as.ClassName,
|
||||||
|
|
@ -109,6 +112,7 @@ type AxisData struct {
|
||||||
}
|
}
|
||||||
type AxisDataList []AxisData
|
type AxisDataList []AxisData
|
||||||
|
|
||||||
|
// TextList returns the text list of axis data
|
||||||
func (l AxisDataList) TextList() []string {
|
func (l AxisDataList) TextList() []string {
|
||||||
textList := make([]string, len(l))
|
textList := make([]string, len(l))
|
||||||
for index, item := range l {
|
for index, item := range l {
|
||||||
|
|
@ -125,6 +129,7 @@ type axisRenderOption struct {
|
||||||
modValue int
|
modValue int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAxisDataListFromStringList creates a new axis data list from string list
|
||||||
func NewAxisDataListFromStringList(textList []string) AxisDataList {
|
func NewAxisDataListFromStringList(textList []string) AxisDataList {
|
||||||
list := make(AxisDataList, len(textList))
|
list := make(AxisDataList, len(textList))
|
||||||
for index, text := range textList {
|
for index, text := range textList {
|
||||||
|
|
|
||||||
27
bar_chart.go
27
bar_chart.go
|
|
@ -37,7 +37,7 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
yRange := result.yRange
|
// yRange := result.yRange
|
||||||
xRange := result.xRange
|
xRange := result.xRange
|
||||||
x0, x1 := xRange.GetRange(0)
|
x0, x1 := xRange.GetRange(0)
|
||||||
width := int(x1 - x0)
|
width := int(x1 - x0)
|
||||||
|
|
@ -50,7 +50,7 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
// 总的宽度-两个margin-(总数-1)的barMargin
|
// 总的宽度-两个margin-(总数-1)的barMargin
|
||||||
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(opt.SeriesList)
|
barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(opt.SeriesList)
|
||||||
|
|
||||||
barMaxHeight := yRange.Size
|
barMaxHeight := result.getYRange(0).Size
|
||||||
theme := NewTheme(opt.Theme)
|
theme := NewTheme(opt.Theme)
|
||||||
|
|
||||||
seriesNames := opt.Legend.Data
|
seriesNames := opt.Legend.Data
|
||||||
|
|
@ -58,6 +58,9 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
r := d.Render
|
r := d.Render
|
||||||
|
|
||||||
for i, series := range opt.SeriesList {
|
for i, series := range opt.SeriesList {
|
||||||
|
yRange := result.getYRange(series.YAxisIndex)
|
||||||
|
points := make([]Point, len(series.Data))
|
||||||
|
seriesColor := theme.GetSeriesColor(i)
|
||||||
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)
|
||||||
|
|
@ -67,26 +70,32 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
h := int(yRange.getHeight(item.Value))
|
h := int(yRange.getHeight(item.Value))
|
||||||
fillColor := theme.GetSeriesColor(i)
|
fillColor := seriesColor
|
||||||
if !item.Style.FillColor.IsZero() {
|
if !item.Style.FillColor.IsZero() {
|
||||||
fillColor = item.Style.FillColor
|
fillColor = item.Style.FillColor
|
||||||
}
|
}
|
||||||
|
top := barMaxHeight - h
|
||||||
d.Bar(chart.Box{
|
d.Bar(chart.Box{
|
||||||
Top: barMaxHeight - h,
|
Top: top,
|
||||||
Left: x,
|
Left: x,
|
||||||
Right: x + barWidth,
|
Right: x + barWidth,
|
||||||
Bottom: barMaxHeight - 1,
|
Bottom: barMaxHeight - 1,
|
||||||
}, BarStyle{
|
}, BarStyle{
|
||||||
FillColor: fillColor,
|
FillColor: fillColor,
|
||||||
})
|
})
|
||||||
|
// 用于生成marker point
|
||||||
|
points[j] = Point{
|
||||||
|
// 居中的位置
|
||||||
|
X: x + barWidth>>1,
|
||||||
|
Y: top,
|
||||||
|
}
|
||||||
if !series.Label.Show {
|
if !series.Label.Show {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
||||||
labelStyle := chart.Style{
|
labelStyle := chart.Style{
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: 10,
|
FontSize: labelFontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}
|
}
|
||||||
if !series.Label.Color.IsZero() {
|
if !series.Label.Color.IsZero() {
|
||||||
|
|
@ -96,6 +105,12 @@ 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{
|
||||||
|
FillColor: seriesColor,
|
||||||
|
Font: opt.Font,
|
||||||
|
Points: points,
|
||||||
|
Series: &series,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.d, nil
|
return result.d, nil
|
||||||
|
|
|
||||||
63
chart.go
63
chart.go
|
|
@ -42,6 +42,8 @@ type Point struct {
|
||||||
Y int
|
Y int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const labelFontSize = 10
|
||||||
|
|
||||||
type ChartOption struct {
|
type ChartOption struct {
|
||||||
Type string
|
Type string
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
|
|
@ -49,7 +51,7 @@ type ChartOption struct {
|
||||||
Title TitleOption
|
Title TitleOption
|
||||||
Legend LegendOption
|
Legend LegendOption
|
||||||
XAxis XAxisOption
|
XAxis XAxisOption
|
||||||
YAxis YAxisOption
|
YAxisList []YAxisOption
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
Parent *Draw
|
Parent *Draw
|
||||||
|
|
@ -61,6 +63,17 @@ type ChartOption struct {
|
||||||
|
|
||||||
func (o *ChartOption) FillDefault(theme string) {
|
func (o *ChartOption) FillDefault(theme string) {
|
||||||
t := NewTheme(theme)
|
t := NewTheme(theme)
|
||||||
|
// 如果为空,初始化
|
||||||
|
yAxisCount := 1
|
||||||
|
for _, series := range o.SeriesList {
|
||||||
|
if series.YAxisIndex >= yAxisCount {
|
||||||
|
yAxisCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yAxisList := make([]YAxisOption, yAxisCount)
|
||||||
|
copy(yAxisList, o.YAxisList)
|
||||||
|
o.YAxisList = yAxisList
|
||||||
|
|
||||||
if o.Font == nil {
|
if o.Font == nil {
|
||||||
o.Font, _ = chart.GetDefaultFont()
|
o.Font, _ = chart.GetDefaultFont()
|
||||||
}
|
}
|
||||||
|
|
@ -136,9 +149,13 @@ func (o *ChartOption) getHeight() int {
|
||||||
return o.Height
|
return o.Height
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *ChartOption) getYRange(axisIndex int) Range {
|
func (o *ChartOption) newYRange(axisIndex int) Range {
|
||||||
min := math.MaxFloat64
|
min := math.MaxFloat64
|
||||||
max := -math.MaxFloat64
|
max := -math.MaxFloat64
|
||||||
|
if axisIndex >= len(o.YAxisList) {
|
||||||
|
axisIndex = 0
|
||||||
|
}
|
||||||
|
yAxis := o.YAxisList[axisIndex]
|
||||||
|
|
||||||
for _, series := range o.SeriesList {
|
for _, series := range o.SeriesList {
|
||||||
if series.YAxisIndex != axisIndex {
|
if series.YAxisIndex != axisIndex {
|
||||||
|
|
@ -155,21 +172,21 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
|
||||||
}
|
}
|
||||||
min = min * 0.9
|
min = min * 0.9
|
||||||
max = max * 1.1
|
max = max * 1.1
|
||||||
if o.YAxis.Min != nil {
|
if yAxis.Min != nil {
|
||||||
min = *o.YAxis.Min
|
min = *yAxis.Min
|
||||||
}
|
}
|
||||||
if o.YAxis.Max != nil {
|
if yAxis.Max != nil {
|
||||||
max = *o.YAxis.Max
|
max = *yAxis.Max
|
||||||
}
|
}
|
||||||
divideCount := 6
|
divideCount := 6
|
||||||
// y轴分设置默认划分为6块
|
// y轴分设置默认划分为6块
|
||||||
r := NewRange(min, max, divideCount)
|
r := NewRange(min, max, divideCount)
|
||||||
|
|
||||||
// 由于NewRange会重新计算min max
|
// 由于NewRange会重新计算min max
|
||||||
if o.YAxis.Min != nil {
|
if yAxis.Min != nil {
|
||||||
r.Min = min
|
r.Min = min
|
||||||
}
|
}
|
||||||
if o.YAxis.Max != nil {
|
if yAxis.Max != nil {
|
||||||
r.Max = max
|
r.Max = max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,11 +195,18 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
|
||||||
|
|
||||||
type basicRenderResult struct {
|
type basicRenderResult struct {
|
||||||
xRange *Range
|
xRange *Range
|
||||||
yRange *Range
|
yRangeList []*Range
|
||||||
d *Draw
|
d *Draw
|
||||||
titleBox chart.Box
|
titleBox chart.Box
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *basicRenderResult) getYRange(index int) *Range {
|
||||||
|
if index >= len(r.yRangeList) {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
return r.yRangeList[index]
|
||||||
|
}
|
||||||
|
|
||||||
func Render(opt ChartOption) (*Draw, error) {
|
func Render(opt ChartOption) (*Draw, error) {
|
||||||
if len(opt.SeriesList) == 0 {
|
if len(opt.SeriesList) == 0 {
|
||||||
return nil, errors.New("series can not be nil")
|
return nil, errors.New("series can not be nil")
|
||||||
|
|
@ -206,7 +230,9 @@ func Render(opt ChartOption) (*Draw, error) {
|
||||||
// pie不需要axis
|
// pie不需要axis
|
||||||
if isPieChart {
|
if isPieChart {
|
||||||
opt.XAxis.Hidden = true
|
opt.XAxis.Hidden = true
|
||||||
opt.YAxis.Hidden = true
|
for index := range opt.YAxisList {
|
||||||
|
opt.YAxisList[index].Hidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result, err := chartBasicRender(&opt)
|
result, err := chartBasicRender(&opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -284,6 +310,9 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
opt.FillDefault(opt.Theme)
|
opt.FillDefault(opt.Theme)
|
||||||
|
if len(opt.YAxisList) > 2 {
|
||||||
|
return nil, errors.New("y axis should not be gt 2")
|
||||||
|
}
|
||||||
if opt.Parent == nil {
|
if opt.Parent == nil {
|
||||||
d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor)
|
d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor)
|
||||||
}
|
}
|
||||||
|
|
@ -299,25 +328,29 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
|
||||||
|
|
||||||
if !opt.XAxis.Hidden {
|
if !opt.XAxis.Hidden {
|
||||||
// xAxis
|
// xAxis
|
||||||
xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis)
|
xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis, len(opt.YAxisList))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 暂时仅支持单一yaxis
|
yRangeList := make([]*Range, len(opt.YAxisList))
|
||||||
|
|
||||||
|
for index, yAxis := range opt.YAxisList {
|
||||||
var yRange *Range
|
var yRange *Range
|
||||||
if !opt.YAxis.Hidden {
|
if !yAxis.Hidden {
|
||||||
yRange, err = drawYAxis(d, opt, xAxisHeight, chart.Box{
|
yRange, err = drawYAxis(d, opt, index, xAxisHeight, chart.Box{
|
||||||
Top: titleBox.Height(),
|
Top: titleBox.Height(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
yRangeList[index] = yRange
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &basicRenderResult{
|
return &basicRenderResult{
|
||||||
xRange: xRange,
|
xRange: xRange,
|
||||||
yRange: yRange,
|
yRangeList: yRangeList,
|
||||||
d: d,
|
d: d,
|
||||||
titleBox: titleBox,
|
titleBox: titleBox,
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
||||||
55
draw.go
55
draw.go
|
|
@ -173,7 +173,60 @@ 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.Fill()
|
d.Render.Stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Draw) arrowLeft(x, y, width, height int) {
|
||||||
|
d.arrow(x, y, width, height, PositionLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Draw) arrowRight(x, y, width, height int) {
|
||||||
|
d.arrow(x, y, width, height, PositionRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Draw) arrowTop(x, y, width, height int) {
|
||||||
|
d.arrow(x, y, width, height, PositionTop)
|
||||||
|
}
|
||||||
|
func (d *Draw) arrowBottom(x, y, width, height int) {
|
||||||
|
d.arrow(x, y, width, height, PositionBottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Draw) arrow(x, y, width, height int, direction string) {
|
||||||
|
halfWidth := width >> 1
|
||||||
|
halfHeight := height >> 1
|
||||||
|
if direction == PositionTop || direction == PositionBottom {
|
||||||
|
x0 := x - halfWidth
|
||||||
|
x1 := x0 + width
|
||||||
|
dy := -height / 3
|
||||||
|
y0 := y
|
||||||
|
y1 := y0 - height
|
||||||
|
if direction == PositionBottom {
|
||||||
|
y0 = y - height
|
||||||
|
y1 = y
|
||||||
|
dy = 2 * dy
|
||||||
|
}
|
||||||
|
d.moveTo(x0, y0)
|
||||||
|
d.lineTo(x0+halfWidth, y1)
|
||||||
|
d.lineTo(x1, y0)
|
||||||
|
d.lineTo(x0+halfWidth, y+dy)
|
||||||
|
d.lineTo(x0, y0)
|
||||||
|
} else {
|
||||||
|
x0 := x + width
|
||||||
|
x1 := x0 - width
|
||||||
|
y0 := y - halfHeight
|
||||||
|
dx := -width / 3
|
||||||
|
if direction == PositionRight {
|
||||||
|
x0 = x - width
|
||||||
|
dx = -dx
|
||||||
|
x1 = x0 + width
|
||||||
|
}
|
||||||
|
d.moveTo(x0, y0)
|
||||||
|
d.lineTo(x1, y0+halfHeight)
|
||||||
|
d.lineTo(x0, y0+height)
|
||||||
|
d.lineTo(x0+dx, y0+halfHeight)
|
||||||
|
d.lineTo(x0, y0)
|
||||||
|
}
|
||||||
|
d.Render.Stroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Draw) circle(radius float64, x, y int) {
|
func (d *Draw) circle(radius float64, x, y int) {
|
||||||
|
|
|
||||||
89
draw_test.go
89
draw_test.go
|
|
@ -239,6 +239,7 @@ 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 205 110\nA 100 100 90.00 0 1 105 210\nZ\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,255,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 205 110\nA 100 100 90.00 0 1 105 210\nZ\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:rgba(0,0,255,1.0)\"/></svg>",
|
||||||
},
|
},
|
||||||
|
// pin
|
||||||
{
|
{
|
||||||
fn: func(d *Draw) {
|
fn: func(d *Draw) {
|
||||||
chart.Style{
|
chart.Style{
|
||||||
|
|
@ -260,6 +261,94 @@ 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 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\" 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>",
|
||||||
},
|
},
|
||||||
|
// arrow left
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}.WriteToRenderer(d.Render)
|
||||||
|
d.arrowLeft(30, 30, 16, 10)
|
||||||
|
},
|
||||||
|
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 51 35\nL 35 40\nL 51 45\nL 46 40\nL 51 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
|
},
|
||||||
|
// arrow right
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}.WriteToRenderer(d.Render)
|
||||||
|
d.arrowRight(30, 30, 16, 10)
|
||||||
|
},
|
||||||
|
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 19 35\nL 35 40\nL 19 45\nL 24 40\nL 19 35\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
|
},
|
||||||
|
// arrow top
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}.WriteToRenderer(d.Render)
|
||||||
|
d.arrowTop(30, 30, 10, 16)
|
||||||
|
},
|
||||||
|
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 40\nL 35 24\nL 40 40\nL 35 35\nL 30 40\" style=\"stroke-width:1;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/></svg>",
|
||||||
|
},
|
||||||
|
// arrow bottom
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}.WriteToRenderer(d.Render)
|
||||||
|
d.arrowBottom(30, 30, 10, 16)
|
||||||
|
},
|
||||||
|
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>",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
d, err := NewDraw(DrawOption{
|
d, err := NewDraw(DrawOption{
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/wcharczuk/go-chart/v2"
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
"github.com/wcharczuk/go-chart/v2/drawing"
|
"github.com/wcharczuk/go-chart/v2/drawing"
|
||||||
)
|
)
|
||||||
|
|
@ -45,36 +43,25 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
seriesNames := opt.Legend.Data
|
seriesNames := opt.Legend.Data
|
||||||
|
|
||||||
r := d.Render
|
r := d.Render
|
||||||
yRange := result.yRange
|
|
||||||
xRange := result.xRange
|
xRange := result.xRange
|
||||||
for i, series := range opt.SeriesList {
|
for i, series := range opt.SeriesList {
|
||||||
points := make([]Point, 0)
|
yRange := result.getYRange(series.YAxisIndex)
|
||||||
minIndex := -1
|
points := make([]Point, len(series.Data))
|
||||||
maxIndex := -1
|
|
||||||
minValue := math.MaxFloat64
|
|
||||||
maxValue := -math.MaxFloat64
|
|
||||||
for j, item := range series.Data {
|
for j, item := range series.Data {
|
||||||
if item.Value < minValue {
|
|
||||||
minIndex = j
|
|
||||||
minValue = item.Value
|
|
||||||
}
|
|
||||||
if item.Value > maxValue {
|
|
||||||
maxIndex = j
|
|
||||||
maxValue = item.Value
|
|
||||||
}
|
|
||||||
y := yRange.getRestHeight(item.Value)
|
y := yRange.getRestHeight(item.Value)
|
||||||
x := xRange.getWidth(float64(j))
|
x := xRange.getWidth(float64(j))
|
||||||
points = append(points, Point{
|
points[j] = Point{
|
||||||
Y: y,
|
Y: y,
|
||||||
X: x,
|
X: x,
|
||||||
})
|
}
|
||||||
if !series.Label.Show {
|
if !series.Label.Show {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1)
|
||||||
labelStyle := chart.Style{
|
labelStyle := chart.Style{
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: 10,
|
FontSize: labelFontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}
|
}
|
||||||
if !series.Label.Color.IsZero() {
|
if !series.Label.Color.IsZero() {
|
||||||
|
|
@ -101,33 +88,12 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
|
||||||
DotFillColor: dotFillColor,
|
DotFillColor: dotFillColor,
|
||||||
})
|
})
|
||||||
// draw mark point
|
// draw mark point
|
||||||
symbolSize := 30
|
markPointRender(d, markPointRenderOption{
|
||||||
if series.MarkPoint.SymbolSize > 0 {
|
|
||||||
symbolSize = series.MarkPoint.SymbolSize
|
|
||||||
}
|
|
||||||
for _, markPointData := range series.MarkPoint.Data {
|
|
||||||
p := points[minIndex]
|
|
||||||
value := minValue
|
|
||||||
switch markPointData.Type {
|
|
||||||
case SeriesMarkPointDataTypeMax:
|
|
||||||
p = points[maxIndex]
|
|
||||||
value = maxValue
|
|
||||||
}
|
|
||||||
chart.Style{
|
|
||||||
FillColor: seriesColor,
|
FillColor: seriesColor,
|
||||||
}.WriteToRenderer(r)
|
|
||||||
d.pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
|
||||||
|
|
||||||
chart.Style{
|
|
||||||
FontColor: NewTheme(ThemeDark).GetTextColor(),
|
|
||||||
FontSize: 10,
|
|
||||||
StrokeWidth: 1,
|
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}.WriteTextOptionsToRenderer(d.Render)
|
Points: points,
|
||||||
text := commafWithDigits(value)
|
Series: &series,
|
||||||
textBox := r.MeasureText(text)
|
})
|
||||||
d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.d, nil
|
return result.d, nil
|
||||||
|
|
|
||||||
75
mark_point.go
Normal file
75
mark_point.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type markPointRenderOption struct {
|
||||||
|
FillColor drawing.Color
|
||||||
|
Font *truetype.Font
|
||||||
|
Series *Series
|
||||||
|
Points []Point
|
||||||
|
}
|
||||||
|
|
||||||
|
func markPointRender(d *Draw, opt markPointRenderOption) {
|
||||||
|
s := opt.Series
|
||||||
|
if len(s.MarkPoint.Data) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
points := opt.Points
|
||||||
|
summary := s.Summary()
|
||||||
|
symbolSize := s.MarkPoint.SymbolSize
|
||||||
|
if symbolSize == 0 {
|
||||||
|
symbolSize = 30
|
||||||
|
}
|
||||||
|
r := d.Render
|
||||||
|
// 设置填充样式
|
||||||
|
chart.Style{
|
||||||
|
FillColor: opt.FillColor,
|
||||||
|
}.WriteToRenderer(r)
|
||||||
|
// 设置文本样式
|
||||||
|
chart.Style{
|
||||||
|
FontColor: NewTheme(ThemeDark).GetTextColor(),
|
||||||
|
FontSize: 10,
|
||||||
|
StrokeWidth: 1,
|
||||||
|
Font: opt.Font,
|
||||||
|
}.WriteTextOptionsToRenderer(r)
|
||||||
|
for _, markPointData := range s.MarkPoint.Data {
|
||||||
|
p := points[summary.MinIndex]
|
||||||
|
value := summary.MinValue
|
||||||
|
switch markPointData.Type {
|
||||||
|
case SeriesMarkPointDataTypeMax:
|
||||||
|
p = points[summary.MaxIndex]
|
||||||
|
value = summary.MaxValue
|
||||||
|
}
|
||||||
|
|
||||||
|
d.pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
||||||
|
text := commafWithDigits(value)
|
||||||
|
textBox := r.MeasureText(text)
|
||||||
|
d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -136,7 +136,7 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
|
||||||
r.Stroke()
|
r.Stroke()
|
||||||
textStyle := chart.Style{
|
textStyle := chart.Style{
|
||||||
FontColor: theme.GetTextColor(),
|
FontColor: theme.GetTextColor(),
|
||||||
FontSize: 10,
|
FontSize: labelFontSize,
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
}
|
}
|
||||||
if !series.Label.Color.IsZero() {
|
if !series.Label.Color.IsZero() {
|
||||||
|
|
|
||||||
6
range.go
6
range.go
|
|
@ -40,9 +40,15 @@ func NewRange(min, max float64, divideCount int) Range {
|
||||||
// 最小单位计算
|
// 最小单位计算
|
||||||
unit := 2
|
unit := 2
|
||||||
if r > 10 {
|
if r > 10 {
|
||||||
|
unit = 4
|
||||||
|
}
|
||||||
|
if r > 30 {
|
||||||
unit = 5
|
unit = 5
|
||||||
}
|
}
|
||||||
if r > 100 {
|
if r > 100 {
|
||||||
|
unit = 10
|
||||||
|
}
|
||||||
|
if r > 200 {
|
||||||
unit = 20
|
unit = 20
|
||||||
}
|
}
|
||||||
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
|
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,16 @@ func TestRange(t *testing.T) {
|
||||||
|
|
||||||
r = NewRange(0, 12, 6)
|
r = NewRange(0, 12, 6)
|
||||||
assert.Equal(0.0, r.Min)
|
assert.Equal(0.0, r.Min)
|
||||||
assert.Equal(30.0, r.Max)
|
assert.Equal(24.0, r.Max)
|
||||||
|
|
||||||
r = NewRange(-13, 18, 6)
|
r = NewRange(-13, 18, 6)
|
||||||
assert.Equal(-20.0, r.Min)
|
assert.Equal(-20.0, r.Min)
|
||||||
assert.Equal(40.0, r.Max)
|
assert.Equal(40.0, r.Max)
|
||||||
|
|
||||||
|
r = NewRange(0, 150, 6)
|
||||||
|
assert.Equal(0.0, r.Min)
|
||||||
|
assert.Equal(180.0, r.Max)
|
||||||
|
|
||||||
r = NewRange(0, 400, 6)
|
r = NewRange(0, 400, 6)
|
||||||
assert.Equal(0.0, r.Min)
|
assert.Equal(0.0, r.Min)
|
||||||
assert.Equal(480.0, r.Max)
|
assert.Equal(480.0, r.Max)
|
||||||
|
|
|
||||||
35
series.go
35
series.go
|
|
@ -23,6 +23,7 @@
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
|
@ -86,6 +87,40 @@ type Series struct {
|
||||||
MarkPoint SeriesMarkPoint
|
MarkPoint SeriesMarkPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type seriesSummary struct {
|
||||||
|
MaxIndex int
|
||||||
|
MaxValue float64
|
||||||
|
MinIndex int
|
||||||
|
MinValue float64
|
||||||
|
AverageValue float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Series) Summary() seriesSummary {
|
||||||
|
minIndex := -1
|
||||||
|
maxIndex := -1
|
||||||
|
minValue := math.MaxFloat64
|
||||||
|
maxValue := -math.MaxFloat64
|
||||||
|
sum := float64(0)
|
||||||
|
for j, item := range s.Data {
|
||||||
|
if item.Value < minValue {
|
||||||
|
minIndex = j
|
||||||
|
minValue = item.Value
|
||||||
|
}
|
||||||
|
if item.Value > maxValue {
|
||||||
|
maxIndex = j
|
||||||
|
maxValue = item.Value
|
||||||
|
}
|
||||||
|
sum += item.Value
|
||||||
|
}
|
||||||
|
return seriesSummary{
|
||||||
|
MaxIndex: maxIndex,
|
||||||
|
MaxValue: maxValue,
|
||||||
|
MinIndex: minIndex,
|
||||||
|
MinValue: minValue,
|
||||||
|
AverageValue: sum / float64(len(s.Data)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type LabelFormatter func(index int, value float64, percent float64) string
|
type LabelFormatter func(index int, value float64, percent float64) string
|
||||||
|
|
||||||
func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
|
||||||
|
|
|
||||||
4
util.go
4
util.go
|
|
@ -106,7 +106,7 @@ func isFalse(flag *bool) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func toFloatPoint(f float64) *float64 {
|
func NewFloatPoint(f float64) *float64 {
|
||||||
v := f
|
v := f
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
@ -118,7 +118,7 @@ func commafWithDigits(value float64) string {
|
||||||
}
|
}
|
||||||
k := float64(1000)
|
k := float64(1000)
|
||||||
if value >= k {
|
if value >= k {
|
||||||
return humanize.CommafWithDigits(value/k, decimals) + " K"
|
return humanize.CommafWithDigits(value/k, decimals) + "k"
|
||||||
}
|
}
|
||||||
return humanize.CommafWithDigits(value, decimals)
|
return humanize.CommafWithDigits(value, decimals)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
xaxis.go
11
xaxis.go
|
|
@ -22,7 +22,9 @@
|
||||||
|
|
||||||
package charts
|
package charts
|
||||||
|
|
||||||
import "github.com/wcharczuk/go-chart/v2"
|
import (
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
)
|
||||||
|
|
||||||
type XAxisOption struct {
|
type XAxisOption struct {
|
||||||
// The boundary gap on both sides of a coordinate axis.
|
// The boundary gap on both sides of a coordinate axis.
|
||||||
|
|
@ -39,16 +41,19 @@ type XAxisOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawXAxis draws x axis, and returns the height, range of if.
|
// drawXAxis draws x axis, and returns the height, range of if.
|
||||||
func drawXAxis(p *Draw, opt *XAxisOption) (int, *Range, error) {
|
func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) {
|
||||||
if opt.Hidden {
|
if opt.Hidden {
|
||||||
return 0, nil, nil
|
return 0, nil, nil
|
||||||
}
|
}
|
||||||
|
left := YAxisWidth
|
||||||
|
right := (yAxisCount - 1) * YAxisWidth
|
||||||
dXAxis, err := NewDraw(
|
dXAxis, err := NewDraw(
|
||||||
DrawOption{
|
DrawOption{
|
||||||
Parent: p,
|
Parent: p,
|
||||||
},
|
},
|
||||||
PaddingOption(chart.Box{
|
PaddingOption(chart.Box{
|
||||||
Left: YAxisWidth,
|
Left: left,
|
||||||
|
Right: right,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ func TestDrawXAxis(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
d := tt.newDraw()
|
d := tt.newDraw()
|
||||||
height, _, err := drawXAxis(d, tt.newOption())
|
height, _, err := drawXAxis(d, tt.newOption(), 1)
|
||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
assert.Equal(25, height)
|
assert.Equal(25, height)
|
||||||
data, err := d.Bytes()
|
data, err := d.Bytes()
|
||||||
|
|
|
||||||
17
yaxis.go
17
yaxis.go
|
|
@ -41,11 +41,11 @@ type YAxisOption struct {
|
||||||
|
|
||||||
const YAxisWidth = 40
|
const YAxisWidth = 40
|
||||||
|
|
||||||
func drawYAxis(p *Draw, opt *ChartOption, xAxisHeight int, padding chart.Box) (*Range, error) {
|
func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) {
|
||||||
theme := NewTheme(opt.Theme)
|
theme := NewTheme(opt.Theme)
|
||||||
yRange := opt.getYRange(0)
|
yRange := opt.newYRange(axisIndex)
|
||||||
values := yRange.Values()
|
values := yRange.Values()
|
||||||
formatter := opt.YAxis.Formatter
|
formatter := opt.YAxisList[axisIndex].Formatter
|
||||||
if len(formatter) != 0 {
|
if len(formatter) != 0 {
|
||||||
for index, text := range values {
|
for index, text := range values {
|
||||||
values[index] = strings.ReplaceAll(formatter, "{value}", text)
|
values[index] = strings.ReplaceAll(formatter, "{value}", text)
|
||||||
|
|
@ -64,12 +64,21 @@ func drawYAxis(p *Draw, opt *ChartOption, xAxisHeight int, padding chart.Box) (*
|
||||||
}
|
}
|
||||||
width := NewAxis(p, data, style).measureAxis()
|
width := NewAxis(p, data, style).measureAxis()
|
||||||
|
|
||||||
|
yAxisCount := len(opt.YAxisList)
|
||||||
|
boxWidth := p.Box.Width()
|
||||||
|
if axisIndex > 0 {
|
||||||
|
style.SplitLineShow = false
|
||||||
|
style.Position = PositionRight
|
||||||
|
padding.Right += (axisIndex - 1) * YAxisWidth
|
||||||
|
} else {
|
||||||
|
boxWidth = p.Box.Width() - (yAxisCount-1)*YAxisWidth
|
||||||
padding.Left += (YAxisWidth - width)
|
padding.Left += (YAxisWidth - width)
|
||||||
|
}
|
||||||
|
|
||||||
dYAxis, err := NewDraw(
|
dYAxis, err := NewDraw(
|
||||||
DrawOption{
|
DrawOption{
|
||||||
Parent: p,
|
Parent: p,
|
||||||
Width: p.Box.Width(),
|
Width: boxWidth,
|
||||||
// 减去x轴的高
|
// 减去x轴的高
|
||||||
Height: p.Box.Height() - xAxisHeight,
|
Height: p.Box.Height() - xAxisHeight,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,10 @@ func TestDrawYAxis(t *testing.T) {
|
||||||
newDraw: newDraw,
|
newDraw: newDraw,
|
||||||
newOption: func() *ChartOption {
|
newOption: func() *ChartOption {
|
||||||
return &ChartOption{
|
return &ChartOption{
|
||||||
YAxis: YAxisOption{
|
YAxisList: []YAxisOption{
|
||||||
Max: toFloatPoint(20),
|
{
|
||||||
|
Max: NewFloatPoint(20),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
SeriesList: []Series{
|
SeriesList: []Series{
|
||||||
{
|
{
|
||||||
|
|
@ -72,7 +74,7 @@ func TestDrawYAxis(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
d := tt.newDraw()
|
d := tt.newDraw()
|
||||||
r, err := drawYAxis(d, tt.newOption(), tt.xAxisHeight, chart.NewBox(10, 10, 10, 10))
|
r, err := drawYAxis(d, tt.newOption(), 0, 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