@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
@@ -1,3 +1,4 @@
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); }
1
2
  import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo } from 'react';
2
3
  import { runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated';
3
4
  import { useTheme } from '@coinbase/cds-mobile';
@@ -45,15 +46,19 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
45
46
  scrubberPosition
46
47
  } = useScrubberContext();
47
48
  const {
49
+ layout,
48
50
  getXSerializableScale,
51
+ getYSerializableScale,
49
52
  getXAxis,
53
+ getYAxis,
50
54
  series,
51
55
  drawingArea,
52
56
  animate,
53
57
  dataLength
54
58
  } = useCartesianChartContext();
55
- const xAxis = useMemo(() => getXAxis(), [getXAxis]);
56
- const xScale = useMemo(() => getXSerializableScale(), [getXSerializableScale]);
59
+ const categoryAxisIsX = useMemo(() => layout !== 'horizontal', [layout]);
60
+ const indexAxis = useMemo(() => categoryAxisIsX ? getXAxis() : getYAxis(), [categoryAxisIsX, getXAxis, getYAxis]);
61
+ const indexScale = useMemo(() => categoryAxisIsX ? getXSerializableScale() : getYSerializableScale(), [categoryAxisIsX, getXSerializableScale, getYSerializableScale]);
57
62
 
58
63
  // Animation state for delayed scrubber rendering (matches web timing)
59
64
  const scrubberOpacity = useSharedValue(animate ? 0 : 1);
@@ -76,27 +81,43 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
76
81
  var _scrubberPosition$val;
77
82
  return (_scrubberPosition$val = scrubberPosition.value) != null ? _scrubberPosition$val : Math.max(0, dataLength - 1);
78
83
  }, [scrubberPosition, dataLength]);
79
- const dataX = useDerivedValue(() => {
80
- if (xAxis != null && xAxis.data && Array.isArray(xAxis.data) && xAxis.data[dataIndex.value] !== undefined) {
81
- const dataValue = xAxis.data[dataIndex.value];
82
- return typeof dataValue === 'string' ? dataIndex.value : dataValue;
84
+ const dataValue = useDerivedValue(() => {
85
+ if (indexAxis != null && indexAxis.data && Array.isArray(indexAxis.data) && indexAxis.data[dataIndex.value] !== undefined) {
86
+ const axisValue = indexAxis.data[dataIndex.value];
87
+ return typeof axisValue === 'string' ? dataIndex.value : axisValue;
83
88
  }
84
89
  return dataIndex.value;
85
- }, [xAxis, dataIndex]);
90
+ }, [indexAxis, dataIndex]);
86
91
  const lineOpacity = useDerivedValue(() => {
87
92
  return scrubberPosition.value !== undefined ? 1 : 0;
88
93
  }, [scrubberPosition]);
89
94
  const overlayOpacity = useDerivedValue(() => {
90
95
  return scrubberPosition.value !== undefined ? 0.8 : 0;
91
96
  }, [scrubberPosition]);
97
+ const pixelPosition = useDerivedValue(() => {
98
+ if (dataValue.value === undefined || !indexScale) return undefined;
99
+ return getPointOnSerializableScale(dataValue.value, indexScale);
100
+ }, [dataValue, indexScale]);
92
101
  const overlayWidth = useDerivedValue(() => {
93
- const pixelX = dataX.value !== undefined && xScale ? getPointOnSerializableScale(dataX.value, xScale) : 0;
94
- return drawingArea.x + drawingArea.width - pixelX + overlayOffset;
95
- }, [dataX, xScale]);
102
+ var _pixelPosition$value;
103
+ const pixel = (_pixelPosition$value = pixelPosition.value) != null ? _pixelPosition$value : 0;
104
+ return categoryAxisIsX ? drawingArea.x + drawingArea.width - pixel + overlayOffset : drawingArea.width + overlayOffset * 2;
105
+ }, [pixelPosition, categoryAxisIsX, drawingArea, overlayOffset]);
106
+ const overlayHeight = useDerivedValue(() => {
107
+ var _pixelPosition$value2;
108
+ const pixel = (_pixelPosition$value2 = pixelPosition.value) != null ? _pixelPosition$value2 : 0;
109
+ return categoryAxisIsX ? drawingArea.height + overlayOffset * 2 : drawingArea.y + drawingArea.height - pixel + overlayOffset;
110
+ }, [pixelPosition, categoryAxisIsX, drawingArea, overlayOffset]);
96
111
  const overlayX = useDerivedValue(() => {
97
- const xValue = dataX.value !== undefined && xScale ? getPointOnSerializableScale(dataX.value, xScale) : 0;
98
- return xValue;
99
- }, [dataX, xScale]);
112
+ var _pixelPosition$value3;
113
+ const pixel = (_pixelPosition$value3 = pixelPosition.value) != null ? _pixelPosition$value3 : 0;
114
+ return categoryAxisIsX ? pixel : drawingArea.x - overlayOffset;
115
+ }, [pixelPosition, categoryAxisIsX, drawingArea, overlayOffset]);
116
+ const overlayY = useDerivedValue(() => {
117
+ var _pixelPosition$value4;
118
+ const pixel = (_pixelPosition$value4 = pixelPosition.value) != null ? _pixelPosition$value4 : 0;
119
+ return categoryAxisIsX ? drawingArea.y - overlayOffset : pixel;
120
+ }, [pixelPosition, categoryAxisIsX, drawingArea, overlayOffset]);
100
121
  const resolvedLabelValue = useSharedValue('');
101
122
  const updateResolvedLabel = useCallback(index => {
102
123
  if (!label) {
@@ -125,7 +146,8 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
125
146
  color: s.color
126
147
  }))) != null ? _series$filter$filter : [];
127
148
  }, [series, filteredSeriesIds]);
128
- const isReady = !!xScale;
149
+ const showBeaconLabels = !hideBeaconLabels && categoryAxisIsX && beaconLabels.length > 0;
150
+ const isReady = !!indexScale;
129
151
  const groupEnterTransition = useMemo(() => getTransition(transitions == null ? void 0 : transitions.enter, animate, defaultAccessoryEnterTransition), [transitions == null ? void 0 : transitions.enter, animate]);
130
152
  useEffect(() => {
131
153
  if (animate && isReady) {
@@ -137,29 +159,33 @@ export const Scrubber = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_ref, ref) =>
137
159
  opacity: scrubberOpacity,
138
160
  children: [!hideOverlay && /*#__PURE__*/_jsx(Rect, {
139
161
  color: theme.color.bg,
140
- height: drawingArea.height + overlayOffset * 2,
162
+ height: overlayHeight,
141
163
  opacity: overlayOpacity,
142
164
  width: overlayWidth,
143
165
  x: overlayX,
144
- y: drawingArea.y - overlayOffset
145
- }), !hideLine && /*#__PURE__*/_jsx(ReferenceLine, {
166
+ y: overlayY
167
+ }), !hideLine && /*#__PURE__*/_jsx(ReferenceLine, _extends({
146
168
  LabelComponent: LabelComponent,
147
- LineComponent: LineComponent,
148
- dataX: dataX,
169
+ LineComponent: LineComponent
170
+ }, categoryAxisIsX ? {
171
+ dataX: dataValue
172
+ } : {
173
+ dataY: dataValue
174
+ }, {
149
175
  label: resolvedLabelValue,
150
176
  labelBoundsInset: labelBoundsInset,
151
177
  labelElevated: labelElevated,
152
178
  labelFont: labelFont,
153
179
  opacity: lineOpacity,
154
180
  stroke: lineStroke
155
- }), /*#__PURE__*/_jsx(ScrubberBeaconGroup, {
181
+ })), /*#__PURE__*/_jsx(ScrubberBeaconGroup, {
156
182
  ref: beaconGroupRef,
157
183
  BeaconComponent: BeaconComponent,
158
184
  idlePulse: idlePulse,
159
185
  seriesIds: filteredSeriesIds,
160
186
  stroke: beaconStroke,
161
187
  transitions: transitions
162
- }), !hideBeaconLabels && beaconLabels.length > 0 && /*#__PURE__*/_jsx(ScrubberBeaconLabelGroup, {
188
+ }), showBeaconLabels && /*#__PURE__*/_jsx(ScrubberBeaconLabelGroup, {
163
189
  BeaconLabelComponent: BeaconLabelComponent,
164
190
  labelFont: beaconLabelFont,
165
191
  labelHorizontalOffset: beaconLabelHorizontalOffset,
@@ -12,7 +12,7 @@ const BeaconWithData = /*#__PURE__*/memo(_ref => {
12
12
  let {
13
13
  seriesId,
14
14
  dataIndex,
15
- dataX,
15
+ dataIndexValue,
16
16
  isIdle,
17
17
  BeaconComponent,
18
18
  idlePulse,
@@ -22,6 +22,7 @@ const BeaconWithData = /*#__PURE__*/memo(_ref => {
22
22
  stroke
23
23
  } = _ref;
24
24
  const {
25
+ layout,
25
26
  getSeries,
26
27
  getSeriesData,
27
28
  getXScale,
@@ -49,10 +50,10 @@ const BeaconWithData = /*#__PURE__*/memo(_ref => {
49
50
  // Get scales for gradient evaluation
50
51
  const gradientScale = useMemo(() => {
51
52
  if (!gradient) return undefined;
52
- const scale = gradient.axis === 'x' ? getXScale() : getYScale(series == null ? void 0 : series.yAxisId);
53
+ const scale = gradient.axis === 'x' ? getXScale(series == null ? void 0 : series.xAxisId) : getYScale(series == null ? void 0 : series.yAxisId);
53
54
  if (!scale) return undefined;
54
55
  return convertToSerializableScale(scale);
55
- }, [gradient, getXScale, getYScale, series == null ? void 0 : series.yAxisId]);
56
+ }, [gradient, getXScale, getYScale, series == null ? void 0 : series.xAxisId, series == null ? void 0 : series.yAxisId]);
56
57
  const gradientStops = useMemo(() => {
57
58
  if (!gradient || !gradientScale) return undefined;
58
59
  const domain = {
@@ -70,25 +71,25 @@ const BeaconWithData = /*#__PURE__*/memo(_ref => {
70
71
  var _series$color;
71
72
  if (gradient && gradientScale && gradientStops) {
72
73
  var _gradient$axis;
73
- const axis = (_gradient$axis = gradient.axis) != null ? _gradient$axis : 'y';
74
- const dataValue = axis === 'x' ? dataX.value : dataY.value;
75
- if (dataValue !== undefined) {
76
- const evaluatedColor = evaluateGradientAtValue(gradientStops, dataValue, gradientScale);
77
- if (evaluatedColor) {
78
- return evaluatedColor;
79
- }
74
+ const categoryAxisIsX = layout !== 'horizontal';
75
+ const gradientAxis = (_gradient$axis = gradient.axis) != null ? _gradient$axis : 'y';
76
+ const valueForAxis = gradientAxis === 'x' ? categoryAxisIsX ? dataIndexValue.value : dataY.value : categoryAxisIsX ? dataY.value : dataIndexValue.value;
77
+ const evaluatedColor = evaluateGradientAtValue(gradientStops, valueForAxis, gradientScale);
78
+ if (evaluatedColor) {
79
+ return evaluatedColor;
80
80
  }
81
81
  }
82
82
 
83
83
  // Fallback to series color
84
84
  return (_series$color = series == null ? void 0 : series.color) != null ? _series$color : theme.color.fgPrimary;
85
- }, [gradient, gradientScale, gradientStops, dataX, dataY, series == null ? void 0 : series.color, theme.color.fgPrimary]);
85
+ }, [gradient, gradientScale, gradientStops, dataIndexValue, dataY, series == null ? void 0 : series.color, theme.color.fgPrimary, layout]);
86
+ const categoryAxisIsX = layout !== 'horizontal';
86
87
  return /*#__PURE__*/_jsx(BeaconComponent, {
87
88
  ref: beaconRef,
88
89
  animate: animate,
89
90
  color: color,
90
- dataX: dataX,
91
- dataY: dataY,
91
+ dataX: categoryAxisIsX ? dataIndexValue : dataY,
92
+ dataY: categoryAxisIsX ? dataY : dataIndexValue,
92
93
  idlePulse: idlePulse,
93
94
  isIdle: isIdle,
94
95
  seriesId: seriesId,
@@ -109,12 +110,15 @@ export const ScrubberBeaconGroup = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_r
109
110
  scrubberPosition
110
111
  } = useScrubberContext();
111
112
  const {
113
+ layout,
112
114
  getXAxis,
115
+ getYAxis,
113
116
  series,
114
117
  dataLength,
115
118
  animate
116
119
  } = useCartesianChartContext();
117
- const xAxis = useMemo(() => getXAxis(), [getXAxis]);
120
+ const categoryAxisIsX = useMemo(() => layout !== 'horizontal', [layout]);
121
+ const indexAxis = useMemo(() => categoryAxisIsX ? getXAxis() : getYAxis(), [categoryAxisIsX, getXAxis, getYAxis]);
118
122
 
119
123
  // Expose imperative handle with pulse method
120
124
  useImperativeHandle(ref, () => ({
@@ -132,14 +136,14 @@ export const ScrubberBeaconGroup = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_r
132
136
  var _scrubberPosition$val;
133
137
  return (_scrubberPosition$val = scrubberPosition.value) != null ? _scrubberPosition$val : Math.max(0, dataLength - 1);
134
138
  }, [scrubberPosition, dataLength]);
135
- const dataX = useDerivedValue(() => {
136
- // Convert index to actual x value if axis has data
137
- if (xAxis != null && xAxis.data && Array.isArray(xAxis.data) && xAxis.data[dataIndex.value] !== undefined) {
138
- const dataValue = xAxis.data[dataIndex.value];
139
+ const dataIndexValue = useDerivedValue(() => {
140
+ // Convert index to actual category-axis value if axis has data.
141
+ if (indexAxis != null && indexAxis.data && Array.isArray(indexAxis.data) && indexAxis.data[dataIndex.value] !== undefined) {
142
+ const dataValue = indexAxis.data[dataIndex.value];
139
143
  return typeof dataValue === 'string' ? dataIndex.value : dataValue;
140
144
  }
141
145
  return dataIndex.value;
142
- }, [xAxis, dataIndex]);
146
+ }, [indexAxis, dataIndex]);
143
147
  const isIdle = useDerivedValue(() => {
144
148
  return scrubberPosition.value === undefined;
145
149
  }, [scrubberPosition]);
@@ -155,7 +159,7 @@ export const ScrubberBeaconGroup = /*#__PURE__*/memo(/*#__PURE__*/forwardRef((_r
155
159
  animate: animate,
156
160
  beaconRef: createBeaconRef(s.id),
157
161
  dataIndex: dataIndex,
158
- dataX: dataX,
162
+ dataIndexValue: dataIndexValue,
159
163
  idlePulse: idlePulse,
160
164
  isIdle: isIdle,
161
165
  seriesId: s.id,
@@ -23,25 +23,29 @@ export const ScrubberProvider = _ref => {
23
23
  throw new Error('ScrubberProvider must be used within a ChartContext');
24
24
  }
25
25
  const {
26
+ layout,
26
27
  getXSerializableScale,
27
- getXAxis
28
+ getYSerializableScale,
29
+ getXAxis,
30
+ getYAxis
28
31
  } = chartContext;
29
32
  const scrubberPosition = useSharedValue(undefined);
30
- const xAxis = useMemo(() => getXAxis(), [getXAxis]);
31
- const xScale = useMemo(() => getXSerializableScale(), [getXSerializableScale]);
32
- const getDataIndexFromX = useCallback(touchX => {
33
+ const categoryAxisIsX = useMemo(() => layout !== 'horizontal', [layout]);
34
+ const categoryAxis = useMemo(() => categoryAxisIsX ? getXAxis() : getYAxis(), [categoryAxisIsX, getXAxis, getYAxis]);
35
+ const categoryScale = useMemo(() => categoryAxisIsX ? getXSerializableScale() : getYSerializableScale(), [categoryAxisIsX, getXSerializableScale, getYSerializableScale]);
36
+ const getDataIndexFromPosition = useCallback(touchPosition => {
33
37
  'worklet';
34
38
 
35
- if (!xScale || !xAxis) return 0;
36
- if (xScale.type === 'band') {
37
- const [domainMin, domainMax] = xScale.domain;
39
+ if (!categoryScale || !categoryAxis) return 0;
40
+ if (categoryScale.type === 'band') {
41
+ const [domainMin, domainMax] = categoryScale.domain;
38
42
  const categoryCount = domainMax - domainMin + 1;
39
43
  let closestIndex = 0;
40
44
  let closestDistance = Infinity;
41
45
  for (let i = 0; i < categoryCount; i++) {
42
- const xPos = getPointOnSerializableScale(i, xScale);
43
- if (xPos !== undefined) {
44
- const distance = Math.abs(touchX - xPos);
46
+ const categoryPos = getPointOnSerializableScale(i, categoryScale);
47
+ if (categoryPos !== undefined) {
48
+ const distance = Math.abs(touchPosition - categoryPos);
45
49
  if (distance < closestDistance) {
46
50
  closestDistance = distance;
47
51
  closestIndex = i;
@@ -50,18 +54,17 @@ export const ScrubberProvider = _ref => {
50
54
  }
51
55
  return closestIndex;
52
56
  } else {
53
- // For numeric scales with axis data, find the nearest data point
54
- const axisData = xAxis.data;
57
+ // For numeric scales with axis data, find the nearest data point.
58
+ const axisData = categoryAxis.data;
55
59
  if (axisData && Array.isArray(axisData) && typeof axisData[0] === 'number') {
56
- // We have numeric axis data - find the closest data point
57
60
  const numericData = axisData;
58
61
  let closestIndex = 0;
59
62
  let closestDistance = Infinity;
60
63
  for (let i = 0; i < numericData.length; i++) {
61
- const xValue = numericData[i];
62
- const xPos = getPointOnSerializableScale(xValue, xScale);
63
- if (xPos !== undefined) {
64
- const distance = Math.abs(touchX - xPos);
64
+ const dataValue = numericData[i];
65
+ const categoryPos = getPointOnSerializableScale(dataValue, categoryScale);
66
+ if (categoryPos !== undefined) {
67
+ const distance = Math.abs(touchPosition - categoryPos);
65
68
  if (distance < closestDistance) {
66
69
  closestDistance = distance;
67
70
  closestIndex = i;
@@ -71,13 +74,13 @@ export const ScrubberProvider = _ref => {
71
74
  return closestIndex;
72
75
  } else {
73
76
  var _domain$min, _domain$max;
74
- const xValue = invertSerializableScale(touchX, xScale);
75
- const dataIndex = Math.round(xValue);
76
- const domain = xAxis.domain;
77
+ const dataValue = invertSerializableScale(touchPosition, categoryScale);
78
+ const dataIndex = Math.round(dataValue);
79
+ const domain = categoryAxis.domain;
77
80
  return Math.max((_domain$min = domain.min) != null ? _domain$min : 0, Math.min(dataIndex, (_domain$max = domain.max) != null ? _domain$max : 0));
78
81
  }
79
82
  }
80
- }, [xAxis, xScale]);
83
+ }, [categoryAxis, categoryScale]);
81
84
  const handleStartEndHaptics = useCallback(() => {
82
85
  void Haptics.lightImpact();
83
86
  }, []);
@@ -95,13 +98,15 @@ export const ScrubberProvider = _ref => {
95
98
 
96
99
  // Android does not trigger onUpdate when the gesture starts. This achieves consistent behavior across both iOS and Android
97
100
  if (Platform.OS === 'android') {
98
- const newScrubberPosition = getDataIndexFromX(event.x);
101
+ const pointerPosition = categoryAxisIsX ? event.x : event.y;
102
+ const newScrubberPosition = getDataIndexFromPosition(pointerPosition);
99
103
  if (newScrubberPosition !== scrubberPosition.value) {
100
104
  scrubberPosition.value = newScrubberPosition;
101
105
  }
102
106
  }
103
107
  }).onUpdate(function onUpdate(event) {
104
- const newScrubberPosition = getDataIndexFromX(event.x);
108
+ const pointerPosition = categoryAxisIsX ? event.x : event.y;
109
+ const newScrubberPosition = getDataIndexFromPosition(pointerPosition);
105
110
  if (newScrubberPosition !== scrubberPosition.value) {
106
111
  scrubberPosition.value = newScrubberPosition;
107
112
  }
@@ -114,7 +119,7 @@ export const ScrubberProvider = _ref => {
114
119
  if (enableScrubbing) {
115
120
  scrubberPosition.value = undefined;
116
121
  }
117
- }), [allowOverflowGestures, handleStartEndHaptics, getDataIndexFromX, scrubberPosition, enableScrubbing]);
122
+ }), [allowOverflowGestures, handleStartEndHaptics, getDataIndexFromPosition, categoryAxisIsX, scrubberPosition, enableScrubbing]);
118
123
  const contextValue = useMemo(() => ({
119
124
  enableScrubbing: !!enableScrubbing,
120
125
  scrubberPosition
@@ -1,5 +1,6 @@
1
1
  const _excluded = ["seriesId", "color", "label"],
2
- _excluded2 = ["seriesId", "color", "label"];
2
+ _excluded2 = ["seriesId", "color", "label"],
3
+ _excluded3 = ["seriesId", "color"];
3
4
  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
5
  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); }
5
6
  import { memo, useCallback, useMemo, useRef, useState } from 'react';
@@ -654,6 +655,136 @@ const HideOverlay = () => {
654
655
  })
655
656
  });
656
657
  };
658
+ const matchupBlueData = [47, 50, 51, 52, 53, 53, 53, 53, 52, 51, 51, 52, 53, 55, 57, 58, 59, 61, 63, 65, 64, 64, 64, 64, 64, 63, 63, 63, 64, 66, 68, 70, 71, 72, 74, 76, 76, 75, 74, 73, 74, 75, 75, 78];
659
+ const matchupRedData = matchupBlueData.map(value => 100 - value);
660
+ const matchupTeamLabels = {
661
+ blue: 'BLUE',
662
+ red: 'RED'
663
+ };
664
+ const MatchupBeaconLabels = () => {
665
+ const theme = useTheme();
666
+ const MatchupScrubberBeaconLabel = /*#__PURE__*/memo(_ref7 => {
667
+ var _matchupTeamLabels$se;
668
+ let {
669
+ seriesId,
670
+ color
671
+ } = _ref7,
672
+ props = _objectWithoutPropertiesLoose(_ref7, _excluded3);
673
+ const {
674
+ getSeriesData,
675
+ series,
676
+ fontProvider
677
+ } = useCartesianChartContext();
678
+ const {
679
+ scrubberPosition
680
+ } = useScrubberContext();
681
+ const seriesData = useMemo(() => getLineData(getSeriesData(seriesId)), [getSeriesData, seriesId]);
682
+ const dataLength = useMemo(() => {
683
+ var _series$reduce3;
684
+ return (_series$reduce3 = series == null ? void 0 : series.reduce((max, currentSeries) => {
685
+ var _data$length3;
686
+ const data = getSeriesData(currentSeries.id);
687
+ return Math.max(max, (_data$length3 = data == null ? void 0 : data.length) != null ? _data$length3 : 0);
688
+ }, 0)) != null ? _series$reduce3 : 0;
689
+ }, [series, getSeriesData]);
690
+ const dataIndex = useDerivedValue(() => {
691
+ var _scrubberPosition$val3;
692
+ return (_scrubberPosition$val3 = scrubberPosition.value) != null ? _scrubberPosition$val3 : Math.max(0, dataLength - 1);
693
+ }, [scrubberPosition, dataLength]);
694
+ const teamLabel = (_matchupTeamLabels$se = matchupTeamLabels[seriesId]) != null ? _matchupTeamLabels$se : String(seriesId).toUpperCase();
695
+ const labelColor = color != null ? color : theme.color.fgPrimary;
696
+ const legalFontSize = theme.fontSize.legal;
697
+ const title3FontSize = theme.fontSize.title3;
698
+ const teamStyle = useMemo(() => ({
699
+ fontFamilies: ['Inter'],
700
+ fontSize: legalFontSize,
701
+ fontStyle: {
702
+ weight: FontWeight.Normal
703
+ },
704
+ color: Skia.Color(labelColor)
705
+ }), [labelColor, legalFontSize]);
706
+ const percentageStyle = useMemo(() => ({
707
+ fontFamilies: ['Inter'],
708
+ fontSize: title3FontSize,
709
+ fontStyle: {
710
+ weight: FontWeight.Bold
711
+ },
712
+ color: Skia.Color(labelColor)
713
+ }), [title3FontSize, labelColor]);
714
+ const matchupLabel = useDerivedValue(() => {
715
+ if (seriesData === undefined) {
716
+ return teamLabel;
717
+ }
718
+ const value = seriesData[dataIndex.value];
719
+ const builder = Skia.ParagraphBuilder.Make({
720
+ textAlign: TextAlign.Left
721
+ }, fontProvider);
722
+ builder.pushStyle(teamStyle);
723
+ builder.addText(teamLabel);
724
+ builder.addText('\n');
725
+ builder.pushStyle(percentageStyle);
726
+ builder.addText(value + "%");
727
+ const paragraph = builder.build();
728
+ paragraph.layout(240);
729
+ return paragraph;
730
+ }, [dataIndex, fontProvider, percentageStyle, seriesData, teamLabel, teamStyle]);
731
+ return /*#__PURE__*/_jsx(DefaultScrubberBeaconLabel, _extends({}, props, {
732
+ background: "transparent",
733
+ color: labelColor,
734
+ elevated: false,
735
+ inset: 0,
736
+ label: matchupLabel,
737
+ seriesId: seriesId
738
+ }));
739
+ });
740
+ return /*#__PURE__*/_jsx(LineChart, {
741
+ enableScrubbing: true,
742
+ showArea: true,
743
+ areaType: "dotted",
744
+ height: 300,
745
+ inset: {
746
+ bottom: 8,
747
+ left: 8,
748
+ top: 8,
749
+ right: 0
750
+ },
751
+ series: [{
752
+ id: 'blue',
753
+ data: matchupBlueData,
754
+ color: "rgb(" + theme.spectrum.blue50 + ")",
755
+ label: 'BLUE'
756
+ }, {
757
+ id: 'red',
758
+ data: matchupRedData,
759
+ color: "rgb(" + theme.spectrum.red50 + ")",
760
+ label: 'RED'
761
+ }],
762
+ xAxis: {
763
+ range: _ref8 => {
764
+ let {
765
+ min,
766
+ max
767
+ } = _ref8;
768
+ return {
769
+ min,
770
+ max: max - 64
771
+ };
772
+ }
773
+ },
774
+ yAxis: {
775
+ domain: {
776
+ min: 0,
777
+ max: 100
778
+ }
779
+ },
780
+ children: /*#__PURE__*/_jsx(Scrubber, {
781
+ idlePulse: true,
782
+ BeaconLabelComponent: MatchupScrubberBeaconLabel,
783
+ beaconLabelHorizontalOffset: 16,
784
+ beaconLabelPreferredSide: "right"
785
+ })
786
+ });
787
+ };
657
788
  const ExampleNavigator = () => {
658
789
  const [currentIndex, setCurrentIndex] = useState(0);
659
790
  const examples = useMemo(() => [{
@@ -707,6 +838,9 @@ const ExampleNavigator = () => {
707
838
  }, {
708
839
  title: 'Hide Overlay',
709
840
  component: /*#__PURE__*/_jsx(HideOverlay, {})
841
+ }, {
842
+ title: 'Matchup Beacon Labels',
843
+ component: /*#__PURE__*/_jsx(MatchupBeaconLabels, {})
710
844
  }], []);
711
845
  const currentExample = examples[currentIndex];
712
846
  const isFirstExample = currentIndex === 0;
@@ -47,7 +47,7 @@ export const toPointAnchor = placement => {
47
47
  */
48
48
 
49
49
  /**
50
- * Gets a D3 scale based on the axis configuration.
50
+ * Gets a D3 scale based on the cartesian axis configuration.
51
51
  * Handles both numeric (linear/log) and categorical (band) scales.
52
52
  *
53
53
  * For numeric scales, the domain limit controls whether bounds are "nice" (human-friendly)
@@ -57,19 +57,27 @@ export const toPointAnchor = placement => {
57
57
  * @returns The D3 scale function
58
58
  * @throws An Error if bounds are invalid
59
59
  */
60
- export const getAxisScale = _ref => {
60
+ export const getCartesianAxisScale = _ref => {
61
61
  var _config$scaleType;
62
62
  let {
63
63
  config,
64
64
  type,
65
65
  range,
66
- dataDomain
66
+ dataDomain,
67
+ layout = 'vertical'
67
68
  } = _ref;
68
69
  const scaleType = (_config$scaleType = config == null ? void 0 : config.scaleType) != null ? _config$scaleType : 'linear';
69
70
  let adjustedRange = range;
70
71
 
71
- // Invert range for Y axis for SVG coordinate system
72
- if (type === 'y') {
72
+ // Determine if this axis needs range inversion for SVG coordinate system.
73
+ // SVG Y coordinates increase downward, so we need to invert for value axes
74
+ // where we want higher values at the top.
75
+ //
76
+ // For vertical layout: Y axis is the value axis -> invert (higher values at top)
77
+ // For horizontal layout: Y axis is the category axis -> don't invert (first category at top is natural)
78
+ // X axis never needs inversion (left-to-right is natural for both layouts)
79
+ const shouldInvertRange = type === 'y' && layout !== 'horizontal';
80
+ if (shouldInvertRange) {
73
81
  adjustedRange = {
74
82
  min: adjustedRange.max,
75
83
  max: adjustedRange.min
@@ -121,6 +129,8 @@ export const getAxisConfig = function (type, axes, defaultId, defaultScaleType)
121
129
  defaultScaleType = defaultAxisScaleType;
122
130
  }
123
131
  const defaultDomainLimit = type === 'x' ? 'strict' : 'nice';
132
+ const axisName = type === 'x' ? 'x-axis' : 'y-axis';
133
+ const axisDocUrl = type === 'x' ? 'https://cds.coinbase.com/components/charts/XAxis' : 'https://cds.coinbase.com/components/charts/YAxis';
124
134
  if (!axes) {
125
135
  return [{
126
136
  id: defaultId,
@@ -137,16 +147,27 @@ export const getAxisConfig = function (type, axes, defaultId, defaultScaleType)
137
147
  } = _ref2;
138
148
  return id === undefined;
139
149
  })) {
140
- throw new Error('When defining multiple axes, each must have a unique id. See https://cds.coinbase.com/components/charts/YAxis/#multiple-y-axes.');
150
+ throw new Error("When defining multiple " + axisName + ", each must have a unique id. See " + axisDocUrl + ".");
141
151
  }
142
- return axes.map(_ref3 => {
152
+ if (axesLength > 1) {
153
+ const ids = axes.map(_ref3 => {
154
+ let {
155
+ id
156
+ } = _ref3;
157
+ return id;
158
+ }).filter(id => id !== undefined);
159
+ if (new Set(ids).size !== ids.length) {
160
+ throw new Error("When defining multiple " + axisName + ", each must have a unique id. See " + axisDocUrl + ".");
161
+ }
162
+ }
163
+ return axes.map(_ref4 => {
143
164
  let {
144
165
  id
145
- } = _ref3,
146
- axis = _objectWithoutPropertiesLoose(_ref3, _excluded);
166
+ } = _ref4,
167
+ axis = _objectWithoutPropertiesLoose(_ref4, _excluded);
147
168
  return _extends({
148
169
  // defaults the axis id if only a single axis is provided
149
- id: axesLength > 1 ? id != null ? id : defaultAxisId : id,
170
+ id: axesLength > 1 ? id != null ? id : defaultAxisId : id != null ? id : defaultId,
150
171
  scaleType: defaultScaleType,
151
172
  domainLimit: defaultDomainLimit
152
173
  }, axis);
@@ -168,10 +189,14 @@ export const getAxisConfig = function (type, axes, defaultId, defaultScaleType)
168
189
  * @param axisParam - The axis configuration
169
190
  * @param series - Array of series objects (for stacking support)
170
191
  * @param axisType - Whether this is an 'x' or 'y' axis
192
+ * @param layout - The chart layout orientation
171
193
  * @returns The calculated axis bounds
172
194
  */
173
- export const getAxisDomain = (axisParam, series, axisType) => {
195
+ export const getCartesianAxisDomain = function (axisParam, series, axisType, layout) {
174
196
  var _finalDomain$min, _finalDomain$max;
197
+ if (layout === void 0) {
198
+ layout = 'vertical';
199
+ }
175
200
  let dataDomain = null;
176
201
  if (axisParam.data && Array.isArray(axisParam.data) && axisParam.data.length > 0) {
177
202
  const firstItem = axisParam.data[0];
@@ -193,7 +218,10 @@ export const getAxisDomain = (axisParam, series, axisType) => {
193
218
  }
194
219
 
195
220
  // Calculate domain from series data
196
- const seriesDomain = axisType === 'x' ? getChartDomain(series) : getChartRange(series);
221
+ // In vertical layout: X is category (index), Y is value (value)
222
+ // In horizontal layout: Y is category (index), X is value (value)
223
+ const isCategoryAxis = layout !== 'horizontal' && axisType === 'x' || layout === 'horizontal' && axisType === 'y';
224
+ const seriesDomain = isCategoryAxis ? getChartDomain(series) : getChartRange(series);
197
225
 
198
226
  // If data sets the domain, use that instead of the series domain
199
227
  const preferredDataDomain = dataDomain != null ? dataDomain : seriesDomain;
@@ -487,7 +515,7 @@ const generateEvenlyDistributedTicks = (scale, tickInterval, possibleTickValues,
487
515
  * });
488
516
  * // Returns tick positions centered in each selected band
489
517
  */
490
- export const getAxisTicksData = _ref4 => {
518
+ export const getAxisTicksData = _ref5 => {
491
519
  var _options$anchor;
492
520
  let {
493
521
  ticks,
@@ -497,7 +525,7 @@ export const getAxisTicksData = _ref4 => {
497
525
  possibleTickValues,
498
526
  tickInterval,
499
527
  options
500
- } = _ref4;
528
+ } = _ref5;
501
529
  const anchor = (_options$anchor = options == null ? void 0 : options.anchor) != null ? _options$anchor : 'middle';
502
530
 
503
531
  // Handle band scales