react-native-graph-plus
Version:
📈 Beautiful, high-performance Graphs and Charts for React Native +
415 lines (369 loc) • 17.9 kB
JavaScript
"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