diff --git a/axis.go b/axis.go
index 29cc3cc..333c8c8 100644
--- a/axis.go
+++ b/axis.go
@@ -68,6 +68,10 @@ type axis struct {
data *AxisDataList
style *AxisOption
}
+type axisMeasurement struct {
+ Width int
+ Height int
+}
func NewAxis(d *Draw, data AxisDataList, style AxisOption) *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
r := d.Render
s := a.style.Style(d.Font)
@@ -389,18 +393,21 @@ func (a *axis) axisMeasureTextMaxWidthHeight() (int, int) {
return measureTextMaxWidthHeight(data.TextList(), r)
}
-// measureAxis returns the measurement of axis.
-// If the position is left or right, it will be textMaxWidth + labelMargin + tickLength.
-// If the position is top or bottom, it will be textMaxHeight + labelMargin + tickLength.
-func (a *axis) measureAxis() int {
+// measure returns the measurement of axis.
+// Width will be textMaxWidth + labelMargin + tickLength for position left or right.
+// Height will be textMaxHeight + labelMargin + tickLength for position top or bottom.
+func (a *axis) measure() axisMeasurement {
style := a.style
value := style.GetLabelMargin() + style.GetTickLength()
- textMaxWidth, textMaxHeight := a.axisMeasureTextMaxWidthHeight()
+ textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
+ info := axisMeasurement{}
if style.Position == PositionLeft ||
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
@@ -409,7 +416,7 @@ func (a *axis) Render() {
if isFalse(style.Show) {
return
}
- textMaxWidth, textMaxHeight := a.axisMeasureTextMaxWidthHeight()
+ textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight()
opt := &axisRenderOption{
textMaxWith: textMaxWidth,
textMaxHeight: textMaxHeight,
diff --git a/axis_test.go b/axis_test.go
index a6a7690..37c8314 100644
--- a/axis_test.go
+++ b/axis_test.go
@@ -247,13 +247,13 @@ func TestMeasureAxis(t *testing.T) {
FontSize: 12,
Font: f,
Position: PositionLeft,
- }).measureAxis()
+ }).measure().Width
assert.Equal(44, width)
height := NewAxis(d, data, AxisOption{
FontSize: 12,
Font: f,
Position: PositionTop,
- }).measureAxis()
+ }).measure().Height
assert.Equal(28, height)
}
diff --git a/bar_chart.go b/bar_chart.go
index 7a5805b..597388c 100644
--- a/bar_chart.go
+++ b/bar_chart.go
@@ -26,7 +26,7 @@ import (
"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{
Parent: result.d,
@@ -57,10 +57,29 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
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)
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 {
x0, _ := xRange.GetRange(j)
x := int(x0)
@@ -105,7 +124,9 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
textBox := r.MeasureText(text)
d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-5)
}
- markPointRender(d, markPointRenderOption{
+
+ markPointRenderOptions = append(markPointRenderOptions, &markPointRenderOption{
+ Draw: d,
FillColor: seriesColor,
Font: opt.Font,
Points: points,
@@ -113,5 +134,5 @@ func barChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
})
}
- return result.d, nil
+ return markPointRenderOptions, nil
}
diff --git a/chart.go b/chart.go
index dbc4db4..3321f50 100644
--- a/chart.go
+++ b/chart.go
@@ -80,6 +80,14 @@ func (o *ChartOption) FillDefault(theme string) {
if o.BackgroundColor.IsZero() {
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() {
@@ -110,7 +118,7 @@ func (o *ChartOption) FillDefault(theme string) {
o.Title.SubtextStyle.Font = o.Font
}
- o.Legend.Theme = theme
+ o.Legend.theme = theme
if o.Legend.Style.FontSize == 0 {
o.Legend.Style.FontSize = 10
}
@@ -238,13 +246,14 @@ func Render(opt ChartOption) (*Draw, error) {
if err != nil {
return nil, err
}
+ markPointRenderOptions := make([]*markPointRenderOption, 0)
fns := []func() error{
// pie render
func() error {
if !isPieChart {
return nil
}
- _, err := pieChartRender(opt, result)
+ err := pieChartRender(opt, result)
return err
},
// bar render
@@ -255,8 +264,12 @@ func Render(opt ChartOption) (*Draw, error) {
}
o := opt
o.SeriesList = barSeries
- _, err := barChartRender(o, result)
- return err
+ options, err := barChartRender(o, result)
+ if err != nil {
+ return err
+ }
+ markPointRenderOptions = append(markPointRenderOptions, options...)
+ return nil
},
// line render
func() error {
@@ -266,14 +279,26 @@ func Render(opt ChartOption) (*Draw, error) {
}
o := opt
o.SeriesList = lineSeries
- _, err := lineChartRender(o, result)
- return err
+ options, err := lineChartRender(o, result)
+ if err != nil {
+ return err
+ }
+ markPointRenderOptions = append(markPointRenderOptions, options...)
+ return nil
},
- // legend需要在顶层,因此最后render
+ // legend需要在顶层,因此此处render
func() error {
_, err := NewLegend(result.d, opt.Legend).Render()
return err
},
+ // mark point最后render
+ func() error {
+ // mark point render不会出错
+ for _, opt := range markPointRenderOptions {
+ markPointRender(opt)
+ }
+ return nil
+ },
}
for _, fn := range fns {
@@ -296,6 +321,7 @@ func Render(opt ChartOption) (*Draw, error) {
}
func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
+ opt.FillDefault(opt.Theme)
d, err := NewDraw(
DrawOption{
Type: opt.Type,
@@ -309,7 +335,6 @@ func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
return nil, err
}
- opt.FillDefault(opt.Theme)
if len(opt.YAxisList) > 2 {
return nil, errors.New("y axis should not be gt 2")
}
diff --git a/draw.go b/draw.go
index 5c65d87..12c9d06 100644
--- a/draw.go
+++ b/draw.go
@@ -161,7 +161,9 @@ func (d *Draw) pin(x, y, width int) {
delta := 2*math.Pi - 2*angle
d.arcTo(x, y, r, r, startAngle, delta)
d.lineTo(x, y)
- d.Render.Fill()
+ d.Render.Close()
+ d.Render.FillStroke()
+
startX := x - int(r)
startY := y
endX := x + int(r)
@@ -173,7 +175,8 @@ func (d *Draw) pin(x, y, width int) {
cx := x
cy := y + int(r*2.5)
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) {
@@ -226,7 +229,20 @@ func (d *Draw) arrow(x, y, width, height int, direction string) {
d.lineTo(x0+dx, y0+halfHeight)
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.SetStrokeDashArray([]float64{})
+ d.arrowRight(endX, y, arrowWidth, arrowHeight)
}
func (d *Draw) circle(radius float64, x, y int) {
diff --git a/draw_test.go b/draw_test.go
index efda7eb..11f8709 100644
--- a/draw_test.go
+++ b/draw_test.go
@@ -259,7 +259,7 @@ func TestDraw(t *testing.T) {
}.WriteToRenderer(d.Render)
d.pin(30, 30, 30)
},
- result: "",
+ result: "",
},
// arrow left
{
@@ -349,6 +349,32 @@ func TestDraw(t *testing.T) {
},
result: "",
},
+ // 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: "",
+ },
}
for _, tt := range tests {
d, err := NewDraw(DrawOption{
diff --git a/legend.go b/legend.go
index 7b6722e..263b312 100644
--- a/legend.go
+++ b/legend.go
@@ -30,7 +30,7 @@ import (
)
type LegendOption struct {
- Theme string
+ theme string
// Legend show flag, if nil or true, the legend will be shown
Show *bool
// Legend text style
@@ -67,7 +67,7 @@ func (l *legend) Render() (chart.Box, error) {
if len(opt.Data) == 0 || isFalse(opt.Show) {
return chart.BoxZero, nil
}
- theme := NewTheme(opt.Theme)
+ theme := NewTheme(opt.theme)
padding := opt.Style.Padding
legendDraw, err := NewDraw(DrawOption{
Parent: d,
diff --git a/line_chart.go b/line_chart.go
index 39d8b4d..af87cc9 100644
--- a/line_chart.go
+++ b/line_chart.go
@@ -27,7 +27,7 @@ import (
"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)
@@ -44,9 +44,29 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
r := d.Render
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)
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 {
y := yRange.getRestHeight(item.Value)
@@ -71,11 +91,7 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
textBox := r.MeasureText(text)
d.text(text, x-textBox.Width()>>1, y-5)
}
- index := series.index
- if index == 0 {
- index = i
- }
- seriesColor := theme.GetSeriesColor(index)
+
dotFillColor := drawing.ColorWhite
if theme.IsDark() {
dotFillColor = seriesColor
@@ -88,7 +104,8 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
DotFillColor: dotFillColor,
})
// draw mark point
- markPointRender(d, markPointRenderOption{
+ markPointRenderOptions = append(markPointRenderOptions, &markPointRenderOption{
+ Draw: d,
FillColor: seriesColor,
Font: opt.Font,
Points: points,
@@ -96,5 +113,5 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
})
}
- return result.d, nil
+ return markPointRenderOptions, nil
}
diff --git a/mark_line.go b/mark_line.go
new file mode 100644
index 0000000..e895131
--- /dev/null
+++ b/mark_line.go
@@ -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)
+ }
+
+}
diff --git a/mark_point.go b/mark_point.go
index f1a429f..3f05445 100644
--- a/mark_point.go
+++ b/mark_point.go
@@ -28,14 +28,28 @@ import (
"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 {
+ Draw *Draw
FillColor drawing.Color
Font *truetype.Font
Series *Series
Points []Point
}
-func markPointRender(d *Draw, opt markPointRenderOption) {
+func markPointRender(opt *markPointRenderOption) {
+ d := opt.Draw
s := opt.Series
if len(s.MarkPoint.Data) == 0 {
return
@@ -54,7 +68,7 @@ func markPointRender(d *Draw, opt markPointRenderOption) {
// 设置文本样式
chart.Style{
FontColor: NewTheme(ThemeDark).GetTextColor(),
- FontSize: 10,
+ FontSize: labelFontSize,
StrokeWidth: 1,
Font: opt.Font,
}.WriteTextOptionsToRenderer(r)
@@ -62,7 +76,7 @@ func markPointRender(d *Draw, opt markPointRenderOption) {
p := points[summary.MinIndex]
value := summary.MinValue
switch markPointData.Type {
- case SeriesMarkPointDataTypeMax:
+ case SeriesMarkDataTypeMax:
p = points[summary.MaxIndex]
value = summary.MaxValue
}
diff --git a/pie_chart.go b/pie_chart.go
index 84751f9..a8deb7c 100644
--- a/pie_chart.go
+++ b/pie_chart.go
@@ -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{
Parent: result.d,
}, PaddingOption(chart.Box{
Top: result.titleBox.Height(),
}))
if err != nil {
- return nil, err
+ return err
}
values := make([]float64, len(opt.SeriesList))
@@ -155,5 +155,5 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) {
d.text(text, x, y)
}
}
- return result.d, nil
+ return nil
}
diff --git a/range.go b/range.go
index 308dba8..1e02a51 100644
--- a/range.go
+++ b/range.go
@@ -54,9 +54,11 @@ func NewRange(min, max float64, divideCount int) Range {
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
if min != 0 {
+ isLessThanZero := min < 0
min = float64(int(min/float64(unit)) * unit)
// 如果是小于0,int的时候向上取整了,因此调整
- if min < 0 {
+ if min < 0 ||
+ (isLessThanZero && min == 0) {
min -= float64(unit)
}
}
diff --git a/series.go b/series.go
index b37e13d..57fe12a 100644
--- a/series.go
+++ b/series.go
@@ -63,8 +63,9 @@ type SeriesLabel struct {
}
const (
- SeriesMarkPointDataTypeMax = "max"
- SeriesMarkPointDataTypeMin = "min"
+ SeriesMarkDataTypeMax = "max"
+ SeriesMarkDataTypeMin = "min"
+ SeriesMarkDataTypeAverage = "average"
)
type SeriesMarkPointData struct {
@@ -74,6 +75,12 @@ type SeriesMarkPoint struct {
SymbolSize int
Data []SeriesMarkPointData
}
+type SeriesMarkLineData struct {
+ Type string
+}
+type SeriesMarkLine struct {
+ Data []SeriesMarkLineData
+}
type Series struct {
index int
Type string
@@ -85,6 +92,7 @@ type Series struct {
// Radius of Pie chart, e.g.: 40%
Radius string
MarkPoint SeriesMarkPoint
+ MarkLine SeriesMarkLine
}
type seriesSummary struct {
diff --git a/xaxis.go b/xaxis.go
index 5c21c14..cb7cf33 100644
--- a/xaxis.go
+++ b/xaxis.go
@@ -77,8 +77,7 @@ func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) {
}
axis := NewAxis(dXAxis, data, style)
axis.Render()
-
- return axis.measureAxis(), &Range{
+ return axis.measure().Height, &Range{
divideCount: len(opt.Data),
Min: 0,
Max: max,
diff --git a/yaxis.go b/yaxis.go
index b978d08..cbce44f 100644
--- a/yaxis.go
+++ b/yaxis.go
@@ -62,7 +62,7 @@ func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding ch
SplitLineColor: theme.GetAxisSplitLineColor(),
SplitLineShow: true,
}
- width := NewAxis(p, data, style).measureAxis()
+ width := NewAxis(p, data, style).measure().Width
yAxisCount := len(opt.YAxisList)
boxWidth := p.Box.Width()
diff --git a/yaxis_test.go b/yaxis_test.go
index 5c66caa..0bbef7a 100644
--- a/yaxis_test.go
+++ b/yaxis_test.go
@@ -42,6 +42,7 @@ func TestDrawYAxis(t *testing.T) {
tests := []struct {
newDraw func() *Draw
newOption func() *ChartOption
+ axisIndex int
xAxisHeight int
result string
}{
@@ -70,11 +71,40 @@ func TestDrawYAxis(t *testing.T) {
},
result: "",
},
+ {
+ 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: "",
+ },
}
for _, tt := range tests {
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.Equal(&Range{
divideCount: 6,