Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[charts] Introduce the plugin system #15513

Merged
merged 14 commits into from
Nov 26, 2024
Merged

Conversation

alexfauquette
Copy link
Member

@alexfauquette alexfauquette commented Nov 20, 2024

This PR brings all the typing and pipeline to support plugins in the ChartProvider.

For now the hooks only replace 2 features:

  • The svgRef
  • The chartId

Chart state

The main idea is to have in a provider multiple ref you can ask to interact/render the chart

  • The instance which contains all the methods to get/update data
  • The plublicApi that contains the methods we will let people use
  • The svgRef that already exist
  • The store introduced in the previous PR that manage state accessible by selectors.

Typing

The plugin types work as follows:

  1. You define the UseChartXxxSignature which defines the state, the interface, ...
  2. You can then use ChartState<[UseChartInteractionSignature, ...]> and it provide a type that merge the instance, and the store of the plugin signatures you listed

@alexfauquette alexfauquette added the component: charts This is the name of the generic UI component, not the React module! label Nov 20, 2024
@mui-bot
Copy link

mui-bot commented Nov 20, 2024

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

Generated by 🚫 dangerJS against fb4a9fc

Copy link

codspeed-hq bot commented Nov 21, 2024

CodSpeed Performance Report

Merging #15513 will improve performances by 35.98%

Comparing alexfauquette:pluggins (fb4a9fc) with master (7914aa5)

Summary

⚡ 1 improvements
✅ 5 untouched benchmarks

Benchmarks breakdown

Benchmark master alexfauquette:pluggins Change
BarChartPro with big data amount 325.5 ms 239.4 ms +35.98%

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Nov 22, 2024
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Nov 22, 2024
@alexfauquette alexfauquette changed the title [charts][Draft] Introduce the pluggin system [charts] Introduce the plugin system Nov 22, 2024
function ChartProvider(props: ChartProviderProps) {
const { children } = props;

const { contextValue } = useCharts([], {});
Copy link
Member Author

Choose a reason for hiding this comment

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

Here we will have to propagate the plugins and to props to generate the internal state and add the different methods

Copy link
Member

Choose a reason for hiding this comment

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

what do you mean?

Copy link
Member Author

Choose a reason for hiding this comment

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

For example the cartesian chart shoudl enable axis processing with something like

<ChartProvider 
  plugins={[
    useCartesianAxes
  ]}
>

This plugin would add the x/y axes to the store when doing

const { contextValue } = useCharts([useCartesianAxes], { xAxis, yAxis; ... });

import { UseChartIdSignature } from './useChartId.types';
import { createChartDefaultId } from './useChartId.utils';

export const useChartId: ChartPlugin<UseChartIdSignature> = ({ params, store }) => {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is taken from the tree-view.

It allows the user to pass a custom id that override the default one. We don't use that feature we don't have an id prop

Copy link
Member

Choose a reason for hiding this comment

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

probably would be good to add it in the future though


if (inputApiRef) {
if (inputApiRef.current == null) {
// eslint-disable-next-line react-compiler/react-compiler
Copy link
Member Author

Choose a reason for hiding this comment

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

That's because eslint does not see we are using a ref

Copy link
Member

Choose a reason for hiding this comment

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

Should work once we have facebook/react#29916

@alexfauquette alexfauquette marked this pull request as ready for review November 22, 2024 13:47
Copy link
Member

@flaviendelangle flaviendelangle left a comment

Choose a reason for hiding this comment

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

I don't have enough product context, but the plugin itself seems to be correctly migrated


if (inputApiRef) {
if (inputApiRef.current == null) {
// eslint-disable-next-line react-compiler/react-compiler
Copy link
Member

Choose a reason for hiding this comment

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

Should work once we have facebook/react#29916

Comment on lines -111 to +113
if (element === null) {
if (svgRef.current === null) {
return undefined;
}
const element = svgRef.current;
Copy link
Member

Choose a reason for hiding this comment

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

This seems wrong. Technically the current can change from filled to null after the if, but before before we assign it to element. I think eslint used to complain about this 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

How could it be? Between the if and the assignment there is no thing, So I don't see how the main JS thread could be interupted

Copy link
Member

Choose a reason for hiding this comment

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

That's why I said "technically" 😅

The ref is a mutable ref, so we can technically change it at any time, if there are async processes that set/remove it, then it could change.

Copy link
Member Author

Choose a reason for hiding this comment

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

if there are async processes that set/remove it, then it could change.

Not fully sure, but for me JS is mono-thread, which means the current function is executed up to its end, or an await. Only after finishing this execution, the event loop will take other tasks to work on. That's why we sometime use the setTimeout(..., 0) By running a code in async way, we are sure to be executed after the end of React main script

Comment on lines +5 to +6
import { useStore } from '../internals/store/useStore';
import { useSelector } from '../internals/store/useSelector';
Copy link
Member

Choose a reason for hiding this comment

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

should we barrel these two together through a internals/store/index.ts?

function ChartProvider(props: ChartProviderProps) {
const { children } = props;

const { contextValue } = useCharts([], {});
Copy link
Member

Choose a reason for hiding this comment

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

what do you mean?

import { UseChartIdSignature } from './useChartId.types';
import { createChartDefaultId } from './useChartId.utils';

export const useChartId: ChartPlugin<UseChartIdSignature> = ({ params, store }) => {
Copy link
Member

Choose a reason for hiding this comment

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

probably would be good to add it in the future though

Comment on lines +22 to +24
useChartId.params = {
id: true,
};
Copy link
Member

Choose a reason for hiding this comment

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

where are the these params used? 🤔

Copy link
Member

Choose a reason for hiding this comment

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

In the tree view, they are used to split the props that are forwarded to the DOM and the props that are owned by the plugins and should not be forwarded to the DOM

globalId += 1;

const initialState = {
// TODO remove when the interaction moves to plugin
Copy link
Member

Choose a reason for hiding this comment

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

Is this TODO still relevant? 🤔

Copy link
Member Author

@alexfauquette alexfauquette Nov 25, 2024

Choose a reason for hiding this comment

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

Yes, because the interaction pluggin is created but not used.

To move interaction behavior I would need to migrate the useAxisListener, which implies to have access to the drawingArea.

The confusion comes form the fact I started to implement it, but it's not fully operational

Copy link
Member

Choose a reason for hiding this comment

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

Ah, got it


export function useStore(skipError?: boolean): ChartStore {
const charts = React.useContext(ChartsContext);
// This hook should be removed because user and us should not interact with the store directly, but with public/private APIs
Copy link
Member

Choose a reason for hiding this comment

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

When should we remove it?

Copy link
Member Author

Choose a reason for hiding this comment

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

When the interaction fully move to the plugins, which would allow to remove the hook for axis listener, and voronoi handler which are using the store to update interaction state.

…s.ts

Co-authored-by: Jose C Quintas Jr <juniorquintas@gmail.com>
Signed-off-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com>
Object.assign(initialState, plugin.getInitialState({}));
}
});
storeRef.current = new ChartStore(initialState);
Copy link
Member Author

Choose a reason for hiding this comment

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

@JCQuintas

In this line the state is directly set because we are in the condition storeRef.current == null so we directly compute the initial state.

The discussion we had is about storing data from props into the store.

Since it's a ref we will need to use a useEffect to update the store. For example, the cartesian pluggin could contain to following code to make the xAxis available to selectors

useEffect(() => {
  store.update(updateXAxis(xAxis))
}, [xAxis])

Which would cause 2 renders. One when user update their prop. A second one due to the store update.

So we trade potential double render with selectors

Copy link
Member

Choose a reason for hiding this comment

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

Small note from the meeting: Unless the chart never uses any selector that contain this data AND the child component (the series? the dot? I don't know what consumes that 😆 ) doesn't have props update or context update when this data changes from the parent.

In which case, we have a first render on the chart and then a second render on the child, but both only render once (except for the mount).

@alexfauquette alexfauquette merged commit db5cf78 into mui:master Nov 26, 2024
19 checks passed
@alexfauquette alexfauquette deleted the pluggins branch November 26, 2024 08:57
LukasTy pushed a commit to LukasTy/mui-x that referenced this pull request Dec 19, 2024
Signed-off-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com>
Co-authored-by: Jose C Quintas Jr <juniorquintas@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: charts This is the name of the generic UI component, not the React module!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants