@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
@@ -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;
@@ -0,0 +1,54 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import { memo } from 'react';
3
+ import { G, Text as SvgText } from 'react-native-svg';
4
+
5
+ import type { ChartTooltipItemProps } from '../types';
6
+ import { PADDING_X } from './constants';
7
+
8
+ /**
9
+ * Memoized so it only re-renders when one of its primitive props actually
10
+ * changes. Combined with the stable per-index ref callbacks built in
11
+ * `DefaultScrubberTooltip`, this keeps the row out of the per-scrub-frame
12
+ * reconciliation path when its label / value / position have not changed.
13
+ */
14
+ export const ChartTooltipItem = memo(function ChartTooltipItem({
15
+ label,
16
+ value,
17
+ x = 0,
18
+ y = 0,
19
+ width,
20
+ labelRef,
21
+ valueRef,
22
+ }: Readonly<ChartTooltipItemProps>) {
23
+ const { theme } = useTheme();
24
+
25
+ return (
26
+ <G>
27
+ <SvgText
28
+ ref={labelRef}
29
+ x={x + PADDING_X}
30
+ y={y}
31
+ textAnchor='start'
32
+ alignmentBaseline='central'
33
+ fill={theme.colors.text.muted}
34
+ fontSize={theme.typographies.body4.fontSize}
35
+ fontFamily={theme.fontFamilies.sans}
36
+ >
37
+ {String(label)}
38
+ </SvgText>
39
+ <SvgText
40
+ ref={valueRef}
41
+ x={x + width - PADDING_X}
42
+ y={y}
43
+ textAnchor='end'
44
+ alignmentBaseline='central'
45
+ fill={theme.colors.text.base}
46
+ fontSize={theme.typographies.body4.fontSize}
47
+ fontWeight={theme.typographies.body4.fontWeight}
48
+ fontFamily={theme.fontFamilies.sans}
49
+ >
50
+ {String(value)}
51
+ </SvgText>
52
+ </G>
53
+ );
54
+ });
@@ -0,0 +1,259 @@
1
+ import { describe, expect, it, jest } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { ThemeProvider } from '@ledgerhq/lumen-ui-rnative';
4
+ import { render } from '@testing-library/react-native';
5
+ import type { ComponentProps, ReactNode } from 'react';
6
+
7
+ import { CartesianChart } from '../../CartesianChart';
8
+ import { ScrubberContextProvider } from '../context';
9
+ import { Scrubber } from '../Scrubber';
10
+ import type { ScrubberContextValue, ScrubberTooltipContent } from '../types';
11
+
12
+ const sampleSeries = [
13
+ { id: 's1', stroke: '#7B61FF', data: [10, 20, 30, 40, 50] },
14
+ ];
15
+
16
+ const Wrapper = ({ children }: { children: ReactNode }) => (
17
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
18
+ {children}
19
+ </ThemeProvider>
20
+ );
21
+
22
+ const activeScrubberContext: ScrubberContextValue = {
23
+ enableScrubbing: true,
24
+ scrubberPosition: 2,
25
+ onScrubberPositionChange: () => undefined,
26
+ };
27
+
28
+ const idleScrubberContext: ScrubberContextValue = {
29
+ enableScrubbing: true,
30
+ scrubberPosition: undefined,
31
+ onScrubberPositionChange: () => undefined,
32
+ };
33
+
34
+ const renderTooltip = ({
35
+ scrubberProps = { tooltip: () => ({ items: [] }) },
36
+ scrubberContext = activeScrubberContext,
37
+ width = 400,
38
+ height = 200,
39
+ }: {
40
+ scrubberProps?: ComponentProps<typeof Scrubber>;
41
+ scrubberContext?: ScrubberContextValue;
42
+ width?: number;
43
+ height?: number;
44
+ } = {}) =>
45
+ render(
46
+ <Wrapper>
47
+ <CartesianChart series={sampleSeries} width={width} height={height}>
48
+ <ScrubberContextProvider value={scrubberContext}>
49
+ <Scrubber {...scrubberProps} />
50
+ </ScrubberContextProvider>
51
+ </CartesianChart>
52
+ </Wrapper>,
53
+ );
54
+
55
+ describe('DefaultScrubberTooltip', () => {
56
+ it('renders nothing when no scrubber position is active', () => {
57
+ const { queryByTestId } = renderTooltip({
58
+ scrubberProps: {
59
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
60
+ },
61
+ scrubberContext: idleScrubberContext,
62
+ });
63
+ expect(queryByTestId('chart-tooltip')).toBeNull();
64
+ });
65
+
66
+ it('renders nothing when items callback returns an empty array', () => {
67
+ const { queryByTestId } = renderTooltip({
68
+ scrubberProps: { tooltip: () => ({ items: [] }) },
69
+ });
70
+ expect(queryByTestId('chart-tooltip')).toBeNull();
71
+ });
72
+
73
+ it('renders the tooltip group when scrubber is active and items are provided', () => {
74
+ const { getByTestId } = renderTooltip({
75
+ scrubberProps: {
76
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
77
+ },
78
+ });
79
+ expect(getByTestId('chart-tooltip')).toBeTruthy();
80
+ });
81
+
82
+ it('renders the background rect', () => {
83
+ const { getByTestId } = renderTooltip({
84
+ scrubberProps: {
85
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
86
+ },
87
+ });
88
+ expect(getByTestId('chart-tooltip-rect')).toBeTruthy();
89
+ });
90
+
91
+ it('renders item label and value text', () => {
92
+ const { getByText } = renderTooltip({
93
+ scrubberProps: {
94
+ tooltip: () => ({ items: [{ label: 'Date', value: 'Jan' }] }),
95
+ },
96
+ });
97
+ expect(getByText('Date')).toBeTruthy();
98
+ expect(getByText('Jan')).toBeTruthy();
99
+ });
100
+
101
+ it('renders multiple items', () => {
102
+ const { getByText } = renderTooltip({
103
+ scrubberProps: {
104
+ tooltip: () => ({
105
+ items: [
106
+ { label: 'Date', value: 'Jan' },
107
+ { label: 'Price', value: '$10' },
108
+ ],
109
+ }),
110
+ },
111
+ });
112
+ expect(getByText('Date')).toBeTruthy();
113
+ expect(getByText('Jan')).toBeTruthy();
114
+ expect(getByText('Price')).toBeTruthy();
115
+ expect(getByText('$10')).toBeTruthy();
116
+ });
117
+
118
+ it('passes the active data index to the tooltip callback', () => {
119
+ const tooltip = jest.fn().mockReturnValue({
120
+ items: [{ label: 'i', value: '0' }],
121
+ }) as unknown as (index: number) => ScrubberTooltipContent;
122
+ renderTooltip({ scrubberProps: { tooltip } });
123
+ expect(tooltip).toHaveBeenCalledWith(
124
+ activeScrubberContext.scrubberPosition,
125
+ );
126
+ });
127
+
128
+ it('suppresses the tooltip when items returns empty for the active index', () => {
129
+ const { queryByTestId } = renderTooltip({
130
+ scrubberProps: { tooltip: () => ({ items: [] }) },
131
+ });
132
+ expect(queryByTestId('chart-tooltip')).toBeNull();
133
+ });
134
+
135
+ describe('positioning logic', () => {
136
+ const defaultTooltip = (): ScrubberTooltipContent => ({
137
+ items: [{ label: 'Date', value: 'Jan' }],
138
+ });
139
+
140
+ const scrubberAt = (position: number): ScrubberContextValue => ({
141
+ enableScrubbing: true,
142
+ scrubberPosition: position,
143
+ onScrubberPositionChange: () => undefined,
144
+ });
145
+
146
+ it('places tooltip to the right of the scrubber when mid-chart', () => {
147
+ const { getByTestId } = renderTooltip({
148
+ scrubberProps: { tooltip: defaultTooltip },
149
+ scrubberContext: scrubberAt(2),
150
+ });
151
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(260);
152
+ });
153
+
154
+ it('flips tooltip left when right side would overflow (index 3)', () => {
155
+ const { getByTestId } = renderTooltip({
156
+ scrubberProps: {
157
+ tooltip: () => ({ ...defaultTooltip(), minWidth: 120 }),
158
+ },
159
+ scrubberContext: scrubberAt(3),
160
+ });
161
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(220);
162
+ });
163
+
164
+ it('flips at the rightmost scrubber position (index 4)', () => {
165
+ const { getByTestId } = renderTooltip({
166
+ scrubberProps: {
167
+ tooltip: () => ({ ...defaultTooltip(), minWidth: 120 }),
168
+ },
169
+ scrubberContext: scrubberAt(4),
170
+ });
171
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(320);
172
+ });
173
+
174
+ it('custom offset shifts the no-flip tooltip position', () => {
175
+ const { getByTestId } = renderTooltip({
176
+ scrubberProps: {
177
+ tooltip: () => ({ ...defaultTooltip(), offset: 20 }),
178
+ },
179
+ scrubberContext: scrubberAt(2),
180
+ });
181
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(270);
182
+ });
183
+
184
+ it('wider minWidth can trigger a flip that the default width would not', () => {
185
+ const { getByTestId } = renderTooltip({
186
+ scrubberProps: {
187
+ tooltip: () => ({ ...defaultTooltip(), minWidth: 200 }),
188
+ },
189
+ scrubberContext: scrubberAt(2),
190
+ });
191
+ expect(getByTestId('chart-tooltip-rect').props.x).toBe(50);
192
+ });
193
+ });
194
+
195
+ describe('title in tooltip content', () => {
196
+ it('renders a static string title', () => {
197
+ const { getByTestId, getByText } = renderTooltip({
198
+ scrubberProps: {
199
+ tooltip: () => ({
200
+ title: 'My Title',
201
+ items: [{ label: 'T', value: 'V' }],
202
+ }),
203
+ },
204
+ });
205
+ expect(getByTestId('chart-tooltip-title')).toBeTruthy();
206
+ expect(getByText('My Title')).toBeTruthy();
207
+ });
208
+
209
+ it('renders the title returned by a callback', () => {
210
+ const { getByTestId } = renderTooltip({
211
+ scrubberProps: {
212
+ tooltip: () => ({
213
+ title: (i: number) => `Index ${i}`,
214
+ items: [{ label: 'T', value: 'V' }],
215
+ }),
216
+ },
217
+ });
218
+ expect(getByTestId('chart-tooltip-title')).toBeTruthy();
219
+ });
220
+
221
+ it('passes the active data index to the title callback', () => {
222
+ const title = jest.fn().mockReturnValue('Title') as unknown as (
223
+ index: number,
224
+ ) => string;
225
+ renderTooltip({
226
+ scrubberProps: {
227
+ tooltip: () => ({
228
+ title,
229
+ items: [{ label: 'T', value: 'V' }],
230
+ }),
231
+ },
232
+ });
233
+ expect(title).toHaveBeenCalledWith(
234
+ activeScrubberContext.scrubberPosition,
235
+ );
236
+ });
237
+
238
+ it('does not render a title element when title is undefined', () => {
239
+ const { queryByTestId } = renderTooltip({
240
+ scrubberProps: {
241
+ tooltip: () => ({ items: [{ label: 'T', value: 'V' }] }),
242
+ },
243
+ });
244
+ expect(queryByTestId('chart-tooltip-title')).toBeNull();
245
+ });
246
+
247
+ it('does not render a title element when title callback returns undefined', () => {
248
+ const { queryByTestId } = renderTooltip({
249
+ scrubberProps: {
250
+ tooltip: () => ({
251
+ title: () => undefined,
252
+ items: [{ label: 'T', value: 'V' }],
253
+ }),
254
+ },
255
+ });
256
+ expect(queryByTestId('chart-tooltip-title')).toBeNull();
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,105 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import { G, Rect, Text as SvgText } from 'react-native-svg';
3
+
4
+ import type { ScrubberTooltipProps } from '../types';
5
+ import { ChartTooltipItem } from './ChartTooltipItem';
6
+ import {
7
+ BORDER_RADIUS,
8
+ DEFAULT_OFFSET,
9
+ DEFAULT_TOOLTIP_MIN_WIDTH,
10
+ PADDING_X,
11
+ PADDING_Y,
12
+ ROW_GAP,
13
+ ROW_HEIGHT,
14
+ } from './constants';
15
+ import {
16
+ computeItemsBaseY,
17
+ computeTooltipHeight,
18
+ computeTooltipWidth,
19
+ computeTooltipX,
20
+ useBuildRefSetters,
21
+ useTooltipMeasurement,
22
+ } from './utils';
23
+
24
+ /**
25
+ * Default structured tooltip anchored to the scrubber line.
26
+ *
27
+ * The tooltip auto-fits its width to the rendered content via `getBBox`,
28
+ * with `minWidth` acting as an optional minimum width override. Use with
29
+ * {@link ScrubberProps.tooltip}; layout options (`offset`, `minWidth`) belong
30
+ * on the object returned from the `tooltip` callback. Return `{ items: [] }`
31
+ * from the callback to hide the tooltip at a given index.
32
+ */
33
+ export function DefaultScrubberTooltip({
34
+ pixelX,
35
+ drawingArea,
36
+ title,
37
+ items,
38
+ offset = DEFAULT_OFFSET,
39
+ minWidth = DEFAULT_TOOLTIP_MIN_WIDTH,
40
+ }: Readonly<ScrubberTooltipProps>) {
41
+ const { theme } = useTheme();
42
+
43
+ const hasTitle = title !== undefined;
44
+
45
+ const { widths, titleRef, labelRefs, valueRefs } = useTooltipMeasurement(
46
+ items,
47
+ hasTitle,
48
+ title,
49
+ );
50
+
51
+ const labelRefSetters = useBuildRefSetters(labelRefs, items.length);
52
+ const valueRefSetters = useBuildRefSetters(valueRefs, items.length);
53
+
54
+ if (items.length === 0) {
55
+ return null;
56
+ }
57
+
58
+ const tooltipWidth = computeTooltipWidth(widths, hasTitle, minWidth);
59
+ const tooltipX = computeTooltipX(pixelX, offset, tooltipWidth, drawingArea);
60
+ const tooltipHeight = computeTooltipHeight(items.length, hasTitle);
61
+ const itemsBaseY = computeItemsBaseY(drawingArea.y, hasTitle);
62
+
63
+ return (
64
+ <G testID='chart-tooltip' opacity={widths === null ? 0 : 1}>
65
+ <Rect
66
+ testID='chart-tooltip-rect'
67
+ x={tooltipX}
68
+ y={drawingArea.y}
69
+ width={tooltipWidth}
70
+ height={tooltipHeight}
71
+ rx={BORDER_RADIUS}
72
+ fill={theme.colors.bg.muted}
73
+ />
74
+ {hasTitle && (
75
+ <SvgText
76
+ ref={(el) => {
77
+ titleRef.current = el as unknown as (typeof titleRef)['current'];
78
+ }}
79
+ testID='chart-tooltip-title'
80
+ x={tooltipX + PADDING_X}
81
+ y={drawingArea.y + PADDING_Y + ROW_HEIGHT / 2}
82
+ alignmentBaseline='central'
83
+ fill={theme.colors.text.base}
84
+ fontSize={theme.typographies.body4.fontSize}
85
+ fontWeight={theme.typographies.body4.fontWeight}
86
+ fontFamily={theme.fontFamilies.sans}
87
+ >
88
+ {String(title)}
89
+ </SvgText>
90
+ )}
91
+ {items.map((item, i) => (
92
+ <ChartTooltipItem
93
+ key={i}
94
+ label={item.label}
95
+ value={item.value}
96
+ x={tooltipX}
97
+ y={itemsBaseY + i * (ROW_HEIGHT + ROW_GAP) + ROW_HEIGHT / 2}
98
+ width={tooltipWidth}
99
+ labelRef={labelRefSetters[i]}
100
+ valueRef={valueRefSetters[i]}
101
+ />
102
+ ))}
103
+ </G>
104
+ );
105
+ }
@@ -0,0 +1,9 @@
1
+ export const DEFAULT_OFFSET = 10;
2
+ export const DEFAULT_TOOLTIP_MIN_WIDTH = 80;
3
+ export const PADDING_X = 8;
4
+ export const PADDING_Y = 8;
5
+ export const ROW_HEIGHT = 16;
6
+ export const ROW_GAP = 6;
7
+ export const TITLE_GAP = 6;
8
+ export const BORDER_RADIUS = 4;
9
+ export const LABEL_VALUE_GAP = 12;