UNPKG

react-native-graph-plus

Version:

📈 Beautiful, high-performance Graphs and Charts for React Native +

415 lines (369 loc) • 17.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AnimatedLineGraph = AnimatedLineGraph; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated")); var _reactNativeGestureHandler = require("react-native-gesture-handler"); var _reactNativeSkia = require("@shopify/react-native-skia"); var _SelectionDot = require("./SelectionDot"); var _CreateGraphPath = require("./CreateGraphPath"); var _getSixDigitHex = require("./utils/getSixDigitHex"); var _usePanGesture = require("./hooks/usePanGesture"); var _GetYForX = require("./GetYForX"); var _hexToRgba = require("./utils/hexToRgba"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } const INDICATOR_RADIUS = 7; const INDICATOR_BORDER_MULTIPLIER = 1.3; const INDICATOR_PULSE_BLUR_RADIUS_SMALL = INDICATOR_RADIUS * INDICATOR_BORDER_MULTIPLIER; const INDICATOR_PULSE_BLUR_RADIUS_BIG = INDICATOR_RADIUS * INDICATOR_BORDER_MULTIPLIER + 20; function AnimatedLineGraph(_ref) { let { points: allPoints, color, gradientFillColors, lineThickness = 3, range, enableFadeInMask, enablePanGesture = false, onPointSelected, onGestureStart, onGestureEnd, panGestureDelay = 300, SelectionDot = _SelectionDot.SelectionDot, enableIndicator = false, incrementPanBy, indicatorPulsating = false, horizontalPadding = enableIndicator ? Math.ceil(INDICATOR_RADIUS * INDICATOR_BORDER_MULTIPLIER) : 0, verticalPadding = lineThickness, TopAxisLabel, BottomAxisLabel, ...props } = _ref; const [width, setWidth] = (0, _react.useState)(0); const [height, setHeight] = (0, _react.useState)(0); const interpolateProgress = (0, _reactNativeSkia.useValue)(0); const { gesture, isActive, x } = (0, _usePanGesture.usePanGesture)({ enabled: enablePanGesture, holdDuration: panGestureDelay }); const circleX = (0, _reactNativeReanimated.useSharedValue)(0); const circleY = (0, _reactNativeReanimated.useSharedValue)(0); const pathEnd = (0, _reactNativeReanimated.useSharedValue)(0); const indicatorRadius = (0, _reactNativeReanimated.useSharedValue)(enableIndicator ? INDICATOR_RADIUS : 0); const indicatorBorderRadius = (0, _reactNativeReanimated.useDerivedValue)(() => indicatorRadius.value * INDICATOR_BORDER_MULTIPLIER); const pulseTrigger = (0, _reactNativeReanimated.useDerivedValue)(() => isActive.value ? 1 : 0); const indicatorPulseAnimation = (0, _reactNativeReanimated.useSharedValue)(0); const indicatorPulseRadius = (0, _reactNativeReanimated.useDerivedValue)(() => { if (pulseTrigger.value === 0) { return (0, _reactNativeSkia.mix)(indicatorPulseAnimation.value, INDICATOR_PULSE_BLUR_RADIUS_SMALL, INDICATOR_PULSE_BLUR_RADIUS_BIG); } return 0; }); const indicatorPulseOpacity = (0, _reactNativeReanimated.useDerivedValue)(() => { if (pulseTrigger.value === 0) { return (0, _reactNativeSkia.mix)(indicatorPulseAnimation.value, 1, 0); } return 0; }); const positions = (0, _reactNativeReanimated.useDerivedValue)(() => [0, Math.min(0.15, pathEnd.value), pathEnd.value, pathEnd.value, 1]); const onLayout = (0, _react.useCallback)(_ref2 => { let { nativeEvent: { layout } } = _ref2; setWidth(Math.round(layout.width)); setHeight(Math.round(layout.height)); }, []); const straightLine = (0, _react.useMemo)(() => { const path = _reactNativeSkia.Skia.Path.Make(); path.moveTo(0, height / 2); for (let i = 0; i < width - 1; i += 2) { const y = height / 2; path.cubicTo(i, y, i, y, i, y); } return path; }, [height, width]); const paths = (0, _reactNativeSkia.useValue)({}); const gradientPaths = (0, _reactNativeSkia.useValue)({}); const commands = (0, _reactNativeReanimated.useSharedValue)([]); const [commandsChanged, setCommandsChanged] = (0, _react.useState)(0); const pointSelectedIndex = (0, _react.useRef)(); const pathRange = (0, _react.useMemo)(() => (0, _CreateGraphPath.getGraphPathRange)(allPoints, range), [allPoints, range]); const pointsInRange = (0, _react.useMemo)(() => (0, _CreateGraphPath.getPointsInRange)(allPoints, pathRange), [allPoints, pathRange]); const drawingWidth = (0, _react.useMemo)(() => width - 2 * horizontalPadding, [horizontalPadding, width]); const lineWidth = (0, _react.useMemo)(() => { const lastPoint = pointsInRange[pointsInRange.length - 1]; if (lastPoint == null) return drawingWidth; return Math.max((0, _CreateGraphPath.getXInRange)(drawingWidth, lastPoint.date, pathRange.x), 0); }, [drawingWidth, pathRange.x, pointsInRange]); const indicatorX = (0, _reactNativeReanimated.useDerivedValue)(() => Math.floor(lineWidth) + horizontalPadding); const indicatorY = (0, _reactNativeReanimated.useDerivedValue)(() => (0, _GetYForX.getYForX)(commands.value, indicatorX.value) || 0); const indicatorPulseColor = (0, _react.useMemo)(() => (0, _hexToRgba.hexToRgba)(color, 0.4), [color]); const shouldFillGradient = gradientFillColors != null; (0, _react.useEffect)(() => { var _previous$to2, _from$interpolate2; if (height < 1 || width < 1) { // view is not yet measured! return; } if (pointsInRange.length < 1) { // points are still empty! return; } let path; let gradientPath; const createGraphPathProps = { pointsInRange, range: pathRange, horizontalPadding, verticalPadding, canvasHeight: height, canvasWidth: width }; if (shouldFillGradient) { const { path: pathNew, gradientPath: gradientPathNew } = (0, _CreateGraphPath.createGraphPathWithGradient)(createGraphPathProps); path = pathNew; gradientPath = gradientPathNew; } else { path = (0, _CreateGraphPath.createGraphPath)(createGraphPathProps); } commands.value = path.toCmds(); if (gradientPath != null) { var _previous$to, _from$interpolate; const previous = gradientPaths.current; let from = (_previous$to = previous.to) !== null && _previous$to !== void 0 ? _previous$to : straightLine; if (previous.from != null && interpolateProgress.current < 1) from = (_from$interpolate = from.interpolate(previous.from, interpolateProgress.current)) !== null && _from$interpolate !== void 0 ? _from$interpolate : from; if (gradientPath.isInterpolatable(from)) { gradientPaths.current = { from, to: gradientPath }; } else { gradientPaths.current = { from: gradientPath, to: gradientPath }; } } const previous = paths.current; let from = (_previous$to2 = previous.to) !== null && _previous$to2 !== void 0 ? _previous$to2 : straightLine; if (previous.from != null && interpolateProgress.current < 1) from = (_from$interpolate2 = from.interpolate(previous.from, interpolateProgress.current)) !== null && _from$interpolate2 !== void 0 ? _from$interpolate2 : from; if (path.isInterpolatable(from)) { paths.current = { from, to: path }; } else { paths.current = { from: path, to: path }; } setCommandsChanged(commandsChanged + 1); (0, _reactNativeSkia.runSpring)(interpolateProgress, { from: 0, to: 1 }, { mass: 1, stiffness: 500, damping: 400, velocity: 0 }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [height, horizontalPadding, interpolateProgress, pathRange, paths, shouldFillGradient, gradientPaths, pointsInRange, range, straightLine, verticalPadding, width]); const gradientColors = (0, _react.useMemo)(() => { if (enableFadeInMask) { return [`${(0, _getSixDigitHex.getSixDigitHex)(color)}00`, `${(0, _getSixDigitHex.getSixDigitHex)(color)}ff`, `${(0, _getSixDigitHex.getSixDigitHex)(color)}ff`, `${(0, _getSixDigitHex.getSixDigitHex)(color)}33`, `${(0, _getSixDigitHex.getSixDigitHex)(color)}33`]; } return [color, color, color, `${(0, _getSixDigitHex.getSixDigitHex)(color)}33`, `${(0, _getSixDigitHex.getSixDigitHex)(color)}33`]; }, [color, enableFadeInMask]); const path = (0, _reactNativeSkia.useComputedValue)(() => { var _paths$current$from, _paths$current$to; const from = (_paths$current$from = paths.current.from) !== null && _paths$current$from !== void 0 ? _paths$current$from : straightLine; const to = (_paths$current$to = paths.current.to) !== null && _paths$current$to !== void 0 ? _paths$current$to : straightLine; return to.interpolate(from, interpolateProgress.current); }, // RN Skia deals with deps differently. They are actually the required SkiaValues that the derived value listens to, not react values. [interpolateProgress]); const gradientPath = (0, _reactNativeSkia.useComputedValue)(() => { var _gradientPaths$curren, _gradientPaths$curren2; const from = (_gradientPaths$curren = gradientPaths.current.from) !== null && _gradientPaths$curren !== void 0 ? _gradientPaths$curren : straightLine; const to = (_gradientPaths$curren2 = gradientPaths.current.to) !== null && _gradientPaths$curren2 !== void 0 ? _gradientPaths$curren2 : straightLine; return to.interpolate(from, interpolateProgress.current); }, // RN Skia deals with deps differently. They are actually the required SkiaValues that the derived value listens to, not react values. [interpolateProgress]); const stopPulsating = (0, _react.useCallback)(() => { (0, _reactNativeReanimated.cancelAnimation)(indicatorPulseAnimation); indicatorPulseAnimation.value = 0; }, [indicatorPulseAnimation]); const startPulsating = (0, _react.useCallback)(() => { indicatorPulseAnimation.value = (0, _reactNativeReanimated.withRepeat)((0, _reactNativeReanimated.withDelay)(1000, (0, _reactNativeReanimated.withSequence)((0, _reactNativeReanimated.withTiming)(1, { duration: 1100 }), (0, _reactNativeReanimated.withTiming)(0, { duration: 0 }), // revert to 0 (0, _reactNativeReanimated.withTiming)(0, { duration: 1200 }), // delay between pulses (0, _reactNativeReanimated.withTiming)(1, { duration: 1100 }), (0, _reactNativeReanimated.withTiming)(1, { duration: 2000 }) // delay after both pulses )), -1); }, [indicatorPulseAnimation]); const setFingerPoint = (0, _react.useCallback)(fingerX => { const fingerXInRange = Math.max(fingerX - horizontalPadding, 0); const index = Math.round(fingerXInRange / (0, _CreateGraphPath.getXInRange)(drawingWidth, pointsInRange[pointsInRange.length - 1].date, pathRange.x) * (pointsInRange.length - 1)); const pointIndex = Math.min(Math.max(index, 0), pointsInRange.length - 1); if (pointSelectedIndex.current !== pointIndex) { const dataPoint = pointsInRange[pointIndex]; pointSelectedIndex.current = pointIndex; if (dataPoint != null) { onPointSelected === null || onPointSelected === void 0 ? void 0 : onPointSelected(dataPoint); } } }, [drawingWidth, horizontalPadding, onPointSelected, pathRange.x, pointsInRange]); const setFingerX = (0, _react.useCallback)(fingerX => { 'worklet'; const newFingerX = incrementPanBy ? Math.round(fingerX / incrementPanBy) * incrementPanBy : fingerX; const y = (0, _GetYForX.getYForX)(commands.value, newFingerX); if (y != null) { circleX.value = newFingerX; circleY.value = y; } if (isActive.value) pathEnd.value = fingerX / width; }, // pathRange.x must be extra included in deps otherwise onPointSelected doesn't work, IDK why // eslint-disable-next-line react-hooks/exhaustive-deps [circleX, circleY, isActive, pathEnd, pathRange.x, width, commands]); const setIsActive = (0, _react.useCallback)(active => { indicatorRadius.value = (0, _reactNativeReanimated.withSpring)(!active ? INDICATOR_RADIUS : 0, { mass: 1, stiffness: 1000, damping: 50, velocity: 0 }); if (active) { onGestureStart === null || onGestureStart === void 0 ? void 0 : onGestureStart(); stopPulsating(); } else { onGestureEnd === null || onGestureEnd === void 0 ? void 0 : onGestureEnd(); if (pointsInRange) { const dataPoint = pointsInRange[pointsInRange.length - 1]; pointSelectedIndex.current = pointsInRange.length - 1; if (dataPoint) { onPointSelected === null || onPointSelected === void 0 ? void 0 : onPointSelected(dataPoint); } } else { pointSelectedIndex.current = undefined; } pathEnd.value = 1; startPulsating(); } }, [indicatorRadius, onGestureEnd, onGestureStart, onPointSelected, pathEnd, pointsInRange, startPulsating, stopPulsating]); (0, _reactNativeReanimated.useAnimatedReaction)(() => x.value, fingerX => { if (isActive.value || fingerX) { setFingerX(fingerX); (0, _reactNativeReanimated.runOnJS)(setFingerPoint)(fingerX); } }, [isActive, setFingerX, width, x]); (0, _reactNativeReanimated.useAnimatedReaction)(() => isActive.value, active => { (0, _reactNativeReanimated.runOnJS)(setIsActive)(active); }, [isActive, setIsActive]); (0, _react.useEffect)(() => { if (pointsInRange.length !== 0 && commands.value.length !== 0) pathEnd.value = 1; }, [commands, pathEnd, pointsInRange.length]); (0, _react.useEffect)(() => { if (indicatorPulsating) { startPulsating(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [indicatorPulsating]); const axisLabelContainerStyle = { paddingTop: TopAxisLabel != null ? 20 : 0, paddingBottom: BottomAxisLabel != null ? 20 : 0 }; const indicatorVisible = enableIndicator && commandsChanged > 0; return /*#__PURE__*/_react.default.createElement(_reactNative.View, props, /*#__PURE__*/_react.default.createElement(_reactNativeGestureHandler.GestureDetector, { gesture: gesture }, /*#__PURE__*/_react.default.createElement(_reactNativeReanimated.default.View, { style: [styles.container, axisLabelContainerStyle] }, TopAxisLabel != null && /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: styles.axisRow }, /*#__PURE__*/_react.default.createElement(TopAxisLabel, null)), /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: styles.container, onLayout: onLayout }, /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Canvas, { style: styles.svg }, /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Group, null, /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Path // @ts-expect-error , { path: path, strokeWidth: lineThickness, style: "stroke", strokeJoin: "round", strokeCap: "round" }, /*#__PURE__*/_react.default.createElement(_reactNativeSkia.LinearGradient, { start: (0, _reactNativeSkia.vec)(0, 0), end: (0, _reactNativeSkia.vec)(width, 0), colors: gradientColors, positions: positions })), shouldFillGradient && /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Path // @ts-expect-error , { path: gradientPath }, /*#__PURE__*/_react.default.createElement(_reactNativeSkia.LinearGradient, { start: (0, _reactNativeSkia.vec)(0, 0), end: (0, _reactNativeSkia.vec)(0, height), colors: gradientFillColors }))), SelectionDot != null && /*#__PURE__*/_react.default.createElement(SelectionDot, { isActive: isActive, color: color, lineThickness: lineThickness, circleX: circleX, circleY: circleY }), indicatorVisible && /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Group, null, indicatorPulsating && /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Circle, { cx: indicatorX, cy: indicatorY, r: indicatorPulseRadius, opacity: indicatorPulseOpacity, color: indicatorPulseColor, style: "fill" }), /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Circle, { cx: indicatorX, cy: indicatorY, r: indicatorBorderRadius, color: "#ffffff" }, /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Shadow, { dx: 2, dy: 2, color: "rgba(0,0,0,0.2)", blur: 4 })), /*#__PURE__*/_react.default.createElement(_reactNativeSkia.Circle, { cx: indicatorX, cy: indicatorY, r: indicatorRadius, color: color })))), BottomAxisLabel != null && /*#__PURE__*/_react.default.createElement(_reactNative.View, { style: styles.axisRow }, /*#__PURE__*/_react.default.createElement(BottomAxisLabel, null))))); } const styles = _reactNative.StyleSheet.create({ svg: { flex: 1 }, container: { flex: 1 }, axisRow: { height: 17 } }); //# sourceMappingURL=AnimatedLineGraph.js.map