Skip to content

Commit f4b09b5

Browse files
committed
add optimized line plot
1 parent 89f8805 commit f4b09b5

File tree

6 files changed

+90
-3
lines changed

6 files changed

+90
-3
lines changed

cmd/canvas-example/canvas-example

-2.37 MB
Binary file not shown.

cmd/canvas-example/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"io/ioutil"
77
"math"
88

9-
"github.com/kr/pretty"
109
"github.com/loov/plot"
1110
)
1211

@@ -54,7 +53,7 @@ func main() {
5453
Dash: []plot.Length{1, 2, 3},
5554
})
5655

57-
pretty.Print(canvas)
56+
fmt.Println(canvas)
5857

5958
ioutil.WriteFile("example.svg", canvas.Bytes(), 0755)
6059
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/loov/plot
2+
3+
go 1.12

go.sum

Whitespace-only changes.

line.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package plot
22

3+
import "math"
4+
35
// Line implements a simple line plot.
46
type Line struct {
57
Style
@@ -32,3 +34,76 @@ func (line *Line) Draw(plot *Plot, canvas Canvas) {
3234
canvas.Poly(points, &plot.Theme.Line)
3335
}
3436
}
37+
38+
// OptimizedLine implements a simple line plot.
39+
type OptimizedLine struct {
40+
Style
41+
Label string
42+
43+
Data []Point
44+
45+
ThresholdPx float64
46+
}
47+
48+
// NewOptimizedLine creates a new line element that tries to optimize drawing.
49+
func NewOptimizedLine(label string, points []Point, thresholdPx float64) *OptimizedLine {
50+
return &OptimizedLine{
51+
Label: label,
52+
ThresholdPx: thresholdPx,
53+
Data: points,
54+
}
55+
}
56+
57+
// Stats calculates element statistics.
58+
func (line *OptimizedLine) Stats() Stats {
59+
return PointsStats(line.Data)
60+
}
61+
62+
// Draw draws the element to canvas.
63+
func (line *OptimizedLine) Draw(plot *Plot, canvas Canvas) {
64+
canvas = canvas.Clip(canvas.Bounds())
65+
points := project(line.Data, plot.X, plot.Y, canvas.Bounds())
66+
67+
const optimizeCount = 100
68+
if len(points) < optimizeCount {
69+
if !line.Style.IsZero() {
70+
canvas.Poly(points, &line.Style)
71+
} else {
72+
canvas.Poly(points, &plot.Theme.Line)
73+
}
74+
return
75+
}
76+
77+
// always include the first point
78+
optimized := points[:1]
79+
80+
prev := points[0]
81+
mid := points[1]
82+
for _, next := range points[2:] {
83+
// does the mid change significantly from the previous?
84+
if math.Abs(prev.Y-mid.Y) < line.ThresholdPx && math.Abs(prev.X-mid.X) < line.ThresholdPx {
85+
mid = next
86+
continue
87+
}
88+
89+
// is the mid on the line from prev to next?
90+
p := invlerp(mid.X, prev.X, next.X)
91+
knownY := lerp(p, prev.Y, next.Y)
92+
if math.Abs(knownY-mid.X) < line.ThresholdPx {
93+
mid = next
94+
continue
95+
}
96+
97+
// otherwise let's output
98+
optimized = append(optimized, mid)
99+
prev, mid = mid, next
100+
}
101+
// add the final point
102+
optimized = append(optimized, points[len(points)-1])
103+
104+
if !line.Style.IsZero() {
105+
canvas.Poly(optimized, &line.Style)
106+
} else {
107+
canvas.Poly(optimized, &plot.Theme.Line)
108+
}
109+
}

math.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,17 @@ func niceNumber(span float64, round bool) float64 {
3535
return nice * math.Pow(10, exp)
3636
}
3737

38-
// lerpUnit implements interpolates between min and max.
38+
// lerp interpolates between min and max using p=[0,1].
39+
func lerp(p, min, max float64) float64 {
40+
return p*(max-min) + min
41+
}
42+
43+
// invlerp return inverse lerp from min, max and the value.
44+
func invlerp(v, min, max float64) float64 {
45+
return (v - min) / (max - min)
46+
}
47+
48+
// lerpUnit interpolates between min and max using p=[-1,1].
3949
func lerpUnit(p, min, max float64) float64 {
4050
pu := (p + 1) * 0.5
4151
return pu*(max-min) + min

0 commit comments

Comments
 (0)