Skip to content

Conversation

@JCQuintas
Copy link
Member

Fixes #20082

@JCQuintas JCQuintas requested a review from arminmeh October 24, 2025 13:30
@JCQuintas JCQuintas self-assigned this Oct 24, 2025
@JCQuintas JCQuintas added scope: data grid Changes related to the data grid. type: regression A bug, but worse, it used to behave as expected. plan: Premium Impact at least one Premium user. scope: charts Changes related to the charts. labels Oct 24, 2025
@mui-bot
Copy link

mui-bot commented Oct 24, 2025

Deploy preview: https://deploy-preview-20083--material-ui-x.netlify.app/

Bundle size report

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts ▼-36B(-0.01%) ▼-8B(-0.01%)
@mui/x-charts-pro ▼-36B(-0.01%) ▼-9B(-0.01%)
@mui/x-charts-premium ▼-36B(-0.01%) ▼-9B(-0.01%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against 3c73437

Copy link
Member

@bernardobelchior bernardobelchior left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain what was the root cause for the issue and why this fixes it?

It's a bit hard to understand without more context.

},
{
memoizeOptions: {
equalityCheck: isDeepEqual,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this only works because getNormalizedAxisScale always returns a new scale, making this isDeepEqual unnecessary because it will always return false.

The reason for isDeepEqual in the normalized domain was to avoid recomputing scale selectors if the domain didn't change, so I think this is negating the performance improvements from #19790 since we'll always be re-creating the Flatbush data structure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is resultEqualityCheck and equalityCheck. This is checking the parameters, not the result. The in the original, the selectorChartFilteredXDomains always re-runs regardless of value, then it checks if the result is equal and return it if they are different.

This is kinda useless, since in selectorChartNormalizedXScales, which is the only place selectorChartFilteredXDomains is used, we depend on selectorChartRawXAxis which is pretty much always different.

Our options are to either memoize the parameters of selectorChartNormalizedXScales, at the cost of traversing and checking the scales, or not memoize at all at the cost of re-render.


The general cause seems to be that @arminmeh mentioned in the body of the issue #20082

But I fail to understand why exactly it happens.

It seems that due to the memoization, the incoming xAxis[].data gets desynced from the actual data used in the scaleBand(domain), where the original arrays change, but their content doesn't.

I believe the solution proposed here solves that by syncing the result of the nomalizer selector to retrigger downstream selectors whenever the deep equal of its inputs changes, at the cost of having to deep traverse the axes every time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is checking the parameters, not the result

I didn't realize that, thanks for calling it out. I wonder if it'll end up being more expensive, though, as checking an object of size-2 arrays is probably cheaper than doing a deep equal of all the arguments. Especially since selectorChartRawXAxis can contain the data array in line and bar charts, which can be a large array.

This is kinda useless, since in selectorChartNormalizedXScales, which is the only place selectorChartFilteredXDomains is used, we depend on selectorChartRawXAxis which is pretty much always different.

selectorChartFilteredXDomains indirectly depends on selectorChartZoomMap. The use case for memoizing the result of the filtered domain was to avoid re-creating the normalized scales when the zoom map changed but didn't cause the domains to change. In that case, we don't want to re-create the scales because it'll cause the flatbush to be re-indexed which is somewhat expensive.

When the axis changes, it'll recompute the world, it's true. We need additional selector work to fix that.

The general cause seems to be that @arminmeh mentioned in the body of the issue #20082

But I fail to understand why exactly it happens.

It seems the root cause isn't entirely known. I'll spend some time investigating this to understand the issue

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I found the issue and why this fix works.

It seems that the line @arminmeh mentioned (xScale(baseValue)) finds the baseValue within the xScale.domain().

d3's ordinal scale uses internmap to find compare non-primitives, but it relies on valueOf(). For dates, this works because its valueOf() is a unix timestamp, which is a primitive that can be compared. For arrays, however, this doesn't work (['a','b'].valueOf() === ['a','b'].valueOf() returns false).

Our data is an array of arrays because we're using grouping. The initial render works fine because they're the same reference. However, due to domain memoization, whenever a re-render happens, the arrays in data will be different from the arrays in the scale's domains, causing xScale(baseValue) to return an invalid value.

The change from this PR fixes the issue by breaking memoization whenever the axis changes, which causes the reference to be the same, but to be honest this doesn't address the root cause.

To me, the issue at the root of this problem is how d3 is comparing values when they aren't primitives. IMO, that's what we need to fix.


You can reproduce my assertion by adding the following code here:

if (dataIndex === 0) {
  console.log(xScale.domain()[0], baseValue, xScale.domain()[0] === baseValue);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more piece of information that might help
When I was debugging this, I noticed that the data from useBarPlotData is calculated twice when I do the change that breaks the chart.
First time the data seems to be correct, but the second time the x value becomes NaN (reasons mentioned above)

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 24, 2025

CodSpeed Performance Report

Merging #20083 will not alter performance

Comparing JCQuintas:fix-integration (3c73437) with master (e8522b1)1

Summary

✅ 12 untouched
🆕 1 new

Benchmarks breakdown

Mode Benchmark BASE HEAD Change
🆕 Simulation SankeyChart with big data amount N/A 134.3 ms N/A

Footnotes

  1. No successful run was found on master (9dd9268) during the generation of this report, so e8522b1 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@JCQuintas JCQuintas marked this pull request as draft October 24, 2025 13:56
@JCQuintas JCQuintas marked this pull request as ready for review October 24, 2025 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plan: Premium Impact at least one Premium user. scope: charts Changes related to the charts. scope: data grid Changes related to the data grid. type: regression A bug, but worse, it used to behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[charts] Render errors with grouped x-axis after prop updates

4 participants