@ledgerhq/lumen-ui-rnative-visualization 0.1.23 → 0.1.25

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 (45) hide show
  1. package/dist/module/lib/Components/Line/Line.js +4 -4
  2. package/dist/module/lib/Components/Line/Line.js.map +1 -1
  3. package/dist/module/lib/Components/LineChart/LineChart.js +4 -4
  4. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  5. package/dist/module/lib/Components/LineChart/LineChart.performance.test.js +99 -0
  6. package/dist/module/lib/Components/LineChart/LineChart.performance.test.js.map +1 -0
  7. package/dist/module/lib/Components/Point/Point.js +3 -2
  8. package/dist/module/lib/Components/Point/Point.js.map +1 -1
  9. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js +19 -9
  10. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js.map +1 -1
  11. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js +10 -1
  12. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js.map +1 -1
  13. package/dist/module/lib/Components/Point/pointContext/index.js +1 -1
  14. package/dist/module/lib/Components/Point/pointContext/index.js.map +1 -1
  15. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js +22 -4
  16. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js.map +1 -1
  17. package/dist/module/lib/Components/Point/usePointGeometry.js +3 -3
  18. package/dist/module/lib/Components/Point/usePointGeometry.js.map +1 -1
  19. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +22 -30
  20. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  21. package/dist/typescript/src/lib/Components/Line/Line.d.ts +1 -1
  22. package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -1
  23. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  24. package/dist/typescript/src/lib/Components/Point/Point.d.ts +1 -1
  25. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  26. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -1
  27. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +1 -1
  28. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -1
  29. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +22 -5
  30. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -1
  31. package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts +2 -2
  32. package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts.map +1 -1
  33. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +3 -4
  34. package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
  35. package/package.json +4 -4
  36. package/src/lib/Components/Line/Line.tsx +4 -4
  37. package/src/lib/Components/LineChart/LineChart.performance.test.tsx +101 -0
  38. package/src/lib/Components/LineChart/LineChart.tsx +18 -11
  39. package/src/lib/Components/Point/Point.tsx +5 -2
  40. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +13 -1
  41. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +20 -8
  42. package/src/lib/Components/Point/pointContext/index.ts +4 -2
  43. package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +29 -7
  44. package/src/lib/Components/Point/usePointGeometry.ts +5 -5
  45. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +23 -30
@@ -1,3 +1,3 @@
1
1
  import type { LineProps } from './types';
2
- export declare const Line: ({ seriesId, stroke, showArea, areaType: _areaType, curve, connectNulls, }: LineProps) => import("react/jsx-runtime").JSX.Element | null;
2
+ export declare const Line: import("react").NamedExoticComponent<LineProps>;
3
3
  //# sourceMappingURL=Line.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Line.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Line/Line.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGzC,eAAO,MAAM,IAAI,GAAI,2EAOlB,SAAS,mDAkFX,CAAC"}
1
+ {"version":3,"file":"Line.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Line/Line.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAGzC,eAAO,MAAM,IAAI,iDAyFf,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"LineChart.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/LineChart/LineChart.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAGV,cAAc,EAEf,MAAM,SAAS,CAAC;AA0FjB,eAAO,MAAM,SAAS,GAAI,0MAmBvB,QAAQ,CAAC,cAAc,CAAC,4CAgF1B,CAAC"}
1
+ {"version":3,"file":"LineChart.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/LineChart/LineChart.tsx"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAGV,cAAc,EAEf,MAAM,SAAS,CAAC;AA0FjB,eAAO,MAAM,SAAS,GAAI,0MAmBvB,QAAQ,CAAC,cAAc,CAAC,4CAuF1B,CAAC"}
@@ -1,4 +1,4 @@
1
1
  import type { PointLabelProps, PointProps } from './types';
2
2
  export declare function PointLabel({ textAnchor, ...props }: Readonly<PointLabelProps>): import("react/jsx-runtime").JSX.Element;
3
- export declare function Point({ dataX, dataY, color, label, LabelComponent, labelPosition, hidePoint, showLabelArrow, size, onPress, magnetic, labelAlignment, }: Readonly<PointProps>): import("react/jsx-runtime").JSX.Element | null;
3
+ export declare const Point: import("react").NamedExoticComponent<Readonly<PointProps>>;
4
4
  //# sourceMappingURL=Point.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Point.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/Point.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,eAAe,EAEf,UAAU,EACX,MAAM,SAAS,CAAC;AAMjB,wBAAgB,UAAU,CAAC,EACzB,UAAqB,EACrB,GAAG,KAAK,EACT,EAAE,QAAQ,CAAC,eAAe,CAAC,2CAa3B;AAgCD,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,KAAK,EACL,KAAK,EACL,KAAK,EACL,cAAc,EACd,aAAqB,EACrB,SAAiB,EACjB,cAAqB,EACrB,IAAmB,EACnB,OAAO,EACP,QAAgB,EAChB,cAAuB,GACxB,EAAE,QAAQ,CAAC,UAAU,CAAC,kDAoDtB"}
1
+ {"version":3,"file":"Point.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/Point.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,eAAe,EAEf,UAAU,EACX,MAAM,SAAS,CAAC;AAOjB,wBAAgB,UAAU,CAAC,EACzB,UAAqB,EACrB,GAAG,KAAK,EACT,EAAE,QAAQ,CAAC,eAAe,CAAC,2CAa3B;AAgCD,eAAO,MAAM,KAAK,4DAiEhB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"MagneticPointsProvider.d.ts","sourceRoot":"","sources":["../../../../../../../src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAOvC,KAAK,2BAA2B,GAAG;IACjC,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,GACT,EAAE,QAAQ,CAAC,2BAA2B,CAAC,2CA8BvC"}
1
+ {"version":3,"file":"MagneticPointsProvider.d.ts","sourceRoot":"","sources":["../../../../../../../src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AASvC,KAAK,2BAA2B,GAAG;IACjC,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,GACT,EAAE,QAAQ,CAAC,2BAA2B,CAAC,2CAwCvC"}
@@ -1,3 +1,3 @@
1
- export { useMagneticPointsContext, type MagneticPointsContextValue, } from './magneticPointsContext';
1
+ export { useMagneticRegistry, useMagneticSnapshot, type MagneticRegistryValue, type MagneticSnapshotValue, } from './magneticPointsContext';
2
2
  export { MagneticPointsProvider } from './MagneticPointsProvider';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../src/lib/Components/Point/pointContext/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,KAAK,0BAA0B,GAChC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../../src/lib/Components/Point/pointContext/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC"}
@@ -1,13 +1,30 @@
1
- export type MagneticPointsContextValue = {
1
+ /**
2
+ * Stable registration API for magnetic points. Its identity never changes for
3
+ * the provider's lifetime, so consumers (every `Point`) do not re-render when
4
+ * the magnetic set changes — only when they mount/unmount.
5
+ */
6
+ export type MagneticRegistryValue = {
2
7
  register: (dataIndex: number) => void;
3
8
  unregister: (dataIndex: number) => void;
9
+ };
10
+ /**
11
+ * Mutable snapshot of the registered magnetic points. Changes on every
12
+ * register/unregister (via `version`) and is consumed only by the scrubber,
13
+ * which needs to recompute its snap targets when the set changes.
14
+ */
15
+ export type MagneticSnapshotValue = {
4
16
  version: number;
5
17
  getMagneticPoints: () => ReadonlySet<number>;
6
18
  };
7
- declare const MagneticPointsContextProvider: import("react").FC<{
19
+ declare const MagneticRegistryProvider: import("react").FC<{
20
+ children: import("react").ReactNode;
21
+ value: MagneticRegistryValue;
22
+ }>;
23
+ declare const MagneticSnapshotProvider: import("react").FC<{
8
24
  children: import("react").ReactNode;
9
- value: MagneticPointsContextValue;
25
+ value: MagneticSnapshotValue;
10
26
  }>;
11
- export declare const useMagneticPointsContext: () => MagneticPointsContextValue;
12
- export { MagneticPointsContextProvider };
27
+ export declare const useMagneticRegistry: () => MagneticRegistryValue;
28
+ export declare const useMagneticSnapshot: () => MagneticSnapshotValue;
29
+ export { MagneticRegistryProvider, MagneticSnapshotProvider };
13
30
  //# sourceMappingURL=magneticPointsContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"magneticPointsContext.d.ts","sourceRoot":"","sources":["../../../../../../../src/lib/Components/Point/pointContext/magneticPointsContext.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;CAC9C,CAAC;AAEF,QAAA,MAAO,6BAA6B;;;EAC6B,CAAC;AAElE,eAAO,MAAM,wBAAwB,QAAO,0BAIxC,CAAC;AAEL,OAAO,EAAE,6BAA6B,EAAE,CAAC"}
1
+ {"version":3,"file":"magneticPointsContext.d.ts","sourceRoot":"","sources":["../../../../../../../src/lib/Components/Point/pointContext/magneticPointsContext.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;CAC9C,CAAC;AAEF,QAAA,MAAO,wBAAwB;;;EAC+B,CAAC;AAE/D,QAAA,MAAO,wBAAwB;;;EAC+B,CAAC;AAE/D,eAAO,MAAM,mBAAmB,QAAO,qBAInC,CAAC;AAEL,eAAO,MAAM,mBAAmB,QAAO,qBAInC,CAAC;AAEL,OAAO,EAAE,wBAAwB,EAAE,wBAAwB,EAAE,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import type { DrawingArea } from '../../utils/types';
2
2
  import type { BaseAxisProps } from '../Axis';
3
3
  import { useRevealFadeProps } from '../CartesianChart/RevealAnimation';
4
- import type { MagneticPointsContextValue } from './pointContext/magneticPointsContext';
4
+ import type { MagneticRegistryValue } from './pointContext/magneticPointsContext';
5
5
  type Pixel = {
6
6
  x: number;
7
7
  y: number;
@@ -28,6 +28,6 @@ export declare const usePointGeometry: ({ dataX, dataY, magnetic, }: UsePointGeo
28
28
  * Registers/unregisters a data index as a magnetic snap target
29
29
  * when the Point has `magnetic` enabled.
30
30
  */
31
- export declare const useMagneticRegistration: (magnetic: boolean, dataX: number, getXAxisConfig: () => BaseAxisProps | undefined, { register, unregister }: MagneticPointsContextValue) => void;
31
+ export declare const useMagneticRegistration: (magnetic: boolean, dataX: number, getXAxisConfig: () => BaseAxisProps | undefined, { register, unregister }: MagneticRegistryValue) => void;
32
32
  export {};
33
33
  //# sourceMappingURL=usePointGeometry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePointGeometry.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/usePointGeometry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAEvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAGvF,KAAK,KAAK,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,KAAK,sBAAsB,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;IACjD,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,6BAI9B,sBAAsB,KAAG,aAqB3B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,OAAO,EACjB,OAAO,MAAM,EACb,gBAAgB,MAAM,aAAa,GAAG,SAAS,EAC/C,0BAA0B,0BAA0B,KACnD,IAQF,CAAC"}
1
+ {"version":3,"file":"usePointGeometry.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/usePointGeometry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAEvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAGlF,KAAK,KAAK,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,KAAK,sBAAsB,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,KAAK,EAAE,KAAK,GAAG,SAAS,CAAC;IACzB,WAAW,EAAE,WAAW,CAAC;IACzB,SAAS,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;IACjD,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,6BAI9B,sBAAsB,KAAG,aAqB3B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,OAAO,EACjB,OAAO,MAAM,EACb,gBAAgB,MAAM,aAAa,GAAG,SAAS,EAC/C,0BAA0B,qBAAqB,KAC9C,IAQF,CAAC"}
@@ -7,10 +7,9 @@ import type { ScrubberProviderProps } from './types';
7
7
  * gesture-capture `View` overlay). `CartesianChart` handles this positioning
8
8
  * automatically when `enableScrubbing` is true.
9
9
  *
10
- * Activation requires a 50 ms long press before panning begins. This prevents
11
- * accidental scrubbing when the user is scrolling a parent `ScrollView` or
12
- * `FlatList`. The `isScrubbing` shared value gates the pan via manual activation
13
- * so scroll and scrub gestures never compete.
10
+ * Scrubbing engages on horizontal panning (`activeOffsetX`) or a press-and-hold
11
+ * (`LongPress`); vertical movement fails the pan (`failOffsetY`) so an ancestor
12
+ * vertical scroll view keeps scrolling.
14
13
  */
15
14
  export declare function ScrubberProvider({ children, width, height, enableScrubbing, onScrubberPositionChange, magnetRadius, style, }: Readonly<ScrubberProviderProps>): import("react/jsx-runtime").JSX.Element;
16
15
  //# sourceMappingURL=ScrubberProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ScrubberProvider.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Scrubber/ScrubberProvider.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAUrD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,KAAK,EACL,MAAM,EACN,eAAe,EACf,wBAAwB,EACxB,YAAgB,EAChB,KAAK,GACN,EAAE,QAAQ,CAAC,qBAAqB,CAAC,2CAwKjC"}
1
+ {"version":3,"file":"ScrubberProvider.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Scrubber/ScrubberProvider.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAcrD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,KAAK,EACL,MAAM,EACN,eAAe,EACf,wBAAwB,EACxB,YAAgB,EAChB,KAAK,GACN,EAAE,QAAQ,CAAC,qBAAqB,CAAC,2CA8JjC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative-visualization",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "keywords": [
@@ -31,7 +31,7 @@
31
31
  "!**/*.tsbuildinfo"
32
32
  ],
33
33
  "dependencies": {
34
- "@ledgerhq/lumen-utils-shared": "0.1.6",
34
+ "@ledgerhq/lumen-utils-shared": "0.1.7",
35
35
  "d3-array": "^3.2.4",
36
36
  "d3-scale": "^4.0.2",
37
37
  "d3-shape": "^3.2.0"
@@ -44,8 +44,8 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@types/react": "^19.0.0",
47
- "@ledgerhq/lumen-design-core": "0.1.18",
48
- "@ledgerhq/lumen-ui-rnative": "0.1.46",
47
+ "@ledgerhq/lumen-design-core": "0.1.19",
48
+ "@ledgerhq/lumen-ui-rnative": "0.1.48",
49
49
  "react": "^19.0.0",
50
50
  "react-native": "~0.81.6",
51
51
  "react-native-svg": "^15.0.0",
@@ -1,5 +1,5 @@
1
1
  import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
- import { useId, useMemo } from 'react';
2
+ import { useId, useMemo, memo } from 'react';
3
3
  import { Defs, G, LinearGradient, Path, Stop } from 'react-native-svg';
4
4
 
5
5
  import { isNumericScale } from '../../utils/scales/scales';
@@ -10,14 +10,14 @@ import { LINE_AREA_GRADIENT_OPACITY, LINE_STROKE_WIDTH } from './constants';
10
10
  import type { LineProps } from './types';
11
11
  import { buildAreaPath, buildLinePath, toScaledPoints } from './utils';
12
12
 
13
- export const Line = ({
13
+ export const Line = memo(function Line({
14
14
  seriesId,
15
15
  stroke,
16
16
  showArea = false,
17
17
  areaType: _areaType = 'gradient',
18
18
  curve,
19
19
  connectNulls,
20
- }: LineProps) => {
20
+ }: LineProps) {
21
21
  const { getXScale, getYScale, getXAxisConfig, drawingArea, seriesMap } =
22
22
  useCartesianChartContext();
23
23
  const clipPath = usePathReveal();
@@ -99,4 +99,4 @@ export const Line = ({
99
99
  />
100
100
  </G>
101
101
  );
102
- };
102
+ });
@@ -0,0 +1,101 @@
1
+ import { beforeEach, describe, expect, 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
+ import React, { Profiler } from 'react';
6
+ import type { ProfilerOnRenderCallback } from 'react';
7
+
8
+ import { Point } from '../Point';
9
+
10
+ import { LineChart } from './LineChart';
11
+
12
+ // Deterministic render budgets: assert on counts, never time, so CI never flakes.
13
+ // Native scrubbing is gesture/worklet driven and not simulable here, so the
14
+ // scrub re-render guard lives only in the React lib.
15
+
16
+ const POINT_COUNT = 24;
17
+
18
+ const buildData = (length: number): number[] =>
19
+ Array.from({ length }, (_, i) => (i % 9) + 1);
20
+
21
+ // Counts commits of the Point subtree itself (via React.Profiler) rather than a
22
+ // wrapper's renders. A wrapper counter sits above Point and never re-runs when a
23
+ // context Point consumes updates, so it would silently miss the version-cascade
24
+ // regression this budget guards against.
25
+ let pointMountCommits = 0;
26
+ let pointUpdateCommits = 0;
27
+ const onPointRender: ProfilerOnRenderCallback = (_id, phase) => {
28
+ if (phase === 'mount') {
29
+ pointMountCommits++;
30
+ } else {
31
+ pointUpdateCommits++;
32
+ }
33
+ };
34
+
35
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
36
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
37
+ {children}
38
+ </ThemeProvider>
39
+ );
40
+
41
+ beforeEach(() => {
42
+ pointMountCommits = 0;
43
+ pointUpdateCommits = 0;
44
+ });
45
+
46
+ describe('LineChart performance budgets', () => {
47
+ it('emits a bounded node count per point', () => {
48
+ const data = buildData(POINT_COUNT);
49
+
50
+ const { queryAllByTestId } = render(
51
+ <Wrapper>
52
+ <LineChart
53
+ series={[{ id: 's', stroke: '#000', data }]}
54
+ width={400}
55
+ height={200}
56
+ animate={false}
57
+ >
58
+ {data.map((value, index) => (
59
+ <Point key={index} dataX={index} dataY={value} />
60
+ ))}
61
+ </LineChart>
62
+ </Wrapper>,
63
+ );
64
+
65
+ const groups = queryAllByTestId('point-group');
66
+ const circles = queryAllByTestId('point-circle');
67
+ const arrows = queryAllByTestId('point-arrow');
68
+
69
+ expect(circles).toHaveLength(groups.length);
70
+ expect(arrows).toHaveLength(0);
71
+ expect(groups.length).toBeGreaterThan(0);
72
+ });
73
+
74
+ it('renders each magnetic point exactly once on mount (no version cascade)', () => {
75
+ const data = buildData(POINT_COUNT);
76
+
77
+ render(
78
+ <Wrapper>
79
+ <LineChart
80
+ series={[{ id: 's', stroke: '#000', data }]}
81
+ width={400}
82
+ height={200}
83
+ animate={false}
84
+ >
85
+ {data.map((value, index) => (
86
+ <Profiler
87
+ key={index}
88
+ id={`point-${index}`}
89
+ onRender={onPointRender}
90
+ >
91
+ <Point magnetic dataX={index} dataY={value} />
92
+ </Profiler>
93
+ ))}
94
+ </LineChart>
95
+ </Wrapper>,
96
+ );
97
+
98
+ expect(pointMountCommits).toBe(POINT_COUNT);
99
+ expect(pointUpdateCommits).toBe(0);
100
+ });
101
+ });
@@ -127,17 +127,24 @@ export const LineChart = ({
127
127
  emptyLabel = 'No data',
128
128
  connectNulls,
129
129
  }: Readonly<LineChartProps>) => {
130
- const xAxisConfig = {
131
- ...defaultXAxisProps,
132
- ...xAxis,
133
- position: xAxis?.position ?? defaultXAxisProps.position,
134
- };
135
- const yAxisConfig = {
136
- ...defaultYAxisProps,
137
- ...yAxis,
138
- position: yAxis?.position ?? defaultYAxisProps.position,
139
- width: yAxis?.width ?? defaultYAxisProps.width,
140
- };
130
+ const xAxisConfig = useMemo(
131
+ () => ({
132
+ ...defaultXAxisProps,
133
+ ...xAxis,
134
+ position: xAxis?.position ?? defaultXAxisProps.position,
135
+ }),
136
+ [xAxis],
137
+ );
138
+
139
+ const yAxisConfig = useMemo(
140
+ () => ({
141
+ ...defaultYAxisProps,
142
+ ...yAxis,
143
+ position: yAxis?.position ?? defaultYAxisProps.position,
144
+ width: yAxis?.width ?? defaultYAxisProps.width,
145
+ }),
146
+ [yAxis],
147
+ );
141
148
 
142
149
  const xAxisPosition = xAxisConfig.position;
143
150
  const yAxisPosition = yAxisConfig.position;
@@ -1,4 +1,6 @@
1
1
  import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+
3
+ import { memo } from 'react';
2
4
  import Animated from 'react-native-reanimated';
3
5
  import { Circle, G, Polygon, Text as SvgText } from 'react-native-svg';
4
6
 
@@ -9,6 +11,7 @@ import type {
9
11
  PointMarkerProps,
10
12
  PointProps,
11
13
  } from './types';
14
+
12
15
  import { usePointGeometry } from './usePointGeometry';
13
16
  import { buildArrowPoints, computeLabelGeometry, resolveLabel } from './utils';
14
17
 
@@ -62,7 +65,7 @@ function PointArrow({ x, y, size, position }: Readonly<PointArrowProps>) {
62
65
  );
63
66
  }
64
67
 
65
- export function Point({
68
+ export const Point = memo(function Point({
66
69
  dataX,
67
70
  dataY,
68
71
  color,
@@ -127,4 +130,4 @@ export function Point({
127
130
  )}
128
131
  </AnimatedG>
129
132
  );
130
- }
133
+ });
@@ -2,13 +2,25 @@ import { describe, expect, it } from '@jest/globals';
2
2
  import { renderHook, act } from '@testing-library/react-native';
3
3
  import type { ReactNode } from 'react';
4
4
 
5
- import { useMagneticPointsContext } from './magneticPointsContext';
5
+ import {
6
+ useMagneticRegistry,
7
+ useMagneticSnapshot,
8
+ } from './magneticPointsContext';
6
9
  import { MagneticPointsProvider } from './MagneticPointsProvider';
7
10
 
8
11
  const wrapper = ({ children }: { children: ReactNode }) => (
9
12
  <MagneticPointsProvider>{children}</MagneticPointsProvider>
10
13
  );
11
14
 
15
+ /**
16
+ * Reads both split contexts so the existing assertions (register/unregister/
17
+ * getMagneticPoints/version) keep working against a single result object.
18
+ */
19
+ const useMagneticPointsContext = () => ({
20
+ ...useMagneticRegistry(),
21
+ ...useMagneticSnapshot(),
22
+ });
23
+
12
24
  describe('MagneticPointsProvider', () => {
13
25
  it('starts with an empty set', () => {
14
26
  const { result } = renderHook(() => useMagneticPointsContext(), {
@@ -2,8 +2,10 @@ import { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import type { ReactNode } from 'react';
3
3
 
4
4
  import {
5
- MagneticPointsContextProvider,
6
- type MagneticPointsContextValue,
5
+ MagneticRegistryProvider,
6
+ MagneticSnapshotProvider,
7
+ type MagneticRegistryValue,
8
+ type MagneticSnapshotValue,
7
9
  } from './magneticPointsContext';
8
10
 
9
11
  type MagneticPointsProviderProps = {
@@ -32,14 +34,24 @@ export function MagneticPointsProvider({
32
34
  return pointsRef.current;
33
35
  }, []);
34
36
 
35
- const value: MagneticPointsContextValue = useMemo(
36
- () => ({ register, unregister, getMagneticPoints, version }),
37
- [register, unregister, getMagneticPoints, version],
37
+ // Stable identity for the provider's lifetime: `Point` consumers read this
38
+ // and therefore never re-render when the magnetic set changes.
39
+ const registry: MagneticRegistryValue = useMemo(
40
+ () => ({ register, unregister }),
41
+ [register, unregister],
42
+ );
43
+
44
+ // Changes on every register/unregister: consumed only by the scrubber.
45
+ const snapshot: MagneticSnapshotValue = useMemo(
46
+ () => ({ version, getMagneticPoints }),
47
+ [version, getMagneticPoints],
38
48
  );
39
49
 
40
50
  return (
41
- <MagneticPointsContextProvider value={value}>
42
- {children}
43
- </MagneticPointsContextProvider>
51
+ <MagneticRegistryProvider value={registry}>
52
+ <MagneticSnapshotProvider value={snapshot}>
53
+ {children}
54
+ </MagneticSnapshotProvider>
55
+ </MagneticRegistryProvider>
44
56
  );
45
57
  }
@@ -1,5 +1,7 @@
1
1
  export {
2
- useMagneticPointsContext,
3
- type MagneticPointsContextValue,
2
+ useMagneticRegistry,
3
+ useMagneticSnapshot,
4
+ type MagneticRegistryValue,
5
+ type MagneticSnapshotValue,
4
6
  } from './magneticPointsContext';
5
7
  export { MagneticPointsProvider } from './MagneticPointsProvider';
@@ -1,19 +1,41 @@
1
1
  import { createSafeContext } from '@ledgerhq/lumen-utils-shared';
2
2
 
3
- export type MagneticPointsContextValue = {
3
+ /**
4
+ * Stable registration API for magnetic points. Its identity never changes for
5
+ * the provider's lifetime, so consumers (every `Point`) do not re-render when
6
+ * the magnetic set changes — only when they mount/unmount.
7
+ */
8
+ export type MagneticRegistryValue = {
4
9
  register: (dataIndex: number) => void;
5
10
  unregister: (dataIndex: number) => void;
11
+ };
12
+
13
+ /**
14
+ * Mutable snapshot of the registered magnetic points. Changes on every
15
+ * register/unregister (via `version`) and is consumed only by the scrubber,
16
+ * which needs to recompute its snap targets when the set changes.
17
+ */
18
+ export type MagneticSnapshotValue = {
6
19
  version: number;
7
20
  getMagneticPoints: () => ReadonlySet<number>;
8
21
  };
9
22
 
10
- const [MagneticPointsContextProvider, _useMagneticPointsSafeContext] =
11
- createSafeContext<MagneticPointsContextValue>('MagneticPoints');
23
+ const [MagneticRegistryProvider, _useMagneticRegistrySafeContext] =
24
+ createSafeContext<MagneticRegistryValue>('MagneticRegistry');
25
+
26
+ const [MagneticSnapshotProvider, _useMagneticSnapshotSafeContext] =
27
+ createSafeContext<MagneticSnapshotValue>('MagneticSnapshot');
28
+
29
+ export const useMagneticRegistry = (): MagneticRegistryValue =>
30
+ _useMagneticRegistrySafeContext({
31
+ consumerName: 'useMagneticRegistry',
32
+ contextRequired: true,
33
+ });
12
34
 
13
- export const useMagneticPointsContext = (): MagneticPointsContextValue =>
14
- _useMagneticPointsSafeContext({
15
- consumerName: 'useMagneticPointsContext',
35
+ export const useMagneticSnapshot = (): MagneticSnapshotValue =>
36
+ _useMagneticSnapshotSafeContext({
37
+ consumerName: 'useMagneticSnapshot',
16
38
  contextRequired: true,
17
39
  });
18
40
 
19
- export { MagneticPointsContextProvider };
41
+ export { MagneticRegistryProvider, MagneticSnapshotProvider };
@@ -5,8 +5,8 @@ import type { DrawingArea } from '../../utils/types';
5
5
  import type { BaseAxisProps } from '../Axis';
6
6
  import { useCartesianChartContext } from '../CartesianChart/context';
7
7
  import { useRevealFadeProps } from '../CartesianChart/RevealAnimation';
8
- import { useMagneticPointsContext } from './pointContext';
9
- import type { MagneticPointsContextValue } from './pointContext/magneticPointsContext';
8
+ import { useMagneticRegistry } from './pointContext';
9
+ import type { MagneticRegistryValue } from './pointContext/magneticPointsContext';
10
10
  import { isWithinBounds, resolveDataXToIndex } from './utils';
11
11
 
12
12
  type Pixel = { x: number; y: number };
@@ -37,9 +37,9 @@ export const usePointGeometry = ({
37
37
  }: UsePointGeometryParams): PointGeometry => {
38
38
  const { getXScale, getYScale, getXAxisConfig, drawingArea } =
39
39
  useCartesianChartContext();
40
- const magneticContext = useMagneticPointsContext();
40
+ const magneticRegistry = useMagneticRegistry();
41
41
 
42
- useMagneticRegistration(magnetic, dataX, getXAxisConfig, magneticContext);
42
+ useMagneticRegistration(magnetic, dataX, getXAxisConfig, magneticRegistry);
43
43
 
44
44
  const xScale = getXScale();
45
45
  const yScale = getYScale();
@@ -65,7 +65,7 @@ export const useMagneticRegistration = (
65
65
  magnetic: boolean,
66
66
  dataX: number,
67
67
  getXAxisConfig: () => BaseAxisProps | undefined,
68
- { register, unregister }: MagneticPointsContextValue,
68
+ { register, unregister }: MagneticRegistryValue,
69
69
  ): void => {
70
70
  useEffect(() => {
71
71
  if (!magnetic) return;
@@ -2,12 +2,12 @@ import { triggerHapticFeedback } from '@ledgerhq/lumen-ui-rnative';
2
2
  import { useCallback, useMemo, useRef, useState } from 'react';
3
3
  import { StyleSheet, View } from 'react-native';
4
4
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
5
- import Animated, { useSharedValue } from 'react-native-reanimated';
5
+ import Animated from 'react-native-reanimated';
6
6
  import { scheduleOnRN } from 'react-native-worklets';
7
7
 
8
8
  import { clamp } from '../../utils/numbers';
9
9
  import { useCartesianChartContext } from '../CartesianChart/context';
10
- import { useMagneticPointsContext } from '../Point/pointContext';
10
+ import { useMagneticSnapshot } from '../Point/pointContext';
11
11
  import { ScrubberContextProvider } from './context';
12
12
  import type { ScrubberProviderProps } from './types';
13
13
  import {
@@ -17,7 +17,11 @@ import {
17
17
  buildSortedMagnets,
18
18
  } from './utils';
19
19
 
20
- const LONG_PRESS_MIN_DURATION_MS = 50;
20
+ const LONG_PRESS_MIN_DURATION_MS = 100;
21
+
22
+ // Travel (px) past which a horizontal drag scrubs and a vertical drag fails
23
+ // the pan (yielding to an ancestor scroll view). Also bounds the long press.
24
+ const PAN_AXIS_THRESHOLD_PX = 10;
21
25
 
22
26
  /**
23
27
  * Provides scrubbing interaction for a chart.
@@ -27,10 +31,9 @@ const LONG_PRESS_MIN_DURATION_MS = 50;
27
31
  * gesture-capture `View` overlay). `CartesianChart` handles this positioning
28
32
  * automatically when `enableScrubbing` is true.
29
33
  *
30
- * Activation requires a 50 ms long press before panning begins. This prevents
31
- * accidental scrubbing when the user is scrolling a parent `ScrollView` or
32
- * `FlatList`. The `isScrubbing` shared value gates the pan via manual activation
33
- * so scroll and scrub gestures never compete.
34
+ * Scrubbing engages on horizontal panning (`activeOffsetX`) or a press-and-hold
35
+ * (`LongPress`); vertical movement fails the pan (`failOffsetY`) so an ancestor
36
+ * vertical scroll view keeps scrolling.
34
37
  */
35
38
  export function ScrubberProvider({
36
39
  children,
@@ -46,13 +49,11 @@ export function ScrubberProvider({
46
49
  >();
47
50
 
48
51
  const { getXScale, getXAxisConfig, dataLength } = useCartesianChartContext();
49
- const { getMagneticPoints, version } = useMagneticPointsContext();
52
+ const { getMagneticPoints, version } = useMagneticSnapshot();
50
53
 
51
- // All values touched by the gesture's JS-thread callback live in a single
52
- // ref. Reading via `latest.current` keeps the callbacks reference-stable
53
- // across data, scale, and prop updates, which in turn keeps `composed` and
54
- // the gesture surface JSX stable so re-rendering the provider on each
55
- // index change never tears down the GestureDetector or the View tree.
54
+ // Gesture callbacks read everything through this ref so they stay
55
+ // reference-stable, keeping `composed` and the surface JSX from re-creating
56
+ // (which would tear down the GestureDetector) on each index change.
56
57
  const sortedMagnets = useMemo(() => {
57
58
  const magneticIndices = getMagneticPoints();
58
59
  return buildSortedMagnets({
@@ -140,43 +141,35 @@ export function ScrubberProvider({
140
141
  [setScrubberPositionAndNotify],
141
142
  );
142
143
 
143
- const isScrubbing = useSharedValue(false);
144
-
145
144
  const resetScrubber = useCallback((): void => {
146
145
  'worklet';
147
- isScrubbing.value = false;
148
146
  scheduleOnRN(handlePositionChange, null);
149
- }, [isScrubbing, handlePositionChange]);
147
+ }, [handlePositionChange]);
150
148
 
151
149
  const composed = useMemo(() => {
152
150
  const longPress = Gesture.LongPress()
153
151
  .minDuration(LONG_PRESS_MIN_DURATION_MS)
152
+ .maxDistance(PAN_AXIS_THRESHOLD_PX)
154
153
  .onStart((e) => {
155
154
  'worklet';
156
- isScrubbing.value = true;
157
155
  scheduleOnRN(handlePositionChange, e.x);
158
- });
156
+ })
157
+ .onEnd(resetScrubber);
159
158
 
160
159
  const pan = Gesture.Pan()
161
- .manualActivation(true)
162
- .onTouchesMove((_, manager) => {
163
- 'worklet';
164
- if (isScrubbing.value) manager.activate();
165
- })
160
+ .activeOffsetX([-PAN_AXIS_THRESHOLD_PX, PAN_AXIS_THRESHOLD_PX])
161
+ .failOffsetY([-PAN_AXIS_THRESHOLD_PX, PAN_AXIS_THRESHOLD_PX])
166
162
  .onUpdate((e) => {
167
163
  'worklet';
168
164
  scheduleOnRN(handlePositionChange, e.x);
169
165
  })
170
- .onEnd(resetScrubber)
171
166
  .onFinalize(resetScrubber);
172
167
 
173
168
  return Gesture.Simultaneous(longPress, pan);
174
- }, [isScrubbing, handlePositionChange, resetScrubber]);
169
+ }, [handlePositionChange, resetScrubber]);
175
170
 
176
- // Memoizing the surface lets React's element-identity bailout skip
177
- // reconciling the View / GestureDetector / chart `children` subtree when
178
- // only `scrubberPosition` changes. The Scrubber consumer still updates via
179
- // context as expected.
171
+ // Memoized so a `scrubberPosition` change doesn't re-render the chart
172
+ // subtree; the Scrubber still updates through context.
180
173
  const surface = useMemo(
181
174
  () => (
182
175
  <View style={[{ width, height }, style]}>