Skip to content

Commit

Permalink
[charts] Improve performance of rendering ticks in x-axis (#16536)
Browse files Browse the repository at this point in the history
  • Loading branch information
bernardobelchior authored Feb 21, 2025
1 parent 7405c3e commit b949938
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 67 deletions.
149 changes: 82 additions & 67 deletions packages/x-charts/src/ChartsXAxis/ChartsXAxis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,62 +32,79 @@ const useUtilityClasses = (ownerState: AxisConfig<any, any, ChartsXAxisProps>) =
return composeClasses(slots, getAxisUtilityClass, classes);
};

type LabelExtraData = { width: number; height: number; skipLabel?: boolean };

function addLabelDimension(
/* Returns a set of indices of the tick labels that should be visible. */
function getVisibleLabels(
xTicks: TickItemType[],
{
tickLabelStyle: style,
tickLabelInterval,
tickLabelMinGap,
reverse,
isMounted,
isPointInside,
}: Pick<ChartsXAxisProps, 'tickLabelInterval' | 'tickLabelStyle'> &
Pick<AxisDefaultized<ScaleName, any, ChartsXAxisProps>, 'reverse'> & {
isMounted: boolean;
tickLabelMinGap: NonNullable<ChartsXAxisProps['tickLabelMinGap']>;
isPointInside: (position: number) => boolean;
},
): (TickItemType & LabelExtraData)[] {
const withDimension = xTicks.map((tick) => {
): Set<TickItemType> {
const getTickLabelSize = (tick: TickItemType) => {
if (!isMounted || tick.formattedValue === undefined) {
return { ...tick, width: 0, height: 0 };
return { width: 0, height: 0 };
}

const tickSizes = getWordsByLines({ style, needsComputation: true, text: tick.formattedValue });

return {
...tick,
width: Math.max(...tickSizes.map((size) => size.width)),
height: Math.max(tickSizes.length * tickSizes[0].height),
};
});
};

if (typeof tickLabelInterval === 'function') {
return withDimension.map((item, index) => ({
...item,
skipLabel: !tickLabelInterval(item.value, index),
}));
return new Set(xTicks.filter((item, index) => tickLabelInterval(item.value, index)));
}

// Filter label to avoid overlap
let previousTextLimit = 0;
const direction = reverse ? -1 : 1;
return withDimension.map((item, labelIndex) => {
const { width, offset, labelOffset, height } = item;

const distance = getMinXTranslation(width, height, style?.angle);
const textPosition = offset + labelOffset;

const currentTextLimit = textPosition - (direction * distance) / 2;
if (
labelIndex > 0 &&
direction * currentTextLimit < direction * (previousTextLimit + tickLabelMinGap)
) {
// Except for the first label, we skip all label that overlap with the last accepted.
// Notice that the early return prevents `previousTextLimit` from being updated.
return { ...item, skipLabel: true };
}
previousTextLimit = textPosition + (direction * distance) / 2;
return item;
});

return new Set(
xTicks.filter((item, labelIndex) => {
const { offset, labelOffset } = item;
const textPosition = offset + labelOffset;

if (
labelIndex > 0 &&
direction * textPosition < direction * (previousTextLimit + tickLabelMinGap)
) {
return false;
}

if (!isPointInside(textPosition)) {
return false;
}

/* Measuring text width is expensive, so we need to delay it as much as possible to improve performance. */
const { width, height } = getTickLabelSize(item);

const distance = getMinXTranslation(width, height, style?.angle);

const currentTextLimit = textPosition - (direction * distance) / 2;
if (
labelIndex > 0 &&
direction * currentTextLimit < direction * (previousTextLimit + tickLabelMinGap)
) {
// Except for the first label, we skip all label that overlap with the last accepted.
// Notice that the early return prevents `previousTextLimit` from being updated.
return false;
}

previousTextLimit = textPosition + (direction * distance) / 2;
return true;
}),
);
}

const XAxisRoot = styled(AxisRoot, {
Expand Down Expand Up @@ -184,12 +201,13 @@ function ChartsXAxis(inProps: ChartsXAxisProps) {
tickLabelPlacement,
});

const xTicksWithDimension = addLabelDimension(xTicks, {
const visibleLabels = getVisibleLabels(xTicks, {
tickLabelStyle: axisTickLabelProps.style,
tickLabelInterval,
tickLabelMinGap,
reverse,
isMounted,
isPointInside: (x: number) => instance.isPointInside({ x, y: -1 }, { direction: 'x' }),
});

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

{xTicksWithDimension.map(
({ formattedValue, offset: tickOffset, labelOffset, skipLabel }, index) => {
const xTickLabel = labelOffset ?? 0;
const yTickLabel = positionSign * (tickSize + 3);

const showTick = instance.isPointInside({ x: tickOffset, y: -1 }, { direction: 'x' });
const showTickLabel = instance.isPointInside(
{ x: tickOffset + xTickLabel, y: -1 },
{ direction: 'x' },
);
return (
<g
key={index}
transform={`translate(${tickOffset}, 0)`}
className={classes.tickContainer}
>
{!disableTicks && showTick && (
<Tick
y2={positionSign * tickSize}
className={classes.tick}
{...slotProps?.axisTick}
/>
)}

{formattedValue !== undefined && !skipLabel && showTickLabel && (
<TickLabel
x={xTickLabel}
y={yTickLabel}
{...axisTickLabelProps}
text={formattedValue.toString()}
/>
)}
</g>
);
},
)}
{xTicks.map((item, index) => {
const { formattedValue, offset: tickOffset, labelOffset } = item;
const xTickLabel = labelOffset ?? 0;
const yTickLabel = positionSign * (tickSize + 3);

const showTick = instance.isPointInside({ x: tickOffset, y: -1 }, { direction: 'x' });
const showTickLabel = visibleLabels.has(item);

return (
<g
key={index}
transform={`translate(${tickOffset}, 0)`}
className={classes.tickContainer}
>
{!disableTicks && showTick && (
<Tick
y2={positionSign * tickSize}
className={classes.tick}
{...slotProps?.axisTick}
/>
)}

{formattedValue !== undefined && showTickLabel && (
<TickLabel
x={xTickLabel}
y={yTickLabel}
{...axisTickLabelProps}
text={formattedValue.toString()}
/>
)}
</g>
);
})}

{label && (
<g className={classes.label}>
Expand Down
1 change: 1 addition & 0 deletions packages/x-charts/src/internals/domUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const getStyleString = (style: React.CSSProperties) =>
);

let domCleanTimeout: NodeJS.Timeout | undefined;

/**
*
* @param text The string to estimate
Expand Down

0 comments on commit b949938

Please sign in to comment.