@ledgerhq/lumen-ui-rnative-visualization 0.1.14 → 0.1.16

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 (164) hide show
  1. package/dist/module/lib/Components/Axis/Axis.constants.js +21 -0
  2. package/dist/module/lib/Components/Axis/Axis.constants.js.map +1 -0
  3. package/dist/module/lib/Components/Axis/index.js +5 -0
  4. package/dist/module/lib/Components/Axis/index.js.map +1 -0
  5. package/dist/module/lib/Components/CartesianChart/CartesianChart.js +34 -22
  6. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  7. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js +5 -4
  8. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js.map +1 -1
  9. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js +54 -0
  10. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js.map +1 -1
  11. package/dist/module/lib/Components/CartesianChart/utils.js +22 -12
  12. package/dist/module/lib/Components/CartesianChart/utils.js.map +1 -1
  13. package/dist/module/lib/Components/Line/utils.js +6 -1
  14. package/dist/module/lib/Components/Line/utils.js.map +1 -1
  15. package/dist/module/lib/Components/Line/utils.test.js +88 -0
  16. package/dist/module/lib/Components/Line/utils.test.js.map +1 -0
  17. package/dist/module/lib/Components/LineChart/LineChart.js +19 -31
  18. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  19. package/dist/module/lib/Components/LineChart/LineChart.stories.js +38 -0
  20. package/dist/module/lib/Components/LineChart/LineChart.stories.js.map +1 -1
  21. package/dist/module/lib/Components/LineChart/LineChart.test.js +192 -0
  22. package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
  23. package/dist/module/lib/Components/Point/Point.js +8 -2
  24. package/dist/module/lib/Components/Point/Point.js.map +1 -1
  25. package/dist/module/lib/Components/Point/constants.js +9 -0
  26. package/dist/module/lib/Components/Point/constants.js.map +1 -0
  27. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js +35 -0
  28. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js.map +1 -0
  29. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js +153 -0
  30. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js.map +1 -0
  31. package/dist/module/lib/Components/Point/pointContext/index.js +5 -0
  32. package/dist/module/lib/Components/Point/pointContext/index.js.map +1 -0
  33. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js +10 -0
  34. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js.map +1 -0
  35. package/dist/module/lib/Components/Point/utils.js +26 -6
  36. package/dist/module/lib/Components/Point/utils.js.map +1 -1
  37. package/dist/module/lib/Components/Point/utils.test.js +84 -2
  38. package/dist/module/lib/Components/Point/utils.test.js.map +1 -1
  39. package/dist/module/lib/Components/ReferenceLine/utils.js.map +1 -1
  40. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js +3 -3
  41. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js.map +1 -1
  42. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js +5 -5
  43. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js.map +1 -1
  44. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js +5 -5
  45. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js +1 -2
  46. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js.map +1 -1
  47. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +31 -10
  48. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -1
  49. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js +4 -4
  50. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js.map +1 -1
  51. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +35 -7
  52. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  53. package/dist/module/lib/Components/Scrubber/utils.js +64 -0
  54. package/dist/module/lib/Components/Scrubber/utils.js.map +1 -1
  55. package/dist/module/lib/Components/Scrubber/utils.test.js +128 -1
  56. package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -1
  57. package/dist/module/lib/utils/domain/domain.js.map +1 -1
  58. package/dist/module/lib/utils/scales/scales.js +13 -4
  59. package/dist/module/lib/utils/scales/scales.js.map +1 -1
  60. package/dist/module/lib/utils/scales/scales.test.js +29 -0
  61. package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
  62. package/dist/module/lib/utils/ticks/ticks.js +13 -3
  63. package/dist/module/lib/utils/ticks/ticks.js.map +1 -1
  64. package/dist/module/lib/utils/ticks/ticks.test.js +34 -0
  65. package/dist/module/lib/utils/ticks/ticks.test.js.map +1 -1
  66. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts +5 -0
  67. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts.map +1 -0
  68. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts +39 -0
  69. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts.map +1 -1
  70. package/dist/typescript/src/lib/Components/Axis/index.d.ts +3 -0
  71. package/dist/typescript/src/lib/Components/Axis/index.d.ts.map +1 -0
  72. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  73. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  74. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts +5 -4
  75. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts.map +1 -1
  76. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +11 -3
  77. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  78. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts +18 -8
  79. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts.map +1 -1
  80. package/dist/typescript/src/lib/Components/Line/utils.d.ts +4 -0
  81. package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -1
  82. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
  83. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  84. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +10 -3
  85. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
  86. package/dist/typescript/src/lib/Components/Point/Point.d.ts +1 -1
  87. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  88. package/dist/typescript/src/lib/Components/Point/constants.d.ts +7 -0
  89. package/dist/typescript/src/lib/Components/Point/constants.d.ts.map +1 -0
  90. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts +7 -0
  91. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -0
  92. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +3 -0
  93. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -0
  94. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +13 -0
  95. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -0
  96. package/dist/typescript/src/lib/Components/Point/types.d.ts +7 -0
  97. package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -1
  98. package/dist/typescript/src/lib/Components/Point/utils.d.ts +8 -6
  99. package/dist/typescript/src/lib/Components/Point/utils.d.ts.map +1 -1
  100. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +3 -2
  101. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -1
  102. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts.map +1 -1
  103. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts +1 -2
  104. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts.map +1 -1
  105. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts +5 -1
  106. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -1
  107. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +1 -1
  108. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  109. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +13 -0
  110. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -1
  111. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +33 -3
  112. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -1
  113. package/dist/typescript/src/lib/utils/domain/domain.d.ts +5 -4
  114. package/dist/typescript/src/lib/utils/domain/domain.d.ts.map +1 -1
  115. package/dist/typescript/src/lib/utils/index.d.ts +1 -1
  116. package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
  117. package/dist/typescript/src/lib/utils/scales/scales.d.ts +11 -5
  118. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
  119. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +13 -5
  120. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -1
  121. package/dist/typescript/src/lib/utils/types.d.ts +3 -28
  122. package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
  123. package/package.json +4 -4
  124. package/src/lib/Components/Axis/Axis.constants.ts +23 -0
  125. package/src/lib/Components/Axis/Axis.types.ts +40 -0
  126. package/src/lib/Components/Axis/index.ts +2 -0
  127. package/src/lib/Components/CartesianChart/CartesianChart.tsx +39 -23
  128. package/src/lib/Components/CartesianChart/context/useBuildChartContext.test.ts +56 -0
  129. package/src/lib/Components/CartesianChart/context/useBuildChartContext.ts +8 -8
  130. package/src/lib/Components/CartesianChart/types.ts +11 -3
  131. package/src/lib/Components/CartesianChart/utils.ts +22 -12
  132. package/src/lib/Components/Line/utils.test.ts +90 -0
  133. package/src/lib/Components/Line/utils.ts +6 -1
  134. package/src/lib/Components/LineChart/LineChart.stories.tsx +36 -0
  135. package/src/lib/Components/LineChart/LineChart.test.tsx +156 -0
  136. package/src/lib/Components/LineChart/LineChart.tsx +30 -40
  137. package/src/lib/Components/LineChart/types.ts +10 -3
  138. package/src/lib/Components/Point/Point.tsx +9 -3
  139. package/src/lib/Components/Point/constants.ts +6 -0
  140. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +162 -0
  141. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +45 -0
  142. package/src/lib/Components/Point/pointContext/index.ts +5 -0
  143. package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +19 -0
  144. package/src/lib/Components/Point/types.ts +7 -0
  145. package/src/lib/Components/Point/utils.test.ts +95 -5
  146. package/src/lib/Components/Point/utils.ts +35 -7
  147. package/src/lib/Components/ReferenceLine/utils.ts +4 -7
  148. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.tsx +3 -3
  149. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.tsx +5 -5
  150. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.tsx +4 -5
  151. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.ts +1 -2
  152. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.ts +4 -4
  153. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +29 -15
  154. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +41 -5
  155. package/src/lib/Components/Scrubber/types.ts +13 -0
  156. package/src/lib/Components/Scrubber/utils.test.ts +150 -0
  157. package/src/lib/Components/Scrubber/utils.ts +93 -3
  158. package/src/lib/utils/domain/domain.ts +6 -5
  159. package/src/lib/utils/index.ts +0 -2
  160. package/src/lib/utils/scales/scales.test.ts +19 -0
  161. package/src/lib/utils/scales/scales.ts +15 -9
  162. package/src/lib/utils/ticks/ticks.test.ts +45 -0
  163. package/src/lib/utils/ticks/ticks.ts +19 -9
  164. package/src/lib/utils/types.ts +3 -26
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+ import { renderHook, act } from '@testing-library/react-native';
3
+ import type { ReactNode } from 'react';
4
+
5
+ import { useMagneticPointsContext } from './magneticPointsContext';
6
+ import { MagneticPointsProvider } from './MagneticPointsProvider';
7
+
8
+ const wrapper = ({ children }: { children: ReactNode }) => (
9
+ <MagneticPointsProvider>{children}</MagneticPointsProvider>
10
+ );
11
+
12
+ describe('MagneticPointsProvider', () => {
13
+ it('starts with an empty set', () => {
14
+ const { result } = renderHook(() => useMagneticPointsContext(), {
15
+ wrapper,
16
+ });
17
+ expect(result.current.getMagneticPoints().size).toBe(0);
18
+ });
19
+
20
+ it('registers a magnetic point', () => {
21
+ const { result } = renderHook(() => useMagneticPointsContext(), {
22
+ wrapper,
23
+ });
24
+
25
+ act(() => {
26
+ result.current.register(3);
27
+ });
28
+
29
+ expect(result.current.getMagneticPoints().has(3)).toBe(true);
30
+ expect(result.current.getMagneticPoints().size).toBe(1);
31
+ });
32
+
33
+ it('registers multiple magnetic points', () => {
34
+ const { result } = renderHook(() => useMagneticPointsContext(), {
35
+ wrapper,
36
+ });
37
+
38
+ act(() => {
39
+ result.current.register(2);
40
+ result.current.register(7);
41
+ });
42
+
43
+ const points = result.current.getMagneticPoints();
44
+ expect(points.has(2)).toBe(true);
45
+ expect(points.has(7)).toBe(true);
46
+ expect(points.size).toBe(2);
47
+ });
48
+
49
+ it('unregisters a magnetic point', () => {
50
+ const { result } = renderHook(() => useMagneticPointsContext(), {
51
+ wrapper,
52
+ });
53
+
54
+ act(() => {
55
+ result.current.register(5);
56
+ result.current.register(8);
57
+ });
58
+
59
+ act(() => {
60
+ result.current.unregister(5);
61
+ });
62
+
63
+ const points = result.current.getMagneticPoints();
64
+ expect(points.has(5)).toBe(false);
65
+ expect(points.has(8)).toBe(true);
66
+ expect(points.size).toBe(1);
67
+ });
68
+
69
+ it('handles duplicate registrations gracefully', () => {
70
+ const { result } = renderHook(() => useMagneticPointsContext(), {
71
+ wrapper,
72
+ });
73
+
74
+ act(() => {
75
+ result.current.register(4);
76
+ result.current.register(4);
77
+ });
78
+
79
+ expect(result.current.getMagneticPoints().size).toBe(1);
80
+ });
81
+
82
+ it('handles unregistering a non-existent point gracefully', () => {
83
+ const { result } = renderHook(() => useMagneticPointsContext(), {
84
+ wrapper,
85
+ });
86
+
87
+ act(() => {
88
+ result.current.unregister(99);
89
+ });
90
+
91
+ expect(result.current.getMagneticPoints().size).toBe(0);
92
+ });
93
+
94
+ describe('version counter', () => {
95
+ it('starts at 0', () => {
96
+ const { result } = renderHook(() => useMagneticPointsContext(), {
97
+ wrapper,
98
+ });
99
+ expect(result.current.version).toBe(0);
100
+ });
101
+
102
+ it('increments on register', () => {
103
+ const { result } = renderHook(() => useMagneticPointsContext(), {
104
+ wrapper,
105
+ });
106
+
107
+ act(() => {
108
+ result.current.register(1);
109
+ });
110
+
111
+ expect(result.current.version).toBe(1);
112
+ });
113
+
114
+ it('increments on unregister', () => {
115
+ const { result } = renderHook(() => useMagneticPointsContext(), {
116
+ wrapper,
117
+ });
118
+
119
+ act(() => {
120
+ result.current.register(1);
121
+ });
122
+
123
+ act(() => {
124
+ result.current.unregister(1);
125
+ });
126
+
127
+ expect(result.current.version).toBe(2);
128
+ });
129
+
130
+ it('does not increment on duplicate register', () => {
131
+ const { result } = renderHook(() => useMagneticPointsContext(), {
132
+ wrapper,
133
+ });
134
+
135
+ act(() => {
136
+ result.current.register(5);
137
+ });
138
+
139
+ const versionAfterFirst = result.current.version;
140
+
141
+ act(() => {
142
+ result.current.register(5);
143
+ });
144
+
145
+ expect(result.current.version).toBe(versionAfterFirst);
146
+ });
147
+
148
+ it('does not increment when unregistering a non-existent point', () => {
149
+ const { result } = renderHook(() => useMagneticPointsContext(), {
150
+ wrapper,
151
+ });
152
+
153
+ const versionBefore = result.current.version;
154
+
155
+ act(() => {
156
+ result.current.unregister(99);
157
+ });
158
+
159
+ expect(result.current.version).toBe(versionBefore);
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,45 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import type { ReactNode } from 'react';
3
+
4
+ import {
5
+ MagneticPointsContextProvider,
6
+ type MagneticPointsContextValue,
7
+ } from './magneticPointsContext';
8
+
9
+ type MagneticPointsProviderProps = {
10
+ children: ReactNode;
11
+ };
12
+
13
+ export function MagneticPointsProvider({
14
+ children,
15
+ }: Readonly<MagneticPointsProviderProps>) {
16
+ const pointsRef = useRef(new Set<number>());
17
+ const [version, setVersion] = useState(0);
18
+
19
+ const register = useCallback((dataIndex: number): void => {
20
+ if (pointsRef.current.has(dataIndex)) return;
21
+ pointsRef.current.add(dataIndex);
22
+ setVersion((v) => v + 1);
23
+ }, []);
24
+
25
+ const unregister = useCallback((dataIndex: number): void => {
26
+ if (!pointsRef.current.has(dataIndex)) return;
27
+ pointsRef.current.delete(dataIndex);
28
+ setVersion((v) => v + 1);
29
+ }, []);
30
+
31
+ const getMagneticPoints = useCallback((): ReadonlySet<number> => {
32
+ return pointsRef.current;
33
+ }, []);
34
+
35
+ const value: MagneticPointsContextValue = useMemo(
36
+ () => ({ register, unregister, getMagneticPoints, version }),
37
+ [register, unregister, getMagneticPoints, version],
38
+ );
39
+
40
+ return (
41
+ <MagneticPointsContextProvider value={value}>
42
+ {children}
43
+ </MagneticPointsContextProvider>
44
+ );
45
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ useMagneticPointsContext,
3
+ type MagneticPointsContextValue,
4
+ } from './magneticPointsContext';
5
+ export { MagneticPointsProvider } from './MagneticPointsProvider';
@@ -0,0 +1,19 @@
1
+ import { createSafeContext } from '@ledgerhq/lumen-utils-shared';
2
+
3
+ export type MagneticPointsContextValue = {
4
+ register: (dataIndex: number) => void;
5
+ unregister: (dataIndex: number) => void;
6
+ version: number;
7
+ getMagneticPoints: () => ReadonlySet<number>;
8
+ };
9
+
10
+ const [MagneticPointsContextProvider, _useMagneticPointsSafeContext] =
11
+ createSafeContext<MagneticPointsContextValue>('MagneticPoints');
12
+
13
+ export const useMagneticPointsContext = (): MagneticPointsContextValue =>
14
+ _useMagneticPointsSafeContext({
15
+ consumerName: 'useMagneticPointsContext',
16
+ contextRequired: true,
17
+ });
18
+
19
+ export { MagneticPointsContextProvider };
@@ -73,4 +73,11 @@ export type PointProps = {
73
73
  * Called when the point is pressed.
74
74
  */
75
75
  onPress?: (event: GestureResponderEvent) => void;
76
+ /**
77
+ * When `true`, the scrubber magnetically snaps to this point within the
78
+ * chart's `magnetRadius`. Requires `enableScrubbing` to be set on the
79
+ * parent chart.
80
+ * @default false
81
+ */
82
+ magnetic?: boolean;
76
83
  };
@@ -1,14 +1,14 @@
1
- import { describe, expect, it } from '@jest/globals';
1
+ import { describe, expect, it, jest } from '@jest/globals';
2
+ import { renderHook } from '@testing-library/react-native';
2
3
 
4
+ import type { BaseAxisProps } from '../Axis';
5
+ import { ARROW_HEIGHT, ARROW_WIDTH, GAP, LABEL_FONT_SIZE } from './constants';
3
6
  import {
4
- ARROW_HEIGHT,
5
- ARROW_WIDTH,
6
7
  buildArrowPoints,
7
8
  computeLabelY,
8
- GAP,
9
9
  isWithinBounds,
10
- LABEL_FONT_SIZE,
11
10
  resolveLabel,
11
+ useMagneticRegistration,
12
12
  } from './utils';
13
13
 
14
14
  describe('isWithinBounds', () => {
@@ -116,3 +116,93 @@ describe('computeLabelY', () => {
116
116
  expect(result).toBe(pixelY + radius + GAP + GAP + LABEL_FONT_SIZE);
117
117
  });
118
118
  });
119
+
120
+ const makeContext = () => ({
121
+ register: jest.fn(),
122
+ unregister: jest.fn(),
123
+ version: 0,
124
+ getMagneticPoints: () => new Set<number>(),
125
+ });
126
+
127
+ const noAxisConfig = (): BaseAxisProps | undefined => undefined;
128
+
129
+ describe('useMagneticRegistration', () => {
130
+ it('registers the data index when magnetic is true', () => {
131
+ const ctx = makeContext();
132
+ renderHook(() => useMagneticRegistration(true, 3, noAxisConfig, ctx));
133
+
134
+ expect(ctx.register).toHaveBeenCalledWith(3);
135
+ });
136
+
137
+ it('does not register when magnetic is false', () => {
138
+ const ctx = makeContext();
139
+ renderHook(() => useMagneticRegistration(false, 3, noAxisConfig, ctx));
140
+
141
+ expect(ctx.register).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('unregisters on unmount', () => {
145
+ const ctx = makeContext();
146
+ const { unmount } = renderHook(() =>
147
+ useMagneticRegistration(true, 3, noAxisConfig, ctx),
148
+ );
149
+
150
+ unmount();
151
+ expect(ctx.unregister).toHaveBeenCalledWith(3);
152
+ });
153
+
154
+ it('resolves dataX through axis config data when provided', () => {
155
+ const ctx = makeContext();
156
+ const getXAxisConfig = (): BaseAxisProps => ({
157
+ scaleType: 'band',
158
+ data: [100, 200, 300],
159
+ });
160
+
161
+ renderHook(() => useMagneticRegistration(true, 200, getXAxisConfig, ctx));
162
+
163
+ expect(ctx.register).toHaveBeenCalledWith(1);
164
+ });
165
+
166
+ it('does not register when axis data exists but does not contain the value', () => {
167
+ const ctx = makeContext();
168
+ const getXAxisConfig = (): BaseAxisProps => ({
169
+ scaleType: 'band',
170
+ data: [100, 200, 300],
171
+ });
172
+
173
+ renderHook(() => useMagneticRegistration(true, 999, getXAxisConfig, ctx));
174
+
175
+ expect(ctx.register).not.toHaveBeenCalled();
176
+ });
177
+
178
+ it('re-registers when dataX changes', () => {
179
+ const ctx = makeContext();
180
+ const { rerender } = renderHook(
181
+ ({ dataX }: { dataX: number }) =>
182
+ useMagneticRegistration(true, dataX, noAxisConfig, ctx),
183
+ { initialProps: { dataX: 2 } },
184
+ );
185
+
186
+ expect(ctx.register).toHaveBeenCalledWith(2);
187
+
188
+ rerender({ dataX: 4 });
189
+
190
+ expect(ctx.unregister).toHaveBeenCalledWith(2);
191
+ expect(ctx.register).toHaveBeenCalledWith(4);
192
+ });
193
+
194
+ it('unregisters and stops when magnetic switches to false', () => {
195
+ const ctx = makeContext();
196
+ const { rerender } = renderHook(
197
+ ({ magnetic }: { magnetic: boolean }) =>
198
+ useMagneticRegistration(magnetic, 3, noAxisConfig, ctx),
199
+ { initialProps: { magnetic: true } },
200
+ );
201
+
202
+ expect(ctx.register).toHaveBeenCalledWith(3);
203
+
204
+ rerender({ magnetic: false });
205
+
206
+ expect(ctx.unregister).toHaveBeenCalledWith(3);
207
+ });
208
+ });
@@ -1,11 +1,9 @@
1
- import type { DrawingArea } from '../../utils/types';
1
+ import { useEffect } from 'react';
2
2
 
3
- export const DEFAULT_SIZE = 10;
4
- export const STROKE_WIDTH = 2;
5
- export const LABEL_FONT_SIZE = 10;
6
- export const ARROW_WIDTH = 6;
7
- export const ARROW_HEIGHT = 4;
8
- export const GAP = 4;
3
+ import type { DrawingArea } from '../../utils/types';
4
+ import type { BaseAxisProps } from '../Axis';
5
+ import { LABEL_FONT_SIZE, ARROW_WIDTH, ARROW_HEIGHT, GAP } from './constants';
6
+ import type { MagneticPointsContextValue } from './pointContext/magneticPointsContext';
9
7
 
10
8
  export const isWithinBounds = (
11
9
  px: number,
@@ -53,6 +51,36 @@ export const resolveLabel = (
53
51
  return resolved === '' ? undefined : resolved;
54
52
  };
55
53
 
54
+ export const resolveDataXToIndex = (
55
+ dataX: number,
56
+ axisConfig: BaseAxisProps | undefined,
57
+ ): number | undefined => {
58
+ const axisData = axisConfig?.data;
59
+ if (!axisData || typeof axisData[0] !== 'number') return dataX;
60
+ const index = (axisData as number[]).indexOf(dataX);
61
+ if (index === -1) return undefined;
62
+ return index;
63
+ };
64
+
65
+ /**
66
+ * Registers/unregisters a data index as a magnetic snap target
67
+ * when the Point has `magnetic` enabled.
68
+ */
69
+ export const useMagneticRegistration = (
70
+ magnetic: boolean,
71
+ dataX: number,
72
+ getXAxisConfig: () => BaseAxisProps | undefined,
73
+ { register, unregister }: MagneticPointsContextValue,
74
+ ): void => {
75
+ useEffect(() => {
76
+ if (!magnetic) return;
77
+ const index = resolveDataXToIndex(dataX, getXAxisConfig());
78
+ if (index === undefined) return;
79
+ register(index);
80
+ return () => unregister(index);
81
+ }, [magnetic, dataX, getXAxisConfig, register, unregister]);
82
+ };
83
+
56
84
  /**
57
85
  * Computes the vertical position of the label text baseline.
58
86
  */
@@ -1,9 +1,6 @@
1
1
  import { getPointOnScale, isCategoricalScale } from '../../utils/scales/scales';
2
- import type {
3
- AxisConfigProps,
4
- ChartScaleFunction,
5
- DrawingArea,
6
- } from '../../utils/types';
2
+ import type { ChartScaleFunction, DrawingArea } from '../../utils/types';
3
+ import type { BaseAxisProps } from '../Axis';
7
4
 
8
5
  import type { LabelAlignment, LabelPosition } from './types';
9
6
 
@@ -28,7 +25,7 @@ type ResolvePixelParams = {
28
25
  scale: ChartScaleFunction | undefined;
29
26
  axis: 'x' | 'y';
30
27
  drawingArea: DrawingArea;
31
- axisConfig?: AxisConfigProps;
28
+ axisConfig?: BaseAxisProps;
32
29
  };
33
30
 
34
31
  type LabelParams = {
@@ -95,7 +92,7 @@ export const isPixelWithinDrawingArea = (
95
92
  */
96
93
  const resolveDataValue = (
97
94
  index: number,
98
- axisConfig?: AxisConfigProps,
95
+ axisConfig?: BaseAxisProps,
99
96
  ): number | undefined => {
100
97
  const data = axisConfig?.data;
101
98
  if (!data || data.length === 0) return index;
@@ -31,7 +31,7 @@ export const ChartTooltipItem = memo(function ChartTooltipItem({
31
31
  textAnchor='start'
32
32
  alignmentBaseline='central'
33
33
  fill={theme.colors.text.muted}
34
- fontSize={theme.typographies.body4.fontSize}
34
+ fontSize={theme.typographies.body3.fontSize}
35
35
  fontFamily={theme.fontFamilies.sans}
36
36
  >
37
37
  {String(label)}
@@ -43,8 +43,8 @@ export const ChartTooltipItem = memo(function ChartTooltipItem({
43
43
  textAnchor='end'
44
44
  alignmentBaseline='central'
45
45
  fill={theme.colors.text.base}
46
- fontSize={theme.typographies.body4.fontSize}
47
- fontWeight={theme.typographies.body4.fontWeight}
46
+ fontSize={theme.typographies.body3SemiBold.fontSize}
47
+ fontWeight={theme.typographies.body3SemiBold.fontWeight}
48
48
  fontFamily={theme.fontFamilies.sans}
49
49
  >
50
50
  {String(value)}
@@ -148,7 +148,7 @@ describe('DefaultScrubberTooltip', () => {
148
148
  scrubberProps: { tooltip: defaultTooltip },
149
149
  scrubberContext: scrubberAt(2),
150
150
  });
151
- expect(getByTestId('chart-tooltip-rect').props.x).toBe(210);
151
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(235);
152
152
  });
153
153
 
154
154
  it('flips tooltip left when right side would overflow (index 3)', () => {
@@ -158,7 +158,7 @@ describe('DefaultScrubberTooltip', () => {
158
158
  },
159
159
  scrubberContext: scrubberAt(3),
160
160
  });
161
- expect(getByTestId('chart-tooltip-rect').props.x).toBe(145);
161
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(195);
162
162
  });
163
163
 
164
164
  it('flips at the rightmost scrubber position (index 4)', () => {
@@ -168,7 +168,7 @@ describe('DefaultScrubberTooltip', () => {
168
168
  },
169
169
  scrubberContext: scrubberAt(4),
170
170
  });
171
- expect(getByTestId('chart-tooltip-rect').props.x).toBe(220);
171
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(295);
172
172
  });
173
173
 
174
174
  it('custom offset shifts the no-flip tooltip position', () => {
@@ -178,7 +178,7 @@ describe('DefaultScrubberTooltip', () => {
178
178
  },
179
179
  scrubberContext: scrubberAt(2),
180
180
  });
181
- expect(getByTestId('chart-tooltip-rect').props.x).toBe(220);
181
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(245);
182
182
  });
183
183
 
184
184
  it('wider minWidth can trigger a flip that the default width would not', () => {
@@ -188,7 +188,7 @@ describe('DefaultScrubberTooltip', () => {
188
188
  },
189
189
  scrubberContext: scrubberAt(2),
190
190
  });
191
- expect(getByTestId('chart-tooltip-rect').props.x).toBe(50);
191
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(25);
192
192
  });
193
193
  });
194
194
 
@@ -4,7 +4,6 @@ import { G, Rect, Text as SvgText } from 'react-native-svg';
4
4
  import type { ScrubberTooltipProps } from '../types';
5
5
  import { ChartTooltipItem } from './ChartTooltipItem';
6
6
  import {
7
- BORDER_RADIUS,
8
7
  DEFAULT_OFFSET,
9
8
  DEFAULT_TOOLTIP_MIN_WIDTH,
10
9
  PADDING_X,
@@ -68,7 +67,7 @@ export function DefaultScrubberTooltip({
68
67
  y={drawingArea.y}
69
68
  width={tooltipWidth}
70
69
  height={tooltipHeight}
71
- rx={BORDER_RADIUS}
70
+ rx={theme.borderRadius.sm}
72
71
  fill={theme.colors.bg.muted}
73
72
  />
74
73
  {hasTitle && (
@@ -80,9 +79,9 @@ export function DefaultScrubberTooltip({
80
79
  x={tooltipX + PADDING_X}
81
80
  y={drawingArea.y + PADDING_Y + ROW_HEIGHT / 2}
82
81
  alignmentBaseline='central'
83
- fill={theme.colors.text.base}
84
- fontSize={theme.typographies.body4.fontSize}
85
- fontWeight={theme.typographies.body4.fontWeight}
82
+ fill={theme.colors.text.muted}
83
+ fontSize={theme.typographies.body3.fontSize}
84
+ fontWeight={theme.typographies.body3.fontWeight}
86
85
  fontFamily={theme.fontFamilies.sans}
87
86
  >
88
87
  {String(title)}
@@ -3,7 +3,6 @@ export const DEFAULT_TOOLTIP_MIN_WIDTH = 80;
3
3
  export const PADDING_X = 8;
4
4
  export const PADDING_Y = 8;
5
5
  export const ROW_HEIGHT = 16;
6
- export const ROW_GAP = 6;
6
+ export const ROW_GAP = 4;
7
7
  export const TITLE_GAP = 6;
8
- export const BORDER_RADIUS = 4;
9
8
  export const LABEL_VALUE_GAP = 12;
@@ -192,7 +192,7 @@ describe('measureWidths', () => {
192
192
  titleRef,
193
193
  labelRefs,
194
194
  valueRefs,
195
- );
195
+ ).promise;
196
196
 
197
197
  expect(result).toEqual({ title: 50, labels: [30], values: [20] });
198
198
  raf.mockRestore();
@@ -212,7 +212,7 @@ describe('measureWidths', () => {
212
212
  titleRef,
213
213
  labelRefs,
214
214
  valueRefs,
215
- );
215
+ ).promise;
216
216
 
217
217
  expect(result.title).toBe(0);
218
218
  raf.mockRestore();
@@ -235,7 +235,7 @@ describe('measureWidths', () => {
235
235
  titleRef,
236
236
  labelRefs,
237
237
  valueRefs,
238
- );
238
+ ).promise;
239
239
 
240
240
  expect(result.labels).toEqual([10, 20]);
241
241
  expect(result.values).toEqual([5, 15]);
@@ -256,7 +256,7 @@ describe('measureWidths', () => {
256
256
  titleRef,
257
257
  labelRefs,
258
258
  valueRefs,
259
- );
259
+ ).promise;
260
260
 
261
261
  expect(result).toEqual({ title: 0, labels: [0], values: [0] });
262
262
  raf.mockRestore();
@@ -107,29 +107,41 @@ export const computeItemsBaseY = (
107
107
  return drawingAreaY + PADDING_Y + titleBlockHeight;
108
108
  };
109
109
 
110
- const waitForAnimationFrame = (): Promise<void> =>
111
- new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
110
+ const waitForAnimationFrame = (): {
111
+ promise: Promise<void>;
112
+ cancel: () => void;
113
+ } => {
114
+ let id: number;
115
+ const promise = new Promise<void>((resolve) => {
116
+ id = requestAnimationFrame(() => resolve());
117
+ });
118
+ return { promise, cancel: () => cancelAnimationFrame(id) };
119
+ };
112
120
 
113
121
  /**
114
122
  * Reads the rendered widths of all tooltip text elements via async `getBBox`.
115
123
  * Defers until the next animation frame so layout has settled.
124
+ * Returns a cancel function so callers can abort the pending RAF.
116
125
  */
117
- export const measureWidths = async (
126
+ export const measureWidths = (
118
127
  items: ChartTooltipItemData[],
119
128
  hasTitle: boolean,
120
129
  titleRef: RefObject<SvgBBoxElement | null>,
121
130
  labelRefs: RefObject<(SvgBBoxElement | null)[]>,
122
131
  valueRefs: RefObject<(SvgBBoxElement | null)[]>,
123
- ): Promise<Widths> => {
124
- await waitForAnimationFrame();
125
- const title = hasTitle ? await safeGetBBoxWidth(titleRef.current) : 0;
126
- const labels = await Promise.all(
127
- items.map((_, i) => safeGetBBoxWidth(labelRefs.current[i])),
128
- );
129
- const values = await Promise.all(
130
- items.map((_, i) => safeGetBBoxWidth(valueRefs.current[i])),
131
- );
132
- return { title, labels, values };
132
+ ): { promise: Promise<Widths>; cancel: () => void } => {
133
+ const raf = waitForAnimationFrame();
134
+ const promise = raf.promise.then(async () => {
135
+ const title = hasTitle ? await safeGetBBoxWidth(titleRef.current) : 0;
136
+ const labels = await Promise.all(
137
+ items.map((_, i) => safeGetBBoxWidth(labelRefs.current[i])),
138
+ );
139
+ const values = await Promise.all(
140
+ items.map((_, i) => safeGetBBoxWidth(valueRefs.current[i])),
141
+ );
142
+ return { title, labels, values };
143
+ });
144
+ return { promise, cancel: raf.cancel };
133
145
  };
134
146
 
135
147
  /**
@@ -186,18 +198,20 @@ export function useTooltipMeasurement(
186
198
 
187
199
  let cancelled = false;
188
200
 
189
- void measureWidths(
201
+ const { promise, cancel } = measureWidths(
190
202
  itemsRef.current,
191
203
  hasTitleRef.current,
192
204
  titleRef,
193
205
  labelRefs,
194
206
  valueRefs,
195
- ).then((result) => {
207
+ );
208
+ void promise.then((result) => {
196
209
  if (!cancelled) setWidths(result);
197
210
  });
198
211
 
199
212
  return () => {
200
213
  cancelled = true;
214
+ cancel();
201
215
  };
202
216
  }, [shapeKey]);
203
217