From a1f58d4315638e493a353701d5ae4258ac67031e Mon Sep 17 00:00:00 2001 From: Vijay Karthik Date: Tue, 18 Feb 2025 15:39:44 -0800 Subject: [PATCH] Support timeline --- examples/painter/main.go | 2 +- line_chart.go | 1 + main/main.go | 157 +++++++++++++++++++++++++++++++++++++++ mark_line.go | 55 ++++++++++++-- mark_point.go | 6 +- painter.go | 25 +++++-- painter_test.go | 2 +- range.go | 3 + series.go | 17 +++-- 9 files changed, 246 insertions(+), 22 deletions(-) create mode 100644 main/main.go diff --git a/examples/painter/main.go b/examples/painter/main.go index b7a5832..ca3444c 100644 --- a/examples/painter/main.go +++ b/examples/painter/main.go @@ -101,7 +101,7 @@ func main() { 4, 2, }, - }).MarkLine(0, 0, p.Width()) + }).MarkLine(0, 0, p.Width(), false) top += 60 // Polygon diff --git a/line_chart.go b/line_chart.go index 363cd36..48cff83 100644 --- a/line_chart.go +++ b/line_chart.go @@ -206,6 +206,7 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) ( Font: opt.Font, Series: series, Range: yRange, + Points: points, }) } // 最大、最小的mark point diff --git a/main/main.go b/main/main.go new file mode 100644 index 0000000..07dc771 --- /dev/null +++ b/main/main.go @@ -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) + } +} diff --git a/mark_line.go b/mark_line.go index 019dcd3..08873a2 100644 --- a/mark_line.go +++ b/mark_line.go @@ -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 diff --git a/mark_point.go b/mark_point.go index bd1af83..661371b 100644 --- a/mark_point.go +++ b/mark_point.go @@ -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 { diff --git a/painter.go b/painter.go index 2bbbe2e..7f3a5f4 100644 --- a/painter.go +++ b/painter.go @@ -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 } diff --git a/painter_test.go b/painter_test.go index b159328..c9d58d6 100644 --- a/painter_test.go +++ b/painter_test.go @@ -270,7 +270,7 @@ func TestPainter(t *testing.T) { 2, }, }) - p.MarkLine(0, 20, 300) + p.MarkLine(0, 20, 300, false) }, result: "\\n", }, diff --git a/range.go b/range.go index ec64c2d..84409a1 100644 --- a/range.go +++ b/range.go @@ -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) } diff --git a/series.go b/series.go index aee6278..9df44fe 100644 --- a/series.go +++ b/series.go @@ -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