@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.
- package/dist/module/lib/Components/Line/Line.js +4 -4
- package/dist/module/lib/Components/Line/Line.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.js +4 -4
- package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.performance.test.js +99 -0
- package/dist/module/lib/Components/LineChart/LineChart.performance.test.js.map +1 -0
- package/dist/module/lib/Components/Point/Point.js +3 -2
- package/dist/module/lib/Components/Point/Point.js.map +1 -1
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js +19 -9
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js.map +1 -1
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js +10 -1
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js.map +1 -1
- package/dist/module/lib/Components/Point/pointContext/index.js +1 -1
- package/dist/module/lib/Components/Point/pointContext/index.js.map +1 -1
- package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js +22 -4
- package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js.map +1 -1
- package/dist/module/lib/Components/Point/usePointGeometry.js +3 -3
- package/dist/module/lib/Components/Point/usePointGeometry.js.map +1 -1
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +22 -30
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
- package/dist/typescript/src/lib/Components/Line/Line.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/Point.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +22 -5
- package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts +2 -2
- package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +3 -4
- package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/lib/Components/Line/Line.tsx +4 -4
- package/src/lib/Components/LineChart/LineChart.performance.test.tsx +101 -0
- package/src/lib/Components/LineChart/LineChart.tsx +18 -11
- package/src/lib/Components/Point/Point.tsx +5 -2
- package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +13 -1
- package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +20 -8
- package/src/lib/Components/Point/pointContext/index.ts +4 -2
- package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +29 -7
- package/src/lib/Components/Point/usePointGeometry.ts +5 -5
- 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:
|
|
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,
|
|
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,
|
|
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
|
|
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":"
|
|
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"}
|
package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map
CHANGED
|
@@ -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;
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
|
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:
|
|
25
|
+
value: MagneticSnapshotValue;
|
|
10
26
|
}>;
|
|
11
|
-
export declare const
|
|
12
|
-
export
|
|
27
|
+
export declare const useMagneticRegistry: () => MagneticRegistryValue;
|
|
28
|
+
export declare const useMagneticSnapshot: () => MagneticSnapshotValue;
|
|
29
|
+
export { MagneticRegistryProvider, MagneticSnapshotProvider };
|
|
13
30
|
//# sourceMappingURL=magneticPointsContext.d.ts.map
|
package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"magneticPointsContext.d.ts","sourceRoot":"","sources":["../../../../../../../src/lib/Components/Point/pointContext/magneticPointsContext.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,
|
|
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 {
|
|
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 }:
|
|
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,
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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;
|
|
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.
|
|
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.
|
|
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.
|
|
48
|
-
"@ledgerhq/lumen-ui-rnative": "0.1.
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 {
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
<
|
|
42
|
-
{
|
|
43
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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 [
|
|
11
|
-
createSafeContext<
|
|
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
|
|
14
|
-
|
|
15
|
-
consumerName: '
|
|
35
|
+
export const useMagneticSnapshot = (): MagneticSnapshotValue =>
|
|
36
|
+
_useMagneticSnapshotSafeContext({
|
|
37
|
+
consumerName: 'useMagneticSnapshot',
|
|
16
38
|
contextRequired: true,
|
|
17
39
|
});
|
|
18
40
|
|
|
19
|
-
export {
|
|
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 {
|
|
9
|
-
import type {
|
|
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
|
|
40
|
+
const magneticRegistry = useMagneticRegistry();
|
|
41
41
|
|
|
42
|
-
useMagneticRegistration(magnetic, dataX, getXAxisConfig,
|
|
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 }:
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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 } =
|
|
52
|
+
const { getMagneticPoints, version } = useMagneticSnapshot();
|
|
50
53
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
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
|
-
}, [
|
|
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
|
-
.
|
|
162
|
-
.
|
|
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
|
-
}, [
|
|
169
|
+
}, [handlePositionChange, resetScrubber]);
|
|
175
170
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
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]}>
|