From d9461e5b1ed6c39e94335ccd1853ffaccc5d87cf Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 15 Apr 2024 19:40:09 +0200 Subject: [PATCH 1/6] add disableSmoothing prop --- src/AnimatedLineGraph.tsx | 2 ++ src/CreateGraphPath.ts | 55 ++++++++++++++++++++++++--------------- src/LineGraphProps.ts | 4 +++ src/StaticLineGraph.tsx | 4 ++- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/AnimatedLineGraph.tsx b/src/AnimatedLineGraph.tsx index bee225a..89d2b88 100644 --- a/src/AnimatedLineGraph.tsx +++ b/src/AnimatedLineGraph.tsx @@ -71,6 +71,7 @@ export function AnimatedLineGraph({ verticalPadding = lineThickness, TopAxisLabel, BottomAxisLabel, + disableSmoothing = false, ...props }: AnimatedLineGraphProps): React.ReactElement { const [width, setWidth] = useState(0) @@ -196,6 +197,7 @@ export function AnimatedLineGraph({ verticalPadding, canvasHeight: height, canvasWidth: width, + disableSmoothing, } if (shouldFillGradient) { diff --git a/src/CreateGraphPath.ts b/src/CreateGraphPath.ts index 67e9a2e..8a33b7c 100644 --- a/src/CreateGraphPath.ts +++ b/src/CreateGraphPath.ts @@ -43,6 +43,10 @@ type GraphPathConfig = { * Range of the graph's x and y-axis */ range: GraphPathRange + /** + * Disables smoothing of the graph line to increase accuracy of graph according to the dataset + */ + disableSmoothing: boolean } type GraphPathConfigWithGradient = GraphPathConfig & { @@ -140,6 +144,7 @@ function createGraphPathBase({ canvasHeight: height, canvasWidth: width, shouldFillGradient, + disableSmoothing, }: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient): | SkPath | GraphPathWithGradient { @@ -208,27 +213,35 @@ function createGraphPathBase({ for (let i = 0; i < points.length; i++) { const point = points[i]! - // first point needs to start the path - if (i === 0) path.moveTo(point.x, point.y) - - 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) + // Start the path or add a line directly to the next point + if (i === 0) { + path.moveTo(point.x, point.y) + } else { + if (disableSmoothing) { + // Direct line to the next point for no smoothing + path.lineTo(point.x, point.y) + } else { + // 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) + } + } } } diff --git a/src/LineGraphProps.ts b/src/LineGraphProps.ts index 21e147f..3e4d1dc 100644 --- a/src/LineGraphProps.ts +++ b/src/LineGraphProps.ts @@ -47,6 +47,10 @@ interface BaseLineGraphProps extends ViewProps { * Enable the Fade-In Gradient Effect at the beginning of the Graph */ enableFadeInMask?: boolean + /** + * Disables smoothing of the graph line to increase accuracy of graph according to the dataset + */ + disableSmoothing?: boolean } export type StaticLineGraphProps = BaseLineGraphProps & { diff --git a/src/StaticLineGraph.tsx b/src/StaticLineGraph.tsx index 5790400..e42f522 100644 --- a/src/StaticLineGraph.tsx +++ b/src/StaticLineGraph.tsx @@ -16,6 +16,7 @@ export function StaticLineGraph({ color, lineThickness = 3, enableFadeInMask, + disableSmoothing = false, style, ...props }: StaticLineGraphProps): React.ReactElement { @@ -49,8 +50,9 @@ export function StaticLineGraph({ canvasWidth: width, horizontalPadding: lineThickness, verticalPadding: lineThickness, + disableSmoothing, }), - [height, lineThickness, pathRange, pointsInRange, width] + [disableSmoothing, height, lineThickness, pathRange, pointsInRange, width] ) const gradientColors = useMemo( From 23e6e60b3858b75784c4775438f9f8ec74fcf6f5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 15 Apr 2024 20:10:57 +0200 Subject: [PATCH 2/6] fix: getYForX --- src/AnimatedLineGraph.tsx | 4 +- src/GetYForX.ts | 83 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/AnimatedLineGraph.tsx b/src/AnimatedLineGraph.tsx index 89d2b88..436ddd5 100644 --- a/src/AnimatedLineGraph.tsx +++ b/src/AnimatedLineGraph.tsx @@ -170,7 +170,7 @@ export function AnimatedLineGraph({ () => Math.floor(lineWidth) + horizontalPadding ) const indicatorY = useDerivedValue( - () => getYForX(commands.value, indicatorX.value) || 0 + () => getYForX(commands.value, indicatorX.value, disableSmoothing) || 0 ) const indicatorPulseColor = useMemo(() => hexToRgba(color, 0.4), [color]) @@ -372,7 +372,7 @@ export function AnimatedLineGraph({ (fingerX: number) => { 'worklet' - const y = getYForX(commands.value, fingerX) + const y = getYForX(commands.value, fingerX, disableSmoothing) if (y != null) { circleX.value = fingerX diff --git a/src/GetYForX.ts b/src/GetYForX.ts index d243ac7..76a9bbd 100644 --- a/src/GetYForX.ts +++ b/src/GetYForX.ts @@ -1,8 +1,9 @@ import type { Vector, PathCommand } from '@shopify/react-native-skia' import { PathVerb, vec } from '@shopify/react-native-skia' -// code from William Candillon +const GET_Y_FOR_X_PRECISION = 2 +// code from William Candillon const round = (value: number, precision = 0): number => { 'worklet' @@ -156,15 +157,85 @@ export const selectCurve = ( return undefined } +const linearInterpolation = (x: number, from: Vector, to: Vector): number => { + 'worklet' + if (from.x === to.x) return from.y + return from.y + ((to.y - from.y) * (x - from.x)) / (to.x - from.x) +} + +export const selectSegment = ( + cmds: PathCommand[], + x: number, + disableSmoothing: boolean +): Cubic | { from: Vector; to: Vector } | undefined => { + 'worklet' + + let from: Vector = vec(0, 0) + for (let i = 0; i < cmds.length; i++) { + const cmd = cmds[i] + if (cmd == null) continue + + switch (cmd[0]) { + case PathVerb.Move: + from = vec(cmd[1], cmd[2]) + break + case PathVerb.Line: + const lineTo = vec(cmd[1], cmd[2]) + if ( + x >= Math.min(from.x, lineTo.x) && + x <= Math.max(from.x, lineTo.x) + ) { + return { from, to: lineTo } + } + from = lineTo + break + case PathVerb.Cubic: + const cubicTo = vec(cmd[5], cmd[6]) + if (disableSmoothing) { + if ( + x >= Math.min(from.x, cubicTo.x) && + x <= Math.max(from.x, cubicTo.x) + ) { + return { from, to: cubicTo } + } + } else { + const c1 = vec(cmd[1], cmd[2]) + const c2 = vec(cmd[3], cmd[4]) + if ( + x >= Math.min(from.x, cubicTo.x) && + x <= Math.max(from.x, cubicTo.x) + ) { + return { from, c1, c2, to: cubicTo } + } + } + from = cubicTo + break + } + } + + return undefined +} + export const getYForX = ( cmds: PathCommand[], x: number, - precision = 2 + disableSmoothing: boolean ): number | undefined => { 'worklet' - const c = selectCurve(cmds, x) - if (c == null) return undefined - - return cubicBezierYForX(x, c.from, c.c1, c.c2, c.to, precision) + const segment = selectSegment(cmds, x, disableSmoothing) + if (!segment) return undefined + + if ('c1' in segment) { + return cubicBezierYForX( + x, + segment.from, + segment.c1, + segment.c2, + segment.to, + GET_Y_FOR_X_PRECISION + ) + } else { + return linearInterpolation(x, segment.from, segment.to) + } } From 3470c852995a77494af8682cd38d08e2817c82b9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Mon, 15 Apr 2024 20:14:21 +0200 Subject: [PATCH 3/6] add comments --- src/GetYForX.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/GetYForX.ts b/src/GetYForX.ts index 76a9bbd..66bc98b 100644 --- a/src/GetYForX.ts +++ b/src/GetYForX.ts @@ -158,8 +158,13 @@ export const selectCurve = ( } const linearInterpolation = (x: number, from: Vector, to: Vector): number => { - 'worklet' - if (from.x === to.x) return from.y + // Handles vertical lines or when 'from' and 'to' have the same x-coordinate + if (from.x === to.x) return from.y // Return the y-value of 'from' (or 'to') if the line is vertical + + // Calculate the y-coordinate for the given x using linear interpolation + // (y - y1) / (x - x1) = (y2 - y1) / (x2 - x1) + // This equation comes from the slope formula m = (y2 - y1) / (x2 - x1), + // rearranged to find 'y' given 'x'. return from.y + ((to.y - from.y) * (x - from.x)) / (to.x - from.x) } @@ -170,28 +175,38 @@ export const selectSegment = ( ): Cubic | { from: Vector; to: Vector } | undefined => { 'worklet' + // Starting point for path segments let from: Vector = vec(0, 0) + for (let i = 0; i < cmds.length; i++) { const cmd = cmds[i] + // Skip null commands, ensuring robustness if (cmd == null) continue switch (cmd[0]) { case PathVerb.Move: + // Set the starting point for the next segment from = vec(cmd[1], cmd[2]) break case PathVerb.Line: + // Handle direct line segments const lineTo = vec(cmd[1], cmd[2]) + // Check if 'x' is within the horizontal span of the line segment if ( x >= Math.min(from.x, lineTo.x) && x <= Math.max(from.x, lineTo.x) ) { + // Return the segment as a simple line return { from, to: lineTo } } + // Update 'from' to the endpoint of the line for the next segment from = lineTo break case PathVerb.Cubic: + // Handle cubic bezier curves const cubicTo = vec(cmd[5], cmd[6]) if (disableSmoothing) { + // Treat the cubic curve as a straight line if smoothing is disabled if ( x >= Math.min(from.x, cubicTo.x) && x <= Math.max(from.x, cubicTo.x) @@ -199,6 +214,7 @@ export const selectSegment = ( return { from, to: cubicTo } } } else { + // Construct the cubic curve segment if smoothing is enabled const c1 = vec(cmd[1], cmd[2]) const c2 = vec(cmd[3], cmd[4]) if ( @@ -208,11 +224,13 @@ export const selectSegment = ( return { from, c1, c2, to: cubicTo } } } + // Move 'from' to the end of the cubic curve from = cubicTo break } } + // Return undefined if no segment matches the given 'x' return undefined } From 19d797b1e83957668f5a5e5c5e01046f748d2d24 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Apr 2024 12:32:49 +0200 Subject: [PATCH 4/6] change prop to "enableSmoothing" --- src/AnimatedLineGraph.tsx | 8 ++++---- src/CreateGraphPath.ts | 15 ++++++++------- src/GetYForX.ts | 16 ++++++++-------- src/LineGraphProps.ts | 5 +++-- src/StaticLineGraph.tsx | 6 +++--- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/AnimatedLineGraph.tsx b/src/AnimatedLineGraph.tsx index 436ddd5..3b91388 100644 --- a/src/AnimatedLineGraph.tsx +++ b/src/AnimatedLineGraph.tsx @@ -71,7 +71,7 @@ export function AnimatedLineGraph({ verticalPadding = lineThickness, TopAxisLabel, BottomAxisLabel, - disableSmoothing = false, + enableSmoothing = true, ...props }: AnimatedLineGraphProps): React.ReactElement { const [width, setWidth] = useState(0) @@ -170,7 +170,7 @@ export function AnimatedLineGraph({ () => Math.floor(lineWidth) + horizontalPadding ) const indicatorY = useDerivedValue( - () => getYForX(commands.value, indicatorX.value, disableSmoothing) || 0 + () => getYForX(commands.value, indicatorX.value, enableSmoothing) || 0 ) const indicatorPulseColor = useMemo(() => hexToRgba(color, 0.4), [color]) @@ -197,7 +197,7 @@ export function AnimatedLineGraph({ verticalPadding, canvasHeight: height, canvasWidth: width, - disableSmoothing, + enableSmoothing, } if (shouldFillGradient) { @@ -372,7 +372,7 @@ export function AnimatedLineGraph({ (fingerX: number) => { 'worklet' - const y = getYForX(commands.value, fingerX, disableSmoothing) + const y = getYForX(commands.value, fingerX, enableSmoothing) if (y != null) { circleX.value = fingerX diff --git a/src/CreateGraphPath.ts b/src/CreateGraphPath.ts index 8a33b7c..6361478 100644 --- a/src/CreateGraphPath.ts +++ b/src/CreateGraphPath.ts @@ -44,9 +44,10 @@ type GraphPathConfig = { */ range: GraphPathRange /** - * Disables smoothing of the graph line to increase accuracy of graph according to the dataset + * Enables smoothing of the graph line using a cubic bezier curve. + * When disabled, the graph will be more accurate according to the dataset */ - disableSmoothing: boolean + enableSmoothing: boolean } type GraphPathConfigWithGradient = GraphPathConfig & { @@ -144,7 +145,7 @@ function createGraphPathBase({ canvasHeight: height, canvasWidth: width, shouldFillGradient, - disableSmoothing, + enableSmoothing, }: GraphPathConfigWithGradient | GraphPathConfigWithoutGradient): | SkPath | GraphPathWithGradient { @@ -217,10 +218,7 @@ function createGraphPathBase({ if (i === 0) { path.moveTo(point.x, point.y) } else { - if (disableSmoothing) { - // Direct line to the next point for no smoothing - path.lineTo(point.x, point.y) - } else { + if (enableSmoothing) { // Continue using smoothing const prev = points[i - 1] const prevPrev = points[i - 2] @@ -241,6 +239,9 @@ function createGraphPathBase({ 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) } } } diff --git a/src/GetYForX.ts b/src/GetYForX.ts index 66bc98b..9bf8a52 100644 --- a/src/GetYForX.ts +++ b/src/GetYForX.ts @@ -171,7 +171,7 @@ const linearInterpolation = (x: number, from: Vector, to: Vector): number => { export const selectSegment = ( cmds: PathCommand[], x: number, - disableSmoothing: boolean + enableSmoothing: boolean ): Cubic | { from: Vector; to: Vector } | undefined => { 'worklet' @@ -205,23 +205,23 @@ export const selectSegment = ( case PathVerb.Cubic: // Handle cubic bezier curves const cubicTo = vec(cmd[5], cmd[6]) - if (disableSmoothing) { - // Treat the cubic curve as a straight line if smoothing is disabled + if (enableSmoothing) { + // Construct the cubic curve segment if smoothing is enabled + const c1 = vec(cmd[1], cmd[2]) + const c2 = vec(cmd[3], cmd[4]) if ( x >= Math.min(from.x, cubicTo.x) && x <= Math.max(from.x, cubicTo.x) ) { - return { from, to: cubicTo } + return { from, c1, c2, to: cubicTo } } } else { - // Construct the cubic curve segment if smoothing is enabled - const c1 = vec(cmd[1], cmd[2]) - const c2 = vec(cmd[3], cmd[4]) + // Treat the cubic curve as a straight line if smoothing is disabled if ( x >= Math.min(from.x, cubicTo.x) && x <= Math.max(from.x, cubicTo.x) ) { - return { from, c1, c2, to: cubicTo } + return { from, to: cubicTo } } } // Move 'from' to the end of the cubic curve diff --git a/src/LineGraphProps.ts b/src/LineGraphProps.ts index 3e4d1dc..0ae45af 100644 --- a/src/LineGraphProps.ts +++ b/src/LineGraphProps.ts @@ -48,9 +48,10 @@ interface BaseLineGraphProps extends ViewProps { */ enableFadeInMask?: boolean /** - * Disables smoothing of the graph line to increase accuracy of graph according to the dataset + * Enables smoothing of the graph line using a cubic bezier curve. + * When disabled, the graph will be more accurate according to the dataset */ - disableSmoothing?: boolean + enableSmoothing?: boolean } export type StaticLineGraphProps = BaseLineGraphProps & { diff --git a/src/StaticLineGraph.tsx b/src/StaticLineGraph.tsx index e42f522..72e4e99 100644 --- a/src/StaticLineGraph.tsx +++ b/src/StaticLineGraph.tsx @@ -16,7 +16,7 @@ export function StaticLineGraph({ color, lineThickness = 3, enableFadeInMask, - disableSmoothing = false, + enableSmoothing = true, style, ...props }: StaticLineGraphProps): React.ReactElement { @@ -50,9 +50,9 @@ export function StaticLineGraph({ canvasWidth: width, horizontalPadding: lineThickness, verticalPadding: lineThickness, - disableSmoothing, + enableSmoothing, }), - [disableSmoothing, height, lineThickness, pathRange, pointsInRange, width] + [enableSmoothing, height, lineThickness, pathRange, pointsInRange, width] ) const gradientColors = useMemo( From 220cc7f40a360967d8ecdeed853a725c920500e9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Apr 2024 12:34:15 +0200 Subject: [PATCH 5/6] fix: rename param --- src/GetYForX.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GetYForX.ts b/src/GetYForX.ts index 9bf8a52..43408df 100644 --- a/src/GetYForX.ts +++ b/src/GetYForX.ts @@ -237,11 +237,11 @@ export const selectSegment = ( export const getYForX = ( cmds: PathCommand[], x: number, - disableSmoothing: boolean + enableSmoothing: boolean ): number | undefined => { 'worklet' - const segment = selectSegment(cmds, x, disableSmoothing) + const segment = selectSegment(cmds, x, enableSmoothing) if (!segment) return undefined if ('c1' in segment) { From 52fc2c9df29ca432920d8e8781027909f2ce724d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Tue, 16 Apr 2024 12:34:53 +0200 Subject: [PATCH 6/6] remove unused function --- src/GetYForX.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/GetYForX.ts b/src/GetYForX.ts index 43408df..22fa433 100644 --- a/src/GetYForX.ts +++ b/src/GetYForX.ts @@ -127,36 +127,6 @@ interface Cubic { to: Vector } -export const selectCurve = ( - cmds: PathCommand[], - x: number -): Cubic | undefined => { - 'worklet' - - let from: Vector = vec(0, 0) - for (let i = 0; i < cmds.length; i++) { - const cmd = cmds[i] - if (cmd == null) return undefined - if (cmd[0] === PathVerb.Move) { - from = vec(cmd[1], cmd[2]) - } else if (cmd[0] === PathVerb.Cubic) { - const c1 = vec(cmd[1], cmd[2]) - const c2 = vec(cmd[3], cmd[4]) - const to = vec(cmd[5], cmd[6]) - if (x >= from.x && x <= to.x) { - return { - from, - c1, - c2, - to, - } - } - from = to - } - } - return undefined -} - const linearInterpolation = (x: number, from: Vector, to: Vector): number => { // Handles vertical lines or when 'from' and 'to' have the same x-coordinate if (from.x === to.x) return from.y // Return the y-value of 'from' (or 'to') if the line is vertical