From 4459ad0c4338b1535398e895c5058181c3b9a0a9 Mon Sep 17 00:00:00 2001 From: Conor Barnes Date: Tue, 8 Jun 2021 10:43:26 -0300 Subject: [PATCH 1/4] add clearTimeout to ChartPathProvider useEffect --- src/charts/linear/ChartPath.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/charts/linear/ChartPath.js b/src/charts/linear/ChartPath.js index 1a4fc47..987b78d 100644 --- a/src/charts/linear/ChartPath.js +++ b/src/charts/linear/ChartPath.js @@ -221,6 +221,7 @@ export default function ChartPathProvider({ ); setContextValue((prev) => ({ ...prev, ...newExtremes, data })); setExtremes(newExtremes); + let timeout; if (prevData.value.length !== 0) { valuesStore.current.prevData = currData.value; prevData.value = currData.value; @@ -232,7 +233,7 @@ export default function ChartPathProvider({ curroriginalData.value = parsedoriginalData; currSmoothing.value = data.smoothingFactor || 0; isAnimationInProgress.value = true; - setTimeout( + timeout = setTimeout( () => { isAnimationInProgress.value = false; if (dataQueue.value.length !== 0) { @@ -257,6 +258,7 @@ export default function ChartPathProvider({ currData.value = parsedData; curroriginalData.value = parsedoriginalData; } + return () => clearTimeout(timeout); }, [data]); const isStarted = useSharedValue(false, 'isStarted'); From 5a5f89a8163d0fcb448a5c6ee5f1604a0e7fdcab Mon Sep 17 00:00:00 2001 From: Conor Barnes Date: Mon, 14 Jun 2021 13:44:09 -0300 Subject: [PATCH 2/4] lineDot option --- src/charts/linear/ChartPathProvider.js | 61 ++++++++++++-------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/src/charts/linear/ChartPathProvider.js b/src/charts/linear/ChartPathProvider.js index 058219e..189bcc6 100644 --- a/src/charts/linear/ChartPathProvider.js +++ b/src/charts/linear/ChartPathProvider.js @@ -1,36 +1,33 @@ -import React, { useMemo, useState } from 'react'; -import { useAnimatedStyle } from 'react-native-reanimated'; +import React, { useMemo, useState } from "react"; +import { useAnimatedStyle } from "react-native-reanimated"; -import ChartContext, { useGenerateValues } from '../../helpers/ChartContext'; +import ChartContext, { useGenerateValues } from "../../helpers/ChartContext"; -export default function ChartPathProvider({ data: providedData, children }) { - const values = useGenerateValues(); - const dotStyle = useAnimatedStyle( - () => ({ - opacity: values.dotScale.value, - transform: [ - { translateX: values.positionX.value }, - { translateY: values.positionY.value + 10 }, // TODO temporary fix for clipped chart - { scale: values.dotScale.value }, - ], - }), - [] - ); - const [contextReanimatedValue, setContextValue] = useState({}); - const contextValue = useMemo( - () => ({ - dotStyle, - ...values, - ...contextReanimatedValue, - providedData, - setContextValue, - }), - [dotStyle, values, contextReanimatedValue, providedData] - ); +export default function ChartPathProvider({ data: providedData, children, lineDot }) { + const values = useGenerateValues(); + const dotStyle = useAnimatedStyle( + () => ({ + opacity: values.dotScale.value, + transform: [ + { translateX: values.positionX.value }, + { translateY: lineDot ? 0 : values.positionY.value + 10 }, // TODO temporary fix for clipped chart + { scale: lineDot ? 1 : values.dotScale.value }, + ], + + }), + [] + ); + const [contextReanimatedValue, setContextValue] = useState({}); + const contextValue = useMemo( + () => ({ + dotStyle, + ...values, + ...contextReanimatedValue, + providedData, + setContextValue, + }), + [dotStyle, values, contextReanimatedValue, providedData] + ); - return ( - - {children} - - ); + return {children}; } From 1dbeddf2721c23b47a861168d42e7c79edcf2ad0 Mon Sep 17 00:00:00 2001 From: Conor Barnes Date: Mon, 14 Jun 2021 17:02:01 -0300 Subject: [PATCH 3/4] buff useeffect cleanup --- src/charts/linear/ChartPath.js | 1340 +++++++++++++++----------------- 1 file changed, 646 insertions(+), 694 deletions(-) diff --git a/src/charts/linear/ChartPath.js b/src/charts/linear/ChartPath.js index 987b78d..f9227dc 100644 --- a/src/charts/linear/ChartPath.js +++ b/src/charts/linear/ChartPath.js @@ -1,737 +1,689 @@ -import React, { - createContext, - useContext, - useEffect, - useRef, - useState, -} from 'react'; -import { Platform } from 'react-native'; -import { LongPressGestureHandler } from 'react-native-gesture-handler'; -import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; +import React, { createContext, useContext, useEffect, useRef, useState } from "react"; +import { Platform } from "react-native"; +import { LongPressGestureHandler } from "react-native-gesture-handler"; +import ReactNativeHapticFeedback from "react-native-haptic-feedback"; import Animated, { - useAnimatedGestureHandler, - useAnimatedStyle, - useDerivedValue, - useSharedValue, - withSpring, - withTiming, -} from 'react-native-reanimated'; -import { Path, Svg } from 'react-native-svg'; - -import ChartContext, { - useGenerateValues as generateValues, -} from '../../helpers/ChartContext'; -import { findYExtremes } from '../../helpers/extremesHelpers'; -import { svgBezierPath } from '../../smoothing/smoothSVG'; + useAnimatedGestureHandler, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withSpring, + withTiming, +} from "react-native-reanimated"; +import { Path, Svg } from "react-native-svg"; + +import ChartContext, { useGenerateValues as generateValues } from "../../helpers/ChartContext"; +import { findYExtremes } from "../../helpers/extremesHelpers"; +import { svgBezierPath } from "../../smoothing/smoothSVG"; function impactHeavy() { - 'worklet'; - (Animated.runOnJS - ? Animated.runOnJS(ReactNativeHapticFeedback.trigger) - : ReactNativeHapticFeedback.trigger)('impactHeavy'); + "worklet"; + (Animated.runOnJS + ? Animated.runOnJS(ReactNativeHapticFeedback.trigger) + : ReactNativeHapticFeedback.trigger)("impactHeavy"); } export const InternalContext = createContext(null); -const android = Platform.OS === 'android'; +const android = Platform.OS === "android"; const springDefaultConfig = { - damping: 15, - mass: 1, - stiffness: 600, + damping: 15, + mass: 1, + stiffness: 600, }; const timingFeedbackDefaultConfig = { - duration: 80, + duration: 80, }; const timingAnimationDefaultConfig = { - duration: 300, + duration: 300, }; function combineConfigs(a, b) { - 'worklet'; - const r = {}; - const keysA = Object.keys(a); - for (let i = 0; i < keysA.length; i++) { - const key = keysA[i]; - r[key] = a[key]; - } - const keysB = Object.keys(b); - for (let i = 0; i < keysB.length; i++) { - const key = keysB[i]; - r[key] = b[key]; - } - return r; + "worklet"; + const r = {}; + const keysA = Object.keys(a); + for (let i = 0; i < keysA.length; i++) { + const key = keysA[i]; + r[key] = a[key]; + } + const keysB = Object.keys(b); + for (let i = 0; i < keysB.length; i++) { + const key = keysB[i]; + r[key] = b[key]; + } + return r; } const parse = (data, yRange) => { - const { greatestY, smallestY } = findYExtremes(data); - const minY = yRange ? yRange[0] : smallestY.y; - const maxY = yRange ? yRange[1] : greatestY.y; - const smallestX = data[0]; - const greatestX = data[data.length - 1]; - return [ - data.map(({ x, y }) => ({ - originalX: x, - originalY: y, - x: (x - smallestX.x) / (greatestX.x - smallestX.x), - y: 1 - (y - minY) / (maxY - minY), - })), - { - greatestX, - greatestY, - smallestX, - smallestY, - }, - ]; + const { greatestY, smallestY } = findYExtremes(data); + const minY = yRange ? yRange[0] : smallestY.y; + const maxY = yRange ? yRange[1] : greatestY.y; + const smallestX = data[0]; + const greatestX = data[data.length - 1]; + return [ + data.map(({ x, y }) => ({ + originalX: x, + originalY: y, + x: (x - smallestX.x) / (greatestX.x - smallestX.x), + y: 1 - (y - minY) / (maxY - minY), + })), + { + greatestX, + greatestY, + smallestX, + smallestY, + }, + ]; }; -function setoriginalXYAccordingToPosition( - originalX, - originalY, - position, - data -) { - 'worklet'; - let idx = 0; - for (let i = 0; i < data.value.length; i++) { - if (data.value[i].x >= position) { - idx = i; - break; - } - if (i === data.value.length - 1) { - idx = data.value.length - 1; - } - } - if (!data.value[idx]) { - // prevent the following error on android: - // java.lang.RuntimeException: undefined is not an object (evaluating 'data.value[idx].originalX') - // why data.value = [] sometimes onActive? - console.warn('No data available for chart', data.value.length, idx); - return; - } - originalX.value = data.value[idx].originalX.toString(); - originalY.value = data.value[idx].originalY - ? data.value[idx].originalY.toString() - : 'undefined'; +function setoriginalXYAccordingToPosition(originalX, originalY, position, data) { + "worklet"; + let idx = 0; + for (let i = 0; i < data.value.length; i++) { + if (data.value[i].x >= position) { + idx = i; + break; + } + if (i === data.value.length - 1) { + idx = data.value.length - 1; + } + } + if (!data.value[idx]) { + // prevent the following error on android: + // java.lang.RuntimeException: undefined is not an object (evaluating 'data.value[idx].originalX') + // why data.value = [] sometimes onActive? + console.warn("No data available for chart", data.value.length, idx); + return; + } + originalX.value = data.value[idx].originalX.toString(); + originalY.value = data.value[idx].originalY + ? data.value[idx].originalY.toString() + : "undefined"; } function positionXWithMargin(x, margin, width) { - 'worklet'; - if (x < margin) { - return Math.max(3 * x - 2 * margin, 0); - } else if (width - x < margin) { - return Math.min(margin + x * 2 - width, width); - } else { - return x; - } + "worklet"; + if (x < margin) { + return Math.max(3 * x - 2 * margin, 0); + } else if (width - x < margin) { + return Math.min(margin + x * 2 - width, width); + } else { + return x; + } } function getValue(data, i, smoothingStrategy) { - 'worklet'; - if (smoothingStrategy.value === 'bezier') { - if (i === 0) { - return data.value[i]; - } - - const p0 = data.value[i - 2] || data.value[i - 1] || data.value[i]; - - const x0 = p0.x; - const y0 = p0.y; - const p1 = data.value[i - 1] || data.value[i]; - const x1 = p1.x; - const y1 = p1.y; - const p = data.value[i]; - const x = p.x; - const y = p.y; - const cp3x = (x0 + 4 * x1 + x) / 6; - const cp3y = (y0 + 4 * y1 + y) / 6; - return { x: cp3x, y: cp3y }; - } - return data.value[i]; + "worklet"; + if (smoothingStrategy.value === "bezier") { + if (i === 0) { + return data.value[i]; + } + + const p0 = data.value[i - 2] || data.value[i - 1] || data.value[i]; + + const x0 = p0.x; + const y0 = p0.y; + const p1 = data.value[i - 1] || data.value[i]; + const x1 = p1.x; + const y1 = p1.y; + const p = data.value[i]; + const x = p.x; + const y = p.y; + const cp3x = (x0 + 4 * x1 + x) / 6; + const cp3y = (y0 + 4 * y1 + y) / 6; + return { x: cp3x, y: cp3y }; + } + return data.value[i]; } export default function ChartPathProvider({ - data: rawData, - hitSlop = 0, - hapticsEnabled = false, - springConfig = {}, - timingFeedbackConfig = {}, - timingAnimationConfig = {}, - children, - ...rest + data: rawData, + hitSlop = 0, + hapticsEnabled = false, + springConfig = {}, + timingFeedbackConfig = {}, + timingAnimationConfig = {}, + children, + ...rest }) { - const valuesStore = useRef(null); - if (valuesStore.current == null) { - valuesStore.current = { - currData: [], - curroriginalData: [], - dataQueue: [], - prevData: [], - }; - } - - const { - currSmoothing, - dotScale, - originalX, - originalY, - pathOpacity, - positionX, - positionY, - prevSmoothing, - progress, - layoutSize, - state, - setContextValue = () => {}, - providedData = rawData, - } = useContext(ChartContext) || generateValues(); - - const prevData = useSharedValue(valuesStore.current.prevData, 'prevData'); - const currData = useSharedValue(valuesStore.current.currData, 'currData'); - const curroriginalData = useSharedValue( - valuesStore.current.curroriginalData, - 'curroriginalData' - ); - const hitSlopValue = useSharedValue(hitSlop); - const hapticsEnabledValue = useSharedValue(hapticsEnabled); - const [extremes, setExtremes] = useState({}); - const isAnimationInProgress = useSharedValue(false, 'isAnimationInProgress'); - - const [data, setData] = useState(providedData); - const dataQueue = useSharedValue(valuesStore.current.dataQueue, 'dataQueue'); - useEffect(() => { - if (isAnimationInProgress.value) { - dataQueue.value.push(providedData); - } else { - setData(providedData); - } - }, [providedData]); - - const smoothingStrategy = useSharedValue(data.smoothingStrategy); - - useEffect(() => { - if (!data || !data.points || data.points.length === 0) { - return; - } - const [parsedData] = parse(data.points, data.yRange); - const [parsedoriginalData, newExtremes] = parse( - data.nativePoints || data.points - ); - setContextValue((prev) => ({ ...prev, ...newExtremes, data })); - setExtremes(newExtremes); - let timeout; - if (prevData.value.length !== 0) { - valuesStore.current.prevData = currData.value; - prevData.value = currData.value; - prevSmoothing.value = currSmoothing.value; - progress.value = 0; - valuesStore.current.currData = parsedData; - currData.value = parsedData; - valuesStore.current.curroriginalData = parsedoriginalData; - curroriginalData.value = parsedoriginalData; - currSmoothing.value = data.smoothingFactor || 0; - isAnimationInProgress.value = true; - timeout = setTimeout( - () => { - isAnimationInProgress.value = false; - if (dataQueue.value.length !== 0) { - setData(dataQueue.value[0]); - dataQueue.value.shift(); - } - }, - timingAnimationConfig.duration === undefined - ? timingAnimationDefaultConfig.duration - : timingAnimationConfig.duration - ); - progress.value = withTiming( - 1, - combineConfigs(timingAnimationDefaultConfig, timingAnimationConfig) - ); - } else { - prevSmoothing.value = data.smoothing || 0; - currSmoothing.value = data.smoothing || 0; - valuesStore.current.currData = parsedData; - valuesStore.current.curroriginalData = parsedData; - prevData.value = parsedData; - currData.value = parsedData; - curroriginalData.value = parsedoriginalData; - } - return () => clearTimeout(timeout); - }, [data]); - - const isStarted = useSharedValue(false, 'isStarted'); - - const onLongPressGestureEvent = useAnimatedGestureHandler({ - onActive: (event) => { - state.value = event.state; - if (!currData.value || currData.value.length === 0) { - return; - } - if (!isStarted.value) { - dotScale.value = withSpring( - 1, - combineConfigs(springDefaultConfig, springConfig) - ); - pathOpacity.value = withTiming( - 0, - combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) - ); - } - - if (hapticsEnabledValue.value && !isStarted.value) { - impactHeavy(); - } - isStarted.value = true; - - const eventX = positionXWithMargin( - event.x, - hitSlopValue.value, - layoutSize.value.width - ); - - let idx = 0; - const ss = smoothingStrategy; - for (let i = 0; i < currData.value.length; i++) { - if (getValue(currData, i, ss).x > eventX / layoutSize.value.width) { - idx = i; - break; - } - if (i === currData.value.length - 1) { - idx = currData.value.length - 1; - } - } - - if ( - ss.value === 'bezier' && - currData.value.length > 30 && - eventX / layoutSize.value.width >= - currData.value[currData.value.length - 2].x - ) { - const prevLastY = currData.value[currData.value.length - 2].y; - const prevLastX = currData.value[currData.value.length - 2].x; - const lastY = currData.value[currData.value.length - 1].y; - const lastX = currData.value[currData.value.length - 1].x; - const progress = - (eventX / layoutSize.value.width - prevLastX) / (lastX - prevLastX); - positionY.value = - (prevLastY + progress * (lastY - prevLastY)) * - layoutSize.value.height; - } else if (idx === 0) { - positionY.value = - getValue(currData, idx, ss).y * layoutSize.value.height; - } else { - // prev + diff over X - positionY.value = - (getValue(currData, idx - 1, ss).y + - (getValue(currData, idx, ss).y - - getValue(currData, idx - 1, ss).y) * - ((eventX / layoutSize.value.width - - getValue(currData, idx - 1, ss).x) / - (getValue(currData, idx, ss).x - - getValue(currData, idx - 1, ss).x))) * - layoutSize.value.height; - } - - setoriginalXYAccordingToPosition( - originalX, - originalY, - eventX / layoutSize.value.width, - curroriginalData - ); - positionX.value = eventX; - }, - onCancel: (event) => { - isStarted.value = false; - state.value = event.state; - originalX.value = ''; - originalY.value = ''; - dotScale.value = withSpring( - 0, - combineConfigs(springDefaultConfig, springConfig) - ); - if (android) { - pathOpacity.value = 1; - } else { - pathOpacity.value = withTiming( - 1, - combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) - ); - } - }, - onEnd: (event) => { - isStarted.value = false; - state.value = event.state; - originalX.value = ''; - originalY.value = ''; - dotScale.value = withSpring( - 0, - combineConfigs(springDefaultConfig, springConfig) - ); - if (android) { - pathOpacity.value = 1; - } else { - pathOpacity.value = withTiming( - 1, - combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) - ); - } - - if (hapticsEnabledValue.value) { - impactHeavy(); - } - }, - onFail: (event) => { - isStarted.value = false; - state.value = event.state; - originalX.value = ''; - originalY.value = ''; - dotScale.value = withSpring( - 0, - combineConfigs(springDefaultConfig, springConfig) - ); - if (android) { - pathOpacity.value = 1; - } else { - pathOpacity.value = withTiming( - 1, - combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) - ); - } - }, - onStart: (event) => { - // WARNING: the following code does not run on using iOS, but it does on Android. - // I use the same code from onActive except of "progress.value = 1" which was taken from the original onStart. - state.value = event.state; - if (!currData.value || currData.value.length === 0) { - return; - } - if (!isStarted.value) { - dotScale.value = withSpring( - 1, - combineConfigs(springDefaultConfig, springConfig) - ); - pathOpacity.value = withTiming( - 0, - combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) - ); - } - - if (hapticsEnabledValue.value && !isStarted.value) { - impactHeavy(); - } - isStarted.value = true; - - const eventX = positionXWithMargin( - event.x, - hitSlopValue.value, - layoutSize.value.width - ); - - progress.value = 1; - let idx = 0; - const ss = smoothingStrategy; - for (let i = 0; i < currData.value.length; i++) { - if (getValue(currData, i, ss).x > eventX / layoutSize.value.width) { - idx = i; - break; - } - if (i === currData.value.length - 1) { - idx = currData.value.length - 1; - } - } - - if ( - ss.value === 'bezier' && - currData.value.length > 30 && - eventX / layoutSize.value.width >= - currData.value[currData.value.length - 2].x - ) { - const prevLastY = currData.value[currData.value.length - 2].y; - const prevLastX = currData.value[currData.value.length - 2].x; - const lastY = currData.value[currData.value.length - 1].y; - const lastX = currData.value[currData.value.length - 1].x; - const progress = - (eventX / layoutSize.value.width - prevLastX) / (lastX - prevLastX); - positionY.value = - (prevLastY + progress * (lastY - prevLastY)) * - layoutSize.value.height; - } else if (idx === 0) { - positionY.value = - getValue(currData, idx, ss).y * layoutSize.value.height; - } else { - // prev + diff over X - positionY.value = - (getValue(currData, idx - 1, ss).y + - (getValue(currData, idx, ss).y - - getValue(currData, idx - 1, ss).y) * - ((eventX / layoutSize.value.width - - getValue(currData, idx - 1, ss).x) / - (getValue(currData, idx, ss).x - - getValue(currData, idx - 1, ss).x))) * - layoutSize.value.height; - } - - setoriginalXYAccordingToPosition( - originalX, - originalY, - eventX / layoutSize.value.width, - curroriginalData - ); - positionX.value = eventX; - }, - }); - - const dotStyle = useAnimatedStyle( - () => ({ - opacity: dotScale.value, - transform: [ - { translateX: positionX.value }, - { translateY: positionY.value + 10 }, // TODO temporary fix for clipped chart - { scale: dotScale.value }, - ], - }), - [] - ); - - return ( - - ); + const valuesStore = useRef(null); + if (valuesStore.current == null) { + valuesStore.current = { + currData: [], + curroriginalData: [], + dataQueue: [], + prevData: [], + }; + } + + const { + currSmoothing, + dotScale, + originalX, + originalY, + pathOpacity, + positionX, + positionY, + prevSmoothing, + progress, + layoutSize, + state, + setContextValue = () => {}, + providedData = rawData, + } = useContext(ChartContext) || generateValues(); + + const prevData = useSharedValue(valuesStore.current.prevData, "prevData"); + const currData = useSharedValue(valuesStore.current.currData, "currData"); + const curroriginalData = useSharedValue( + valuesStore.current.curroriginalData, + "curroriginalData" + ); + const hitSlopValue = useSharedValue(hitSlop); + const hapticsEnabledValue = useSharedValue(hapticsEnabled); + const [extremes, setExtremes] = useState({}); + const isAnimationInProgress = useSharedValue(false, "isAnimationInProgress"); + + const [data, setData] = useState(providedData); + const dataQueue = useSharedValue(valuesStore.current.dataQueue, "dataQueue"); + useEffect(() => { + if (isAnimationInProgress.value) { + dataQueue.value.push(providedData); + } else { + setData(providedData); + } + }, [providedData]); + + const smoothingStrategy = useSharedValue(data.smoothingStrategy); + + useEffect(() => { + let active = true; + if (!data || !data.points || data.points.length === 0) { + return; + } + const [parsedData] = parse(data.points, data.yRange); + const [parsedoriginalData, newExtremes] = parse(data.nativePoints || data.points); + setContextValue((prev) => ({ ...prev, ...newExtremes, data })); + setExtremes(newExtremes); + let timeout; + if (prevData.value.length !== 0) { + valuesStore.current.prevData = currData.value; + prevData.value = currData.value; + prevSmoothing.value = currSmoothing.value; + progress.value = 0; + valuesStore.current.currData = parsedData; + currData.value = parsedData; + valuesStore.current.curroriginalData = parsedoriginalData; + curroriginalData.value = parsedoriginalData; + currSmoothing.value = data.smoothingFactor || 0; + isAnimationInProgress.value = true; + timeout = setTimeout( + () => { + isAnimationInProgress.value = false; + if (dataQueue.value.length !== 0 && active) { + setData(dataQueue.value[0]); + dataQueue.value.shift(); + } + }, + timingAnimationConfig.duration === undefined + ? timingAnimationDefaultConfig.duration + : timingAnimationConfig.duration + ); + progress.value = withTiming( + 1, + combineConfigs(timingAnimationDefaultConfig, timingAnimationConfig) + ); + } else { + prevSmoothing.value = data.smoothing || 0; + currSmoothing.value = data.smoothing || 0; + valuesStore.current.currData = parsedData; + valuesStore.current.curroriginalData = parsedData; + prevData.value = parsedData; + currData.value = parsedData; + curroriginalData.value = parsedoriginalData; + } + return () => { + active = false; + clearTimeout(timeout); + }; + }, [data]); + + const isStarted = useSharedValue(false, "isStarted"); + + const onLongPressGestureEvent = useAnimatedGestureHandler({ + onActive: (event) => { + state.value = event.state; + if (!currData.value || currData.value.length === 0) { + return; + } + if (!isStarted.value) { + dotScale.value = withSpring(1, combineConfigs(springDefaultConfig, springConfig)); + pathOpacity.value = withTiming( + 0, + combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) + ); + } + + if (hapticsEnabledValue.value && !isStarted.value) { + impactHeavy(); + } + isStarted.value = true; + + const eventX = positionXWithMargin(event.x, hitSlopValue.value, layoutSize.value.width); + + let idx = 0; + const ss = smoothingStrategy; + for (let i = 0; i < currData.value.length; i++) { + if (getValue(currData, i, ss).x > eventX / layoutSize.value.width) { + idx = i; + break; + } + if (i === currData.value.length - 1) { + idx = currData.value.length - 1; + } + } + + if ( + ss.value === "bezier" && + currData.value.length > 30 && + eventX / layoutSize.value.width >= currData.value[currData.value.length - 2].x + ) { + const prevLastY = currData.value[currData.value.length - 2].y; + const prevLastX = currData.value[currData.value.length - 2].x; + const lastY = currData.value[currData.value.length - 1].y; + const lastX = currData.value[currData.value.length - 1].x; + const progress = + (eventX / layoutSize.value.width - prevLastX) / (lastX - prevLastX); + positionY.value = + (prevLastY + progress * (lastY - prevLastY)) * layoutSize.value.height; + } else if (idx === 0) { + positionY.value = getValue(currData, idx, ss).y * layoutSize.value.height; + } else { + // prev + diff over X + positionY.value = + (getValue(currData, idx - 1, ss).y + + (getValue(currData, idx, ss).y - getValue(currData, idx - 1, ss).y) * + ((eventX / layoutSize.value.width - getValue(currData, idx - 1, ss).x) / + (getValue(currData, idx, ss).x - + getValue(currData, idx - 1, ss).x))) * + layoutSize.value.height; + } + + setoriginalXYAccordingToPosition( + originalX, + originalY, + eventX / layoutSize.value.width, + curroriginalData + ); + positionX.value = eventX; + }, + onCancel: (event) => { + isStarted.value = false; + state.value = event.state; + originalX.value = ""; + originalY.value = ""; + dotScale.value = withSpring(0, combineConfigs(springDefaultConfig, springConfig)); + if (android) { + pathOpacity.value = 1; + } else { + pathOpacity.value = withTiming( + 1, + combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) + ); + } + }, + onEnd: (event) => { + isStarted.value = false; + state.value = event.state; + originalX.value = ""; + originalY.value = ""; + dotScale.value = withSpring(0, combineConfigs(springDefaultConfig, springConfig)); + if (android) { + pathOpacity.value = 1; + } else { + pathOpacity.value = withTiming( + 1, + combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) + ); + } + + if (hapticsEnabledValue.value) { + impactHeavy(); + } + }, + onFail: (event) => { + isStarted.value = false; + state.value = event.state; + originalX.value = ""; + originalY.value = ""; + dotScale.value = withSpring(0, combineConfigs(springDefaultConfig, springConfig)); + if (android) { + pathOpacity.value = 1; + } else { + pathOpacity.value = withTiming( + 1, + combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) + ); + } + }, + onStart: (event) => { + // WARNING: the following code does not run on using iOS, but it does on Android. + // I use the same code from onActive except of "progress.value = 1" which was taken from the original onStart. + state.value = event.state; + if (!currData.value || currData.value.length === 0) { + return; + } + if (!isStarted.value) { + dotScale.value = withSpring(1, combineConfigs(springDefaultConfig, springConfig)); + pathOpacity.value = withTiming( + 0, + combineConfigs(timingFeedbackDefaultConfig, timingFeedbackConfig) + ); + } + + if (hapticsEnabledValue.value && !isStarted.value) { + impactHeavy(); + } + isStarted.value = true; + + const eventX = positionXWithMargin(event.x, hitSlopValue.value, layoutSize.value.width); + + progress.value = 1; + let idx = 0; + const ss = smoothingStrategy; + for (let i = 0; i < currData.value.length; i++) { + if (getValue(currData, i, ss).x > eventX / layoutSize.value.width) { + idx = i; + break; + } + if (i === currData.value.length - 1) { + idx = currData.value.length - 1; + } + } + + if ( + ss.value === "bezier" && + currData.value.length > 30 && + eventX / layoutSize.value.width >= currData.value[currData.value.length - 2].x + ) { + const prevLastY = currData.value[currData.value.length - 2].y; + const prevLastX = currData.value[currData.value.length - 2].x; + const lastY = currData.value[currData.value.length - 1].y; + const lastX = currData.value[currData.value.length - 1].x; + const progress = + (eventX / layoutSize.value.width - prevLastX) / (lastX - prevLastX); + positionY.value = + (prevLastY + progress * (lastY - prevLastY)) * layoutSize.value.height; + } else if (idx === 0) { + positionY.value = getValue(currData, idx, ss).y * layoutSize.value.height; + } else { + // prev + diff over X + positionY.value = + (getValue(currData, idx - 1, ss).y + + (getValue(currData, idx, ss).y - getValue(currData, idx - 1, ss).y) * + ((eventX / layoutSize.value.width - getValue(currData, idx - 1, ss).x) / + (getValue(currData, idx, ss).x - + getValue(currData, idx - 1, ss).x))) * + layoutSize.value.height; + } + + setoriginalXYAccordingToPosition( + originalX, + originalY, + eventX / layoutSize.value.width, + curroriginalData + ); + positionX.value = eventX; + }, + }); + + const dotStyle = useAnimatedStyle( + () => ({ + opacity: dotScale.value, + transform: [ + { translateX: positionX.value }, + { translateY: positionY.value + 10 }, // TODO temporary fix for clipped chart + { scale: dotScale.value }, + ], + }), + [] + ); + + return ( + + ); } const AnimatedPath = Animated.createAnimatedComponent(Path); function ChartPath({ - smoothingWhileTransitioningEnabled, - height, - width, - longPressGestureHandlerProps, - selectedStrokeWidth = 1, - strokeWidth = 1, - gestureEnabled = true, - selectedOpacity = 0.7, - style, - onLongPressGestureEvent, - prevData, - currData, - smoothingStrategy, - prevSmoothing, - currSmoothing, - pathOpacity, - progress, - layoutSize, - __disableRendering, - children, - ...props + smoothingWhileTransitioningEnabled, + height, + width, + longPressGestureHandlerProps, + selectedStrokeWidth = 1, + strokeWidth = 1, + gestureEnabled = true, + selectedOpacity = 0.7, + style, + onLongPressGestureEvent, + prevData, + currData, + smoothingStrategy, + prevSmoothing, + currSmoothing, + pathOpacity, + progress, + layoutSize, + __disableRendering, + children, + ...props }) { - const smoothingWhileTransitioningEnabledValue = useSharedValue( - smoothingWhileTransitioningEnabled - ); - const selectedStrokeWidthValue = useSharedValue(selectedStrokeWidth); - const strokeWidthValue = useSharedValue(strokeWidth); - - useEffect(() => { - layoutSize.value = { height, width }; - }, [height, layoutSize, width]); - - const path = useDerivedValue(() => { - let fromValue = prevData.value; - let toValue = currData.value; - let res; - let smoothing = 0; - const strategy = smoothingStrategy.value; - if (progress.value !== 1) { - const numOfPoints = Math.round( - fromValue.length + - (toValue.length - fromValue.length) * - Math.min(progress.value, 0.5) * - 2 - ); - if (fromValue.length !== numOfPoints) { - const mappedFrom = []; - const coef = (fromValue.length - 1) / (numOfPoints - 1); - for (let i = 0; i < numOfPoints; i++) { - mappedFrom.push(fromValue[Math.round(i * coef)]); - } - fromValue = mappedFrom; - } - - if (toValue.length !== numOfPoints) { - const mappedTo = []; - const coef = (toValue.length - 1) / (numOfPoints - 1); - - for (let i = 0; i < numOfPoints; i++) { - mappedTo.push(toValue[Math.round(i * coef)]); - } - toValue = mappedTo; - } - - if (!smoothingWhileTransitioningEnabledValue.value) { - if (prevSmoothing.value > currSmoothing.value) { - smoothing = - prevSmoothing.value + - Math.min(progress.value * 5, 1) * - (currSmoothing.value - prevSmoothing.value); - } else { - smoothing = - prevSmoothing.value + - Math.max(Math.min((progress.value - 0.7) * 4, 1), 0) * - (currSmoothing.value - prevSmoothing.value); - } - } - - res = fromValue.map(({ x, y }, i) => { - const { x: nX, y: nY } = toValue[i]; - const mX = (x + (nX - x) * progress.value) * layoutSize.value.width; - const mY = (y + (nY - y) * progress.value) * layoutSize.value.height; - return { x: mX, y: mY }; - }); - } else { - smoothing = currSmoothing.value; - res = toValue.map(({ x, y }) => { - return { - x: x * layoutSize.value.width, - y: y * layoutSize.value.height, - }; - }); - } - - // For som reason isNaN(y) does not work - res = res.filter(({ y }) => y === Number(y)); - - if (res.length !== 0) { - const firstValue = res[0]; - const lastValue = res[res.length - 1]; - if (firstValue.x === 0 && strategy !== 'bezier') { - // extrapolate the first points - res = [ - { x: res[0].x, y: res[0].y }, - { x: -res[4].x, y: res[0].y }, - ].concat(res); - } - if (lastValue.x === layoutSize.value.width && strategy !== 'bezier') { - // extrapolate the last points - res[res.length - 1].x = lastValue.x + 20; - if (res.length > 2) { - res[res.length - 2].x = res[res.length - 2].x + 10; - } - } - } - - if ( - (smoothing !== 0 && (strategy === 'complex' || strategy === 'simple')) || - (strategy === 'bezier' && - (!smoothingWhileTransitioningEnabledValue.value || - progress.value === 1)) - ) { - return svgBezierPath(res, smoothing, strategy); - } - - return res - .map(({ x, y }) => { - return `L ${x} ${y}`; - }) - .join(' ') - .replace('L', 'M'); - }); - - const animatedProps = useAnimatedStyle(() => { - const props = { - d: path.value, - strokeWidth: - pathOpacity.value * - (Number(strokeWidthValue.value) - - Number(selectedStrokeWidthValue.value)) + - Number(selectedStrokeWidthValue.value), - }; - if (Platform.OS === 'ios') { - props.style = { - opacity: pathOpacity.value * (1 - selectedOpacity) + selectedOpacity, - }; - } - return props; - }, []); - - const animatedStyle = useAnimatedStyle(() => { - return { - opacity: pathOpacity.value * (1 - selectedOpacity) + selectedOpacity, - }; - }, undefined); - - return ( - - {__disableRendering ? children : } - - ); + const smoothingWhileTransitioningEnabledValue = useSharedValue( + smoothingWhileTransitioningEnabled + ); + const selectedStrokeWidthValue = useSharedValue(selectedStrokeWidth); + const strokeWidthValue = useSharedValue(strokeWidth); + + useEffect(() => { + layoutSize.value = { height, width }; + }, [height, layoutSize, width]); + + const path = useDerivedValue(() => { + let fromValue = prevData.value; + let toValue = currData.value; + let res; + let smoothing = 0; + const strategy = smoothingStrategy.value; + if (progress.value !== 1) { + const numOfPoints = Math.round( + fromValue.length + + (toValue.length - fromValue.length) * Math.min(progress.value, 0.5) * 2 + ); + if (fromValue.length !== numOfPoints) { + const mappedFrom = []; + const coef = (fromValue.length - 1) / (numOfPoints - 1); + for (let i = 0; i < numOfPoints; i++) { + mappedFrom.push(fromValue[Math.round(i * coef)]); + } + fromValue = mappedFrom; + } + + if (toValue.length !== numOfPoints) { + const mappedTo = []; + const coef = (toValue.length - 1) / (numOfPoints - 1); + + for (let i = 0; i < numOfPoints; i++) { + mappedTo.push(toValue[Math.round(i * coef)]); + } + toValue = mappedTo; + } + + if (!smoothingWhileTransitioningEnabledValue.value) { + if (prevSmoothing.value > currSmoothing.value) { + smoothing = + prevSmoothing.value + + Math.min(progress.value * 5, 1) * + (currSmoothing.value - prevSmoothing.value); + } else { + smoothing = + prevSmoothing.value + + Math.max(Math.min((progress.value - 0.7) * 4, 1), 0) * + (currSmoothing.value - prevSmoothing.value); + } + } + + res = fromValue.map(({ x, y }, i) => { + const { x: nX, y: nY } = toValue[i]; + const mX = (x + (nX - x) * progress.value) * layoutSize.value.width; + const mY = (y + (nY - y) * progress.value) * layoutSize.value.height; + return { x: mX, y: mY }; + }); + } else { + smoothing = currSmoothing.value; + res = toValue.map(({ x, y }) => { + return { + x: x * layoutSize.value.width, + y: y * layoutSize.value.height, + }; + }); + } + + // For som reason isNaN(y) does not work + res = res.filter(({ y }) => y === Number(y)); + + if (res.length !== 0) { + const firstValue = res[0]; + const lastValue = res[res.length - 1]; + if (firstValue.x === 0 && strategy !== "bezier") { + // extrapolate the first points + res = [ + { x: res[0].x, y: res[0].y }, + { x: -res[4].x, y: res[0].y }, + ].concat(res); + } + if (lastValue.x === layoutSize.value.width && strategy !== "bezier") { + // extrapolate the last points + res[res.length - 1].x = lastValue.x + 20; + if (res.length > 2) { + res[res.length - 2].x = res[res.length - 2].x + 10; + } + } + } + + if ( + (smoothing !== 0 && (strategy === "complex" || strategy === "simple")) || + (strategy === "bezier" && + (!smoothingWhileTransitioningEnabledValue.value || progress.value === 1)) + ) { + return svgBezierPath(res, smoothing, strategy); + } + + return res + .map(({ x, y }) => { + return `L ${x} ${y}`; + }) + .join(" ") + .replace("L", "M"); + }); + + const animatedProps = useAnimatedStyle(() => { + const props = { + d: path.value, + strokeWidth: + pathOpacity.value * + (Number(strokeWidthValue.value) - Number(selectedStrokeWidthValue.value)) + + Number(selectedStrokeWidthValue.value), + }; + if (Platform.OS === "ios") { + props.style = { + opacity: pathOpacity.value * (1 - selectedOpacity) + selectedOpacity, + }; + } + return props; + }, []); + + const animatedStyle = useAnimatedStyle(() => { + return { + opacity: pathOpacity.value * (1 - selectedOpacity) + selectedOpacity, + }; + }, undefined); + + return ( + + {__disableRendering ? children : } + + ); } export function SvgComponent() { - const { - style, - animatedStyle, - height, - width, - animatedProps, - props, - onLongPressGestureEvent, - gestureEnabled, - longPressGestureHandlerProps, - } = useContext(InternalContext); - return ( - - - - - - - - ); + const { + style, + animatedStyle, + height, + width, + animatedProps, + props, + onLongPressGestureEvent, + gestureEnabled, + longPressGestureHandlerProps, + } = useContext(InternalContext); + return ( + + + + + + + + ); } From bc76581dbc8289a29ffaf18a2c27a8f41a780752 Mon Sep 17 00:00:00 2001 From: Conor Barnes Date: Wed, 23 Jun 2021 12:11:51 -0300 Subject: [PATCH 4/4] add hack onShow method --- src/charts/linear/ChartLabels.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/charts/linear/ChartLabels.js b/src/charts/linear/ChartLabels.js index 2cbde8e..2065efe 100644 --- a/src/charts/linear/ChartLabels.js +++ b/src/charts/linear/ChartLabels.js @@ -23,13 +23,16 @@ function ChartLabelFactory(style) { return ( { + props?.onShow && props.onShow(val.value) + }} animatedProps={textProps} - defaultValue={format ? format(val.value) : val.value} + // defaultValue={format ? format(val.value) : val.value} + value={format ? format(val.value) : val.value} editable={false} /> ); - }; -} + }} -export const ChartYLabel = ChartLabelFactory('originalY'); -export const ChartXLabel = ChartLabelFactory('originalX'); + export const ChartYLabel = ChartLabelFactory('originalY'); +export const ChartXLabel = ChartLabelFactory('originalX'); \ No newline at end of file