@coinbase/cds-mobile-visualization 3.4.0-beta.8 → 3.4.0

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 (170) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/dts/chart/CartesianChart.d.ts +92 -7
  3. package/dts/chart/CartesianChart.d.ts.map +1 -1
  4. package/dts/chart/ChartContextBridge.d.ts.map +1 -1
  5. package/dts/chart/ChartProvider.d.ts +3 -0
  6. package/dts/chart/ChartProvider.d.ts.map +1 -1
  7. package/dts/chart/Path.d.ts +36 -13
  8. package/dts/chart/Path.d.ts.map +1 -1
  9. package/dts/chart/PeriodSelector.d.ts +21 -6
  10. package/dts/chart/PeriodSelector.d.ts.map +1 -1
  11. package/dts/chart/area/Area.d.ts +14 -11
  12. package/dts/chart/area/Area.d.ts.map +1 -1
  13. package/dts/chart/area/AreaChart.d.ts +33 -9
  14. package/dts/chart/area/AreaChart.d.ts.map +1 -1
  15. package/dts/chart/area/DottedArea.d.ts.map +1 -1
  16. package/dts/chart/area/GradientArea.d.ts.map +1 -1
  17. package/dts/chart/area/SolidArea.d.ts.map +1 -1
  18. package/dts/chart/axis/Axis.d.ts +22 -42
  19. package/dts/chart/axis/Axis.d.ts.map +1 -1
  20. package/dts/chart/axis/XAxis.d.ts +6 -0
  21. package/dts/chart/axis/XAxis.d.ts.map +1 -1
  22. package/dts/chart/axis/YAxis.d.ts +1 -0
  23. package/dts/chart/axis/YAxis.d.ts.map +1 -1
  24. package/dts/chart/bar/Bar.d.ts +51 -51
  25. package/dts/chart/bar/Bar.d.ts.map +1 -1
  26. package/dts/chart/bar/BarChart.d.ts +56 -11
  27. package/dts/chart/bar/BarChart.d.ts.map +1 -1
  28. package/dts/chart/bar/BarPlot.d.ts +2 -1
  29. package/dts/chart/bar/BarPlot.d.ts.map +1 -1
  30. package/dts/chart/bar/BarStack.d.ts +45 -20
  31. package/dts/chart/bar/BarStack.d.ts.map +1 -1
  32. package/dts/chart/bar/BarStackGroup.d.ts +2 -1
  33. package/dts/chart/bar/BarStackGroup.d.ts.map +1 -1
  34. package/dts/chart/bar/DefaultBar.d.ts.map +1 -1
  35. package/dts/chart/bar/DefaultBarStack.d.ts.map +1 -1
  36. package/dts/chart/gradient/Gradient.d.ts +5 -0
  37. package/dts/chart/gradient/Gradient.d.ts.map +1 -1
  38. package/dts/chart/index.d.ts +1 -0
  39. package/dts/chart/index.d.ts.map +1 -1
  40. package/dts/chart/legend/DefaultLegendEntry.d.ts +5 -0
  41. package/dts/chart/legend/DefaultLegendEntry.d.ts.map +1 -0
  42. package/dts/chart/legend/DefaultLegendShape.d.ts +5 -0
  43. package/dts/chart/legend/DefaultLegendShape.d.ts.map +1 -0
  44. package/dts/chart/legend/Legend.d.ts +168 -0
  45. package/dts/chart/legend/Legend.d.ts.map +1 -0
  46. package/dts/chart/legend/index.d.ts +4 -0
  47. package/dts/chart/legend/index.d.ts.map +1 -0
  48. package/dts/chart/line/DottedLine.d.ts.map +1 -1
  49. package/dts/chart/line/Line.d.ts +23 -19
  50. package/dts/chart/line/Line.d.ts.map +1 -1
  51. package/dts/chart/line/LineChart.d.ts +26 -9
  52. package/dts/chart/line/LineChart.d.ts.map +1 -1
  53. package/dts/chart/line/ReferenceLine.d.ts +1 -0
  54. package/dts/chart/line/ReferenceLine.d.ts.map +1 -1
  55. package/dts/chart/line/SolidLine.d.ts.map +1 -1
  56. package/dts/chart/point/Point.d.ts +26 -2
  57. package/dts/chart/point/Point.d.ts.map +1 -1
  58. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts +32 -2
  59. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts.map +1 -1
  60. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts +2 -1
  61. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts.map +1 -1
  62. package/dts/chart/scrubber/Scrubber.d.ts +86 -17
  63. package/dts/chart/scrubber/Scrubber.d.ts.map +1 -1
  64. package/dts/chart/scrubber/ScrubberAccessibilityView.d.ts +12 -0
  65. package/dts/chart/scrubber/ScrubberAccessibilityView.d.ts.map +1 -0
  66. package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts +10 -0
  67. package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts.map +1 -1
  68. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts +16 -1
  69. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts.map +1 -1
  70. package/dts/chart/scrubber/ScrubberProvider.d.ts.map +1 -1
  71. package/dts/chart/utils/axis.d.ts +45 -10
  72. package/dts/chart/utils/axis.d.ts.map +1 -1
  73. package/dts/chart/utils/bar.d.ts +190 -0
  74. package/dts/chart/utils/bar.d.ts.map +1 -1
  75. package/dts/chart/utils/chart.d.ts +32 -0
  76. package/dts/chart/utils/chart.d.ts.map +1 -1
  77. package/dts/chart/utils/context.d.ts +21 -6
  78. package/dts/chart/utils/context.d.ts.map +1 -1
  79. package/dts/chart/utils/gradient.d.ts +3 -1
  80. package/dts/chart/utils/gradient.d.ts.map +1 -1
  81. package/dts/chart/utils/path.d.ts +26 -0
  82. package/dts/chart/utils/path.d.ts.map +1 -1
  83. package/dts/chart/utils/point.d.ts +24 -12
  84. package/dts/chart/utils/point.d.ts.map +1 -1
  85. package/dts/chart/utils/scale.d.ts +11 -0
  86. package/dts/chart/utils/scale.d.ts.map +1 -1
  87. package/dts/chart/utils/scrubber.d.ts +2 -1
  88. package/dts/chart/utils/scrubber.d.ts.map +1 -1
  89. package/dts/chart/utils/transition.d.ts +63 -22
  90. package/dts/chart/utils/transition.d.ts.map +1 -1
  91. package/dts/sparkline/Sparkline.d.ts +2 -1
  92. package/dts/sparkline/Sparkline.d.ts.map +1 -1
  93. package/dts/sparkline/SparklineArea.d.ts +2 -1
  94. package/dts/sparkline/SparklineArea.d.ts.map +1 -1
  95. package/dts/sparkline/SparklineGradient.d.ts +2 -1
  96. package/dts/sparkline/SparklineGradient.d.ts.map +1 -1
  97. package/dts/sparkline/sparkline-interactive/SparklineInteractive.d.ts +2 -1
  98. package/dts/sparkline/sparkline-interactive/SparklineInteractive.d.ts.map +1 -1
  99. package/esm/chart/CartesianChart.js +176 -82
  100. package/esm/chart/ChartContextBridge.js +14 -3
  101. package/esm/chart/ChartProvider.js +2 -2
  102. package/esm/chart/Path.js +34 -29
  103. package/esm/chart/PeriodSelector.js +6 -2
  104. package/esm/chart/__stories__/CartesianChart.stories.js +27 -86
  105. package/esm/chart/__stories__/ChartAccessibility.stories.js +721 -0
  106. package/esm/chart/__stories__/ChartTransitions.stories.js +625 -0
  107. package/esm/chart/__stories__/PeriodSelector.stories.js +102 -4
  108. package/esm/chart/area/Area.js +21 -9
  109. package/esm/chart/area/AreaChart.js +18 -13
  110. package/esm/chart/area/DottedArea.js +28 -18
  111. package/esm/chart/area/GradientArea.js +14 -7
  112. package/esm/chart/area/SolidArea.js +6 -2
  113. package/esm/chart/area/__stories__/AreaChart.stories.js +47 -5
  114. package/esm/chart/axis/Axis.js +5 -41
  115. package/esm/chart/axis/XAxis.js +116 -47
  116. package/esm/chart/axis/YAxis.js +105 -26
  117. package/esm/chart/axis/__stories__/Axis.stories.js +324 -48
  118. package/esm/chart/bar/Bar.js +17 -15
  119. package/esm/chart/bar/BarChart.js +38 -33
  120. package/esm/chart/bar/BarPlot.js +40 -45
  121. package/esm/chart/bar/BarStack.js +92 -475
  122. package/esm/chart/bar/BarStackGroup.js +37 -27
  123. package/esm/chart/bar/DefaultBar.js +27 -18
  124. package/esm/chart/bar/DefaultBarStack.js +25 -9
  125. package/esm/chart/bar/__stories__/BarChart.stories.js +728 -54
  126. package/esm/chart/gradient/Gradient.js +2 -1
  127. package/esm/chart/index.js +1 -0
  128. package/esm/chart/legend/DefaultLegendEntry.js +42 -0
  129. package/esm/chart/legend/DefaultLegendShape.js +64 -0
  130. package/esm/chart/legend/Legend.js +59 -0
  131. package/esm/chart/legend/__stories__/Legend.stories.js +574 -0
  132. package/esm/chart/legend/index.js +3 -0
  133. package/esm/chart/line/DottedLine.js +6 -2
  134. package/esm/chart/line/Line.js +42 -38
  135. package/esm/chart/line/LineChart.js +36 -12
  136. package/esm/chart/line/SolidLine.js +6 -2
  137. package/esm/chart/line/__stories__/LineChart.stories.js +241 -594
  138. package/esm/chart/line/__stories__/ReferenceLine.stories.js +95 -1
  139. package/esm/chart/point/Point.js +35 -36
  140. package/esm/chart/scrubber/DefaultScrubberBeacon.js +41 -38
  141. package/esm/chart/scrubber/DefaultScrubberLabel.js +26 -10
  142. package/esm/chart/scrubber/Scrubber.js +67 -35
  143. package/esm/chart/scrubber/ScrubberAccessibilityView.js +177 -0
  144. package/esm/chart/scrubber/ScrubberBeaconGroup.js +30 -22
  145. package/esm/chart/scrubber/ScrubberBeaconLabelGroup.js +35 -8
  146. package/esm/chart/scrubber/ScrubberProvider.js +29 -24
  147. package/esm/chart/scrubber/__stories__/Scrubber.stories.js +946 -0
  148. package/esm/chart/utils/axis.js +88 -44
  149. package/esm/chart/utils/bar.js +820 -0
  150. package/esm/chart/utils/chart.js +34 -7
  151. package/esm/chart/utils/context.js +7 -0
  152. package/esm/chart/utils/gradient.js +8 -4
  153. package/esm/chart/utils/path.js +91 -61
  154. package/esm/chart/utils/point.js +92 -39
  155. package/esm/chart/utils/scale.js +13 -2
  156. package/esm/chart/utils/scrubber.js +12 -5
  157. package/esm/chart/utils/transition.js +108 -60
  158. package/esm/sparkline/Sparkline.js +2 -1
  159. package/esm/sparkline/SparklineArea.js +2 -1
  160. package/esm/sparkline/SparklineGradient.js +2 -1
  161. package/esm/sparkline/__figma__/Sparkline.figma.js +1 -1
  162. package/esm/sparkline/__stories__/Sparkline.stories.js +11 -7
  163. package/esm/sparkline/__stories__/SparklineGradient.stories.js +7 -4
  164. package/esm/sparkline/sparkline-interactive/SparklineInteractive.js +2 -1
  165. package/esm/sparkline/sparkline-interactive/__figma__/SparklineInteractive.figma.js +1 -1
  166. package/esm/sparkline/sparkline-interactive/__stories__/SparklineInteractive.stories.js +51 -26
  167. package/esm/sparkline/sparkline-interactive-header/__figma__/SparklineInteractiveHeader.figma.js +1 -1
  168. package/esm/sparkline/sparkline-interactive-header/__stories__/SparklineInteractiveHeader.stories.js +19 -9
  169. package/package.json +13 -10
  170. package/esm/chart/__stories__/Chart.stories.js +0 -77
@@ -1,6 +1,19 @@
1
1
  import { isSharedValue } from 'react-native-reanimated';
2
2
  import { stack as d3Stack, stackOffsetDiverging, stackOrderNone } from 'd3-shape';
3
3
  export const defaultStackId = 'DEFAULT_STACK_ID';
4
+
5
+ /**
6
+ * Shape variants available for legend items.
7
+ */
8
+
9
+ /**
10
+ * Shape for legend items. Can be a preset variant or a custom ReactNode.
11
+ */
12
+
13
+ /**
14
+ * Position of the legend relative to the chart.
15
+ */
16
+
4
17
  /**
5
18
  * Type guard to check if bounds are complete with both min and max values.
6
19
  * @param bounds - The bounds to validate
@@ -33,15 +46,16 @@ export const getChartDomain = (series, min, max) => {
33
46
  };
34
47
 
35
48
  /**
36
- * Creates a composite stack key that includes both stack ID and y-axis ID.
37
- * 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.
38
51
  */
39
52
  const createStackKey = series => {
40
53
  if (series.stackId === undefined) return undefined;
41
54
 
42
- // Include y-axis ID to prevent cross-scale stacking
55
+ // Include axis IDs to prevent cross-scale stacking
56
+ const xAxisId = series.xAxisId || 'default';
43
57
  const yAxisId = series.yAxisId || 'default';
44
- return series.stackId + ":" + yAxisId;
58
+ return series.stackId + ":" + xAxisId + ":" + yAxisId;
45
59
  };
46
60
 
47
61
  /**
@@ -127,9 +141,9 @@ export const getLineData = data => {
127
141
  const firstNonNull = data.find(d => d !== null);
128
142
  if (Array.isArray(firstNonNull)) {
129
143
  return data.map(d => {
130
- var _d$at;
144
+ var _d;
131
145
  if (d === null) return null;
132
- if (Array.isArray(d)) return (_d$at = d.at(-1)) != null ? _d$at : null;
146
+ if (Array.isArray(d)) return (_d = d[d.length - 1]) != null ? _d : null;
133
147
  return d;
134
148
  });
135
149
  }
@@ -212,12 +226,25 @@ export const getChartRange = (series, min, max) => {
212
226
  }
213
227
  return range;
214
228
  };
215
- export const defaultChartInset = {
229
+ export const defaultVerticalLayoutChartInset = {
216
230
  top: 32,
217
231
  left: 16,
218
232
  bottom: 16,
219
233
  right: 16
220
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. This will be removed in a future major release.
244
+ * @deprecationExpectedRemoval v4
245
+ * `defaultHorizontalLayoutChartInset` for horizontal layout charts.
246
+ */
247
+ export const defaultChartInset = defaultVerticalLayoutChartInset;
221
248
 
222
249
  /**
223
250
  * 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,22 +1,35 @@
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
+ /**
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
7
15
  * @param curve - The curve type. Defaults to 'linear'.
16
+ * @param layout - The chart layout. Defaults to 'vertical'.
8
17
  * @returns The d3 curve function.
9
18
  */
10
- export const getPathCurveFunction = function (curve) {
19
+ export const getPathCurveFunction = function (curve, layout) {
11
20
  if (curve === void 0) {
12
21
  curve = 'linear';
13
22
  }
23
+ if (layout === void 0) {
24
+ layout = 'vertical';
25
+ }
14
26
  switch (curve) {
15
27
  case 'catmullRom':
16
28
  return curveCatmullRom;
17
29
  case 'monotone':
18
- // When we support layout="vertical" this should dynamically switch to curveMonotoneY
19
- 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;
20
33
  case 'natural':
21
34
  return curveNatural;
22
35
  case 'step':
@@ -26,8 +39,9 @@ export const getPathCurveFunction = function (curve) {
26
39
  case 'stepAfter':
27
40
  return curveStepAfter;
28
41
  case 'bump':
29
- // When we support layout="vertical" this should dynamically switch to curveBumpY
30
- 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;
31
45
  case 'linearClosed':
32
46
  return curveLinearClosed;
33
47
  case 'linear':
@@ -53,17 +67,21 @@ export const getLinePath = _ref => {
53
67
  xScale,
54
68
  yScale,
55
69
  xData,
56
- connectNulls = false
70
+ yData,
71
+ connectNulls = false,
72
+ layout = 'vertical'
57
73
  } = _ref;
58
74
  if (data.length === 0) {
59
75
  return '';
60
76
  }
61
- const curveFunction = getPathCurveFunction(curve);
77
+ const curveFunction = getPathCurveFunction(curve, layout);
62
78
  const dataPoints = projectPoints({
63
79
  data,
64
80
  xScale,
65
81
  yScale,
66
- xData
82
+ xData,
83
+ yData,
84
+ layout
67
85
  });
68
86
 
69
87
  // When connectNulls is true, filter out null values before rendering
@@ -102,14 +120,20 @@ export const getAreaPath = _ref2 => {
102
120
  xScale,
103
121
  yScale,
104
122
  xData,
105
- connectNulls = false
123
+ yData,
124
+ connectNulls = false,
125
+ layout = 'vertical'
106
126
  } = _ref2;
107
127
  if (data.length === 0) {
108
128
  return '';
109
129
  }
110
- const curveFunction = getPathCurveFunction(curve);
111
- const yDomain = yScale.domain();
112
- 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);
113
137
  const normalizedData = data.map((item, index) => {
114
138
  if (item === null) {
115
139
  return null;
@@ -121,7 +145,7 @@ export const getAreaPath = _ref2 => {
121
145
  return null;
122
146
  }
123
147
  if (typeof item === 'number') {
124
- return [yMin, item];
148
+ return [min, item];
125
149
  }
126
150
  return null;
127
151
  });
@@ -129,37 +153,28 @@ export const getAreaPath = _ref2 => {
129
153
  if (range === null) {
130
154
  return {
131
155
  x: 0,
156
+ y: 0,
132
157
  low: null,
133
158
  high: null,
134
159
  isValid: false
135
160
  };
136
161
  }
137
- let xValue = index;
138
- if (!isCategoricalScale(xScale) && xData && xData[index] !== undefined) {
139
- 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];
140
169
  }
141
- const xPoint = projectPoint({
142
- x: xValue,
143
- y: 0,
144
- xScale,
145
- yScale
146
- });
147
- const lowPoint = projectPoint({
148
- x: xValue,
149
- y: range[0],
150
- xScale,
151
- yScale
152
- });
153
- const highPoint = projectPoint({
154
- x: xValue,
155
- y: range[1],
156
- xScale,
157
- yScale
158
- });
170
+ const position = getPointOnScale(indexValue, indexScale);
171
+ const low = getPointOnScale(range[0], valueScale);
172
+ const high = getPointOnScale(range[1], valueScale);
159
173
  return {
160
- x: xPoint.x,
161
- low: lowPoint.y,
162
- high: highPoint.y,
174
+ x: categoryAxisIsX ? position : 0,
175
+ y: !categoryAxisIsX ? position : 0,
176
+ low,
177
+ high,
163
178
  isValid: true
164
179
  };
165
180
  });
@@ -167,16 +182,25 @@ export const getAreaPath = _ref2 => {
167
182
  // When connectNulls is true, filter out invalid points before rendering
168
183
  // When false, use defined() to create gaps in the area
169
184
  const filteredPoints = connectNulls ? dataPoints.filter(d => d.isValid) : dataPoints;
170
- const areaGenerator = d3Area().x(d => d.x).y0(d => {
171
- var _d$low;
172
- return (_d$low = d.low) != null ? _d$low : 0;
173
- }) // Bottom boundary (low values), fallback to 0
174
- .y1(d => {
175
- var _d$high;
176
- return (_d$high = d.high) != null ? _d$high : 0;
177
- }) // Top boundary (high values), fallback to 0
178
- .curve(curveFunction).defined(d => connectNulls || d.isValid && d.low != null && d.high != null); // Only draw where both values exist
179
-
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);
180
204
  const result = areaGenerator(filteredPoints);
181
205
  return result != null ? result : '';
182
206
  };
@@ -208,22 +232,28 @@ export const lineToPath = (x1, y1, x2, y2) => {
208
232
  * const roundedPath = getBarPath(10, 20, 50, 100, 8, true, false);
209
233
  * ```
210
234
  */
211
- 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 isVerticalLayout = layout === 'vertical';
212
240
  const roundBothSides = roundTop && roundBottom;
213
241
  const r = Math.min(radius, width / 2, roundBothSides ? height / 2 : height);
214
- const topR = roundTop ? r : 0;
215
- const bottomR = roundBottom ? r : 0;
242
+ const rTL = isVerticalLayout ? roundTop ? r : 0 : roundBottom ? r : 0;
243
+ const rTR = isVerticalLayout ? roundTop ? r : 0 : roundTop ? r : 0;
244
+ const rBR = isVerticalLayout ? roundBottom ? r : 0 : roundTop ? r : 0;
245
+ const rBL = isVerticalLayout ? roundBottom ? r : 0 : roundBottom ? r : 0;
216
246
 
217
247
  // Build path with selective rounding
218
- let path = "M " + (x + (roundTop ? r : 0)) + " " + y;
219
- path += " L " + (x + width - topR) + " " + y;
220
- path += " A " + topR + " " + topR + " 0 0 1 " + (x + width) + " " + (y + topR);
221
- path += " L " + (x + width) + " " + (y + height - bottomR);
222
- path += " A " + bottomR + " " + bottomR + " 0 0 1 " + (x + width - bottomR) + " " + (y + height);
223
- path += " L " + (x + bottomR) + " " + (y + height);
224
- path += " A " + bottomR + " " + bottomR + " 0 0 1 " + x + " " + (y + height - bottomR);
225
- path += " L " + x + " " + (y + topR);
226
- path += " A " + topR + " " + topR + " 0 0 1 " + (x + topR) + " " + y;
248
+ let path = "M " + (x + rTL) + " " + y;
249
+ path += " L " + (x + width - rTR) + " " + y;
250
+ path += " A " + rTR + " " + rTR + " 0 0 1 " + (x + width) + " " + (y + rTR);
251
+ path += " L " + (x + width) + " " + (y + height - rBR);
252
+ path += " A " + rBR + " " + rBR + " 0 0 1 " + (x + width - rBR) + " " + (y + height);
253
+ path += " L " + (x + rBL) + " " + (y + height);
254
+ path += " A " + rBL + " " + rBL + " 0 0 1 " + x + " " + (y + height - rBL);
255
+ path += " L " + x + " " + (y + rTL);
256
+ path += " A " + rTL + " " + rTL + " 0 0 1 " + (x + rTL) + " " + y;
227
257
  path += ' Z';
228
258
  return path;
229
259
  };
@@ -1,4 +1,4 @@
1
- import { 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
@@ -10,19 +10,38 @@ import { applySerializableScale, isCategoricalScale, isLogScale, isNumericScale
10
10
 
11
11
  /**
12
12
  * Get a point from a data value and a scale.
13
- * @note for categorical scales, the point will be centered within the band.
14
- * @note for log scales, zero and negative values are clamped to a small positive value.
15
- * @param data - the data value.
16
- * @param scale - the scale function.
17
- * @returns the pixel value (defaulting to 0 if data value is not defined in scale).
13
+ *
14
+ * @param dataValue - The data value to convert to a pixel position.
15
+ * @param scale - The scale function.
16
+ * @param anchor (@default 'middle') - For band scales, where to anchor the point within the band.
17
+ * @returns The pixel value (@default 0 if data value is not defined in scale).
18
18
  */
19
- export const getPointOnScale = (dataValue, scale) => {
20
- var _scale2;
19
+ export const getPointOnScale = function (dataValue, scale, anchor) {
20
+ var _scale;
21
+ if (anchor === void 0) {
22
+ anchor = 'middle';
23
+ }
21
24
  if (isCategoricalScale(scale)) {
22
- var _scale, _scale$bandwidth;
23
- const bandStart = (_scale = scale(dataValue)) != null ? _scale : 0;
24
- const bandwidth = (_scale$bandwidth = scale.bandwidth()) != null ? _scale$bandwidth : 0;
25
- return bandStart + bandwidth / 2;
25
+ var _bandScale$bandwidth, _bandScale$step;
26
+ const bandScale = scale;
27
+ const bandStart = bandScale(dataValue);
28
+ if (bandStart === undefined) return 0;
29
+ const bandwidth = (_bandScale$bandwidth = bandScale.bandwidth == null ? void 0 : bandScale.bandwidth()) != null ? _bandScale$bandwidth : 0;
30
+ const step = (_bandScale$step = bandScale.step == null ? void 0 : bandScale.step()) != null ? _bandScale$step : bandwidth;
31
+ const paddingOffset = (step - bandwidth) / 2;
32
+ const stepStart = bandStart - paddingOffset;
33
+ switch (anchor) {
34
+ case 'stepStart':
35
+ return stepStart;
36
+ case 'bandStart':
37
+ return bandStart;
38
+ case 'middle':
39
+ return bandStart + bandwidth / 2;
40
+ case 'bandEnd':
41
+ return bandStart + bandwidth;
42
+ case 'stepEnd':
43
+ return stepStart + step;
44
+ }
26
45
  }
27
46
 
28
47
  // For log scales, ensure the value is positive
@@ -30,23 +49,47 @@ export const getPointOnScale = (dataValue, scale) => {
30
49
  if (isLogScale(scale) && dataValue <= 0) {
31
50
  adjustedValue = 0.001; // Use a small positive value for log scales
32
51
  }
33
- return (_scale2 = scale(adjustedValue)) != null ? _scale2 : 0;
52
+ return (_scale = scale(adjustedValue)) != null ? _scale : 0;
34
53
  };
35
54
 
36
55
  /**
37
56
  * Get a point from a data value and a serializable scale (worklet-compatible).
38
- * @note for categorical scales, the point will be centered within the band.
39
- * @note for log scales, zero and negative values are clamped to a small positive value.
40
- * @param dataValue - the data value.
41
- * @param scale - the serializable scale object.
42
- * @returns the pixel value (defaulting to 0 if data value is not defined in scale).
57
+ *
58
+ * @param dataValue - The data value to convert to a pixel position.
59
+ * @param scale - The serializable scale function.
60
+ * @param anchor (@default 'middle') - For band scales, where to anchor the point within the band.
61
+ * @returns The pixel value (@default 0 if data value is not defined in scale).
43
62
  */
44
- export function getPointOnSerializableScale(dataValue, scale) {
63
+ export function getPointOnSerializableScale(dataValue, scale, anchor) {
45
64
  'worklet';
46
65
 
66
+ // Handle band scales with the specified position
67
+ if (anchor === void 0) {
68
+ anchor = 'middle';
69
+ }
47
70
  if (scale.type === 'band') {
48
- const bandStart = applySerializableScale(dataValue, scale);
49
- return bandStart + scale.bandwidth / 2;
71
+ const bandScale = scale;
72
+ const [domainMin, domainMax] = bandScale.domain;
73
+ const index = dataValue - domainMin;
74
+ const n = domainMax - domainMin + 1;
75
+ if (index < 0 || index >= n) {
76
+ return 0;
77
+ }
78
+ const bandStart = applyBandScale(dataValue, bandScale);
79
+ const paddingOffset = (bandScale.step - bandScale.bandwidth) / 2;
80
+ const stepStart = bandStart - paddingOffset;
81
+ switch (anchor) {
82
+ case 'stepStart':
83
+ return stepStart;
84
+ case 'bandStart':
85
+ return bandStart;
86
+ case 'middle':
87
+ return bandStart + bandScale.bandwidth / 2;
88
+ case 'bandEnd':
89
+ return bandStart + bandScale.bandwidth;
90
+ case 'stepEnd':
91
+ return stepStart + bandScale.step;
92
+ }
50
93
  }
51
94
 
52
95
  // For log scales, ensure the value is positive
@@ -121,7 +164,8 @@ export const projectPoints = _ref3 => {
121
164
  xScale,
122
165
  yScale,
123
166
  xData,
124
- yData
167
+ yData,
168
+ layout = 'vertical'
125
169
  } = _ref3;
126
170
  if (data.length === 0) {
127
171
  return [];
@@ -139,28 +183,37 @@ export const projectPoints = _ref3 => {
139
183
  });
140
184
  }
141
185
 
142
- // For scales with axis data, determine the correct x value
143
- let xValue = index;
144
-
145
- // For band scales, always use the index
146
- if (!isCategoricalScale(xScale)) {
147
- // For numeric scales with axis data, use the axis data values instead of indices
148
- if (xData && Array.isArray(xData) && xData.length > 0) {
149
- // Check if it's numeric data
150
- if (typeof xData[0] === 'number') {
151
- var _numericXData$index;
152
- const numericXData = xData;
153
- xValue = (_numericXData$index = numericXData[index]) != null ? _numericXData$index : 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;
190
+
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;
154
198
  }
155
199
  }
156
200
  }
157
- let yValue = value;
158
- if (isNumericScale(yScale) && yData && Array.isArray(yData) && yData.length > 0 && typeof yData[0] === 'number' && typeof value === 'number') {
159
- 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
+ });
160
213
  }
161
214
  return projectPoint({
162
- x: xValue,
163
- y: yValue,
215
+ x: valueAsNumber,
216
+ y: indexValue,
164
217
  xScale,
165
218
  yScale
166
219
  });
@@ -53,10 +53,21 @@ export const getCategoricalScale = _ref2 => {
53
53
  const domainArray = Array.from({
54
54
  length: domain.max - domain.min + 1
55
55
  }, (_, i) => i);
56
- const scale = scaleBand().domain(domainArray).range([range.min, range.max]).padding(padding);
56
+ const scale = scaleBand().domain(domainArray).range([range.min, range.max]).paddingInner(padding).paddingOuter(padding / 2);
57
57
  return scale;
58
58
  };
59
59
 
60
+ /**
61
+ * Anchor position for points on a scale. Currently used only for band scales.
62
+ *
63
+ * For band scales, this determines where within the band to position a point:
64
+ * - `'stepStart'` - At the start of the step
65
+ * - `'bandStart'` - At the start of the band
66
+ * - `'middle'` - At the center of the band
67
+ * - `'bandEnd'` - At the end of the band
68
+ * - `'stepEnd'` - At the end of the step
69
+ */
70
+
60
71
  /**
61
72
  * Convert a D3 scale to a serializable scale configuration that can be used in worklets
62
73
  */
@@ -176,7 +187,7 @@ export function applyBandScale(value, scale) {
176
187
  if (index < 0 || index >= n) {
177
188
  return r0;
178
189
  }
179
- const paddingOffset = step - scale.bandwidth;
190
+ const paddingOffset = (step - scale.bandwidth) / 2;
180
191
  const bandStart = r0 + step * index + paddingOffset;
181
192
  return bandStart;
182
193
  }
@@ -1,20 +1,27 @@
1
1
  /**
2
2
  * Determines which side (left/right) to place scrubber labels based on available space.
3
- * Prefers right side, switches to left when labels would overflow.
3
+ * Honors the preferred side when there's enough space, otherwise switches to the opposite side.
4
4
  */
5
- export const getLabelPosition = function (beaconX, maxLabelWidth, drawingArea, xOffset) {
5
+ export const getLabelPosition = function (beaconX, maxLabelWidth, drawingArea, xOffset, preferredSide) {
6
6
  'worklet';
7
7
 
8
8
  // any regular functions in ui thread must be marked with 'worklet'
9
9
  if (xOffset === void 0) {
10
10
  xOffset = 16;
11
11
  }
12
+ if (preferredSide === void 0) {
13
+ preferredSide = 'right';
14
+ }
12
15
  if (drawingArea.width <= 0 || drawingArea.height <= 0) {
13
- return 'right';
16
+ return preferredSide;
14
17
  }
15
- const availableRightSpace = drawingArea.x + drawingArea.width - beaconX;
16
18
  const requiredSpace = maxLabelWidth + xOffset;
17
- return requiredSpace <= availableRightSpace ? 'right' : 'left';
19
+ if (preferredSide === 'right') {
20
+ const availableSpace = drawingArea.x + drawingArea.width - beaconX;
21
+ return requiredSpace <= availableSpace ? 'right' : 'left';
22
+ }
23
+ const availableSpace = beaconX - drawingArea.x;
24
+ return requiredSpace <= availableSpace ? 'left' : 'right';
18
25
  };
19
26
  /**
20
27
  * Calculates Y positions for all labels avoiding overlaps while maintaining order.