@coinbase/cds-mobile-visualization 3.4.0-beta.21 → 3.4.0-beta.23

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 (106) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dts/chart/CartesianChart.d.ts +39 -7
  3. package/dts/chart/CartesianChart.d.ts.map +1 -1
  4. package/dts/chart/Path.d.ts.map +1 -1
  5. package/dts/chart/PeriodSelector.d.ts +18 -6
  6. package/dts/chart/PeriodSelector.d.ts.map +1 -1
  7. package/dts/chart/area/Area.d.ts +7 -0
  8. package/dts/chart/area/Area.d.ts.map +1 -1
  9. package/dts/chart/area/AreaChart.d.ts +33 -9
  10. package/dts/chart/area/AreaChart.d.ts.map +1 -1
  11. package/dts/chart/area/DottedArea.d.ts.map +1 -1
  12. package/dts/chart/area/GradientArea.d.ts.map +1 -1
  13. package/dts/chart/area/SolidArea.d.ts.map +1 -1
  14. package/dts/chart/axis/Axis.d.ts +3 -1
  15. package/dts/chart/axis/Axis.d.ts.map +1 -1
  16. package/dts/chart/axis/XAxis.d.ts +6 -0
  17. package/dts/chart/axis/XAxis.d.ts.map +1 -1
  18. package/dts/chart/axis/YAxis.d.ts +1 -0
  19. package/dts/chart/axis/YAxis.d.ts.map +1 -1
  20. package/dts/chart/bar/Bar.d.ts +4 -2
  21. package/dts/chart/bar/Bar.d.ts.map +1 -1
  22. package/dts/chart/bar/BarChart.d.ts +49 -9
  23. package/dts/chart/bar/BarChart.d.ts.map +1 -1
  24. package/dts/chart/bar/BarPlot.d.ts.map +1 -1
  25. package/dts/chart/bar/BarStack.d.ts +30 -9
  26. package/dts/chart/bar/BarStack.d.ts.map +1 -1
  27. package/dts/chart/bar/BarStackGroup.d.ts +1 -1
  28. package/dts/chart/bar/BarStackGroup.d.ts.map +1 -1
  29. package/dts/chart/bar/DefaultBar.d.ts.map +1 -1
  30. package/dts/chart/bar/DefaultBarStack.d.ts.map +1 -1
  31. package/dts/chart/gradient/Gradient.d.ts +5 -0
  32. package/dts/chart/gradient/Gradient.d.ts.map +1 -1
  33. package/dts/chart/line/DottedLine.d.ts.map +1 -1
  34. package/dts/chart/line/Line.d.ts +7 -0
  35. package/dts/chart/line/Line.d.ts.map +1 -1
  36. package/dts/chart/line/LineChart.d.ts +8 -5
  37. package/dts/chart/line/LineChart.d.ts.map +1 -1
  38. package/dts/chart/line/ReferenceLine.d.ts +1 -0
  39. package/dts/chart/line/ReferenceLine.d.ts.map +1 -1
  40. package/dts/chart/line/SolidLine.d.ts.map +1 -1
  41. package/dts/chart/point/Point.d.ts +7 -0
  42. package/dts/chart/point/Point.d.ts.map +1 -1
  43. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts.map +1 -1
  44. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts +2 -1
  45. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts.map +1 -1
  46. package/dts/chart/scrubber/Scrubber.d.ts +8 -0
  47. package/dts/chart/scrubber/Scrubber.d.ts.map +1 -1
  48. package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts.map +1 -1
  49. package/dts/chart/scrubber/ScrubberProvider.d.ts.map +1 -1
  50. package/dts/chart/utils/axis.d.ts +20 -9
  51. package/dts/chart/utils/axis.d.ts.map +1 -1
  52. package/dts/chart/utils/bar.d.ts +6 -5
  53. package/dts/chart/utils/bar.d.ts.map +1 -1
  54. package/dts/chart/utils/chart.d.ts +13 -0
  55. package/dts/chart/utils/chart.d.ts.map +1 -1
  56. package/dts/chart/utils/context.d.ts +21 -6
  57. package/dts/chart/utils/context.d.ts.map +1 -1
  58. package/dts/chart/utils/gradient.d.ts +3 -1
  59. package/dts/chart/utils/gradient.d.ts.map +1 -1
  60. package/dts/chart/utils/path.d.ts +20 -0
  61. package/dts/chart/utils/path.d.ts.map +1 -1
  62. package/dts/chart/utils/point.d.ts +7 -0
  63. package/dts/chart/utils/point.d.ts.map +1 -1
  64. package/dts/chart/utils/transition.d.ts +7 -4
  65. package/dts/chart/utils/transition.d.ts.map +1 -1
  66. package/esm/chart/CartesianChart.js +107 -68
  67. package/esm/chart/Path.js +18 -14
  68. package/esm/chart/__stories__/ChartTransitions.stories.js +6 -10
  69. package/esm/chart/area/Area.js +19 -9
  70. package/esm/chart/area/AreaChart.js +18 -13
  71. package/esm/chart/area/DottedArea.js +23 -17
  72. package/esm/chart/area/GradientArea.js +11 -6
  73. package/esm/chart/area/SolidArea.js +3 -1
  74. package/esm/chart/area/__stories__/AreaChart.stories.js +30 -2
  75. package/esm/chart/axis/XAxis.js +14 -21
  76. package/esm/chart/axis/YAxis.js +4 -3
  77. package/esm/chart/bar/Bar.js +9 -5
  78. package/esm/chart/bar/BarChart.js +34 -31
  79. package/esm/chart/bar/BarPlot.js +7 -5
  80. package/esm/chart/bar/BarStack.js +176 -36
  81. package/esm/chart/bar/BarStackGroup.js +37 -27
  82. package/esm/chart/bar/DefaultBar.js +24 -8
  83. package/esm/chart/bar/DefaultBarStack.js +24 -10
  84. package/esm/chart/bar/__stories__/BarChart.stories.js +99 -3
  85. package/esm/chart/gradient/Gradient.js +2 -1
  86. package/esm/chart/line/DottedLine.js +3 -1
  87. package/esm/chart/line/Line.js +36 -21
  88. package/esm/chart/line/LineChart.js +13 -11
  89. package/esm/chart/line/SolidLine.js +3 -1
  90. package/esm/chart/line/__stories__/LineChart.stories.js +31 -0
  91. package/esm/chart/point/Point.js +2 -1
  92. package/esm/chart/scrubber/DefaultScrubberBeacon.js +1 -1
  93. package/esm/chart/scrubber/DefaultScrubberLabel.js +26 -10
  94. package/esm/chart/scrubber/Scrubber.js +47 -21
  95. package/esm/chart/scrubber/ScrubberBeaconGroup.js +24 -20
  96. package/esm/chart/scrubber/ScrubberProvider.js +29 -24
  97. package/esm/chart/scrubber/__stories__/Scrubber.stories.js +135 -1
  98. package/esm/chart/utils/axis.js +42 -14
  99. package/esm/chart/utils/bar.js +6 -4
  100. package/esm/chart/utils/chart.js +18 -5
  101. package/esm/chart/utils/context.js +7 -0
  102. package/esm/chart/utils/gradient.js +8 -4
  103. package/esm/chart/utils/path.js +90 -61
  104. package/esm/chart/utils/point.js +28 -18
  105. package/esm/chart/utils/transition.js +28 -10
  106. package/package.json +5 -5
@@ -6,7 +6,8 @@ import { defaultTransition } from './transition';
6
6
  /**
7
7
  * A bar-specific transition that extends Transition with stagger support.
8
8
  * When `staggerDelay` is provided, bars will animate with increasing delays
9
- * based on their horizontal position (leftmost starts first, rightmost last).
9
+ * based on their position along the category axis (vertical: left-to-right,
10
+ * horizontal: top-to-bottom).
10
11
  *
11
12
  * @example
12
13
  * // Bars stagger in from left to right over 250ms, each animating for 750ms
@@ -17,18 +18,19 @@ import { defaultTransition } from './transition';
17
18
  * Strips `staggerDelay` from a transition and computes a positional delay.
18
19
  *
19
20
  * @param transition - The transition config (may include staggerDelay)
20
- * @param normalizedX - The bar's normalized x position (0 = left edge, 1 = right edge)
21
+ * @param normalizedPosition - The bar's normalized position along the category axis (0–1)
21
22
  * @returns A standard Transition with computed delay
22
23
  */
23
- export const withStaggerDelayTransition = (transition, normalizedX) => {
24
+ export const withStaggerDelayTransition = (transition, normalizedPosition) => {
24
25
  var _baseTransition$delay;
26
+ if (!transition) return null;
25
27
  const {
26
28
  staggerDelay
27
29
  } = transition,
28
30
  baseTransition = _objectWithoutPropertiesLoose(transition, _excluded);
29
31
  if (!staggerDelay) return transition;
30
32
  return _extends({}, baseTransition, {
31
- delay: ((_baseTransition$delay = baseTransition == null ? void 0 : baseTransition.delay) != null ? _baseTransition$delay : 0) + normalizedX * staggerDelay
33
+ delay: ((_baseTransition$delay = baseTransition == null ? void 0 : baseTransition.delay) != null ? _baseTransition$delay : 0) + normalizedPosition * staggerDelay
32
34
  });
33
35
  };
34
36
 
@@ -46,15 +46,16 @@ export const getChartDomain = (series, min, max) => {
46
46
  };
47
47
 
48
48
  /**
49
- * Creates a composite stack key that includes both stack ID and y-axis ID.
50
- * This ensures series with different y-scales don't get stacked together.
49
+ * Creates a composite stack key that includes stack ID and axis IDs.
50
+ * This ensures series with different scales don't get stacked together.
51
51
  */
52
52
  const createStackKey = series => {
53
53
  if (series.stackId === undefined) return undefined;
54
54
 
55
- // Include y-axis ID to prevent cross-scale stacking
55
+ // Include axis IDs to prevent cross-scale stacking
56
+ const xAxisId = series.xAxisId || 'default';
56
57
  const yAxisId = series.yAxisId || 'default';
57
- return series.stackId + ":" + yAxisId;
58
+ return series.stackId + ":" + xAxisId + ":" + yAxisId;
58
59
  };
59
60
 
60
61
  /**
@@ -225,12 +226,24 @@ export const getChartRange = (series, min, max) => {
225
226
  }
226
227
  return range;
227
228
  };
228
- export const defaultChartInset = {
229
+ export const defaultVerticalLayoutChartInset = {
229
230
  top: 32,
230
231
  left: 16,
231
232
  bottom: 16,
232
233
  right: 16
233
234
  };
235
+ export const defaultHorizontalLayoutChartInset = {
236
+ top: 16,
237
+ left: 16,
238
+ bottom: 16,
239
+ right: 48
240
+ };
241
+
242
+ /**
243
+ * @deprecated Use `defaultVerticalLayoutChartInset` for vertical layout charts or
244
+ * `defaultHorizontalLayoutChartInset` for horizontal layout charts.
245
+ */
246
+ export const defaultChartInset = defaultVerticalLayoutChartInset;
234
247
 
235
248
  /**
236
249
  * Normalize padding to include all sides with a value.
@@ -1,5 +1,12 @@
1
1
  import { createContext, useContext } from 'react';
2
2
 
3
+ /**
4
+ * Chart layout for Cartesian charts.
5
+ * Describes the direction bars/areas grow.
6
+ * - 'vertical': Bars grow vertically (up/down). X is category axis, Y is value axis.
7
+ * - 'horizontal': Bars grow horizontally (left/right). Y is category axis, X is value axis.
8
+ */
9
+
3
10
  /**
4
11
  * Context value for Cartesian (X/Y) coordinate charts.
5
12
  * Contains axis-specific methods and properties for rectangular coordinate systems.
@@ -262,9 +262,13 @@ export const getBaseline = function (axisBounds, baseline) {
262
262
  * @param fill - The color to use for the gradient
263
263
  * @param peakOpacity - Opacity at the peak of the gradient
264
264
  * @param baselineOpacity - Opacity at the baseline
265
- * @returns A gradient definition with y-axis stops in ascending order
265
+ * @param axis - The axis the gradient maps to ('y' for vertical, 'x' for horizontal layout)
266
+ * @returns A gradient definition with stops in ascending order
266
267
  */
267
- export const createGradient = (axisBounds, baselineValue, fill, peakOpacity, baselineOpacity) => {
268
+ export const createGradient = function (axisBounds, baselineValue, fill, peakOpacity, baselineOpacity, axis) {
269
+ if (axis === void 0) {
270
+ axis = 'y';
271
+ }
268
272
  const {
269
273
  min,
270
274
  max
@@ -273,7 +277,7 @@ export const createGradient = (axisBounds, baselineValue, fill, peakOpacity, bas
273
277
  const upperBound = Math.max(min, max);
274
278
  if (lowerBound < baselineValue && baselineValue < upperBound) {
275
279
  return {
276
- axis: 'y',
280
+ axis,
277
281
  stops: [{
278
282
  offset: lowerBound,
279
283
  color: fill,
@@ -291,7 +295,7 @@ export const createGradient = (axisBounds, baselineValue, fill, peakOpacity, bas
291
295
  }
292
296
  const peakValue = Math.abs(min - baselineValue) > Math.abs(max - baselineValue) ? min : max;
293
297
  return {
294
- axis: 'y',
298
+ axis,
295
299
  stops: [{
296
300
  offset: peakValue,
297
301
  color: fill,
@@ -1,5 +1,5 @@
1
- import { area as d3Area, curveBumpX, curveCatmullRom, curveLinear, curveLinearClosed, curveMonotoneX, curveNatural, curveStep, curveStepAfter, curveStepBefore, line as d3Line } from 'd3-shape';
2
- import { projectPoint, projectPoints } from './point';
1
+ import { area as d3Area, curveBumpX, curveBumpY, curveCatmullRom, curveLinear, curveLinearClosed, curveMonotoneX, curveMonotoneY, curveNatural, curveStep, curveStepAfter, curveStepBefore, line as d3Line } from 'd3-shape';
2
+ import { getPointOnScale, projectPoints } from './point';
3
3
  import { isCategoricalScale } from './scale';
4
4
  /**
5
5
  * Default enter transition for path-based components (Line, Area).
@@ -13,18 +13,23 @@ export const defaultPathEnterTransition = {
13
13
  * Get the d3 curve function for a path.
14
14
  * See https://d3js.org/d3-shape/curve
15
15
  * @param curve - The curve type. Defaults to 'linear'.
16
+ * @param layout - The chart layout. Defaults to 'vertical'.
16
17
  * @returns The d3 curve function.
17
18
  */
18
- export const getPathCurveFunction = function (curve) {
19
+ export const getPathCurveFunction = function (curve, layout) {
19
20
  if (curve === void 0) {
20
21
  curve = 'linear';
21
22
  }
23
+ if (layout === void 0) {
24
+ layout = 'vertical';
25
+ }
22
26
  switch (curve) {
23
27
  case 'catmullRom':
24
28
  return curveCatmullRom;
25
29
  case 'monotone':
26
- // When we support layout="vertical" this should dynamically switch to curveMonotoneY
27
- return curveMonotoneX;
30
+ // For vertical layout, X is the independent axis (category/index), so use MonotoneX.
31
+ // For horizontal layout, Y is the independent axis (category/index), so use MonotoneY.
32
+ return layout !== 'horizontal' ? curveMonotoneX : curveMonotoneY;
28
33
  case 'natural':
29
34
  return curveNatural;
30
35
  case 'step':
@@ -34,8 +39,9 @@ export const getPathCurveFunction = function (curve) {
34
39
  case 'stepAfter':
35
40
  return curveStepAfter;
36
41
  case 'bump':
37
- // When we support layout="vertical" this should dynamically switch to curveBumpY
38
- return curveBumpX;
42
+ // For vertical layout, X is the independent axis (category/index), so use BumpX.
43
+ // For horizontal layout, Y is the independent axis (category/index), so use BumpY.
44
+ return layout !== 'horizontal' ? curveBumpX : curveBumpY;
39
45
  case 'linearClosed':
40
46
  return curveLinearClosed;
41
47
  case 'linear':
@@ -61,17 +67,21 @@ export const getLinePath = _ref => {
61
67
  xScale,
62
68
  yScale,
63
69
  xData,
64
- connectNulls = false
70
+ yData,
71
+ connectNulls = false,
72
+ layout = 'vertical'
65
73
  } = _ref;
66
74
  if (data.length === 0) {
67
75
  return '';
68
76
  }
69
- const curveFunction = getPathCurveFunction(curve);
77
+ const curveFunction = getPathCurveFunction(curve, layout);
70
78
  const dataPoints = projectPoints({
71
79
  data,
72
80
  xScale,
73
81
  yScale,
74
- xData
82
+ xData,
83
+ yData,
84
+ layout
75
85
  });
76
86
 
77
87
  // When connectNulls is true, filter out null values before rendering
@@ -110,14 +120,20 @@ export const getAreaPath = _ref2 => {
110
120
  xScale,
111
121
  yScale,
112
122
  xData,
113
- connectNulls = false
123
+ yData,
124
+ connectNulls = false,
125
+ layout = 'vertical'
114
126
  } = _ref2;
115
127
  if (data.length === 0) {
116
128
  return '';
117
129
  }
118
- const curveFunction = getPathCurveFunction(curve);
119
- const yDomain = yScale.domain();
120
- const yMin = Math.min(...yDomain);
130
+ const curveFunction = getPathCurveFunction(curve, layout);
131
+ const categoryAxisIsX = layout !== 'horizontal';
132
+
133
+ // Determine baseline from the value scale.
134
+ const valueScale = categoryAxisIsX ? yScale : xScale;
135
+ const domain = valueScale.domain();
136
+ const min = Math.min(...domain);
121
137
  const normalizedData = data.map((item, index) => {
122
138
  if (item === null) {
123
139
  return null;
@@ -129,7 +145,7 @@ export const getAreaPath = _ref2 => {
129
145
  return null;
130
146
  }
131
147
  if (typeof item === 'number') {
132
- return [yMin, item];
148
+ return [min, item];
133
149
  }
134
150
  return null;
135
151
  });
@@ -137,37 +153,28 @@ export const getAreaPath = _ref2 => {
137
153
  if (range === null) {
138
154
  return {
139
155
  x: 0,
156
+ y: 0,
140
157
  low: null,
141
158
  high: null,
142
159
  isValid: false
143
160
  };
144
161
  }
145
- let xValue = index;
146
- if (!isCategoricalScale(xScale) && xData && xData[index] !== undefined) {
147
- xValue = xData[index];
162
+
163
+ // Determine the position along the independent (index/category) axis.
164
+ let indexValue = index;
165
+ const indexScale = categoryAxisIsX ? xScale : yScale;
166
+ const indexData = categoryAxisIsX ? xData : yData;
167
+ if (!isCategoricalScale(indexScale) && indexData && indexData[index] !== undefined) {
168
+ indexValue = indexData[index];
148
169
  }
149
- const xPoint = projectPoint({
150
- x: xValue,
151
- y: 0,
152
- xScale,
153
- yScale
154
- });
155
- const lowPoint = projectPoint({
156
- x: xValue,
157
- y: range[0],
158
- xScale,
159
- yScale
160
- });
161
- const highPoint = projectPoint({
162
- x: xValue,
163
- y: range[1],
164
- xScale,
165
- yScale
166
- });
170
+ const position = getPointOnScale(indexValue, indexScale);
171
+ const low = getPointOnScale(range[0], valueScale);
172
+ const high = getPointOnScale(range[1], valueScale);
167
173
  return {
168
- x: xPoint.x,
169
- low: lowPoint.y,
170
- high: highPoint.y,
174
+ x: categoryAxisIsX ? position : 0,
175
+ y: !categoryAxisIsX ? position : 0,
176
+ low,
177
+ high,
171
178
  isValid: true
172
179
  };
173
180
  });
@@ -175,16 +182,25 @@ export const getAreaPath = _ref2 => {
175
182
  // When connectNulls is true, filter out invalid points before rendering
176
183
  // When false, use defined() to create gaps in the area
177
184
  const filteredPoints = connectNulls ? dataPoints.filter(d => d.isValid) : dataPoints;
178
- const areaGenerator = d3Area().x(d => d.x).y0(d => {
179
- var _d$low;
180
- return (_d$low = d.low) != null ? _d$low : 0;
181
- }) // Bottom boundary (low values), fallback to 0
182
- .y1(d => {
183
- var _d$high;
184
- return (_d$high = d.high) != null ? _d$high : 0;
185
- }) // Top boundary (high values), fallback to 0
186
- .curve(curveFunction).defined(d => connectNulls || d.isValid && d.low != null && d.high != null); // Only draw where both values exist
187
-
185
+ const areaGenerator = d3Area();
186
+ if (categoryAxisIsX) {
187
+ areaGenerator.x(d => d.x).y0(d => {
188
+ var _d$low;
189
+ return (_d$low = d.low) != null ? _d$low : 0;
190
+ }).y1(d => {
191
+ var _d$high;
192
+ return (_d$high = d.high) != null ? _d$high : 0;
193
+ });
194
+ } else {
195
+ areaGenerator.y(d => d.y).x0(d => {
196
+ var _d$low2;
197
+ return (_d$low2 = d.low) != null ? _d$low2 : 0;
198
+ }).x1(d => {
199
+ var _d$high2;
200
+ return (_d$high2 = d.high) != null ? _d$high2 : 0;
201
+ });
202
+ }
203
+ areaGenerator.curve(curveFunction).defined(d => connectNulls || d.isValid && d.low != null && d.high != null);
188
204
  const result = areaGenerator(filteredPoints);
189
205
  return result != null ? result : '';
190
206
  };
@@ -216,22 +232,35 @@ export const lineToPath = (x1, y1, x2, y2) => {
216
232
  * const roundedPath = getBarPath(10, 20, 50, 100, 8, true, false);
217
233
  * ```
218
234
  */
219
- export const getBarPath = (x, y, width, height, radius, roundTop, roundBottom) => {
235
+ export const getBarPath = function (x, y, width, height, radius, roundTop, roundBottom, layout) {
236
+ if (layout === void 0) {
237
+ layout = 'vertical';
238
+ }
239
+ const barsGrowVertically = layout !== 'horizontal';
220
240
  const roundBothSides = roundTop && roundBottom;
221
241
  const r = Math.min(radius, width / 2, roundBothSides ? height / 2 : height);
222
- const topR = roundTop ? r : 0;
223
- const bottomR = roundBottom ? r : 0;
242
+
243
+ // In vertical layout (bars grow up/down):
244
+ // - roundTop rounds the top face (min Y)
245
+ // - roundBottom rounds the bottom face (max Y)
246
+ // In horizontal layout (bars grow left/right):
247
+ // - roundTop rounds the right face (max X)
248
+ // - roundBottom rounds the left face (min X)
249
+ const rTL = barsGrowVertically ? roundTop ? r : 0 : roundBottom ? r : 0;
250
+ const rTR = barsGrowVertically ? roundTop ? r : 0 : roundTop ? r : 0;
251
+ const rBR = barsGrowVertically ? roundBottom ? r : 0 : roundTop ? r : 0;
252
+ const rBL = barsGrowVertically ? roundBottom ? r : 0 : roundBottom ? r : 0;
224
253
 
225
254
  // Build path with selective rounding
226
- let path = "M " + (x + (roundTop ? r : 0)) + " " + y;
227
- path += " L " + (x + width - topR) + " " + y;
228
- path += " A " + topR + " " + topR + " 0 0 1 " + (x + width) + " " + (y + topR);
229
- path += " L " + (x + width) + " " + (y + height - bottomR);
230
- path += " A " + bottomR + " " + bottomR + " 0 0 1 " + (x + width - bottomR) + " " + (y + height);
231
- path += " L " + (x + bottomR) + " " + (y + height);
232
- path += " A " + bottomR + " " + bottomR + " 0 0 1 " + x + " " + (y + height - bottomR);
233
- path += " L " + x + " " + (y + topR);
234
- path += " A " + topR + " " + topR + " 0 0 1 " + (x + topR) + " " + y;
255
+ let path = "M " + (x + rTL) + " " + y;
256
+ path += " L " + (x + width - rTR) + " " + y;
257
+ path += " A " + rTR + " " + rTR + " 0 0 1 " + (x + width) + " " + (y + rTR);
258
+ path += " L " + (x + width) + " " + (y + height - rBR);
259
+ path += " A " + rBR + " " + rBR + " 0 0 1 " + (x + width - rBR) + " " + (y + height);
260
+ path += " L " + (x + rBL) + " " + (y + height);
261
+ path += " A " + rBL + " " + rBL + " 0 0 1 " + x + " " + (y + height - rBL);
262
+ path += " L " + x + " " + (y + rTL);
263
+ path += " A " + rTL + " " + rTL + " 0 0 1 " + (x + rTL) + " " + y;
235
264
  path += ' Z';
236
265
  return path;
237
266
  };
@@ -1,4 +1,4 @@
1
- import { applyBandScale, applySerializableScale, isCategoricalScale, isLogScale, isNumericScale } from './scale';
1
+ import { applyBandScale, applySerializableScale, isCategoricalScale, isLogScale } from './scale';
2
2
 
3
3
  /**
4
4
  * Position a label should be placed relative to the point
@@ -164,7 +164,8 @@ export const projectPoints = _ref3 => {
164
164
  xScale,
165
165
  yScale,
166
166
  xData,
167
- yData
167
+ yData,
168
+ layout = 'vertical'
168
169
  } = _ref3;
169
170
  if (data.length === 0) {
170
171
  return [];
@@ -182,28 +183,37 @@ export const projectPoints = _ref3 => {
182
183
  });
183
184
  }
184
185
 
185
- // For scales with axis data, determine the correct x value
186
- let xValue = index;
186
+ // Determine values/scales based on role (index vs value) and layout.
187
+ const categoryAxisIsX = layout !== 'horizontal';
188
+ const indexScale = categoryAxisIsX ? xScale : yScale;
189
+ const indexData = categoryAxisIsX ? xData : yData;
187
190
 
188
- // For band scales, always use the index
189
- if (!isCategoricalScale(xScale)) {
190
- // For numeric scales with axis data, use the axis data values instead of indices
191
- if (xData && Array.isArray(xData) && xData.length > 0) {
192
- // Check if it's numeric data
193
- if (typeof xData[0] === 'number') {
194
- var _numericXData$index;
195
- const numericXData = xData;
196
- xValue = (_numericXData$index = numericXData[index]) != null ? _numericXData$index : index;
191
+ // 1. Calculate position along the index axis (categorical or numeric domain).
192
+ let indexValue = index;
193
+ if (!isCategoricalScale(indexScale)) {
194
+ if (indexData && Array.isArray(indexData) && indexData.length > 0) {
195
+ if (typeof indexData[0] === 'number') {
196
+ var _indexData$index;
197
+ indexValue = (_indexData$index = indexData[index]) != null ? _indexData$index : index;
197
198
  }
198
199
  }
199
200
  }
200
- let yValue = value;
201
- if (isNumericScale(yScale) && yData && Array.isArray(yData) && yData.length > 0 && typeof yData[0] === 'number' && typeof value === 'number') {
202
- yValue = value;
201
+
202
+ // 2. Calculate position along the value axis (measured magnitude).
203
+ const valueAsNumber = value;
204
+
205
+ // 3. Project final coordinates based on layout.
206
+ if (categoryAxisIsX) {
207
+ return projectPoint({
208
+ x: indexValue,
209
+ y: valueAsNumber,
210
+ xScale,
211
+ yScale
212
+ });
203
213
  }
204
214
  return projectPoint({
205
- x: xValue,
206
- y: yValue,
215
+ x: valueAsNumber,
216
+ y: indexValue,
207
217
  xScale,
208
218
  yScale
209
219
  });
@@ -56,13 +56,18 @@ export const defaultAccessoryEnterTransition = {
56
56
  delay: accessoryFadeTransitionDelay
57
57
  };
58
58
 
59
+ // Avoid exact endpoint samples, which can intermittently produce non-interpolatable
60
+ // path pairs for SkPath.interpolate on complex morphs.
61
+ // See https://github.com/wcandillon/can-it-be-done-in-react-native/blob/db8d6ee7024e37e8f8d2cb237c0b953b5fc766fe/season5/src/Headspace/Play.tsx
62
+ const pathInterpolationEpsilon = 1e-3;
63
+
59
64
  /**
60
65
  * Resolves a transition value based on the animation state and a default.
61
66
  * @note Passing in null will disable an animation.
62
67
  * @note Passing in undefined will use the provided default.
63
68
  */
64
69
  export const getTransition = (value, animate, defaultValue) => {
65
- if (!animate || value === null) return instantTransition;
70
+ if (!animate || value === null) return null;
66
71
  return value != null ? value : defaultValue;
67
72
  };
68
73
 
@@ -106,6 +111,7 @@ export const useInterpolator = (factory, value, interpolator, input, output, opt
106
111
  export const buildTransition = (targetValue, transition) => {
107
112
  'worklet';
108
113
 
114
+ if (transition === null) return targetValue;
109
115
  const delayMs = transition.delay;
110
116
  let animation;
111
117
  switch (transition.type) {
@@ -160,14 +166,14 @@ export const buildTransition = (targetValue, transition) => {
160
166
  * });
161
167
  */
162
168
  export const usePathTransition = _ref => {
163
- var _transitions$update, _Skia$Path$MakeFromSV;
169
+ var _Skia$Path$MakeFromSV;
164
170
  let {
165
171
  currentPath,
166
172
  initialPath,
167
173
  transitions,
168
174
  transition = defaultTransition
169
175
  } = _ref;
170
- const updateTransition = (_transitions$update = transitions == null ? void 0 : transitions.update) != null ? _transitions$update : transition;
176
+ const updateTransition = (transitions == null ? void 0 : transitions.update) !== undefined ? transitions.update : transition;
171
177
  const enterTransition = transitions == null ? void 0 : transitions.enter;
172
178
  const targetPathRef = useRef(initialPath != null ? initialPath : currentPath);
173
179
  const isFirstAnimation = useRef(!!initialPath);
@@ -180,24 +186,36 @@ export const usePathTransition = _ref => {
180
186
  const result = useSharedValue(initialSkiaPath);
181
187
  useEffect(() => {
182
188
  if (targetPathRef.current !== currentPath) {
183
- var _Skia$Path$MakeFromSV2, _Skia$Path$MakeFromSV3, _Skia$Path$MakeFromSV4;
189
+ var _Skia$Path$MakeFromSV3, _Skia$Path$MakeFromSV4, _Skia$Path$MakeFromSV5;
184
190
  let fromPath = targetPathRef.current;
185
191
  if (interpolatorRef.current) {
186
192
  const p = Math.min(Math.max(progress.value, 0), 1);
187
193
  fromPath = interpolatorRef.current(p);
188
194
  }
189
195
  targetPathRef.current = currentPath;
190
- const pathInterpolator = interpolatePath(fromPath, currentPath);
191
- interpolatorRef.current = pathInterpolator;
192
- normalizedStartShared.value = (_Skia$Path$MakeFromSV2 = Skia.Path.MakeFromSVGString(pathInterpolator(0))) != null ? _Skia$Path$MakeFromSV2 : Skia.Path.Make();
193
- normalizedEndShared.value = (_Skia$Path$MakeFromSV3 = Skia.Path.MakeFromSVGString(pathInterpolator(1))) != null ? _Skia$Path$MakeFromSV3 : Skia.Path.Make();
194
- fallbackPathShared.value = (_Skia$Path$MakeFromSV4 = Skia.Path.MakeFromSVGString(currentPath)) != null ? _Skia$Path$MakeFromSV4 : Skia.Path.Make();
195
196
  const activeTransition = isFirstAnimation.current && enterTransition !== undefined ? enterTransition : updateTransition;
196
197
  isFirstAnimation.current = false;
198
+ if (activeTransition === null) {
199
+ var _Skia$Path$MakeFromSV2;
200
+ const targetPath = (_Skia$Path$MakeFromSV2 = Skia.Path.MakeFromSVGString(currentPath)) != null ? _Skia$Path$MakeFromSV2 : Skia.Path.Make();
201
+ interpolatorRef.current = null;
202
+ normalizedStartShared.value = targetPath;
203
+ normalizedEndShared.value = targetPath;
204
+ fallbackPathShared.value = targetPath;
205
+ progress.value = 1;
206
+ result.value = targetPath;
207
+ notifyChange(result);
208
+ return;
209
+ }
210
+ const pathInterpolator = interpolatePath(fromPath, currentPath);
211
+ interpolatorRef.current = pathInterpolator;
212
+ normalizedStartShared.value = (_Skia$Path$MakeFromSV3 = Skia.Path.MakeFromSVGString(pathInterpolator(pathInterpolationEpsilon))) != null ? _Skia$Path$MakeFromSV3 : Skia.Path.Make();
213
+ normalizedEndShared.value = (_Skia$Path$MakeFromSV4 = Skia.Path.MakeFromSVGString(pathInterpolator(1 - pathInterpolationEpsilon))) != null ? _Skia$Path$MakeFromSV4 : Skia.Path.Make();
214
+ fallbackPathShared.value = (_Skia$Path$MakeFromSV5 = Skia.Path.MakeFromSVGString(currentPath)) != null ? _Skia$Path$MakeFromSV5 : Skia.Path.Make();
197
215
  progress.value = 0;
198
216
  progress.value = buildTransition(1, activeTransition);
199
217
  }
200
- }, [currentPath, updateTransition, enterTransition, progress, normalizedStartShared, normalizedEndShared, fallbackPathShared]);
218
+ }, [currentPath, updateTransition, enterTransition, progress, normalizedStartShared, normalizedEndShared, fallbackPathShared, result]);
201
219
  useAnimatedReaction(() => ({
202
220
  p: progress.value,
203
221
  to: fallbackPathShared.value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cds-mobile-visualization",
3
- "version": "3.4.0-beta.21",
3
+ "version": "3.4.0-beta.23",
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.49.0",
39
+ "@coinbase/cds-common": "^8.52.0",
40
40
  "@coinbase/cds-lottie-files": "^3.3.4",
41
- "@coinbase/cds-mobile": "^8.49.0",
41
+ "@coinbase/cds-mobile": "^8.52.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.49.0",
60
+ "@coinbase/cds-common": "^8.52.0",
61
61
  "@coinbase/cds-lottie-files": "^3.3.4",
62
- "@coinbase/cds-mobile": "^8.49.0",
62
+ "@coinbase/cds-mobile": "^8.52.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",