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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/module/lib/Components/Axis/Axis.constants.js +21 -0
  2. package/dist/module/lib/Components/Axis/Axis.constants.js.map +1 -0
  3. package/dist/module/lib/Components/Axis/index.js +5 -0
  4. package/dist/module/lib/Components/Axis/index.js.map +1 -0
  5. package/dist/module/lib/Components/CartesianChart/CartesianChart.js +34 -22
  6. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  7. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js +5 -4
  8. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js.map +1 -1
  9. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js +54 -0
  10. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js.map +1 -1
  11. package/dist/module/lib/Components/CartesianChart/utils.js +22 -12
  12. package/dist/module/lib/Components/CartesianChart/utils.js.map +1 -1
  13. package/dist/module/lib/Components/Line/utils.js +6 -1
  14. package/dist/module/lib/Components/Line/utils.js.map +1 -1
  15. package/dist/module/lib/Components/Line/utils.test.js +88 -0
  16. package/dist/module/lib/Components/Line/utils.test.js.map +1 -0
  17. package/dist/module/lib/Components/LineChart/LineChart.js +19 -31
  18. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  19. package/dist/module/lib/Components/LineChart/LineChart.stories.js +38 -0
  20. package/dist/module/lib/Components/LineChart/LineChart.stories.js.map +1 -1
  21. package/dist/module/lib/Components/LineChart/LineChart.test.js +192 -0
  22. package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
  23. package/dist/module/lib/Components/Point/Point.js +8 -2
  24. package/dist/module/lib/Components/Point/Point.js.map +1 -1
  25. package/dist/module/lib/Components/Point/constants.js +9 -0
  26. package/dist/module/lib/Components/Point/constants.js.map +1 -0
  27. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js +35 -0
  28. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js.map +1 -0
  29. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js +153 -0
  30. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js.map +1 -0
  31. package/dist/module/lib/Components/Point/pointContext/index.js +5 -0
  32. package/dist/module/lib/Components/Point/pointContext/index.js.map +1 -0
  33. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js +10 -0
  34. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js.map +1 -0
  35. package/dist/module/lib/Components/Point/utils.js +26 -6
  36. package/dist/module/lib/Components/Point/utils.js.map +1 -1
  37. package/dist/module/lib/Components/Point/utils.test.js +84 -2
  38. package/dist/module/lib/Components/Point/utils.test.js.map +1 -1
  39. package/dist/module/lib/Components/ReferenceLine/utils.js.map +1 -1
  40. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js +3 -3
  41. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.js.map +1 -1
  42. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js +5 -5
  43. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.js.map +1 -1
  44. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js +5 -5
  45. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js +1 -2
  46. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/constants.js.map +1 -1
  47. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +31 -10
  48. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -1
  49. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js +4 -4
  50. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js.map +1 -1
  51. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +35 -7
  52. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  53. package/dist/module/lib/Components/Scrubber/utils.js +64 -0
  54. package/dist/module/lib/Components/Scrubber/utils.js.map +1 -1
  55. package/dist/module/lib/Components/Scrubber/utils.test.js +128 -1
  56. package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -1
  57. package/dist/module/lib/utils/domain/domain.js.map +1 -1
  58. package/dist/module/lib/utils/scales/scales.js +13 -4
  59. package/dist/module/lib/utils/scales/scales.js.map +1 -1
  60. package/dist/module/lib/utils/scales/scales.test.js +29 -0
  61. package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
  62. package/dist/module/lib/utils/ticks/ticks.js +13 -3
  63. package/dist/module/lib/utils/ticks/ticks.js.map +1 -1
  64. package/dist/module/lib/utils/ticks/ticks.test.js +34 -0
  65. package/dist/module/lib/utils/ticks/ticks.test.js.map +1 -1
  66. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts +5 -0
  67. package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts.map +1 -0
  68. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts +39 -0
  69. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts.map +1 -1
  70. package/dist/typescript/src/lib/Components/Axis/index.d.ts +3 -0
  71. package/dist/typescript/src/lib/Components/Axis/index.d.ts.map +1 -0
  72. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  73. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  74. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts +5 -4
  75. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts.map +1 -1
  76. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +11 -3
  77. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  78. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts +18 -8
  79. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts.map +1 -1
  80. package/dist/typescript/src/lib/Components/Line/utils.d.ts +4 -0
  81. package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -1
  82. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
  83. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  84. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +10 -3
  85. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
  86. package/dist/typescript/src/lib/Components/Point/Point.d.ts +1 -1
  87. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  88. package/dist/typescript/src/lib/Components/Point/constants.d.ts +7 -0
  89. package/dist/typescript/src/lib/Components/Point/constants.d.ts.map +1 -0
  90. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts +7 -0
  91. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -0
  92. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +3 -0
  93. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -0
  94. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +13 -0
  95. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -0
  96. package/dist/typescript/src/lib/Components/Point/types.d.ts +7 -0
  97. package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -1
  98. package/dist/typescript/src/lib/Components/Point/utils.d.ts +8 -6
  99. package/dist/typescript/src/lib/Components/Point/utils.d.ts.map +1 -1
  100. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +3 -2
  101. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -1
  102. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.d.ts.map +1 -1
  103. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts +1 -2
  104. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.d.ts.map +1 -1
  105. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts +5 -1
  106. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -1
  107. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +1 -1
  108. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  109. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +13 -0
  110. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -1
  111. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +33 -3
  112. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -1
  113. package/dist/typescript/src/lib/utils/domain/domain.d.ts +5 -4
  114. package/dist/typescript/src/lib/utils/domain/domain.d.ts.map +1 -1
  115. package/dist/typescript/src/lib/utils/index.d.ts +1 -1
  116. package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
  117. package/dist/typescript/src/lib/utils/scales/scales.d.ts +11 -5
  118. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
  119. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +13 -5
  120. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -1
  121. package/dist/typescript/src/lib/utils/types.d.ts +3 -28
  122. package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
  123. package/package.json +4 -4
  124. package/src/lib/Components/Axis/Axis.constants.ts +23 -0
  125. package/src/lib/Components/Axis/Axis.types.ts +40 -0
  126. package/src/lib/Components/Axis/index.ts +2 -0
  127. package/src/lib/Components/CartesianChart/CartesianChart.tsx +39 -23
  128. package/src/lib/Components/CartesianChart/context/useBuildChartContext.test.ts +56 -0
  129. package/src/lib/Components/CartesianChart/context/useBuildChartContext.ts +8 -8
  130. package/src/lib/Components/CartesianChart/types.ts +11 -3
  131. package/src/lib/Components/CartesianChart/utils.ts +22 -12
  132. package/src/lib/Components/Line/utils.test.ts +90 -0
  133. package/src/lib/Components/Line/utils.ts +6 -1
  134. package/src/lib/Components/LineChart/LineChart.stories.tsx +36 -0
  135. package/src/lib/Components/LineChart/LineChart.test.tsx +156 -0
  136. package/src/lib/Components/LineChart/LineChart.tsx +30 -40
  137. package/src/lib/Components/LineChart/types.ts +10 -3
  138. package/src/lib/Components/Point/Point.tsx +9 -3
  139. package/src/lib/Components/Point/constants.ts +6 -0
  140. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +162 -0
  141. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +45 -0
  142. package/src/lib/Components/Point/pointContext/index.ts +5 -0
  143. package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +19 -0
  144. package/src/lib/Components/Point/types.ts +7 -0
  145. package/src/lib/Components/Point/utils.test.ts +95 -5
  146. package/src/lib/Components/Point/utils.ts +35 -7
  147. package/src/lib/Components/ReferenceLine/utils.ts +4 -7
  148. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/ChartTooltipItem.tsx +3 -3
  149. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.tsx +5 -5
  150. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.tsx +4 -5
  151. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/constants.ts +1 -2
  152. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.ts +4 -4
  153. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +29 -15
  154. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +41 -5
  155. package/src/lib/Components/Scrubber/types.ts +13 -0
  156. package/src/lib/Components/Scrubber/utils.test.ts +150 -0
  157. package/src/lib/Components/Scrubber/utils.ts +93 -3
  158. package/src/lib/utils/domain/domain.ts +6 -5
  159. package/src/lib/utils/index.ts +0 -2
  160. package/src/lib/utils/scales/scales.test.ts +19 -0
  161. package/src/lib/utils/scales/scales.ts +15 -9
  162. package/src/lib/utils/ticks/ticks.test.ts +45 -0
  163. package/src/lib/utils/ticks/ticks.ts +19 -9
  164. package/src/lib/utils/types.ts +3 -26
@@ -1,6 +1,7 @@
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
+ import type { BaseAxisProps } from '../Axis';
4
5
 
5
6
  export type CartesianChartProps = {
6
7
  /**
@@ -10,11 +11,11 @@ export type CartesianChartProps = {
10
11
  /**
11
12
  * Scale and domain configuration for the x-axis.
12
13
  */
13
- xAxis?: Partial<AxisConfigProps>;
14
+ xAxis?: Partial<BaseAxisProps>;
14
15
  /**
15
16
  * Scale and domain configuration for the y-axis.
16
17
  */
17
- yAxis?: Partial<AxisConfigProps>;
18
+ yAxis?: Partial<BaseAxisProps>;
18
19
  /**
19
20
  * Width of the chart in pixels.
20
21
  * When omitted, the component auto-measures via `onLayout`.
@@ -63,4 +64,11 @@ export type CartesianChartProps = {
63
64
  * @default true
64
65
  */
65
66
  animate?: boolean;
67
+ /**
68
+ * Pixel radius within which the scrubber magnetically snaps to registered
69
+ * magnetic `<Point>` components. Requires `enableScrubbing` to be `true`.
70
+ * Set to `0` to disable magnetisation.
71
+ * @default 6
72
+ */
73
+ magnetRadius?: number;
66
74
  };
@@ -4,21 +4,31 @@ import type { CartesianChartProps } from './types';
4
4
  export const DEFAULT_HEIGHT = 160;
5
5
 
6
6
  /**
7
- * Internal buffer that prevents SVG content (labels, points, ticks) from being
8
- * clipped at the edge of the container. Compensated by negative margins on the
9
- * wrapper so the chart's visual footprint stays unchanged.
7
+ * Internal buffer added around the drawing area so SVG content (labels, points,
8
+ * ticks) drawn at the edges is not clipped. The SVG canvas is enlarged by this
9
+ * buffer on every side and shifted back by {@link OVERFLOW_OFFSET} so
10
+ * the chart's layout footprint (and the drawing area) stays exactly the
11
+ * consumer-provided width/height.
10
12
  */
11
13
  export const OVERFLOW_BUFFER: ChartInset = {
12
- top: 50,
13
- right: 50,
14
- bottom: 50,
15
- left: 50,
14
+ top: 25,
15
+ right: 25,
16
+ bottom: 25,
17
+ left: 25,
16
18
  };
17
- export const OVERFLOW_NEGATIVE_MARGIN = {
18
- marginTop: -OVERFLOW_BUFFER.top,
19
- marginRight: -OVERFLOW_BUFFER.right,
20
- marginBottom: -OVERFLOW_BUFFER.bottom,
21
- marginLeft: -OVERFLOW_BUFFER.left,
19
+ /**
20
+ * Top/left offset applied to the (enlarged) SVG group so its drawing area aligns
21
+ * with the container's top-left. The extra width/height added to the SVG
22
+ * overflows symmetrically on every side via `overflow: visible`.
23
+ *
24
+ * Uses `position: relative` to keep parity with the web implementation, where a
25
+ * negative `margin-top` would collapse through the container instead of
26
+ * offsetting the SVG inside it.
27
+ */
28
+ export const OVERFLOW_OFFSET = {
29
+ position: 'relative' as const,
30
+ top: -OVERFLOW_BUFFER.top,
31
+ left: -OVERFLOW_BUFFER.left,
22
32
  };
23
33
  export const ZERO_PADDING: ChartInset = {
24
34
  top: 0,
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+
3
+ import {
4
+ getCategoricalScale,
5
+ getNumericScale,
6
+ } from '../../utils/scales/scales';
7
+
8
+ import { toScaledPoints } from './utils';
9
+
10
+ const xScale = getNumericScale({
11
+ scaleType: 'linear',
12
+ domain: { min: 0, max: 7 },
13
+ range: { min: 0, max: 800 },
14
+ nice: false,
15
+ });
16
+
17
+ const yScale = getNumericScale({
18
+ scaleType: 'linear',
19
+ domain: { min: 0, max: 100 },
20
+ range: { min: 200, max: 0 },
21
+ });
22
+
23
+ describe('toScaledPoints', () => {
24
+ it('uses index as x input when no xData is provided', () => {
25
+ const data = [10, 50, 90];
26
+ const points = toScaledPoints(data, xScale, yScale);
27
+
28
+ expect(points).not.toBeNull();
29
+ expect(points).toHaveLength(3);
30
+ expect(points![0][0]).toBe(0);
31
+ expect(points![1][0]).toBeCloseTo(800 / 7);
32
+ expect(points![2][0]).toBeCloseTo((800 / 7) * 2);
33
+ });
34
+
35
+ it('caps iteration to xData.length when series is longer', () => {
36
+ const data = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
37
+ const xData = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'];
38
+
39
+ const points = toScaledPoints(data, xScale, yScale, xData);
40
+
41
+ expect(points).not.toBeNull();
42
+ expect(points).toHaveLength(xData.length);
43
+ expect(points![0][0]).toBe(0);
44
+ expect(points![points!.length - 1][0]).toBe(800);
45
+ });
46
+
47
+ it('skips null values without affecting the cap', () => {
48
+ const data: (number | null)[] = [10, null, 30, 40, 50, 60, 70, 80, 90];
49
+ const xData = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
50
+
51
+ const points = toScaledPoints(data, xScale, yScale, xData);
52
+
53
+ expect(points).not.toBeNull();
54
+ expect(points).toHaveLength(7);
55
+ expect(points![points!.length - 1][0]).toBe((800 / 7) * 7);
56
+ });
57
+
58
+ it('uses numeric xData values as the x input', () => {
59
+ const data = [10, 20, 30];
60
+ const customX = [0, 3.5, 7];
61
+
62
+ const points = toScaledPoints(data, xScale, yScale, customX);
63
+
64
+ expect(points![0][0]).toBe(0);
65
+ expect(points![1][0]).toBeCloseTo(400);
66
+ expect(points![2][0]).toBe(800);
67
+ });
68
+
69
+ it('centers points within categorical band scales', () => {
70
+ const bandScale = getCategoricalScale({
71
+ domain: { min: 0, max: 2 },
72
+ range: { min: 0, max: 300 },
73
+ padding: 0,
74
+ });
75
+ const data = [10, 50, 90];
76
+
77
+ const points = toScaledPoints(data, bandScale, yScale);
78
+
79
+ expect(points).toHaveLength(3);
80
+ const bandwidth = bandScale.bandwidth();
81
+ expect(points![0][0]).toBe((bandScale(0) ?? 0) + bandwidth / 2);
82
+ });
83
+
84
+ it('returns null when fewer than 2 valid points remain', () => {
85
+ const data: (number | null)[] = [10, null, null];
86
+ const xData = ['A', 'B', 'C'];
87
+
88
+ expect(toScaledPoints(data, xScale, yScale, xData)).toBeNull();
89
+ });
90
+ });
@@ -14,6 +14,10 @@ type Point = [x: number, y: number];
14
14
  *
15
15
  * When `xData` contains numeric values, those values are fed into the scale
16
16
  * instead of the array index so the points honour a numeric X domain.
17
+ *
18
+ * When `xData` is provided, iteration is capped at `xData.length` so a series
19
+ * with more points than the axis has labels does not overflow past the right
20
+ * edge of the chart. The axis is treated as authoritative for the X domain.
17
21
  */
18
22
  export const toScaledPoints = (
19
23
  data: (number | null)[],
@@ -22,8 +26,9 @@ export const toScaledPoints = (
22
26
  xData?: readonly (string | number)[],
23
27
  ): Point[] | null => {
24
28
  const pts: Point[] = [];
29
+ const limit = xData ? Math.min(data.length, xData.length) : data.length;
25
30
 
26
- for (let i = 0; i < data.length; i++) {
31
+ for (let i = 0; i < limit; i++) {
27
32
  const value = data[i];
28
33
  if (value === null) continue;
29
34
 
@@ -76,6 +76,42 @@ export const WithXAxis: Story = {
76
76
  },
77
77
  };
78
78
 
79
+ export const XAxisExplicitNumericData: Story = {
80
+ parameters: {
81
+ layout: 'centered',
82
+ backgrounds: { default: 'light' },
83
+ },
84
+ args: {
85
+ series: sampleSeries,
86
+ width: 400,
87
+ height: 250,
88
+ showXAxis: true,
89
+ xAxis: {
90
+ showLine: true,
91
+ showGrid: true,
92
+ data: [0, 2, 4],
93
+ },
94
+ },
95
+ };
96
+
97
+ export const XAxisStringData: Story = {
98
+ parameters: {
99
+ layout: 'centered',
100
+ backgrounds: { default: 'light' },
101
+ },
102
+ args: {
103
+ series: sampleSeries,
104
+ width: 400,
105
+ height: 250,
106
+ showXAxis: true,
107
+ xAxis: {
108
+ showLine: true,
109
+ showGrid: true,
110
+ data: ['Jan', 'Feb', 'Mar'],
111
+ },
112
+ },
113
+ };
114
+
79
115
  export const WithStringLabels: Story = {
80
116
  parameters: {
81
117
  layout: 'centered',
@@ -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;