@ledgerhq/lumen-ui-rnative-visualization 0.1.22 → 0.1.24
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 +81 -50
- package/dist/module/lib/Components/Point/Point.js.map +1 -1
- package/dist/module/lib/Components/Point/Point.test.js +41 -0
- package/dist/module/lib/Components/Point/Point.test.js.map +1 -1
- package/dist/module/lib/Components/Point/constants.js +2 -0
- package/dist/module/lib/Components/Point/constants.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 +60 -0
- package/dist/module/lib/Components/Point/usePointGeometry.js.map +1 -0
- package/dist/module/lib/Components/Point/usePointGeometry.test.js +86 -0
- package/dist/module/lib/Components/Point/usePointGeometry.test.js.map +1 -0
- package/dist/module/lib/Components/Point/utils.js +58 -15
- package/dist/module/lib/Components/Point/utils.js.map +1 -1
- package/dist/module/lib/Components/Point/utils.test.js +37 -75
- package/dist/module/lib/Components/Point/utils.test.js.map +1 -1
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +2 -2
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
- package/dist/module/lib/utils/domain/domain.js +4 -4
- package/dist/module/lib/utils/domain/domain.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/constants.d.ts +2 -0
- package/dist/typescript/src/lib/Components/Point/constants.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/types.d.ts +32 -0
- package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts +33 -0
- package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Point/utils.d.ts +30 -4
- package/dist/typescript/src/lib/Components/Point/utils.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.test.tsx +46 -0
- package/src/lib/Components/Point/Point.tsx +79 -57
- package/src/lib/Components/Point/constants.ts +2 -0
- 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/types.ts +41 -0
- package/src/lib/Components/Point/usePointGeometry.test.ts +95 -0
- package/src/lib/Components/Point/usePointGeometry.ts +77 -0
- package/src/lib/Components/Point/utils.test.ts +34 -80
- package/src/lib/Components/Point/utils.ts +77 -19
- package/src/lib/Components/Scrubber/ScrubberProvider.tsx +2 -2
- package/src/lib/utils/domain/domain.ts +4 -4
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { useTheme } from '@ledgerhq/lumen-ui-rnative';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { memo } from 'react';
|
|
3
4
|
import Animated from 'react-native-reanimated';
|
|
4
5
|
import { Circle, G, Polygon, Text as SvgText } from 'react-native-svg';
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import { DEFAULT_SIZE, LABEL_FONT_SIZE, STROKE_WIDTH } from './constants';
|
|
8
|
+
import type {
|
|
9
|
+
PointArrowProps,
|
|
10
|
+
PointLabelProps,
|
|
11
|
+
PointMarkerProps,
|
|
12
|
+
PointProps,
|
|
13
|
+
} from './types';
|
|
11
14
|
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
buildArrowPoints,
|
|
15
|
-
computeLabelY,
|
|
16
|
-
isWithinBounds,
|
|
17
|
-
resolveLabel,
|
|
18
|
-
useMagneticRegistration,
|
|
19
|
-
} from './utils';
|
|
15
|
+
import { usePointGeometry } from './usePointGeometry';
|
|
16
|
+
import { buildArrowPoints, computeLabelGeometry, resolveLabel } from './utils';
|
|
20
17
|
|
|
21
18
|
const AnimatedG = Animated.createAnimatedComponent(G);
|
|
22
19
|
|
|
@@ -30,7 +27,7 @@ export function PointLabel({
|
|
|
30
27
|
<SvgText
|
|
31
28
|
textAnchor={textAnchor}
|
|
32
29
|
fill={theme.colors.text.base}
|
|
33
|
-
fontSize={
|
|
30
|
+
fontSize={LABEL_FONT_SIZE}
|
|
34
31
|
fontWeight={theme.typographies.body4.fontWeight}
|
|
35
32
|
fontFamily={theme.fontFamilies.sans}
|
|
36
33
|
{...props}
|
|
@@ -38,7 +35,37 @@ export function PointLabel({
|
|
|
38
35
|
);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
function PointMarker({ x, y, size, color }: Readonly<PointMarkerProps>) {
|
|
39
|
+
const { theme } = useTheme();
|
|
40
|
+
const radius = size / 2;
|
|
41
|
+
const fill = color ?? theme.colors.bg.mutedStrong;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Circle
|
|
45
|
+
testID='point-circle'
|
|
46
|
+
cx={x}
|
|
47
|
+
cy={y}
|
|
48
|
+
r={radius}
|
|
49
|
+
fill={fill}
|
|
50
|
+
stroke={theme.colors.bg.canvas}
|
|
51
|
+
strokeWidth={STROKE_WIDTH}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function PointArrow({ x, y, size, position }: Readonly<PointArrowProps>) {
|
|
57
|
+
const { theme } = useTheme();
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Polygon
|
|
61
|
+
testID='point-arrow'
|
|
62
|
+
points={buildArrowPoints(x, y, size / 2, position)}
|
|
63
|
+
fill={theme.colors.text.base}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const Point = memo(function Point({
|
|
42
69
|
dataX,
|
|
43
70
|
dataY,
|
|
44
71
|
color,
|
|
@@ -50,62 +77,57 @@ export function Point({
|
|
|
50
77
|
size = DEFAULT_SIZE,
|
|
51
78
|
onPress,
|
|
52
79
|
magnetic = false,
|
|
80
|
+
labelAlignment = 'auto',
|
|
53
81
|
}: Readonly<PointProps>) {
|
|
54
|
-
const {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
useMagneticRegistration(magnetic, dataX, getXAxisConfig, magneticContext);
|
|
60
|
-
const { theme } = useTheme();
|
|
61
|
-
|
|
62
|
-
const xScale = getXScale();
|
|
63
|
-
const yScale = getYScale();
|
|
82
|
+
const { pixel, drawingArea, fadeProps, isVisible } = usePointGeometry({
|
|
83
|
+
dataX,
|
|
84
|
+
dataY,
|
|
85
|
+
magnetic,
|
|
86
|
+
});
|
|
64
87
|
|
|
65
|
-
|
|
66
|
-
const fill = color ?? theme.colors.bg.mutedStrong;
|
|
67
|
-
|
|
68
|
-
const pixel = useMemo(() => {
|
|
69
|
-
if (!xScale || !yScale) return undefined;
|
|
70
|
-
return projectPoint(dataX, dataY, xScale, yScale);
|
|
71
|
-
}, [dataX, dataY, xScale, yScale]);
|
|
72
|
-
|
|
73
|
-
if (!pixel || !isWithinBounds(pixel.x, pixel.y, drawingArea)) {
|
|
88
|
+
if (!isVisible || !pixel) {
|
|
74
89
|
return null;
|
|
75
90
|
}
|
|
76
91
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
92
|
+
const labelText = resolveLabel(label, dataX);
|
|
93
|
+
const isLabelVisible = labelText !== undefined;
|
|
94
|
+
const labelGeometry = isLabelVisible
|
|
95
|
+
? computeLabelGeometry({
|
|
96
|
+
text: labelText,
|
|
97
|
+
pixelX: pixel.x,
|
|
98
|
+
pixelY: pixel.y,
|
|
99
|
+
size,
|
|
100
|
+
labelPosition,
|
|
101
|
+
showLabelArrow,
|
|
102
|
+
area: drawingArea,
|
|
103
|
+
alignment: labelAlignment,
|
|
104
|
+
})
|
|
105
|
+
: null;
|
|
81
106
|
|
|
82
107
|
const Label = LabelComponent ?? PointLabel;
|
|
83
108
|
|
|
84
109
|
return (
|
|
85
110
|
<AnimatedG testID='point-group' onPress={onPress} animatedProps={fadeProps}>
|
|
86
111
|
{!hidePoint && (
|
|
87
|
-
<
|
|
88
|
-
testID='point-circle'
|
|
89
|
-
cx={pixel.x}
|
|
90
|
-
cy={pixel.y}
|
|
91
|
-
r={radius}
|
|
92
|
-
fill={fill}
|
|
93
|
-
stroke={theme.colors.bg.canvas}
|
|
94
|
-
strokeWidth={STROKE_WIDTH}
|
|
95
|
-
/>
|
|
112
|
+
<PointMarker x={pixel.x} y={pixel.y} size={size} color={color} />
|
|
96
113
|
)}
|
|
97
|
-
{
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
114
|
+
{isLabelVisible && showLabelArrow && (
|
|
115
|
+
<PointArrow
|
|
116
|
+
x={pixel.x}
|
|
117
|
+
y={pixel.y}
|
|
118
|
+
size={size}
|
|
119
|
+
position={labelPosition}
|
|
102
120
|
/>
|
|
103
121
|
)}
|
|
104
|
-
{
|
|
105
|
-
<Label
|
|
106
|
-
{
|
|
122
|
+
{labelText != null && labelGeometry && (
|
|
123
|
+
<Label
|
|
124
|
+
x={labelGeometry.x}
|
|
125
|
+
y={labelGeometry.y}
|
|
126
|
+
textAnchor={labelGeometry.textAnchor}
|
|
127
|
+
>
|
|
128
|
+
{labelText}
|
|
107
129
|
</Label>
|
|
108
130
|
)}
|
|
109
131
|
</AnimatedG>
|
|
110
132
|
);
|
|
111
|
-
}
|
|
133
|
+
});
|
|
@@ -4,3 +4,5 @@ export const LABEL_FONT_SIZE = 10;
|
|
|
4
4
|
export const ARROW_WIDTH = 6;
|
|
5
5
|
export const ARROW_HEIGHT = 4;
|
|
6
6
|
export const GAP = 4;
|
|
7
|
+
/** Approximate width of a character in the label font, as a ratio of the font size. Used to compute the label width. */
|
|
8
|
+
export const LABEL_CHAR_WIDTH_RATIO = 0.6;
|
|
@@ -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 };
|
|
@@ -10,6 +10,38 @@ export type PointLabelProps = {
|
|
|
10
10
|
|
|
11
11
|
export type PointLabelComponent = ComponentType<PointLabelProps>;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Horizontal alignment strategy for a point's label.
|
|
15
|
+
*
|
|
16
|
+
* - `'auto'`: keep the label inside the drawing area, anchoring it to the
|
|
17
|
+
* nearest edge when it would overflow.
|
|
18
|
+
* - `'center'`: always centre the label on the point.
|
|
19
|
+
*/
|
|
20
|
+
export type LabelAlignment = 'center' | 'auto';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pixel position and styling inputs shared by the point's rendered glyphs. Each
|
|
24
|
+
* glyph picks the subset it needs and derives its own pixel geometry (e.g.
|
|
25
|
+
* radius from `size`).
|
|
26
|
+
*/
|
|
27
|
+
type PointGlyphProps = {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
size: number;
|
|
31
|
+
color?: string;
|
|
32
|
+
position: 'top' | 'bottom';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type PointMarkerProps = Pick<
|
|
36
|
+
PointGlyphProps,
|
|
37
|
+
'x' | 'y' | 'size' | 'color'
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
export type PointArrowProps = Pick<
|
|
41
|
+
PointGlyphProps,
|
|
42
|
+
'x' | 'y' | 'size' | 'position'
|
|
43
|
+
>;
|
|
44
|
+
|
|
13
45
|
export type PointProps = {
|
|
14
46
|
/**
|
|
15
47
|
* X coordinate in data space (index or explicit value).
|
|
@@ -80,4 +112,13 @@ export type PointProps = {
|
|
|
80
112
|
* @default false
|
|
81
113
|
*/
|
|
82
114
|
magnetic?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Horizontal alignment of the label relative to the chart's drawing area.
|
|
117
|
+
* With `'auto'`, a label that would overflow the left/right edge is anchored
|
|
118
|
+
* to that edge and grows inward instead of being clipped, while the arrow
|
|
119
|
+
* keeps pointing at the exact data point. Use `'center'` to always centre the
|
|
120
|
+
* label on the point.
|
|
121
|
+
* @default 'auto'
|
|
122
|
+
*/
|
|
123
|
+
labelAlignment?: LabelAlignment;
|
|
83
124
|
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
2
|
+
import { renderHook } from '@testing-library/react-native';
|
|
3
|
+
|
|
4
|
+
import type { BaseAxisProps } from '../Axis';
|
|
5
|
+
import { useMagneticRegistration } from './usePointGeometry';
|
|
6
|
+
|
|
7
|
+
const makeContext = () => ({
|
|
8
|
+
register: jest.fn(),
|
|
9
|
+
unregister: jest.fn(),
|
|
10
|
+
version: 0,
|
|
11
|
+
getMagneticPoints: () => new Set<number>(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const noAxisConfig = (): BaseAxisProps | undefined => undefined;
|
|
15
|
+
|
|
16
|
+
describe('useMagneticRegistration', () => {
|
|
17
|
+
it('registers the data index when magnetic is true', () => {
|
|
18
|
+
const ctx = makeContext();
|
|
19
|
+
renderHook(() => useMagneticRegistration(true, 3, noAxisConfig, ctx));
|
|
20
|
+
|
|
21
|
+
expect(ctx.register).toHaveBeenCalledWith(3);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not register when magnetic is false', () => {
|
|
25
|
+
const ctx = makeContext();
|
|
26
|
+
renderHook(() => useMagneticRegistration(false, 3, noAxisConfig, ctx));
|
|
27
|
+
|
|
28
|
+
expect(ctx.register).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('unregisters on unmount', () => {
|
|
32
|
+
const ctx = makeContext();
|
|
33
|
+
const { unmount } = renderHook(() =>
|
|
34
|
+
useMagneticRegistration(true, 3, noAxisConfig, ctx),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
unmount();
|
|
38
|
+
expect(ctx.unregister).toHaveBeenCalledWith(3);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('resolves dataX through axis config data when provided', () => {
|
|
42
|
+
const ctx = makeContext();
|
|
43
|
+
const getXAxisConfig = (): BaseAxisProps => ({
|
|
44
|
+
scaleType: 'band',
|
|
45
|
+
data: [100, 200, 300],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
renderHook(() => useMagneticRegistration(true, 200, getXAxisConfig, ctx));
|
|
49
|
+
|
|
50
|
+
expect(ctx.register).toHaveBeenCalledWith(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('does not register when axis data exists but does not contain the value', () => {
|
|
54
|
+
const ctx = makeContext();
|
|
55
|
+
const getXAxisConfig = (): BaseAxisProps => ({
|
|
56
|
+
scaleType: 'band',
|
|
57
|
+
data: [100, 200, 300],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
renderHook(() => useMagneticRegistration(true, 999, getXAxisConfig, ctx));
|
|
61
|
+
|
|
62
|
+
expect(ctx.register).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('re-registers when dataX changes', () => {
|
|
66
|
+
const ctx = makeContext();
|
|
67
|
+
const { rerender } = renderHook(
|
|
68
|
+
({ dataX }: { dataX: number }) =>
|
|
69
|
+
useMagneticRegistration(true, dataX, noAxisConfig, ctx),
|
|
70
|
+
{ initialProps: { dataX: 2 } },
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(ctx.register).toHaveBeenCalledWith(2);
|
|
74
|
+
|
|
75
|
+
rerender({ dataX: 4 });
|
|
76
|
+
|
|
77
|
+
expect(ctx.unregister).toHaveBeenCalledWith(2);
|
|
78
|
+
expect(ctx.register).toHaveBeenCalledWith(4);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('unregisters and stops when magnetic switches to false', () => {
|
|
82
|
+
const ctx = makeContext();
|
|
83
|
+
const { rerender } = renderHook(
|
|
84
|
+
({ magnetic }: { magnetic: boolean }) =>
|
|
85
|
+
useMagneticRegistration(magnetic, 3, noAxisConfig, ctx),
|
|
86
|
+
{ initialProps: { magnetic: true } },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(ctx.register).toHaveBeenCalledWith(3);
|
|
90
|
+
|
|
91
|
+
rerender({ magnetic: false });
|
|
92
|
+
|
|
93
|
+
expect(ctx.unregister).toHaveBeenCalledWith(3);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import { projectPoint } from '../../utils/scales/scales';
|
|
4
|
+
import type { DrawingArea } from '../../utils/types';
|
|
5
|
+
import type { BaseAxisProps } from '../Axis';
|
|
6
|
+
import { useCartesianChartContext } from '../CartesianChart/context';
|
|
7
|
+
import { useRevealFadeProps } from '../CartesianChart/RevealAnimation';
|
|
8
|
+
import { useMagneticRegistry } from './pointContext';
|
|
9
|
+
import type { MagneticRegistryValue } from './pointContext/magneticPointsContext';
|
|
10
|
+
import { isWithinBounds, resolveDataXToIndex } from './utils';
|
|
11
|
+
|
|
12
|
+
type Pixel = { x: number; y: number };
|
|
13
|
+
|
|
14
|
+
type UsePointGeometryParams = {
|
|
15
|
+
dataX: number;
|
|
16
|
+
dataY: number;
|
|
17
|
+
magnetic: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type PointGeometry = {
|
|
21
|
+
pixel: Pixel | undefined;
|
|
22
|
+
drawingArea: DrawingArea;
|
|
23
|
+
fadeProps: ReturnType<typeof useRevealFadeProps>;
|
|
24
|
+
isVisible: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolves a point's chart geometry and behaviour: projects its data
|
|
29
|
+
* coordinates to pixels, registers it as a magnetic snap target when enabled,
|
|
30
|
+
* exposes the reveal-animation props, and reports whether it falls inside the
|
|
31
|
+
* drawing area.
|
|
32
|
+
*/
|
|
33
|
+
export const usePointGeometry = ({
|
|
34
|
+
dataX,
|
|
35
|
+
dataY,
|
|
36
|
+
magnetic,
|
|
37
|
+
}: UsePointGeometryParams): PointGeometry => {
|
|
38
|
+
const { getXScale, getYScale, getXAxisConfig, drawingArea } =
|
|
39
|
+
useCartesianChartContext();
|
|
40
|
+
const magneticRegistry = useMagneticRegistry();
|
|
41
|
+
|
|
42
|
+
useMagneticRegistration(magnetic, dataX, getXAxisConfig, magneticRegistry);
|
|
43
|
+
|
|
44
|
+
const xScale = getXScale();
|
|
45
|
+
const yScale = getYScale();
|
|
46
|
+
|
|
47
|
+
const pixel = useMemo(() => {
|
|
48
|
+
if (!xScale || !yScale) return undefined;
|
|
49
|
+
return projectPoint(dataX, dataY, xScale, yScale);
|
|
50
|
+
}, [dataX, dataY, xScale, yScale]);
|
|
51
|
+
|
|
52
|
+
const fadeProps = useRevealFadeProps();
|
|
53
|
+
|
|
54
|
+
const isVisible =
|
|
55
|
+
pixel !== undefined && isWithinBounds(pixel.x, pixel.y, drawingArea);
|
|
56
|
+
|
|
57
|
+
return { pixel, drawingArea, fadeProps, isVisible };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registers/unregisters a data index as a magnetic snap target
|
|
62
|
+
* when the Point has `magnetic` enabled.
|
|
63
|
+
*/
|
|
64
|
+
export const useMagneticRegistration = (
|
|
65
|
+
magnetic: boolean,
|
|
66
|
+
dataX: number,
|
|
67
|
+
getXAxisConfig: () => BaseAxisProps | undefined,
|
|
68
|
+
{ register, unregister }: MagneticRegistryValue,
|
|
69
|
+
): void => {
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!magnetic) return;
|
|
72
|
+
const index = resolveDataXToIndex(dataX, getXAxisConfig());
|
|
73
|
+
if (index === undefined) return;
|
|
74
|
+
register(index);
|
|
75
|
+
return () => unregister(index);
|
|
76
|
+
}, [magnetic, dataX, getXAxisConfig, register, unregister]);
|
|
77
|
+
};
|