Skip to content

Commit d09bb65

Browse files
committed
Unify label hit-testing between the charts
1 parent c54295c commit d09bb65

4 files changed

Lines changed: 22 additions & 91 deletions

File tree

src/components/Charts/BarChart/BarChartContent.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import ChartTooltipLayer from '@components/Charts/components/ChartTooltipLayer';
1010
import ChartXAxisLabels from '@components/Charts/components/ChartXAxisLabels';
1111
import ChartYAxisLabels from '@components/Charts/components/ChartYAxisLabels';
1212
import {AXIS_LABEL_GAP, CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, GLYPH_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
13-
import type {ComputeGeometryFn, HitTestArgs} from '@components/Charts/hooks';
13+
import type {HitTestArgs} from '@components/Charts/hooks';
1414
import {
1515
useChartFontManager,
1616
useChartInteractions,
@@ -22,7 +22,7 @@ import {
2222
useYAxisLabelWidth,
2323
} from '@components/Charts/hooks';
2424
import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
25-
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getAdditionalOffset, getChartColor, rotatedLabelYOffset} from '@components/Charts/utils';
25+
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getChartColor} from '@components/Charts/utils';
2626
import useTheme from '@hooks/useTheme';
2727
import useThemeStyles from '@hooks/useThemeStyles';
2828
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
@@ -36,29 +36,6 @@ const BAR_INNER_PADDING = 0.3;
3636
*/
3737
const BASE_DOMAIN_PADDING = {top: 32, bottom: 1, left: 0, right: 0};
3838

39-
/**
40-
* Bar chart geometry for label hit-testing.
41-
* Labels are center-anchored: the 45° parallelogram's upper-right corner is offset
42-
* by (halfLabelWidth * sinA) right and up, so the box straddles the tick symmetrically.
43-
*/
44-
const computeBarLabelGeometry: ComputeGeometryFn = ({ascent, descent, sinA, angleRad, labelWidths, padding}) => {
45-
const maxLabelWidth = labelWidths.length > 0 ? Math.max(...labelWidths) : 0;
46-
const centeredUpwardOffset = angleRad > 0 ? (maxLabelWidth / 2) * sinA : 0;
47-
const halfLabelSins = labelWidths.map((w) => (w / 2) * sinA - variables.iconSizeExtraSmall / 3);
48-
const halfWidths = labelWidths.map((w) => w / 2);
49-
const additionalOffset = getAdditionalOffset(angleRad);
50-
return {
51-
labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) + centeredUpwardOffset - additionalOffset,
52-
iconSin: variables.iconSizeExtraSmall * sinA,
53-
labelSins: labelWidths.map((w) => w * sinA),
54-
halfWidths,
55-
cornerAnchorDX: halfLabelSins,
56-
cornerAnchorDY: halfLabelSins.map((v) => -v),
57-
yMin90Offsets: halfWidths.map((hw) => -hw + padding),
58-
yMax90Offsets: halfWidths.map((hw) => hw + padding),
59-
};
60-
};
61-
6239
type BarChartProps = CartesianChartProps & {
6340
/** Callback when a bar is pressed */
6441
onBarPress?: (dataPoint: ChartDataPoint, index: number) => void;
@@ -141,7 +118,6 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left'
141118
labelRotation,
142119
labelSkipInterval,
143120
chartBottom,
144-
computeGeometry: computeBarLabelGeometry,
145121
});
146122

147123
const handleChartBoundsChange = (bounds: ChartBounds) => {

src/components/Charts/LineChart/LineChartContent.tsx

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import ChartYAxisLabels from '@components/Charts/components/ChartYAxisLabels';
1212
import LeftFrameLine from '@components/Charts/components/LeftFrameLine';
1313
import ScatterPoints from '@components/Charts/components/ScatterPoints';
1414
import {AXIS_LABEL_GAP, CHART_CONTENT_MIN_HEIGHT, CHART_PADDING, GLYPH_PADDING, X_AXIS_LINE_WIDTH, Y_AXIS_LINE_WIDTH, Y_AXIS_TICK_COUNT} from '@components/Charts/constants';
15-
import type {ComputeGeometryFn, HitTestArgs} from '@components/Charts/hooks';
15+
import type {HitTestArgs} from '@components/Charts/hooks';
1616
import {
1717
useChartFontManager,
1818
useChartInteractions,
@@ -24,7 +24,7 @@ import {
2424
useYAxisLabelWidth,
2525
} from '@components/Charts/hooks';
2626
import type {CartesianChartProps, ChartDataPoint} from '@components/Charts/types';
27-
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR, getAdditionalOffset, rotatedLabelYOffset} from '@components/Charts/utils';
27+
import {calculateMinDomainPadding, DEFAULT_CHART_COLOR} from '@components/Charts/utils';
2828
import useTheme from '@hooks/useTheme';
2929
import useThemeStyles from '@hooks/useThemeStyles';
3030
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
@@ -42,26 +42,6 @@ const MIN_SAFE_PADDING = DOT_RADIUS + DOT_HOVER_EXTRA_RADIUS;
4242
/** Base domain padding applied to all sides */
4343
const BASE_DOMAIN_PADDING = {top: 16, bottom: 16, left: 0, right: 0};
4444

45-
/**
46-
* Line chart geometry for label hit-testing.
47-
* Labels are start-anchored at the tick: the 45° parallelogram's upper-right corner is
48-
* offset by (iconSize/3 * sinA) left and down, placing the box just below the axis line.
49-
*/
50-
const computeLineLabelGeometry: ComputeGeometryFn = ({ascent, descent, sinA, angleRad, labelWidths, padding}) => {
51-
const iconThirdSin = (variables.iconSizeExtraSmall / 3) * sinA;
52-
const additionalOffset = getAdditionalOffset(angleRad);
53-
return {
54-
labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) - additionalOffset,
55-
iconSin: variables.iconSizeExtraSmall * sinA,
56-
labelSins: labelWidths.map((w) => w * sinA),
57-
halfWidths: labelWidths.map((w) => w / 2),
58-
cornerAnchorDX: labelWidths.map(() => -iconThirdSin),
59-
cornerAnchorDY: labelWidths.map(() => iconThirdSin),
60-
yMin90Offsets: labelWidths.map(() => padding),
61-
yMax90Offsets: labelWidths.map((w) => w + padding),
62-
};
63-
};
64-
6545
type LineChartProps = CartesianChartProps & {
6646
/** Callback when a data point is pressed */
6747
onPointPress?: (dataPoint: ChartDataPoint, index: number) => void;
@@ -157,7 +137,6 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left
157137
labelRotation,
158138
labelSkipInterval,
159139
chartBottom,
160-
computeGeometry: computeLineLabelGeometry,
161140
});
162141

163142
const handleChartBoundsChange = (bounds: ChartBounds) => {

src/components/Charts/hooks/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ export {default as useChartLabelFormats} from './useChartLabelFormats';
99
export {default as useDynamicYDomain} from './useDynamicYDomain';
1010
export {useTooltipData} from './useTooltipData';
1111
export {default as useLabelHitTesting} from './useLabelHitTesting';
12-
export type {ComputeGeometryFn, ComputeGeometryInput} from './useLabelHitTesting';

src/components/Charts/hooks/useLabelHitTesting.ts

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type {SkTypefaceFontProvider} from '@shopify/react-native-skia';
22
import type {SharedValue} from 'react-native-reanimated';
33
import {useSharedValue} from 'react-native-reanimated';
44
import type {Scale} from 'victory-native';
5-
import {DIAGONAL_ANGLE_RADIAN_THRESHOLD} from '@components/Charts/constants';
5+
import {AXIS_LABEL_GAP, DIAGONAL_ANGLE_RADIAN_THRESHOLD} from '@components/Charts/constants';
66
import type {LabelRotation} from '@components/Charts/types';
7-
import {getFontLineMetrics, isCursorOverChartLabel} from '@components/Charts/utils';
7+
import {getAdditionalOffset, getFontLineMetrics, isCursorOverChartLabel, rotatedLabelYOffset} from '@components/Charts/utils';
88
import variables from '@styles/variables';
99
import type {HitTestArgs} from './useChartInteractions';
1010

@@ -34,28 +34,6 @@ type LabelHitGeometry = {
3434
yMax90Offsets: number[];
3535
};
3636

37-
type ComputeGeometryInput = {
38-
/** The ascent of the font */
39-
ascent: number;
40-
41-
/** The descent of the font */
42-
descent: number;
43-
44-
/** The sine of the angle */
45-
sinA: number;
46-
47-
/** The angle in radians */
48-
angleRad: number;
49-
50-
/** The widths of the labels */
51-
labelWidths: number[];
52-
53-
/** The padding of the labels */
54-
padding: number;
55-
};
56-
57-
type ComputeGeometryFn = (input: ComputeGeometryInput) => LabelHitGeometry;
58-
5937
type UseLabelHitTestingParams = {
6038
fontMgr: SkTypefaceFontProvider | null | undefined;
6139
fontSize: number;
@@ -64,45 +42,45 @@ type UseLabelHitTestingParams = {
6442
labelRotation: LabelRotation;
6543
labelSkipInterval: number;
6644
chartBottom: SharedValue<number>;
67-
68-
/**
69-
* Chart-specific geometry factory.
70-
* Receives font metrics, trig values, and per-label widths; returns the
71-
* normalized geometry shape. Typically a module-level constant.
72-
*/
73-
computeGeometry: ComputeGeometryFn;
7445
};
7546

7647
/**
7748
* Shared hook for x-axis label hit-testing in cartesian charts.
7849
*
7950
* Encapsulates angle conversion, pre-computed hit geometry, and the
8051
* isCursorOverLabel / findLabelCursorX worklets — all of which are identical
81-
* between bar and line chart except for how the hit geometry is computed.
52+
* between bar and line charts.
8253
*
8354
* Label widths are accepted as a pre-computed array (from useChartLabelLayout)
8455
* so no Skia measurement happens here.
8556
*
86-
* Chart-specific geometry (45° corner anchor offsets, 90° vertical bounds) is supplied
87-
* via the `computeGeometry` callback, typically a module-level constant.
57+
* Labels are right-aligned at the tick: the 45° parallelogram's upper-right corner is
58+
* offset by (iconSize/3 * sinA) left and down, placing the box just below the axis line.
8859
*/
89-
function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotation, labelSkipInterval, chartBottom, computeGeometry}: UseLabelHitTestingParams) {
60+
function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotation, labelSkipInterval, chartBottom}: UseLabelHitTestingParams) {
9061
const tickXPositions = useSharedValue<number[]>([]);
9162

9263
const angleRad = (Math.abs(labelRotation) * Math.PI) / 180;
9364

9465
const fontMetrics = fontMgr ? getFontLineMetrics(fontMgr, fontSize) : null;
9566

96-
/**
97-
* Geometry for label hit-testing. The `computeGeometry` callback supplies the
98-
* chart-specific differences (bar vs. line anchor offsets).
99-
*/
10067
let labelHitGeometry: LabelHitGeometry | null = null;
10168
if (fontMetrics) {
10269
const {ascent, descent} = fontMetrics;
10370
const sinA = Math.sin(angleRad);
10471
const padding = variables.iconSizeExtraSmall / 2;
105-
labelHitGeometry = computeGeometry({ascent, descent, sinA, angleRad, labelWidths: truncatedLabelWidths, padding});
72+
const iconThirdSin = (variables.iconSizeExtraSmall / 3) * sinA;
73+
const additionalOffset = getAdditionalOffset(angleRad);
74+
labelHitGeometry = {
75+
labelYOffset: AXIS_LABEL_GAP + rotatedLabelYOffset(ascent, descent, angleRad) - additionalOffset,
76+
iconSin: variables.iconSizeExtraSmall * sinA,
77+
labelSins: truncatedLabelWidths.map((w) => w * sinA),
78+
halfWidths: truncatedLabelWidths.map((w) => w / 2),
79+
cornerAnchorDX: truncatedLabelWidths.map(() => -iconThirdSin),
80+
cornerAnchorDY: truncatedLabelWidths.map(() => iconThirdSin),
81+
yMin90Offsets: truncatedLabelWidths.map(() => padding),
82+
yMax90Offsets: truncatedLabelWidths.map((w) => w + padding),
83+
};
10684
}
10785

10886
/**
@@ -183,4 +161,3 @@ function useLabelHitTesting({fontMgr, fontSize, truncatedLabelWidths, labelRotat
183161
}
184162

185163
export default useLabelHitTesting;
186-
export type {ComputeGeometryFn, ComputeGeometryInput, LabelHitGeometry};

0 commit comments

Comments
 (0)