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] Replace path with circle for perf improvement #14518

Merged
merged 11 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/line-chart-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"default": "object Depends on the charts type."
},
"markPerfUpdate": { "type": { "name": "bool" } },
"onAreaClick": { "type": { "name": "func" } },
"onAxisClick": {
"type": { "name": "func" },
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/line-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"default": "object Depends on the charts type."
},
"markPerfUpdate": { "type": { "name": "bool" } },
"onAreaClick": { "type": { "name": "func" } },
"onAxisClick": {
"type": { "name": "func" },
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/mark-plot.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"describedArgs": ["event", "lineItemIdentifier"]
}
},
"perfUpdate": { "type": { "name": "bool" }, "default": "false" },
"skipAnimation": { "type": { "name": "bool" }, "default": "false" },
"slotProps": { "type": { "name": "object" }, "default": "{}" },
"slots": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"margin": {
"description": "The margin between the SVG and the drawing area. It&#39;s used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: <code>top</code>, <code>bottom</code>, <code>left</code>, and <code>right</code>."
},
"markPerfUpdate": {
"description": "If <code>true</code> marks will render <code>&lt;circle /&gt;</code> instead of <code>&lt;path /&gt;</code> and drop theme override for faster rendering."
},
"onAreaClick": { "description": "Callback fired when an area element is clicked." },
"onAxisClick": {
"description": "The function called for onClick events. The second argument contains information about all line/bar elements at the current mouse position.",
Expand Down
3 changes: 3 additions & 0 deletions docs/translations/api-docs/charts/line-chart/line-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"margin": {
"description": "The margin between the SVG and the drawing area. It&#39;s used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: <code>top</code>, <code>bottom</code>, <code>left</code>, and <code>right</code>."
},
"markPerfUpdate": {
"description": "If <code>true</code> marks will render <code>&lt;circle /&gt;</code> instead of <code>&lt;path /&gt;</code> and drop theme override for faster rendering."
},
"onAreaClick": { "description": "Callback fired when an area element is clicked." },
"onAxisClick": {
"description": "The function called for onClick events. The second argument contains information about all line/bar elements at the current mouse position.",
Expand Down
3 changes: 3 additions & 0 deletions docs/translations/api-docs/charts/mark-plot/mark-plot.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"lineItemIdentifier": "The line mark item identifier."
}
},
"perfUpdate": {
"description": "If <code>true</code> the mark element will only be able to render circle. Giving fewer customization options, but saving around 40ms per 1.000 marks."
},
"skipAnimation": { "description": "If <code>true</code>, animations are skipped." },
"slotProps": { "description": "The props used for each component slot." },
"slots": { "description": "Overridable component slots." }
Expand Down
4 changes: 4 additions & 0 deletions packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ LineChartPro.propTypes = {
right: PropTypes.number,
top: PropTypes.number,
}),
/**
* If `true` marks will render `<circle />` instead of `<path />` and drop theme override for faster rendering.
*/
markPerfUpdate: PropTypes.bool,
/**
* Callback fired when an area element is clicked.
*/
Expand Down
120 changes: 120 additions & 0 deletions packages/x-charts/src/LineChart/CircleMarkElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { useTheme } from '@mui/material/styles';
import { animated, useSpring } from '@react-spring/web';
import { InteractionContext } from '../context/InteractionProvider';
import { useInteractionItemProps } from '../hooks/useInteractionItemProps';
import { useItemHighlighted } from '../context';
import { MarkElementOwnerState, useUtilityClasses } from './markElementClasses';
import { warnOnce } from '../internals/warning';

export type CircleMarkElementProps = Omit<MarkElementOwnerState, 'isFaded' | 'isHighlighted'> &
Omit<React.SVGProps<SVGPathElement>, 'ref' | 'id'> & {
/**
* The shape of the marker.
*/
shape: 'circle' | 'cross' | 'diamond' | 'square' | 'star' | 'triangle' | 'wye';
/**
* If `true`, animations are skipped.
* @default false
*/
skipAnimation?: boolean;
/**
* The index to the element in the series' data array.
*/
dataIndex: number;
};

/**
* The line mark element that only render circle for performance improvement.
*
* Demos:
*
* - [Lines](https://mui.com/x/react-charts/lines/)
* - [Line demonstration](https://mui.com/x/react-charts/line-demo/)
*
* API:
*
* - [CircleMarkElement API](https://mui.com/x/api/charts/circle-mark-element/)
*/
function CircleMarkElement(props: CircleMarkElementProps) {
const {
x,
y,
id,
classes: innerClasses,
color,
dataIndex,
onClick,
skipAnimation,
shape,
...other
} = props;

if (shape !== 'circle') {
warnOnce(
[
`MUI X: The mark element of your line chart have shape "${shape}" which is not supported when using \`perfUpdate=true\`.`,
'Only "circle" are supported with `perfUpdate`.',
].join('\n'),
'error',
);
}
const theme = useTheme();
const getInteractionItemProps = useInteractionItemProps();
const { isFaded, isHighlighted } = useItemHighlighted({
seriesId: id,
});
const { axis } = React.useContext(InteractionContext);

const position = useSpring({ to: { x, y }, immediate: skipAnimation });
const ownerState = {
id,
classes: innerClasses,
isHighlighted: axis.x?.index === dataIndex || isHighlighted,
isFaded,
color,
};
const classes = useUtilityClasses(ownerState);

return (
<animated.circle
{...other}
cx={position.x}
cy={position.y}
r={5}
fill={(theme.vars || theme).palette.background.paper}
stroke={color}
strokeWidth={2}
className={classes.root}
onClick={onClick}
cursor={onClick ? 'pointer' : 'unset'}
{...getInteractionItemProps({ type: 'line', seriesId: id, dataIndex })}
/>
);
}

CircleMarkElement.propTypes = {
// ----------------------------- Warning --------------------------------
// | These PropTypes are generated from the TypeScript type definitions |
// | To update them edit the TypeScript types and run "pnpm proptypes" |
// ----------------------------------------------------------------------
classes: PropTypes.object,
/**
* The index to the element in the series' data array.
*/
dataIndex: PropTypes.number.isRequired,
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
/**
* The shape of the marker.
*/
shape: PropTypes.oneOf(['circle', 'cross', 'diamond', 'square', 'star', 'triangle', 'wye'])
.isRequired,
/**
* If `true`, animations are skipped.
* @default false
*/
skipAnimation: PropTypes.bool,
} as any;

export { CircleMarkElement };
8 changes: 8 additions & 0 deletions packages/x-charts/src/LineChart/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ export interface LineChartProps
* @default false
*/
skipAnimation?: boolean;
/**
* If `true` marks will render `<circle />` instead of `<path />` and drop theme override for faster rendering.
*/
markPerfUpdate?: boolean;
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
markPerfUpdate?: boolean;
usePerformantMarks?: boolean;

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
markPerfUpdate?: boolean;
improveMarkPerformance?: boolean;

Copy link
Member

Choose a reason for hiding this comment

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

markPerfUpdate sounds vague, "update" is a status, so it doesn't fully describe the action I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

What about experimentalMarkRendering To say it's planned to be the future default behavior in v8?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think that would be great

}

/**
Expand Down Expand Up @@ -291,6 +295,10 @@ LineChart.propTypes = {
right: PropTypes.number,
top: PropTypes.number,
}),
/**
* If `true` marks will render `<circle />` instead of `<path />` and drop theme override for faster rendering.
*/
markPerfUpdate: PropTypes.bool,
/**
* Callback fired when an area element is clicked.
*/
Expand Down
43 changes: 1 addition & 42 deletions packages/x-charts/src/LineChart/MarkElement.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,13 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import composeClasses from '@mui/utils/composeClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
import { styled } from '@mui/material/styles';
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import { symbol as d3Symbol, symbolsFill as d3SymbolsFill } from '@mui/x-charts-vendor/d3-shape';
import { animated, to, useSpring } from '@react-spring/web';
import { getSymbol } from '../internals/getSymbol';
import { InteractionContext } from '../context/InteractionProvider';
import { useInteractionItemProps } from '../hooks/useInteractionItemProps';
import { SeriesId } from '../models/seriesType/common';
import { useItemHighlighted } from '../context';

export interface MarkElementClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element when highlighted. */
highlighted: string;
/** Styles applied to the root element when faded. */
faded: string;
}

export type MarkElementClassKey = keyof MarkElementClasses;

interface MarkElementOwnerState {
id: SeriesId;
color: string;
isFaded: boolean;
isHighlighted: boolean;
classes?: Partial<MarkElementClasses>;
}

export function getMarkElementUtilityClass(slot: string) {
return generateUtilityClass('MuiMarkElement', slot);
}

export const markElementClasses: MarkElementClasses = generateUtilityClasses('MuiMarkElement', [
'root',
'highlighted',
'faded',
]);

const useUtilityClasses = (ownerState: MarkElementOwnerState) => {
const { classes, id, isFaded, isHighlighted } = ownerState;
const slots = {
root: ['root', `series-${id}`, isHighlighted && 'highlighted', isFaded && 'faded'],
};

return composeClasses(slots, getMarkElementUtilityClass, classes);
};
import { MarkElementOwnerState, useUtilityClasses } from './markElementClasses';

const MarkElementPath = styled(animated.path, {
name: 'MuiMarkElement',
Expand Down
17 changes: 15 additions & 2 deletions packages/x-charts/src/LineChart/MarkPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { cleanId } from '../internals/cleanId';
import getColor from './getColor';
import { useLineSeries } from '../hooks/useSeries';
import { useDrawingArea } from '../hooks/useDrawingArea';
import { CircleMarkElement } from './CircleMarkElement';

export interface MarkPlotSlots {
mark?: React.JSXElementConstructor<MarkElementProps>;
Expand Down Expand Up @@ -41,6 +42,12 @@ export interface MarkPlotProps
event: React.MouseEvent<SVGElement, MouseEvent>,
lineItemIdentifier: LineItemIdentifier,
) => void;
/**
* If `true` the mark element will only be able to render circle.
* Giving fewer customization options, but saving around 40ms per 1.000 marks.
* @default false
*/
perfUpdate?: boolean;
}

/**
Expand All @@ -54,14 +61,14 @@ export interface MarkPlotProps
* - [MarkPlot API](https://mui.com/x/api/charts/mark-plot/)
*/
function MarkPlot(props: MarkPlotProps) {
const { slots, slotProps, skipAnimation, onItemClick, ...other } = props;
const { slots, slotProps, skipAnimation, onItemClick, perfUpdate, ...other } = props;

const seriesData = useLineSeries();
const axisData = useCartesianContext();
const chartId = useChartId();
const drawingArea = useDrawingArea();

const Mark = slots?.mark ?? MarkElement;
const Mark = slots?.mark ?? (perfUpdate ? CircleMarkElement : MarkElement);

if (seriesData === undefined) {
return null;
Expand Down Expand Up @@ -182,6 +189,12 @@ MarkPlot.propTypes = {
* @param {LineItemIdentifier} lineItemIdentifier The line mark item identifier.
*/
onItemClick: PropTypes.func,
/**
* If `true` the mark element will only be able to render circle.
* Giving fewer customization options, but saving around 40ms per 1.000 marks.
* @default false
*/
perfUpdate: PropTypes.bool,
/**
* If `true`, animations are skipped.
* @default false
Expand Down
3 changes: 3 additions & 0 deletions packages/x-charts/src/LineChart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export * from './LineElement';
export * from './AnimatedLine';
export * from './MarkElement';
export * from './LineHighlightElement';

export type { MarkElementClasses, MarkElementClassKey } from './markElementClasses';
export { getMarkElementUtilityClass, markElementClasses } from './markElementClasses';
42 changes: 42 additions & 0 deletions packages/x-charts/src/LineChart/markElementClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import composeClasses from '@mui/utils/composeClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import { SeriesId } from '../models/seriesType/common';

export interface MarkElementClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element when highlighted. */
highlighted: string;
/** Styles applied to the root element when faded. */
faded: string;
}

export type MarkElementClassKey = keyof MarkElementClasses;

export interface MarkElementOwnerState {
id: SeriesId;
color: string;
isFaded: boolean;
isHighlighted: boolean;
classes?: Partial<MarkElementClasses>;
}

export function getMarkElementUtilityClass(slot: string) {
return generateUtilityClass('MuiMarkElement', slot);
}

export const markElementClasses: MarkElementClasses = generateUtilityClasses('MuiMarkElement', [
'root',
'highlighted',
'faded',
]);

export const useUtilityClasses = (ownerState: MarkElementOwnerState) => {
const { classes, id, isFaded, isHighlighted } = ownerState;
const slots = {
root: ['root', `series-${id}`, isHighlighted && 'highlighted', isFaded && 'faded'],
};

return composeClasses(slots, getMarkElementUtilityClass, classes);
};
2 changes: 2 additions & 0 deletions packages/x-charts/src/LineChart/useLineChartProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const useLineChartProps = (props: LineChartProps) => {
highlightedItem,
onHighlightChange,
className,
markPerfUpdate,
...other
} = props;

Expand Down Expand Up @@ -130,6 +131,7 @@ export const useLineChartProps = (props: LineChartProps) => {
slotProps,
onItemClick: onMarkClick,
skipAnimation,
perfUpdate: markPerfUpdate,
};

const overlayProps: ChartsOverlayProps = {
Expand Down
1 change: 1 addition & 0 deletions scripts/buildApiDocs/chartsSettings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default apiPages;
'x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx',
'x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx',
'x-charts/src/ChartsLegend/LegendPerItem.tsx',
'x-charts/src/LineChart/CircleMarkElement.tsx',
].some((invalidPath) => filename.endsWith(invalidPath));
},
skipAnnotatingComponentDefinition: true,
Expand Down
1 change: 1 addition & 0 deletions test/performance-charts/tests/LineChart.bench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('LineChart', () => {
]}
width={500}
height={300}
markPerfUpdate
/>,
);

Expand Down