@coinbase/cds-web-visualization 3.4.0-beta.2 → 3.4.0-beta.20

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 (198) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dts/chart/CartesianChart.d.ts +56 -3
  3. package/dts/chart/CartesianChart.d.ts.map +1 -1
  4. package/dts/chart/ChartProvider.d.ts +3 -0
  5. package/dts/chart/ChartProvider.d.ts.map +1 -1
  6. package/dts/chart/Path.d.ts +64 -7
  7. package/dts/chart/Path.d.ts.map +1 -1
  8. package/dts/chart/PeriodSelector.d.ts +5 -15
  9. package/dts/chart/PeriodSelector.d.ts.map +1 -1
  10. package/dts/chart/area/Area.d.ts +50 -25
  11. package/dts/chart/area/Area.d.ts.map +1 -1
  12. package/dts/chart/area/AreaChart.d.ts +46 -6
  13. package/dts/chart/area/AreaChart.d.ts.map +1 -1
  14. package/dts/chart/area/DottedArea.d.ts +21 -44
  15. package/dts/chart/area/DottedArea.d.ts.map +1 -1
  16. package/dts/chart/area/GradientArea.d.ts +21 -12
  17. package/dts/chart/area/GradientArea.d.ts.map +1 -1
  18. package/dts/chart/area/SolidArea.d.ts +16 -1
  19. package/dts/chart/area/SolidArea.d.ts.map +1 -1
  20. package/dts/chart/axis/Axis.d.ts +109 -63
  21. package/dts/chart/axis/Axis.d.ts.map +1 -1
  22. package/dts/chart/axis/DefaultAxisTickLabel.d.ts +8 -0
  23. package/dts/chart/axis/DefaultAxisTickLabel.d.ts.map +1 -0
  24. package/dts/chart/axis/XAxis.d.ts +1 -1
  25. package/dts/chart/axis/XAxis.d.ts.map +1 -1
  26. package/dts/chart/axis/YAxis.d.ts +2 -2
  27. package/dts/chart/axis/YAxis.d.ts.map +1 -1
  28. package/dts/chart/axis/index.d.ts +1 -0
  29. package/dts/chart/axis/index.d.ts.map +1 -1
  30. package/dts/chart/bar/Bar.d.ts +50 -12
  31. package/dts/chart/bar/Bar.d.ts.map +1 -1
  32. package/dts/chart/bar/BarChart.d.ts +20 -8
  33. package/dts/chart/bar/BarChart.d.ts.map +1 -1
  34. package/dts/chart/bar/BarPlot.d.ts +3 -1
  35. package/dts/chart/bar/BarPlot.d.ts.map +1 -1
  36. package/dts/chart/bar/BarStack.d.ts +41 -46
  37. package/dts/chart/bar/BarStack.d.ts.map +1 -1
  38. package/dts/chart/bar/BarStackGroup.d.ts +2 -0
  39. package/dts/chart/bar/BarStackGroup.d.ts.map +1 -1
  40. package/dts/chart/bar/DefaultBar.d.ts.map +1 -1
  41. package/dts/chart/bar/DefaultBarStack.d.ts.map +1 -1
  42. package/dts/chart/gradient/Gradient.d.ts +35 -0
  43. package/dts/chart/gradient/Gradient.d.ts.map +1 -0
  44. package/dts/chart/gradient/index.d.ts +2 -0
  45. package/dts/chart/gradient/index.d.ts.map +1 -0
  46. package/dts/chart/index.d.ts +3 -1
  47. package/dts/chart/index.d.ts.map +1 -1
  48. package/dts/chart/legend/DefaultLegendEntry.d.ts +21 -0
  49. package/dts/chart/legend/DefaultLegendEntry.d.ts.map +1 -0
  50. package/dts/chart/legend/DefaultLegendShape.d.ts +7 -0
  51. package/dts/chart/legend/DefaultLegendShape.d.ts.map +1 -0
  52. package/dts/chart/legend/Legend.d.ts +169 -0
  53. package/dts/chart/legend/Legend.d.ts.map +1 -0
  54. package/dts/chart/legend/index.d.ts +4 -0
  55. package/dts/chart/legend/index.d.ts.map +1 -0
  56. package/dts/chart/line/DefaultReferenceLineLabel.d.ts +9 -0
  57. package/dts/chart/line/DefaultReferenceLineLabel.d.ts.map +1 -0
  58. package/dts/chart/line/DottedLine.d.ts +15 -3
  59. package/dts/chart/line/DottedLine.d.ts.map +1 -1
  60. package/dts/chart/line/Line.d.ts +84 -28
  61. package/dts/chart/line/Line.d.ts.map +1 -1
  62. package/dts/chart/line/LineChart.d.ts +28 -8
  63. package/dts/chart/line/LineChart.d.ts.map +1 -1
  64. package/dts/chart/line/ReferenceLine.d.ts +91 -44
  65. package/dts/chart/line/ReferenceLine.d.ts.map +1 -1
  66. package/dts/chart/line/SolidLine.d.ts +14 -3
  67. package/dts/chart/line/SolidLine.d.ts.map +1 -1
  68. package/dts/chart/line/index.d.ts +1 -1
  69. package/dts/chart/line/index.d.ts.map +1 -1
  70. package/dts/chart/point/DefaultPointLabel.d.ts +10 -0
  71. package/dts/chart/point/DefaultPointLabel.d.ts.map +1 -0
  72. package/dts/chart/point/Point.d.ts +217 -0
  73. package/dts/chart/point/Point.d.ts.map +1 -0
  74. package/dts/chart/point/index.d.ts +3 -0
  75. package/dts/chart/point/index.d.ts.map +1 -0
  76. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts +41 -0
  77. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts.map +1 -0
  78. package/dts/chart/scrubber/DefaultScrubberBeaconLabel.d.ts +12 -0
  79. package/dts/chart/scrubber/DefaultScrubberBeaconLabel.d.ts.map +1 -0
  80. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts +10 -0
  81. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts.map +1 -0
  82. package/dts/chart/scrubber/Scrubber.d.ts +287 -70
  83. package/dts/chart/scrubber/Scrubber.d.ts.map +1 -1
  84. package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts +80 -0
  85. package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts.map +1 -0
  86. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts +47 -0
  87. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts.map +1 -0
  88. package/dts/chart/scrubber/index.d.ts +3 -0
  89. package/dts/chart/scrubber/index.d.ts.map +1 -1
  90. package/dts/chart/text/ChartText.d.ts +46 -43
  91. package/dts/chart/text/ChartText.d.ts.map +1 -1
  92. package/dts/chart/text/{SmartChartTextGroup.d.ts → ChartTextGroup.d.ts} +9 -3
  93. package/dts/chart/text/ChartTextGroup.d.ts.map +1 -0
  94. package/dts/chart/text/index.d.ts +1 -1
  95. package/dts/chart/text/index.d.ts.map +1 -1
  96. package/dts/chart/utils/axis.d.ts +25 -1
  97. package/dts/chart/utils/axis.d.ts.map +1 -1
  98. package/dts/chart/utils/bar.d.ts +34 -0
  99. package/dts/chart/utils/bar.d.ts.map +1 -1
  100. package/dts/chart/utils/chart.d.ts +45 -7
  101. package/dts/chart/utils/chart.d.ts.map +1 -1
  102. package/dts/chart/utils/context.d.ts +6 -0
  103. package/dts/chart/utils/context.d.ts.map +1 -1
  104. package/dts/chart/utils/gradient.d.ts +104 -0
  105. package/dts/chart/utils/gradient.d.ts.map +1 -0
  106. package/dts/chart/utils/index.d.ts +4 -0
  107. package/dts/chart/utils/index.d.ts.map +1 -1
  108. package/dts/chart/utils/interpolate.d.ts +112 -0
  109. package/dts/chart/utils/interpolate.d.ts.map +1 -0
  110. package/dts/chart/utils/path.d.ts +30 -1
  111. package/dts/chart/utils/path.d.ts.map +1 -1
  112. package/dts/chart/utils/point.d.ts +40 -7
  113. package/dts/chart/utils/point.d.ts.map +1 -1
  114. package/dts/chart/utils/scale.d.ts +11 -0
  115. package/dts/chart/utils/scale.d.ts.map +1 -1
  116. package/dts/chart/utils/scrubber.d.ts +40 -0
  117. package/dts/chart/utils/scrubber.d.ts.map +1 -0
  118. package/dts/chart/utils/transition.d.ts +101 -0
  119. package/dts/chart/utils/transition.d.ts.map +1 -0
  120. package/dts/sparkline/sparkline-interactive/SparklineInteractiveMarkerDates.d.ts.map +1 -1
  121. package/esm/chart/CartesianChart.js +170 -83
  122. package/esm/chart/ChartProvider.js +2 -2
  123. package/esm/chart/Path.js +59 -54
  124. package/esm/chart/PeriodSelector.js +36 -32
  125. package/esm/chart/area/Area.js +26 -34
  126. package/esm/chart/area/AreaChart.js +29 -15
  127. package/esm/chart/area/DottedArea.js +39 -89
  128. package/esm/chart/area/GradientArea.js +37 -80
  129. package/esm/chart/area/SolidArea.js +32 -11
  130. package/esm/chart/axis/Axis.js +4 -39
  131. package/esm/chart/axis/DefaultAxisTickLabel.js +15 -0
  132. package/esm/chart/axis/XAxis.js +184 -63
  133. package/esm/chart/axis/YAxis.js +190 -57
  134. package/esm/chart/axis/index.js +1 -0
  135. package/esm/chart/bar/Bar.js +7 -1
  136. package/esm/chart/bar/BarChart.js +17 -32
  137. package/esm/chart/bar/BarPlot.js +5 -2
  138. package/esm/chart/bar/BarStack.js +74 -22
  139. package/esm/chart/bar/BarStackGroup.js +8 -18
  140. package/esm/chart/bar/DefaultBar.js +23 -28
  141. package/esm/chart/bar/DefaultBarStack.js +24 -20
  142. package/esm/chart/gradient/Gradient.js +104 -0
  143. package/esm/chart/gradient/index.js +1 -0
  144. package/esm/chart/index.js +3 -1
  145. package/esm/chart/legend/DefaultLegendEntry.css +1 -0
  146. package/esm/chart/legend/DefaultLegendEntry.js +50 -0
  147. package/esm/chart/legend/DefaultLegendShape.css +5 -0
  148. package/esm/chart/legend/DefaultLegendShape.js +47 -0
  149. package/esm/chart/legend/Legend.js +76 -0
  150. package/esm/chart/legend/index.js +3 -0
  151. package/esm/chart/line/DefaultReferenceLineLabel.js +81 -0
  152. package/esm/chart/line/DottedLine.js +41 -17
  153. package/esm/chart/line/Line.js +87 -75
  154. package/esm/chart/line/LineChart.js +24 -8
  155. package/esm/chart/line/ReferenceLine.js +41 -43
  156. package/esm/chart/line/SolidLine.js +39 -15
  157. package/esm/chart/line/index.js +1 -1
  158. package/esm/chart/{line/GradientLine.js → point/DefaultPointLabel.js} +31 -45
  159. package/esm/chart/point/Point.css +2 -0
  160. package/esm/chart/{Point.js → point/Point.js} +87 -62
  161. package/esm/chart/point/index.js +2 -0
  162. package/esm/chart/scrubber/DefaultScrubberBeacon.js +154 -0
  163. package/esm/chart/scrubber/DefaultScrubberBeaconLabel.js +57 -0
  164. package/esm/chart/scrubber/{ScrubberBeaconLabel.js → DefaultScrubberLabel.js} +15 -18
  165. package/esm/chart/scrubber/Scrubber.js +97 -392
  166. package/esm/chart/scrubber/ScrubberBeaconGroup.js +174 -0
  167. package/esm/chart/scrubber/ScrubberBeaconLabelGroup.js +201 -0
  168. package/esm/chart/scrubber/index.js +3 -1
  169. package/esm/chart/text/ChartText.js +15 -20
  170. package/esm/chart/text/{SmartChartTextGroup.js → ChartTextGroup.js} +4 -3
  171. package/esm/chart/text/index.js +1 -1
  172. package/esm/chart/utils/axis.js +47 -31
  173. package/esm/chart/utils/bar.js +48 -0
  174. package/esm/chart/utils/chart.js +42 -3
  175. package/esm/chart/utils/gradient.js +257 -0
  176. package/esm/chart/utils/index.js +4 -0
  177. package/esm/chart/utils/interpolate.js +644 -0
  178. package/esm/chart/utils/path.js +41 -9
  179. package/esm/chart/utils/point.js +99 -12
  180. package/esm/chart/utils/scale.js +13 -2
  181. package/esm/chart/utils/scrubber.js +137 -0
  182. package/esm/chart/utils/transition.js +133 -0
  183. package/esm/sparkline/__figma__/Sparkline.figma.js +1 -1
  184. package/esm/sparkline/sparkline-interactive/SparklineInteractiveMarkerDates.js +8 -4
  185. package/esm/sparkline/sparkline-interactive/__figma__/SparklineInteractive.figma.js +1 -1
  186. package/esm/sparkline/sparkline-interactive-header/__figma__/SparklineInteractiveHeader.figma.js +1 -1
  187. package/package.json +12 -11
  188. package/dts/chart/Point.d.ts +0 -153
  189. package/dts/chart/Point.d.ts.map +0 -1
  190. package/dts/chart/line/GradientLine.d.ts +0 -42
  191. package/dts/chart/line/GradientLine.d.ts.map +0 -1
  192. package/dts/chart/scrubber/ScrubberBeacon.d.ts +0 -93
  193. package/dts/chart/scrubber/ScrubberBeacon.d.ts.map +0 -1
  194. package/dts/chart/scrubber/ScrubberBeaconLabel.d.ts +0 -7
  195. package/dts/chart/scrubber/ScrubberBeaconLabel.d.ts.map +0 -1
  196. package/dts/chart/text/SmartChartTextGroup.d.ts.map +0 -1
  197. package/esm/chart/Point.css +0 -2
  198. package/esm/chart/scrubber/ScrubberBeacon.js +0 -195
@@ -1,6 +1,15 @@
1
1
  import { area as d3Area, curveBumpX, curveCatmullRom, curveLinear, curveLinearClosed, curveMonotoneX, curveNatural, curveStep, curveStepAfter, curveStepBefore, line as d3Line } from 'd3-shape';
2
2
  import { projectPoint, projectPoints } from './point';
3
3
  import { isCategoricalScale } from './scale';
4
+
5
+ /**
6
+ * Default enter transition for path-based components (Line, Area).
7
+ * `{ type: 'tween', duration: 0.5 }`
8
+ */
9
+ export const defaultPathEnterTransition = {
10
+ type: 'tween',
11
+ duration: 0.5
12
+ };
4
13
  /**
5
14
  * Get the d3 curve function for a path.
6
15
  * See https://d3js.org/d3-shape/curve
@@ -40,17 +49,18 @@ export const getPathCurveFunction = function () {
40
49
  * @example
41
50
  * ```typescript
42
51
  * const chartScale = getChartScale({ chartRect, domain, range, xScale, yScale });
43
- * const path = getLinePath({ data: [1, 2, 3], chartScale, curve: 'linear' });
52
+ * const path = getLinePath({ data: [1, 2, 3], chartScale, curve: 'bump' });
44
53
  * ```
45
54
  */
46
55
  export const getLinePath = _ref => {
47
56
  var _pathGenerator;
48
57
  let {
49
58
  data,
50
- curve = 'linear',
59
+ curve = 'bump',
51
60
  xScale,
52
61
  yScale,
53
- xData
62
+ xData,
63
+ connectNulls
54
64
  } = _ref;
55
65
  if (data.length === 0) {
56
66
  return '';
@@ -62,9 +72,12 @@ export const getLinePath = _ref => {
62
72
  yScale,
63
73
  xData
64
74
  });
65
- const pathGenerator = d3Line().x(d => d.x).y(d => d.y).curve(curveFunction).defined(d => d !== null); // Only draw lines where point is not null
66
75
 
67
- return (_pathGenerator = pathGenerator(dataPoints)) !== null && _pathGenerator !== void 0 ? _pathGenerator : '';
76
+ // When connectNulls is true, filter out null values before rendering
77
+ // When false, use defined() to create gaps in the line
78
+ const filteredPoints = connectNulls ? dataPoints.filter(d => d !== null) : dataPoints;
79
+ const pathGenerator = d3Line().x(d => d.x).y(d => d.y).curve(curveFunction).defined(d => connectNulls || d !== null);
80
+ return (_pathGenerator = pathGenerator(filteredPoints)) !== null && _pathGenerator !== void 0 ? _pathGenerator : '';
68
81
  };
69
82
 
70
83
  /**
@@ -92,10 +105,11 @@ export const getLinePath = _ref => {
92
105
  export const getAreaPath = _ref2 => {
93
106
  let {
94
107
  data,
95
- curve = 'linear',
108
+ curve = 'bump',
96
109
  xScale,
97
110
  yScale,
98
- xData
111
+ xData,
112
+ connectNulls
99
113
  } = _ref2;
100
114
  if (data.length === 0) {
101
115
  return '';
@@ -156,6 +170,10 @@ export const getAreaPath = _ref2 => {
156
170
  isValid: true
157
171
  };
158
172
  });
173
+
174
+ // When connectNulls is true, filter out invalid points before rendering
175
+ // When false, use defined() to create gaps in the area
176
+ const filteredPoints = connectNulls ? dataPoints.filter(d => d.isValid) : dataPoints;
159
177
  const areaGenerator = d3Area().x(d => d.x).y0(d => {
160
178
  var _d$low;
161
179
  return (_d$low = d.low) !== null && _d$low !== void 0 ? _d$low : 0;
@@ -164,12 +182,26 @@ export const getAreaPath = _ref2 => {
164
182
  var _d$high;
165
183
  return (_d$high = d.high) !== null && _d$high !== void 0 ? _d$high : 0;
166
184
  }) // Top boundary (high values), fallback to 0
167
- .curve(curveFunction).defined(d => d.isValid && d.low != null && d.high != null); // Only draw where both values exist
185
+ .curve(curveFunction).defined(d => connectNulls || d.isValid && d.low != null && d.high != null); // Only draw where both values exist
168
186
 
169
- const result = areaGenerator(dataPoints);
187
+ const result = areaGenerator(filteredPoints);
170
188
  return result !== null && result !== void 0 ? result : '';
171
189
  };
172
190
 
191
+ /**
192
+ * Converts line coordinates to an SVG path string.
193
+ * Useful for rendering axis lines and tick marks.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * const path = lineToPath(0, 0, 100, 100);
198
+ * // Returns: "M 0 0 L 100 100"
199
+ * ```
200
+ */
201
+ export const lineToPath = (x1, y1, x2, y2) => {
202
+ return "M".concat(x1, ",").concat(y1, " L").concat(x2, ",").concat(y2);
203
+ };
204
+
173
205
  /**
174
206
  * Creates an SVG path string for a rectangle with selective corner rounding.
175
207
  * Useful for creating bars in charts with optional rounded corners.
@@ -1,20 +1,46 @@
1
1
  import { isCategoricalScale, isLogScale, isNumericScale } from './scale';
2
2
 
3
+ /**
4
+ * Position a label should be placed relative to the point
5
+ *
6
+ * @example
7
+ * 'top' would have the label be located above the point itself,
8
+ * and thus the vertical alignment of that text would be bottom.
9
+ */
10
+
3
11
  /**
4
12
  * Get a point from a data value and a scale.
5
- * @note for categorical scales, the point will be centered within the band.
6
- * @note for log scales, zero and negative values are clamped to a small positive value.
7
- * @param data - the data value.
8
- * @param scale - the scale function.
9
- * @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).
10
18
  */
11
- export const getPointOnScale = (dataValue, scale) => {
12
- var _scale2;
19
+ export const getPointOnScale = function (dataValue, scale) {
20
+ var _scale;
21
+ let anchor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'middle';
13
22
  if (isCategoricalScale(scale)) {
14
- var _scale, _scale$bandwidth;
15
- const bandStart = (_scale = scale(dataValue)) !== null && _scale !== void 0 ? _scale : 0;
16
- const bandwidth = (_scale$bandwidth = scale.bandwidth()) !== null && _scale$bandwidth !== void 0 ? _scale$bandwidth : 0;
17
- return bandStart + bandwidth / 2;
23
+ var _bandScale$bandwidth, _bandScale$bandwidth2, _bandScale$step, _bandScale$step2;
24
+ const bandScale = scale;
25
+ const bandStart = bandScale(dataValue);
26
+ if (bandStart === undefined) return 0;
27
+ const bandwidth = (_bandScale$bandwidth = (_bandScale$bandwidth2 = bandScale.bandwidth) === null || _bandScale$bandwidth2 === void 0 ? void 0 : _bandScale$bandwidth2.call(bandScale)) !== null && _bandScale$bandwidth !== void 0 ? _bandScale$bandwidth : 0;
28
+ const step = (_bandScale$step = (_bandScale$step2 = bandScale.step) === null || _bandScale$step2 === void 0 ? void 0 : _bandScale$step2.call(bandScale)) !== null && _bandScale$step !== void 0 ? _bandScale$step : bandwidth;
29
+ const paddingOffset = (step - bandwidth) / 2;
30
+ const stepStart = bandStart - paddingOffset;
31
+ switch (anchor) {
32
+ case 'stepStart':
33
+ return stepStart;
34
+ case 'bandStart':
35
+ return bandStart;
36
+ case 'bandEnd':
37
+ return bandStart + bandwidth;
38
+ case 'stepEnd':
39
+ return stepStart + step;
40
+ case 'middle':
41
+ default:
42
+ return bandStart + bandwidth / 2;
43
+ }
18
44
  }
19
45
 
20
46
  // For log scales, ensure the value is positive
@@ -22,7 +48,7 @@ export const getPointOnScale = (dataValue, scale) => {
22
48
  if (isLogScale(scale) && dataValue <= 0) {
23
49
  adjustedValue = 0.001; // Use a small positive value for log scales
24
50
  }
25
- return (_scale2 = scale(adjustedValue)) !== null && _scale2 !== void 0 ? _scale2 : 0;
51
+ return (_scale = scale(adjustedValue)) !== null && _scale !== void 0 ? _scale : 0;
26
52
  };
27
53
 
28
54
  /**
@@ -115,4 +141,65 @@ export const projectPoints = _ref2 => {
115
141
  yScale
116
142
  });
117
143
  });
144
+ };
145
+
146
+ /**
147
+ * Determines text alignment based on label position.
148
+ * For example, a 'top' position needs the text aligned to the 'bottom' so it appears above the point.
149
+ */
150
+ export const getAlignmentFromPosition = position => {
151
+ let horizontalAlignment = 'center';
152
+ let verticalAlignment = 'middle';
153
+ switch (position) {
154
+ case 'top':
155
+ verticalAlignment = 'bottom';
156
+ break;
157
+ case 'bottom':
158
+ verticalAlignment = 'top';
159
+ break;
160
+ case 'left':
161
+ horizontalAlignment = 'right';
162
+ break;
163
+ case 'right':
164
+ horizontalAlignment = 'left';
165
+ break;
166
+ case 'center':
167
+ default:
168
+ horizontalAlignment = 'center';
169
+ verticalAlignment = 'middle';
170
+ break;
171
+ }
172
+ return {
173
+ horizontalAlignment,
174
+ verticalAlignment
175
+ };
176
+ };
177
+
178
+ /**
179
+ * Calculates the final label coordinates by applying offset based on position.
180
+ */
181
+ export const getLabelCoordinates = (x, y, position, offset) => {
182
+ let dx = 0;
183
+ let dy = 0;
184
+ switch (position) {
185
+ case 'top':
186
+ dy = -offset;
187
+ break;
188
+ case 'bottom':
189
+ dy = offset;
190
+ break;
191
+ case 'left':
192
+ dx = -offset;
193
+ break;
194
+ case 'right':
195
+ dx = offset;
196
+ break;
197
+ case 'center':
198
+ default:
199
+ break;
200
+ }
201
+ return {
202
+ x: x + dx,
203
+ y: y + dy
204
+ };
118
205
  };
@@ -43,6 +43,17 @@ export const getCategoricalScale = _ref2 => {
43
43
  const domainArray = Array.from({
44
44
  length: domain.max - domain.min + 1
45
45
  }, (_, i) => i);
46
- const scale = scaleBand().domain(domainArray).range([range.min, range.max]).padding(padding);
46
+ const scale = scaleBand().domain(domainArray).range([range.min, range.max]).paddingInner(padding).paddingOuter(padding / 2);
47
47
  return scale;
48
- };
48
+ };
49
+
50
+ /**
51
+ * Anchor position for points on a scale. Currently used only for band scales.
52
+ *
53
+ * For band scales, this determines where within the band to position a point:
54
+ * - `'stepStart'` - At the start of the step
55
+ * - `'bandStart'` - At the start of the band
56
+ * - `'middle'` - At the center of the band
57
+ * - `'bandEnd'` - At the end of the band
58
+ * - `'stepEnd'` - At the end of the step
59
+ */
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Determines which side (left/right) to place scrubber labels based on available space.
3
+ * Honors the preferred side when there's enough space, otherwise switches to the opposite side.
4
+ */
5
+ export const getLabelPosition = function (beaconX, maxLabelWidth, drawingArea) {
6
+ let xOffset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 16;
7
+ let preferredSide = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 'right';
8
+ if (drawingArea.width <= 0 || drawingArea.height <= 0) {
9
+ return preferredSide;
10
+ }
11
+ const requiredSpace = maxLabelWidth + xOffset;
12
+ if (preferredSide === 'right') {
13
+ const availableSpace = drawingArea.x + drawingArea.width - beaconX;
14
+ return requiredSpace <= availableSpace ? 'right' : 'left';
15
+ }
16
+ const availableSpace = beaconX - drawingArea.x;
17
+ return requiredSpace <= availableSpace ? 'left' : 'right';
18
+ };
19
+ /**
20
+ * Calculates Y positions for all labels avoiding overlaps while maintaining order.
21
+ */
22
+ export const calculateLabelYPositions = (dimensions, drawingArea, labelHeight, minGap) => {
23
+ if (dimensions.length === 0) {
24
+ return new Map();
25
+ }
26
+
27
+ // Sort by preferred Y values and create working labels
28
+ const sortedLabels = [...dimensions].sort((a, b) => a.preferredY - b.preferredY).map(dim => ({
29
+ seriesId: dim.seriesId,
30
+ preferredY: dim.preferredY,
31
+ finalY: dim.preferredY
32
+ }));
33
+
34
+ // Initial bounds fitting
35
+ const minY = drawingArea.y + labelHeight / 2;
36
+ const maxY = drawingArea.y + drawingArea.height - labelHeight / 2;
37
+ const requiredDistance = labelHeight + minGap;
38
+ for (const label of sortedLabels) {
39
+ // Clamp each label to the drawing area
40
+ label.finalY = Math.max(minY, Math.min(maxY, label.preferredY));
41
+ }
42
+
43
+ // First pass: push down any overlapping labels
44
+ for (let i = 1; i < sortedLabels.length; i++) {
45
+ const prev = sortedLabels[i - 1];
46
+ const current = sortedLabels[i];
47
+ const minAllowedY = prev.finalY + requiredDistance;
48
+ if (current.finalY < minAllowedY) {
49
+ current.finalY = minAllowedY;
50
+ }
51
+ }
52
+
53
+ // Find collision groups - groups of labels that are tightly packed (gap < minGap between them)
54
+ const collisionGroups = [];
55
+ let currentGroup = [sortedLabels[0]];
56
+ for (let i = 1; i < sortedLabels.length; i++) {
57
+ const prev = sortedLabels[i - 1];
58
+ const current = sortedLabels[i];
59
+ const gap = current.finalY - prev.finalY - labelHeight;
60
+ if (gap < minGap + 0.01) {
61
+ // Labels are touching or very close - part of same collision group
62
+ currentGroup.push(current);
63
+ } else {
64
+ // Gap is large enough - start new group
65
+ collisionGroups.push(currentGroup);
66
+ currentGroup = [current];
67
+ }
68
+ }
69
+ collisionGroups.push(currentGroup);
70
+
71
+ // Process each collision group - optimize positioning to minimize displacement
72
+ for (const group of collisionGroups) {
73
+ if (group.length === 1) {
74
+ // Single label, already at best position
75
+ continue;
76
+ }
77
+ const groupLastLabel = group[group.length - 1];
78
+ const groupFirstLabel = group[0];
79
+ const groupOverflow = groupLastLabel.finalY + labelHeight / 2 - (drawingArea.y + drawingArea.height);
80
+
81
+ // Calculate the ideal center point for this group
82
+ const groupPreferredCenter = group.reduce((sum, label) => sum + label.preferredY, 0) / group.length;
83
+ const groupTotalNeeded = group.length * labelHeight + (group.length - 1) * minGap;
84
+ if (groupOverflow <= 0) {
85
+ // Group fits, but let's center it better if possible
86
+ // Calculate how much we can shift up/down to center around preferred positions
87
+ const currentCenter = (groupFirstLabel.finalY + groupLastLabel.finalY) / 2;
88
+ const desiredShift = groupPreferredCenter - currentCenter;
89
+
90
+ // Calculate max shift in each direction
91
+ const maxShiftUp = groupFirstLabel.finalY - minY;
92
+ const maxShiftDown = maxY - groupLastLabel.finalY;
93
+
94
+ // Apply the shift, constrained by boundaries
95
+ const actualShift = Math.max(-maxShiftUp, Math.min(maxShiftDown, desiredShift));
96
+ if (Math.abs(actualShift) > 0.01) {
97
+ for (const label of group) {
98
+ label.finalY += actualShift;
99
+ }
100
+ }
101
+ } else {
102
+ // Group overflows - need to adjust
103
+ const groupStartY = groupFirstLabel.finalY - labelHeight / 2;
104
+ const availableSpace = drawingArea.y + drawingArea.height - groupStartY;
105
+ const maxShiftUp = groupFirstLabel.finalY - minY;
106
+ if (maxShiftUp >= groupOverflow) {
107
+ // Can shift entire group up to fit
108
+ for (const label of group) {
109
+ label.finalY -= groupOverflow;
110
+ }
111
+ } else if (groupTotalNeeded <= availableSpace) {
112
+ // Can't shift enough, but there's room - redistribute with proper spacing
113
+ let currentY = Math.max(minY, groupFirstLabel.finalY - maxShiftUp);
114
+ const gap = (availableSpace - group.length * labelHeight) / Math.max(1, group.length - 1);
115
+ for (const label of group) {
116
+ label.finalY = currentY;
117
+ currentY += labelHeight + gap;
118
+ }
119
+ } else {
120
+ // Not enough space even with compression - compress gaps and fit to bottom
121
+ const compressedGap = Math.max(1, (availableSpace - group.length * labelHeight) / Math.max(1, group.length - 1));
122
+ // Position so last label is at maxY
123
+ let currentY = maxY - (group.length - 1) * (labelHeight + compressedGap);
124
+ currentY = Math.max(minY, currentY);
125
+ for (const label of group) {
126
+ label.finalY = currentY;
127
+ currentY += labelHeight + compressedGap;
128
+ }
129
+ }
130
+ }
131
+ }
132
+ const result = new Map();
133
+ for (const label of sortedLabels) {
134
+ result.set(label.seriesId, label.finalY);
135
+ }
136
+ return result;
137
+ };
@@ -0,0 +1,133 @@
1
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
2
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
3
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
4
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
5
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
6
+ import { useEffect, useRef } from 'react';
7
+ import { interpolatePath } from 'd3-interpolate-path';
8
+ import { animate, useMotionValue } from 'framer-motion';
9
+
10
+ /**
11
+ * Default update transition used across all chart components.
12
+ * `{ type: 'spring', stiffness: 900, damping: 120, mass: 4 }`
13
+ */
14
+ export const defaultTransition = {
15
+ type: 'spring',
16
+ stiffness: 900,
17
+ damping: 120,
18
+ mass: 4
19
+ };
20
+
21
+ /**
22
+ * Instant transition that completes immediately with no animation.
23
+ * Used when a transition is set to `null`.
24
+ */
25
+ export const instantTransition = {
26
+ type: 'tween',
27
+ duration: 0
28
+ };
29
+
30
+ /**
31
+ * Duration in seconds for accessory elements to fade in.
32
+ */
33
+ export const accessoryFadeTransitionDuration = 0.15;
34
+
35
+ /**
36
+ * Delay in seconds before accessory elements fade in.
37
+ */
38
+ export const accessoryFadeTransitionDelay = 0.35;
39
+
40
+ /**
41
+ * Default enter transition for accessory elements (Point, Scrubber beacons).
42
+ * `{ type: 'tween', duration: 0.15, delay: 0.35 }`
43
+ */
44
+ export const defaultAccessoryEnterTransition = {
45
+ type: 'tween',
46
+ duration: accessoryFadeTransitionDuration,
47
+ delay: accessoryFadeTransitionDelay
48
+ };
49
+
50
+ /**
51
+ * Resolves a transition value based on the animation state and a default.
52
+ * @note Passing in null will disable an animation.
53
+ * @note Passing in undefined will use the provided default.
54
+ */
55
+ export const getTransition = (value, animate, defaultValue) => {
56
+ if (!animate || value === null) return instantTransition;
57
+ return value !== null && value !== void 0 ? value : defaultValue;
58
+ };
59
+
60
+ /**
61
+ * Hook for path animation state and transitions.
62
+ *
63
+ * @param currentPath - Current target path to animate to
64
+ * @param initialPath - Initial path for enter animation. When provided, the first animation will go from initialPath to currentPath.
65
+ * @param transitions - Transition configuration for enter and update animations
66
+ * @returns MotionValue containing the current interpolated path string
67
+ *
68
+ * @example
69
+ * // Simple path transition
70
+ * const animatedPath = usePathTransition({
71
+ * currentPath: d ?? '',
72
+ * transitions: {
73
+ * update: { type: 'spring', stiffness: 300, damping: 20 },
74
+ * },
75
+ * });
76
+ *
77
+ * @example
78
+ * // Enter animation with different initial config (like DefaultBar)
79
+ * const animatedPath = usePathTransition({
80
+ * currentPath: targetPath,
81
+ * initialPath: baselinePath,
82
+ * transitions: {
83
+ * enter: { type: 'tween', duration: 0.5 },
84
+ * update: { type: 'spring', stiffness: 900, damping: 120, mass: 4 },
85
+ * },
86
+ * });
87
+ */
88
+ export const usePathTransition = _ref => {
89
+ var _transitions$update;
90
+ let {
91
+ currentPath,
92
+ initialPath,
93
+ transitions,
94
+ transition = defaultTransition
95
+ } = _ref;
96
+ const updateTransition = (_transitions$update = transitions === null || transitions === void 0 ? void 0 : transitions.update) !== null && _transitions$update !== void 0 ? _transitions$update : transition;
97
+ const enterTransition = transitions === null || transitions === void 0 ? void 0 : transitions.enter;
98
+ const previousPathRef = useRef(initialPath !== null && initialPath !== void 0 ? initialPath : currentPath);
99
+ const targetPathRef = useRef(initialPath !== null && initialPath !== void 0 ? initialPath : currentPath);
100
+ const animationRef = useRef(null);
101
+ const isFirstAnimation = useRef(!!initialPath);
102
+ const animatedPath = useMotionValue(initialPath !== null && initialPath !== void 0 ? initialPath : currentPath);
103
+ useEffect(() => {
104
+ if (targetPathRef.current !== currentPath) {
105
+ const currentVisualPath = animatedPath.get();
106
+ if (animationRef.current) {
107
+ animationRef.current.stop();
108
+ animationRef.current = null;
109
+ previousPathRef.current = currentVisualPath;
110
+ }
111
+ targetPathRef.current = currentPath;
112
+ const activeTransition = isFirstAnimation.current && enterTransition !== undefined ? enterTransition : updateTransition;
113
+ isFirstAnimation.current = false;
114
+ const pathInterpolator = interpolatePath(previousPathRef.current, currentPath);
115
+ animationRef.current = animate(0, 1, _objectSpread(_objectSpread({}, activeTransition), {}, {
116
+ onUpdate: latest => {
117
+ animatedPath.set(pathInterpolator(latest));
118
+ },
119
+ onComplete: () => {
120
+ animatedPath.set(currentPath);
121
+ previousPathRef.current = currentPath;
122
+ animationRef.current = null;
123
+ }
124
+ }));
125
+ }
126
+ return () => {
127
+ if (animationRef.current) {
128
+ animationRef.current.stop();
129
+ }
130
+ };
131
+ }, [currentPath, updateTransition, enterTransition, animatedPath]);
132
+ return animatedPath;
133
+ };
@@ -5,7 +5,7 @@ import { figma } from '@figma/code-connect';
5
5
  import { Sparkline } from '../Sparkline';
6
6
  import { jsx as _jsx } from "react/jsx-runtime";
7
7
  figma.connect(Sparkline, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/✨-CDS-Components?node-id=320-15040&m=dev', {
8
- imports: ["import { Sparkline } from '@coinbase/cds-web-visualization';", "import { useSparklinePath } from '@coinbase/cds-common/visualizations/useSparklinePath';"],
8
+ imports: ["import { Sparkline } from '@coinbase/cds-web-visualization'", "import { useSparklinePath } from '@coinbase/cds-common/visualizations/useSparklinePath'"],
9
9
  example: () => {
10
10
  const data = [20, 30, 5, 45, 0];
11
11
  // @ts-expect-error: useSparklinePath is not typed correctly
@@ -13,14 +13,15 @@ const noPointerEvents = {
13
13
  };
14
14
  const SparklineInteractiveMarkerDate = /*#__PURE__*/memo(_ref => {
15
15
  let {
16
- getFormattedDate
16
+ getFormattedDate,
17
+ containerOffsetLeft
17
18
  } = _ref;
18
19
  const [xPos, setXPos] = useState(0);
19
20
  const setupRef = useCallback(ref => {
20
21
  if (ref) {
21
- setXPos(ref.offsetLeft + ref.offsetWidth / 2);
22
+ setXPos(ref.offsetLeft + ref.offsetWidth / 2 - containerOffsetLeft);
22
23
  }
23
- }, []);
24
+ }, [containerOffsetLeft]);
24
25
  const dateStr = getFormattedDate(xPos);
25
26
 
26
27
  // take up space while loading so when it finishes loading there is no jump
@@ -48,6 +49,7 @@ function SparklineInteractiveMarkerDatesWithGeneric(_ref2) {
48
49
  timePeriodGutter = 2
49
50
  } = _ref2;
50
51
  const [numberOfLabels, setNumberOfLabels] = useState(0);
52
+ const [containerOffsetLeft, setContainerOffsetLeft] = useState(0);
51
53
  const getFormattedDate = useDateLookup({
52
54
  getMarker,
53
55
  formatDate,
@@ -57,15 +59,17 @@ function SparklineInteractiveMarkerDatesWithGeneric(_ref2) {
57
59
  if (ref) {
58
60
  const numberOfLabelsFromWidth = Math.floor(ref.offsetWidth / labelWidth);
59
61
  setNumberOfLabels(Math.max(numberOfLabelsFromWidth, 4));
62
+ setContainerOffsetLeft(ref.offsetLeft);
60
63
  }
61
64
  }, []);
62
65
  const markers = useMemo(() => {
63
66
  return times(numberOfLabels).map((_, i) => {
64
67
  return /*#__PURE__*/_jsx(SparklineInteractiveMarkerDate, {
68
+ containerOffsetLeft: containerOffsetLeft,
65
69
  getFormattedDate: getFormattedDate
66
70
  }, i);
67
71
  });
68
- }, [getFormattedDate, numberOfLabels]);
72
+ }, [containerOffsetLeft, getFormattedDate, numberOfLabels]);
69
73
  return /*#__PURE__*/_jsx(HStack, {
70
74
  ref: setupRef,
71
75
  className: fadeInCss,
@@ -8,7 +8,7 @@ import { figma } from '@figma/code-connect';
8
8
  import { SparklineInteractive } from '../SparklineInteractive';
9
9
  import { jsx as _jsx } from "react/jsx-runtime";
10
10
  figma.connect(SparklineInteractive, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/✨-CDS-Components?node-id=320-14858&m=dev', {
11
- imports: ["import { SparklineInteractive } from '@coinbase/cds-web-visualization';"],
11
+ imports: ["import { SparklineInteractive } from '@coinbase/cds-web-visualization'"],
12
12
  props: {
13
13
  compact: figma.boolean('compact'),
14
14
  disableScrubbing: figma.boolean('scrubbing', {
@@ -9,7 +9,7 @@ import { SparklineInteractive } from '../../sparkline-interactive/SparklineInter
9
9
  import { SparklineInteractiveHeader } from '../SparklineInteractiveHeader';
10
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
11
  figma.connect(SparklineInteractiveHeader, 'https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/✨-CDS-Components?node-id=320-14931&m=dev', {
12
- imports: ["import { SparklineInteractiveHeader } from '@coinbase/cds-web-visualization';", "import { SparklineInteractive } from '@coinbase/cds-web-visualization';"],
12
+ imports: ["import { SparklineInteractiveHeader } from '@coinbase/cds-web-visualization'", "import { SparklineInteractive } from '@coinbase/cds-web-visualization'"],
13
13
  props: {
14
14
  compact: figma.boolean('compact'),
15
15
  disableScrubbing: figma.boolean('scrubbing', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cds-web-visualization",
3
- "version": "3.4.0-beta.2",
3
+ "version": "3.4.0-beta.20",
4
4
  "description": "Coinbase Design System - Web Sparkline",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,10 +38,11 @@
38
38
  "CHANGELOG"
39
39
  ],
40
40
  "peerDependencies": {
41
- "@coinbase/cds-common": "^8.14.0",
42
- "@coinbase/cds-lottie-files": "^3.3.2",
43
- "@coinbase/cds-utils": "^2.3.3",
44
- "@coinbase/cds-web": "^8.14.0",
41
+ "@coinbase/cds-common": "^8.49.0",
42
+ "@coinbase/cds-lottie-files": "^3.3.4",
43
+ "@coinbase/cds-utils": "^2.3.5",
44
+ "@coinbase/cds-web": "^8.49.0",
45
+ "framer-motion": "^10.18.0",
45
46
  "react": "^18.3.1",
46
47
  "react-dom": "^18.3.1"
47
48
  },
@@ -58,13 +59,13 @@
58
59
  "@babel/preset-env": "^7.28.0",
59
60
  "@babel/preset-react": "^7.27.1",
60
61
  "@babel/preset-typescript": "^7.27.1",
61
- "@coinbase/cds-common": "^8.14.0",
62
- "@coinbase/cds-lottie-files": "^3.3.2",
63
- "@coinbase/cds-utils": "^2.3.3",
64
- "@coinbase/cds-web": "^8.14.0",
65
- "@figma/code-connect": "^1.3.4",
62
+ "@coinbase/cds-common": "^8.49.0",
63
+ "@coinbase/cds-lottie-files": "^3.3.4",
64
+ "@coinbase/cds-utils": "^2.3.5",
65
+ "@coinbase/cds-web": "^8.49.0",
66
66
  "@linaria/core": "^3.0.0-beta.22",
67
67
  "@types/react": "^18.3.12",
68
- "@types/react-dom": "^18.3.1"
68
+ "@types/react-dom": "^18.3.1",
69
+ "framer-motion": "^10.18.0"
69
70
  }
70
71
  }