@ledgerhq/lumen-ui-rnative-visualization 0.1.13 → 0.1.15

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 (182) 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 +18 -8
  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 +52 -0
  41. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js.map +1 -0
  42. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js +81 -0
  43. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js.map +1 -0
  44. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js +340 -0
  45. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js.map +1 -0
  46. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js +12 -0
  47. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js.map +1 -0
  48. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +166 -0
  49. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -0
  50. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js +253 -0
  51. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js.map +1 -0
  52. package/dist/module/lib/Components/Scrubber/Scrubber.js +40 -21
  53. package/dist/module/lib/Components/Scrubber/Scrubber.js.map +1 -1
  54. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js +80 -30
  55. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js.map +1 -1
  56. package/dist/module/lib/Components/Scrubber/Scrubber.test.js +10 -5
  57. package/dist/module/lib/Components/Scrubber/Scrubber.test.js.map +1 -1
  58. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +81 -29
  59. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  60. package/dist/module/lib/Components/Scrubber/utils.js +64 -0
  61. package/dist/module/lib/Components/Scrubber/utils.js.map +1 -1
  62. package/dist/module/lib/Components/Scrubber/utils.test.js +128 -1
  63. package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -1
  64. package/dist/module/lib/utils/domain/domain.js.map +1 -1
  65. package/dist/module/lib/utils/scales/scales.js +13 -4
  66. package/dist/module/lib/utils/scales/scales.js.map +1 -1
  67. package/dist/module/lib/utils/scales/scales.test.js +29 -0
  68. package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
  69. package/dist/module/lib/utils/ticks/ticks.js +13 -3
  70. package/dist/module/lib/utils/ticks/ticks.js.map +1 -1
  71. package/dist/module/lib/utils/ticks/ticks.test.js +34 -0
  72. package/dist/module/lib/utils/ticks/ticks.test.js.map +1 -1
  73. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts +5 -0
  74. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts.map +1 -0
  75. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts +39 -0
  76. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts.map +1 -1
  77. package/dist/typescript/src/lib/Components/Axis/index.d.ts +3 -0
  78. package/dist/typescript/src/lib/Components/Axis/index.d.ts.map +1 -0
  79. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  80. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  81. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts +5 -4
  82. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts.map +1 -1
  83. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +11 -3
  84. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  85. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts +18 -8
  86. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts.map +1 -1
  87. package/dist/typescript/src/lib/Components/Line/utils.d.ts +4 -0
  88. package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -1
  89. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
  90. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  91. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +10 -3
  92. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
  93. package/dist/typescript/src/lib/Components/Point/Point.d.ts +1 -1
  94. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  95. package/dist/typescript/src/lib/Components/Point/constants.d.ts +7 -0
  96. package/dist/typescript/src/lib/Components/Point/constants.d.ts.map +1 -0
  97. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts +7 -0
  98. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -0
  99. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +3 -0
  100. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -0
  101. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +13 -0
  102. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -0
  103. package/dist/typescript/src/lib/Components/Point/types.d.ts +7 -0
  104. package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -1
  105. package/dist/typescript/src/lib/Components/Point/utils.d.ts +8 -6
  106. package/dist/typescript/src/lib/Components/Point/utils.d.ts.map +1 -1
  107. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +3 -2
  108. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -1
  109. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.d.ts +9 -0
  110. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.d.ts.map +1 -0
  111. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts +12 -0
  112. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts.map +1 -0
  113. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts +10 -0
  114. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts.map +1 -0
  115. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts +69 -0
  116. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -0
  117. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts +14 -2
  118. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts.map +1 -1
  119. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +1 -1
  120. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  121. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts +1 -1
  122. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts.map +1 -1
  123. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +116 -4
  124. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -1
  125. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +33 -3
  126. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -1
  127. package/dist/typescript/src/lib/utils/domain/domain.d.ts +5 -4
  128. package/dist/typescript/src/lib/utils/domain/domain.d.ts.map +1 -1
  129. package/dist/typescript/src/lib/utils/index.d.ts +1 -1
  130. package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
  131. package/dist/typescript/src/lib/utils/scales/scales.d.ts +11 -5
  132. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
  133. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +13 -5
  134. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -1
  135. package/dist/typescript/src/lib/utils/types.d.ts +3 -28
  136. package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
  137. package/package.json +4 -4
  138. package/src/lib/Components/Axis/Axis.constants.ts +23 -0
  139. package/src/lib/Components/Axis/Axis.types.ts +40 -0
  140. package/src/lib/Components/Axis/index.ts +2 -0
  141. package/src/lib/Components/CartesianChart/CartesianChart.tsx +39 -24
  142. package/src/lib/Components/CartesianChart/context/useBuildChartContext.test.ts +56 -0
  143. package/src/lib/Components/CartesianChart/context/useBuildChartContext.ts +8 -8
  144. package/src/lib/Components/CartesianChart/types.ts +11 -3
  145. package/src/lib/Components/CartesianChart/utils.ts +18 -8
  146. package/src/lib/Components/Line/utils.test.ts +90 -0
  147. package/src/lib/Components/Line/utils.ts +6 -1
  148. package/src/lib/Components/LineChart/LineChart.stories.tsx +36 -0
  149. package/src/lib/Components/LineChart/LineChart.test.tsx +156 -0
  150. package/src/lib/Components/LineChart/LineChart.tsx +30 -40
  151. package/src/lib/Components/LineChart/types.ts +10 -3
  152. package/src/lib/Components/Point/Point.tsx +9 -3
  153. package/src/lib/Components/Point/constants.ts +6 -0
  154. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +162 -0
  155. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +45 -0
  156. package/src/lib/Components/Point/pointContext/index.ts +5 -0
  157. package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +19 -0
  158. package/src/lib/Components/Point/types.ts +7 -0
  159. package/src/lib/Components/Point/utils.test.ts +95 -5
  160. package/src/lib/Components/Point/utils.ts +35 -7
  161. package/src/lib/Components/ReferenceLine/utils.ts +4 -7
  162. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.tsx +54 -0
  163. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.tsx +259 -0
  164. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.tsx +105 -0
  165. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.ts +9 -0
  166. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.ts +264 -0
  167. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +243 -0
  168. package/src/lib/Components/Scrubber/Scrubber.stories.tsx +104 -42
  169. package/src/lib/Components/Scrubber/Scrubber.test.tsx +7 -5
  170. package/src/lib/Components/Scrubber/Scrubber.tsx +47 -24
  171. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +92 -28
  172. package/src/lib/Components/Scrubber/index.ts +10 -1
  173. package/src/lib/Components/Scrubber/types.ts +122 -4
  174. package/src/lib/Components/Scrubber/utils.test.ts +150 -0
  175. package/src/lib/Components/Scrubber/utils.ts +93 -3
  176. package/src/lib/utils/domain/domain.ts +6 -5
  177. package/src/lib/utils/index.ts +0 -2
  178. package/src/lib/utils/scales/scales.test.ts +19 -0
  179. package/src/lib/utils/scales/scales.ts +15 -9
  180. package/src/lib/utils/ticks/ticks.test.ts +45 -0
  181. package/src/lib/utils/ticks/ticks.ts +19 -9
  182. package/src/lib/utils/types.ts +3 -26
@@ -0,0 +1,264 @@
1
+ import { describe, expect, it, jest } from '@jest/globals';
2
+
3
+ import {
4
+ LABEL_VALUE_GAP,
5
+ PADDING_X,
6
+ PADDING_Y,
7
+ ROW_GAP,
8
+ ROW_HEIGHT,
9
+ TITLE_GAP,
10
+ } from './constants';
11
+ import {
12
+ computeItemsBaseY,
13
+ computeTooltipHeight,
14
+ computeTooltipWidth,
15
+ computeTooltipX,
16
+ measureWidths,
17
+ safeGetBBoxWidth,
18
+ } from './utils';
19
+
20
+ describe('safeGetBBoxWidth', () => {
21
+ it('returns 0 for null', async () => {
22
+ expect(await safeGetBBoxWidth(null)).toBe(0);
23
+ });
24
+
25
+ it('returns the width from getBBox', async () => {
26
+ const el = { getBBox: async () => ({ width: 42 }) };
27
+ expect(await safeGetBBoxWidth(el)).toBe(42);
28
+ });
29
+
30
+ it('returns 0 when getBBox resolves to undefined', async () => {
31
+ const el = { getBBox: async () => undefined };
32
+ expect(await safeGetBBoxWidth(el)).toBe(0);
33
+ });
34
+
35
+ it('returns 0 when getBBox throws', async () => {
36
+ const el = {
37
+ getBBox: async () => {
38
+ throw new Error('not supported');
39
+ },
40
+ };
41
+ expect(await safeGetBBoxWidth(el)).toBe(0);
42
+ });
43
+ });
44
+
45
+ describe('computeTooltipWidth', () => {
46
+ it('returns minWidth when widths is null', () => {
47
+ expect(computeTooltipWidth(null, false, 80)).toBe(80);
48
+ });
49
+
50
+ it('returns minWidth when computed fit is smaller', () => {
51
+ const widths = { title: 0, labels: [10], values: [10] };
52
+ const fitWidth = 10 + LABEL_VALUE_GAP + 10 + PADDING_X * 2;
53
+ expect(computeTooltipWidth(widths, false, fitWidth + 50)).toBe(
54
+ fitWidth + 50,
55
+ );
56
+ });
57
+
58
+ it('returns fit width when it exceeds minWidth', () => {
59
+ const widths = { title: 0, labels: [200], values: [200] };
60
+ const expected = 200 + LABEL_VALUE_GAP + 200 + PADDING_X * 2;
61
+ expect(computeTooltipWidth(widths, false, 80)).toBe(expected);
62
+ });
63
+
64
+ it('accounts for title width when hasTitle is true', () => {
65
+ const widths = { title: 300, labels: [10], values: [10] };
66
+ const expected = 300 + PADDING_X * 2;
67
+ expect(computeTooltipWidth(widths, true, 80)).toBe(expected);
68
+ });
69
+
70
+ it('ignores title width when hasTitle is false', () => {
71
+ const widths = { title: 300, labels: [10], values: [10] };
72
+ const rowFit = 10 + LABEL_VALUE_GAP + 10 + PADDING_X * 2;
73
+ expect(computeTooltipWidth(widths, false, 40)).toBe(rowFit);
74
+ expect(computeTooltipWidth(widths, false, 80)).toBe(80);
75
+ });
76
+
77
+ it('uses the widest row when multiple items are present', () => {
78
+ const widths = {
79
+ title: 0,
80
+ labels: [10, 50],
81
+ values: [10, 50],
82
+ };
83
+ const widestRow = 50 + LABEL_VALUE_GAP + 50;
84
+ const expected = widestRow + PADDING_X * 2;
85
+ expect(computeTooltipWidth(widths, false, 80)).toBe(expected);
86
+ });
87
+ });
88
+
89
+ describe('computeTooltipX', () => {
90
+ const drawingArea = { x: 0, y: 0, width: 400, height: 200 };
91
+
92
+ it('places tooltip to the right when there is enough space', () => {
93
+ expect(computeTooltipX(100, 10, 80, drawingArea)).toBe(110);
94
+ });
95
+
96
+ it('flips to the left when placing right would overflow', () => {
97
+ expect(computeTooltipX(350, 10, 80, drawingArea)).toBe(260);
98
+ });
99
+
100
+ it('clamps to drawingArea.x when flipped position is negative', () => {
101
+ expect(computeTooltipX(195, 10, 200, drawingArea)).toBe(0);
102
+ });
103
+
104
+ it('clamps the right-side position to not overflow the right edge', () => {
105
+ expect(computeTooltipX(100, 10, 400, drawingArea)).toBe(0);
106
+ });
107
+
108
+ it('respects a non-zero drawingArea.x as the minimum clamp', () => {
109
+ const area = { x: 20, y: 0, width: 400, height: 200 };
110
+ expect(computeTooltipX(5, 10, 200, area)).toBe(20);
111
+ });
112
+
113
+ it('respects a custom offset', () => {
114
+ expect(computeTooltipX(100, 20, 80, drawingArea)).toBe(120);
115
+ });
116
+ });
117
+
118
+ describe('computeTooltipHeight', () => {
119
+ it('computes height for a single item without a title', () => {
120
+ const expected = PADDING_Y * 2 + ROW_HEIGHT;
121
+ expect(computeTooltipHeight(1, false)).toBe(expected);
122
+ });
123
+
124
+ it('adds the title block height when hasTitle is true', () => {
125
+ const titleBlockHeight = ROW_HEIGHT + TITLE_GAP;
126
+ const expected = PADDING_Y * 2 + titleBlockHeight + ROW_HEIGHT;
127
+ expect(computeTooltipHeight(1, true)).toBe(expected);
128
+ });
129
+
130
+ it('adds row gaps for multiple items', () => {
131
+ const itemCount = 3;
132
+ const expected =
133
+ PADDING_Y * 2 + itemCount * ROW_HEIGHT + (itemCount - 1) * ROW_GAP;
134
+ expect(computeTooltipHeight(itemCount, false)).toBe(expected);
135
+ });
136
+
137
+ it('combines title block and row gaps correctly', () => {
138
+ const itemCount = 2;
139
+ const titleBlockHeight = ROW_HEIGHT + TITLE_GAP;
140
+ const expected =
141
+ PADDING_Y * 2 +
142
+ titleBlockHeight +
143
+ itemCount * ROW_HEIGHT +
144
+ (itemCount - 1) * ROW_GAP;
145
+ expect(computeTooltipHeight(itemCount, true)).toBe(expected);
146
+ });
147
+ });
148
+
149
+ describe('computeItemsBaseY', () => {
150
+ it('returns drawingAreaY + PADDING_Y when there is no title', () => {
151
+ expect(computeItemsBaseY(10, false)).toBe(10 + PADDING_Y);
152
+ });
153
+
154
+ it('adds the title block height when hasTitle is true', () => {
155
+ const titleBlockHeight = ROW_HEIGHT + TITLE_GAP;
156
+ expect(computeItemsBaseY(10, true)).toBe(10 + PADDING_Y + titleBlockHeight);
157
+ });
158
+
159
+ it('handles drawingAreaY of 0', () => {
160
+ expect(computeItemsBaseY(0, false)).toBe(PADDING_Y);
161
+ });
162
+ });
163
+
164
+ describe('measureWidths', () => {
165
+ const makeBBoxEl = (width: number) => ({
166
+ getBBox: jest.fn(async () => ({ width })),
167
+ });
168
+
169
+ const makeRaf = () =>
170
+ jest
171
+ .spyOn(global, 'requestAnimationFrame')
172
+ .mockImplementation((cb: FrameRequestCallback) => {
173
+ cb(0);
174
+ return 0;
175
+ });
176
+
177
+ it('measures title, label and value widths after a RAF tick', async () => {
178
+ const raf = makeRaf();
179
+
180
+ const titleEl = makeBBoxEl(50);
181
+ const labelEl = makeBBoxEl(30);
182
+ const valueEl = makeBBoxEl(20);
183
+
184
+ const items = [{ label: 'L', value: 'V' }];
185
+ const titleRef = { current: titleEl };
186
+ const labelRefs = { current: [labelEl] };
187
+ const valueRefs = { current: [valueEl] };
188
+
189
+ const result = await measureWidths(
190
+ items,
191
+ true,
192
+ titleRef,
193
+ labelRefs,
194
+ valueRefs,
195
+ ).promise;
196
+
197
+ expect(result).toEqual({ title: 50, labels: [30], values: [20] });
198
+ raf.mockRestore();
199
+ });
200
+
201
+ it('returns 0 for the title when hasTitle is false', async () => {
202
+ const raf = makeRaf();
203
+
204
+ const items = [{ label: 'L', value: 'V' }];
205
+ const titleRef = { current: makeBBoxEl(99) };
206
+ const labelRefs = { current: [makeBBoxEl(10)] };
207
+ const valueRefs = { current: [makeBBoxEl(10)] };
208
+
209
+ const result = await measureWidths(
210
+ items,
211
+ false,
212
+ titleRef,
213
+ labelRefs,
214
+ valueRefs,
215
+ ).promise;
216
+
217
+ expect(result.title).toBe(0);
218
+ raf.mockRestore();
219
+ });
220
+
221
+ it('measures multiple items', async () => {
222
+ const raf = makeRaf();
223
+
224
+ const items = [
225
+ { label: 'A', value: '1' },
226
+ { label: 'B', value: '2' },
227
+ ];
228
+ const titleRef = { current: null };
229
+ const labelRefs = { current: [makeBBoxEl(10), makeBBoxEl(20)] };
230
+ const valueRefs = { current: [makeBBoxEl(5), makeBBoxEl(15)] };
231
+
232
+ const result = await measureWidths(
233
+ items,
234
+ false,
235
+ titleRef,
236
+ labelRefs,
237
+ valueRefs,
238
+ ).promise;
239
+
240
+ expect(result.labels).toEqual([10, 20]);
241
+ expect(result.values).toEqual([5, 15]);
242
+ raf.mockRestore();
243
+ });
244
+
245
+ it('returns 0 for null refs', async () => {
246
+ const raf = makeRaf();
247
+
248
+ const items = [{ label: 'L', value: 'V' }];
249
+ const titleRef = { current: null };
250
+ const labelRefs = { current: [null] };
251
+ const valueRefs = { current: [null] };
252
+
253
+ const result = await measureWidths(
254
+ items,
255
+ true,
256
+ titleRef,
257
+ labelRefs,
258
+ valueRefs,
259
+ ).promise;
260
+
261
+ expect(result).toEqual({ title: 0, labels: [0], values: [0] });
262
+ raf.mockRestore();
263
+ });
264
+ });
@@ -0,0 +1,243 @@
1
+ import type { RefObject } from 'react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+
4
+ import type { DrawingArea } from '../../../utils/types';
5
+ import type { ChartTooltipItemData, SvgBBoxElement } from '../types';
6
+ import {
7
+ LABEL_VALUE_GAP,
8
+ PADDING_X,
9
+ PADDING_Y,
10
+ ROW_GAP,
11
+ ROW_HEIGHT,
12
+ TITLE_GAP,
13
+ } from './constants';
14
+
15
+ type Widths = {
16
+ title: number;
17
+ labels: number[];
18
+ values: number[];
19
+ };
20
+
21
+ type UseTooltipMeasurementReturn = {
22
+ widths: Widths | null;
23
+ titleRef: RefObject<SvgBBoxElement | null>;
24
+ labelRefs: RefObject<(SvgBBoxElement | null)[]>;
25
+ valueRefs: RefObject<(SvgBBoxElement | null)[]>;
26
+ };
27
+
28
+ export const safeGetBBoxWidth = async (
29
+ el: SvgBBoxElement | null,
30
+ ): Promise<number> => {
31
+ if (!el) return 0;
32
+ try {
33
+ const result = await el.getBBox();
34
+ return result?.width ?? 0;
35
+ } catch {
36
+ return 0;
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Derive the auto-fit tooltip width from measured text widths,
42
+ * clamped to a minimum floor.
43
+ */
44
+ export const computeTooltipWidth = (
45
+ widths: Widths | null,
46
+ hasTitle: boolean,
47
+ minWidth: number,
48
+ ): number => {
49
+ const titleWidth = widths && hasTitle ? widths.title : 0;
50
+ const rowWidths = widths
51
+ ? widths.labels.map(
52
+ (lw, i) => lw + LABEL_VALUE_GAP + (widths.values[i] ?? 0),
53
+ )
54
+ : [];
55
+ const contentWidth = Math.max(titleWidth, ...rowWidths);
56
+ const fitWidth = contentWidth + PADDING_X * 2;
57
+ return Math.max(fitWidth, minWidth);
58
+ };
59
+
60
+ /**
61
+ * Compute the tooltip's horizontal position, flipping to the left
62
+ * of the scrubber line when it would overflow the drawing area.
63
+ * Also clamps from above to prevent overflow on the right edge.
64
+ */
65
+ export const computeTooltipX = (
66
+ pixelX: number,
67
+ offset: number,
68
+ tooltipWidth: number,
69
+ drawingArea: DrawingArea,
70
+ ): number => {
71
+ const shouldFlip =
72
+ pixelX + offset + tooltipWidth > drawingArea.x + drawingArea.width;
73
+ const preferredTooltipX = shouldFlip
74
+ ? pixelX - offset - tooltipWidth
75
+ : pixelX + offset;
76
+ const maxTooltipX = Math.max(
77
+ drawingArea.x,
78
+ drawingArea.x + drawingArea.width - tooltipWidth,
79
+ );
80
+ return Math.min(maxTooltipX, Math.max(drawingArea.x, preferredTooltipX));
81
+ };
82
+
83
+ /**
84
+ * Compute the total tooltip height from item count and title presence.
85
+ */
86
+ export const computeTooltipHeight = (
87
+ itemCount: number,
88
+ hasTitle: boolean,
89
+ ): number => {
90
+ const titleBlockHeight = hasTitle ? ROW_HEIGHT + TITLE_GAP : 0;
91
+ return (
92
+ PADDING_Y * 2 +
93
+ titleBlockHeight +
94
+ itemCount * ROW_HEIGHT +
95
+ (itemCount - 1) * ROW_GAP
96
+ );
97
+ };
98
+
99
+ /**
100
+ * Vertical offset where tooltip rows begin, accounting for title presence.
101
+ */
102
+ export const computeItemsBaseY = (
103
+ drawingAreaY: number,
104
+ hasTitle: boolean,
105
+ ): number => {
106
+ const titleBlockHeight = hasTitle ? ROW_HEIGHT + TITLE_GAP : 0;
107
+ return drawingAreaY + PADDING_Y + titleBlockHeight;
108
+ };
109
+
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
+ };
120
+
121
+ /**
122
+ * Reads the rendered widths of all tooltip text elements via async `getBBox`.
123
+ * Defers until the next animation frame so layout has settled.
124
+ * Returns a cancel function so callers can abort the pending RAF.
125
+ */
126
+ export const measureWidths = (
127
+ items: ChartTooltipItemData[],
128
+ hasTitle: boolean,
129
+ titleRef: RefObject<SvgBBoxElement | null>,
130
+ labelRefs: RefObject<(SvgBBoxElement | null)[]>,
131
+ valueRefs: RefObject<(SvgBBoxElement | null)[]>,
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 };
145
+ };
146
+
147
+ /**
148
+ * Builds a stable string signature for the tooltip's content shape (labels,
149
+ * title, and item count). Used as the sole dependency of the measurement
150
+ * effect so re-measurement only happens when something that can change the
151
+ * tooltip's natural width actually changes — typically once per shape change,
152
+ * not on every scrub frame as value strings update.
153
+ */
154
+ const computeShapeSignature = (
155
+ items: ChartTooltipItemData[],
156
+ hasTitle: boolean,
157
+ title: string | number | undefined,
158
+ ): string => {
159
+ let labels = '';
160
+ for (let i = 0; i < items.length; i++) {
161
+ if (i > 0) labels += '\u001F';
162
+ labels += String(items[i].label);
163
+ }
164
+ return `${hasTitle ? '1' : '0'}|${labels}|${title ?? ''}`;
165
+ };
166
+
167
+ /**
168
+ * Manages SVG text measurement for the tooltip via async `getBBox`.
169
+ *
170
+ * Uses `requestAnimationFrame` to defer measurement until after layout, with a
171
+ * cancellation flag to prevent stale state updates when items change rapidly.
172
+ *
173
+ * The effect's dependency is a shape signature (labels + title + count) rather
174
+ * than the raw `items` reference, so high-frequency value updates during a
175
+ * scrub do not trigger a fresh measurement chain. Value-width auto-fit is
176
+ * handled by `minWidth`; consumers that need pixel-perfect value fitting
177
+ * should set a `minWidth` large enough to cover the widest value.
178
+ */
179
+ export function useTooltipMeasurement(
180
+ items: ChartTooltipItemData[],
181
+ hasTitle: boolean,
182
+ title: string | number | undefined,
183
+ ): UseTooltipMeasurementReturn {
184
+ const titleRef = useRef<SvgBBoxElement | null>(null);
185
+ const labelRefs = useRef<(SvgBBoxElement | null)[]>([]);
186
+ const valueRefs = useRef<(SvgBBoxElement | null)[]>([]);
187
+
188
+ const [widths, setWidths] = useState<Widths | null>(null);
189
+
190
+ const shapeKey = computeShapeSignature(items, hasTitle, title);
191
+ const itemsRef = useRef(items);
192
+ itemsRef.current = items;
193
+ const hasTitleRef = useRef(hasTitle);
194
+ hasTitleRef.current = hasTitle;
195
+
196
+ useEffect(() => {
197
+ if (itemsRef.current.length === 0) return;
198
+
199
+ let cancelled = false;
200
+
201
+ const { promise, cancel } = measureWidths(
202
+ itemsRef.current,
203
+ hasTitleRef.current,
204
+ titleRef,
205
+ labelRefs,
206
+ valueRefs,
207
+ );
208
+ void promise.then((result) => {
209
+ if (!cancelled) setWidths(result);
210
+ });
211
+
212
+ return () => {
213
+ cancelled = true;
214
+ cancel();
215
+ };
216
+ }, [shapeKey]);
217
+
218
+ return { widths, titleRef, labelRefs, valueRefs };
219
+ }
220
+
221
+ type RefSetter = (el: SvgBBoxElement | null) => void;
222
+
223
+ /**
224
+ * Builds stable per-index ref-callback arrays that mutate the given refs in
225
+ * place. Recomputed only when `length` changes, so the same callback identity
226
+ * is passed to each `ChartTooltipItem` across scrub frames. This unlocks
227
+ * `React.memo` on `ChartTooltipItem` and avoids the per-frame detach /
228
+ * re-attach dance of inline arrow ref callbacks.
229
+ */
230
+ export const useBuildRefSetters = (
231
+ refs: { current: (SvgBBoxElement | null)[] },
232
+ length: number,
233
+ ): RefSetter[] => {
234
+ return useMemo(() => {
235
+ const setters: RefSetter[] = new Array(length);
236
+ for (let i = 0; i < length; i++) {
237
+ setters[i] = (el) => {
238
+ refs.current[i] = el;
239
+ };
240
+ }
241
+ return setters;
242
+ }, [refs, length]);
243
+ };
@@ -1,9 +1,8 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
- import { useState } from 'react';
3
- import { Text, View } from 'react-native';
4
2
 
5
3
  import { StoryDecorator } from '../../../../.storybook/StoryDecorator.tsx';
6
4
  import { LineChart } from '../LineChart';
5
+ import { Point } from '../Point';
7
6
  import { Scrubber } from './Scrubber';
8
7
  import type { ScrubberProps } from './types';
9
8
 
@@ -23,21 +22,6 @@ const meta = {
23
22
  export default meta;
24
23
  type Story = StoryObj<typeof Scrubber>;
25
24
 
26
- const dates = [
27
- 'Jan',
28
- 'Feb',
29
- 'Mar',
30
- 'Apr',
31
- 'May',
32
- 'Jun',
33
- 'Jul',
34
- 'Aug',
35
- 'Sep',
36
- 'Oct',
37
- 'Nov',
38
- 'Dec',
39
- ];
40
-
41
25
  const singleSeries = [
42
26
  {
43
27
  id: 'prices',
@@ -70,34 +54,10 @@ export const Base: Story = {
70
54
  args: {},
71
55
  };
72
56
 
73
- export const WithLabel: Story = {
74
- render: (args: ScrubberProps) => {
75
- const [activeIndex, setActiveIndex] = useState<number | undefined>();
76
- const displayIndex = activeIndex ?? singleSeries[0].data.length - 1;
77
- return (
78
- <View>
79
- <Text style={{ marginBottom: 8, fontSize: 14 }}>
80
- {dates[displayIndex]} — ${singleSeries[0].data[displayIndex]}
81
- </Text>
82
- <LineChart
83
- series={singleSeries}
84
- height={200}
85
- showArea
86
- enableScrubbing
87
- onScrubberPositionChange={setActiveIndex}
88
- >
89
- <Scrubber {...args} label={(i: number) => dates[i] ?? ''} />
90
- </LineChart>
91
- </View>
92
- );
93
- },
94
- args: {},
95
- };
96
-
97
57
  export const MultiSeries: Story = {
98
58
  render: (args: ScrubberProps) => (
99
59
  <LineChart series={multiSeries} height={200} enableScrubbing>
100
- <Scrubber {...args} label={(i: number) => dates[i] ?? ''} showBeacons />
60
+ <Scrubber {...args} showBeacons />
101
61
  </LineChart>
102
62
  ),
103
63
  args: {},
@@ -120,3 +80,105 @@ export const HiddenOverlay: Story = {
120
80
  ),
121
81
  args: {},
122
82
  };
83
+
84
+ const tooltipSampleSeries = [
85
+ {
86
+ id: 'prices',
87
+ stroke: '#7B61FF',
88
+ data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
89
+ },
90
+ ];
91
+
92
+ const tooltipDates = [
93
+ 'Jan',
94
+ 'Feb',
95
+ 'Mar',
96
+ 'Apr',
97
+ 'May',
98
+ 'Jun',
99
+ 'Jul',
100
+ 'Aug',
101
+ 'Sep',
102
+ 'Oct',
103
+ 'Nov',
104
+ 'Dec',
105
+ 'Jan',
106
+ 'Feb',
107
+ ];
108
+
109
+ const tooltipAnnotatedIndices = new Set([4, 9]);
110
+
111
+ export const WithBaseTooltip: Story = {
112
+ render: () => (
113
+ <LineChart
114
+ series={tooltipSampleSeries}
115
+ height={250}
116
+ enableScrubbing
117
+ showArea
118
+ >
119
+ <Scrubber
120
+ tooltip={(dataIndex) => ({
121
+ items: [
122
+ { label: 'Date', value: tooltipDates[dataIndex] },
123
+ {
124
+ label: 'Price',
125
+ value: `$${tooltipSampleSeries[0].data[dataIndex]}`,
126
+ },
127
+ ],
128
+ })}
129
+ />
130
+ </LineChart>
131
+ ),
132
+ };
133
+
134
+ export const WithTooltipTitle: Story = {
135
+ render: () => (
136
+ <LineChart
137
+ series={tooltipSampleSeries}
138
+ height={250}
139
+ enableScrubbing
140
+ showArea
141
+ >
142
+ <Scrubber
143
+ tooltip={(dataIndex) => ({
144
+ title: `${tooltipSampleSeries[0].data[dataIndex]} Transactions`,
145
+ items: [
146
+ { label: 'Date', value: tooltipDates[dataIndex] },
147
+ {
148
+ label: 'Price',
149
+ value: `$${tooltipSampleSeries[0].data[dataIndex]}`,
150
+ },
151
+ ],
152
+ })}
153
+ />
154
+ </LineChart>
155
+ ),
156
+ };
157
+
158
+ export const WithTooltipOnPoints: Story = {
159
+ render: () => (
160
+ <LineChart
161
+ series={tooltipSampleSeries}
162
+ height={250}
163
+ enableScrubbing
164
+ showArea
165
+ >
166
+ <Scrubber
167
+ tooltip={(dataIndex) => {
168
+ if (!tooltipAnnotatedIndices.has(dataIndex)) return { items: [] };
169
+ return {
170
+ items: [
171
+ { label: 'Date', value: tooltipDates[dataIndex] },
172
+ {
173
+ label: 'Price',
174
+ value: `$${tooltipSampleSeries[0].data[dataIndex]}`,
175
+ },
176
+ ],
177
+ };
178
+ }}
179
+ />
180
+ <Point dataX={4} dataY={98} label='ATH' />
181
+ <Point dataX={9} dataY={4} label='Low' labelPosition='bottom' />
182
+ </LineChart>
183
+ ),
184
+ };
@@ -96,15 +96,17 @@ describe('Scrubber', () => {
96
96
  expect(getAllByTestId(/scrubber-beacon-/)).toHaveLength(2);
97
97
  });
98
98
 
99
- it('renders a label when a label function is provided', () => {
99
+ it('renders chart tooltip when tooltip prop is set', () => {
100
100
  const { getByTestId } = renderScrubber({
101
- scrubberProps: { label: (i: number) => `Index ${i}` },
101
+ scrubberProps: {
102
+ tooltip: () => ({ items: [{ label: 'L', value: 'V' }] }),
103
+ },
102
104
  });
103
- expect(getByTestId('scrubber-label')).toBeTruthy();
105
+ expect(getByTestId('chart-tooltip')).toBeTruthy();
104
106
  });
105
107
 
106
- it('does not render a label when no label function is provided', () => {
108
+ it('does not render chart tooltip when tooltip prop is not set', () => {
107
109
  const { queryByTestId } = renderScrubber();
108
- expect(queryByTestId('scrubber-label')).toBeNull();
110
+ expect(queryByTestId('chart-tooltip')).toBeNull();
109
111
  });
110
112
  });