Support timeline
This commit is contained in:
parent
406d35c6a6
commit
a1f58d4315
9 changed files with 246 additions and 22 deletions
|
|
@ -101,7 +101,7 @@ func main() {
|
||||||
4,
|
4,
|
||||||
2,
|
2,
|
||||||
},
|
},
|
||||||
}).MarkLine(0, 0, p.Width())
|
}).MarkLine(0, 0, p.Width(), false)
|
||||||
|
|
||||||
top += 60
|
top += 60
|
||||||
// Polygon
|
// Polygon
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
|
||||||
Font: opt.Font,
|
Font: opt.Font,
|
||||||
Series: series,
|
Series: series,
|
||||||
Range: yRange,
|
Range: yRange,
|
||||||
|
Points: points,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// 最大、最小的mark point
|
// 最大、最小的mark point
|
||||||
|
|
|
||||||
157
main/main.go
Normal file
157
main/main.go
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
|
|
||||||
|
charts "github.com/vicanso/go-charts/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeFile(buf []byte) error {
|
||||||
|
tmpPath := "./main"
|
||||||
|
err := os.MkdirAll(tmpPath, 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
file := filepath.Join(tmpPath, "time-line-chart.png")
|
||||||
|
err = os.WriteFile(file, buf, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
xAxisValue := []string{}
|
||||||
|
values := []float64{}
|
||||||
|
now := time.Now()
|
||||||
|
firstAxis := 0
|
||||||
|
for i := 0; i < 300; i++ {
|
||||||
|
if firstAxis == 0 && now.Minute() == 0 {
|
||||||
|
firstAxis = i
|
||||||
|
}
|
||||||
|
xAxisValue = append(xAxisValue, now.Format("15:04"))
|
||||||
|
now = now.Add(time.Minute)
|
||||||
|
value, _ := rand.Int(rand.Reader, big.NewInt(100))
|
||||||
|
if (i%50)/10 < 3 {
|
||||||
|
values = append(values, math.NaN())
|
||||||
|
} else {
|
||||||
|
values = append(values, float64(value.Int64()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p, err := charts.LineRender(
|
||||||
|
[][]float64{
|
||||||
|
values,
|
||||||
|
},
|
||||||
|
charts.TitleTextOptionFunc("Line"),
|
||||||
|
charts.XAxisDataOptionFunc(xAxisValue, charts.FalseFlag()),
|
||||||
|
charts.LegendLabelsOptionFunc([]string{
|
||||||
|
"Demo",
|
||||||
|
}, "50"),
|
||||||
|
func(opt *charts.ChartOption) {
|
||||||
|
opt.SeriesList[0].MarkLine = charts.SeriesMarkLine{
|
||||||
|
Data: []charts.SeriesMarkData{
|
||||||
|
{
|
||||||
|
Type: charts.SeriesMarkDataTypeCustom,
|
||||||
|
CustomYVal: -1,
|
||||||
|
FillColor: &charts.Color{
|
||||||
|
R: 240,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
StrokeColor: &charts.Color{
|
||||||
|
R: 240,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 150,
|
||||||
|
},
|
||||||
|
HideValue: true,
|
||||||
|
IgnoreStrokeDashed: true,
|
||||||
|
IgnoreArrow: true,
|
||||||
|
StrokeWidth: 4,
|
||||||
|
XAxisIndex: 200,
|
||||||
|
XAxisEndIndex: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.SeriesMarkDataTypeCustom,
|
||||||
|
CustomYVal: 80,
|
||||||
|
FillColor: &charts.Color{
|
||||||
|
R: 240,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
StrokeColor: &charts.Color{
|
||||||
|
R: 240,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
AboveColor: &charts.Color{
|
||||||
|
R: 240,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: charts.SeriesMarkDataTypeCustom,
|
||||||
|
CustomYVal: 50,
|
||||||
|
FillColor: &charts.Color{
|
||||||
|
R: 255,
|
||||||
|
G: 165,
|
||||||
|
B: 0,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
StrokeColor: &charts.Color{
|
||||||
|
R: 255,
|
||||||
|
G: 165,
|
||||||
|
B: 0,
|
||||||
|
A: 255,
|
||||||
|
},
|
||||||
|
BelowColor: &charts.Color{
|
||||||
|
R: 255,
|
||||||
|
G: 165,
|
||||||
|
B: 0,
|
||||||
|
A: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
opt.XAxis.FirstAxis = firstAxis
|
||||||
|
opt.XAxis.SplitNumber = 60
|
||||||
|
opt.Legend.Padding = charts.Box{
|
||||||
|
Top: 5,
|
||||||
|
Bottom: 10,
|
||||||
|
}
|
||||||
|
opt.SymbolShow = charts.FalseFlag()
|
||||||
|
opt.LineStrokeWidth = 1
|
||||||
|
opt.ValueFormatter = func(f float64) string {
|
||||||
|
return fmt.Sprintf("%.0f", f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
charts.PaddingOptionFunc(chart.NewBox(10, 10, 30, 10)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := p.Bytes()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
err = writeFile(buf)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
55
mark_line.go
55
mark_line.go
|
|
@ -24,6 +24,7 @@ package charts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/wcharczuk/go-chart/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewMarkLine returns a series mark line
|
// NewMarkLine returns a series mark line
|
||||||
|
|
@ -63,6 +64,7 @@ type markLineRenderOption struct {
|
||||||
Font *truetype.Font
|
Font *truetype.Font
|
||||||
Series Series
|
Series Series
|
||||||
Range axisRange
|
Range axisRange
|
||||||
|
Points []Point
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *markLinePainter) Render() (Box, error) {
|
func (m *markLinePainter) Render() (Box, error) {
|
||||||
|
|
@ -93,14 +95,23 @@ func (m *markLinePainter) Render() (Box, error) {
|
||||||
fontColor = *markLine.FontColor
|
fontColor = *markLine.FontColor
|
||||||
}
|
}
|
||||||
|
|
||||||
painter.OverrideDrawingStyle(Style{
|
strokeWidth := float64(1)
|
||||||
FillColor: fillColor,
|
if markLine.StrokeWidth != 0 {
|
||||||
StrokeColor: strokeColor,
|
strokeWidth = markLine.StrokeWidth
|
||||||
StrokeWidth: 1,
|
}
|
||||||
StrokeDashArray: []float64{
|
|
||||||
|
var strokeDashArray []float64
|
||||||
|
if !markLine.IgnoreStrokeDashed {
|
||||||
|
strokeDashArray = []float64{
|
||||||
4,
|
4,
|
||||||
2,
|
2,
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
painter.OverrideDrawingStyle(Style{
|
||||||
|
FillColor: fillColor,
|
||||||
|
StrokeColor: strokeColor,
|
||||||
|
StrokeWidth: strokeWidth,
|
||||||
|
StrokeDashArray: strokeDashArray,
|
||||||
}).OverrideTextStyle(Style{
|
}).OverrideTextStyle(Style{
|
||||||
Font: font,
|
Font: font,
|
||||||
FontColor: fontColor,
|
FontColor: fontColor,
|
||||||
|
|
@ -121,8 +132,36 @@ func (m *markLinePainter) Render() (Box, error) {
|
||||||
width := painter.Width()
|
width := painter.Width()
|
||||||
text := commafWithDigits(value)
|
text := commafWithDigits(value)
|
||||||
textBox := painter.MeasureText(text)
|
textBox := painter.MeasureText(text)
|
||||||
painter.MarkLine(0, y, width-2)
|
endOffset := 2
|
||||||
painter.Text(text, width, y+textBox.Height()>>1-2)
|
if markLine.IgnoreArrow {
|
||||||
|
endOffset = 0
|
||||||
|
}
|
||||||
|
xPoint := 0
|
||||||
|
if markLine.XAxisIndex > 0 {
|
||||||
|
xPoint = opt.Points[markLine.XAxisIndex].X
|
||||||
|
}
|
||||||
|
xAxiesEndIndex := width
|
||||||
|
if markLine.XAxisEndIndex > 0 {
|
||||||
|
xAxiesEndIndex = opt.Points[markLine.XAxisEndIndex].X
|
||||||
|
}
|
||||||
|
painter.MarkLine(xPoint, y, xAxiesEndIndex-endOffset-xPoint, markLine.IgnoreArrow)
|
||||||
|
if !markLine.HideValue {
|
||||||
|
painter.Text(text, width, y+textBox.Height()>>1-2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if markLine.AboveColor != nil {
|
||||||
|
painter.OverrideDrawingStyle(Style{
|
||||||
|
FillColor: *markLine.AboveColor,
|
||||||
|
})
|
||||||
|
painter.Rect(chart.NewBox(y, 0, width, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if markLine.BelowColor != nil {
|
||||||
|
painter.OverrideDrawingStyle(Style{
|
||||||
|
FillColor: *markLine.BelowColor,
|
||||||
|
})
|
||||||
|
painter.Rect(chart.NewBox(opt.Range.size, 0, width, y))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BoxZero, nil
|
return BoxZero, nil
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,11 @@ func (m *markPointPainter) Render() (Box, error) {
|
||||||
value = markPointData.CustomYVal
|
value = markPointData.CustomYVal
|
||||||
}
|
}
|
||||||
|
|
||||||
painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize)
|
pinY := p.Y
|
||||||
|
if p.Y < symbolSize {
|
||||||
|
pinY = symbolSize
|
||||||
|
}
|
||||||
|
painter.Pin(p.X, pinY-symbolSize>>1, symbolSize)
|
||||||
text := commafWithDigits(value)
|
text := commafWithDigits(value)
|
||||||
textBox := painter.MeasureText(text)
|
textBox := painter.MeasureText(text)
|
||||||
if textBox.Width() > symbolSize {
|
if textBox.Width() > symbolSize {
|
||||||
|
|
|
||||||
25
painter.go
25
painter.go
|
|
@ -460,6 +460,12 @@ func (p *Painter) LineStroke(points []Point) *Painter {
|
||||||
shouldMoveTo = true
|
shouldMoveTo = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if y < 0 {
|
||||||
|
// Ignore point, create a gap.
|
||||||
|
shouldMoveTo = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if shouldMoveTo || index == 0 {
|
if shouldMoveTo || index == 0 {
|
||||||
p.MoveTo(x, y)
|
p.MoveTo(x, y)
|
||||||
shouldMoveTo = false
|
shouldMoveTo = false
|
||||||
|
|
@ -517,17 +523,24 @@ func (p *Painter) SetBackground(width, height int, color Color, inside ...bool)
|
||||||
p.FillStroke()
|
p.FillStroke()
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
func (p *Painter) MarkLine(x, y, width int) *Painter {
|
func (p *Painter) MarkLine(x, y, width int, ignoreArrow bool) *Painter {
|
||||||
arrowWidth := 16
|
arrowWidth := 16
|
||||||
arrowHeight := 10
|
arrowHeight := 10
|
||||||
endX := x + width
|
endX := x + width
|
||||||
radius := 3
|
radius := 3
|
||||||
p.Circle(3, x+radius, y)
|
if !ignoreArrow {
|
||||||
p.render.Fill()
|
p.Circle(3, x+radius, y)
|
||||||
p.MoveTo(x+radius*3, y)
|
p.render.Fill()
|
||||||
p.LineTo(endX-arrowWidth, y)
|
p.MoveTo(x+radius*3, y)
|
||||||
|
p.LineTo(endX-arrowWidth, y)
|
||||||
|
} else {
|
||||||
|
p.MoveTo(x, y)
|
||||||
|
p.LineTo(endX, y)
|
||||||
|
}
|
||||||
p.Stroke()
|
p.Stroke()
|
||||||
p.ArrowRight(endX, y, arrowWidth, arrowHeight)
|
if !ignoreArrow {
|
||||||
|
p.ArrowRight(endX, y, arrowWidth, arrowHeight)
|
||||||
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ func TestPainter(t *testing.T) {
|
||||||
2,
|
2,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
p.MarkLine(0, 20, 300)
|
p.MarkLine(0, 20, 300, false)
|
||||||
},
|
},
|
||||||
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"8\" 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 14 30\nL 289 30\" 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 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>",
|
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<circle cx=\"8\" 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 14 30\nL 289 30\" 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 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>",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
3
range.go
3
range.go
|
|
@ -129,6 +129,9 @@ func (r *axisRange) getHeight(value float64) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *axisRange) getRestHeight(value float64) int {
|
func (r *axisRange) getRestHeight(value float64) int {
|
||||||
|
if math.IsNaN(value) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
return r.size - r.getHeight(value)
|
return r.size - r.getHeight(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
series.go
17
series.go
|
|
@ -100,11 +100,18 @@ type SeriesMarkData struct {
|
||||||
Type string
|
Type string
|
||||||
|
|
||||||
// Custom options.
|
// Custom options.
|
||||||
XAxisIndex int
|
XAxisIndex int
|
||||||
CustomYVal float64
|
XAxisEndIndex int
|
||||||
FillColor *Color
|
CustomYVal float64
|
||||||
StrokeColor *Color
|
FillColor *Color
|
||||||
FontColor *Color
|
StrokeColor *Color
|
||||||
|
FontColor *Color
|
||||||
|
AboveColor *Color
|
||||||
|
BelowColor *Color
|
||||||
|
HideValue bool
|
||||||
|
StrokeWidth float64
|
||||||
|
IgnoreStrokeDashed bool
|
||||||
|
IgnoreArrow bool
|
||||||
}
|
}
|
||||||
type SeriesMarkPoint struct {
|
type SeriesMarkPoint struct {
|
||||||
// The width of symbol, default value is 30
|
// The width of symbol, default value is 30
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue