@ledgerhq/lumen-ui-rnative-visualization 0.1.14 → 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 (152) 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/DefaultScrubberTooltip.test.js +4 -4
  41. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +31 -10
  42. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -1
  43. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js +4 -4
  44. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js.map +1 -1
  45. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +35 -7
  46. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  47. package/dist/module/lib/Components/Scrubber/utils.js +64 -0
  48. package/dist/module/lib/Components/Scrubber/utils.js.map +1 -1
  49. package/dist/module/lib/Components/Scrubber/utils.test.js +128 -1
  50. package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -1
  51. package/dist/module/lib/utils/domain/domain.js.map +1 -1
  52. package/dist/module/lib/utils/scales/scales.js +13 -4
  53. package/dist/module/lib/utils/scales/scales.js.map +1 -1
  54. package/dist/module/lib/utils/scales/scales.test.js +29 -0
  55. package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
  56. package/dist/module/lib/utils/ticks/ticks.js +13 -3
  57. package/dist/module/lib/utils/ticks/ticks.js.map +1 -1
  58. package/dist/module/lib/utils/ticks/ticks.test.js +34 -0
  59. package/dist/module/lib/utils/ticks/ticks.test.js.map +1 -1
  60. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts +5 -0
  61. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts.map +1 -0
  62. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts +39 -0
  63. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts.map +1 -1
  64. package/dist/typescript/src/lib/Components/Axis/index.d.ts +3 -0
  65. package/dist/typescript/src/lib/Components/Axis/index.d.ts.map +1 -0
  66. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  67. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  68. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts +5 -4
  69. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts.map +1 -1
  70. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +11 -3
  71. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  72. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts +18 -8
  73. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts.map +1 -1
  74. package/dist/typescript/src/lib/Components/Line/utils.d.ts +4 -0
  75. package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -1
  76. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
  77. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  78. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +10 -3
  79. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
  80. package/dist/typescript/src/lib/Components/Point/Point.d.ts +1 -1
  81. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  82. package/dist/typescript/src/lib/Components/Point/constants.d.ts +7 -0
  83. package/dist/typescript/src/lib/Components/Point/constants.d.ts.map +1 -0
  84. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts +7 -0
  85. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -0
  86. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +3 -0
  87. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -0
  88. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +13 -0
  89. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -0
  90. package/dist/typescript/src/lib/Components/Point/types.d.ts +7 -0
  91. package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -1
  92. package/dist/typescript/src/lib/Components/Point/utils.d.ts +8 -6
  93. package/dist/typescript/src/lib/Components/Point/utils.d.ts.map +1 -1
  94. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +3 -2
  95. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -1
  96. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts +5 -1
  97. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -1
  98. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +1 -1
  99. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  100. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +13 -0
  101. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -1
  102. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +33 -3
  103. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -1
  104. package/dist/typescript/src/lib/utils/domain/domain.d.ts +5 -4
  105. package/dist/typescript/src/lib/utils/domain/domain.d.ts.map +1 -1
  106. package/dist/typescript/src/lib/utils/index.d.ts +1 -1
  107. package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
  108. package/dist/typescript/src/lib/utils/scales/scales.d.ts +11 -5
  109. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
  110. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +13 -5
  111. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -1
  112. package/dist/typescript/src/lib/utils/types.d.ts +3 -28
  113. package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
  114. package/package.json +4 -4
  115. package/src/lib/Components/Axis/Axis.constants.ts +23 -0
  116. package/src/lib/Components/Axis/Axis.types.ts +40 -0
  117. package/src/lib/Components/Axis/index.ts +2 -0
  118. package/src/lib/Components/CartesianChart/CartesianChart.tsx +39 -23
  119. package/src/lib/Components/CartesianChart/context/useBuildChartContext.test.ts +56 -0
  120. package/src/lib/Components/CartesianChart/context/useBuildChartContext.ts +8 -8
  121. package/src/lib/Components/CartesianChart/types.ts +11 -3
  122. package/src/lib/Components/CartesianChart/utils.ts +18 -8
  123. package/src/lib/Components/Line/utils.test.ts +90 -0
  124. package/src/lib/Components/Line/utils.ts +6 -1
  125. package/src/lib/Components/LineChart/LineChart.stories.tsx +36 -0
  126. package/src/lib/Components/LineChart/LineChart.test.tsx +156 -0
  127. package/src/lib/Components/LineChart/LineChart.tsx +30 -40
  128. package/src/lib/Components/LineChart/types.ts +10 -3
  129. package/src/lib/Components/Point/Point.tsx +9 -3
  130. package/src/lib/Components/Point/constants.ts +6 -0
  131. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +162 -0
  132. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +45 -0
  133. package/src/lib/Components/Point/pointContext/index.ts +5 -0
  134. package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +19 -0
  135. package/src/lib/Components/Point/types.ts +7 -0
  136. package/src/lib/Components/Point/utils.test.ts +95 -5
  137. package/src/lib/Components/Point/utils.ts +35 -7
  138. package/src/lib/Components/ReferenceLine/utils.ts +4 -7
  139. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.tsx +4 -4
  140. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.ts +4 -4
  141. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +29 -15
  142. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +41 -5
  143. package/src/lib/Components/Scrubber/types.ts +13 -0
  144. package/src/lib/Components/Scrubber/utils.test.ts +150 -0
  145. package/src/lib/Components/Scrubber/utils.ts +93 -3
  146. package/src/lib/utils/domain/domain.ts +6 -5
  147. package/src/lib/utils/index.ts +0 -2
  148. package/src/lib/utils/scales/scales.test.ts +19 -0
  149. package/src/lib/utils/scales/scales.ts +15 -9
  150. package/src/lib/utils/ticks/ticks.test.ts +45 -0
  151. package/src/lib/utils/ticks/ticks.ts +19 -9
  152. package/src/lib/utils/types.ts +3 -26
@@ -2,6 +2,7 @@ import { describe, it, expect } from '@jest/globals';
2
2
  import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
3
  import { ThemeProvider } from '@ledgerhq/lumen-ui-rnative';
4
4
  import { render } from '@testing-library/react-native';
5
+ import { Circle } from 'react-native-svg';
5
6
 
6
7
  import { LineChart } from './LineChart';
7
8
 
@@ -99,4 +100,159 @@ describe('LineChart', () => {
99
100
  );
100
101
  getByTestId('chart-svg');
101
102
  });
103
+
104
+ it('renders a Path for each series when multiple series are provided', () => {
105
+ const multipleSeries = [
106
+ { id: 'a', stroke: '#000', data: [10, 20, 30, 40, 50] },
107
+ { id: 'b', stroke: '#f00', data: [5, 15, 25, 35, 45] },
108
+ ];
109
+ const { getAllByTestId } = render(
110
+ <LineChartWrapper>
111
+ <LineChart series={multipleSeries} width={400} height={200} />
112
+ </LineChartWrapper>,
113
+ );
114
+ expect(getAllByTestId('line-path')).toHaveLength(2);
115
+ });
116
+
117
+ it('renders an area fill for each series when showArea is true', () => {
118
+ const multipleSeries = [
119
+ { id: 'a', stroke: '#000', data: [10, 20, 30, 40, 50] },
120
+ { id: 'b', stroke: '#f00', data: [5, 15, 25, 35, 45] },
121
+ ];
122
+ const { getAllByTestId } = render(
123
+ <LineChartWrapper>
124
+ <LineChart series={multipleSeries} width={400} height={200} showArea />
125
+ </LineChartWrapper>,
126
+ );
127
+ expect(getAllByTestId('line-area')).toHaveLength(2);
128
+ });
129
+
130
+ it('renders custom children inside the chart', () => {
131
+ const { getByTestId } = render(
132
+ <LineChartWrapper>
133
+ <LineChart series={sampleSeries} width={400} height={200}>
134
+ <Circle testID='custom-child' cx={10} cy={10} r={5} />
135
+ </LineChart>
136
+ </LineChartWrapper>,
137
+ );
138
+ getByTestId('custom-child');
139
+ });
140
+
141
+ it('renders x-axis tick labels when showXAxis is true', () => {
142
+ const { getByText } = render(
143
+ <LineChartWrapper>
144
+ <LineChart
145
+ series={sampleSeries}
146
+ width={400}
147
+ height={200}
148
+ showXAxis
149
+ xAxis={{ ticks: [0, 2, 4] }}
150
+ />
151
+ </LineChartWrapper>,
152
+ );
153
+ getByText('0');
154
+ getByText('2');
155
+ getByText('4');
156
+ });
157
+
158
+ it('renders x-axis tick labels when positioned at the top', () => {
159
+ const { getByText } = render(
160
+ <LineChartWrapper>
161
+ <LineChart
162
+ series={sampleSeries}
163
+ width={400}
164
+ height={200}
165
+ showXAxis
166
+ xAxis={{ position: 'top', ticks: [0, 4] }}
167
+ />
168
+ </LineChartWrapper>,
169
+ );
170
+ getByText('0');
171
+ getByText('4');
172
+ });
173
+
174
+ it('renders y-axis tick labels when showYAxis is true', () => {
175
+ const { getByText } = render(
176
+ <LineChartWrapper>
177
+ <LineChart
178
+ series={sampleSeries}
179
+ width={400}
180
+ height={200}
181
+ showYAxis
182
+ yAxis={{ ticks: [10, 50] }}
183
+ />
184
+ </LineChartWrapper>,
185
+ );
186
+ getByText('10');
187
+ getByText('50');
188
+ });
189
+
190
+ it('renders y-axis tick labels when positioned at the end', () => {
191
+ const { getByText } = render(
192
+ <LineChartWrapper>
193
+ <LineChart
194
+ series={sampleSeries}
195
+ width={400}
196
+ height={200}
197
+ showYAxis
198
+ yAxis={{ position: 'end', ticks: [10, 50] }}
199
+ />
200
+ </LineChartWrapper>,
201
+ );
202
+ getByText('10');
203
+ getByText('50');
204
+ });
205
+
206
+ it('does not render axis tick labels by default', () => {
207
+ const { queryByText } = render(
208
+ <LineChartWrapper>
209
+ <LineChart
210
+ series={sampleSeries}
211
+ width={400}
212
+ height={200}
213
+ xAxis={{ ticks: [0, 2, 4] }}
214
+ />
215
+ </LineChartWrapper>,
216
+ );
217
+ expect(queryByText('0')).toBeNull();
218
+ expect(queryByText('2')).toBeNull();
219
+ expect(queryByText('4')).toBeNull();
220
+ });
221
+
222
+ it('renders with scrubbing enabled', () => {
223
+ const { getByTestId } = render(
224
+ <LineChartWrapper>
225
+ <LineChart
226
+ series={sampleSeries}
227
+ width={400}
228
+ height={200}
229
+ enableScrubbing
230
+ />
231
+ </LineChartWrapper>,
232
+ );
233
+ getByTestId('chart-svg');
234
+ });
235
+
236
+ it('renders with a custom inset', () => {
237
+ const { getByTestId } = render(
238
+ <LineChartWrapper>
239
+ <LineChart
240
+ series={sampleSeries}
241
+ width={400}
242
+ height={200}
243
+ inset={{ top: 10, bottom: 10, left: 20, right: 20 }}
244
+ />
245
+ </LineChartWrapper>,
246
+ );
247
+ expect(getByTestId('line-path')).toBeTruthy();
248
+ });
249
+
250
+ it('uses the default height when none is provided', () => {
251
+ const { getByTestId } = render(
252
+ <LineChartWrapper>
253
+ <LineChart series={sampleSeries} width={400} />
254
+ </LineChartWrapper>,
255
+ );
256
+ getByTestId('chart-svg');
257
+ });
102
258
  });
@@ -1,8 +1,9 @@
1
1
  import { useMemo } from 'react';
2
2
 
3
- import type { AxisConfigProps, ChartInset } from '../../utils/types';
3
+ import type { ChartInset } from '../../utils/types';
4
+ import { defaultXAxisProps, defaultYAxisProps } from '../Axis';
4
5
  import { DEFAULT_AXIS_HEIGHT, XAxis } from '../Axis/XAxis';
5
- import { DEFAULT_AXIS_WIDTH, YAxis } from '../Axis/YAxis';
6
+ import { YAxis } from '../Axis/YAxis';
6
7
  import { CartesianChart } from '../CartesianChart';
7
8
  import { Line } from '../Line';
8
9
 
@@ -23,52 +24,40 @@ export const LineChart = ({
23
24
  enableScrubbing = false,
24
25
  onScrubberPositionChange,
25
26
  animate,
27
+ magnetRadius,
26
28
  }: LineChartProps) => {
27
- const {
28
- scaleType: xScaleType,
29
- data: xData,
30
- domain: xDomain,
31
- ...xAxisVisualProps
32
- } = xAxis ?? {};
33
-
34
- const {
35
- scaleType: yScaleType,
36
- data: yData,
37
- domain: yDomain,
38
- ...yAxisVisualProps
39
- } = yAxis ?? {};
40
-
41
- const xAxisConfig: Partial<AxisConfigProps> = {
42
- scaleType: xScaleType,
43
- data: xData,
44
- domain: xDomain,
29
+ const xAxisConfig = {
30
+ ...defaultXAxisProps,
31
+ ...xAxis,
45
32
  };
46
-
47
- const yAxisConfig: Partial<AxisConfigProps> = {
48
- scaleType: yScaleType,
49
- data: yData,
50
- domain: yDomain,
33
+ const yAxisConfig = {
34
+ ...defaultYAxisProps,
35
+ ...yAxis,
51
36
  };
52
37
 
53
38
  const axisPadding: Partial<ChartInset> | undefined = useMemo(() => {
54
- if (!showXAxis && !showYAxis) return undefined;
55
- const xAxisPosition =
56
- xAxisVisualProps.position === 'top' ? 'top' : 'bottom';
57
- const yAxisPosition =
58
- yAxisVisualProps.position === 'end' ? 'right' : 'left';
59
- const yAxisWidth = yAxisVisualProps.width ?? DEFAULT_AXIS_WIDTH;
39
+ if (!showXAxis && !showYAxis) {
40
+ return undefined;
41
+ }
42
+
60
43
  return {
61
- top: showXAxis && xAxisPosition === 'top' ? DEFAULT_AXIS_HEIGHT : 0,
62
- bottom: showXAxis && xAxisPosition === 'bottom' ? DEFAULT_AXIS_HEIGHT : 0,
63
- left: showYAxis && yAxisPosition === 'left' ? yAxisWidth : 0,
64
- right: showYAxis && yAxisPosition === 'right' ? yAxisWidth : 0,
44
+ top:
45
+ showXAxis && xAxisConfig.position === 'top' ? DEFAULT_AXIS_HEIGHT : 0,
46
+ bottom:
47
+ showXAxis && xAxisConfig.position === 'bottom'
48
+ ? DEFAULT_AXIS_HEIGHT
49
+ : 0,
50
+ left:
51
+ showYAxis && yAxisConfig.position === 'start' ? yAxisConfig.width : 0,
52
+ right:
53
+ showYAxis && yAxisConfig.position === 'end' ? yAxisConfig.width : 0,
65
54
  };
66
55
  }, [
67
56
  showXAxis,
68
57
  showYAxis,
69
- xAxisVisualProps.position,
70
- yAxisVisualProps.position,
71
- yAxisVisualProps.width,
58
+ xAxisConfig?.position,
59
+ yAxisConfig?.position,
60
+ yAxisConfig?.width,
72
61
  ]);
73
62
 
74
63
  return (
@@ -83,9 +72,10 @@ export const LineChart = ({
83
72
  enableScrubbing={enableScrubbing}
84
73
  onScrubberPositionChange={onScrubberPositionChange}
85
74
  animate={animate}
75
+ magnetRadius={magnetRadius}
86
76
  >
87
- {showXAxis && <XAxis {...xAxisVisualProps} />}
88
- {showYAxis && <YAxis {...yAxisVisualProps} />}
77
+ {showXAxis && <XAxis {...xAxisConfig} />}
78
+ {showYAxis && <YAxis {...yAxisConfig} />}
89
79
  {series?.map((s) => (
90
80
  <Line
91
81
  key={s.id}
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
 
3
- import type { AxisConfigProps, ChartInset, Series } from '../../utils/types';
3
+ import type { ChartInset, Series } from '../../utils/types';
4
4
  import type { XAxisProps } from '../Axis/XAxis';
5
5
  import type { YAxisProps } from '../Axis/YAxis';
6
6
 
@@ -35,12 +35,12 @@ export type LineChartProps = {
35
35
  * Combined axis configuration and visual props for the x-axis.
36
36
  * Includes scale/domain settings as well as visual options like `showGrid` and `showLine`.
37
37
  */
38
- xAxis?: Partial<AxisConfigProps> & XAxisProps;
38
+ xAxis?: XAxisProps;
39
39
  /**
40
40
  * Combined axis configuration and visual props for the y-axis.
41
41
  * Includes scale/domain settings as well as visual options like `showGrid` and `showLine`.
42
42
  */
43
- yAxis?: Partial<AxisConfigProps> & YAxisProps;
43
+ yAxis?: YAxisProps;
44
44
  /**
45
45
  * Width of the chart in pixels.
46
46
  * When omitted, the component auto-measures via `onLayout`.
@@ -76,4 +76,11 @@ export type LineChartProps = {
76
76
  * @default true
77
77
  */
78
78
  animate?: boolean;
79
+ /**
80
+ * Pixel radius within which the scrubber magnetically snaps to registered
81
+ * magnetic `<Point>` components. Requires `enableScrubbing` to be `true`.
82
+ * Set to `0` to disable magnetisation.
83
+ * @default 6
84
+ */
85
+ magnetRadius?: number;
79
86
  };
@@ -5,15 +5,16 @@ import { Circle, G, Polygon, Text as SvgText } from 'react-native-svg';
5
5
  import { projectPoint } from '../../utils/scales/scales';
6
6
  import { useCartesianChartContext } from '../CartesianChart/context';
7
7
  import { useRevealClip } from '../CartesianChart/RevealClip';
8
+ import { DEFAULT_SIZE, STROKE_WIDTH } from './constants';
9
+ import { useMagneticPointsContext } from './pointContext';
8
10
 
9
11
  import type { PointLabelProps, PointProps } from './types';
10
12
  import {
11
13
  buildArrowPoints,
12
14
  computeLabelY,
13
- DEFAULT_SIZE,
14
15
  isWithinBounds,
15
16
  resolveLabel,
16
- STROKE_WIDTH,
17
+ useMagneticRegistration,
17
18
  } from './utils';
18
19
 
19
20
  export function PointLabel({
@@ -45,9 +46,14 @@ export function Point({
45
46
  showLabelArrow = true,
46
47
  size = DEFAULT_SIZE,
47
48
  onPress,
49
+ magnetic = false,
48
50
  }: Readonly<PointProps>) {
49
- const { getXScale, getYScale, drawingArea } = useCartesianChartContext();
51
+ const { getXScale, getYScale, getXAxisConfig, drawingArea } =
52
+ useCartesianChartContext();
50
53
  const clipPath = useRevealClip();
54
+ const magneticContext = useMagneticPointsContext();
55
+
56
+ useMagneticRegistration(magnetic, dataX, getXAxisConfig, magneticContext);
51
57
  const { theme } = useTheme();
52
58
 
53
59
  const xScale = getXScale();
@@ -0,0 +1,6 @@
1
+ export const DEFAULT_SIZE = 10;
2
+ export const STROKE_WIDTH = 2;
3
+ export const LABEL_FONT_SIZE = 10;
4
+ export const ARROW_WIDTH = 6;
5
+ export const ARROW_HEIGHT = 4;
6
+ export const GAP = 4;
@@ -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
  };