Skip to content

Commit c564036

Browse files
committed
resolve react compiler compliance in BarChartContent, useChartInteractions, useChartInteractionState, useChartLabelFormats
1 parent ed6accb commit c564036

4 files changed

Lines changed: 77 additions & 117 deletions

File tree

src/components/Charts/BarChart/BarChartContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import {useFont} from '@shopify/react-native-skia';
12
import React, {useCallback, useMemo, useState} from 'react';
23
import type {LayoutChangeEvent} from 'react-native';
34
import {View} from 'react-native';
45
import Animated, {useSharedValue} from 'react-native-reanimated';
56
import type {ChartBounds, PointsArray} from 'victory-native';
67
import {Bar, CartesianChart} from 'victory-native';
78
import ActivityIndicator from '@components/ActivityIndicator';
8-
import ChartHeader from '@components/Charts/components/ChartHeader';
99
import ChartTooltip from '@components/Charts/ChartTooltip';
10+
import ChartHeader from '@components/Charts/components/ChartHeader';
1011
import {
1112
BAR_INNER_PADDING,
1213
BAR_ROUNDED_CORNERS,
@@ -21,7 +22,6 @@ import {
2122
Y_AXIS_LINE_WIDTH,
2223
Y_AXIS_TICK_COUNT,
2324
} from '@components/Charts/constants';
24-
import {useFont} from '@shopify/react-native-skia';
2525
import fontSource from '@components/Charts/font';
2626
import type {HitTestArgs} from '@components/Charts/hooks';
2727
import {useChartColors, useChartInteractions, useChartLabelFormats, useChartLabelLayout} from '@components/Charts/hooks';

src/components/Charts/hooks/useChartInteractionState.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState } from 'react';
2-
import type { SharedValue } from 'react-native-reanimated';
3-
import { makeMutable, useAnimatedReaction } from 'react-native-reanimated';
4-
import { scheduleOnRN } from 'react-native-worklets';
1+
import {useState} from 'react';
2+
import type {SharedValue} from 'react-native-reanimated';
3+
import {makeMutable, useAnimatedReaction} from 'react-native-reanimated';
4+
import {scheduleOnRN} from 'react-native-worklets';
55

66
/**
77
* Input field type - matches Victory Native's InputFieldType
@@ -72,13 +72,15 @@ function useIsInteractionActive<Init extends ChartInteractionStateInit>(state: C
7272
* @param initialValues - Initial x and y values matching your chart data structure
7373
* @returns Object containing the interaction state and a boolean indicating if interaction is active
7474
*/
75-
function useChartInteractionState<Init extends ChartInteractionStateInit>(initialValues: Init): {
75+
function useChartInteractionState<Init extends ChartInteractionStateInit>(
76+
initialValues: Init,
77+
): {
7678
state: ChartInteractionState<Init>;
7779
isActive: boolean;
7880
} {
7981
// The React Compiler will automatically memoize this object creation.
8082
// We remove the explicit useMemo and dependency on 'keys'.
81-
const yState = {} as Record<keyof Init['y'], { value: SharedValue<number>; position: SharedValue<number> }>;
83+
const yState = {} as Record<keyof Init['y'], {value: SharedValue<number>; position: SharedValue<number>}>;
8284

8385
for (const [key, initVal] of Object.entries(initialValues.y)) {
8486
yState[key as keyof Init['y']] = {
@@ -104,8 +106,8 @@ function useChartInteractionState<Init extends ChartInteractionStateInit>(initia
104106

105107
const isActive = useIsInteractionActive(state);
106108

107-
return { state, isActive };
109+
return {state, isActive};
108110
}
109111

110-
export { useChartInteractionState };
111-
export type { ChartInteractionState, ChartInteractionStateInit };
112+
export {useChartInteractionState};
113+
export type {ChartInteractionState, ChartInteractionStateInit};
Lines changed: 40 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,46 @@
1-
import { useRef, useState } from 'react';
2-
import { Gesture } from 'react-native-gesture-handler';
3-
import type { SharedValue } from 'react-native-reanimated';
4-
import { useAnimatedReaction, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated';
5-
import { scheduleOnRN } from 'react-native-worklets';
6-
import { TOOLTIP_BAR_GAP } from '@components/Charts/constants';
7-
import { useChartInteractionState } from './useChartInteractionState';
8-
9-
/**
10-
* Arguments passed to the checkIsOver callback for hit-testing
11-
*/
1+
import {useRef, useState} from 'react';
2+
import {Gesture} from 'react-native-gesture-handler';
3+
import type {SharedValue} from 'react-native-reanimated';
4+
import {useAnimatedReaction, useAnimatedStyle, useDerivedValue} from 'react-native-reanimated';
5+
import {scheduleOnRN} from 'react-native-worklets';
6+
import {TOOLTIP_BAR_GAP} from '@components/Charts/constants';
7+
import {useChartInteractionState} from './useChartInteractionState';
8+
9+
const INITIAL_INTERACTION_STATE = {x: 0, y: {y: 0}};
10+
1211
type HitTestArgs = {
13-
/** Current raw X position of the cursor */
1412
cursorX: number;
15-
/** Current raw Y position of the cursor */
1613
cursorY: number;
17-
/** Calculated X position of the matched data point */
1814
targetX: number;
19-
/** Calculated Y position of the matched data point */
2015
targetY: number;
21-
/** The bottom boundary of the chart area */
2216
chartBottom: number;
2317
};
2418

25-
/**
26-
* Configuration for the chart interactions hook
27-
*/
2819
type UseChartInteractionsProps = {
29-
/** Callback triggered when a valid data point is tapped/clicked */
3020
handlePress: (index: number) => void;
31-
/**
32-
* Worklet function to determine if the cursor is technically "hovering"
33-
* over a specific chart element (e.g., within a bar's width or a point's radius).
34-
*/
3521
checkIsOver: (args: HitTestArgs) => boolean;
36-
/** Optional shared value containing bar dimensions used for hit-testing in bar charts */
37-
barGeometry?: SharedValue<{ barWidth: number; chartBottom: number; yZero: number }>;
22+
barGeometry?: SharedValue<{barWidth: number; chartBottom: number; yZero: number}>;
3823
};
3924

40-
/**
41-
* Type for Victory's actionsRef handle.
42-
* Used to manually trigger Victory's internal touch handling logic.
43-
*/
4425
type CartesianActionsHandle = {
4526
handleTouch: (state: unknown, x: number, y: number) => void;
4627
};
4728

48-
/**
49-
* Hook to manage complex chart interactions including hover gestures (web),
50-
* tap gestures (mobile/web), hit-testing, and animated tooltip positioning.
51-
*
52-
* It synchronizes high-frequency interaction data from the UI thread to React state
53-
* for metadata display (like tooltips) and navigation.
54-
*
55-
* @param props - Configuration including press handlers and hit-test logic.
56-
* @returns An object containing refs, gestures, and state for the chart component.
57-
*
58-
* @example
59-
* ```tsx
60-
* const { actionsRef, customGestures, activeDataIndex, isTooltipActive, tooltipStyle } = useChartInteractions({
61-
* handlePress: (index) => console.log("Pressed index:", index),
62-
* checkIsOver: ({ cursorX, targetX, barWidth }) => {
63-
* 'worklet';
64-
* return Math.abs(cursorX - targetX) < barWidth / 2;
65-
* },
66-
* barGeometry: myBarSharedValue,
67-
* });
68-
*
69-
* return (
70-
* <View>
71-
* <CartesianChart customGestures={customGestures} actionsRef={actionsRef} ... />
72-
* {isTooltipActive && <Animated.View style={tooltipStyle}><Tooltip index={activeDataIndex} /></Animated.View>}
73-
* </View>
74-
* );
75-
* ```
76-
*/
77-
function useChartInteractions({ handlePress, checkIsOver, barGeometry }: UseChartInteractionsProps) {
78-
const { state: chartInteractionState, isActive: isTooltipActiveState } = useChartInteractionState({ x: 0, y: { y: 0 } });
79-
const actionsRef = useRef<CartesianActionsHandle>(null);
29+
function useChartInteractions({handlePress, checkIsOver, barGeometry}: UseChartInteractionsProps) {
30+
const {state: chartInteractionState, isActive: isTooltipActiveState} = useChartInteractionState(INITIAL_INTERACTION_STATE);
8031

8132
const [activeDataIndex, setActiveDataIndex] = useState(-1);
8233
const [isOverTarget, setIsOverTarget] = useState(false);
8334

35+
const actionsRef = useRef<CartesianActionsHandle>(null);
36+
37+
const handleTouchWorklet = useDerivedValue(() => {
38+
return actionsRef.current?.handleTouch;
39+
});
40+
8441
const isCursorOverTarget = useDerivedValue(() => {
42+
'worklet';
43+
8544
const cursorX = chartInteractionState.cursor.x.get();
8645
const cursorY = chartInteractionState.cursor.y.get();
8746
const targetX = chartInteractionState.x.position.get();
@@ -111,22 +70,29 @@ function useChartInteractions({ handlePress, checkIsOver, barGeometry }: UseChar
11170
},
11271
);
11372

114-
// React Compiler automatycznie zmemoizuje te obiekty gestów
11573
const hoverGesture = Gesture.Hover()
11674
.onBegin((e) => {
11775
'worklet';
11876

11977
chartInteractionState.isActive.set(true);
12078
chartInteractionState.cursor.x.set(e.x);
12179
chartInteractionState.cursor.y.set(e.y);
122-
actionsRef.current?.handleTouch(chartInteractionState, e.x, e.y);
80+
81+
const touchFn = handleTouchWorklet.get();
82+
if (touchFn) {
83+
touchFn(chartInteractionState, e.x, e.y);
84+
}
12385
})
12486
.onUpdate((e) => {
12587
'worklet';
12688

12789
chartInteractionState.cursor.x.set(e.x);
12890
chartInteractionState.cursor.y.set(e.y);
129-
actionsRef.current?.handleTouch(chartInteractionState, e.x, e.y);
91+
92+
const touchFn = handleTouchWorklet.get();
93+
if (touchFn) {
94+
touchFn(chartInteractionState, e.x, e.y);
95+
}
13096
})
13197
.onEnd(() => {
13298
'worklet';
@@ -140,11 +106,13 @@ function useChartInteractions({ handlePress, checkIsOver, barGeometry }: UseChar
140106
chartInteractionState.cursor.x.set(e.x);
141107
chartInteractionState.cursor.y.set(e.y);
142108

143-
actionsRef.current?.handleTouch(chartInteractionState, e.x, e.y);
144-
const matchedIndex = chartInteractionState.matchedIndex.get();
145-
const isOver = isCursorOverTarget.get();
109+
const touchFn = handleTouchWorklet.get();
110+
if (touchFn) {
111+
touchFn(chartInteractionState, e.x, e.y);
112+
}
146113

147-
if (matchedIndex >= 0 && isOver) {
114+
const matchedIndex = chartInteractionState.matchedIndex.get();
115+
if (matchedIndex >= 0 && isCursorOverTarget.get()) {
148116
scheduleOnRN(handlePress, matchedIndex);
149117
}
150118
});
@@ -156,14 +124,13 @@ function useChartInteractions({ handlePress, checkIsOver, barGeometry }: UseChar
156124
const targetY = chartInteractionState.y.y.position.get();
157125
const yZero = barGeometry?.get().yZero ?? targetY;
158126
const barTopY = Math.min(targetY, yZero);
159-
160127
const isVisible = chartInteractionState.isActive.get() && isCursorOverTarget.get();
161128

162129
return {
163130
position: 'absolute',
164131
left: posX,
165132
top: barTopY - TOOLTIP_BAR_GAP,
166-
transform: [{ translateX: '-50%' }, { translateY: '-100%' }],
133+
transform: [{translateX: '-50%'}, {translateY: '-100%'}],
167134
opacity: isVisible ? 1 : 0,
168135
};
169136
});
@@ -177,5 +144,5 @@ function useChartInteractions({ handlePress, checkIsOver, barGeometry }: UseChar
177144
};
178145
}
179146

180-
export { useChartInteractions };
181-
export type { HitTestArgs };
147+
export {useChartInteractions};
148+
export type {HitTestArgs};

src/components/Charts/hooks/useChartLabelFormats.ts

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import {useCallback} from 'react';
2-
31
type ChartDataPoint = {
42
label: string;
53
};
@@ -13,38 +11,31 @@ type UseChartLabelFormatsProps = {
1311
truncatedLabels: string[];
1412
};
1513

14+
/**
15+
* Hook for styling chart labels.
16+
*/
1617
export default function useChartLabelFormats({data, yAxisUnit, yAxisUnitPosition = 'left', labelSkipInterval, labelRotation, truncatedLabels}: UseChartLabelFormatsProps) {
17-
const formatYAxisLabel = useCallback(
18-
(value: number) => {
19-
const formatted = value.toLocaleString();
20-
if (!yAxisUnit) {
21-
return formatted;
22-
}
23-
// Add space for multi-character codes (e.g., "PLN 100") but not for symbols (e.g., "$100")
24-
const separator = yAxisUnit.length > 1 ? ' ' : '';
25-
return yAxisUnitPosition === 'left' ? `${yAxisUnit}${separator}${formatted}` : `${formatted}${separator}${yAxisUnit}`;
26-
},
27-
[yAxisUnit, yAxisUnitPosition],
28-
);
29-
30-
const formatXAxisLabel = useCallback(
31-
(value: number) => {
32-
const index = Math.round(value);
33-
34-
// Skip labels based on calculated interval
35-
if (index % labelSkipInterval !== 0) {
36-
return '';
37-
}
38-
39-
// Use pre-truncated labels
40-
// If rotation is vertical (-90), we usually want full labels
41-
// because they have more space vertically.
42-
const sourceToUse = labelRotation === -90 ? data.map((p) => p.label) : truncatedLabels;
43-
44-
return sourceToUse.at(index) ?? '';
45-
},
46-
[labelSkipInterval, labelRotation, truncatedLabels, data],
47-
);
18+
const formatYAxisLabel = (value: number) => {
19+
const formatted = value.toLocaleString();
20+
if (!yAxisUnit) {
21+
return formatted;
22+
}
23+
24+
const separator = yAxisUnit.length > 1 ? ' ' : '';
25+
return yAxisUnitPosition === 'left' ? `${yAxisUnit}${separator}${formatted}` : `${formatted}${separator}${yAxisUnit}`;
26+
};
27+
28+
const formatXAxisLabel = (value: number) => {
29+
const index = Math.round(value);
30+
31+
if (index % labelSkipInterval !== 0) {
32+
return '';
33+
}
34+
35+
const sourceToUse = labelRotation === -90 ? data.map((p) => p.label) : truncatedLabels;
36+
37+
return sourceToUse.at(index) ?? '';
38+
};
4839

4940
return {
5041
formatXAxisLabel,

0 commit comments

Comments
 (0)