Skip to content

Commit

Permalink
[charts] Replace path with circle for perf improvement (#14518)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfauquette authored Sep 12, 2024
1 parent 5d86576 commit 7120da2
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 44 deletions.
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 @@ -26,6 +26,7 @@
"dataset": { "type": { "name": "arrayOf", "description": "Array<object>" } },
"disableAxisListener": { "type": { "name": "bool" }, "default": "false" },
"disableLineItemHighlight": { "type": { "name": "bool" } },
"experimentalMarkRendering": { "type": { "name": "bool" } },
"grid": {
"type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" }
},
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 @@ -26,6 +26,7 @@
"dataset": { "type": { "name": "arrayOf", "description": "Array<object>" } },
"disableAxisListener": { "type": { "name": "bool" }, "default": "false" },
"disableLineItemHighlight": { "type": { "name": "bool" } },
"experimentalMarkRendering": { "type": { "name": "bool" } },
"grid": {
"type": { "name": "shape", "description": "{ horizontal?: bool, vertical?: bool }" }
},
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
@@ -1,5 +1,6 @@
{
"props": {
"experimentalRendering": { "type": { "name": "bool" }, "default": "false" },
"onItemClick": {
"type": { "name": "func" },
"signature": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
"disableLineItemHighlight": {
"description": "If <code>true</code>, render the line highlight item."
},
"experimentalMarkRendering": {
"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."
},
"grid": { "description": "Option to display a cartesian grid in the background." },
"height": {
"description": "The height of the chart in px. If not defined, it takes the height of the parent element."
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 @@ -18,6 +18,9 @@
"disableLineItemHighlight": {
"description": "If <code>true</code>, render the line highlight item."
},
"experimentalMarkRendering": {
"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."
},
"grid": { "description": "Option to display a cartesian grid in the background." },
"height": {
"description": "The height of the chart in px. If not defined, it takes the height of the parent element."
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
@@ -1,6 +1,9 @@
{
"componentDescription": "",
"propDescriptions": {
"experimentalRendering": {
"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."
},
"onItemClick": {
"description": "Callback fired when a line mark item is clicked.",
"typeDescriptions": {
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 @@ -146,6 +146,10 @@ LineChartPro.propTypes = {
* If `true`, render the line highlight item.
*/
disableLineItemHighlight: PropTypes.bool,
/**
* If `true` marks will render `<circle />` instead of `<path />` and drop theme override for faster rendering.
*/
experimentalMarkRendering: PropTypes.bool,
/**
* Option to display a cartesian grid in the background.
*/
Expand Down
121 changes: 121 additions & 0 deletions packages/x-charts/src/LineChart/CircleMarkElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { useTheme } from '@mui/material/styles';
import { warnOnce } from '@mui/x-internals/warning';
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';

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 \`experimentalRendering=true\`.`,
'Only "circle" are supported with `experimentalRendering`.',
].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 @@ -125,6 +125,10 @@ export interface LineChartProps
* @default false
*/
skipAnimation?: boolean;
/**
* If `true` marks will render `<circle />` instead of `<path />` and drop theme override for faster rendering.
*/
experimentalMarkRendering?: boolean;
}

/**
Expand Down Expand Up @@ -223,6 +227,10 @@ LineChart.propTypes = {
* If `true`, render the line highlight item.
*/
disableLineItemHighlight: PropTypes.bool,
/**
* If `true` marks will render `<circle />` instead of `<path />` and drop theme override for faster rendering.
*/
experimentalMarkRendering: PropTypes.bool,
/**
* Option to display a cartesian grid in the background.
*/
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,55 +1,14 @@
'use client';
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 @@ -11,6 +11,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 @@ -42,6 +43,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
*/
experimentalRendering?: boolean;
}

/**
Expand All @@ -55,14 +62,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, experimentalRendering, ...other } = props;

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

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

if (seriesData === undefined) {
return null;
Expand Down Expand Up @@ -177,6 +184,12 @@ MarkPlot.propTypes = {
// | These PropTypes are generated from the TypeScript type definitions |
// | To update them edit the TypeScript types and run "pnpm proptypes" |
// ----------------------------------------------------------------------
/**
* 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
*/
experimentalRendering: PropTypes.bool,
/**
* Callback fired when a line mark item is clicked.
* @param {React.MouseEvent<SVGPathElement, MouseEvent>} event The event source of the callback.
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 @@ -55,6 +55,7 @@ export const useLineChartProps = (props: LineChartProps) => {
highlightedItem,
onHighlightChange,
className,
experimentalMarkRendering,
...other
} = props;

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

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}
experimentalMarkRendering
/>,
);

Expand Down

0 comments on commit 7120da2

Please sign in to comment.