Skip to content

Conversation

@alexfauquette
Copy link
Member

@alexfauquette alexfauquette commented Nov 24, 2025

Before

The sankey introduced a notion of series item with data.

The issue is that data are computed in the SankeyPlot component.

image

Issues

If you want to controled tooltip, you can't expect to user to provide the additional data. They shoudl be able to say "Place tooltip on the node with id xxx"

So we need to be able to derive the ItemIdentifierWithData from the ItemIdentifier.

It's also usefull to compute the item position only once. For example for anchored tooltip, it would be nice to get node/links coordinates

After

image

Modifications

I added in the config an object that defines additional properties that can be added when the processor has access to the drawing area.

It's done such that if none of the series need that the object references stay unchanged

Potential use cases

This could also be usefull for series like the pie chart to compute the center

@alexfauquette alexfauquette added type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. scope: charts Changes related to the charts. labels Nov 24, 2025
@alexfauquette alexfauquette changed the title [charts] Add a series processor aware of the drawing area [charts] Add a series processor using the drawing area Nov 24, 2025
@mui-bot
Copy link

mui-bot commented Nov 24, 2025

Deploy preview: https://deploy-preview-20437--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 🔺+327B(+0.10%) 🔺+121B(+0.12%)
@mui/x-charts-pro 🔺+533B(+0.12%) 🔺+255B(+0.19%)
@mui/x-charts-premium 🔺+361B(+0.08%) 🔺+134B(+0.10%)
@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 325b48b

@codspeed-hq
Copy link

codspeed-hq bot commented Nov 24, 2025

CodSpeed Performance Report

Merging #20437 will not alter performance

Comparing alexfauquette:refactor-sankey (325b48b) with master (6ef52fd)1

Summary

✅ 13 untouched

Footnotes

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

* This selector computes the processed series on-demand from the defaultized series.
* @returns {ProcessedSeries} The processed series.
*/
export const selectorChartSeriesProcessed = createSelectorMemoized(
Copy link
Member

Choose a reason for hiding this comment

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

If we are using the same selector, why not make the regular series processor use the drawing area?

Copy link
Member Author

Choose a reason for hiding this comment

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

With this approach if the drawing area is modified, but no series uses it the selectors is an identity function.

Not sure it's usefull. But if some selector uses the processed series and not the drawing area they should be able to avoid a recomputation. Maybe bernarod's work on the scatter zoom performance can benefit from it. Not sure

Comment on lines 16 to 22
seriesProcessor: SeriesProcessorWithoutDimensions<TSeriesType>;
colorProcessor: ColorProcessor<TSeriesType>;
legendGetter: LegendGetter<TSeriesType>;
tooltipGetter: TooltipGetter<TSeriesType>;
tooltipItemPositionGetter?: TooltipItemPositionGetter<TSeriesType>;
getSeriesWithDefaultValues: GetSeriesWithDefaultValues<TSeriesType>;
seriesProcessorWithDrawingArea?: SeriesProcessor<TSeriesType>;
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't these types names be inverted?

Like

seriesProcessor -> SeriesProcessor<TSeriesType>
seriesProcessorWithDrawingArea -> SeriesProcessorWithDrawingArea<TSeriesType>

or

seriesProcessorWithoutDrawingArea -> SeriesProcessorWithoutDrawingArea<TSeriesType>
seriesProcessor -> SeriesProcessor<TSeriesType>

Copy link
Member Author

Choose a reason for hiding this comment

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

I will got that way

seriesProcessorWithoutDrawingArea -> SeriesProcessorWithoutDrawingArea<TSeriesType>
seriesProcessor -> SeriesProcessor<TSeriesType>

The underlying reason I did that is the type renaming. Lot of stuff rely on the SeriesProcessor so I kept it as it to avoid doubling the number of files with dumb modification and having WithDrawingArea every where in the codebase

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, let's do the renaming in a follow-up PR

Copy link
Member Author

Choose a reason for hiding this comment

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

JC renaming proposal is not that impacting. I can do it now

@bernardobelchior
Copy link
Member

This change seems to be made because the Sankey provides layout information in its data in the onNodeClick/onLinkClick. Would it make sense to remove this layout information from the returned data? The Sankey is still unstable, so we can make this change.

It doesn't make sense to provide the layout information here because it isn't usable. For example, if the sankey resizes, the layout will be outdated, but the user won't be informed of the resize. Or if the sankey data changes, causing a re-layout, then the layout info will also be outdated.

If a user wants to position something relative to a node maybe we should provide a way to do it that is more inline with React, e.g., by using a component that re-renders when the layout changes.

If we remove the layout information from the identifier, we don't need this change, right? Would it make sense to remove it, then?

CC @JCQuintas as you might have more insights

Comment on lines +110 to +112
if (!processingDetected) {
return processedSeries as ProcessedSeries<TSeriesType>;
}
Copy link
Member

Choose a reason for hiding this comment

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

Is processingDetected just to avoid breaking selectors' cache when processing results in the same value?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that way either not providing seriesProcessor() or having seriesProcessor=(x)=>x will be transparent

Comment on lines 16 to 22
seriesProcessor: SeriesProcessorWithoutDimensions<TSeriesType>;
colorProcessor: ColorProcessor<TSeriesType>;
legendGetter: LegendGetter<TSeriesType>;
tooltipGetter: TooltipGetter<TSeriesType>;
tooltipItemPositionGetter?: TooltipItemPositionGetter<TSeriesType>;
getSeriesWithDefaultValues: GetSeriesWithDefaultValues<TSeriesType>;
seriesProcessorWithDrawingArea?: SeriesProcessor<TSeriesType>;
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, let's do the renaming in a follow-up PR

series: DefaultizedBarSeriesType;
/**
* Additional data computed from the series plus drawing area.
* Usefull for special charts like sankey where the series data is not sufficient to draw the series.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Usefull for special charts like sankey where the series data is not sufficient to draw the series.
* Useful for special charts like sankey where the series data is not sufficient to draw the series.

@alexfauquette
Copy link
Member Author

@bernardobelchior About the layout, I agree, the position is not interesting to provide to users. But some other aspect are. In particular the sankey layout provides a nice way to navigate the graph.

If you click on a node, the onClick(item) will allow you to do item.node.sourceLinks and item.node.targetLinks which is more convenient than having to pick item.nodeId and the loop on your data to know which link has a relation with this node

@bernardobelchior
Copy link
Member

will allow you to do item.node.sourceLinks and item.node.targetLinks

Right, but for that we don't need the drawing area, do we?

That's only derived from the data, no need to add the drawing area as a dependency, or am I missing something?

@alexfauquette
Copy link
Member Author

That's only derived from the data, no need to add the drawing area as a dependency, or am I missing something?

Yes but that's done all at once in the calculateSankeyLayout() by th D3 sankey generator

@bernardobelchior
Copy link
Member

Isn't it easier to replicate that logic just for the sankey chart than adding a seriesProcessorWithDrawingArea to all chart types that will only be used by the sankey chart?

As a shortcut, we can use a dummy drawing area just to obtain the same data structure and hide the layout information. Then, if we want to, we can replace that with our implementation which would probably be faster since we don't need to lay out the nodes and links. Obviously, we'd still keep d3-sankey for layout.

Additionally, we could make it so that targetLinks and sourceLinks are getters to delay computation if not used by the end user.

What do you think?

@alexfauquette
Copy link
Member Author

Isn't it easier to replicate that logic just for the sankey chart than adding a seriesProcessorWithDrawingArea to all chart types that will only be used by the sankey chart?

For me this solution is more future proof. For example it solves the issue about node anchoring for sankey

For now most of our charts are a mix between an "axis management" + "series management"

And the tooltip, highlight are designed from them. Based on a useSeries and useAxis we get all the information we want

With special charts like the sankey or the tree map, having a special piece of code to handle coordinates based on the drawing area does not make that much sense since it will not be shared with others

The proposal is to have

  • useSeries + useAxis allows to plot common charts
  • useSeries alone allows to plot specific charts

The adventage for us is to keep the tooltip/highlight management consistent with the other series

@bernardobelchior
Copy link
Member

I'm not sure I got your comment 😅

useSeries alone allows to plot specific charts

So the idea is that for sankey the useSeries hook should return the layout as well?

In other charts, useSeries doesn't return the layout. It's the useAxis hook that returns a function (the scale) that can be used to map from data point to its position in the drawing area.

Since a Sankey chart doesn't have axis, a useAxis hook obviously doesn't make sense. If we were to draw a parallel, I suppose we'd need a useLayout hook that returned something similar to a scale, i.e., a function that maps data points to layout. That would be too much IMO, so a useLayout hook that would return the layout would make sense to me.

I think exposing a useLayout (or useSankeyLayout) hook would also solve node anchoring. Could you confirm?

Plus, we could remove the dependency of series on the drawing area.

This can provide other performance benefits: components that only depend on the data/series wouldn't re-render on drawing area changes, e.g., tooltip contents.

The layout is derived from the data + drawing area, so exposing it separately will probably be a good idea to reduce unnecessary computation.

What do you think?

@alexfauquette
Copy link
Member Author

What bother me with a dedicated hook is that it will create edge case management. Because for now the tootlip content or tooltip position are simple:

  1. We get values (series/axis) from the store with selectors
  2. We apply the appropriate seriesConfig function
  3. We have the tooltip content ready to be used

If we have a dedicated useSankeyLayout() we will need to also have a dedicated tooltip. That's why I want to do this with selectors

Plus, we could remove the dependency of series on the drawing area.

For me it's not that much of a deal. The current modification implies two selectors

image

We have two different cases: The series types uses the additional selector (basically the sankey. Potentially the pie chart) or the series type does not use it (i.e. bar, line, scatter)

We can ignore the second case, because the second selector is the identity function. so if the drawing area get modified, it will just loop other bar, line, and scatter to make sure their config does not contain a processor using the drawin area

For the sankey, we could effectively have a small improvement by using the result of the first selectors when layout is not needed. For example in tooltip/legend.


What about the following:

  1. I introduce a "dumb sankey layout" in the first selector to get acces to the links/nodes relation without depending on the drawing area
  2. I cleanup sankey to have interaction call back do remove all references to position
  3. I open a PR to introduce the second selector but do not use it to create the useSankeyLayout() that is selecting sankeyLayout form it.
  4. when something like tooltip position requires the sankey layout, I swhich from processedSeriesSelector to processedSeriesWithPositionSelector

Here I talk about sankey, but it might be interesting to do the same stuff to compute pie charts slices only once for all. Currenlty it's done in PieArcPlot, PieArcLabelPlot, and could simplify the tooltipPosition()

@bernardobelchior
Copy link
Member

If we have a dedicated useSankeyLayout() we will need to also have a dedicated tooltip. That's why I want to do this with selectors

But won't we need a separate case regardless? Even if the layout is part of the series, we need to either use a different hook (useSeriesWithPosition()) or check if layout is part of the series to handle the sankey case, right? How are you planning on handling that?

Maybe I'm missing something 😅

  1. I introduce a "dumb sankey layout" in the first selector to get acces to the links/nodes relation without depending on the drawing area
  2. I cleanup sankey to have interaction call back do remove all references to position

Sounds good 👍

  1. I open a PR to introduce the second selector but do not use it to create the useSankeyLayout() that is selecting sankeyLayout form it.

What do you mean by this? I suppose it's adding a processedSeriesWithPositionSelector, but how do we access it if not from useSankeyLayout()? Using the normal useStore?

@alexfauquette
Copy link
Member Author

Maybe I'm missing something 😅

I think you miss the selectorChartsTooltipItemPosition. If I do the sankey layout computation in a selector instead of a hook, I can use the result in selectorChartsTooltipItemPosition

Such that adding an anchor="node" to sankey just means adding a property to the series config.
If the computation is done in a hook I will have to modify the tooltip container

What do you mean by this?

For the useSankeyLayout() something like that

function useSankeyLayout() {
  const store = useStore()
  const series = useSelector(store, selectorChartSeriesProcessedWithPositions)

  return series.sankey.series[series.sankey.orderedIds[0]].sankeyLayout
}

For the point 4. I'm especially thinking about the tooltip item position

export const selectorChartsTooltipItemPosition = createSelector(
  selectorChartsTooltipItem,
  selectorChartDrawingArea,
  selectorChartSeriesConfig,
  selectorChartXAxis,
  selectorChartYAxis,
-  selectorChartSeriesProcessed,
+  selectorChartSeriesProcessedWithPositions,
  (_, placement?: 'top' | 'bottom' | 'left' | 'right') => placement,

  function selectorChartsTooltipItemPosition<T extends ChartSeriesType>(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: charts Changes related to the charts. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants