@ledgerhq/lumen-ui-rnative-visualization 0.1.10 → 0.1.11

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 (163) hide show
  1. package/dist/module/lib/Components/Axis/XAxis/XAxis.js +1 -1
  2. package/dist/module/lib/Components/Axis/XAxis/XAxis.js.map +1 -1
  3. package/dist/module/lib/Components/Axis/YAxis/YAxis.js +1 -1
  4. package/dist/module/lib/Components/Axis/YAxis/YAxis.js.map +1 -1
  5. package/dist/module/lib/Components/CartesianChart/CartesianChart.js +31 -28
  6. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  7. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js +53 -0
  8. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.js.map +1 -0
  9. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.js +144 -0
  10. package/dist/module/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.js.map +1 -0
  11. package/dist/module/lib/Components/CartesianChart/RevealClip/context.js +8 -0
  12. package/dist/module/lib/Components/CartesianChart/RevealClip/context.js.map +1 -0
  13. package/dist/module/lib/Components/CartesianChart/RevealClip/index.js +5 -0
  14. package/dist/module/lib/Components/CartesianChart/RevealClip/index.js.map +1 -0
  15. package/dist/module/lib/Components/CartesianChart/RevealClip/types.js +4 -0
  16. package/dist/module/lib/Components/CartesianChart/RevealClip/types.js.map +1 -0
  17. package/dist/module/lib/Components/CartesianChart/RevealClip/utils.js +32 -0
  18. package/dist/module/lib/Components/CartesianChart/RevealClip/utils.js.map +1 -0
  19. package/dist/module/lib/Components/CartesianChart/utils.js +4 -4
  20. package/dist/module/lib/Components/Line/Line.js +5 -2
  21. package/dist/module/lib/Components/Line/Line.js.map +1 -1
  22. package/dist/module/lib/Components/LineChart/LineChart.js +7 -1
  23. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  24. package/dist/module/lib/Components/LineChart/LineChart.test.js +25 -0
  25. package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
  26. package/dist/module/lib/Components/Point/Point.js +3 -0
  27. package/dist/module/lib/Components/Point/Point.js.map +1 -1
  28. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js +125 -0
  29. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.js.map +1 -0
  30. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.stories.js +111 -0
  31. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.stories.js.map +1 -0
  32. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.test.js +338 -0
  33. package/dist/module/lib/Components/ReferenceLine/ReferenceLine.test.js.map +1 -0
  34. package/dist/module/lib/Components/ReferenceLine/constants.js +5 -0
  35. package/dist/module/lib/Components/ReferenceLine/constants.js.map +1 -0
  36. package/dist/module/lib/Components/ReferenceLine/index.js +5 -0
  37. package/dist/module/lib/Components/ReferenceLine/index.js.map +1 -0
  38. package/dist/module/lib/Components/ReferenceLine/types.js +2 -0
  39. package/dist/module/lib/Components/ReferenceLine/types.js.map +1 -0
  40. package/dist/module/lib/Components/ReferenceLine/utils.js +121 -0
  41. package/dist/module/lib/Components/ReferenceLine/utils.js.map +1 -0
  42. package/dist/module/lib/Components/ReferenceLine/utils.test.js +297 -0
  43. package/dist/module/lib/Components/ReferenceLine/utils.test.js.map +1 -0
  44. package/dist/module/lib/Components/Scrubber/Scrubber.js +152 -0
  45. package/dist/module/lib/Components/Scrubber/Scrubber.js.map +1 -0
  46. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js +113 -0
  47. package/dist/module/lib/Components/Scrubber/Scrubber.stories.js.map +1 -0
  48. package/dist/module/lib/Components/Scrubber/Scrubber.test.js +132 -0
  49. package/dist/module/lib/Components/Scrubber/Scrubber.test.js.map +1 -0
  50. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +105 -0
  51. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -0
  52. package/dist/module/lib/Components/Scrubber/ScrubberProvider.test.js +217 -0
  53. package/dist/module/lib/Components/Scrubber/ScrubberProvider.test.js.map +1 -0
  54. package/dist/module/lib/Components/Scrubber/constants.js +10 -0
  55. package/dist/module/lib/Components/Scrubber/constants.js.map +1 -0
  56. package/dist/module/lib/Components/Scrubber/context/index.js +4 -0
  57. package/dist/module/lib/Components/Scrubber/context/index.js.map +1 -0
  58. package/dist/module/lib/Components/Scrubber/context/scrubberContext.js +10 -0
  59. package/dist/module/lib/Components/Scrubber/context/scrubberContext.js.map +1 -0
  60. package/dist/module/lib/Components/Scrubber/index.js +5 -0
  61. package/dist/module/lib/Components/Scrubber/index.js.map +1 -0
  62. package/dist/module/lib/Components/Scrubber/types.js +4 -0
  63. package/dist/module/lib/Components/Scrubber/types.js.map +1 -0
  64. package/dist/module/lib/Components/Scrubber/utils.js +83 -0
  65. package/dist/module/lib/Components/Scrubber/utils.js.map +1 -0
  66. package/dist/module/lib/Components/Scrubber/utils.test.js +192 -0
  67. package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -0
  68. package/dist/module/lib/Components/index.js +2 -0
  69. package/dist/module/lib/Components/index.js.map +1 -1
  70. package/dist/module/lib/utils/scales/scales.js +16 -8
  71. package/dist/module/lib/utils/scales/scales.js.map +1 -1
  72. package/dist/module/lib/utils/scales/scales.test.js +32 -1
  73. package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
  74. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  75. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  76. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.d.ts +3 -0
  77. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.d.ts.map +1 -0
  78. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/context.d.ts +7 -0
  79. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/context.d.ts.map +1 -0
  80. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/index.d.ts +3 -0
  81. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/index.d.ts.map +1 -0
  82. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts +38 -0
  83. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/types.d.ts.map +1 -0
  84. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/utils.d.ts +17 -0
  85. package/dist/typescript/src/lib/Components/CartesianChart/RevealClip/utils.d.ts.map +1 -0
  86. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +17 -0
  87. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  88. package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -1
  89. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
  90. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  91. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +16 -0
  92. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -1
  93. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  94. package/dist/typescript/src/lib/Components/ReferenceLine/ReferenceLine.d.ts +3 -0
  95. package/dist/typescript/src/lib/Components/ReferenceLine/ReferenceLine.d.ts.map +1 -0
  96. package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts +3 -0
  97. package/dist/typescript/src/lib/Components/ReferenceLine/constants.d.ts.map +1 -0
  98. package/dist/typescript/src/lib/Components/ReferenceLine/index.d.ts +3 -0
  99. package/dist/typescript/src/lib/Components/ReferenceLine/index.d.ts.map +1 -0
  100. package/dist/typescript/src/lib/Components/ReferenceLine/types.d.ts +74 -0
  101. package/dist/typescript/src/lib/Components/ReferenceLine/types.d.ts.map +1 -0
  102. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +55 -0
  103. package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -0
  104. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts +17 -0
  105. package/dist/typescript/src/lib/Components/Scrubber/Scrubber.d.ts.map +1 -0
  106. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +16 -0
  107. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -0
  108. package/dist/typescript/src/lib/Components/Scrubber/constants.d.ts +8 -0
  109. package/dist/typescript/src/lib/Components/Scrubber/constants.d.ts.map +1 -0
  110. package/dist/typescript/src/lib/Components/Scrubber/context/index.d.ts +2 -0
  111. package/dist/typescript/src/lib/Components/Scrubber/context/index.d.ts.map +1 -0
  112. package/dist/typescript/src/lib/Components/Scrubber/context/scrubberContext.d.ts +8 -0
  113. package/dist/typescript/src/lib/Components/Scrubber/context/scrubberContext.d.ts.map +1 -0
  114. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts +4 -0
  115. package/dist/typescript/src/lib/Components/Scrubber/index.d.ts.map +1 -0
  116. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +57 -0
  117. package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -0
  118. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +22 -0
  119. package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -0
  120. package/dist/typescript/src/lib/Components/index.d.ts +2 -0
  121. package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
  122. package/dist/typescript/src/lib/utils/scales/scales.d.ts +6 -0
  123. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
  124. package/package.json +6 -3
  125. package/src/lib/Components/Axis/XAxis/XAxis.tsx +1 -1
  126. package/src/lib/Components/Axis/YAxis/YAxis.tsx +1 -1
  127. package/src/lib/Components/CartesianChart/CartesianChart.tsx +32 -32
  128. package/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.test.tsx +129 -0
  129. package/src/lib/Components/CartesianChart/RevealClip/RevealClipDefs.tsx +59 -0
  130. package/src/lib/Components/CartesianChart/RevealClip/context.ts +13 -0
  131. package/src/lib/Components/CartesianChart/RevealClip/index.ts +2 -0
  132. package/src/lib/Components/CartesianChart/RevealClip/types.ts +38 -0
  133. package/src/lib/Components/CartesianChart/RevealClip/utils.ts +51 -0
  134. package/src/lib/Components/CartesianChart/types.ts +17 -0
  135. package/src/lib/Components/CartesianChart/utils.ts +4 -4
  136. package/src/lib/Components/Line/Line.tsx +5 -3
  137. package/src/lib/Components/LineChart/LineChart.test.tsx +23 -0
  138. package/src/lib/Components/LineChart/LineChart.tsx +6 -0
  139. package/src/lib/Components/LineChart/types.ts +16 -0
  140. package/src/lib/Components/Point/Point.tsx +3 -1
  141. package/src/lib/Components/ReferenceLine/ReferenceLine.stories.tsx +109 -0
  142. package/src/lib/Components/ReferenceLine/ReferenceLine.test.tsx +269 -0
  143. package/src/lib/Components/ReferenceLine/ReferenceLine.tsx +149 -0
  144. package/src/lib/Components/ReferenceLine/constants.ts +2 -0
  145. package/src/lib/Components/ReferenceLine/index.ts +2 -0
  146. package/src/lib/Components/ReferenceLine/types.ts +79 -0
  147. package/src/lib/Components/ReferenceLine/utils.test.ts +332 -0
  148. package/src/lib/Components/ReferenceLine/utils.ts +175 -0
  149. package/src/lib/Components/Scrubber/Scrubber.stories.tsx +122 -0
  150. package/src/lib/Components/Scrubber/Scrubber.test.tsx +110 -0
  151. package/src/lib/Components/Scrubber/Scrubber.tsx +189 -0
  152. package/src/lib/Components/Scrubber/ScrubberProvider.test.tsx +223 -0
  153. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +132 -0
  154. package/src/lib/Components/Scrubber/constants.ts +7 -0
  155. package/src/lib/Components/Scrubber/context/index.ts +1 -0
  156. package/src/lib/Components/Scrubber/context/scrubberContext.ts +14 -0
  157. package/src/lib/Components/Scrubber/index.ts +3 -0
  158. package/src/lib/Components/Scrubber/types.ts +59 -0
  159. package/src/lib/Components/Scrubber/utils.test.ts +172 -0
  160. package/src/lib/Components/Scrubber/utils.ts +126 -0
  161. package/src/lib/Components/index.ts +2 -0
  162. package/src/lib/utils/scales/scales.test.ts +22 -0
  163. package/src/lib/utils/scales/scales.ts +19 -9
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { ThemeProvider } from '@ledgerhq/lumen-ui-rnative';
4
+ import { render } from '@testing-library/react-native';
5
+ import { Svg } from 'react-native-svg';
6
+
7
+ import type { DrawingArea, Series } from '../../../utils/types';
8
+ import { OVERFLOW_BUFFER } from '../utils';
9
+
10
+ import { useRevealClip } from './context';
11
+ import { RevealClipDefs } from './RevealClipDefs';
12
+
13
+ const drawingArea: DrawingArea = {
14
+ x: 10,
15
+ y: 10,
16
+ width: 380,
17
+ height: 180,
18
+ };
19
+
20
+ const series: Series[] = [{ id: 'a', stroke: '#000', data: [1, 2, 3] }];
21
+
22
+ function ClipConsumer() {
23
+ const clipPath = useRevealClip();
24
+ return <Svg testID='consumer' clipPath={clipPath} />;
25
+ }
26
+
27
+ const renderInSvg = (ui: React.ReactElement) =>
28
+ render(
29
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
30
+ <Svg>{ui}</Svg>
31
+ </ThemeProvider>,
32
+ );
33
+
34
+ describe('RevealClipDefs', () => {
35
+ it('renders children', () => {
36
+ const { getByTestId } = renderInSvg(
37
+ <RevealClipDefs
38
+ drawingArea={drawingArea}
39
+ series={series}
40
+ transitions={{ enter: { duration: 0.5 } }}
41
+ >
42
+ <Svg testID='child' />
43
+ </RevealClipDefs>,
44
+ );
45
+
46
+ expect(getByTestId('child')).toBeTruthy();
47
+ });
48
+
49
+ it('renders a ClipPath with a Rect when animate is true', () => {
50
+ const { UNSAFE_root } = renderInSvg(
51
+ <RevealClipDefs
52
+ drawingArea={drawingArea}
53
+ series={series}
54
+ transitions={{ enter: { duration: 0.5 } }}
55
+ >
56
+ <Svg testID='child' />
57
+ </RevealClipDefs>,
58
+ );
59
+
60
+ const clipPaths = UNSAFE_root.findAllByType('ClipPath' as any);
61
+ expect(clipPaths.length).toBeGreaterThan(0);
62
+
63
+ const rects = clipPaths[0].findAllByType('Rect' as any);
64
+ expect(rects.length).toBeGreaterThan(0);
65
+
66
+ const rect = rects[0];
67
+ expect(rect.props.x).toBe(drawingArea.x - OVERFLOW_BUFFER.left);
68
+ expect(rect.props.y).toBe(drawingArea.y - OVERFLOW_BUFFER.top);
69
+ expect(rect.props.height).toBe(
70
+ drawingArea.height + OVERFLOW_BUFFER.top + OVERFLOW_BUFFER.bottom,
71
+ );
72
+ });
73
+
74
+ it('provides clipPathAttr to consumers via context', () => {
75
+ const { UNSAFE_root } = renderInSvg(
76
+ <RevealClipDefs
77
+ drawingArea={drawingArea}
78
+ series={series}
79
+ transitions={{ enter: { duration: 0.5 } }}
80
+ >
81
+ <ClipConsumer />
82
+ </RevealClipDefs>,
83
+ );
84
+
85
+ const consumer = UNSAFE_root.findByProps({ testID: 'consumer' });
86
+ expect(consumer.props.clipPath).toMatch(/^url\(#/);
87
+ });
88
+
89
+ it('skips ClipPath when animate is false', () => {
90
+ const { UNSAFE_root, getByTestId } = renderInSvg(
91
+ <RevealClipDefs drawingArea={drawingArea} series={series} animate={false}>
92
+ <Svg testID='child' />
93
+ </RevealClipDefs>,
94
+ );
95
+
96
+ expect(getByTestId('child')).toBeTruthy();
97
+
98
+ const clipPaths = UNSAFE_root.findAllByType('ClipPath' as any);
99
+ expect(clipPaths).toHaveLength(0);
100
+ });
101
+
102
+ it('does not provide clipPathAttr when animate is false', () => {
103
+ const { UNSAFE_root } = renderInSvg(
104
+ <RevealClipDefs drawingArea={drawingArea} series={series} animate={false}>
105
+ <ClipConsumer />
106
+ </RevealClipDefs>,
107
+ );
108
+
109
+ const consumer = UNSAFE_root.findByProps({ testID: 'consumer' });
110
+ expect(consumer.props.clipPath).toBeUndefined();
111
+ });
112
+
113
+ it('renders without crashing when transitions.enter is not defined', () => {
114
+ const { UNSAFE_root, getByTestId } = renderInSvg(
115
+ <RevealClipDefs
116
+ drawingArea={drawingArea}
117
+ series={series}
118
+ transitions={{}}
119
+ >
120
+ <Svg testID='child' />
121
+ </RevealClipDefs>,
122
+ );
123
+
124
+ expect(getByTestId('child')).toBeTruthy();
125
+
126
+ const clipPaths = UNSAFE_root.findAllByType('ClipPath' as any);
127
+ expect(clipPaths.length).toBeGreaterThan(0);
128
+ });
129
+ });
@@ -0,0 +1,59 @@
1
+ import { useMemo } from 'react';
2
+ import Animated from 'react-native-reanimated';
3
+ import { ClipPath, Defs, Rect } from 'react-native-svg';
4
+
5
+ import { OVERFLOW_BUFFER } from '../utils';
6
+
7
+ import { RevealClipContext } from './context';
8
+ import type { RevealClipDefsProps } from './types';
9
+ import { useComputeDataFingerprint, useRevealClipAnimation } from './utils';
10
+
11
+ const DEFAULT_DURATION_IN_SECONDS = 0.8;
12
+
13
+ const AnimatedRect = Animated.createAnimatedComponent(Rect);
14
+
15
+ export function RevealClipDefs({
16
+ children,
17
+ drawingArea,
18
+ series,
19
+ animate = true,
20
+ transitions,
21
+ }: RevealClipDefsProps) {
22
+ const isDisabled = !animate;
23
+ const durationMs =
24
+ (transitions?.enter?.duration ?? DEFAULT_DURATION_IN_SECONDS) * 1000;
25
+
26
+ const dataFingerprint = useComputeDataFingerprint({ series });
27
+ const { clipId, animatedRectProps } = useRevealClipAnimation({
28
+ durationMs,
29
+ drawingArea,
30
+ dataFingerprint,
31
+ });
32
+
33
+ const contextValue = useMemo(
34
+ () => ({ clipPathAttr: `url(#${clipId})` }),
35
+ [clipId],
36
+ );
37
+
38
+ if (isDisabled) {
39
+ return children;
40
+ }
41
+
42
+ return (
43
+ <RevealClipContext.Provider key={dataFingerprint} value={contextValue}>
44
+ <Defs>
45
+ <ClipPath id={clipId}>
46
+ <AnimatedRect
47
+ x={drawingArea.x - OVERFLOW_BUFFER.left}
48
+ y={drawingArea.y - OVERFLOW_BUFFER.top}
49
+ height={
50
+ drawingArea.height + OVERFLOW_BUFFER.top + OVERFLOW_BUFFER.bottom
51
+ }
52
+ animatedProps={animatedRectProps}
53
+ />
54
+ </ClipPath>
55
+ </Defs>
56
+ {children}
57
+ </RevealClipContext.Provider>
58
+ );
59
+ }
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ type RevealClipContextValue = {
4
+ clipPathAttr: string;
5
+ };
6
+
7
+ export const RevealClipContext = createContext<
8
+ RevealClipContextValue | undefined
9
+ >(undefined);
10
+
11
+ export const useRevealClip = (): string | undefined => {
12
+ return useContext(RevealClipContext)?.clipPathAttr;
13
+ };
@@ -0,0 +1,2 @@
1
+ export { RevealClipDefs } from './RevealClipDefs';
2
+ export { useRevealClip } from './context';
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import type { DrawingArea, Series } from '../../../utils';
4
+
5
+ export type EnterTransitionConfig = {
6
+ /**
7
+ * Duration in seconds. @default 0.8
8
+ */
9
+ duration?: number;
10
+ /**
11
+ * Reanimated easing is configured internally.
12
+ * This field is kept for API parity with the web variant but is unused on native.
13
+ */
14
+ easing?: string;
15
+ };
16
+
17
+ export type RevealClipDefsProps = {
18
+ children: ReactNode;
19
+ /**
20
+ * The drawing area whose bounds define the clip rectangle.
21
+ */
22
+ drawingArea: DrawingArea;
23
+ /**
24
+ * Whether to animate the chart.
25
+ * @default true
26
+ */
27
+ animate?: boolean;
28
+ /**
29
+ * Transition configuration.
30
+ * @default { enter: { duration: 0.8 } }
31
+ */
32
+ transitions?: { enter?: EnterTransitionConfig };
33
+ /**
34
+ * Data series used to compute a fingerprint that restarts
35
+ * the animation when data changes.
36
+ */
37
+ series: Series[];
38
+ };
@@ -0,0 +1,51 @@
1
+ import { useEffect, useId } from 'react';
2
+ import {
3
+ useAnimatedProps,
4
+ useSharedValue,
5
+ withTiming,
6
+ } from 'react-native-reanimated';
7
+
8
+ import type { DrawingArea, Series } from '../../../utils/types';
9
+ import { OVERFLOW_BUFFER } from '../utils';
10
+
11
+ type RevealClipAnimationConfig = {
12
+ durationMs: number;
13
+ drawingArea: DrawingArea;
14
+ dataFingerprint: string;
15
+ };
16
+
17
+ type RevealClipAnimationResult = {
18
+ clipId: string;
19
+ animatedRectProps: ReturnType<typeof useAnimatedProps>;
20
+ };
21
+
22
+ export const useRevealClipAnimation = ({
23
+ durationMs,
24
+ drawingArea,
25
+ dataFingerprint,
26
+ }: RevealClipAnimationConfig): RevealClipAnimationResult => {
27
+ const clipId = useId();
28
+ const width = useSharedValue(0);
29
+
30
+ useEffect(() => {
31
+ width.value = 0;
32
+ width.value = withTiming(
33
+ drawingArea.width + OVERFLOW_BUFFER.left + OVERFLOW_BUFFER.right,
34
+ { duration: durationMs },
35
+ );
36
+ }, [drawingArea.width, durationMs, width, dataFingerprint]);
37
+
38
+ const animatedRectProps = useAnimatedProps(() => ({
39
+ width: width.value,
40
+ }));
41
+
42
+ return { clipId, animatedRectProps };
43
+ };
44
+
45
+ export const useComputeDataFingerprint = ({
46
+ series,
47
+ }: {
48
+ series: Series[];
49
+ }): string => {
50
+ return series.map((s) => s.data?.join(',') ?? '').join('|');
51
+ };
@@ -46,4 +46,21 @@ export type CartesianChartProps = {
46
46
  * SVG content rendered inside the chart's context provider.
47
47
  */
48
48
  children?: ReactNode;
49
+ /**
50
+ * Enables scrubbing (pan gesture) interactions on the chart.
51
+ * When true, add a `<Scrubber>` as a child to visualise the interaction.
52
+ * @default false
53
+ */
54
+ enableScrubbing?: boolean;
55
+ /**
56
+ * Callback fired whenever the scrubber moves to a new data index or is cleared.
57
+ * Receives `undefined` when the scrub gesture ends or the scrubber position is otherwise cleared.
58
+ */
59
+ onScrubberPositionChange?: (index: number | undefined) => void;
60
+ /**
61
+ * Whether to animate the chart on mount and data changes.
62
+ * Duration is 0.8 seconds.
63
+ * @default true
64
+ */
65
+ animate?: boolean;
49
66
  };
@@ -9,10 +9,10 @@ export const DEFAULT_HEIGHT = 160;
9
9
  * wrapper so the chart's visual footprint stays unchanged.
10
10
  */
11
11
  export const OVERFLOW_BUFFER: ChartInset = {
12
- top: 30,
13
- right: 20,
14
- bottom: 30,
15
- left: 20,
12
+ top: 50,
13
+ right: 50,
14
+ bottom: 50,
15
+ left: 50,
16
16
  };
17
17
  export const OVERFLOW_NEGATIVE_MARGIN = {
18
18
  marginTop: -OVERFLOW_BUFFER.top,
@@ -1,8 +1,9 @@
1
1
  import { useId, useMemo } from 'react';
2
- import { Defs, LinearGradient, Path, Stop } from 'react-native-svg';
2
+ import { Defs, G, LinearGradient, Path, Stop } from 'react-native-svg';
3
3
 
4
4
  import { isNumericScale } from '../../utils/scales/scales';
5
5
  import { useCartesianChartContext } from '../CartesianChart/context';
6
+ import { useRevealClip } from '../CartesianChart/RevealClip';
6
7
 
7
8
  import type { LineProps } from './types';
8
9
  import { buildAreaPath, buildLinePath, toScaledPoints } from './utils';
@@ -18,6 +19,7 @@ export const Line = ({
18
19
  }: LineProps) => {
19
20
  const { getXScale, getYScale, getXAxisConfig, drawingArea, seriesMap } =
20
21
  useCartesianChartContext();
22
+ const clipPath = useRevealClip();
21
23
 
22
24
  const xScale = getXScale();
23
25
  const yScale = getYScale();
@@ -53,7 +55,7 @@ export const Line = ({
53
55
  }
54
56
 
55
57
  return (
56
- <>
58
+ <G clipPath={clipPath}>
57
59
  {showArea && areaPath && resolvedStroke && (
58
60
  <>
59
61
  <Defs>
@@ -83,6 +85,6 @@ export const Line = ({
83
85
  strokeLinecap='round'
84
86
  strokeLinejoin='round'
85
87
  />
86
- </>
88
+ </G>
87
89
  );
88
90
  };
@@ -76,4 +76,27 @@ describe('LineChart', () => {
76
76
  );
77
77
  expect(queryAllByTestId('line-area')).toHaveLength(0);
78
78
  });
79
+
80
+ it('renders with animate true by default', () => {
81
+ const { getByTestId } = render(
82
+ <LineChartWrapper>
83
+ <LineChart series={sampleSeries} width={400} height={200} />
84
+ </LineChartWrapper>,
85
+ );
86
+ getByTestId('chart-svg');
87
+ });
88
+
89
+ it('renders with animate false', () => {
90
+ const { getByTestId } = render(
91
+ <LineChartWrapper>
92
+ <LineChart
93
+ series={sampleSeries}
94
+ width={400}
95
+ height={200}
96
+ animate={false}
97
+ />
98
+ </LineChartWrapper>,
99
+ );
100
+ getByTestId('chart-svg');
101
+ });
79
102
  });
@@ -20,6 +20,9 @@ export const LineChart = ({
20
20
  height = 160,
21
21
  inset,
22
22
  children,
23
+ enableScrubbing = false,
24
+ onScrubberPositionChange,
25
+ animate,
23
26
  }: LineChartProps) => {
24
27
  const {
25
28
  scaleType: xScaleType,
@@ -77,6 +80,9 @@ export const LineChart = ({
77
80
  height={height}
78
81
  inset={inset}
79
82
  axisPadding={axisPadding}
83
+ enableScrubbing={enableScrubbing}
84
+ onScrubberPositionChange={onScrubberPositionChange}
85
+ animate={animate}
80
86
  >
81
87
  {showXAxis && <XAxis {...xAxisVisualProps} />}
82
88
  {showYAxis && <YAxis {...yAxisVisualProps} />}
@@ -60,4 +60,20 @@ export type LineChartProps = {
60
60
  * Additional SVG content rendered inside the chart after lines and axes.
61
61
  */
62
62
  children?: ReactNode;
63
+ /**
64
+ * Enables scrubbing (pan gesture) interactions on the chart.
65
+ * When true, add a `<Scrubber>` as a child to visualise the interaction.
66
+ * @default false
67
+ */
68
+ enableScrubbing?: boolean;
69
+ /**
70
+ * Callback fired whenever the scrubber moves to a new data index or is cleared.
71
+ * Receives `undefined` when the scrub gesture ends or the scrubber position is otherwise cleared.
72
+ */
73
+ onScrubberPositionChange?: (index: number | undefined) => void;
74
+ /**
75
+ * Whether to animate the chart on mount and data changes.
76
+ * @default true
77
+ */
78
+ animate?: boolean;
63
79
  };
@@ -4,6 +4,7 @@ import { Circle, G, Polygon, Text as SvgText } from 'react-native-svg';
4
4
 
5
5
  import { projectPoint } from '../../utils/scales/scales';
6
6
  import { useCartesianChartContext } from '../CartesianChart/context';
7
+ import { useRevealClip } from '../CartesianChart/RevealClip';
7
8
 
8
9
  import type { PointLabelProps, PointProps } from './types';
9
10
  import {
@@ -46,6 +47,7 @@ export function Point({
46
47
  onPress,
47
48
  }: Readonly<PointProps>) {
48
49
  const { getXScale, getYScale, drawingArea } = useCartesianChartContext();
50
+ const clipPath = useRevealClip();
49
51
  const { theme } = useTheme();
50
52
 
51
53
  const xScale = getXScale();
@@ -71,7 +73,7 @@ export function Point({
71
73
  const Label = LabelComponent ?? PointLabel;
72
74
 
73
75
  return (
74
- <G testID='point-group' onPress={onPress}>
76
+ <G testID='point-group' clipPath={clipPath} onPress={onPress}>
75
77
  {!hidePoint && (
76
78
  <Circle
77
79
  testID='point-circle'
@@ -0,0 +1,109 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
3
+ import { View } from 'react-native';
4
+
5
+ import { StoryDecorator } from '../../../../.storybook/StoryDecorator.tsx';
6
+ import { LineChart } from '../LineChart';
7
+
8
+ import { ReferenceLine } from './ReferenceLine';
9
+
10
+ const sampleSeries = [
11
+ {
12
+ id: 'prices',
13
+ stroke: '#7B61FF',
14
+ data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
15
+ },
16
+ ];
17
+
18
+ const meta = {
19
+ component: ReferenceLine,
20
+ title: 'Visualization/ReferenceLine',
21
+ tags: ['experimental'],
22
+ decorators: [
23
+ (Story, context) => {
24
+ return (
25
+ <StoryDecorator context={context}>
26
+ <View style={{ width: 600, padding: 16 }}>
27
+ <Story />
28
+ </View>
29
+ </StoryDecorator>
30
+ );
31
+ },
32
+ ],
33
+ } satisfies Meta<typeof ReferenceLine>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<typeof ReferenceLine>;
37
+
38
+ export const HorizontalLine: Story = {
39
+ parameters: {
40
+ layout: 'centered',
41
+ backgrounds: { default: 'light' },
42
+ },
43
+ render: () => (
44
+ <LineChart series={sampleSeries} height={250} showArea>
45
+ <ReferenceLine
46
+ dataY={50}
47
+ label='Target'
48
+ labelDy={-4}
49
+ labelVerticalAlignment='start'
50
+ labelHorizontalAlignment='start'
51
+ />
52
+ </LineChart>
53
+ ),
54
+ };
55
+
56
+ export const VerticalLine: Story = {
57
+ parameters: {
58
+ layout: 'centered',
59
+ backgrounds: { default: 'light' },
60
+ },
61
+ render: () => (
62
+ <LineChart series={sampleSeries} height={250} showArea>
63
+ <ReferenceLine
64
+ dataX={7}
65
+ label='Midpoint'
66
+ labelDx={4}
67
+ labelHorizontalAlignment='end'
68
+ labelVerticalAlignment='end'
69
+ labelPosition='start'
70
+ />
71
+ </LineChart>
72
+ ),
73
+ };
74
+
75
+ export const Combined: Story = {
76
+ parameters: {
77
+ layout: 'centered',
78
+ backgrounds: { default: 'light' },
79
+ },
80
+ render: () => {
81
+ const { theme } = useTheme();
82
+ return (
83
+ <LineChart series={sampleSeries} height={250} showArea>
84
+ <ReferenceLine
85
+ dataY={98}
86
+ label='ATH'
87
+ labelDy={4}
88
+ labelVerticalAlignment='start'
89
+ stroke={theme.colors.border.success}
90
+ />
91
+ <ReferenceLine
92
+ dataY={4}
93
+ label='Low'
94
+ labelDy={-4}
95
+ labelVerticalAlignment='end'
96
+ stroke={theme.colors.border.error}
97
+ />
98
+ <ReferenceLine
99
+ dataX={4}
100
+ label='Peak'
101
+ labelDx={8}
102
+ labelDy={8}
103
+ labelHorizontalAlignment='end'
104
+ labelPosition='start'
105
+ />
106
+ </LineChart>
107
+ );
108
+ },
109
+ };