Skip to content

Commit b949938

Browse files
[charts] Improve performance of rendering ticks in x-axis (#16536)
1 parent 7405c3e commit b949938

File tree

2 files changed

+83
-67
lines changed

2 files changed

+83
-67
lines changed

packages/x-charts/src/ChartsXAxis/ChartsXAxis.tsx

+82-67
Original file line numberDiff line numberDiff line change
@@ -32,62 +32,79 @@ const useUtilityClasses = (ownerState: AxisConfig<any, any, ChartsXAxisProps>) =
3232
return composeClasses(slots, getAxisUtilityClass, classes);
3333
};
3434

35-
type LabelExtraData = { width: number; height: number; skipLabel?: boolean };
36-
37-
function addLabelDimension(
35+
/* Returns a set of indices of the tick labels that should be visible. */
36+
function getVisibleLabels(
3837
xTicks: TickItemType[],
3938
{
4039
tickLabelStyle: style,
4140
tickLabelInterval,
4241
tickLabelMinGap,
4342
reverse,
4443
isMounted,
44+
isPointInside,
4545
}: Pick<ChartsXAxisProps, 'tickLabelInterval' | 'tickLabelStyle'> &
4646
Pick<AxisDefaultized<ScaleName, any, ChartsXAxisProps>, 'reverse'> & {
4747
isMounted: boolean;
4848
tickLabelMinGap: NonNullable<ChartsXAxisProps['tickLabelMinGap']>;
49+
isPointInside: (position: number) => boolean;
4950
},
50-
): (TickItemType & LabelExtraData)[] {
51-
const withDimension = xTicks.map((tick) => {
51+
): Set<TickItemType> {
52+
const getTickLabelSize = (tick: TickItemType) => {
5253
if (!isMounted || tick.formattedValue === undefined) {
53-
return { ...tick, width: 0, height: 0 };
54+
return { width: 0, height: 0 };
5455
}
56+
5557
const tickSizes = getWordsByLines({ style, needsComputation: true, text: tick.formattedValue });
58+
5659
return {
57-
...tick,
5860
width: Math.max(...tickSizes.map((size) => size.width)),
5961
height: Math.max(tickSizes.length * tickSizes[0].height),
6062
};
61-
});
63+
};
6264

6365
if (typeof tickLabelInterval === 'function') {
64-
return withDimension.map((item, index) => ({
65-
...item,
66-
skipLabel: !tickLabelInterval(item.value, index),
67-
}));
66+
return new Set(xTicks.filter((item, index) => tickLabelInterval(item.value, index)));
6867
}
6968

7069
// Filter label to avoid overlap
7170
let previousTextLimit = 0;
7271
const direction = reverse ? -1 : 1;
73-
return withDimension.map((item, labelIndex) => {
74-
const { width, offset, labelOffset, height } = item;
75-
76-
const distance = getMinXTranslation(width, height, style?.angle);
77-
const textPosition = offset + labelOffset;
78-
79-
const currentTextLimit = textPosition - (direction * distance) / 2;
80-
if (
81-
labelIndex > 0 &&
82-
direction * currentTextLimit < direction * (previousTextLimit + tickLabelMinGap)
83-
) {
84-
// Except for the first label, we skip all label that overlap with the last accepted.
85-
// Notice that the early return prevents `previousTextLimit` from being updated.
86-
return { ...item, skipLabel: true };
87-
}
88-
previousTextLimit = textPosition + (direction * distance) / 2;
89-
return item;
90-
});
72+
73+
return new Set(
74+
xTicks.filter((item, labelIndex) => {
75+
const { offset, labelOffset } = item;
76+
const textPosition = offset + labelOffset;
77+
78+
if (
79+
labelIndex > 0 &&
80+
direction * textPosition < direction * (previousTextLimit + tickLabelMinGap)
81+
) {
82+
return false;
83+
}
84+
85+
if (!isPointInside(textPosition)) {
86+
return false;
87+
}
88+
89+
/* Measuring text width is expensive, so we need to delay it as much as possible to improve performance. */
90+
const { width, height } = getTickLabelSize(item);
91+
92+
const distance = getMinXTranslation(width, height, style?.angle);
93+
94+
const currentTextLimit = textPosition - (direction * distance) / 2;
95+
if (
96+
labelIndex > 0 &&
97+
direction * currentTextLimit < direction * (previousTextLimit + tickLabelMinGap)
98+
) {
99+
// Except for the first label, we skip all label that overlap with the last accepted.
100+
// Notice that the early return prevents `previousTextLimit` from being updated.
101+
return false;
102+
}
103+
104+
previousTextLimit = textPosition + (direction * distance) / 2;
105+
return true;
106+
}),
107+
);
91108
}
92109

93110
const XAxisRoot = styled(AxisRoot, {
@@ -184,12 +201,13 @@ function ChartsXAxis(inProps: ChartsXAxisProps) {
184201
tickLabelPlacement,
185202
});
186203

187-
const xTicksWithDimension = addLabelDimension(xTicks, {
204+
const visibleLabels = getVisibleLabels(xTicks, {
188205
tickLabelStyle: axisTickLabelProps.style,
189206
tickLabelInterval,
190207
tickLabelMinGap,
191208
reverse,
192209
isMounted,
210+
isPointInside: (x: number) => instance.isPointInside({ x, y: -1 }, { direction: 'x' }),
193211
});
194212

195213
const labelRefPoint = {
@@ -229,42 +247,39 @@ function ChartsXAxis(inProps: ChartsXAxisProps) {
229247
<Line x1={left} x2={left + width} className={classes.line} {...slotProps?.axisLine} />
230248
)}
231249

232-
{xTicksWithDimension.map(
233-
({ formattedValue, offset: tickOffset, labelOffset, skipLabel }, index) => {
234-
const xTickLabel = labelOffset ?? 0;
235-
const yTickLabel = positionSign * (tickSize + 3);
236-
237-
const showTick = instance.isPointInside({ x: tickOffset, y: -1 }, { direction: 'x' });
238-
const showTickLabel = instance.isPointInside(
239-
{ x: tickOffset + xTickLabel, y: -1 },
240-
{ direction: 'x' },
241-
);
242-
return (
243-
<g
244-
key={index}
245-
transform={`translate(${tickOffset}, 0)`}
246-
className={classes.tickContainer}
247-
>
248-
{!disableTicks && showTick && (
249-
<Tick
250-
y2={positionSign * tickSize}
251-
className={classes.tick}
252-
{...slotProps?.axisTick}
253-
/>
254-
)}
255-
256-
{formattedValue !== undefined && !skipLabel && showTickLabel && (
257-
<TickLabel
258-
x={xTickLabel}
259-
y={yTickLabel}
260-
{...axisTickLabelProps}
261-
text={formattedValue.toString()}
262-
/>
263-
)}
264-
</g>
265-
);
266-
},
267-
)}
250+
{xTicks.map((item, index) => {
251+
const { formattedValue, offset: tickOffset, labelOffset } = item;
252+
const xTickLabel = labelOffset ?? 0;
253+
const yTickLabel = positionSign * (tickSize + 3);
254+
255+
const showTick = instance.isPointInside({ x: tickOffset, y: -1 }, { direction: 'x' });
256+
const showTickLabel = visibleLabels.has(item);
257+
258+
return (
259+
<g
260+
key={index}
261+
transform={`translate(${tickOffset}, 0)`}
262+
className={classes.tickContainer}
263+
>
264+
{!disableTicks && showTick && (
265+
<Tick
266+
y2={positionSign * tickSize}
267+
className={classes.tick}
268+
{...slotProps?.axisTick}
269+
/>
270+
)}
271+
272+
{formattedValue !== undefined && showTickLabel && (
273+
<TickLabel
274+
x={xTickLabel}
275+
y={yTickLabel}
276+
{...axisTickLabelProps}
277+
text={formattedValue.toString()}
278+
/>
279+
)}
280+
</g>
281+
);
282+
})}
268283

269284
{label && (
270285
<g className={classes.label}>

packages/x-charts/src/internals/domUtils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export const getStyleString = (style: React.CSSProperties) =>
9898
);
9999

100100
let domCleanTimeout: NodeJS.Timeout | undefined;
101+
101102
/**
102103
*
103104
* @param text The string to estimate

0 commit comments

Comments
 (0)