-
Notifications
You must be signed in to change notification settings - Fork 462
Expand file tree
/
Copy pathPlot.swift
More file actions
247 lines (187 loc) · 8.16 KB
/
Plot.swift
File metadata and controls
247 lines (187 loc) · 8.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import UIKit
open class Plot {
// The id for this plot. Used when determining which data to give it in the dataSource
open var identifier: String!
weak var graphViewDrawingDelegate: ScrollableGraphViewDrawingDelegate! = nil
// Animation Settings
// ##################
/// How long the animation should take. Affects both the startup animation and the animation when the range of the y-axis adapts to onscreen points.
open var animationDuration: Double = 1.5
open var adaptAnimationType_: Int {
get { return adaptAnimationType.rawValue }
set {
if let enumValue = ScrollableGraphViewAnimationType(rawValue: newValue) {
adaptAnimationType = enumValue
}
}
}
/// The animation style.
open var adaptAnimationType = ScrollableGraphViewAnimationType.easeOut
/// If adaptAnimationType is set to .Custom, then this is the easing function you would like applied for the animation.
open var customAnimationEasingFunction: ((_ t: Double) -> Double)?
// Private Animation State
// #######################
private var currentAnimations = [GraphPointAnimation]()
private var displayLink: CADisplayLink!
private var previousTimestamp: CFTimeInterval = 0
private var currentTimestamp: CFTimeInterval = 0
private var graphPoints = [GraphPoint]()
deinit {
displayLink?.invalidate()
}
// MARK: Different plot types should implement:
// ############################################
func layers(forViewport viewport: CGRect) -> [ScrollableGraphViewDrawingLayer?] {
return []
}
// MARK: Plot Animation
// ####################
// Animation update loop for co-domain changes.
@objc private func animationUpdate() {
let dt = timeSinceLastFrame()
for animation in currentAnimations {
animation.update(withTimestamp: dt)
if animation.finished {
dequeue(animation: animation)
}
}
graphViewDrawingDelegate.updatePaths()
}
private func animate(point: GraphPoint, to position: CGPoint, withDelay delay: Double = 0) {
let currentPoint = CGPoint(x: point.x, y: point.y)
let animation = GraphPointAnimation(fromPoint: currentPoint, toPoint: position, forGraphPoint: point)
animation.animationEasing = getAnimationEasing()
animation.duration = animationDuration
animation.delay = delay
enqueue(animation: animation)
}
private func getAnimationEasing() -> (Double) -> Double {
switch(self.adaptAnimationType) {
case .elastic:
return Easings.easeOutElastic
case .easeOut:
return Easings.easeOutQuad
case .custom:
if let customEasing = customAnimationEasingFunction {
return customEasing
}
else {
fallthrough
}
default:
return Easings.easeOutQuad
}
}
private func enqueue(animation: GraphPointAnimation) {
if (currentAnimations.count == 0) {
// Need to kick off the loop.
displayLink.isPaused = false
}
currentAnimations.append(animation)
}
private func dequeue(animation: GraphPointAnimation) {
if let index = currentAnimations.firstIndex(of: animation) {
currentAnimations.remove(at: index)
}
if(currentAnimations.count == 0) {
// Stop animation loop.
displayLink.isPaused = true
}
}
internal func dequeueAllAnimations() {
for animation in currentAnimations {
animation.animationDidFinish()
}
currentAnimations.removeAll()
displayLink.isPaused = true
}
private func timeSinceLastFrame() -> Double {
if previousTimestamp == 0 {
previousTimestamp = displayLink.timestamp
} else {
previousTimestamp = currentTimestamp
}
currentTimestamp = displayLink.timestamp
var dt = currentTimestamp - previousTimestamp
if dt > 0.032 {
dt = 0.032
}
return dt
}
internal func startAnimations(forPoints pointsToAnimate: CountableRange<Int>, withData data: [Double], withStaggerValue stagger: Double) {
animatePlotPointPositions(forPoints: pointsToAnimate, withData: data, withDelay: stagger)
}
internal func createPlotPoints(numberOfPoints: Int, range: (min: Double, max: Double)) {
for i in 0 ..< numberOfPoints {
let value = range.min
let position = graphViewDrawingDelegate.calculatePosition(atIndex: i, value: value)
let point = GraphPoint(position: position)
graphPoints.append(point)
}
}
// When active interval changes, need to set the position for any NEWLY ACTIVATED points, otherwise
// they will come on screen at the incorrect position.
// Needs to be called when the active interval has changed and during initial setup.
internal func setPlotPointPositions(forNewlyActivatedPoints newPoints: CountableRange<Int>, withData data: [Double]) {
for i in newPoints.startIndex ..< newPoints.endIndex {
// e.g.
// indices: 10...20
// data positions: 0...10 = // 0 to (end - start)
let dataPosition = i - newPoints.startIndex
let value = data[dataPosition]
let newPosition = graphViewDrawingDelegate.calculatePosition(atIndex: i, value: value)
graphPoints[i].x = newPosition.x
graphPoints[i].y = newPosition.y
}
}
// Same as a above, but can take an array with the indicies of the activated points rather than a range.
internal func setPlotPointPositions(forNewlyActivatedPoints activatedPoints: [Int], withData data: [Double]) {
var index = 0
for activatedPointIndex in activatedPoints {
let dataPosition = index
let value = data[dataPosition]
let newPosition = graphViewDrawingDelegate.calculatePosition(atIndex: activatedPointIndex, value: value)
graphPoints[activatedPointIndex].x = newPosition.x
graphPoints[activatedPointIndex].y = newPosition.y
index += 1
}
}
// When the range changes, we need to set the position for any VISIBLE points, either animating or setting directly
// depending on the settings.
// Needs to be called when the range has changed.
internal func animatePlotPointPositions(forPoints pointsToAnimate: CountableRange<Int>, withData data: [Double], withDelay delay: Double) {
// For any visible points, kickoff the animation to their new position after the axis' min/max has changed.
var dataIndex = 0
for pointIndex in pointsToAnimate {
let newPosition = graphViewDrawingDelegate.calculatePosition(atIndex: pointIndex, value: data[dataIndex])
let point = graphPoints[pointIndex]
animate(point: point, to: newPosition, withDelay: Double(dataIndex) * delay)
dataIndex += 1
}
}
internal func setup() {
displayLink = CADisplayLink(target: self, selector: #selector(animationUpdate))
displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common)
displayLink.isPaused = true
}
internal func reset() {
currentAnimations.removeAll()
graphPoints.removeAll()
displayLink?.invalidate()
previousTimestamp = 0
currentTimestamp = 0
}
internal func invalidate() {
currentAnimations.removeAll()
graphPoints.removeAll()
displayLink?.invalidate()
}
internal func graphPoint(forIndex index: Int) -> GraphPoint {
return graphPoints[index]
}
}
@objc public enum ScrollableGraphViewAnimationType : Int {
case easeOut
case elastic
case custom
}