@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.
Files changed (68) hide show
  1. package/dist/module/lib/Components/Line/Line.js +4 -4
  2. package/dist/module/lib/Components/Line/Line.js.map +1 -1
  3. package/dist/module/lib/Components/LineChart/LineChart.js +4 -4
  4. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  5. package/dist/module/lib/Components/LineChart/LineChart.performance.test.js +99 -0
  6. package/dist/module/lib/Components/LineChart/LineChart.performance.test.js.map +1 -0
  7. package/dist/module/lib/Components/Point/Point.js +81 -50
  8. package/dist/module/lib/Components/Point/Point.js.map +1 -1
  9. package/dist/module/lib/Components/Point/Point.test.js +41 -0
  10. package/dist/module/lib/Components/Point/Point.test.js.map +1 -1
  11. package/dist/module/lib/Components/Point/constants.js +2 -0
  12. package/dist/module/lib/Components/Point/constants.js.map +1 -1
  13. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js +19 -9
  14. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.js.map +1 -1
  15. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js +10 -1
  16. package/dist/module/lib/Components/Point/pointContext/MagneticPointsProvider.test.js.map +1 -1
  17. package/dist/module/lib/Components/Point/pointContext/index.js +1 -1
  18. package/dist/module/lib/Components/Point/pointContext/index.js.map +1 -1
  19. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js +22 -4
  20. package/dist/module/lib/Components/Point/pointContext/magneticPointsContext.js.map +1 -1
  21. package/dist/module/lib/Components/Point/usePointGeometry.js +60 -0
  22. package/dist/module/lib/Components/Point/usePointGeometry.js.map +1 -0
  23. package/dist/module/lib/Components/Point/usePointGeometry.test.js +86 -0
  24. package/dist/module/lib/Components/Point/usePointGeometry.test.js.map +1 -0
  25. package/dist/module/lib/Components/Point/utils.js +58 -15
  26. package/dist/module/lib/Components/Point/utils.js.map +1 -1
  27. package/dist/module/lib/Components/Point/utils.test.js +37 -75
  28. package/dist/module/lib/Components/Point/utils.test.js.map +1 -1
  29. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js +2 -2
  30. package/dist/module/lib/Components/Scrubber/ScrubberProvider.js.map +1 -1
  31. package/dist/module/lib/utils/domain/domain.js +4 -4
  32. package/dist/module/lib/utils/domain/domain.js.map +1 -1
  33. package/dist/typescript/src/lib/Components/Line/Line.d.ts +1 -1
  34. package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -1
  35. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  36. package/dist/typescript/src/lib/Components/Point/Point.d.ts +1 -1
  37. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -1
  38. package/dist/typescript/src/lib/Components/Point/constants.d.ts +2 -0
  39. package/dist/typescript/src/lib/Components/Point/constants.d.ts.map +1 -1
  40. package/dist/typescript/src/lib/Components/Point/pointContext/MagneticPointsProvider.d.ts.map +1 -1
  41. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts +1 -1
  42. package/dist/typescript/src/lib/Components/Point/pointContext/index.d.ts.map +1 -1
  43. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts +22 -5
  44. package/dist/typescript/src/lib/Components/Point/pointContext/magneticPointsContext.d.ts.map +1 -1
  45. package/dist/typescript/src/lib/Components/Point/types.d.ts +32 -0
  46. package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -1
  47. package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts +33 -0
  48. package/dist/typescript/src/lib/Components/Point/usePointGeometry.d.ts.map +1 -0
  49. package/dist/typescript/src/lib/Components/Point/utils.d.ts +30 -4
  50. package/dist/typescript/src/lib/Components/Point/utils.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/lib/Components/Line/Line.tsx +4 -4
  53. package/src/lib/Components/LineChart/LineChart.performance.test.tsx +101 -0
  54. package/src/lib/Components/LineChart/LineChart.tsx +18 -11
  55. package/src/lib/Components/Point/Point.test.tsx +46 -0
  56. package/src/lib/Components/Point/Point.tsx +79 -57
  57. package/src/lib/Components/Point/constants.ts +2 -0
  58. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.test.tsx +13 -1
  59. package/src/lib/Components/Point/pointContext/MagneticPointsProvider.tsx +20 -8
  60. package/src/lib/Components/Point/pointContext/index.ts +4 -2
  61. package/src/lib/Components/Point/pointContext/magneticPointsContext.ts +29 -7
  62. package/src/lib/Components/Point/types.ts +41 -0
  63. package/src/lib/Components/Point/usePointGeometry.test.ts +95 -0
  64. package/src/lib/Components/Point/usePointGeometry.ts +77 -0
  65. package/src/lib/Components/Point/utils.test.ts +34 -80
  66. package/src/lib/Components/Point/utils.ts +77 -19
  67. package/src/lib/Components/Scrubber/ScrubberProvider.tsx +2 -2
  68. package/src/lib/utils/domain/domain.ts +4 -4
@@ -1,22 +1,19 @@
1
1
  import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
- import { useMemo } from 'react';
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 { projectPoint } from '../../utils/scales/scales';
7
- import { useCartesianChartContext } from '../CartesianChart/context';
8
- import { useRevealFadeProps } from '../CartesianChart/RevealAnimation';
9
- import { DEFAULT_SIZE, STROKE_WIDTH } from './constants';
10
- import { useMagneticPointsContext } from './pointContext';
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 type { PointLabelProps, PointProps } from './types';
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={theme.typographies.body4.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
- export function Point({
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 { getXScale, getYScale, getXAxisConfig, drawingArea } =
55
- useCartesianChartContext();
56
- const fadeProps = useRevealFadeProps();
57
- const magneticContext = useMagneticPointsContext();
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
- const radius = size / 2;
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 resolvedLabel = resolveLabel(label, dataX);
78
- const hasLabel = resolvedLabel != null;
79
- const renderArrow = showLabelArrow && hasLabel;
80
- const labelY = computeLabelY(pixel.y, radius, labelPosition, renderArrow);
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
- <Circle
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
- {renderArrow && (
98
- <Polygon
99
- testID='point-arrow'
100
- points={buildArrowPoints(pixel.x, pixel.y, radius, labelPosition)}
101
- fill={theme.colors.text.base}
114
+ {isLabelVisible && showLabelArrow && (
115
+ <PointArrow
116
+ x={pixel.x}
117
+ y={pixel.y}
118
+ size={size}
119
+ position={labelPosition}
102
120
  />
103
121
  )}
104
- {resolvedLabel != null && (
105
- <Label x={pixel.x} y={labelY}>
106
- {resolvedLabel}
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 { useMagneticPointsContext } from './magneticPointsContext';
5
+ import {
6
+ useMagneticRegistry,
7
+ useMagneticSnapshot,
8
+ } from './magneticPointsContext';
6
9
  import { MagneticPointsProvider } from './MagneticPointsProvider';
7
10
 
8
11
  const wrapper = ({ children }: { children: ReactNode }) => (
9
12
  <MagneticPointsProvider>{children}</MagneticPointsProvider>
10
13
  );
11
14
 
15
+ /**
16
+ * Reads both split contexts so the existing assertions (register/unregister/
17
+ * getMagneticPoints/version) keep working against a single result object.
18
+ */
19
+ const useMagneticPointsContext = () => ({
20
+ ...useMagneticRegistry(),
21
+ ...useMagneticSnapshot(),
22
+ });
23
+
12
24
  describe('MagneticPointsProvider', () => {
13
25
  it('starts with an empty set', () => {
14
26
  const { result } = renderHook(() => useMagneticPointsContext(), {
@@ -2,8 +2,10 @@ import { useCallback, useMemo, useRef, useState } from 'react';
2
2
  import type { ReactNode } from 'react';
3
3
 
4
4
  import {
5
- MagneticPointsContextProvider,
6
- type MagneticPointsContextValue,
5
+ MagneticRegistryProvider,
6
+ MagneticSnapshotProvider,
7
+ type MagneticRegistryValue,
8
+ type MagneticSnapshotValue,
7
9
  } from './magneticPointsContext';
8
10
 
9
11
  type MagneticPointsProviderProps = {
@@ -32,14 +34,24 @@ export function MagneticPointsProvider({
32
34
  return pointsRef.current;
33
35
  }, []);
34
36
 
35
- const value: MagneticPointsContextValue = useMemo(
36
- () => ({ register, unregister, getMagneticPoints, version }),
37
- [register, unregister, getMagneticPoints, version],
37
+ // Stable identity for the provider's lifetime: `Point` consumers read this
38
+ // and therefore never re-render when the magnetic set changes.
39
+ const registry: MagneticRegistryValue = useMemo(
40
+ () => ({ register, unregister }),
41
+ [register, unregister],
42
+ );
43
+
44
+ // Changes on every register/unregister: consumed only by the scrubber.
45
+ const snapshot: MagneticSnapshotValue = useMemo(
46
+ () => ({ version, getMagneticPoints }),
47
+ [version, getMagneticPoints],
38
48
  );
39
49
 
40
50
  return (
41
- <MagneticPointsContextProvider value={value}>
42
- {children}
43
- </MagneticPointsContextProvider>
51
+ <MagneticRegistryProvider value={registry}>
52
+ <MagneticSnapshotProvider value={snapshot}>
53
+ {children}
54
+ </MagneticSnapshotProvider>
55
+ </MagneticRegistryProvider>
44
56
  );
45
57
  }
@@ -1,5 +1,7 @@
1
1
  export {
2
- useMagneticPointsContext,
3
- type MagneticPointsContextValue,
2
+ useMagneticRegistry,
3
+ useMagneticSnapshot,
4
+ type MagneticRegistryValue,
5
+ type MagneticSnapshotValue,
4
6
  } from './magneticPointsContext';
5
7
  export { MagneticPointsProvider } from './MagneticPointsProvider';
@@ -1,19 +1,41 @@
1
1
  import { createSafeContext } from '@ledgerhq/lumen-utils-shared';
2
2
 
3
- export type MagneticPointsContextValue = {
3
+ /**
4
+ * Stable registration API for magnetic points. Its identity never changes for
5
+ * the provider's lifetime, so consumers (every `Point`) do not re-render when
6
+ * the magnetic set changes — only when they mount/unmount.
7
+ */
8
+ export type MagneticRegistryValue = {
4
9
  register: (dataIndex: number) => void;
5
10
  unregister: (dataIndex: number) => void;
11
+ };
12
+
13
+ /**
14
+ * Mutable snapshot of the registered magnetic points. Changes on every
15
+ * register/unregister (via `version`) and is consumed only by the scrubber,
16
+ * which needs to recompute its snap targets when the set changes.
17
+ */
18
+ export type MagneticSnapshotValue = {
6
19
  version: number;
7
20
  getMagneticPoints: () => ReadonlySet<number>;
8
21
  };
9
22
 
10
- const [MagneticPointsContextProvider, _useMagneticPointsSafeContext] =
11
- createSafeContext<MagneticPointsContextValue>('MagneticPoints');
23
+ const [MagneticRegistryProvider, _useMagneticRegistrySafeContext] =
24
+ createSafeContext<MagneticRegistryValue>('MagneticRegistry');
25
+
26
+ const [MagneticSnapshotProvider, _useMagneticSnapshotSafeContext] =
27
+ createSafeContext<MagneticSnapshotValue>('MagneticSnapshot');
28
+
29
+ export const useMagneticRegistry = (): MagneticRegistryValue =>
30
+ _useMagneticRegistrySafeContext({
31
+ consumerName: 'useMagneticRegistry',
32
+ contextRequired: true,
33
+ });
12
34
 
13
- export const useMagneticPointsContext = (): MagneticPointsContextValue =>
14
- _useMagneticPointsSafeContext({
15
- consumerName: 'useMagneticPointsContext',
35
+ export const useMagneticSnapshot = (): MagneticSnapshotValue =>
36
+ _useMagneticSnapshotSafeContext({
37
+ consumerName: 'useMagneticSnapshot',
16
38
  contextRequired: true,
17
39
  });
18
40
 
19
- export { MagneticPointsContextProvider };
41
+ export { MagneticRegistryProvider, MagneticSnapshotProvider };
@@ -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
+ };