@coinbase/cds-mobile-visualization 3.4.0-beta.1 → 3.4.0-beta.10

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 (184) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dts/chart/CartesianChart.d.ts +57 -33
  3. package/dts/chart/CartesianChart.d.ts.map +1 -1
  4. package/dts/chart/ChartContextBridge.d.ts +28 -0
  5. package/dts/chart/ChartContextBridge.d.ts.map +1 -0
  6. package/dts/chart/Path.d.ts +77 -34
  7. package/dts/chart/Path.d.ts.map +1 -1
  8. package/dts/chart/PeriodSelector.d.ts +2 -2
  9. package/dts/chart/PeriodSelector.d.ts.map +1 -1
  10. package/dts/chart/area/Area.d.ts +42 -27
  11. package/dts/chart/area/Area.d.ts.map +1 -1
  12. package/dts/chart/area/AreaChart.d.ts +51 -10
  13. package/dts/chart/area/AreaChart.d.ts.map +1 -1
  14. package/dts/chart/area/DottedArea.d.ts +21 -2
  15. package/dts/chart/area/DottedArea.d.ts.map +1 -1
  16. package/dts/chart/area/GradientArea.d.ts +19 -13
  17. package/dts/chart/area/GradientArea.d.ts.map +1 -1
  18. package/dts/chart/area/SolidArea.d.ts +17 -2
  19. package/dts/chart/area/SolidArea.d.ts.map +1 -1
  20. package/dts/chart/axis/Axis.d.ts +68 -78
  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 +16 -13
  31. package/dts/chart/bar/Bar.d.ts.map +1 -1
  32. package/dts/chart/bar/BarChart.d.ts +36 -20
  33. package/dts/chart/bar/BarChart.d.ts.map +1 -1
  34. package/dts/chart/bar/BarPlot.d.ts +2 -1
  35. package/dts/chart/bar/BarPlot.d.ts.map +1 -1
  36. package/dts/chart/bar/BarStack.d.ts +39 -48
  37. package/dts/chart/bar/BarStack.d.ts.map +1 -1
  38. package/dts/chart/bar/BarStackGroup.d.ts +1 -0
  39. package/dts/chart/bar/BarStackGroup.d.ts.map +1 -1
  40. package/dts/chart/bar/DefaultBar.d.ts +1 -1
  41. package/dts/chart/bar/DefaultBar.d.ts.map +1 -1
  42. package/dts/chart/bar/DefaultBarStack.d.ts.map +1 -1
  43. package/dts/chart/gradient/Gradient.d.ts +25 -0
  44. package/dts/chart/gradient/Gradient.d.ts.map +1 -0
  45. package/dts/chart/gradient/index.d.ts +2 -0
  46. package/dts/chart/gradient/index.d.ts.map +1 -0
  47. package/dts/chart/index.d.ts +3 -1
  48. package/dts/chart/index.d.ts.map +1 -1
  49. package/dts/chart/line/DefaultReferenceLineLabel.d.ts +9 -0
  50. package/dts/chart/line/DefaultReferenceLineLabel.d.ts.map +1 -0
  51. package/dts/chart/line/DottedLine.d.ts +13 -5
  52. package/dts/chart/line/DottedLine.d.ts.map +1 -1
  53. package/dts/chart/line/Line.d.ts +62 -25
  54. package/dts/chart/line/Line.d.ts.map +1 -1
  55. package/dts/chart/line/LineChart.d.ts +43 -9
  56. package/dts/chart/line/LineChart.d.ts.map +1 -1
  57. package/dts/chart/line/ReferenceLine.d.ts +68 -20
  58. package/dts/chart/line/ReferenceLine.d.ts.map +1 -1
  59. package/dts/chart/line/SolidLine.d.ts +8 -5
  60. package/dts/chart/line/SolidLine.d.ts.map +1 -1
  61. package/dts/chart/line/index.d.ts +1 -1
  62. package/dts/chart/line/index.d.ts.map +1 -1
  63. package/dts/chart/point/DefaultPointLabel.d.ts +10 -0
  64. package/dts/chart/point/DefaultPointLabel.d.ts.map +1 -0
  65. package/dts/chart/point/Point.d.ts +120 -0
  66. package/dts/chart/point/Point.d.ts.map +1 -0
  67. package/dts/chart/point/index.d.ts +3 -0
  68. package/dts/chart/point/index.d.ts.map +1 -0
  69. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts +8 -0
  70. package/dts/chart/scrubber/DefaultScrubberBeacon.d.ts.map +1 -0
  71. package/dts/chart/scrubber/DefaultScrubberBeaconLabel.d.ts +12 -0
  72. package/dts/chart/scrubber/DefaultScrubberBeaconLabel.d.ts.map +1 -0
  73. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts +11 -0
  74. package/dts/chart/scrubber/DefaultScrubberLabel.d.ts.map +1 -0
  75. package/dts/chart/scrubber/Scrubber.d.ts +172 -43
  76. package/dts/chart/scrubber/Scrubber.d.ts.map +1 -1
  77. package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts +44 -0
  78. package/dts/chart/scrubber/ScrubberBeaconGroup.d.ts.map +1 -0
  79. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts +31 -0
  80. package/dts/chart/scrubber/ScrubberBeaconLabelGroup.d.ts.map +1 -0
  81. package/dts/chart/scrubber/ScrubberProvider.d.ts +6 -3
  82. package/dts/chart/scrubber/ScrubberProvider.d.ts.map +1 -1
  83. package/dts/chart/scrubber/index.d.ts +3 -0
  84. package/dts/chart/scrubber/index.d.ts.map +1 -1
  85. package/dts/chart/text/ChartText.d.ts +151 -77
  86. package/dts/chart/text/ChartText.d.ts.map +1 -1
  87. package/dts/chart/text/{SmartChartTextGroup.d.ts → ChartTextGroup.d.ts} +9 -3
  88. package/dts/chart/text/ChartTextGroup.d.ts.map +1 -0
  89. package/dts/chart/text/index.d.ts +1 -1
  90. package/dts/chart/text/index.d.ts.map +1 -1
  91. package/dts/chart/utils/chart.d.ts +34 -7
  92. package/dts/chart/utils/chart.d.ts.map +1 -1
  93. package/dts/chart/utils/context.d.ts +28 -7
  94. package/dts/chart/utils/context.d.ts.map +1 -1
  95. package/dts/chart/utils/gradient.d.ts +117 -0
  96. package/dts/chart/utils/gradient.d.ts.map +1 -0
  97. package/dts/chart/utils/index.d.ts +3 -0
  98. package/dts/chart/utils/index.d.ts.map +1 -1
  99. package/dts/chart/utils/path.d.ts +53 -0
  100. package/dts/chart/utils/path.d.ts.map +1 -1
  101. package/dts/chart/utils/point.d.ts +60 -1
  102. package/dts/chart/utils/point.d.ts.map +1 -1
  103. package/dts/chart/utils/scale.d.ts +91 -0
  104. package/dts/chart/utils/scale.d.ts.map +1 -1
  105. package/dts/chart/utils/scrubber.d.ts +39 -0
  106. package/dts/chart/utils/scrubber.d.ts.map +1 -0
  107. package/dts/chart/utils/transition.d.ts +140 -0
  108. package/dts/chart/utils/transition.d.ts.map +1 -0
  109. package/esm/chart/CartesianChart.js +164 -70
  110. package/esm/chart/ChartContextBridge.js +148 -0
  111. package/esm/chart/Path.js +198 -113
  112. package/esm/chart/PeriodSelector.js +2 -2
  113. package/esm/chart/__stories__/CartesianChart.stories.js +378 -131
  114. package/esm/chart/__stories__/Chart.stories.js +2 -4
  115. package/esm/chart/__stories__/PeriodSelector.stories.js +103 -75
  116. package/esm/chart/area/Area.js +25 -35
  117. package/esm/chart/area/AreaChart.js +17 -12
  118. package/esm/chart/area/DottedArea.js +61 -109
  119. package/esm/chart/area/GradientArea.js +35 -91
  120. package/esm/chart/area/SolidArea.js +22 -8
  121. package/esm/chart/area/__stories__/AreaChart.stories.js +1 -1
  122. package/esm/chart/axis/Axis.js +2 -0
  123. package/esm/chart/axis/DefaultAxisTickLabel.js +11 -0
  124. package/esm/chart/axis/XAxis.js +63 -56
  125. package/esm/chart/axis/YAxis.js +59 -52
  126. package/esm/chart/axis/__stories__/Axis.stories.js +0 -1
  127. package/esm/chart/axis/index.js +1 -0
  128. package/esm/chart/bar/Bar.js +3 -1
  129. package/esm/chart/bar/BarChart.js +15 -37
  130. package/esm/chart/bar/BarPlot.js +41 -35
  131. package/esm/chart/bar/BarStack.js +75 -38
  132. package/esm/chart/bar/BarStackGroup.js +6 -16
  133. package/esm/chart/bar/DefaultBar.js +26 -48
  134. package/esm/chart/bar/DefaultBarStack.js +23 -58
  135. package/esm/chart/bar/__stories__/BarChart.stories.js +463 -77
  136. package/esm/chart/gradient/Gradient.js +53 -0
  137. package/esm/chart/gradient/index.js +1 -0
  138. package/esm/chart/index.js +3 -1
  139. package/esm/chart/line/DefaultReferenceLineLabel.js +66 -0
  140. package/esm/chart/line/DottedLine.js +29 -14
  141. package/esm/chart/line/Line.js +106 -67
  142. package/esm/chart/line/LineChart.js +20 -14
  143. package/esm/chart/line/ReferenceLine.js +80 -63
  144. package/esm/chart/line/SolidLine.js +25 -10
  145. package/esm/chart/line/__stories__/LineChart.stories.js +2101 -1977
  146. package/esm/chart/line/__stories__/ReferenceLine.stories.js +83 -28
  147. package/esm/chart/line/index.js +1 -1
  148. package/esm/chart/point/DefaultPointLabel.js +39 -0
  149. package/esm/chart/point/Point.js +188 -0
  150. package/esm/chart/point/index.js +2 -0
  151. package/esm/chart/scrubber/DefaultScrubberBeacon.js +179 -0
  152. package/esm/chart/scrubber/DefaultScrubberBeaconLabel.js +43 -0
  153. package/esm/chart/scrubber/DefaultScrubberLabel.js +28 -0
  154. package/esm/chart/scrubber/Scrubber.js +126 -146
  155. package/esm/chart/scrubber/ScrubberBeaconGroup.js +161 -0
  156. package/esm/chart/scrubber/ScrubberBeaconLabelGroup.js +185 -0
  157. package/esm/chart/scrubber/ScrubberProvider.js +46 -54
  158. package/esm/chart/scrubber/index.js +3 -1
  159. package/esm/chart/text/ChartText.js +242 -174
  160. package/esm/chart/text/{SmartChartTextGroup.js → ChartTextGroup.js} +6 -5
  161. package/esm/chart/text/index.js +1 -1
  162. package/esm/chart/utils/chart.js +44 -3
  163. package/esm/chart/utils/gradient.js +305 -0
  164. package/esm/chart/utils/index.js +3 -0
  165. package/esm/chart/utils/path.js +76 -8
  166. package/esm/chart/utils/point.js +116 -5
  167. package/esm/chart/utils/scale.js +230 -1
  168. package/esm/chart/utils/scrubber.js +139 -0
  169. package/esm/chart/utils/transition.js +185 -0
  170. package/esm/sparkline/__stories__/Sparkline.stories.js +11 -7
  171. package/esm/sparkline/__stories__/SparklineGradient.stories.js +7 -4
  172. package/esm/sparkline/sparkline-interactive/__stories__/SparklineInteractive.stories.js +51 -26
  173. package/esm/sparkline/sparkline-interactive-header/__stories__/SparklineInteractiveHeader.stories.js +17 -9
  174. package/package.json +15 -9
  175. package/dts/chart/Point.d.ts +0 -103
  176. package/dts/chart/Point.d.ts.map +0 -1
  177. package/dts/chart/line/GradientLine.d.ts +0 -45
  178. package/dts/chart/line/GradientLine.d.ts.map +0 -1
  179. package/dts/chart/scrubber/ScrubberBeacon.d.ts +0 -75
  180. package/dts/chart/scrubber/ScrubberBeacon.d.ts.map +0 -1
  181. package/dts/chart/text/SmartChartTextGroup.d.ts.map +0 -1
  182. package/esm/chart/Point.js +0 -111
  183. package/esm/chart/line/GradientLine.js +0 -62
  184. package/esm/chart/scrubber/ScrubberBeacon.js +0 -199
@@ -1,10 +1,11 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
1
+ import React, { useCallback, useMemo } from 'react';
2
2
  import { Platform } from 'react-native';
3
3
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4
- import { runOnJS } from 'react-native-reanimated';
4
+ import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated';
5
5
  import { Haptics } from '@coinbase/cds-mobile/utils/haptics';
6
6
  import { useCartesianChartContext } from '../ChartProvider';
7
- import { isCategoricalScale, ScrubberContext } from '../utils';
7
+ import { invertSerializableScale, ScrubberContext } from '../utils';
8
+ import { getPointOnSerializableScale } from '../utils/point';
8
9
  import { jsx as _jsx } from "react/jsx-runtime";
9
10
  /**
10
11
  * A component which encapsulates the ScrubberContext.
@@ -22,25 +23,25 @@ export const ScrubberProvider = _ref => {
22
23
  throw new Error('ScrubberProvider must be used within a ChartContext');
23
24
  }
24
25
  const {
25
- getXScale,
26
- getXAxis,
27
- series
26
+ getXSerializableScale,
27
+ getXAxis
28
28
  } = chartContext;
29
- const [scrubberPosition, setScrubberPosition] = useState(undefined);
29
+ const scrubberPosition = useSharedValue(undefined);
30
+ const xAxis = useMemo(() => getXAxis(), [getXAxis]);
31
+ const xScale = useMemo(() => getXSerializableScale(), [getXSerializableScale]);
30
32
  const getDataIndexFromX = useCallback(touchX => {
31
- const xScale = getXScale();
32
- const xAxis = getXAxis();
33
+ 'worklet';
34
+
33
35
  if (!xScale || !xAxis) return 0;
34
- if (isCategoricalScale(xScale)) {
35
- var _ref2, _xScale$domain, _xScale$bandwidth;
36
- const categories = (_ref2 = (_xScale$domain = xScale.domain == null ? void 0 : xScale.domain()) != null ? _xScale$domain : xAxis.data) != null ? _ref2 : [];
37
- const bandwidth = (_xScale$bandwidth = xScale.bandwidth == null ? void 0 : xScale.bandwidth()) != null ? _xScale$bandwidth : 0;
36
+ if (xScale.type === 'band') {
37
+ const [domainMin, domainMax] = xScale.domain;
38
+ const categoryCount = domainMax - domainMin + 1;
38
39
  let closestIndex = 0;
39
40
  let closestDistance = Infinity;
40
- for (let i = 0; i < categories.length; i++) {
41
- const xPos = xScale(i);
41
+ for (let i = 0; i < categoryCount; i++) {
42
+ const xPos = getPointOnSerializableScale(i, xScale);
42
43
  if (xPos !== undefined) {
43
- const distance = Math.abs(touchX - (xPos + bandwidth / 2));
44
+ const distance = Math.abs(touchX - xPos);
44
45
  if (distance < closestDistance) {
45
46
  closestDistance = distance;
46
47
  closestIndex = i;
@@ -58,7 +59,7 @@ export const ScrubberProvider = _ref => {
58
59
  let closestDistance = Infinity;
59
60
  for (let i = 0; i < numericData.length; i++) {
60
61
  const xValue = numericData[i];
61
- const xPos = xScale(xValue);
62
+ const xPos = getPointOnSerializableScale(xValue, xScale);
62
63
  if (xPos !== undefined) {
63
64
  const distance = Math.abs(touchX - xPos);
64
65
  if (distance < closestDistance) {
@@ -70,62 +71,53 @@ export const ScrubberProvider = _ref => {
70
71
  return closestIndex;
71
72
  } else {
72
73
  var _domain$min, _domain$max;
73
- const xValue = xScale.invert(touchX);
74
+ const xValue = invertSerializableScale(touchX, xScale);
74
75
  const dataIndex = Math.round(xValue);
75
76
  const domain = xAxis.domain;
76
77
  return Math.max((_domain$min = domain.min) != null ? _domain$min : 0, Math.min(dataIndex, (_domain$max = domain.max) != null ? _domain$max : 0));
77
78
  }
78
79
  }
79
- }, [getXScale, getXAxis]);
80
- const handlePositionUpdate = useCallback(x => {
81
- if (!enableScrubbing || !series || series.length === 0) return;
82
- const dataIndex = getDataIndexFromX(x);
83
- if (dataIndex !== scrubberPosition) {
84
- setScrubberPosition(dataIndex);
85
- onScrubberPositionChange == null || onScrubberPositionChange(dataIndex);
86
- }
87
- }, [enableScrubbing, series, getDataIndexFromX, scrubberPosition, onScrubberPositionChange]);
88
- const handleInteractionEnd = useCallback(() => {
89
- if (!enableScrubbing) return;
90
- setScrubberPosition(undefined);
91
- onScrubberPositionChange == null || onScrubberPositionChange(undefined);
92
- }, [enableScrubbing, onScrubberPositionChange]);
93
-
94
- // Gesture handler callbacks
95
- const handleOnStartJsThread = useCallback(() => {
80
+ }, [xAxis, xScale]);
81
+ const handleStartEndHaptics = useCallback(() => {
96
82
  void Haptics.lightImpact();
97
- // Could add onScrubStart callback here if needed
98
83
  }, []);
99
- const handleOnEndOrCancelledJsThread = useCallback(() => {
100
- handleInteractionEnd();
101
- }, [handleInteractionEnd]);
102
- const handleOnUpdateJsThread = useCallback(x => {
103
- handlePositionUpdate(x);
104
- }, [handlePositionUpdate]);
105
- const handleOnEndJsThread = useCallback(() => {
106
- void Haptics.lightImpact();
107
- handleOnEndOrCancelledJsThread();
108
- }, [handleOnEndOrCancelledJsThread]);
84
+ useAnimatedReaction(() => scrubberPosition.value, (currentValue, previousValue) => {
85
+ // Confirm changes here and inside of our gesture handler before calling JS thread
86
+ // To prevent any rerenders
87
+ if (onScrubberPositionChange !== undefined && currentValue !== previousValue) {
88
+ runOnJS(onScrubberPositionChange)(currentValue);
89
+ }
90
+ }, [onScrubberPositionChange]);
109
91
 
110
92
  // Create the long press pan gesture
111
93
  const longPressGesture = useMemo(() => Gesture.Pan().activateAfterLongPress(110).shouldCancelWhenOutside(!allowOverflowGestures).onStart(function onStart(event) {
112
- runOnJS(handleOnStartJsThread)();
94
+ runOnJS(handleStartEndHaptics)();
113
95
 
114
96
  // Android does not trigger onUpdate when the gesture starts. This achieves consistent behavior across both iOS and Android
115
97
  if (Platform.OS === 'android') {
116
- runOnJS(handleOnUpdateJsThread)(event.x);
98
+ const newScrubberPosition = getDataIndexFromX(event.x);
99
+ if (newScrubberPosition !== scrubberPosition.value) {
100
+ scrubberPosition.value = newScrubberPosition;
101
+ }
117
102
  }
118
103
  }).onUpdate(function onUpdate(event) {
119
- runOnJS(handleOnUpdateJsThread)(event.x);
104
+ const newScrubberPosition = getDataIndexFromX(event.x);
105
+ if (newScrubberPosition !== scrubberPosition.value) {
106
+ scrubberPosition.value = newScrubberPosition;
107
+ }
120
108
  }).onEnd(function onEnd() {
121
- runOnJS(handleOnEndJsThread)();
109
+ if (enableScrubbing) {
110
+ runOnJS(handleStartEndHaptics)();
111
+ scrubberPosition.value = undefined;
112
+ }
122
113
  }).onTouchesCancelled(function onTouchesCancelled() {
123
- runOnJS(handleOnEndOrCancelledJsThread)();
124
- }), [allowOverflowGestures, handleOnStartJsThread, handleOnUpdateJsThread, handleOnEndJsThread, handleOnEndOrCancelledJsThread]);
114
+ if (enableScrubbing) {
115
+ scrubberPosition.value = undefined;
116
+ }
117
+ }), [allowOverflowGestures, handleStartEndHaptics, getDataIndexFromX, scrubberPosition, enableScrubbing]);
125
118
  const contextValue = useMemo(() => ({
126
119
  enableScrubbing: !!enableScrubbing,
127
- scrubberPosition,
128
- onScrubberPositionChange: setScrubberPosition
120
+ scrubberPosition
129
121
  }), [enableScrubbing, scrubberPosition]);
130
122
  const content = /*#__PURE__*/_jsx(ScrubberContext.Provider, {
131
123
  value: contextValue,
@@ -1,2 +1,4 @@
1
- // Only export Scrubber component
1
+ export * from './DefaultScrubberBeacon';
2
+ export * from './DefaultScrubberBeaconLabel';
3
+ export * from './DefaultScrubberLabel';
2
4
  export * from './Scrubber';
@@ -1,11 +1,28 @@
1
- import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
2
- import { G, Rect as SvgRect, Text } from 'react-native-svg';
1
+ import { memo, useMemo } from 'react';
2
+ import { runOnJS, useAnimatedReaction, useDerivedValue } from 'react-native-reanimated';
3
3
  import { useTheme } from '@coinbase/cds-mobile/hooks/useTheme';
4
+ import { FontSlant, FontWeight, Group, Paint, Paragraph, RoundedRect, Shadow, Skia, TextAlign } from '@shopify/react-native-skia';
4
5
  import { useCartesianChartContext } from '../ChartProvider';
5
- import { getChartInset } from '../utils';
6
+ import { getChartInset, getColorWithOpacity, unwrapAnimatedValue } from '../utils';
7
+
8
+ /**
9
+ * Converts a fontWeight from Theme to a Skia FontWeight
10
+ * @note this only works when the fontWeight is a valid number (ie not 'bold')
11
+ * @param theme - The theme to use
12
+ * @param font - The font to use
13
+ * @returns The FontWeight or undefined if the fontWeight is not a valid number
14
+ */
15
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
+ const getFontWeight = (theme, font) => {
17
+ const themeFontWeight = theme.fontWeight[font];
18
+ const numericWeight = typeof themeFontWeight === 'string' ? Number(themeFontWeight) : themeFontWeight;
19
+ const validFontWeights = Object.values(FontWeight).filter(value => typeof value === 'number');
20
+ return numericWeight !== undefined && validFontWeights.includes(numericWeight) ? numericWeight : undefined;
21
+ };
6
22
 
7
23
  /**
8
24
  * The supported content types for ChartText.
25
+ * Pass a string for simple text, or a SkParagraph for advanced rich text formatting.
9
26
  */
10
27
 
11
28
  /**
@@ -15,137 +32,156 @@ import { getChartInset } from '../utils';
15
32
  /**
16
33
  * Vertical alignment options for chart text.
17
34
  */
18
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- /**
20
- * Maps horizontal alignment to SVG textAnchor.
21
- * This abstraction allows us to provide a consistent alignment API across web and mobile platforms,
22
- * hiding the platform-specific SVG property differences.
23
- */
24
- const getTextAnchor = alignment => {
25
- switch (alignment) {
26
- case 'left':
27
- return 'start';
28
- case 'center':
29
- return 'middle';
30
- case 'right':
31
- return 'end';
32
- }
33
- };
34
35
 
35
- /**
36
- * Maps vertical alignment to SVG alignmentBaseline.
37
- * This abstraction allows us to provide a consistent alignment API across web and mobile platforms,
38
- * hiding the platform-specific SVG property differences.
39
- */
40
- const getAlignmentBaseline = alignment => {
41
- switch (alignment) {
42
- case 'top':
43
- return 'hanging';
44
- case 'middle':
45
- return 'central';
46
- case 'bottom':
47
- return 'ideographic';
48
- }
49
- };
50
- const ChartTextVisible = /*#__PURE__*/memo(_ref => {
51
- let {
52
- children,
53
- background,
54
- textAnchor,
55
- alignmentBaseline,
56
- fontSize,
57
- fontWeight,
58
- fill,
59
- borderRadius,
60
- inset: insetInput,
61
- textDimensions,
62
- dx,
63
- dy
64
- } = _ref;
65
- const theme = useTheme();
66
- const inset = useMemo(() => getChartInset(insetInput), [insetInput]);
67
- const rectHeight = useMemo(() => textDimensions.height + inset.top + inset.bottom, [textDimensions, inset]);
68
- const rectWidth = useMemo(() => textDimensions.width + inset.left + inset.right, [textDimensions, inset]);
69
- return /*#__PURE__*/_jsxs(G, {
70
- children: [background !== 'transparent' && /*#__PURE__*/_jsx(SvgRect, {
71
- fill: background,
72
- height: rectHeight,
73
- rx: borderRadius,
74
- ry: borderRadius,
75
- width: rectWidth,
76
- x: textDimensions.x - inset.left,
77
- y: textDimensions.y - inset.top
78
- }), /*#__PURE__*/_jsx(Text, {
79
- alignmentBaseline: alignmentBaseline,
80
- dx: dx,
81
- dy: dy,
82
- fill: fill != null ? fill : theme.color.fgMuted,
83
- fontSize: fontSize,
84
- fontWeight: fontWeight,
85
- textAnchor: textAnchor,
86
- children: children
87
- })]
88
- });
89
- });
90
- export const ChartText = /*#__PURE__*/memo(_ref2 => {
36
+ export const ChartText = /*#__PURE__*/memo(_ref => {
37
+ var _elevationShadow$shad, _elevationShadow$shad2, _elevationShadow$shad3, _elevationShadow$shad4, _elevationShadow$shad5, _elevationShadow$shad6, _elevationShadow$shad7;
91
38
  let {
92
39
  children,
93
40
  x,
94
41
  y,
42
+ dx = 0,
43
+ dy = 0,
95
44
  horizontalAlignment = 'center',
96
45
  verticalAlignment = 'middle',
97
- dx,
98
- dy,
46
+ paragraphAlignment = TextAlign.Left,
99
47
  disableRepositioning = false,
100
48
  bounds,
101
- testID,
102
- fontSize = 12,
103
- fontWeight,
104
49
  color,
105
- background = 'transparent',
106
- borderRadius,
50
+ background: backgroundProp,
51
+ borderRadius = 4,
107
52
  inset: insetInput,
108
53
  onDimensionsChange,
109
- opacity = 1
110
- } = _ref2;
54
+ opacity = 1,
55
+ fontFamilies,
56
+ font = 'label2',
57
+ fontSize,
58
+ fontWeight,
59
+ fontStyle: fontStyleProp = FontSlant.Upright,
60
+ elevated
61
+ } = _ref;
62
+ const theme = useTheme();
111
63
  const {
112
64
  width: chartWidth,
113
- height: chartHeight
65
+ height: chartHeight,
66
+ fontFamilies: contextFontFamilies,
67
+ fontProvider
114
68
  } = useCartesianChartContext();
115
- const textAnchor = useMemo(() => getTextAnchor(horizontalAlignment), [horizontalAlignment]);
116
- const alignmentBaseline = useMemo(() => getAlignmentBaseline(verticalAlignment), [verticalAlignment]);
117
- const fullChartBounds = useMemo(() => ({
118
- x: 0,
119
- y: 0,
120
- width: chartWidth,
121
- height: chartHeight
122
- }), [chartWidth, chartHeight]);
123
- const [textSize, setTextSize] = useState(null);
124
- const textBBox = useMemo(() => {
125
- if (!textSize) {
126
- return null;
69
+ const inset = useMemo(() => getChartInset(insetInput), [insetInput]);
70
+ const background = backgroundProp != null ? backgroundProp : elevated ? theme.color.bgElevation1 : 'transparent';
71
+ const defaultParagraphStyle = useMemo(() => {
72
+ var _ref2;
73
+ return {
74
+ fontFamilies: (_ref2 = fontFamilies != null ? fontFamilies : contextFontFamilies) != null ? _ref2 : [],
75
+ fontSize: fontSize != null ? fontSize : theme.fontSize[font],
76
+ fontStyle: {
77
+ weight: fontWeight != null ? fontWeight : getFontWeight(theme, font),
78
+ slant: fontStyleProp
79
+ },
80
+ color: Skia.Color(color != null ? color : theme.color.fgMuted)
81
+ };
82
+ }, [fontFamilies, contextFontFamilies, fontSize, theme, font, fontWeight, fontStyleProp, color]);
83
+ const paragraph = useDerivedValue(() => {
84
+ const childrenValue = unwrapAnimatedValue(children);
85
+ if (typeof childrenValue !== 'string') {
86
+ return childrenValue;
127
87
  }
88
+ const builder = Skia.ParagraphBuilder.Make({
89
+ textAlign: TextAlign.Left
90
+ }, fontProvider);
91
+ builder.pushStyle(defaultParagraphStyle);
92
+ builder.addText(childrenValue);
93
+ builder.pop();
94
+ const para = builder.build();
95
+ para.layout(chartWidth);
96
+ return para;
97
+ }, [children, fontProvider, defaultParagraphStyle, chartWidth]);
98
+ const textDimensions = useDerivedValue(() => {
99
+ const unwrappedParagraph = paragraph.value;
100
+ if (!unwrappedParagraph) return {
101
+ width: 0,
102
+ height: 0
103
+ };
128
104
  return {
129
- x: x + textSize.x,
130
- y: y + textSize.y,
131
- width: textSize.width,
132
- height: textSize.height
105
+ width: unwrappedParagraph.getLongestLine(),
106
+ height: unwrappedParagraph.getHeight()
133
107
  };
134
- }, [x, y, textSize]);
135
- const isDimensionsReady = disableRepositioning || textBBox !== null;
136
- const backgroundRectDimensions = useMemo(() => {
137
- if (!textBBox) {
138
- return null;
108
+ }, [paragraph]);
109
+ const backgroundRectSize = useDerivedValue(() => ({
110
+ width: textDimensions.value.width + inset.left + inset.right,
111
+ height: textDimensions.value.height + inset.top + inset.bottom
112
+ }), [textDimensions, inset]);
113
+
114
+ // Calculate background rect position based on alignment
115
+ const backgroundRect = useDerivedValue(() => {
116
+ const horAlignment = unwrapAnimatedValue(horizontalAlignment);
117
+ const verAlignment = unwrapAnimatedValue(verticalAlignment);
118
+ // By default the value is top left
119
+ let rectX = unwrapAnimatedValue(x);
120
+ let rectY = unwrapAnimatedValue(y);
121
+ const rectSize = backgroundRectSize.value;
122
+
123
+ // Adjust for horizontal alignment
124
+ switch (horAlignment) {
125
+ case 'center':
126
+ rectX = rectX - rectSize.width / 2;
127
+ break;
128
+ case 'right':
129
+ rectX = rectX - rectSize.width;
130
+ break;
131
+ }
132
+
133
+ // Adjust for vertical alignment
134
+ switch (verAlignment) {
135
+ case 'middle':
136
+ rectY = rectY - rectSize.height / 2;
137
+ break;
138
+ case 'bottom':
139
+ rectY = rectY - rectSize.height;
140
+ break;
139
141
  }
140
- const inset = getChartInset(insetInput);
141
142
  return {
142
- x: textBBox.x - inset.left,
143
- y: textBBox.y - inset.top,
144
- width: textBBox.width + inset.left + inset.right,
145
- height: textBBox.height + inset.top + inset.bottom
143
+ x: rectX,
144
+ y: rectY,
145
+ width: rectSize.width,
146
+ height: rectSize.height
146
147
  };
147
- }, [textBBox, insetInput]);
148
- const overflowAmount = useMemo(() => {
148
+ }, [x, y, backgroundRectSize, horizontalAlignment, verticalAlignment]);
149
+
150
+ // Paragraph uses top-left positioning
151
+ const textPosition = useDerivedValue(() => {
152
+ const textDims = textDimensions.value;
153
+
154
+ // Calculate horizontal offset based on paragraph alignment
155
+ let horizontalOffset = 0;
156
+ switch (paragraphAlignment) {
157
+ case TextAlign.Center:
158
+ horizontalOffset = -textDims.width / 2;
159
+ break;
160
+ case TextAlign.Right:
161
+ case TextAlign.End:
162
+ horizontalOffset = -textDims.width;
163
+ break;
164
+ default:
165
+ // Left-aligned text needs no offset
166
+ horizontalOffset = 0;
167
+ break;
168
+ }
169
+ return {
170
+ x: backgroundRect.value.x + inset.left + horizontalOffset,
171
+ y: backgroundRect.value.y + inset.top,
172
+ width: textDims.width,
173
+ height: textDims.height
174
+ };
175
+ }, [backgroundRect, textDimensions, inset, paragraphAlignment]);
176
+
177
+ // Calculate overflow and repositioning
178
+ const fullChartBounds = useMemo(() => ({
179
+ x: 0,
180
+ y: 0,
181
+ width: chartWidth,
182
+ height: chartHeight
183
+ }), [chartWidth, chartHeight]);
184
+ const overflowAmount = useDerivedValue(() => {
149
185
  if (disableRepositioning) {
150
186
  return {
151
187
  x: 0,
@@ -153,85 +189,117 @@ export const ChartText = /*#__PURE__*/memo(_ref2 => {
153
189
  };
154
190
  }
155
191
  const parentBounds = bounds != null ? bounds : fullChartBounds;
156
- if (!backgroundRectDimensions || !parentBounds || parentBounds.width <= 0 || parentBounds.height <= 0) {
192
+ if (!parentBounds || parentBounds.width <= 0 || parentBounds.height <= 0) {
157
193
  return {
158
194
  x: 0,
159
195
  y: 0
160
196
  };
161
197
  }
162
- let x = 0;
163
- let y = 0;
198
+ let offsetX = 0;
199
+ let offsetY = 0;
164
200
 
165
201
  // X-axis overflow
166
- if (backgroundRectDimensions.x < parentBounds.x) {
167
- x = parentBounds.x - backgroundRectDimensions.x; // positive = shift right
168
- } else if (backgroundRectDimensions.x + backgroundRectDimensions.width > parentBounds.x + parentBounds.width) {
169
- x = parentBounds.x + parentBounds.width - (backgroundRectDimensions.x + backgroundRectDimensions.width); // negative = shift left
202
+ if (backgroundRect.value.x < parentBounds.x) {
203
+ offsetX = parentBounds.x - backgroundRect.value.x;
204
+ } else if (backgroundRect.value.x + backgroundRect.value.width > parentBounds.x + parentBounds.width) {
205
+ offsetX = parentBounds.x + parentBounds.width - (backgroundRect.value.x + backgroundRect.value.width);
170
206
  }
171
207
 
172
208
  // Y-axis overflow
173
- if (backgroundRectDimensions.y < parentBounds.y) {
174
- y = parentBounds.y - backgroundRectDimensions.y; // positive = shift down
175
- } else if (backgroundRectDimensions.y + backgroundRectDimensions.height > parentBounds.y + parentBounds.height) {
176
- y = parentBounds.y + parentBounds.height - (backgroundRectDimensions.y + backgroundRectDimensions.height); // negative = shift up
209
+ if (backgroundRect.value.y < parentBounds.y) {
210
+ offsetY = parentBounds.y - backgroundRect.value.y;
211
+ } else if (backgroundRect.value.y + backgroundRect.value.height > parentBounds.y + parentBounds.height) {
212
+ offsetY = parentBounds.y + parentBounds.height - (backgroundRect.value.y + backgroundRect.value.height);
177
213
  }
178
214
  return {
179
- x,
180
- y
215
+ x: offsetX,
216
+ y: offsetY
181
217
  };
182
- }, [backgroundRectDimensions, fullChartBounds, bounds, disableRepositioning]);
183
- const reportedRect = useMemo(() => {
184
- if (!backgroundRectDimensions) return null;
218
+ }, [backgroundRect, fullChartBounds, bounds, disableRepositioning]);
219
+
220
+ // Final adjusted positions
221
+ const backgroundRectWithOffset = useDerivedValue(() => {
222
+ const offsetX = unwrapAnimatedValue(dx);
223
+ const offsetY = unwrapAnimatedValue(dy);
185
224
  return {
186
- x: backgroundRectDimensions.x + overflowAmount.x,
187
- y: backgroundRectDimensions.y + overflowAmount.y,
188
- width: backgroundRectDimensions.width,
189
- height: backgroundRectDimensions.height
225
+ x: backgroundRect.value.x + overflowAmount.value.x + offsetX,
226
+ y: backgroundRect.value.y + overflowAmount.value.y + offsetY,
227
+ width: backgroundRect.value.width,
228
+ height: backgroundRect.value.height
190
229
  };
191
- }, [backgroundRectDimensions, overflowAmount.x, overflowAmount.y]);
192
- useEffect(() => {
193
- if (onDimensionsChange && reportedRect !== null) {
194
- onDimensionsChange(reportedRect);
230
+ }, [backgroundRect, overflowAmount, dx, dy]);
231
+ const textWithOffsetX = useDerivedValue(() => textPosition.value.x + overflowAmount.value.x + unwrapAnimatedValue(dx), [textPosition, overflowAmount, dx]);
232
+ const textWithOffsetY = useDerivedValue(() => textPosition.value.y + overflowAmount.value.y + unwrapAnimatedValue(dy), [textPosition, overflowAmount, dy]);
233
+ useAnimatedReaction(() => backgroundRectWithOffset.value, (rect, previous) => {
234
+ if (onDimensionsChange && rect !== previous) {
235
+ runOnJS(onDimensionsChange)(rect);
195
236
  }
196
- }, [reportedRect, onDimensionsChange]);
197
- const onLayout = useCallback(event => {
198
- if (event.nativeEvent.layout.width > 0 && event.nativeEvent.layout.height > 0) {
199
- setTextSize(event.nativeEvent.layout);
237
+ }, [onDimensionsChange]);
238
+
239
+ // Show group if we are ready
240
+ const groupOpacity = useDerivedValue(() => {
241
+ const textSize = textDimensions.value;
242
+ const hasValidContent = paragraph.value && textSize.width > 0 && textSize.height > 0;
243
+ return hasValidContent ? unwrapAnimatedValue(opacity) : 0;
244
+ }, [paragraph, textDimensions, opacity]);
245
+ const backgroundRectHeight = useDerivedValue(() => backgroundRectWithOffset.value.height, [backgroundRectWithOffset]);
246
+ const backgroundRectWidth = useDerivedValue(() => backgroundRectWithOffset.value.width, [backgroundRectWithOffset]);
247
+ const backgroundRectX = useDerivedValue(() => backgroundRectWithOffset.value.x, [backgroundRectWithOffset]);
248
+ const backgroundRectY = useDerivedValue(() => backgroundRectWithOffset.value.y, [backgroundRectWithOffset]);
249
+ const elevationShadow = elevated ? theme.shadow.elevation1 : undefined;
250
+
251
+ // Calculate the paragraph's internal x offset from line metrics based on text alignment
252
+ const paragraphTransform = useDerivedValue(() => {
253
+ if (!paragraph.value || !paragraphAlignment) return [];
254
+ const rects = paragraph.value.getLineMetrics();
255
+ if (rects.length === 0) return [];
256
+ let minOffset;
257
+ switch (paragraphAlignment) {
258
+ case TextAlign.Center:
259
+ // For center-aligned text, account for half the width
260
+ minOffset = Math.min(...rects.map(rect => rect.x - rect.width / 2));
261
+ break;
262
+ case TextAlign.Right:
263
+ case TextAlign.End:
264
+ // For right-aligned text, account for the full width
265
+ minOffset = Math.min(...rects.map(rect => rect.x - rect.width));
266
+ break;
267
+ default:
268
+ // For left-aligned text, use the x position directly
269
+ minOffset = Math.min(...rects.map(rect => rect.x));
270
+ break;
200
271
  }
201
- }, []);
202
- return /*#__PURE__*/_jsxs(G, {
203
- opacity: isDimensionsReady ? opacity : 0,
204
- children: [textSize && /*#__PURE__*/_jsx(G, {
205
- transform: [{
206
- translateX: x + overflowAmount.x
207
- }, {
208
- translateY: y + overflowAmount.y
209
- }],
210
- children: /*#__PURE__*/_jsx(ChartTextVisible, {
211
- alignmentBaseline: alignmentBaseline,
212
- background: background,
213
- borderRadius: borderRadius,
214
- dx: dx,
215
- dy: dy,
216
- fill: color,
217
- fontSize: fontSize,
218
- fontWeight: fontWeight,
219
- inset: insetInput,
220
- textAnchor: textAnchor,
221
- textDimensions: textSize,
222
- children: children
272
+ return [{
273
+ translateX: -minOffset
274
+ }];
275
+ }, [paragraph, paragraphAlignment]);
276
+
277
+ // Opacity on a group doesn't impact the paragraph so we need to apply it to Group
278
+ return /*#__PURE__*/_jsxs(Group, {
279
+ layer: /*#__PURE__*/_jsx(Paint, {
280
+ opacity: groupOpacity
281
+ }),
282
+ children: [background !== 'transparent' && /*#__PURE__*/_jsx(RoundedRect, {
283
+ color: background,
284
+ height: backgroundRectHeight,
285
+ r: borderRadius,
286
+ width: backgroundRectWidth,
287
+ x: backgroundRectX,
288
+ y: backgroundRectY,
289
+ children: elevationShadow && /*#__PURE__*/_jsx(Shadow, {
290
+ blur: Number((_elevationShadow$shad = elevationShadow.shadowRadius) != null ? _elevationShadow$shad : 0),
291
+ color: getColorWithOpacity(String((_elevationShadow$shad2 = elevationShadow.shadowColor) != null ? _elevationShadow$shad2 : '#000000'), Number((_elevationShadow$shad3 = elevationShadow.shadowOpacity) != null ? _elevationShadow$shad3 : 1)),
292
+ dx: Number((_elevationShadow$shad4 = (_elevationShadow$shad5 = elevationShadow.shadowOffset) == null ? void 0 : _elevationShadow$shad5.width) != null ? _elevationShadow$shad4 : 0),
293
+ dy: Number((_elevationShadow$shad6 = (_elevationShadow$shad7 = elevationShadow.shadowOffset) == null ? void 0 : _elevationShadow$shad7.height) != null ? _elevationShadow$shad6 : 0)
294
+ })
295
+ }), /*#__PURE__*/_jsx(Group, {
296
+ transform: paragraphTransform,
297
+ children: /*#__PURE__*/_jsx(Paragraph, {
298
+ paragraph: paragraph,
299
+ width: chartWidth,
300
+ x: textWithOffsetX,
301
+ y: textWithOffsetY
223
302
  })
224
- }), /*#__PURE__*/_jsx(Text, {
225
- alignmentBaseline: alignmentBaseline,
226
- dx: dx,
227
- dy: dy,
228
- fill: "transparent",
229
- fontSize: fontSize,
230
- fontWeight: fontWeight,
231
- onLayout: onLayout,
232
- opacity: 0,
233
- textAnchor: textAnchor,
234
- children: children
235
303
  })]
236
304
  });
237
305
  });