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

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 (166) hide show
  1. package/dist/module/lib/Components/Axis/XAxis/XAxis.js +2 -2
  2. package/dist/module/lib/Components/Axis/XAxis/XAxis.js.map +1 -1
  3. package/dist/module/lib/Components/Axis/YAxis/YAxis.js +2 -2
  4. package/dist/module/lib/Components/Axis/YAxis/YAxis.js.map +1 -1
  5. package/dist/module/lib/Components/CartesianChart/CartesianChart.js +29 -22
  6. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  7. package/dist/module/lib/Components/CartesianChart/ChartEmptyLabel/ChartEmptyLabel.js +61 -0
  8. package/dist/module/lib/Components/CartesianChart/ChartEmptyLabel/ChartEmptyLabel.js.map +1 -0
  9. package/dist/module/lib/Components/CartesianChart/ChartEmptyLabel/index.js +4 -0
  10. package/dist/module/lib/Components/CartesianChart/ChartEmptyLabel/index.js.map +1 -0
  11. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js +3 -4
  12. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js.map +1 -1
  13. package/dist/module/lib/Components/CartesianChart/hooks/useDataFingerprint.js +13 -0
  14. package/dist/module/lib/Components/CartesianChart/hooks/useDataFingerprint.js.map +1 -0
  15. package/dist/module/lib/Components/CartesianChart/{RevealClip/utils.js → hooks/useRevealClipAnimation.js} +1 -6
  16. package/dist/module/lib/Components/CartesianChart/hooks/useRevealClipAnimation.js.map +1 -0
  17. package/dist/module/lib/Components/CartesianChart/hooks/useShimmerAnimation.js +37 -0
  18. package/dist/module/lib/Components/CartesianChart/hooks/useShimmerAnimation.js.map +1 -0
  19. package/dist/module/lib/Components/Line/Line.js +13 -8
  20. package/dist/module/lib/Components/Line/Line.js.map +1 -1
  21. package/dist/module/lib/Components/Line/constants.js +5 -0
  22. package/dist/module/lib/Components/Line/constants.js.map +1 -0
  23. package/dist/module/lib/Components/Line/types.js +2 -0
  24. package/dist/module/lib/Components/Line/utils.js +24 -7
  25. package/dist/module/lib/Components/Line/utils.js.map +1 -1
  26. package/dist/module/lib/Components/Line/utils.test.js +27 -1
  27. package/dist/module/lib/Components/Line/utils.test.js.map +1 -1
  28. package/dist/module/lib/Components/LineChart/LineChart.js +116 -27
  29. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  30. package/dist/module/lib/Components/LineChart/LineChart.test.js +78 -0
  31. package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
  32. package/dist/module/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.js +72 -0
  33. package/dist/module/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.js.map +1 -0
  34. package/dist/module/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.test.js +47 -0
  35. package/dist/module/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.test.js.map +1 -0
  36. package/dist/module/lib/Components/LineChart/LineChartEmptyState/index.js +4 -0
  37. package/dist/module/lib/Components/LineChart/LineChartEmptyState/index.js.map +1 -0
  38. package/dist/module/lib/Components/LineChart/LineChartEmptyState/types.js +4 -0
  39. package/dist/module/lib/Components/LineChart/LineChartEmptyState/types.js.map +1 -0
  40. package/dist/module/lib/Components/LineChart/LineChartEmptyState/utils.js +22 -0
  41. package/dist/module/lib/Components/LineChart/LineChartEmptyState/utils.js.map +1 -0
  42. package/dist/module/lib/Components/LineChart/LineChartEmptyState/utils.test.js +21 -0
  43. package/dist/module/lib/Components/LineChart/LineChartEmptyState/utils.test.js.map +1 -0
  44. package/dist/module/lib/Components/LineChart/utils.js +83 -0
  45. package/dist/module/lib/Components/LineChart/utils.js.map +1 -0
  46. package/dist/module/lib/Components/LineChart/utils.test.js +174 -0
  47. package/dist/module/lib/Components/LineChart/utils.test.js.map +1 -0
  48. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js +4 -4
  49. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js.map +1 -1
  50. package/dist/module/lib/Components/ReferenceLine/constants.js +2 -2
  51. package/dist/module/lib/Components/ReferenceLine/constants.js.map +1 -1
  52. package/dist/module/lib/Components/ReferenceLine/utils.js +2 -1
  53. package/dist/module/lib/Components/ReferenceLine/utils.js.map +1 -1
  54. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +2 -1
  55. package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -1
  56. package/dist/module/lib/Components/Scrubber/Scrubber.js +2 -2
  57. package/dist/module/lib/Components/Scrubber/Scrubber.js.map +1 -1
  58. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +2 -1
  59. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  60. package/dist/module/lib/Components/Scrubber/utils.js +3 -2
  61. package/dist/module/lib/Components/Scrubber/utils.js.map +1 -1
  62. package/dist/module/lib/utils/numbers.js +16 -0
  63. package/dist/module/lib/utils/numbers.js.map +1 -0
  64. package/dist/module/lib/utils/ticks/ticks.js +0 -16
  65. package/dist/module/lib/utils/ticks/ticks.js.map +1 -1
  66. package/dist/typescript/src/lib/Components/Axis/XAxis/XAxis.d.ts.map +1 -1
  67. package/dist/typescript/src/lib/Components/Axis/YAxis/YAxis.d.ts.map +1 -1
  68. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  69. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  70. package/dist/typescript/src/lib/Components/CartesianChart/ChartEmptyLabel/ChartEmptyLabel.d.ts +18 -0
  71. package/dist/typescript/src/lib/Components/CartesianChart/ChartEmptyLabel/ChartEmptyLabel.d.ts.map +1 -0
  72. package/dist/typescript/src/lib/Components/CartesianChart/ChartEmptyLabel/index.d.ts +2 -0
  73. package/dist/typescript/src/lib/Components/CartesianChart/ChartEmptyLabel/index.d.ts.map +1 -0
  74. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.d.ts.map +1 -1
  75. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts +1 -1
  76. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts.map +1 -1
  77. package/dist/typescript/src/lib/Components/CartesianChart/hooks/useDataFingerprint.d.ts +9 -0
  78. package/dist/typescript/src/lib/Components/CartesianChart/hooks/useDataFingerprint.d.ts.map +1 -0
  79. package/dist/typescript/src/lib/Components/CartesianChart/{RevealClip/utils.d.ts → hooks/useRevealClipAnimation.d.ts} +2 -5
  80. package/dist/typescript/src/lib/Components/CartesianChart/hooks/useRevealClipAnimation.d.ts.map +1 -0
  81. package/dist/typescript/src/lib/Components/CartesianChart/hooks/useShimmerAnimation.d.ts +13 -0
  82. package/dist/typescript/src/lib/Components/CartesianChart/hooks/useShimmerAnimation.d.ts.map +1 -0
  83. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +12 -0
  84. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  85. package/dist/typescript/src/lib/Components/Line/Line.d.ts +1 -1
  86. package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -1
  87. package/dist/typescript/src/lib/Components/Line/constants.d.ts +3 -0
  88. package/dist/typescript/src/lib/Components/Line/constants.d.ts.map +1 -0
  89. package/dist/typescript/src/lib/Components/Line/index.d.ts +1 -1
  90. package/dist/typescript/src/lib/Components/Line/index.d.ts.map +1 -1
  91. package/dist/typescript/src/lib/Components/Line/types.d.ts +8 -0
  92. package/dist/typescript/src/lib/Components/Line/types.d.ts.map +1 -1
  93. package/dist/typescript/src/lib/Components/Line/utils.d.ts +5 -4
  94. package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -1
  95. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
  96. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  97. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.d.ts +9 -0
  98. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.d.ts.map +1 -0
  99. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/index.d.ts +3 -0
  100. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/index.d.ts.map +1 -0
  101. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/types.d.ts +3 -0
  102. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/types.d.ts.map +1 -0
  103. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/utils.d.ts +14 -0
  104. package/dist/typescript/src/lib/Components/LineChart/LineChartEmptyState/utils.d.ts.map +1 -0
  105. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +29 -1
  106. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
  107. package/dist/typescript/src/lib/Components/LineChart/utils.d.ts +43 -0
  108. package/dist/typescript/src/lib/Components/LineChart/utils.d.ts.map +1 -0
  109. package/dist/typescript/src/lib/Components/ReferenceLine/ReferenceLine.d.ts.map +1 -1
  110. package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts +2 -2
  111. package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts.map +1 -1
  112. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -1
  113. package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -1
  114. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts.map +1 -1
  115. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  116. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -1
  117. package/dist/typescript/src/lib/utils/index.d.ts +1 -1
  118. package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
  119. package/dist/typescript/src/lib/utils/numbers.d.ts +13 -0
  120. package/dist/typescript/src/lib/utils/numbers.d.ts.map +1 -0
  121. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +1 -11
  122. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -1
  123. package/dist/typescript/src/lib/utils/types.d.ts +20 -2
  124. package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
  125. package/package.json +3 -3
  126. package/src/lib/Components/Axis/XAxis/XAxis.tsx +13 -18
  127. package/src/lib/Components/Axis/YAxis/YAxis.tsx +13 -18
  128. package/src/lib/Components/CartesianChart/CartesianChart.tsx +29 -23
  129. package/src/lib/Components/CartesianChart/ChartEmptyLabel/ChartEmptyLabel.tsx +68 -0
  130. package/src/lib/Components/CartesianChart/ChartEmptyLabel/index.ts +1 -0
  131. package/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.tsx +3 -2
  132. package/src/lib/Components/CartesianChart/RevealClip/types.ts +1 -1
  133. package/src/lib/Components/CartesianChart/hooks/useDataFingerprint.ts +16 -0
  134. package/src/lib/Components/CartesianChart/{RevealClip/utils.ts → hooks/useRevealClipAnimation.ts} +1 -9
  135. package/src/lib/Components/CartesianChart/hooks/useShimmerAnimation.ts +52 -0
  136. package/src/lib/Components/CartesianChart/types.ts +12 -0
  137. package/src/lib/Components/Line/Line.tsx +13 -10
  138. package/src/lib/Components/Line/constants.ts +2 -0
  139. package/src/lib/Components/Line/index.ts +1 -1
  140. package/src/lib/Components/Line/types.ts +10 -0
  141. package/src/lib/Components/Line/utils.test.ts +41 -1
  142. package/src/lib/Components/Line/utils.ts +42 -6
  143. package/src/lib/Components/LineChart/LineChart.test.tsx +64 -0
  144. package/src/lib/Components/LineChart/LineChart.tsx +139 -38
  145. package/src/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.test.tsx +39 -0
  146. package/src/lib/Components/LineChart/LineChartEmptyState/LineChartEmptyState.tsx +72 -0
  147. package/src/lib/Components/LineChart/LineChartEmptyState/index.ts +2 -0
  148. package/src/lib/Components/LineChart/LineChartEmptyState/types.ts +3 -0
  149. package/src/lib/Components/LineChart/LineChartEmptyState/utils.test.ts +22 -0
  150. package/src/lib/Components/LineChart/LineChartEmptyState/utils.ts +24 -0
  151. package/src/lib/Components/LineChart/types.ts +35 -1
  152. package/src/lib/Components/LineChart/utils.test.ts +196 -0
  153. package/src/lib/Components/LineChart/utils.ts +107 -0
  154. package/src/lib/Components/ReferenceLine/ReferenceLine.tsx +8 -4
  155. package/src/lib/Components/ReferenceLine/constants.ts +2 -2
  156. package/src/lib/Components/ReferenceLine/utils.ts +2 -1
  157. package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +2 -1
  158. package/src/lib/Components/Scrubber/Scrubber.tsx +13 -2
  159. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +2 -1
  160. package/src/lib/Components/Scrubber/utils.ts +3 -5
  161. package/src/lib/utils/index.ts +1 -0
  162. package/src/lib/utils/numbers.ts +15 -0
  163. package/src/lib/utils/ticks/ticks.ts +1 -23
  164. package/src/lib/utils/types.ts +28 -2
  165. package/dist/module/lib/Components/CartesianChart/RevealClip/utils.js.map +0 -1
  166. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/utils.d.ts.map +0 -1
@@ -1,8 +1,21 @@
1
- import { area, curveBumpX, line } from 'd3-shape';
1
+ import {
2
+ area,
3
+ curveBumpX,
4
+ curveLinear,
5
+ curveMonotoneX,
6
+ curveNatural,
7
+ curveStep,
8
+ curveStepAfter,
9
+ curveStepBefore,
10
+ line,
11
+ type CurveFactory,
12
+ } from 'd3-shape';
2
13
 
14
+ import { isFiniteNumber } from '../../utils/numbers';
3
15
  import { isCategoricalScale } from '../../utils/scales/scales';
4
16
  import type {
5
17
  ChartScaleFunction,
18
+ CurveType,
6
19
  DrawingArea,
7
20
  NumericScale,
8
21
  } from '../../utils/types';
@@ -10,7 +23,26 @@ import type {
10
23
  type Point = [x: number, y: number];
11
24
 
12
25
  /**
13
- * Project series data into scaled [x, y] pixel coordinates, skipping nulls.
26
+ * Maps the public `CurveType` values to their d3-shape curve factories.
27
+ */
28
+ const CURVE_FACTORIES: Record<CurveType, CurveFactory> = {
29
+ bump: curveBumpX,
30
+ linear: curveLinear,
31
+ monotone: curveMonotoneX,
32
+ natural: curveNatural,
33
+ step: curveStep,
34
+ stepAfter: curveStepAfter,
35
+ stepBefore: curveStepBefore,
36
+ };
37
+
38
+ const DEFAULT_CURVE: CurveType = 'bump';
39
+
40
+ const getCurveFactory = (curve: CurveType = DEFAULT_CURVE): CurveFactory =>
41
+ CURVE_FACTORIES[curve] ?? CURVE_FACTORIES[DEFAULT_CURVE];
42
+
43
+ /**
44
+ * Project series data into scaled [x, y] pixel coordinates, skipping any
45
+ * non-finite entries.
14
46
  *
15
47
  * When `xData` contains numeric values, those values are fed into the scale
16
48
  * instead of the array index so the points honour a numeric X domain.
@@ -30,7 +62,7 @@ export const toScaledPoints = (
30
62
 
31
63
  for (let i = 0; i < limit; i++) {
32
64
  const value = data[i];
33
- if (value === null) continue;
65
+ if (!isFiniteNumber(value)) continue;
34
66
 
35
67
  const xInput =
36
68
  xData && typeof xData[i] === 'number' ? (xData[i] as number) : i;
@@ -49,12 +81,15 @@ export const toScaledPoints = (
49
81
  /**
50
82
  * Build the SVG `d` attribute for the line stroke.
51
83
  */
52
- export const buildLinePath = (points: Point[]): string | null => {
84
+ export const buildLinePath = (
85
+ points: Point[],
86
+ curve?: CurveType,
87
+ ): string | null => {
53
88
  return (
54
89
  line<Point>()
55
90
  .x((d) => d[0])
56
91
  .y((d) => d[1])
57
- .curve(curveBumpX)(points) ?? null
92
+ .curve(getCurveFactory(curve))(points) ?? null
58
93
  );
59
94
  };
60
95
 
@@ -64,6 +99,7 @@ export const buildLinePath = (points: Point[]): string | null => {
64
99
  export const buildAreaPath = (
65
100
  points: Point[],
66
101
  drawingArea: DrawingArea,
102
+ curve?: CurveType,
67
103
  ): string | null => {
68
104
  const yBottom = drawingArea.y + drawingArea.height;
69
105
 
@@ -72,6 +108,6 @@ export const buildAreaPath = (
72
108
  .x((d) => d[0])
73
109
  .y0(yBottom)
74
110
  .y1((d) => d[1])
75
- .curve(curveBumpX)(points) ?? null
111
+ .curve(getCurveFactory(curve))(points) ?? null
76
112
  );
77
113
  };
@@ -255,4 +255,68 @@ describe('LineChart', () => {
255
255
  );
256
256
  getByTestId('chart-svg');
257
257
  });
258
+
259
+ describe('loading and empty states', () => {
260
+ it('renders the placeholder with no data while loading (state 1)', () => {
261
+ const { getByTestId, queryByTestId } = render(
262
+ <LineChartWrapper>
263
+ <LineChart width={400} height={200} loading />
264
+ </LineChartWrapper>,
265
+ );
266
+
267
+ getByTestId('chart-empty-state');
268
+ expect(queryByTestId('line-path')).toBeNull();
269
+ expect(queryByTestId('chart-empty-label')).toBeNull();
270
+ });
271
+
272
+ it('renders the placeholder with the empty label when there is no data and not loading (state 2)', () => {
273
+ const { getByTestId, queryByTestId } = render(
274
+ <LineChartWrapper>
275
+ <LineChart width={400} height={200} emptyLabel='Nothing here' />
276
+ </LineChartWrapper>,
277
+ );
278
+
279
+ getByTestId('chart-empty-state');
280
+ expect(
281
+ getByTestId('chart-empty-label', { includeHiddenElements: true }),
282
+ ).toHaveTextContent('Nothing here');
283
+ expect(queryByTestId('line-path')).toBeNull();
284
+ });
285
+
286
+ it('defaults the empty label to "No data"', () => {
287
+ const { getByTestId } = render(
288
+ <LineChartWrapper>
289
+ <LineChart width={400} height={200} />
290
+ </LineChartWrapper>,
291
+ );
292
+
293
+ expect(
294
+ getByTestId('chart-empty-label', { includeHiddenElements: true }),
295
+ ).toHaveTextContent('No data');
296
+ });
297
+
298
+ it('keeps rendering the real line during a transition load (state 3)', () => {
299
+ const { getByTestId, queryByTestId } = render(
300
+ <LineChartWrapper>
301
+ <LineChart series={sampleSeries} width={400} height={200} loading />
302
+ </LineChartWrapper>,
303
+ );
304
+
305
+ getByTestId('line-path');
306
+ expect(queryByTestId('chart-empty-state')).toBeNull();
307
+ expect(queryByTestId('chart-empty-label')).toBeNull();
308
+ });
309
+
310
+ it('renders the normal chart when idle with data (state 4)', () => {
311
+ const { getByTestId, queryByTestId } = render(
312
+ <LineChartWrapper>
313
+ <LineChart series={sampleSeries} width={400} height={200} />
314
+ </LineChartWrapper>,
315
+ );
316
+
317
+ getByTestId('line-path');
318
+ expect(queryByTestId('chart-empty-state')).toBeNull();
319
+ expect(queryByTestId('chart-empty-label')).toBeNull();
320
+ });
321
+ });
258
322
  });
@@ -1,13 +1,95 @@
1
1
  import { useMemo } from 'react';
2
+ import Animated from 'react-native-reanimated';
3
+ import { G } from 'react-native-svg';
2
4
 
3
- import type { ChartInset } from '../../utils/types';
4
5
  import { defaultXAxisProps, defaultYAxisProps } from '../Axis';
5
- import { DEFAULT_AXIS_HEIGHT, XAxis } from '../Axis/XAxis';
6
+ import { XAxis } from '../Axis/XAxis';
6
7
  import { YAxis } from '../Axis/YAxis';
7
8
  import { CartesianChart } from '../CartesianChart';
9
+ import { ChartEmptyLabel } from '../CartesianChart/ChartEmptyLabel';
10
+ import { useShimmerAnimation } from '../CartesianChart/hooks/useShimmerAnimation';
8
11
  import { Line } from '../Line';
9
12
 
10
- import type { LineChartProps } from './types';
13
+ import { LineChartEmptyState } from './LineChartEmptyState';
14
+ import type {
15
+ LineChartContentProps,
16
+ LineChartLinesProps,
17
+ LineChartProps,
18
+ } from './types';
19
+ import {
20
+ canRenderLine,
21
+ computeAxisPadding,
22
+ getChartDisplayState,
23
+ } from './utils';
24
+
25
+ const AnimatedG = Animated.createAnimatedComponent(G);
26
+
27
+ const LineChartLines = ({
28
+ series,
29
+ showArea,
30
+ areaType,
31
+ }: Readonly<LineChartLinesProps>) => {
32
+ return (
33
+ <>
34
+ {series.map((s) => (
35
+ <Line
36
+ key={s.id}
37
+ seriesId={s.id}
38
+ stroke={s.stroke}
39
+ showArea={showArea}
40
+ areaType={areaType}
41
+ />
42
+ ))}
43
+ </>
44
+ );
45
+ };
46
+
47
+ const LineChartTransitionLines = ({
48
+ series,
49
+ showArea,
50
+ areaType,
51
+ }: Readonly<LineChartLinesProps>) => {
52
+ const { animatedProps } = useShimmerAnimation();
53
+
54
+ return (
55
+ <AnimatedG animatedProps={animatedProps}>
56
+ <LineChartLines series={series} showArea={showArea} areaType={areaType} />
57
+ </AnimatedG>
58
+ );
59
+ };
60
+
61
+ const LineChartContent = ({
62
+ series,
63
+ showArea,
64
+ areaType,
65
+ showXAxis,
66
+ showYAxis,
67
+ xAxisConfig,
68
+ yAxisConfig,
69
+ isTransitionLoading,
70
+ children,
71
+ }: Readonly<LineChartContentProps>) => {
72
+ return (
73
+ <>
74
+ {showXAxis && <XAxis {...xAxisConfig} />}
75
+ {showYAxis && <YAxis {...yAxisConfig} />}
76
+ {isTransitionLoading ? (
77
+ <LineChartTransitionLines
78
+ series={series}
79
+ showArea={showArea}
80
+ areaType={areaType}
81
+ />
82
+ ) : (
83
+ <LineChartLines
84
+ series={series}
85
+ showArea={showArea}
86
+ areaType={areaType}
87
+ />
88
+ )}
89
+ {children}
90
+ </>
91
+ );
92
+ };
11
93
 
12
94
  export const LineChart = ({
13
95
  series,
@@ -18,47 +100,54 @@ export const LineChart = ({
18
100
  xAxis,
19
101
  yAxis,
20
102
  width,
21
- height = 160,
103
+ height = 228,
22
104
  inset,
23
105
  children,
24
106
  enableScrubbing = false,
25
107
  onScrubberPositionChange,
26
108
  animate,
27
109
  magnetRadius,
28
- }: LineChartProps) => {
110
+ loading = false,
111
+ emptyLabel = 'No data',
112
+ }: Readonly<LineChartProps>) => {
29
113
  const xAxisConfig = {
30
114
  ...defaultXAxisProps,
31
115
  ...xAxis,
116
+ position: xAxis?.position ?? defaultXAxisProps.position,
32
117
  };
33
118
  const yAxisConfig = {
34
119
  ...defaultYAxisProps,
35
120
  ...yAxis,
121
+ position: yAxis?.position ?? defaultYAxisProps.position,
122
+ width: yAxis?.width ?? defaultYAxisProps.width,
36
123
  };
37
124
 
38
- const axisPadding: Partial<ChartInset> | undefined = useMemo(() => {
39
- if (!showXAxis && !showYAxis) {
40
- return undefined;
41
- }
125
+ const xAxisPosition = xAxisConfig.position;
126
+ const yAxisPosition = yAxisConfig.position;
127
+ const yAxisWidth = yAxisConfig.width;
42
128
 
43
- return {
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,
54
- };
55
- }, [
56
- showXAxis,
57
- showYAxis,
58
- xAxisConfig?.position,
59
- yAxisConfig?.position,
60
- yAxisConfig?.width,
61
- ]);
129
+ const axisPadding = useMemo(
130
+ () =>
131
+ computeAxisPadding({
132
+ showXAxis,
133
+ showYAxis,
134
+ xAxisPosition,
135
+ yAxisPosition,
136
+ yAxisWidth,
137
+ }),
138
+ [showXAxis, showYAxis, xAxisPosition, yAxisPosition, yAxisWidth],
139
+ );
140
+
141
+ const hasData = canRenderLine(series, xAxisConfig.data);
142
+
143
+ const { status, ariaLabel } = getChartDisplayState({
144
+ loading,
145
+ hasData,
146
+ emptyLabel,
147
+ });
148
+
149
+ const isTransitionLoading = status === 'transition-loading';
150
+ const isPlaceholder = status === 'initial-loading' || status === 'empty';
62
151
 
63
152
  return (
64
153
  <CartesianChart
@@ -71,20 +160,32 @@ export const LineChart = ({
71
160
  axisPadding={axisPadding}
72
161
  enableScrubbing={enableScrubbing}
73
162
  onScrubberPositionChange={onScrubberPositionChange}
74
- animate={animate}
163
+ animate={isTransitionLoading ? false : animate}
75
164
  magnetRadius={magnetRadius}
165
+ ariaLabel={ariaLabel}
166
+ ariaBusy={loading}
167
+ overlay={
168
+ status === 'empty' ? (
169
+ <ChartEmptyLabel>{emptyLabel}</ChartEmptyLabel>
170
+ ) : undefined
171
+ }
76
172
  >
77
- {showXAxis && <XAxis {...xAxisConfig} />}
78
- {showYAxis && <YAxis {...yAxisConfig} />}
79
- {series?.map((s) => (
80
- <Line
81
- key={s.id}
82
- seriesId={s.id}
173
+ {isPlaceholder ? (
174
+ <LineChartEmptyState loading={status === 'initial-loading'} />
175
+ ) : (
176
+ <LineChartContent
177
+ series={series ?? []}
83
178
  showArea={showArea}
84
179
  areaType={areaType}
85
- />
86
- ))}
87
- {children}
180
+ showXAxis={showXAxis}
181
+ showYAxis={showYAxis}
182
+ xAxisConfig={xAxisConfig}
183
+ yAxisConfig={yAxisConfig}
184
+ isTransitionLoading={isTransitionLoading}
185
+ >
186
+ {children}
187
+ </LineChartContent>
188
+ )}
88
189
  </CartesianChart>
89
190
  );
90
191
  };
@@ -0,0 +1,39 @@
1
+ import { describe, it } 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
+
6
+ import { CartesianChart } from '../../CartesianChart';
7
+ import { LineChartEmptyState } from './LineChartEmptyState';
8
+ import type { LineChartEmptyStateProps } from './types';
9
+
10
+ const renderInChart = (props: LineChartEmptyStateProps) => {
11
+ return render(
12
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
13
+ <CartesianChart series={[]} width={400} height={200}>
14
+ <LineChartEmptyState {...props} />
15
+ </CartesianChart>
16
+ </ThemeProvider>,
17
+ );
18
+ };
19
+
20
+ describe('LineChartEmptyState', () => {
21
+ it('renders the placeholder line', () => {
22
+ const { getByTestId } = renderInChart({});
23
+
24
+ getByTestId('chart-empty-state-line');
25
+ });
26
+
27
+ it('renders the grid lines', () => {
28
+ const { getByTestId } = renderInChart({});
29
+
30
+ getByTestId('chart-empty-state-grid');
31
+ });
32
+
33
+ it('renders the placeholder while loading', () => {
34
+ const { getByTestId } = renderInChart({ loading: true });
35
+
36
+ getByTestId('chart-empty-state');
37
+ getByTestId('chart-empty-state-line');
38
+ });
39
+ });
@@ -0,0 +1,72 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import Animated from 'react-native-reanimated';
3
+ import { G, Line as SvgLine, Path } from 'react-native-svg';
4
+
5
+ import { useCartesianChartContext } from '../../CartesianChart/context';
6
+ import { useShimmerAnimation } from '../../CartesianChart/hooks/useShimmerAnimation';
7
+ import { useRevealClip } from '../../CartesianChart/RevealClip';
8
+
9
+ import type { LineChartEmptyStateProps } from './types';
10
+ import { buildPlaceholderTransform, PLACEHOLDER_LINE_PATH } from './utils';
11
+
12
+ const GRID_LINE_RATIOS = [0.3, 0.5, 0.7];
13
+ const GRID_STROKE_WIDTH = 1;
14
+ const PLACEHOLDER_STROKE_WIDTH = 2;
15
+
16
+ const AnimatedG = Animated.createAnimatedComponent(G);
17
+
18
+ /**
19
+ * SVG placeholder line shown when a LineChart is loading with no data yet (with
20
+ * a shimmer) or has no data to display (static). Uses a fixed line shape scaled
21
+ * into the chart's reserved drawing area, plus static horizontal grid lines, so
22
+ * axes are not needed.
23
+ */
24
+ export function LineChartEmptyState({
25
+ loading = false,
26
+ }: Readonly<LineChartEmptyStateProps>) {
27
+ const { drawingArea } = useCartesianChartContext();
28
+ const { theme } = useTheme();
29
+ const { animatedProps } = useShimmerAnimation(loading);
30
+ const clipPath = useRevealClip();
31
+
32
+ if (drawingArea.width <= 0 || drawingArea.height <= 0) {
33
+ return null;
34
+ }
35
+
36
+ return (
37
+ <G testID='chart-empty-state'>
38
+ <G testID='chart-empty-state-grid'>
39
+ {GRID_LINE_RATIOS.map((ratio) => {
40
+ const y = drawingArea.y + drawingArea.height * ratio;
41
+
42
+ return (
43
+ <SvgLine
44
+ key={ratio}
45
+ x1={drawingArea.x}
46
+ y1={y}
47
+ x2={drawingArea.x + drawingArea.width}
48
+ y2={y}
49
+ stroke={theme.colors.border.mutedSubtleTransparent}
50
+ strokeWidth={GRID_STROKE_WIDTH}
51
+ />
52
+ );
53
+ })}
54
+ </G>
55
+ <G clipPath={clipPath}>
56
+ <AnimatedG animatedProps={animatedProps}>
57
+ <Path
58
+ testID='chart-empty-state-line'
59
+ d={PLACEHOLDER_LINE_PATH}
60
+ transform={buildPlaceholderTransform(drawingArea)}
61
+ vectorEffect='non-scaling-stroke'
62
+ fill='none'
63
+ stroke={theme.colors.border.muted}
64
+ strokeWidth={PLACEHOLDER_STROKE_WIDTH}
65
+ strokeLinecap='round'
66
+ strokeLinejoin='round'
67
+ />
68
+ </AnimatedG>
69
+ </G>
70
+ </G>
71
+ );
72
+ }
@@ -0,0 +1,2 @@
1
+ export { LineChartEmptyState } from './LineChartEmptyState';
2
+ export type { LineChartEmptyStateProps } from './types';
@@ -0,0 +1,3 @@
1
+ import type { LineChartProps } from '../types';
2
+
3
+ export type LineChartEmptyStateProps = Pick<LineChartProps, 'loading'>;
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from '@jest/globals';
2
+
3
+ import { buildPlaceholderTransform, PLACEHOLDER_LINE_PATH } from './utils';
4
+
5
+ describe('PLACEHOLDER_LINE_PATH', () => {
6
+ it('is an svg path starting with a move command', () => {
7
+ expect(PLACEHOLDER_LINE_PATH).toMatch(/^M/);
8
+ });
9
+ });
10
+
11
+ describe('buildPlaceholderTransform', () => {
12
+ it('translates to the drawing area origin and scales the viewBox to fit', () => {
13
+ const transform = buildPlaceholderTransform({
14
+ x: 10,
15
+ y: 20,
16
+ width: 364,
17
+ height: 104,
18
+ });
19
+
20
+ expect(transform).toBe('translate(10, 20) scale(0.5, 0.5)');
21
+ });
22
+ });
@@ -0,0 +1,24 @@
1
+ import type { DrawingArea } from '../../../utils/types';
2
+
3
+ /**
4
+ * Fixed placeholder line shape, authored against a {@link PLACEHOLDER_VIEW_WIDTH}
5
+ * x {@link PLACEHOLDER_VIEW_HEIGHT} viewBox. Reused for every empty/loading
6
+ * state so the placeholder always reads the same.
7
+ */
8
+ export const PLACEHOLDER_LINE_PATH =
9
+ 'M1.00 128.27 C4.02 127.46 13.09 125.96 19.13 123.47 C25.19 120.97 31.23 116.53 37.28 113.31 C43.33 110.10 49.38 106.15 55.42 104.20 C61.48 102.24 67.52 102.53 73.57 101.61 C79.61 100.69 85.65 99.89 91.71 98.70 C97.75 97.51 103.80 95.67 109.84 94.47 C115.90 93.27 121.94 91.71 127.99 91.49 C134.03 91.26 140.09 91.53 146.13 93.16 C152.19 94.79 158.23 98.37 164.27 101.25 C170.32 104.13 176.36 108.16 182.42 110.45 C188.46 112.74 194.51 114.41 200.55 115.00 C206.61 115.60 212.65 114.56 218.70 114.03 C224.75 113.50 230.80 112.56 236.84 111.86 C242.88 111.17 248.94 110.27 254.98 109.87 C261.03 109.48 267.07 109.14 273.13 109.49 C279.17 109.82 285.22 110.90 291.26 111.90 C297.32 112.90 303.36 114.47 309.41 115.50 C315.46 116.54 321.50 118.12 327.55 118.11 C333.59 118.10 339.65 118.08 345.69 115.45 C351.74 112.81 357.78 107.02 363.84 102.29 C369.88 97.54 375.93 90.87 381.97 87.01 C388.03 83.16 394.07 79.81 400.11 79.12 C406.17 78.43 412.21 80.79 418.26 82.88 C424.30 84.99 430.36 88.88 436.40 91.74 C442.45 94.59 448.49 98.24 454.55 100.02 C460.59 101.80 466.64 102.08 472.68 102.44 C478.72 102.81 484.78 102.31 490.82 102.21 C496.87 102.11 502.92 101.95 508.97 101.84 C515.01 101.74 521.07 101.47 527.11 101.58 C533.16 101.68 539.20 101.57 545.26 102.48 C551.30 103.39 557.34 105.37 563.39 107.02 C569.43 108.68 575.49 111.02 581.53 112.40 C587.59 113.79 593.63 114.62 599.68 115.30 C605.72 115.98 611.78 115.81 617.82 116.48 C623.87 117.16 629.91 118.39 635.95 119.32 C642.01 120.25 648.05 121.51 654.10 122.04 C660.15 122.57 666.20 123.89 672.24 122.47 C678.30 121.07 684.34 117.39 690.39 113.54 C696.43 109.69 702.49 103.53 708.53 99.38 C714.57 95.23 722.31 90.65 726.66 88.61';
10
+
11
+ const PLACEHOLDER_VIEW_WIDTH = 728;
12
+ const PLACEHOLDER_VIEW_HEIGHT = 208;
13
+
14
+ /**
15
+ * Transform that fits {@link PLACEHOLDER_LINE_PATH} into the drawing area. Pair
16
+ * with `vector-effect: non-scaling-stroke` on the path so the stroke keeps a
17
+ * constant width despite the (potentially non-uniform) scale.
18
+ */
19
+ export const buildPlaceholderTransform = (drawingArea: DrawingArea): string => {
20
+ const scaleX = drawingArea.width / PLACEHOLDER_VIEW_WIDTH;
21
+ const scaleY = drawingArea.height / PLACEHOLDER_VIEW_HEIGHT;
22
+
23
+ return `translate(${drawingArea.x}, ${drawingArea.y}) scale(${scaleX}, ${scaleY})`;
24
+ };
@@ -48,7 +48,7 @@ export type LineChartProps = {
48
48
  width?: number;
49
49
  /**
50
50
  * Height of the chart in pixels.
51
- * @default 160
51
+ * @default 228
52
52
  */
53
53
  height?: number;
54
54
  /**
@@ -83,4 +83,38 @@ export type LineChartProps = {
83
83
  * @default 6
84
84
  */
85
85
  magnetRadius?: number;
86
+ /**
87
+ * Whether the chart is loading. When there is no data, renders an animated
88
+ * shimmer placeholder line (initial loading). When data is present (e.g. a
89
+ * time-range change), the current line is recoloured to a muted grey and
90
+ * shimmers until the new `series` is provided (transition loading).
91
+ *
92
+ * To get the transition behaviour, keep passing the previous `series` while
93
+ * refetching; clearing it falls back to the initial placeholder.
94
+ * @default false
95
+ */
96
+ loading?: boolean;
97
+ /**
98
+ * Text shown in the centre of the chart when there is no data and the chart
99
+ * is not loading.
100
+ * @default 'No data'
101
+ */
102
+ emptyLabel?: string;
86
103
  };
104
+
105
+ /**
106
+ * Series-render fields shared by `LineChart` and its internal line
107
+ * sub-components. Derived from {@link LineChartProps} so the option types stay
108
+ * in sync.
109
+ */
110
+ export type LineChartLinesProps = Required<
111
+ Pick<LineChartProps, 'series' | 'showArea' | 'areaType'>
112
+ >;
113
+
114
+ export type LineChartContentProps = LineChartLinesProps &
115
+ Required<Pick<LineChartProps, 'showXAxis' | 'showYAxis'>> &
116
+ Pick<LineChartProps, 'children'> & {
117
+ xAxisConfig: XAxisProps;
118
+ yAxisConfig: YAxisProps;
119
+ isTransitionLoading: boolean;
120
+ };