-
Notifications
You must be signed in to change notification settings - Fork 131
Expand file tree
/
Copy pathCreateGraphPath.ts
More file actions
270 lines (225 loc) · 6.9 KB
/
CreateGraphPath.ts
File metadata and controls
270 lines (225 loc) · 6.9 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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import { SkPath, Skia, SkPoint } from '@shopify/react-native-skia'
import type { GraphPoint, GraphRange } from './LineGraphProps'
const PIXEL_RATIO = 2
export interface GraphXRange {
min: Date
max: Date
}
export interface GraphYRange {
min: number
max: number
}
export interface GraphPathRange {
x: GraphXRange
y: GraphYRange
}
type GraphPathConfig = {
/**
* Graph Points to use for the Path. Will be normalized and centered.
*/
pointsInRange: GraphPoint[]
/**
* Optional Padding (left, right) for the Graph to correctly round the Path.
*/
horizontalPadding: number
/**
* Optional Padding (top, bottom) for the Graph to correctly round the Path.
*/
verticalPadding: number
/**
* Height of the Canvas (Measured with onLayout)
*/
canvasHeight: number
/**
* Width of the Canvas (Measured with onLayout)
*/
canvasWidth: number
/**
* Range of the graph's x and y-axis
*/
range: GraphPathRange
/**
* Enables smoothing of the graph line using a cubic bezier curve.
* When disabled, the graph will be more accurate according to the dataset
*/
enableSmoothing: boolean
}
type GraphPathConfigWithGradient = GraphPathConfig & {
shouldFillGradient: true
}
type GraphPathConfigWithoutGradient = GraphPathConfig & {
shouldFillGradient: false
}
export function getGraphPathRange(
points: GraphPoint[],
range?: GraphRange
): GraphPathRange {
const minValueX = range?.x?.min ?? points[0]?.date ?? new Date()
const maxValueX =
range?.x?.max ?? points[points.length - 1]?.date ?? new Date()
const minValueY =
range?.y?.min ??
points.reduce(
(prev, curr) => (curr.value < prev ? curr.value : prev),
Number.MAX_SAFE_INTEGER
)
const maxValueY =
range?.y?.max ??
points.reduce(
(prev, curr) => (curr.value > prev ? curr.value : prev),
Number.MIN_SAFE_INTEGER
)
return {
x: { min: minValueX, max: maxValueX },
y: { min: minValueY, max: maxValueY },
}
}
export const getXPositionInRange = (
date: Date,
xRange: GraphXRange
): number => {
const diff = xRange.max.getTime() - xRange.min.getTime()
const x = date.getTime()
return (x - xRange.min.getTime()) / diff
}
export const getXInRange = (
width: number,
date: Date,
xRange: GraphXRange
): number => {
return Math.floor(width * getXPositionInRange(date, xRange))
}
export const getYPositionInRange = (
value: number,
yRange: GraphYRange
): number => {
const diff = yRange.max - yRange.min
const y = value
return (y - yRange.min) / diff
}
export const getYInRange = (
height: number,
value: number,
yRange: GraphYRange
): number => {
return Math.floor(height * getYPositionInRange(value, yRange))
}
export const getPointsInRange = (
allPoints: GraphPoint[],
range: GraphPathRange
) => {
return allPoints.filter((point) => {
const portionFactorX = getXPositionInRange(point.date, range.x)
return portionFactorX <= 1 && portionFactorX >= 0
})
}
type GraphPathWithGradient = { path: SkPath; gradientPath: SkPath }
function createGraphPathBase(
props: GraphPathConfigWithGradient
): GraphPathWithGradient
function createGraphPathBase(props: GraphPathConfigWithoutGradient): SkPath
function createGraphPathBase({
pointsInRange: graphData,
range,
horizontalPadding,
verticalPadding,
canvasHeight: height,
canvasWidth: width,
shouldFillGradient,
enableSmoothing,
}: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient):
| SkPath
| GraphPathWithGradient {
const path = Skia.Path.Make()
// Canvas width substracted by the horizontal padding => Actual drawing width
const drawingWidth = width - 2 * horizontalPadding
// Canvas height substracted by the vertical padding => Actual drawing height
const drawingHeight = height - 2 * verticalPadding
if (graphData[0] == null) return path
const points: SkPoint[] = []
const startX =
getXInRange(drawingWidth, graphData[0]!.date, range.x) + horizontalPadding
const endX =
getXInRange(drawingWidth, graphData[graphData.length - 1]!.date, range.x) +
horizontalPadding
const getGraphDataIndex = (pixel: number) =>
Math.round(((pixel - startX) / (endX - startX)) * (graphData.length - 1))
const getNextPixelValue = (pixel: number) => {
if (pixel === endX || pixel + PIXEL_RATIO < endX) return pixel + PIXEL_RATIO
return endX
}
for (
let pixel = startX;
startX <= pixel && pixel <= endX;
pixel = getNextPixelValue(pixel)
) {
const index = getGraphDataIndex(pixel)
// Draw first point only on the very first pixel
if (index === 0 && pixel !== startX) continue
// Draw last point only on the very last pixel
if (index === graphData.length - 1 && pixel !== endX) continue
if (index !== 0 && index !== graphData.length - 1) {
// Only draw point, when the point is exact
const exactPointX =
getXInRange(drawingWidth, graphData[index]!.date, range.x) +
horizontalPadding
const isExactPointInsidePixelRatio = Array(PIXEL_RATIO)
.fill(0)
.some((_value, additionalPixel) => {
return pixel + additionalPixel === exactPointX
})
if (!isExactPointInsidePixelRatio) continue
}
const value = graphData[index]!.value
const y =
drawingHeight -
getYInRange(drawingHeight, value, range.y) +
verticalPadding
points.push({ x: pixel, y: y })
}
for (let i = 0; i < points.length; i++) {
const point = points[i]!
// Start the path or add a line directly to the next point
if (i === 0) {
path.moveTo(point.x, point.y)
} else {
if (enableSmoothing) {
// Continue using smoothing
const prev = points[i - 1]
const prevPrev = points[i - 2]
if (prev == null) continue
const p0 = prevPrev ?? prev
const p1 = prev
const cp1x = (2 * p0.x + p1.x) / 3
const cp1y = (2 * p0.y + p1.y) / 3
const cp2x = (p0.x + 2 * p1.x) / 3
const cp2y = (p0.y + 2 * p1.y) / 3
const cp3x = (p0.x + 4 * p1.x + point.x) / 6
const cp3y = (p0.y + 4 * p1.y + point.y) / 6
path.cubicTo(cp1x, cp1y, cp2x, cp2y, cp3x, cp3y)
if (i === points.length - 1) {
path.cubicTo(point.x, point.y, point.x, point.y, point.x, point.y)
}
} else {
// Direct line to the next point for no smoothing
path.lineTo(point.x, point.y)
}
}
}
if (!shouldFillGradient) return path
const gradientPath = path.copy()
gradientPath.lineTo(endX, height + verticalPadding)
gradientPath.lineTo(0 + horizontalPadding, height + verticalPadding)
return { path: path, gradientPath: gradientPath }
}
export function createGraphPath(props: GraphPathConfig): SkPath {
return createGraphPathBase({ ...props, shouldFillGradient: false })
}
export function createGraphPathWithGradient(
props: GraphPathConfig
): GraphPathWithGradient {
return createGraphPathBase({
...props,
shouldFillGradient: true,
})
}