Support timeline

This commit is contained in:
Vijay Karthik 2025-02-18 15:39:44 -08:00
parent 406d35c6a6
commit a1f58d4315
9 changed files with 246 additions and 22 deletions

View file

@ -101,7 +101,7 @@ func main() {
4,
2,
},
}).MarkLine(0, 0, p.Width())
}).MarkLine(0, 0, p.Width(), false)
top += 60
// Polygon

View file

@ -206,6 +206,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
Font: opt.Font,
Series: series,
Range: yRange,
Points: points,
})
}
// 最大、最小的mark point

157
main/main.go Normal file
View 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)
}
}

View file

@ -24,6 +24,7 @@ package charts
import (
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
)
// NewMarkLine returns a series mark line
@ -63,6 +64,7 @@ type markLineRenderOption struct {
Font *truetype.Font
Series Series
Range axisRange
Points []Point
}
func (m *markLinePainter) Render() (Box, error) {
@ -93,14 +95,23 @@ func (m *markLinePainter) Render() (Box, error) {
fontColor = *markLine.FontColor
}
painter.OverrideDrawingStyle(Style{
FillColor: fillColor,
StrokeColor: strokeColor,
StrokeWidth: 1,
StrokeDashArray: []float64{
strokeWidth := float64(1)
if markLine.StrokeWidth != 0 {
strokeWidth = markLine.StrokeWidth
}
var strokeDashArray []float64
if !markLine.IgnoreStrokeDashed {
strokeDashArray = []float64{
4,
2,
},
}
}
painter.OverrideDrawingStyle(Style{
FillColor: fillColor,
StrokeColor: strokeColor,
StrokeWidth: strokeWidth,
StrokeDashArray: strokeDashArray,
}).OverrideTextStyle(Style{
Font: font,
FontColor: fontColor,
@ -121,8 +132,36 @@ func (m *markLinePainter) Render() (Box, error) {
width := painter.Width()
text := commafWithDigits(value)
textBox := painter.MeasureText(text)
painter.MarkLine(0, y, width-2)
painter.Text(text, width, y+textBox.Height()>>1-2)
endOffset := 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

View file

@ -113,7 +113,11 @@ func (m *markPointPainter) Render() (Box, error) {
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)
textBox := painter.MeasureText(text)
if textBox.Width() > symbolSize {

View file

@ -460,6 +460,12 @@ func (p *Painter) LineStroke(points []Point) *Painter {
shouldMoveTo = true
continue
}
if y < 0 {
// Ignore point, create a gap.
shouldMoveTo = true
continue
}
if shouldMoveTo || index == 0 {
p.MoveTo(x, y)
shouldMoveTo = false
@ -517,17 +523,24 @@ func (p *Painter) SetBackground(width, height int, color Color, inside ...bool)
p.FillStroke()
return p
}
func (p *Painter) MarkLine(x, y, width int) *Painter {
func (p *Painter) MarkLine(x, y, width int, ignoreArrow bool) *Painter {
arrowWidth := 16
arrowHeight := 10
endX := x + width
radius := 3
p.Circle(3, x+radius, y)
p.render.Fill()
p.MoveTo(x+radius*3, y)
p.LineTo(endX-arrowWidth, y)
if !ignoreArrow {
p.Circle(3, x+radius, y)
p.render.Fill()
p.MoveTo(x+radius*3, y)
p.LineTo(endX-arrowWidth, y)
} else {
p.MoveTo(x, y)
p.LineTo(endX, y)
}
p.Stroke()
p.ArrowRight(endX, y, arrowWidth, arrowHeight)
if !ignoreArrow {
p.ArrowRight(endX, y, arrowWidth, arrowHeight)
}
return p
}

View file

@ -270,7 +270,7 @@ func TestPainter(t *testing.T) {
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>",
},

View file

@ -129,6 +129,9 @@ func (r *axisRange) getHeight(value float64) int {
}
func (r *axisRange) getRestHeight(value float64) int {
if math.IsNaN(value) {
return -1
}
return r.size - r.getHeight(value)
}

View file

@ -100,11 +100,18 @@ type SeriesMarkData struct {
Type string
// Custom options.
XAxisIndex int
CustomYVal float64
FillColor *Color
StrokeColor *Color
FontColor *Color
XAxisIndex int
XAxisEndIndex int
CustomYVal float64
FillColor *Color
StrokeColor *Color
FontColor *Color
AboveColor *Color
BelowColor *Color
HideValue bool
StrokeWidth float64
IgnoreStrokeDashed bool
IgnoreArrow bool
}
type SeriesMarkPoint struct {
// The width of symbol, default value is 30