Skip to content

Commit

Permalink
feat: improve createGraphPath functio and apply changes to LineGraph …
Browse files Browse the repository at this point in the history
…component
  • Loading branch information
chrispader committed Jun 13, 2022
1 parent e066934 commit eab9646
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 64 deletions.
14 changes: 12 additions & 2 deletions src/AnimatedLineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
PathCommand,
} from '@shopify/react-native-skia'
import type { AnimatedLineGraphProps } from './LineGraphProps'
import { createGraphPath } from './CreateGraphPath'
import {
createGraphPath,
getGraphPathRange,
GraphPathRange,
} from './CreateGraphPath'
import Reanimated, {
runOnJS,
useAnimatedReaction,
Expand Down Expand Up @@ -77,6 +81,11 @@ export function AnimatedLineGraph({
const paths = useValue<{ from?: SkPath; to?: SkPath }>({})
const commands = useRef<PathCommand[]>([])

const pathRange: GraphPathRange = useMemo(
() => getGraphPathRange(points, range),
[points, range]
)

useEffect(() => {
if (height < 1 || width < 1) {
// view is not yet measured!
Expand All @@ -89,7 +98,7 @@ export function AnimatedLineGraph({

const path = createGraphPath({
points: points,
range: range,
range: pathRange,
horizontalPadding: horizontalPadding,
verticalPadding: verticalPadding,
canvasHeight: height,
Expand Down Expand Up @@ -129,6 +138,7 @@ export function AnimatedLineGraph({
height,
horizontalPadding,
interpolateProgress,
pathRange,
paths,
points,
range,
Expand Down
221 changes: 162 additions & 59 deletions src/CreateGraphPath.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { SkPath, SkPoint, Skia } from '@shopify/react-native-skia'
import {
SkPath,
Skia,
Vector,
cartesian2Polar,
} from '@shopify/react-native-skia'
import type { GraphPoint } from './LineGraphProps'
import { createSplineFunction } from './Maths'

export interface GraphXRange {
min: Date
max: Date
}

export interface GraphYRange {
min: number
max: number
}

export interface GraphPathRange {
x?: {
min: number
max: number
}
y?: {
min: number
max: number
}
x: GraphXRange
y: GraphYRange
}

interface GraphPathConfig {
Expand All @@ -34,81 +44,174 @@ interface GraphPathConfig {
*/
canvasWidth: number

smoothing?: number

range: GraphPathRange
}

export const controlPoint = (
reverse: boolean,
smoothing: number,
current: Vector,
previous: Vector,
next: Vector
): Vector => {
const p = previous
const n = next
// Properties of the opposed-line
const lengthX = n.x - p.x
const lengthY = n.y - p.y

const o = cartesian2Polar({ x: lengthX, y: lengthY })
// If is end-control-point, add PI to the angle to go backward
const angle = o.theta + (reverse ? Math.PI : 0)
const length = o.radius * smoothing
// The control point position is relative to the current point
const x = current.x + Math.cos(angle) * length
const y = current.y + Math.sin(angle) * length
return { x, y }
}

export function getGraphPathRange(
points: GraphPoint[],
range?: GraphPathRange
): GraphPathRange {
const minValueX = range?.x?.min ?? points[0]!.date
const maxValueX = range?.x?.max ?? points[points.length - 1]!.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 pixelFactorX = (
date: Date,
minValue: Date,
maxValue: Date
): number => {
const diff = maxValue.getTime() - minValue.getTime()
const x = date.getTime()

if (x < minValue.getTime() || x > maxValue.getTime()) return 0
return (x - minValue.getTime()) / diff
}

export const pixelFactorY = (
value: number,
minValue: number,
maxValue: number
): number => {
const diff = maxValue - minValue
const y = value

if (y < minValue || y > maxValue) return 0
return (y - minValue) / diff
}

// A Graph Point will be drawn every second "pixel"
const PIXEL_RATIO = 2

export function createGraphPath({
points: graphData,
points,
smoothing = 0.2,
range,
horizontalPadding,
verticalPadding,
canvasHeight: height,
canvasWidth: width,
range,
}: GraphPathConfig): SkPath {
const minValueX = range?.x != null && range.x.min >= 0 ? range.x.min : 0
const maxValueX = range?.x != null ? range.x.max : width

const minValueY =
range?.y != null
? range.y.min
: graphData.reduce(
(prev, curr) => (curr.value < prev ? curr.value : prev),
Number.MAX_SAFE_INTEGER
)

const maxValueY =
range?.y != null
? range.y.max
: graphData.reduce(
(prev, curr) => (curr.value > prev ? curr.value : prev),
Number.MIN_SAFE_INTEGER
)

const points: SkPoint[] = []
const path = Skia.Path.Make()

for (let pixel = 0; pixel < width; pixel += PIXEL_RATIO) {
const index = Math.floor((pixel / width) * graphData.length)
const value = graphData[index]?.value ?? minValueY
const actualWidth = width - 2 * horizontalPadding
const actualHeight = height - 2 * verticalPadding

const getGraphPoint = (point: GraphPoint): Vector => {
const x =
(pixel / maxValueX) * (maxValueX - 2 * horizontalPadding) +
horizontalPadding -
minValueX
actualWidth * pixelFactorX(point.date, range.x.min, range.x.max) +
horizontalPadding
const y =
height -
((value - minValueY) / (maxValueY - minValueY)) *
(height - 2 * verticalPadding) -
actualHeight -
actualHeight * pixelFactorY(point.value, range.y.min, range.y.max) +
verticalPadding

points.push({ x: x, y: y })
return { x: x, y: y }
}

const path = Skia.Path.Make()
if (points[0] == null) return path

for (let i = 0; i < points.length; i++) {
const point = points[i]!
const firstPoint = getGraphPoint(points[0])
path.moveTo(firstPoint.x, firstPoint.y)

// first point needs to start the path
if (i === 0) path.moveTo(point.x, point.y)
points.forEach((point, i) => {
if (i === 0) {
return
}

const prev = points[i - 1]
const prevPrev = points[i - 2]

if (prev == null) continue
if (prev == null) return
const prevPrev = points[i - 2] ?? prev
const next = points[i + 1] ?? point

const currentPoint = getGraphPoint(point)
const prevPoint = getGraphPoint(prev)
const prevPrevPoint = getGraphPoint(prevPrev)
const nextPoint = getGraphPoint(next)

const cps = controlPoint(
false,
smoothing,
prevPoint,
prevPrevPoint,
currentPoint
)
const cpe = controlPoint(
true,
smoothing,
currentPoint,
prevPoint,
nextPoint
)

const splineFunction = createSplineFunction(
prevPoint,
cps,
cpe,
currentPoint
)

const reps = Math.floor(
(actualWidth *
pixelFactorX(
new Date(point.date.getTime() - prev.date.getTime()),
range.x.min,
range.x.max
) +
horizontalPadding) /
PIXEL_RATIO
)

for (let i2 = 0; i2 <= reps; i2++) {
const p = splineFunction(i2 / reps)

if (p == null) return
path.cubicTo(p.x, p.y, p.x, p.y, p.x, p.y)
}
})

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)
}
return path
}
23 changes: 23 additions & 0 deletions src/Maths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { add, Vector } from '@shopify/react-native-skia'

const mul = (v: Vector, f: number): Vector => ({ x: v.x * f, y: v.y * f })

export type SplineFunction = (t: number) => Vector | undefined

export const createSplineFunction = (
p0: Vector,
p1: Vector,
p2: Vector,
p3: Vector
): SplineFunction => {
return (t: number) => {
if (t > 1 || t < 0) return

const p0Formula = mul(p0, Math.pow(1 - t, 3))
const p1Formula = mul(p1, 3 * Math.pow(1 - t, 2) * t)
const p2Formula = mul(p2, 3 * (1 - t) * Math.pow(t, 2))
const p3Formula = mul(p3, Math.pow(t, 3))

return add(add(p0Formula, p1Formula), add(p2Formula, p3Formula))
}
}
15 changes: 12 additions & 3 deletions src/StaticLineGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Canvas, LinearGradient, Path, vec } from '@shopify/react-native-skia'
import { getSixDigitHex } from './utils/getSixDigitHex'
import React, { useCallback, useMemo, useState } from 'react'
import { View, StyleSheet, LayoutChangeEvent } from 'react-native'
import { createGraphPath } from './CreateGraphPath'
import {
createGraphPath,
getGraphPathRange,
GraphPathRange,
} from './CreateGraphPath'
import type { StaticLineGraphProps } from './LineGraphProps'

export function StaticLineGraph({
Expand All @@ -17,6 +21,11 @@ export function StaticLineGraph({
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)

const pathRange: GraphPathRange = useMemo(
() => getGraphPathRange(points, range),
[points, range]
)

const onLayout = useCallback(
({ nativeEvent: { layout } }: LayoutChangeEvent) => {
setWidth(Math.round(layout.width))
Expand All @@ -29,13 +38,13 @@ export function StaticLineGraph({
() =>
createGraphPath({
points: points,
range,
range: pathRange,
canvasHeight: height,
canvasWidth: width,
horizontalPadding: lineThickness,
verticalPadding: lineThickness,
}),
[height, lineThickness, points, range, width]
[height, lineThickness, pathRange, points, width]
)

const gradientColors = useMemo(
Expand Down

0 comments on commit eab9646

Please sign in to comment.