@coinbase/cds-mobile-visualization 3.4.0-beta.17 → 3.4.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dts/chart/Path.d.ts +35 -13
  3. package/dts/chart/Path.d.ts.map +1 -1
  4. package/dts/chart/area/Area.d.ts +7 -11
  5. package/dts/chart/area/Area.d.ts.map +1 -1
  6. package/dts/chart/area/AreaChart.d.ts +1 -1
  7. package/dts/chart/area/DottedArea.d.ts.map +1 -1
  8. package/dts/chart/area/GradientArea.d.ts.map +1 -1
  9. package/dts/chart/area/SolidArea.d.ts.map +1 -1
  10. package/dts/chart/bar/Bar.d.ts +32 -2
  11. package/dts/chart/bar/Bar.d.ts.map +1 -1
  12. package/dts/chart/bar/BarChart.d.ts +2 -0
  13. package/dts/chart/bar/BarChart.d.ts.map +1 -1
  14. package/dts/chart/bar/BarPlot.d.ts +2 -1
  15. package/dts/chart/bar/BarPlot.d.ts.map +1 -1
  16. package/dts/chart/bar/BarStack.d.ts +5 -10
  17. package/dts/chart/bar/BarStack.d.ts.map +1 -1
  18. package/dts/chart/bar/BarStackGroup.d.ts +1 -0
  19. package/dts/chart/bar/BarStackGroup.d.ts.map +1 -1
  20. package/dts/chart/bar/DefaultBar.d.ts.map +1 -1
  21. package/dts/chart/bar/DefaultBarStack.d.ts.map +1 -1
  22. package/dts/chart/line/DottedLine.d.ts.map +1 -1
  23. package/dts/chart/line/Line.d.ts +4 -9
  24. package/dts/chart/line/Line.d.ts.map +1 -1
  25. package/dts/chart/line/LineChart.d.ts +1 -1
  26. package/dts/chart/line/SolidLine.d.ts.map +1 -1
  27. package/dts/chart/point/Point.d.ts +18 -2
  28. package/dts/chart/point/Point.d.ts.map +1 -1
  29. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts +9 -1
  30. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts.map +1 -1
  31. package/dts/chart/scrubber/Scrubber.d.ts +45 -24
  32. package/dts/chart/scrubber/Scrubber.d.ts.map +1 -1
  33. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts +9 -1
  34. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts.map +1 -1
  35. package/dts/chart/utils/bar.d.ts +34 -0
  36. package/dts/chart/utils/bar.d.ts.map +1 -1
  37. package/dts/chart/utils/path.d.ts +6 -0
  38. package/dts/chart/utils/path.d.ts.map +1 -1
  39. package/dts/chart/utils/transition.d.ts +59 -21
  40. package/dts/chart/utils/transition.d.ts.map +1 -1
  41. package/esm/chart/Path.js +18 -17
  42. package/esm/chart/__stories__/CartesianChart.stories.js +3 -77
  43. package/esm/chart/__stories__/ChartTransitions.stories.js +629 -0
  44. package/esm/chart/area/Area.js +2 -0
  45. package/esm/chart/area/DottedArea.js +7 -3
  46. package/esm/chart/area/GradientArea.js +4 -2
  47. package/esm/chart/area/SolidArea.js +4 -2
  48. package/esm/chart/bar/Bar.js +2 -0
  49. package/esm/chart/bar/BarChart.js +4 -2
  50. package/esm/chart/bar/BarPlot.js +2 -0
  51. package/esm/chart/bar/BarStack.js +3 -0
  52. package/esm/chart/bar/DefaultBar.js +15 -15
  53. package/esm/chart/bar/DefaultBarStack.js +14 -3
  54. package/esm/chart/bar/__stories__/BarChart.stories.js +448 -52
  55. package/esm/chart/line/DottedLine.js +4 -2
  56. package/esm/chart/line/Line.js +6 -17
  57. package/esm/chart/line/SolidLine.js +4 -2
  58. package/esm/chart/line/__stories__/LineChart.stories.js +130 -235
  59. package/esm/chart/line/__stories__/ReferenceLine.stories.js +95 -1
  60. package/esm/chart/point/Point.js +33 -35
  61. package/esm/chart/scrubber/DefaultScrubberBeacon.js +2 -5
  62. package/esm/chart/scrubber/Scrubber.js +15 -14
  63. package/esm/chart/scrubber/ScrubberBeaconLabelGroup.js +29 -7
  64. package/esm/chart/utils/bar.js +43 -0
  65. package/esm/chart/utils/path.js +8 -0
  66. package/esm/chart/utils/transition.js +96 -61
  67. package/package.json +5 -5
  68. package/esm/chart/__stories__/Chart.stories.js +0 -77
@@ -5,7 +5,7 @@ import { useTheme } from '@coinbase/cds-mobile/hooks/useTheme';
5
5
  import { Circle, Group, interpolateColors } from '@shopify/react-native-skia';
6
6
  import { useCartesianChartContext } from '../ChartProvider';
7
7
  import { projectPoint } from '../utils';
8
- import { buildTransition, defaultTransition } from '../utils/transition';
8
+ import { buildTransition, defaultAccessoryEnterTransition, defaultTransition, getTransition } from '../utils/transition';
9
9
  import { DefaultPointLabel } from './DefaultPointLabel';
10
10
 
11
11
  /**
@@ -27,7 +27,8 @@ export const Point = /*#__PURE__*/memo(_ref => {
27
27
  labelPosition = 'center',
28
28
  labelOffset,
29
29
  labelFont,
30
- transition = defaultTransition,
30
+ transitions,
31
+ transition,
31
32
  animate: animateProp
32
33
  } = _ref;
33
34
  const theme = useTheme();
@@ -43,6 +44,8 @@ export const Point = /*#__PURE__*/memo(_ref => {
43
44
  const xScale = getXScale();
44
45
  const yScale = getYScale(yAxisId);
45
46
  const shouldAnimate = animate != null ? animate : false;
47
+ const updateTransition = useMemo(() => getTransition((transitions == null ? void 0 : transitions.update) !== undefined ? transitions.update : transition, animate, defaultTransition), [animate, transitions == null ? void 0 : transitions.update, transition]);
48
+ const enterTransition = useMemo(() => getTransition(transitions == null ? void 0 : transitions.enter, animate, defaultAccessoryEnterTransition), [animate, transitions == null ? void 0 : transitions.enter]);
46
49
 
47
50
  // Calculate pixel coordinates from data coordinates
48
51
  const pixelCoordinate = useMemo(() => {
@@ -62,9 +65,13 @@ export const Point = /*#__PURE__*/memo(_ref => {
62
65
  // Animated values for position
63
66
  const animatedX = useSharedValue(0);
64
67
  const animatedY = useSharedValue(0);
65
-
66
- // Animated value for color interpolation (0 = old color, 1 = new color)
68
+ const enterOpacity = useSharedValue(shouldAnimate ? 0 : 1);
67
69
  const colorProgress = useSharedValue(1);
70
+ const isReady = !!xScale && !!yScale;
71
+ useEffect(() => {
72
+ if (!shouldAnimate || !isReady) return;
73
+ enterOpacity.value = buildTransition(1, enterTransition);
74
+ }, [shouldAnimate, isReady, enterTransition, enterOpacity]);
68
75
 
69
76
  // Update position when coordinates change
70
77
  useEffect(() => {
@@ -72,26 +79,26 @@ export const Point = /*#__PURE__*/memo(_ref => {
72
79
  return;
73
80
  }
74
81
  if (shouldAnimate && previousPixelCoordinate) {
75
- animatedX.value = buildTransition(pixelCoordinate.x, transition);
76
- animatedY.value = buildTransition(pixelCoordinate.y, transition);
82
+ animatedX.value = buildTransition(pixelCoordinate.x, updateTransition);
83
+ animatedY.value = buildTransition(pixelCoordinate.y, updateTransition);
77
84
  } else {
78
85
  cancelAnimation(animatedX);
79
86
  cancelAnimation(animatedY);
80
87
  animatedX.value = pixelCoordinate.x;
81
88
  animatedY.value = pixelCoordinate.y;
82
89
  }
83
- }, [pixelCoordinate, shouldAnimate, previousPixelCoordinate, animatedX, animatedY, transition]);
90
+ }, [pixelCoordinate, shouldAnimate, previousPixelCoordinate, animatedX, animatedY, updateTransition]);
84
91
 
85
92
  // Update color when fill changes
86
93
  useEffect(() => {
87
94
  if (shouldAnimate && previousFill && previousFill !== fill) {
88
95
  colorProgress.value = 0;
89
- colorProgress.value = buildTransition(1, transition);
96
+ colorProgress.value = buildTransition(1, updateTransition);
90
97
  } else {
91
98
  cancelAnimation(colorProgress);
92
99
  colorProgress.value = 1;
93
100
  }
94
- }, [fill, shouldAnimate, previousFill, colorProgress, transition]);
101
+ }, [fill, shouldAnimate, previousFill, colorProgress, updateTransition]);
95
102
 
96
103
  // Create animated point for circles
97
104
  const animatedPoint = useDerivedValue(() => {
@@ -108,24 +115,19 @@ export const Point = /*#__PURE__*/memo(_ref => {
108
115
  }
109
116
  return interpolateColors(colorProgress.value, [0, 1], [previousFill, fill]);
110
117
  }, [colorProgress, previousFill, fill]);
111
-
112
- // Check if point is within drawing area
113
- const isWithinDrawingArea = useDerivedValue(() => {
114
- return animatedX.value >= drawingArea.x && animatedX.value <= drawingArea.x + drawingArea.width && animatedY.value >= drawingArea.y && animatedY.value <= drawingArea.y + drawingArea.height;
115
- }, [animatedX, animatedY, drawingArea]);
116
-
117
- // Compute effective opacity based on drawing area bounds
118
+ const isWithinDrawingArea = useMemo(() => {
119
+ if (!pixelCoordinate) return false;
120
+ return pixelCoordinate.x >= drawingArea.x && pixelCoordinate.x <= drawingArea.x + drawingArea.width && pixelCoordinate.y >= drawingArea.y && pixelCoordinate.y <= drawingArea.y + drawingArea.height;
121
+ }, [pixelCoordinate, drawingArea]);
118
122
  const effectiveOpacity = useDerivedValue(() => {
119
123
  const baseOpacity = opacity != null ? opacity : 1;
120
- return isWithinDrawingArea.value ? baseOpacity : 0;
121
- }, [isWithinDrawingArea, opacity]);
124
+ return isWithinDrawingArea ? baseOpacity * enterOpacity.value : 0;
125
+ }, [isWithinDrawingArea, opacity, enterOpacity]);
122
126
  const offset = useMemo(() => labelOffset != null ? labelOffset : radius * 2, [labelOffset, radius]);
123
127
  if (!pixelCoordinate) {
124
128
  return null;
125
129
  }
126
-
127
- // If animation is disabled or on first render, use static rendering
128
- if (!shouldAnimate || !previousPixelCoordinate) {
130
+ if (!shouldAnimate) {
129
131
  const isWithinBounds = pixelCoordinate.x >= drawingArea.x && pixelCoordinate.x <= drawingArea.x + drawingArea.width && pixelCoordinate.y >= drawingArea.y && pixelCoordinate.y <= drawingArea.y + drawingArea.height;
130
132
  const staticOpacity = isWithinBounds ? opacity != null ? opacity : 1 : 0;
131
133
  return /*#__PURE__*/_jsxs(_Fragment, {
@@ -159,20 +161,16 @@ export const Point = /*#__PURE__*/memo(_ref => {
159
161
  })]
160
162
  });
161
163
  }
162
-
163
- // Animated rendering
164
- return /*#__PURE__*/_jsxs(_Fragment, {
165
- children: [/*#__PURE__*/_jsxs(Group, {
166
- opacity: effectiveOpacity,
167
- children: [strokeWidth > 0 && /*#__PURE__*/_jsx(Circle, {
168
- c: animatedPoint,
169
- color: stroke,
170
- r: radius + strokeWidth / 2
171
- }), /*#__PURE__*/_jsx(Circle, {
172
- c: animatedPoint,
173
- color: animatedFillColor,
174
- r: radius - strokeWidth / 2
175
- })]
164
+ return /*#__PURE__*/_jsxs(Group, {
165
+ opacity: effectiveOpacity,
166
+ children: [strokeWidth > 0 && /*#__PURE__*/_jsx(Circle, {
167
+ c: animatedPoint,
168
+ color: stroke,
169
+ r: radius + strokeWidth / 2
170
+ }), /*#__PURE__*/_jsx(Circle, {
171
+ c: animatedPoint,
172
+ color: animatedFillColor,
173
+ r: radius - strokeWidth / 2
176
174
  }), label && /*#__PURE__*/_jsx(LabelComponent, {
177
175
  dataX: dataX,
178
176
  dataY: dataY,
@@ -5,7 +5,7 @@ import { Circle, Group } from '@shopify/react-native-skia';
5
5
  import { useCartesianChartContext } from '../ChartProvider';
6
6
  import { unwrapAnimatedValue } from '../utils';
7
7
  import { projectPointWithSerializableScale } from '../utils/point';
8
- import { buildTransition, defaultTransition } from '../utils/transition';
8
+ import { buildTransition, defaultTransition, getTransition } from '../utils/transition';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  const defaultRadius = 5;
11
11
  const defaultStrokeWidth = 2;
@@ -48,10 +48,7 @@ export const DefaultScrubberBeacon = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((
48
48
  var _ref2;
49
49
  return (_ref2 = colorProp != null ? colorProp : targetSeries == null ? void 0 : targetSeries.color) != null ? _ref2 : theme.color.fgPrimary;
50
50
  }, [colorProp, targetSeries == null ? void 0 : targetSeries.color, theme.color.fgPrimary]);
51
- const updateTransition = useMemo(() => {
52
- var _transitions$update;
53
- return (_transitions$update = transitions == null ? void 0 : transitions.update) != null ? _transitions$update : defaultTransition;
54
- }, [transitions == null ? void 0 : transitions.update]);
51
+ const updateTransition = useMemo(() => getTransition(transitions == null ? void 0 : transitions.update, animate, defaultTransition), [transitions == null ? void 0 : transitions.update, animate]);
55
52
  const pulseTransition = useMemo(() => {
56
53
  var _transitions$pulse;
57
54
  return (_transitions$pulse = transitions == null ? void 0 : transitions.pulse) != null ? _transitions$pulse : defaultPulseTransition;
@@ -1,10 +1,11 @@
1
1
  import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo } from 'react';
2
- import { runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue, withDelay, withTiming } from 'react-native-reanimated';
2
+ import { runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated';
3
3
  import { useTheme } from '@coinbase/cds-mobile';
4
4
  import { Group, Rect } from '@shopify/react-native-skia';
5
5
  import { useCartesianChartContext } from '../ChartProvider';
6
6
  import { ReferenceLine } from '../line';
7
- import { accessoryFadeTransitionDelay, accessoryFadeTransitionDuration, getPointOnSerializableScale, useScrubberContext } from '../utils';
7
+ import { defaultAccessoryEnterTransition, getPointOnSerializableScale, getTransition, useScrubberContext } from '../utils';
8
+ import { buildTransition } from '../utils/transition';
8
9
  import { DefaultScrubberBeacon } from './DefaultScrubberBeacon';
9
10
  import { DefaultScrubberLabel } from './DefaultScrubberLabel';
10
11
  import { ScrubberBeaconGroup } from './ScrubberBeaconGroup';
@@ -35,6 +36,7 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
35
36
  beaconLabelFont,
36
37
  idlePulse,
37
38
  beaconTransitions,
39
+ transitions = beaconTransitions,
38
40
  beaconStroke
39
41
  } = _ref;
40
42
  const theme = useTheme();
@@ -56,15 +58,6 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
56
58
  // Animation state for delayed scrubber rendering (matches web timing)
57
59
  const scrubberOpacity = useSharedValue(animate ? 0 : 1);
58
60
 
59
- // Delay scrubber appearance until after path enter animation completes
60
- useEffect(() => {
61
- if (animate) {
62
- scrubberOpacity.value = withDelay(accessoryFadeTransitionDelay, withTiming(1, {
63
- duration: accessoryFadeTransitionDuration
64
- }));
65
- }
66
- }, [animate, scrubberOpacity]);
67
-
68
61
  // Expose imperative handle with pulse method
69
62
  useImperativeHandle(ref, () => ({
70
63
  pulse: () => {
@@ -132,7 +125,14 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
132
125
  color: s.color
133
126
  }))) != null ? _series$filter$filter : [];
134
127
  }, [series, filteredSeriesIds]);
135
- if (!xScale) return;
128
+ const isReady = !!xScale;
129
+ const groupEnterTransition = useMemo(() => getTransition(transitions == null ? void 0 : transitions.enter, animate, defaultAccessoryEnterTransition), [transitions == null ? void 0 : transitions.enter, animate]);
130
+ useEffect(() => {
131
+ if (animate && isReady) {
132
+ scrubberOpacity.value = buildTransition(1, groupEnterTransition);
133
+ }
134
+ }, [animate, isReady, scrubberOpacity, groupEnterTransition]);
135
+ if (!isReady) return;
136
136
  return /*#__PURE__*/_jsxs(Group, {
137
137
  opacity: scrubberOpacity,
138
138
  children: [!hideOverlay && /*#__PURE__*/_jsx(Rect, {
@@ -158,14 +158,15 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
158
158
  idlePulse: idlePulse,
159
159
  seriesIds: filteredSeriesIds,
160
160
  stroke: beaconStroke,
161
- transitions: beaconTransitions
161
+ transitions: transitions
162
162
  }), !hideBeaconLabels && beaconLabels.length > 0 && /*#__PURE__*/_jsx(ScrubberBeaconLabelGroup, {
163
163
  BeaconLabelComponent: BeaconLabelComponent,
164
164
  labelFont: beaconLabelFont,
165
165
  labelHorizontalOffset: beaconLabelHorizontalOffset,
166
166
  labelMinGap: beaconLabelMinGap,
167
167
  labelPreferredSide: beaconLabelPreferredSide,
168
- labels: beaconLabels
168
+ labels: beaconLabels,
169
+ transitions: transitions
169
170
  })]
170
171
  });
171
172
  }));
@@ -1,9 +1,10 @@
1
1
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
2
2
  import { memo, useCallback, useMemo, useState } from 'react';
3
- import { useDerivedValue } from 'react-native-reanimated';
3
+ import { useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated';
4
4
  import { useCartesianChartContext } from '../ChartProvider';
5
- import { applySerializableScale, useScrubberContext } from '../utils';
5
+ import { applySerializableScale, unwrapAnimatedValue, useScrubberContext } from '../utils';
6
6
  import { calculateLabelYPositions, getLabelPosition } from '../utils/scrubber';
7
+ import { buildTransition, defaultTransition, getTransition } from '../utils/transition';
7
8
  import { DefaultScrubberBeaconLabel } from './DefaultScrubberBeaconLabel';
8
9
  import { jsx as _jsx } from "react/jsx-runtime";
9
10
  const PositionedLabel = /*#__PURE__*/memo(_ref => {
@@ -11,6 +12,8 @@ const PositionedLabel = /*#__PURE__*/memo(_ref => {
11
12
  index,
12
13
  positions,
13
14
  position,
15
+ isIdle,
16
+ updateTransition,
14
17
  label,
15
18
  color,
16
19
  seriesId,
@@ -24,10 +27,21 @@ const PositionedLabel = /*#__PURE__*/memo(_ref => {
24
27
  var _positions$value$inde, _positions$value$inde2;
25
28
  return (_positions$value$inde = (_positions$value$inde2 = positions.value[index]) == null ? void 0 : _positions$value$inde2.x) != null ? _positions$value$inde : 0;
26
29
  }, [positions, index]);
27
- const y = useDerivedValue(() => {
30
+ const targetY = useDerivedValue(() => {
28
31
  var _positions$value$inde3, _positions$value$inde4;
29
32
  return (_positions$value$inde3 = (_positions$value$inde4 = positions.value[index]) == null ? void 0 : _positions$value$inde4.y) != null ? _positions$value$inde3 : 0;
30
33
  }, [positions, index]);
34
+ const animatedY = useSharedValue(0);
35
+ useAnimatedReaction(() => ({
36
+ y: targetY.value,
37
+ idle: unwrapAnimatedValue(isIdle)
38
+ }), (current, previous) => {
39
+ if (previous === null || !previous.idle || !current.idle) {
40
+ animatedY.value = current.y;
41
+ } else {
42
+ animatedY.value = buildTransition(current.y, updateTransition);
43
+ }
44
+ }, [updateTransition]);
31
45
  const dx = useDerivedValue(() => {
32
46
  return position.value === 'right' ? labelHorizontalOffset : -labelHorizontalOffset;
33
47
  }, [position, labelHorizontalOffset]);
@@ -42,7 +56,7 @@ const PositionedLabel = /*#__PURE__*/memo(_ref => {
42
56
  opacity: opacity,
43
57
  seriesId: seriesId,
44
58
  x: x,
45
- y: y
59
+ y: animatedY
46
60
  });
47
61
  });
48
62
  export const ScrubberBeaconLabelGroup = /*#__PURE__*/memo(_ref2 => {
@@ -52,7 +66,8 @@ export const ScrubberBeaconLabelGroup = /*#__PURE__*/memo(_ref2 => {
52
66
  labelHorizontalOffset = 16,
53
67
  labelFont,
54
68
  labelPreferredSide = 'right',
55
- BeaconLabelComponent = DefaultScrubberBeaconLabel
69
+ BeaconLabelComponent = DefaultScrubberBeaconLabel,
70
+ transitions
56
71
  } = _ref2;
57
72
  const {
58
73
  getSeries,
@@ -61,11 +76,16 @@ export const ScrubberBeaconLabelGroup = /*#__PURE__*/memo(_ref2 => {
61
76
  getYSerializableScale,
62
77
  getXAxis,
63
78
  drawingArea,
64
- dataLength
79
+ dataLength,
80
+ animate
65
81
  } = useCartesianChartContext();
66
82
  const {
67
83
  scrubberPosition
68
84
  } = useScrubberContext();
85
+ const isIdle = useDerivedValue(() => {
86
+ return scrubberPosition.value === undefined;
87
+ }, [scrubberPosition]);
88
+ const updateTransition = useMemo(() => getTransition(transitions == null ? void 0 : transitions.update, animate, defaultTransition), [transitions == null ? void 0 : transitions.update, animate]);
69
89
  const [labelDimensions, setLabelDimensions] = useState({});
70
90
  const handleDimensionsChange = useCallback((id, dimensions) => {
71
91
  setLabelDimensions(prev => {
@@ -174,13 +194,15 @@ export const ScrubberBeaconLabelGroup = /*#__PURE__*/memo(_ref2 => {
174
194
  BeaconLabelComponent: BeaconLabelComponent,
175
195
  color: labelInfo.color,
176
196
  index: index,
197
+ isIdle: isIdle,
177
198
  label: labelInfo.label,
178
199
  labelFont: labelFont,
179
200
  labelHorizontalOffset: labelHorizontalOffset,
180
201
  onDimensionsChange: handleDimensionsChange,
181
202
  position: currentPosition,
182
203
  positions: allLabelPositions,
183
- seriesId: info.seriesId
204
+ seriesId: info.seriesId,
205
+ updateTransition: updateTransition
184
206
  }, info.seriesId);
185
207
  });
186
208
  });
@@ -1,3 +1,46 @@
1
+ const _excluded = ["staggerDelay"];
2
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
3
+ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
4
+ import { defaultTransition } from './transition';
5
+
6
+ /**
7
+ * A bar-specific transition that extends Transition with stagger support.
8
+ * When `staggerDelay` is provided, bars will animate with increasing delays
9
+ * based on their horizontal position (leftmost starts first, rightmost last).
10
+ *
11
+ * @example
12
+ * // Bars stagger in from left to right over 250ms, each animating for 750ms
13
+ * { type: 'timing', duration: 750, staggerDelay: 250 }
14
+ */
15
+
16
+ /**
17
+ * Strips `staggerDelay` from a transition and computes a positional delay.
18
+ *
19
+ * @param transition - The transition config (may include staggerDelay)
20
+ * @param normalizedX - The bar's normalized x position (0 = left edge, 1 = right edge)
21
+ * @returns A standard Transition with computed delay
22
+ */
23
+ export const withStaggerDelayTransition = (transition, normalizedX) => {
24
+ var _baseTransition$delay;
25
+ const {
26
+ staggerDelay
27
+ } = transition,
28
+ baseTransition = _objectWithoutPropertiesLoose(transition, _excluded);
29
+ if (!staggerDelay) return transition;
30
+ return _extends({}, baseTransition, {
31
+ delay: ((_baseTransition$delay = baseTransition == null ? void 0 : baseTransition.delay) != null ? _baseTransition$delay : 0) + normalizedX * staggerDelay
32
+ });
33
+ };
34
+
35
+ /**
36
+ * Default bar enter transition. Uses the default spring with a stagger delay
37
+ * so bars spring into place from left to right.
38
+ * `{ type: 'spring', stiffness: 900, damping: 120, staggerDelay: 250 }`
39
+ */
40
+ export const defaultBarEnterTransition = _extends({}, defaultTransition, {
41
+ staggerDelay: 250
42
+ });
43
+
1
44
  /**
2
45
  * Calculates the size adjustment needed for bars when accounting for gaps between them.
3
46
  * This function helps determine how much to reduce each bar's width to accommodate
@@ -1,6 +1,14 @@
1
1
  import { area as d3Area, curveBumpX, curveCatmullRom, curveLinear, curveLinearClosed, curveMonotoneX, curveNatural, curveStep, curveStepAfter, curveStepBefore, line as d3Line } from 'd3-shape';
2
2
  import { projectPoint, projectPoints } from './point';
3
3
  import { isCategoricalScale } from './scale';
4
+ /**
5
+ * Default enter transition for path-based components (Line, Area).
6
+ * `{ type: 'timing', duration: 500 }`
7
+ */
8
+ export const defaultPathEnterTransition = {
9
+ type: 'timing',
10
+ duration: 500
11
+ };
4
12
  /**
5
13
  * Get the d3 curve function for a path.
6
14
  * See https://d3js.org/d3-shape/curve
@@ -1,5 +1,7 @@
1
+ const _excluded = ["delay"];
2
+ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
1
3
  import { useEffect, useMemo, useRef } from 'react';
2
- import { useAnimatedReaction, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
4
+ import { useAnimatedReaction, useSharedValue, withDelay, withSpring, withTiming } from 'react-native-reanimated';
3
5
  import { notifyChange, Skia } from '@shopify/react-native-skia';
4
6
  import { interpolatePath } from 'd3-interpolate-path';
5
7
 
@@ -18,7 +20,8 @@ import { interpolatePath } from 'd3-interpolate-path';
18
20
  */
19
21
 
20
22
  /**
21
- * Default transition configuration used across all chart components.
23
+ * Default update transition used across all chart components.
24
+ * `{ type: 'spring', stiffness: 900, damping: 120 }`
22
25
  */
23
26
  export const defaultTransition = {
24
27
  type: 'spring',
@@ -26,6 +29,15 @@ export const defaultTransition = {
26
29
  damping: 120
27
30
  };
28
31
 
32
+ /**
33
+ * Instant transition that completes immediately with no animation.
34
+ * Used when a transition is set to `null`.
35
+ */
36
+ export const instantTransition = {
37
+ type: 'timing',
38
+ duration: 0
39
+ };
40
+
29
41
  /**
30
42
  * Duration in milliseconds for accessory elements to fade in.
31
43
  */
@@ -37,41 +49,23 @@ export const accessoryFadeTransitionDuration = 150;
37
49
  export const accessoryFadeTransitionDelay = 350;
38
50
 
39
51
  /**
40
- * Custom hook that uses d3-interpolate-path for more robust path interpolation.
41
- * then use Skia's native interpolation in the worklet.
42
- *
43
- * @param progress - Shared value between 0 and 1
44
- * @param fromPath - Starting path as SVG string
45
- * @param toPath - Ending path as SVG string
46
- * @returns Interpolated SkPath as a shared value
52
+ * Default enter transition for accessory elements (Point, Scrubber beacons).
53
+ * `{ type: 'timing', duration: 150, delay: 350 }`
47
54
  */
48
- export const useD3PathInterpolation = (progress, fromPath, toPath) => {
49
- // Pre-compute intermediate paths on JS thread using d3-interpolate-path
50
- const {
51
- fromSkiaPath,
52
- i0,
53
- i1,
54
- toSkiaPath
55
- } = useMemo(() => {
56
- var _Skia$Path$MakeFromSV, _Skia$Path$MakeFromSV2, _Skia$Path$MakeFromSV3, _Skia$Path$MakeFromSV4;
57
- const pathInterpolator = interpolatePath(fromPath, toPath);
58
- const d = 1e-3;
59
- return {
60
- fromSkiaPath: (_Skia$Path$MakeFromSV = Skia.Path.MakeFromSVGString(fromPath)) != null ? _Skia$Path$MakeFromSV : Skia.Path.Make(),
61
- i0: (_Skia$Path$MakeFromSV2 = Skia.Path.MakeFromSVGString(pathInterpolator(d))) != null ? _Skia$Path$MakeFromSV2 : Skia.Path.Make(),
62
- i1: (_Skia$Path$MakeFromSV3 = Skia.Path.MakeFromSVGString(pathInterpolator(1 - d))) != null ? _Skia$Path$MakeFromSV3 : Skia.Path.Make(),
63
- toSkiaPath: (_Skia$Path$MakeFromSV4 = Skia.Path.MakeFromSVGString(toPath)) != null ? _Skia$Path$MakeFromSV4 : Skia.Path.Make()
64
- };
65
- }, [fromPath, toPath]);
66
- const result = useSharedValue(fromSkiaPath);
67
- useAnimatedReaction(() => progress.value, t => {
68
- 'worklet';
55
+ export const defaultAccessoryEnterTransition = {
56
+ type: 'timing',
57
+ duration: accessoryFadeTransitionDuration,
58
+ delay: accessoryFadeTransitionDelay
59
+ };
69
60
 
70
- var _i1$interpolate;
71
- result.value = (_i1$interpolate = i1.interpolate(i0, t)) != null ? _i1$interpolate : toSkiaPath;
72
- notifyChange(result);
73
- }, [fromSkiaPath, i0, i1, toSkiaPath]);
74
- return result;
61
+ /**
62
+ * Resolves a transition value based on the animation state and a default.
63
+ * @note Passing in null will disable an animation.
64
+ * @note Passing in undefined will use the provided default.
65
+ */
66
+ export const getTransition = (value, animate, defaultValue) => {
67
+ if (!animate || value === null) return instantTransition;
68
+ return value != null ? value : defaultValue;
75
69
  };
76
70
 
77
71
  // Interpolator and useInterpolator are brought over from non exported code in @shopify/react-native-skia
@@ -114,21 +108,32 @@ export const useInterpolator = (factory, value, interpolator, input, output, opt
114
108
  export const buildTransition = (targetValue, transition) => {
115
109
  'worklet';
116
110
 
117
- switch (transition.type) {
111
+ const {
112
+ delay: delayMs
113
+ } = transition,
114
+ config = _objectWithoutPropertiesLoose(transition, _excluded);
115
+ let animation;
116
+ switch (config.type) {
118
117
  case 'timing':
119
118
  {
120
- return withTiming(targetValue, transition);
119
+ animation = withTiming(targetValue, config);
120
+ break;
121
121
  }
122
122
  case 'spring':
123
123
  {
124
- return withSpring(targetValue, transition);
124
+ animation = withSpring(targetValue, config);
125
+ break;
125
126
  }
126
127
  default:
127
128
  {
128
- // Fallback to default transition config
129
- return withSpring(targetValue, defaultTransition);
129
+ animation = withSpring(targetValue, defaultTransition);
130
+ break;
130
131
  }
131
132
  }
133
+ if (delayMs && delayMs > 0) {
134
+ return withDelay(delayMs, animation);
135
+ }
136
+ return animation;
132
137
  };
133
138
 
134
139
  /**
@@ -136,15 +141,16 @@ export const buildTransition = (targetValue, transition) => {
136
141
  *
137
142
  * @param currentPath - Current target path to animate to
138
143
  * @param initialPath - Initial path for enter animation. When provided, the first animation will go from initialPath to currentPath.
139
- * @param transition - Transition configuration
144
+ * @param transitions - Transition configuration for enter and update animations
140
145
  * @returns Animated SkPath as a shared value
141
146
  *
142
147
  * @example
143
148
  * // Simple path transition
144
149
  * const path = usePathTransition({
145
150
  * currentPath: d ?? '',
146
- * animate: shouldAnimate,
147
- * transition: { type: 'timing', duration: 3000 }
151
+ * transitions: {
152
+ * update: { type: 'timing', duration: 3000 },
153
+ * },
148
154
  * });
149
155
  *
150
156
  * @example
@@ -152,34 +158,63 @@ export const buildTransition = (targetValue, transition) => {
152
158
  * const path = usePathTransition({
153
159
  * currentPath: targetPath,
154
160
  * initialPath: baselinePath,
155
- * animate: true,
156
- * transition: { type: 'timing', duration: 300 }
161
+ * transitions: {
162
+ * enter: { type: 'tween', duration: 500 },
163
+ * update: { type: 'spring', stiffness: 900, damping: 120 },
164
+ * },
157
165
  * });
158
166
  */
159
167
  export const usePathTransition = _ref => {
168
+ var _transitions$update, _Skia$Path$MakeFromSV;
160
169
  let {
161
170
  currentPath,
162
171
  initialPath,
172
+ transitions,
163
173
  transition = defaultTransition
164
174
  } = _ref;
165
- // Track the previous path - updated in useEffect AFTER render,
166
- // so during render it naturally holds the "from" path value
167
- const previousPathRef = useRef(initialPath != null ? initialPath : currentPath);
175
+ const updateTransition = (_transitions$update = transitions == null ? void 0 : transitions.update) != null ? _transitions$update : transition;
176
+ const enterTransition = transitions == null ? void 0 : transitions.enter;
177
+ const targetPathRef = useRef(initialPath != null ? initialPath : currentPath);
178
+ const isFirstAnimation = useRef(!!initialPath);
179
+ const interpolatorRef = useRef(null);
168
180
  const progress = useSharedValue(0);
169
-
170
- // During render: previousPathRef still has old value, currentPath is new
171
- const fromPath = previousPathRef.current;
172
- const toPath = currentPath;
181
+ const initialSkiaPath = (_Skia$Path$MakeFromSV = Skia.Path.MakeFromSVGString(initialPath != null ? initialPath : currentPath)) != null ? _Skia$Path$MakeFromSV : Skia.Path.Make();
182
+ const normalizedStartShared = useSharedValue(initialSkiaPath);
183
+ const normalizedEndShared = useSharedValue(initialSkiaPath);
184
+ const fallbackPathShared = useSharedValue(initialSkiaPath);
185
+ const result = useSharedValue(initialSkiaPath);
173
186
  useEffect(() => {
174
- const shouldAnimate = previousPathRef.current !== currentPath;
175
- if (shouldAnimate) {
176
- // Update ref for next path change (happens after this render)
177
- previousPathRef.current = currentPath;
178
-
179
- // Animate from old path to new path
187
+ if (targetPathRef.current !== currentPath) {
188
+ var _Skia$Path$MakeFromSV2, _Skia$Path$MakeFromSV3, _Skia$Path$MakeFromSV4;
189
+ let fromPath = targetPathRef.current;
190
+ if (interpolatorRef.current) {
191
+ const p = Math.min(Math.max(progress.value, 0), 1);
192
+ fromPath = interpolatorRef.current(p);
193
+ }
194
+ targetPathRef.current = currentPath;
195
+ const pathInterpolator = interpolatePath(fromPath, currentPath);
196
+ interpolatorRef.current = pathInterpolator;
197
+ normalizedStartShared.value = (_Skia$Path$MakeFromSV2 = Skia.Path.MakeFromSVGString(pathInterpolator(0))) != null ? _Skia$Path$MakeFromSV2 : Skia.Path.Make();
198
+ normalizedEndShared.value = (_Skia$Path$MakeFromSV3 = Skia.Path.MakeFromSVGString(pathInterpolator(1))) != null ? _Skia$Path$MakeFromSV3 : Skia.Path.Make();
199
+ fallbackPathShared.value = (_Skia$Path$MakeFromSV4 = Skia.Path.MakeFromSVGString(currentPath)) != null ? _Skia$Path$MakeFromSV4 : Skia.Path.Make();
200
+ const activeTransition = isFirstAnimation.current && enterTransition !== undefined ? enterTransition : updateTransition;
201
+ isFirstAnimation.current = false;
180
202
  progress.value = 0;
181
- progress.value = buildTransition(1, transition);
203
+ progress.value = buildTransition(1, activeTransition);
182
204
  }
183
- }, [currentPath, transition, progress]);
184
- return useD3PathInterpolation(progress, fromPath, toPath);
205
+ }, [currentPath, updateTransition, enterTransition, progress, normalizedStartShared, normalizedEndShared, fallbackPathShared]);
206
+ useAnimatedReaction(() => ({
207
+ p: progress.value,
208
+ to: fallbackPathShared.value
209
+ }), _ref2 => {
210
+ 'worklet';
211
+
212
+ var _normalizedEndShared$;
213
+ let {
214
+ p
215
+ } = _ref2;
216
+ result.value = (_normalizedEndShared$ = normalizedEndShared.value.interpolate(normalizedStartShared.value, p)) != null ? _normalizedEndShared$ : fallbackPathShared.value;
217
+ notifyChange(result);
218
+ }, []);
219
+ return result;
185
220
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cds-mobile-visualization",
3
- "version": "3.4.0-beta.17",
3
+ "version": "3.4.0-beta.19",
4
4
  "description": "Coinbase Design System - Mobile Visualization Native",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,9 +36,9 @@
36
36
  "CHANGELOG"
37
37
  ],
38
38
  "peerDependencies": {
39
- "@coinbase/cds-common": "^8.41.0",
39
+ "@coinbase/cds-common": "^8.48.0",
40
40
  "@coinbase/cds-lottie-files": "^3.3.4",
41
- "@coinbase/cds-mobile": "^8.41.0",
41
+ "@coinbase/cds-mobile": "^8.48.0",
42
42
  "@coinbase/cds-utils": "^2.3.5",
43
43
  "@shopify/react-native-skia": "^1.12.4 || ^2.0.0",
44
44
  "react": "^18.3.1",
@@ -57,9 +57,9 @@
57
57
  "@babel/preset-env": "^7.28.0",
58
58
  "@babel/preset-react": "^7.27.1",
59
59
  "@babel/preset-typescript": "^7.27.1",
60
- "@coinbase/cds-common": "^8.41.0",
60
+ "@coinbase/cds-common": "^8.48.0",
61
61
  "@coinbase/cds-lottie-files": "^3.3.4",
62
- "@coinbase/cds-mobile": "^8.41.0",
62
+ "@coinbase/cds-mobile": "^8.48.0",
63
63
  "@coinbase/cds-utils": "^2.3.5",
64
64
  "@shopify/react-native-skia": "1.12.4",
65
65
  "@types/react": "^18.3.12",