@ledgerhq/lumen-ui-rnative-visualization 0.1.14 → 0.1.15
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/Axis/Axis.constants.js +21 -0
- package/dist/module/lib/Components/Axis/Axis.constants.js.map +1 -0
- package/dist/module/lib/Components/Axis/index.js +5 -0
- package/dist/module/lib/Components/Axis/index.js.map +1 -0
- package/dist/module/lib/Components/CartesianChart/CartesianChart.js +34 -22
- package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js +5 -4
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js.map +1 -1
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js +54 -0
- package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js.map +1 -1
- package/dist/module/lib/Components/CartesianChart/utils.js +18 -8
- package/dist/module/lib/Components/CartesianChart/utils.js.map +1 -1
- package/dist/module/lib/Components/Line/utils.js +6 -1
- package/dist/module/lib/Components/Line/utils.js.map +1 -1
- package/dist/module/lib/Components/Line/utils.test.js +88 -0
- package/dist/module/lib/Components/Line/utils.test.js.map +1 -0
- package/dist/module/lib/Components/LineChart/LineChart.js +19 -31
- package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.stories.js +38 -0
- package/dist/module/lib/Components/LineChart/LineChart.stories.js.map +1 -1
- package/dist/module/lib/Components/LineChart/LineChart.test.js +192 -0
- package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
- package/dist/module/lib/Components/Point/Point.js +8 -2
- package/dist/module/lib/Components/Point/Point.js.map +1 -1
- package/dist/module/lib/Components/Point/constants.js +9 -0
- package/dist/module/lib/Components/Point/constants.js.map +1 -0
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js +35 -0
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js.map +1 -0
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js +153 -0
- package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js.map +1 -0
- package/dist/module/lib/Components/Point/pointContext/index.js +5 -0
- package/dist/module/lib/Components/Point/pointContext/index.js.map +1 -0
- package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js +10 -0
- package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js.map +1 -0
- package/dist/module/lib/Components/Point/utils.js +26 -6
- package/dist/module/lib/Components/Point/utils.js.map +1 -1
- package/dist/module/lib/Components/Point/utils.test.js +84 -2
- package/dist/module/lib/Components/Point/utils.test.js.map +1 -1
- package/dist/module/lib/Components/ReferenceLine/utils.js.map +1 -1
- package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.js +4 -4
- package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js +31 -10
- package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.js.map +1 -1
- package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js +4 -4
- package/dist/module/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.js.map +1 -1
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +35 -7
- package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
- package/dist/module/lib/Components/Scrubber/utils.js +64 -0
- package/dist/module/lib/Components/Scrubber/utils.js.map +1 -1
- package/dist/module/lib/Components/Scrubber/utils.test.js +128 -1
- package/dist/module/lib/Components/Scrubber/utils.test.js.map +1 -1
- package/dist/module/lib/utils/domain/domain.js.map +1 -1
- package/dist/module/lib/utils/scales/scales.js +13 -4
- package/dist/module/lib/utils/scales/scales.js.map +1 -1
- package/dist/module/lib/utils/scales/scales.test.js +29 -0
- package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
- package/dist/module/lib/utils/ticks/ticks.js +13 -3
- package/dist/module/lib/utils/ticks/ticks.js.map +1 -1
- package/dist/module/lib/utils/ticks/ticks.test.js +34 -0
- package/dist/module/lib/utils/ticks/ticks.test.js.map +1 -1
- package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts +5 -0
- package/dist/typescript/src/lib/Components/Axis/Axis.constants.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts +39 -0
- package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Axis/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Axis/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
- package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts +5 -4
- package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +11 -3
- package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts +18 -8
- package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Line/utils.d.ts +4 -0
- package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +1 -1
- package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/LineChart/types.d.ts +10 -3
- package/dist/typescript/src/lib/Components/LineChart/types.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 +7 -0
- package/dist/typescript/src/lib/Components/Point/constants.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts +7 -0
- package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +3 -0
- package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +13 -0
- package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -0
- package/dist/typescript/src/lib/Components/Point/types.d.ts +7 -0
- package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Point/utils.d.ts +8 -6
- package/dist/typescript/src/lib/Components/Point/utils.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts +3 -2
- package/dist/typescript/src/lib/Components/ReferenceLine/utils.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts +5 -1
- package/dist/typescript/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts +1 -1
- package/dist/typescript/src/lib/Components/Scrubber/ScrubberProvider.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Scrubber/types.d.ts +13 -0
- package/dist/typescript/src/lib/Components/Scrubber/types.d.ts.map +1 -1
- package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts +33 -3
- package/dist/typescript/src/lib/Components/Scrubber/utils.d.ts.map +1 -1
- package/dist/typescript/src/lib/utils/domain/domain.d.ts +5 -4
- package/dist/typescript/src/lib/utils/domain/domain.d.ts.map +1 -1
- package/dist/typescript/src/lib/utils/index.d.ts +1 -1
- package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
- package/dist/typescript/src/lib/utils/scales/scales.d.ts +11 -5
- package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
- package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +13 -5
- package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -1
- package/dist/typescript/src/lib/utils/types.d.ts +3 -28
- package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/lib/Components/Axis/Axis.constants.ts +23 -0
- package/src/lib/Components/Axis/Axis.types.ts +40 -0
- package/src/lib/Components/Axis/index.ts +2 -0
- package/src/lib/Components/CartesianChart/CartesianChart.tsx +39 -23
- package/src/lib/Components/CartesianChart/context/useBuildChartContext.test.ts +56 -0
- package/src/lib/Components/CartesianChart/context/useBuildChartContext.ts +8 -8
- package/src/lib/Components/CartesianChart/types.ts +11 -3
- package/src/lib/Components/CartesianChart/utils.ts +18 -8
- package/src/lib/Components/Line/utils.test.ts +90 -0
- package/src/lib/Components/Line/utils.ts +6 -1
- package/src/lib/Components/LineChart/LineChart.stories.tsx +36 -0
- package/src/lib/Components/LineChart/LineChart.test.tsx +156 -0
- package/src/lib/Components/LineChart/LineChart.tsx +30 -40
- package/src/lib/Components/LineChart/types.ts +10 -3
- package/src/lib/Components/Point/Point.tsx +9 -3
- package/src/lib/Components/Point/constants.ts +6 -0
- package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +162 -0
- package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +45 -0
- package/src/lib/Components/Point/pointContext/index.ts +5 -0
- package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +19 -0
- package/src/lib/Components/Point/types.ts +7 -0
- package/src/lib/Components/Point/utils.test.ts +95 -5
- package/src/lib/Components/Point/utils.ts +35 -7
- package/src/lib/Components/ReferenceLine/utils.ts +4 -7
- package/src/lib/Components/Scrubber/DefaultScrubberTooltip/DefaultScrubberTooltip.test.tsx +4 -4
- package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.test.ts +4 -4
- package/src/lib/Components/Scrubber/DefaultScrubberTooltip/utils.ts +29 -15
- package/src/lib/Components/Scrubber/ScrubberProvider.tsx +41 -5
- package/src/lib/Components/Scrubber/types.ts +13 -0
- package/src/lib/Components/Scrubber/utils.test.ts +150 -0
- package/src/lib/Components/Scrubber/utils.ts +93 -3
- package/src/lib/utils/domain/domain.ts +6 -5
- package/src/lib/utils/index.ts +0 -2
- package/src/lib/utils/scales/scales.test.ts +19 -0
- package/src/lib/utils/scales/scales.ts +15 -9
- package/src/lib/utils/ticks/ticks.test.ts +45 -0
- package/src/lib/utils/ticks/ticks.ts +19 -9
- package/src/lib/utils/types.ts +3 -26
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { describe, expect, it } from '@jest/globals';
|
|
1
|
+
import { describe, expect, it, jest } from '@jest/globals';
|
|
2
|
+
import { renderHook } from '@testing-library/react-native';
|
|
2
3
|
|
|
4
|
+
import type { BaseAxisProps } from '../Axis';
|
|
5
|
+
import { ARROW_HEIGHT, ARROW_WIDTH, GAP, LABEL_FONT_SIZE } from './constants';
|
|
3
6
|
import {
|
|
4
|
-
ARROW_HEIGHT,
|
|
5
|
-
ARROW_WIDTH,
|
|
6
7
|
buildArrowPoints,
|
|
7
8
|
computeLabelY,
|
|
8
|
-
GAP,
|
|
9
9
|
isWithinBounds,
|
|
10
|
-
LABEL_FONT_SIZE,
|
|
11
10
|
resolveLabel,
|
|
11
|
+
useMagneticRegistration,
|
|
12
12
|
} from './utils';
|
|
13
13
|
|
|
14
14
|
describe('isWithinBounds', () => {
|
|
@@ -116,3 +116,93 @@ describe('computeLabelY', () => {
|
|
|
116
116
|
expect(result).toBe(pixelY + radius + GAP + GAP + LABEL_FONT_SIZE);
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
const makeContext = () => ({
|
|
121
|
+
register: jest.fn(),
|
|
122
|
+
unregister: jest.fn(),
|
|
123
|
+
version: 0,
|
|
124
|
+
getMagneticPoints: () => new Set<number>(),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const noAxisConfig = (): BaseAxisProps | undefined => undefined;
|
|
128
|
+
|
|
129
|
+
describe('useMagneticRegistration', () => {
|
|
130
|
+
it('registers the data index when magnetic is true', () => {
|
|
131
|
+
const ctx = makeContext();
|
|
132
|
+
renderHook(() => useMagneticRegistration(true, 3, noAxisConfig, ctx));
|
|
133
|
+
|
|
134
|
+
expect(ctx.register).toHaveBeenCalledWith(3);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('does not register when magnetic is false', () => {
|
|
138
|
+
const ctx = makeContext();
|
|
139
|
+
renderHook(() => useMagneticRegistration(false, 3, noAxisConfig, ctx));
|
|
140
|
+
|
|
141
|
+
expect(ctx.register).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('unregisters on unmount', () => {
|
|
145
|
+
const ctx = makeContext();
|
|
146
|
+
const { unmount } = renderHook(() =>
|
|
147
|
+
useMagneticRegistration(true, 3, noAxisConfig, ctx),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
unmount();
|
|
151
|
+
expect(ctx.unregister).toHaveBeenCalledWith(3);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('resolves dataX through axis config data when provided', () => {
|
|
155
|
+
const ctx = makeContext();
|
|
156
|
+
const getXAxisConfig = (): BaseAxisProps => ({
|
|
157
|
+
scaleType: 'band',
|
|
158
|
+
data: [100, 200, 300],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
renderHook(() => useMagneticRegistration(true, 200, getXAxisConfig, ctx));
|
|
162
|
+
|
|
163
|
+
expect(ctx.register).toHaveBeenCalledWith(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('does not register when axis data exists but does not contain the value', () => {
|
|
167
|
+
const ctx = makeContext();
|
|
168
|
+
const getXAxisConfig = (): BaseAxisProps => ({
|
|
169
|
+
scaleType: 'band',
|
|
170
|
+
data: [100, 200, 300],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
renderHook(() => useMagneticRegistration(true, 999, getXAxisConfig, ctx));
|
|
174
|
+
|
|
175
|
+
expect(ctx.register).not.toHaveBeenCalled();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('re-registers when dataX changes', () => {
|
|
179
|
+
const ctx = makeContext();
|
|
180
|
+
const { rerender } = renderHook(
|
|
181
|
+
({ dataX }: { dataX: number }) =>
|
|
182
|
+
useMagneticRegistration(true, dataX, noAxisConfig, ctx),
|
|
183
|
+
{ initialProps: { dataX: 2 } },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(ctx.register).toHaveBeenCalledWith(2);
|
|
187
|
+
|
|
188
|
+
rerender({ dataX: 4 });
|
|
189
|
+
|
|
190
|
+
expect(ctx.unregister).toHaveBeenCalledWith(2);
|
|
191
|
+
expect(ctx.register).toHaveBeenCalledWith(4);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('unregisters and stops when magnetic switches to false', () => {
|
|
195
|
+
const ctx = makeContext();
|
|
196
|
+
const { rerender } = renderHook(
|
|
197
|
+
({ magnetic }: { magnetic: boolean }) =>
|
|
198
|
+
useMagneticRegistration(magnetic, 3, noAxisConfig, ctx),
|
|
199
|
+
{ initialProps: { magnetic: true } },
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(ctx.register).toHaveBeenCalledWith(3);
|
|
203
|
+
|
|
204
|
+
rerender({ magnetic: false });
|
|
205
|
+
|
|
206
|
+
expect(ctx.unregister).toHaveBeenCalledWith(3);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const ARROW_HEIGHT = 4;
|
|
8
|
-
export const GAP = 4;
|
|
3
|
+
import type { DrawingArea } from '../../utils/types';
|
|
4
|
+
import type { BaseAxisProps } from '../Axis';
|
|
5
|
+
import { LABEL_FONT_SIZE, ARROW_WIDTH, ARROW_HEIGHT, GAP } from './constants';
|
|
6
|
+
import type { MagneticPointsContextValue } from './pointContext/magneticPointsContext';
|
|
9
7
|
|
|
10
8
|
export const isWithinBounds = (
|
|
11
9
|
px: number,
|
|
@@ -53,6 +51,36 @@ export const resolveLabel = (
|
|
|
53
51
|
return resolved === '' ? undefined : resolved;
|
|
54
52
|
};
|
|
55
53
|
|
|
54
|
+
export const resolveDataXToIndex = (
|
|
55
|
+
dataX: number,
|
|
56
|
+
axisConfig: BaseAxisProps | undefined,
|
|
57
|
+
): number | undefined => {
|
|
58
|
+
const axisData = axisConfig?.data;
|
|
59
|
+
if (!axisData || typeof axisData[0] !== 'number') return dataX;
|
|
60
|
+
const index = (axisData as number[]).indexOf(dataX);
|
|
61
|
+
if (index === -1) return undefined;
|
|
62
|
+
return index;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Registers/unregisters a data index as a magnetic snap target
|
|
67
|
+
* when the Point has `magnetic` enabled.
|
|
68
|
+
*/
|
|
69
|
+
export const useMagneticRegistration = (
|
|
70
|
+
magnetic: boolean,
|
|
71
|
+
dataX: number,
|
|
72
|
+
getXAxisConfig: () => BaseAxisProps | undefined,
|
|
73
|
+
{ register, unregister }: MagneticPointsContextValue,
|
|
74
|
+
): void => {
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!magnetic) return;
|
|
77
|
+
const index = resolveDataXToIndex(dataX, getXAxisConfig());
|
|
78
|
+
if (index === undefined) return;
|
|
79
|
+
register(index);
|
|
80
|
+
return () => unregister(index);
|
|
81
|
+
}, [magnetic, dataX, getXAxisConfig, register, unregister]);
|
|
82
|
+
};
|
|
83
|
+
|
|
56
84
|
/**
|
|
57
85
|
* Computes the vertical position of the label text baseline.
|
|
58
86
|
*/
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { getPointOnScale, isCategoricalScale } from '../../utils/scales/scales';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
ChartScaleFunction,
|
|
5
|
-
DrawingArea,
|
|
6
|
-
} from '../../utils/types';
|
|
2
|
+
import type { ChartScaleFunction, DrawingArea } from '../../utils/types';
|
|
3
|
+
import type { BaseAxisProps } from '../Axis';
|
|
7
4
|
|
|
8
5
|
import type { LabelAlignment, LabelPosition } from './types';
|
|
9
6
|
|
|
@@ -28,7 +25,7 @@ type ResolvePixelParams = {
|
|
|
28
25
|
scale: ChartScaleFunction | undefined;
|
|
29
26
|
axis: 'x' | 'y';
|
|
30
27
|
drawingArea: DrawingArea;
|
|
31
|
-
axisConfig?:
|
|
28
|
+
axisConfig?: BaseAxisProps;
|
|
32
29
|
};
|
|
33
30
|
|
|
34
31
|
type LabelParams = {
|
|
@@ -95,7 +92,7 @@ export const isPixelWithinDrawingArea = (
|
|
|
95
92
|
*/
|
|
96
93
|
const resolveDataValue = (
|
|
97
94
|
index: number,
|
|
98
|
-
axisConfig?:
|
|
95
|
+
axisConfig?: BaseAxisProps,
|
|
99
96
|
): number | undefined => {
|
|
100
97
|
const data = axisConfig?.data;
|
|
101
98
|
if (!data || data.length === 0) return index;
|
|
@@ -148,7 +148,7 @@ describe('DefaultScrubberTooltip', () => {
|
|
|
148
148
|
scrubberProps: { tooltip: defaultTooltip },
|
|
149
149
|
scrubberContext: scrubberAt(2),
|
|
150
150
|
});
|
|
151
|
-
expect(getByTestId('chart-tooltip-rect').props.x).toBe(
|
|
151
|
+
expect(getByTestId('chart-tooltip-rect').props.x).toBe(260);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
it('flips tooltip left when right side would overflow (index 3)', () => {
|
|
@@ -158,7 +158,7 @@ describe('DefaultScrubberTooltip', () => {
|
|
|
158
158
|
},
|
|
159
159
|
scrubberContext: scrubberAt(3),
|
|
160
160
|
});
|
|
161
|
-
expect(getByTestId('chart-tooltip-rect').props.x).toBe(
|
|
161
|
+
expect(getByTestId('chart-tooltip-rect').props.x).toBe(220);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
it('flips at the rightmost scrubber position (index 4)', () => {
|
|
@@ -168,7 +168,7 @@ describe('DefaultScrubberTooltip', () => {
|
|
|
168
168
|
},
|
|
169
169
|
scrubberContext: scrubberAt(4),
|
|
170
170
|
});
|
|
171
|
-
expect(getByTestId('chart-tooltip-rect').props.x).toBe(
|
|
171
|
+
expect(getByTestId('chart-tooltip-rect').props.x).toBe(320);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
it('custom offset shifts the no-flip tooltip position', () => {
|
|
@@ -178,7 +178,7 @@ describe('DefaultScrubberTooltip', () => {
|
|
|
178
178
|
},
|
|
179
179
|
scrubberContext: scrubberAt(2),
|
|
180
180
|
});
|
|
181
|
-
expect(getByTestId('chart-tooltip-rect').props.x).toBe(
|
|
181
|
+
expect(getByTestId('chart-tooltip-rect').props.x).toBe(270);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
it('wider minWidth can trigger a flip that the default width would not', () => {
|
|
@@ -192,7 +192,7 @@ describe('measureWidths', () => {
|
|
|
192
192
|
titleRef,
|
|
193
193
|
labelRefs,
|
|
194
194
|
valueRefs,
|
|
195
|
-
);
|
|
195
|
+
).promise;
|
|
196
196
|
|
|
197
197
|
expect(result).toEqual({ title: 50, labels: [30], values: [20] });
|
|
198
198
|
raf.mockRestore();
|
|
@@ -212,7 +212,7 @@ describe('measureWidths', () => {
|
|
|
212
212
|
titleRef,
|
|
213
213
|
labelRefs,
|
|
214
214
|
valueRefs,
|
|
215
|
-
);
|
|
215
|
+
).promise;
|
|
216
216
|
|
|
217
217
|
expect(result.title).toBe(0);
|
|
218
218
|
raf.mockRestore();
|
|
@@ -235,7 +235,7 @@ describe('measureWidths', () => {
|
|
|
235
235
|
titleRef,
|
|
236
236
|
labelRefs,
|
|
237
237
|
valueRefs,
|
|
238
|
-
);
|
|
238
|
+
).promise;
|
|
239
239
|
|
|
240
240
|
expect(result.labels).toEqual([10, 20]);
|
|
241
241
|
expect(result.values).toEqual([5, 15]);
|
|
@@ -256,7 +256,7 @@ describe('measureWidths', () => {
|
|
|
256
256
|
titleRef,
|
|
257
257
|
labelRefs,
|
|
258
258
|
valueRefs,
|
|
259
|
-
);
|
|
259
|
+
).promise;
|
|
260
260
|
|
|
261
261
|
expect(result).toEqual({ title: 0, labels: [0], values: [0] });
|
|
262
262
|
raf.mockRestore();
|
|
@@ -107,29 +107,41 @@ export const computeItemsBaseY = (
|
|
|
107
107
|
return drawingAreaY + PADDING_Y + titleBlockHeight;
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
-
const waitForAnimationFrame = ():
|
|
111
|
-
|
|
110
|
+
const waitForAnimationFrame = (): {
|
|
111
|
+
promise: Promise<void>;
|
|
112
|
+
cancel: () => void;
|
|
113
|
+
} => {
|
|
114
|
+
let id: number;
|
|
115
|
+
const promise = new Promise<void>((resolve) => {
|
|
116
|
+
id = requestAnimationFrame(() => resolve());
|
|
117
|
+
});
|
|
118
|
+
return { promise, cancel: () => cancelAnimationFrame(id) };
|
|
119
|
+
};
|
|
112
120
|
|
|
113
121
|
/**
|
|
114
122
|
* Reads the rendered widths of all tooltip text elements via async `getBBox`.
|
|
115
123
|
* Defers until the next animation frame so layout has settled.
|
|
124
|
+
* Returns a cancel function so callers can abort the pending RAF.
|
|
116
125
|
*/
|
|
117
|
-
export const measureWidths =
|
|
126
|
+
export const measureWidths = (
|
|
118
127
|
items: ChartTooltipItemData[],
|
|
119
128
|
hasTitle: boolean,
|
|
120
129
|
titleRef: RefObject<SvgBBoxElement | null>,
|
|
121
130
|
labelRefs: RefObject<(SvgBBoxElement | null)[]>,
|
|
122
131
|
valueRefs: RefObject<(SvgBBoxElement | null)[]>,
|
|
123
|
-
): Promise<Widths
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
): { promise: Promise<Widths>; cancel: () => void } => {
|
|
133
|
+
const raf = waitForAnimationFrame();
|
|
134
|
+
const promise = raf.promise.then(async () => {
|
|
135
|
+
const title = hasTitle ? await safeGetBBoxWidth(titleRef.current) : 0;
|
|
136
|
+
const labels = await Promise.all(
|
|
137
|
+
items.map((_, i) => safeGetBBoxWidth(labelRefs.current[i])),
|
|
138
|
+
);
|
|
139
|
+
const values = await Promise.all(
|
|
140
|
+
items.map((_, i) => safeGetBBoxWidth(valueRefs.current[i])),
|
|
141
|
+
);
|
|
142
|
+
return { title, labels, values };
|
|
143
|
+
});
|
|
144
|
+
return { promise, cancel: raf.cancel };
|
|
133
145
|
};
|
|
134
146
|
|
|
135
147
|
/**
|
|
@@ -186,18 +198,20 @@ export function useTooltipMeasurement(
|
|
|
186
198
|
|
|
187
199
|
let cancelled = false;
|
|
188
200
|
|
|
189
|
-
|
|
201
|
+
const { promise, cancel } = measureWidths(
|
|
190
202
|
itemsRef.current,
|
|
191
203
|
hasTitleRef.current,
|
|
192
204
|
titleRef,
|
|
193
205
|
labelRefs,
|
|
194
206
|
valueRefs,
|
|
195
|
-
)
|
|
207
|
+
);
|
|
208
|
+
void promise.then((result) => {
|
|
196
209
|
if (!cancelled) setWidths(result);
|
|
197
210
|
});
|
|
198
211
|
|
|
199
212
|
return () => {
|
|
200
213
|
cancelled = true;
|
|
214
|
+
cancel();
|
|
201
215
|
};
|
|
202
216
|
}, [shapeKey]);
|
|
203
217
|
|
|
@@ -5,9 +5,15 @@ import Animated, { useSharedValue } from 'react-native-reanimated';
|
|
|
5
5
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
6
6
|
|
|
7
7
|
import { useCartesianChartContext } from '../CartesianChart/context';
|
|
8
|
+
import { useMagneticPointsContext } from '../Point/pointContext';
|
|
8
9
|
import { ScrubberContextProvider } from './context';
|
|
9
10
|
import type { ScrubberProviderProps } from './types';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
applyMagnetisation,
|
|
13
|
+
getDataIndexFromPosition,
|
|
14
|
+
resolvePixelX,
|
|
15
|
+
buildSortedMagnets,
|
|
16
|
+
} from './utils';
|
|
11
17
|
|
|
12
18
|
const LONG_PRESS_MIN_DURATION_MS = 50;
|
|
13
19
|
|
|
@@ -30,29 +36,46 @@ export function ScrubberProvider({
|
|
|
30
36
|
height,
|
|
31
37
|
enableScrubbing,
|
|
32
38
|
onScrubberPositionChange,
|
|
39
|
+
magnetRadius = 6,
|
|
40
|
+
style,
|
|
33
41
|
}: Readonly<ScrubberProviderProps>) {
|
|
34
42
|
const [scrubberPosition, setScrubberPosition] = useState<
|
|
35
43
|
number | undefined
|
|
36
44
|
>();
|
|
37
45
|
|
|
38
46
|
const { getXScale, getXAxisConfig, dataLength } = useCartesianChartContext();
|
|
47
|
+
const { getMagneticPoints, version } = useMagneticPointsContext();
|
|
39
48
|
|
|
40
49
|
// All values touched by the gesture's JS-thread callback live in a single
|
|
41
50
|
// ref. Reading via `latest.current` keeps the callbacks reference-stable
|
|
42
51
|
// across data, scale, and prop updates, which in turn keeps `composed` and
|
|
43
52
|
// the gesture surface JSX stable so re-rendering the provider on each
|
|
44
53
|
// index change never tears down the GestureDetector or the View tree.
|
|
54
|
+
const sortedMagnets = useMemo(() => {
|
|
55
|
+
const magneticIndices = getMagneticPoints();
|
|
56
|
+
return buildSortedMagnets({
|
|
57
|
+
magneticIndices,
|
|
58
|
+
getPixelForIndex: (index) =>
|
|
59
|
+
resolvePixelX(index, getXScale, getXAxisConfig()),
|
|
60
|
+
});
|
|
61
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
62
|
+
}, [version, getXScale, getXAxisConfig, getMagneticPoints]);
|
|
63
|
+
|
|
45
64
|
const latest = useRef({
|
|
46
65
|
getXScale,
|
|
47
66
|
getXAxisConfig,
|
|
48
67
|
dataLength,
|
|
49
68
|
onChange: onScrubberPositionChange,
|
|
50
69
|
lastIndex: undefined as number | undefined,
|
|
70
|
+
magnetRadius,
|
|
71
|
+
sortedMagnets,
|
|
51
72
|
});
|
|
52
73
|
latest.current.getXScale = getXScale;
|
|
53
74
|
latest.current.getXAxisConfig = getXAxisConfig;
|
|
54
75
|
latest.current.dataLength = dataLength;
|
|
55
76
|
latest.current.onChange = onScrubberPositionChange;
|
|
77
|
+
latest.current.magnetRadius = magnetRadius;
|
|
78
|
+
latest.current.sortedMagnets = sortedMagnets;
|
|
56
79
|
|
|
57
80
|
const setScrubberPositionAndNotify = useCallback(
|
|
58
81
|
(index: number | undefined) => {
|
|
@@ -78,12 +101,25 @@ export function ScrubberProvider({
|
|
|
78
101
|
const ref = latest.current;
|
|
79
102
|
const scale = ref.getXScale();
|
|
80
103
|
if (!scale || ref.dataLength <= 0) return;
|
|
81
|
-
|
|
104
|
+
|
|
105
|
+
const xAxisConfig = ref.getXAxisConfig();
|
|
106
|
+
|
|
107
|
+
let index = getDataIndexFromPosition(
|
|
82
108
|
pixelX,
|
|
83
109
|
scale,
|
|
84
|
-
|
|
110
|
+
xAxisConfig,
|
|
85
111
|
ref.dataLength,
|
|
86
112
|
);
|
|
113
|
+
|
|
114
|
+
if (ref.magnetRadius > 0) {
|
|
115
|
+
index = applyMagnetisation({
|
|
116
|
+
resolvedIndex: index,
|
|
117
|
+
pixelX,
|
|
118
|
+
sortedMagnets: ref.sortedMagnets,
|
|
119
|
+
magnetRadius: ref.magnetRadius,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
87
123
|
setScrubberPositionAndNotify(index);
|
|
88
124
|
},
|
|
89
125
|
[setScrubberPositionAndNotify],
|
|
@@ -128,7 +164,7 @@ export function ScrubberProvider({
|
|
|
128
164
|
// context as expected.
|
|
129
165
|
const surface = useMemo(
|
|
130
166
|
() => (
|
|
131
|
-
<View style={{ width, height }}>
|
|
167
|
+
<View style={[{ width, height }, style]}>
|
|
132
168
|
{children}
|
|
133
169
|
{enableScrubbing && (
|
|
134
170
|
<GestureDetector gesture={composed}>
|
|
@@ -140,7 +176,7 @@ export function ScrubberProvider({
|
|
|
140
176
|
)}
|
|
141
177
|
</View>
|
|
142
178
|
),
|
|
143
|
-
[width, height, children, enableScrubbing, composed],
|
|
179
|
+
[width, height, style, children, enableScrubbing, composed],
|
|
144
180
|
);
|
|
145
181
|
|
|
146
182
|
const contextValue = useMemo(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
3
|
|
|
3
4
|
import type { DrawingArea } from '../../utils/types';
|
|
4
5
|
|
|
@@ -35,6 +36,18 @@ export type ScrubberProviderProps = {
|
|
|
35
36
|
* Optional external callback fired whenever the scrubber position changes.
|
|
36
37
|
*/
|
|
37
38
|
onScrubberPositionChange?: (index: number | undefined) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Pixel radius within which the scrubber magnetically snaps to registered
|
|
41
|
+
* magnetic points. Set to `0` to disable magnetisation.
|
|
42
|
+
* @default 6
|
|
43
|
+
*/
|
|
44
|
+
magnetRadius?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Optional style applied to the wrapping `View` that holds the chart and the
|
|
47
|
+
* gesture overlay.Used to offset the(enlarged) SVG group so its drawing area
|
|
48
|
+
* aligns with the container footprint.
|
|
49
|
+
*/
|
|
50
|
+
style?: StyleProp<ViewStyle>;
|
|
38
51
|
};
|
|
39
52
|
|
|
40
53
|
export type ChartTooltipItemData = {
|
|
@@ -5,10 +5,14 @@ import {
|
|
|
5
5
|
getNumericScale,
|
|
6
6
|
} from '../../utils/scales/scales';
|
|
7
7
|
import {
|
|
8
|
+
applyMagnetisation,
|
|
9
|
+
buildSortedMagnets,
|
|
8
10
|
getDataIndexFromPosition,
|
|
11
|
+
nearestIndex,
|
|
9
12
|
resolvePixelX,
|
|
10
13
|
resolvePixelY,
|
|
11
14
|
} from './utils';
|
|
15
|
+
import type { MagnetEntry } from './utils';
|
|
12
16
|
|
|
13
17
|
describe('getDataIndexFromPosition', () => {
|
|
14
18
|
describe('with a categorical (band) scale', () => {
|
|
@@ -170,3 +174,149 @@ describe('resolvePixelY', () => {
|
|
|
170
174
|
expect(result).toBe(scale(50));
|
|
171
175
|
});
|
|
172
176
|
});
|
|
177
|
+
|
|
178
|
+
describe('buildSortedMagnets', () => {
|
|
179
|
+
it('returns an empty array when the set is empty', () => {
|
|
180
|
+
const result = buildSortedMagnets({
|
|
181
|
+
magneticIndices: new Set(),
|
|
182
|
+
getPixelForIndex: () => 0,
|
|
183
|
+
});
|
|
184
|
+
expect(result).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns entries sorted by pixelX', () => {
|
|
188
|
+
const result = buildSortedMagnets({
|
|
189
|
+
magneticIndices: new Set([3, 1, 2]),
|
|
190
|
+
getPixelForIndex: (i) => i * 50,
|
|
191
|
+
});
|
|
192
|
+
expect(result).toEqual([
|
|
193
|
+
{ index: 1, pixelX: 50 },
|
|
194
|
+
{ index: 2, pixelX: 100 },
|
|
195
|
+
{ index: 3, pixelX: 150 },
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('filters out indices where getPixelForIndex returns undefined', () => {
|
|
200
|
+
const result = buildSortedMagnets({
|
|
201
|
+
magneticIndices: new Set([0, 1, 2]),
|
|
202
|
+
getPixelForIndex: (i) => (i === 1 ? undefined : i * 100),
|
|
203
|
+
});
|
|
204
|
+
expect(result).toEqual([
|
|
205
|
+
{ index: 0, pixelX: 0 },
|
|
206
|
+
{ index: 2, pixelX: 200 },
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('returns an empty array when all indices resolve to undefined', () => {
|
|
211
|
+
const result = buildSortedMagnets({
|
|
212
|
+
magneticIndices: new Set([0, 1]),
|
|
213
|
+
getPixelForIndex: () => undefined,
|
|
214
|
+
});
|
|
215
|
+
expect(result).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('nearestIndex', () => {
|
|
220
|
+
const magnets = (...pixelXs: number[]): MagnetEntry[] =>
|
|
221
|
+
pixelXs.map((px, i) => ({ index: i, pixelX: px }));
|
|
222
|
+
|
|
223
|
+
it('returns -1 for an empty array', () => {
|
|
224
|
+
expect(nearestIndex([], 100)).toBe(-1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns 0 for a single-element array', () => {
|
|
228
|
+
expect(nearestIndex(magnets(200), 100)).toBe(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('returns the index of the exact match', () => {
|
|
232
|
+
expect(nearestIndex(magnets(100, 200, 300), 200)).toBe(1);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('returns the closer index when target is between two values', () => {
|
|
236
|
+
expect(nearestIndex(magnets(100, 200), 160)).toBe(1);
|
|
237
|
+
expect(nearestIndex(magnets(100, 200), 140)).toBe(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('favors the left index when equidistant', () => {
|
|
241
|
+
expect(nearestIndex(magnets(100, 200), 150)).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('returns the last index when target is beyond all values', () => {
|
|
245
|
+
expect(nearestIndex(magnets(100, 200), 500)).toBe(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns the first index when target is before all values', () => {
|
|
249
|
+
expect(nearestIndex(magnets(100, 200), 0)).toBe(0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('applyMagnetisation', () => {
|
|
254
|
+
const toMagnets = (...indices: number[]): MagnetEntry[] =>
|
|
255
|
+
indices.map((i) => ({ index: i, pixelX: i * 100 }));
|
|
256
|
+
|
|
257
|
+
it('returns resolvedIndex when sortedMagnets is empty', () => {
|
|
258
|
+
expect(
|
|
259
|
+
applyMagnetisation({
|
|
260
|
+
resolvedIndex: 2,
|
|
261
|
+
pixelX: 200,
|
|
262
|
+
sortedMagnets: [],
|
|
263
|
+
magnetRadius: 30,
|
|
264
|
+
}),
|
|
265
|
+
).toBe(2);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns resolvedIndex when magnetRadius is 0', () => {
|
|
269
|
+
expect(
|
|
270
|
+
applyMagnetisation({
|
|
271
|
+
resolvedIndex: 1,
|
|
272
|
+
pixelX: 200,
|
|
273
|
+
sortedMagnets: toMagnets(2),
|
|
274
|
+
magnetRadius: 0,
|
|
275
|
+
}),
|
|
276
|
+
).toBe(1);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('returns resolvedIndex when nearest magnet is outside radius', () => {
|
|
280
|
+
expect(
|
|
281
|
+
applyMagnetisation({
|
|
282
|
+
resolvedIndex: 2,
|
|
283
|
+
pixelX: 200,
|
|
284
|
+
sortedMagnets: toMagnets(0, 4),
|
|
285
|
+
magnetRadius: 30,
|
|
286
|
+
}),
|
|
287
|
+
).toBe(2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('snaps to nearest magnet when within radius', () => {
|
|
291
|
+
expect(
|
|
292
|
+
applyMagnetisation({
|
|
293
|
+
resolvedIndex: 2,
|
|
294
|
+
pixelX: 280,
|
|
295
|
+
sortedMagnets: toMagnets(3),
|
|
296
|
+
magnetRadius: 30,
|
|
297
|
+
}),
|
|
298
|
+
).toBe(3);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('snaps at the exact boundary of magnetRadius', () => {
|
|
302
|
+
expect(
|
|
303
|
+
applyMagnetisation({
|
|
304
|
+
resolvedIndex: 2,
|
|
305
|
+
pixelX: 270,
|
|
306
|
+
sortedMagnets: toMagnets(3),
|
|
307
|
+
magnetRadius: 30,
|
|
308
|
+
}),
|
|
309
|
+
).toBe(3);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('does not snap when distance exceeds magnetRadius by 1', () => {
|
|
313
|
+
expect(
|
|
314
|
+
applyMagnetisation({
|
|
315
|
+
resolvedIndex: 2,
|
|
316
|
+
pixelX: 269,
|
|
317
|
+
sortedMagnets: toMagnets(3),
|
|
318
|
+
magnetRadius: 30,
|
|
319
|
+
}),
|
|
320
|
+
).toBe(2);
|
|
321
|
+
});
|
|
322
|
+
});
|