feat: support make point and make line

This commit is contained in:
vicanso 2022-02-10 23:24:30 +08:00
parent fd05250305
commit e558634dda
16 changed files with 308 additions and 51 deletions

25
axis.go
View file

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

View file

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

View file

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

View file

@ -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
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
// 如果是小于0int的时候向上取整了因此调整 // 如果是小于0int的时候向上取整了因此调整
if min < 0 { if min < 0 ||
(isLessThanZero && min == 0) {
min -= float64(unit) min -= float64(unit)
} }
} }

View file

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

View file

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

View file

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

View file

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