feat: support mark point for line chart

This commit is contained in:
vicanso 2022-02-08 23:16:30 +08:00
parent 524eb79a8e
commit c0bb1654c2
11 changed files with 148 additions and 16 deletions

View file

@ -43,6 +43,7 @@ type Point struct {
} }
type ChartOption struct { type ChartOption struct {
Type string
Font *truetype.Font Font *truetype.Font
Theme string Theme string
Title TitleOption Title TitleOption
@ -271,6 +272,7 @@ func Render(opt ChartOption) (*Draw, error) {
func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) { func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) {
d, err := NewDraw( d, err := NewDraw(
DrawOption{ DrawOption{
Type: opt.Type,
Parent: opt.Parent, Parent: opt.Parent,
Width: opt.getWidth(), Width: opt.getWidth(),
Height: opt.getHeight(), Height: opt.getHeight(),

25
draw.go
View file

@ -25,6 +25,7 @@ package charts
import ( import (
"bytes" "bytes"
"errors" "errors"
"math"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2"
@ -151,6 +152,30 @@ func (d *Draw) lineTo(x, y int) {
d.Render.LineTo(x+d.Box.Left, y+d.Box.Top) d.Render.LineTo(x+d.Box.Left, y+d.Box.Top)
} }
func (d *Draw) pin(x, y, width int) {
r := float64(width) / 2
y -= width / 4
angle := chart.DegreesToRadians(15)
startAngle := math.Pi/2 + angle
delta := 2*math.Pi - 2*angle
d.arcTo(x, y, r, r, startAngle, delta)
d.lineTo(x, y)
d.Render.Fill()
startX := x - int(r)
startY := y
endX := x + int(r)
endY := y
d.moveTo(startX, startY)
left := d.Box.Left
top := d.Box.Top
cx := x
cy := y + int(r*2.5)
d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
d.Render.Fill()
}
func (d *Draw) circle(radius float64, x, y int) { func (d *Draw) circle(radius float64, x, y int) {
d.Render.Circle(radius, x+d.Box.Left, y+d.Box.Top) d.Render.Circle(radius, x+d.Box.Left, y+d.Box.Top)
} }

View file

@ -239,6 +239,27 @@ 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>",
}, },
{
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.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>",
},
} }
for _, tt := range tests { for _, tt := range tests {
d, err := NewDraw(DrawOption{ d, err := NewDraw(DrawOption{

View file

@ -129,9 +129,14 @@ func (l *legend) Render() (chart.Box, error) {
} }
x = left x = left
for index, text := range opt.Data { for index, text := range opt.Data {
seriesColor := theme.GetSeriesColor(index)
fillColor := seriesColor
if !theme.IsDark() {
fillColor = theme.GetBackgroundColor()
}
style := chart.Style{ style := chart.Style{
StrokeColor: theme.GetSeriesColor(index), StrokeColor: seriesColor,
FillColor: theme.GetSeriesColor(index), FillColor: fillColor,
StrokeWidth: 3, StrokeWidth: 3,
} }
style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r) style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r)
@ -144,7 +149,7 @@ func (l *legend) Render() (chart.Box, error) {
x = left x = left
renderText = func() { renderText = func() {
x += textPadding x += textPadding
legendDraw.text(text, x, y+legendDotHeight-2) legendDraw.text(text, x, y+legendDotHeight)
x += textBox.Width() x += textBox.Width()
y += (2*legendDotHeight + legendMargin) y += (2*legendDotHeight + legendMargin)
} }
@ -156,7 +161,7 @@ func (l *legend) Render() (chart.Box, error) {
} }
renderText = func() { renderText = func() {
x += textPadding x += textPadding
legendDraw.text(text, x, y+legendDotHeight-2) legendDraw.text(text, x, y+legendDotHeight)
x += textBox.Width() x += textBox.Width()
x += textPadding x += textPadding
} }

View file

@ -65,7 +65,7 @@ func TestLegendRender(t *testing.T) {
Style: style, Style: style,
}) })
}, },
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 0 10\nL 30 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><circle cx=\"15\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"35\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 76 10\nL 106 10\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><circle cx=\"91\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><text x=\"111\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 148 10\nL 178 10\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><circle cx=\"163\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><text x=\"183\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</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 0 10\nL 30 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 76 10\nL 106 10\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"91\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"111\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 148 10\nL 178 10\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"163\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"183\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
box: chart.Box{ box: chart.Box{
Right: 214, Right: 214,
Bottom: 25, Bottom: 25,
@ -86,7 +86,7 @@ func TestLegendRender(t *testing.T) {
Style: style, Style: style,
}) })
}, },
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<text x=\"191\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 222 10\nL 252 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><circle cx=\"237\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"267\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 294 10\nL 324 10\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><circle cx=\"309\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><text x=\"339\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><path d=\"M 370 10\nL 400 10\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><circle cx=\"385\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,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<text x=\"191\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 222 10\nL 252 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"237\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"267\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 294 10\nL 324 10\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"309\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"339\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><path d=\"M 370 10\nL 400 10\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"385\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/></svg>",
box: chart.Box{ box: chart.Box{
Right: 400, Right: 400,
Bottom: 25, Bottom: 25,
@ -106,7 +106,7 @@ func TestLegendRender(t *testing.T) {
Style: style, Style: style,
}) })
}, },
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 93 10\nL 123 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><circle cx=\"108\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"128\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 169 10\nL 199 10\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><circle cx=\"184\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><text x=\"204\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 241 10\nL 271 10\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><circle cx=\"256\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><text x=\"276\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</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 93 10\nL 123 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"108\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"128\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 169 10\nL 199 10\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"184\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"204\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 241 10\nL 271 10\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"256\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"276\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
box: chart.Box{ box: chart.Box{
Right: 307, Right: 307,
Bottom: 25, Bottom: 25,
@ -127,7 +127,7 @@ func TestLegendRender(t *testing.T) {
Orient: OrientVertical, Orient: OrientVertical,
}) })
}, },
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 0 10\nL 30 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><circle cx=\"15\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"35\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 0 30\nL 30 30\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><circle cx=\"15\" cy=\"30\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><text x=\"35\" y=\"33\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 0 50\nL 30 50\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><circle cx=\"15\" cy=\"50\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><text x=\"35\" y=\"53\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</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 0 10\nL 30 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 0 30\nL 30 30\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"30\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"35\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 0 50\nL 30 50\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"15\" cy=\"50\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"35\" y=\"55\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
box: chart.Box{ box: chart.Box{
Right: 61, Right: 61,
Bottom: 70, Bottom: 70,
@ -152,7 +152,7 @@ func TestLegendRender(t *testing.T) {
Right: 101, Right: 101,
Bottom: 70, Bottom: 70,
}, },
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 40 10\nL 70 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><circle cx=\"55\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(84,112,198,1.0)\"/><text x=\"75\" y=\"13\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 40 30\nL 70 30\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><circle cx=\"55\" cy=\"30\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(145,204,117,1.0)\"/><text x=\"75\" y=\"33\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 40 50\nL 70 50\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><circle cx=\"55\" cy=\"50\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(250,200,88,1.0)\"/><text x=\"75\" y=\"53\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</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 40 10\nL 70 10\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"55\" cy=\"10\" r=\"5\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(84,112,198,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"75\" y=\"15\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><path d=\"M 40 30\nL 70 30\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"55\" cy=\"30\" r=\"5\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(145,204,117,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"75\" y=\"35\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><path d=\"M 40 50\nL 70 50\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><circle cx=\"55\" cy=\"50\" r=\"5\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><path d=\"\" style=\"stroke-width:3;stroke:rgba(250,200,88,1.0);fill:rgba(255,255,255,1.0)\"/><text x=\"75\" y=\"55\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text></svg>",
}, },
} }

View file

@ -23,6 +23,8 @@
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"
) )
@ -47,7 +49,19 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
xRange := result.xRange xRange := result.xRange
for i, series := range opt.SeriesList { for i, series := range opt.SeriesList {
points := make([]Point, 0) points := make([]Point, 0)
minIndex := -1
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 = append(points, Point{
@ -86,6 +100,34 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error)
DotWidth: 2, DotWidth: 2,
DotFillColor: dotFillColor, DotFillColor: dotFillColor,
}) })
// draw mark point
symbolSize := 30
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,
}.WriteToRenderer(r)
d.pin(p.X, p.Y-symbolSize>>1, symbolSize)
chart.Style{
FontColor: NewTheme(ThemeDark).GetTextColor(),
FontSize: 10,
StrokeWidth: 1,
Font: opt.Font,
}.WriteTextOptionsToRenderer(d.Render)
text := commafWithDigits(value)
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

View file

@ -24,8 +24,6 @@ package charts
import ( import (
"math" "math"
"github.com/dustin/go-humanize"
) )
type Range struct { type Range struct {
@ -69,7 +67,7 @@ func (r Range) Values() []string {
values := make([]string, 0) values := make([]string, 0)
for i := 0; i <= r.divideCount; i++ { for i := 0; i <= r.divideCount; i++ {
v := r.Min + float64(i)*offset v := r.Min + float64(i)*offset
value := humanize.CommafWithDigits(v, 2) value := commafWithDigits(v)
values = append(values, value) values = append(values, value)
} }
return values return values

View file

@ -60,6 +60,19 @@ type SeriesLabel struct {
Color drawing.Color Color drawing.Color
Show bool Show bool
} }
const (
SeriesMarkPointDataTypeMax = "max"
SeriesMarkPointDataTypeMin = "min"
)
type SeriesMarkPointData struct {
Type string
}
type SeriesMarkPoint struct {
SymbolSize int
Data []SeriesMarkPointData
}
type Series struct { type Series struct {
index int index int
Type string Type string
@ -69,7 +82,8 @@ type Series struct {
Label SeriesLabel Label SeriesLabel
Name string Name string
// Radius of Pie chart, e.g.: 40% // Radius of Pie chart, e.g.: 40%
Radius string Radius string
MarkPoint SeriesMarkPoint
} }
type LabelFormatter func(index int, value float64, percent float64) string type LabelFormatter func(index int, value float64, percent float64) string

View file

@ -131,8 +131,8 @@ func (t *Theme) GetTextColor() drawing.Color {
if t.IsDark() { if t.IsDark() {
return drawing.Color{ return drawing.Color{
R: 238, R: 238,
G: 241, G: 238,
B: 250, B: 238,
A: 255, A: 255,
} }
} }

13
util.go
View file

@ -26,6 +26,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2"
) )
@ -109,3 +110,15 @@ func toFloatPoint(f float64) *float64 {
v := f v := f
return &v return &v
} }
func commafWithDigits(value float64) string {
decimals := 2
m := float64(1000 * 1000)
if value >= m {
return humanize.CommafWithDigits(value/m, decimals) + " M"
}
k := float64(1000)
if value >= k {
return humanize.CommafWithDigits(value/k, decimals) + " K"
}
return humanize.CommafWithDigits(value, decimals)
}

View file

@ -23,6 +23,8 @@
package charts package charts
import ( import (
"strings"
"github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2"
) )
@ -33,6 +35,8 @@ type YAxisOption struct {
Max *float64 Max *float64
// Hidden y axis // Hidden y axis
Hidden bool Hidden bool
// Formatter for y axis text value
Formatter string
} }
const YAxisWidth = 40 const YAxisWidth = 40
@ -40,7 +44,15 @@ const YAxisWidth = 40
func drawYAxis(p *Draw, opt *ChartOption, xAxisHeight int, padding chart.Box) (*Range, error) { func drawYAxis(p *Draw, opt *ChartOption, xAxisHeight int, padding chart.Box) (*Range, error) {
theme := NewTheme(opt.Theme) theme := NewTheme(opt.Theme)
yRange := opt.getYRange(0) yRange := opt.getYRange(0)
data := NewAxisDataListFromStringList(yRange.Values()) values := yRange.Values()
formatter := opt.YAxis.Formatter
if len(formatter) != 0 {
for index, text := range values {
values[index] = strings.ReplaceAll(formatter, "{value}", text)
}
}
data := NewAxisDataListFromStringList(values)
style := AxisOption{ style := AxisOption{
Position: PositionLeft, Position: PositionLeft,
BoundaryGap: FalseFlag(), BoundaryGap: FalseFlag(),