From b0703ab54f2c4d982fe3aec34a66f784ce57f3e4 Mon Sep 17 00:00:00 2001 From: Jose C Quintas Jr Date: Wed, 11 Sep 2024 21:32:31 +0200 Subject: [PATCH] [charts] Allow `onItemClick` on the `Legend` component (#14231) Signed-off-by: Jose C Quintas Jr Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> --- docs/data/charts/legend/LegendClickNoSnap.js | 148 ++++++++++++++++++ docs/data/charts/legend/legend.md | 18 +++ docs/pages/x/api/charts/charts-legend.json | 13 ++ .../x/api/charts/default-charts-legend.json | 13 ++ docs/pages/x/api/charts/pie-chart.json | 2 +- .../x/api/charts/piecewise-color-legend.json | 13 ++ .../charts/charts-legend/charts-legend.json | 12 ++ .../default-charts-legend.json | 12 ++ .../piecewise-color-legend.json | 12 ++ .../src/BarChartPro/BarChartPro.tsx | 1 + .../src/LineChartPro/LineChartPro.tsx | 1 + .../src/ScatterChartPro/ScatterChartPro.tsx | 1 + packages/x-charts/src/BarChart/BarChart.tsx | 1 + packages/x-charts/src/BarChart/legend.ts | 4 +- .../src/ChartsLegend/ChartsLegend.tsx | 8 + .../src/ChartsLegend/ChartsLegendItem.tsx | 87 ++++++++++ .../src/ChartsLegend/DefaultChartsLegend.tsx | 49 +++++- .../src/ChartsLegend/LegendPerItem.tsx | 42 +++-- .../src/ChartsLegend/PiecewiseColorLegend.tsx | 61 +++++++- .../src/ChartsLegend/chartsLegend.types.ts | 46 +++++- .../src/ChartsLegend/chartsLegendClasses.ts | 3 + packages/x-charts/src/LineChart/LineChart.tsx | 1 + packages/x-charts/src/LineChart/legend.ts | 3 +- packages/x-charts/src/PieChart/PieChart.tsx | 1 + packages/x-charts/src/PieChart/legend.ts | 4 +- .../src/ScatterChart/ScatterChart.tsx | 1 + packages/x-charts/src/ScatterChart/legend.ts | 3 +- 27 files changed, 520 insertions(+), 40 deletions(-) create mode 100644 docs/data/charts/legend/LegendClickNoSnap.js create mode 100644 packages/x-charts/src/ChartsLegend/ChartsLegendItem.tsx diff --git a/docs/data/charts/legend/LegendClickNoSnap.js b/docs/data/charts/legend/LegendClickNoSnap.js new file mode 100644 index 0000000000000..6c022e0982c5f --- /dev/null +++ b/docs/data/charts/legend/LegendClickNoSnap.js @@ -0,0 +1,148 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import UndoOutlinedIcon from '@mui/icons-material/UndoOutlined'; + +import { ChartsLegend, PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend'; + +import { HighlightedCode } from '@mui/docs/HighlightedCode'; +import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer'; + +const pieSeries = [ + { + type: 'pie', + id: 'series-1', + label: 'Series 1', + data: [ + { label: 'Pie A', id: 'P1-A', value: 400 }, + { label: 'Pie B', id: 'P2-B', value: 300 }, + ], + }, +]; + +const barSeries = [ + { + type: 'bar', + id: 'series-1', + label: 'Series 1', + data: [0, 1, 2], + }, + { + type: 'bar', + id: 'series-2', + label: 'Series 2', + data: [0, 1, 2], + }, +]; + +const lineSeries = [ + { + type: 'line', + id: 'series-1', + label: 'Series 1', + data: [0, 1, 2], + }, + { + type: 'line', + id: 'series-2', + label: 'Series 2', + data: [0, 1, 2], + }, +]; + +export default function LegendClickNoSnap() { + const [itemData, setItemData] = React.useState(); + + return ( + + + Chart Legend + + setItemData([context, index])} + /> + + Pie Chart Legend + + setItemData([context, index])} + /> + + Pie Chart Legend + + setItemData([context, index])} + /> + + + + + + Click on the chart + { + setItemData(null); + }} + > + + + + + + + ); +} diff --git a/docs/data/charts/legend/legend.md b/docs/data/charts/legend/legend.md index f3042bc1a75d9..208df12e253d7 100644 --- a/docs/data/charts/legend/legend.md +++ b/docs/data/charts/legend/legend.md @@ -111,3 +111,21 @@ labelFormatter = ({ min, max, formattedMin, formattedMax }) => string | null; ``` {{"demo": "PiecewiseInteractiveDemoNoSnap.js", "hideToolbar": true, "bg": "playground"}} + +## Click event + +You can pass an `onItemClick` function to the `ChartsLegend` or `PiecewiseColorLegend` components to handle click events. +They both provide the following signature. + +```js +const clickHandler = ( + event, // The click event. + context, // An object that identifies the clicked item. + index, // The index of the clicked item. +) => {}; +``` + +The `context` object contains different properties depending on the legend type. +Click the legend items to see their content. + +{{"demo": "LegendClickNoSnap.js"}} diff --git a/docs/pages/x/api/charts/charts-legend.json b/docs/pages/x/api/charts/charts-legend.json index ce69858fcc062..8e58410c7fd64 100644 --- a/docs/pages/x/api/charts/charts-legend.json +++ b/docs/pages/x/api/charts/charts-legend.json @@ -8,6 +8,13 @@ "itemMarkWidth": { "type": { "name": "number" }, "default": "20" }, "labelStyle": { "type": { "name": "object" }, "default": "theme.typography.subtitle1" }, "markGap": { "type": { "name": "number" }, "default": "5" }, + "onItemClick": { + "type": { "name": "func" }, + "signature": { + "type": "function(event: React.MouseEvent, legendItem: SeriesLegendItemContext, index: number) => void", + "describedArgs": ["event", "legendItem", "index"] + } + }, "padding": { "type": { "name": "union", @@ -49,6 +56,12 @@ "description": "Styles applied to the legend with column layout.", "isGlobal": false }, + { + "key": "itemBackground", + "className": "MuiChartsLegend-itemBackground", + "description": "Styles applied to the item background.", + "isGlobal": false + }, { "key": "label", "className": "MuiChartsLegend-label", diff --git a/docs/pages/x/api/charts/default-charts-legend.json b/docs/pages/x/api/charts/default-charts-legend.json index 084948ec6dfed..1c55aa4c7626f 100644 --- a/docs/pages/x/api/charts/default-charts-legend.json +++ b/docs/pages/x/api/charts/default-charts-legend.json @@ -18,6 +18,13 @@ "itemMarkWidth": { "type": { "name": "number" }, "default": "20" }, "labelStyle": { "type": { "name": "object" }, "default": "theme.typography.subtitle1" }, "markGap": { "type": { "name": "number" }, "default": "5" }, + "onItemClick": { + "type": { "name": "func" }, + "signature": { + "type": "function(event: React.MouseEvent, legendItem: SeriesLegendItemContext, index: number) => void", + "describedArgs": ["event", "legendItem", "index"] + } + }, "padding": { "type": { "name": "union", @@ -39,6 +46,12 @@ "description": "Styles applied to the legend with column layout.", "isGlobal": false }, + { + "key": "itemBackground", + "className": "MuiDefaultChartsLegend-itemBackground", + "description": "Styles applied to the item background.", + "isGlobal": false + }, { "key": "label", "className": "MuiDefaultChartsLegend-label", diff --git a/docs/pages/x/api/charts/pie-chart.json b/docs/pages/x/api/charts/pie-chart.json index fa14397997df0..dfb20738cb2a4 100644 --- a/docs/pages/x/api/charts/pie-chart.json +++ b/docs/pages/x/api/charts/pie-chart.json @@ -39,7 +39,7 @@ "legend": { "type": { "name": "shape", - "description": "{ classes?: object, direction?: 'column'
| 'row', hidden?: bool, itemGap?: number, itemMarkHeight?: number, itemMarkWidth?: number, labelStyle?: object, markGap?: number, padding?: number
| { bottom?: number, left?: number, right?: number, top?: number }, position?: { horizontal: 'left'
| 'middle'
| 'right', vertical: 'bottom'
| 'middle'
| 'top' }, slotProps?: object, slots?: object }" + "description": "{ classes?: object, direction?: 'column'
| 'row', hidden?: bool, itemGap?: number, itemMarkHeight?: number, itemMarkWidth?: number, labelStyle?: object, markGap?: number, onItemClick?: func, padding?: number
| { bottom?: number, left?: number, right?: number, top?: number }, position?: { horizontal: 'left'
| 'middle'
| 'right', vertical: 'bottom'
| 'middle'
| 'top' }, slotProps?: object, slots?: object }" }, "default": "{ direction: 'column', position: { vertical: 'middle', horizontal: 'right' } }", "deprecated": true, diff --git a/docs/pages/x/api/charts/piecewise-color-legend.json b/docs/pages/x/api/charts/piecewise-color-legend.json index 40a8462375db1..18c41729f90c5 100644 --- a/docs/pages/x/api/charts/piecewise-color-legend.json +++ b/docs/pages/x/api/charts/piecewise-color-legend.json @@ -35,6 +35,13 @@ }, "labelStyle": { "type": { "name": "object" }, "default": "theme.typography.subtitle1" }, "markGap": { "type": { "name": "number" }, "default": "5" }, + "onItemClick": { + "type": { "name": "func" }, + "signature": { + "type": "function(event: React.MouseEvent, legendItem: PiecewiseColorLegendItemContext, index: number) => void", + "describedArgs": ["event", "legendItem", "index"] + } + }, "padding": { "type": { "name": "union", @@ -56,6 +63,12 @@ "description": "Styles applied to the legend with column layout.", "isGlobal": false }, + { + "key": "itemBackground", + "className": "MuiPiecewiseColorLegend-itemBackground", + "description": "Styles applied to the item background.", + "isGlobal": false + }, { "key": "label", "className": "MuiPiecewiseColorLegend-label", diff --git a/docs/translations/api-docs/charts/charts-legend/charts-legend.json b/docs/translations/api-docs/charts/charts-legend/charts-legend.json index 91893688a7a6a..697f19e692d3b 100644 --- a/docs/translations/api-docs/charts/charts-legend/charts-legend.json +++ b/docs/translations/api-docs/charts/charts-legend/charts-legend.json @@ -11,6 +11,14 @@ "itemMarkWidth": { "description": "Width of the item mark (in px)." }, "labelStyle": { "description": "Style applied to legend labels." }, "markGap": { "description": "Space between the mark and the label (in px)." }, + "onItemClick": { + "description": "Callback fired when a legend item is clicked.", + "typeDescriptions": { + "event": "The click event.", + "legendItem": "The legend item data.", + "index": "The index of the clicked legend item." + } + }, "padding": { "description": "Legend padding (in px). Can either be a single number, or an object with top, left, bottom, right properties." }, @@ -23,6 +31,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the legend with column layout" }, + "itemBackground": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the item background" + }, "label": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the series label" }, "mark": { "description": "Styles applied to {{nodeName}}.", "nodeName": "series mark element" }, "root": { "description": "Styles applied to the root element." }, diff --git a/docs/translations/api-docs/charts/default-charts-legend/default-charts-legend.json b/docs/translations/api-docs/charts/default-charts-legend/default-charts-legend.json index 3d15cae316d15..eea8fa28c58f3 100644 --- a/docs/translations/api-docs/charts/default-charts-legend/default-charts-legend.json +++ b/docs/translations/api-docs/charts/default-charts-legend/default-charts-legend.json @@ -11,6 +11,14 @@ "itemMarkWidth": { "description": "Width of the item mark (in px)." }, "labelStyle": { "description": "Style applied to legend labels." }, "markGap": { "description": "Space between the mark and the label (in px)." }, + "onItemClick": { + "description": "Callback fired when a legend item is clicked.", + "typeDescriptions": { + "event": "The click event.", + "legendItem": "The legend item data.", + "index": "The index of the clicked legend item." + } + }, "padding": { "description": "Legend padding (in px). Can either be a single number, or an object with top, left, bottom, right properties." }, @@ -21,6 +29,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the legend with column layout" }, + "itemBackground": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the item background" + }, "label": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the series label" }, "mark": { "description": "Styles applied to {{nodeName}}.", "nodeName": "series mark element" }, "root": { "description": "Styles applied to the root element." }, diff --git a/docs/translations/api-docs/charts/piecewise-color-legend/piecewise-color-legend.json b/docs/translations/api-docs/charts/piecewise-color-legend/piecewise-color-legend.json index 5937eed5266aa..eb233fb379f1f 100644 --- a/docs/translations/api-docs/charts/piecewise-color-legend/piecewise-color-legend.json +++ b/docs/translations/api-docs/charts/piecewise-color-legend/piecewise-color-legend.json @@ -29,6 +29,14 @@ }, "labelStyle": { "description": "Style applied to legend labels." }, "markGap": { "description": "Space between the mark and the label (in px)." }, + "onItemClick": { + "description": "Callback fired when a legend item is clicked.", + "typeDescriptions": { + "event": "The click event.", + "legendItem": "The legend item data.", + "index": "The index of the clicked legend item." + } + }, "padding": { "description": "Legend padding (in px). Can either be a single number, or an object with top, left, bottom, right properties." }, @@ -39,6 +47,10 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the legend with column layout" }, + "itemBackground": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the item background" + }, "label": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the series label" }, "mark": { "description": "Styles applied to {{nodeName}}.", "nodeName": "series mark element" }, "root": { "description": "Styles applied to the root element." }, diff --git a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx index 4a604bfc337c9..628a47acce0e9 100644 --- a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx +++ b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx @@ -171,6 +171,7 @@ BarChartPro.propTypes = { itemMarkWidth: PropTypes.number, labelStyle: PropTypes.object, markGap: PropTypes.number, + onItemClick: PropTypes.func, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ diff --git a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx index d1dac381b2589..562fea327402b 100644 --- a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx +++ b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx @@ -182,6 +182,7 @@ LineChartPro.propTypes = { itemMarkWidth: PropTypes.number, labelStyle: PropTypes.object, markGap: PropTypes.number, + onItemClick: PropTypes.func, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ diff --git a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx index 1cf04ccf0140e..7b49dc343bc23 100644 --- a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx +++ b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx @@ -153,6 +153,7 @@ ScatterChartPro.propTypes = { itemMarkWidth: PropTypes.number, labelStyle: PropTypes.object, markGap: PropTypes.number, + onItemClick: PropTypes.func, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx index 30421c8d28c87..31ac61783bacc 100644 --- a/packages/x-charts/src/BarChart/BarChart.tsx +++ b/packages/x-charts/src/BarChart/BarChart.tsx @@ -238,6 +238,7 @@ BarChart.propTypes = { itemMarkWidth: PropTypes.number, labelStyle: PropTypes.object, markGap: PropTypes.number, + onItemClick: PropTypes.func, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ diff --git a/packages/x-charts/src/BarChart/legend.ts b/packages/x-charts/src/BarChart/legend.ts index 6098786b5c55e..e2ee7ae8c7e92 100644 --- a/packages/x-charts/src/BarChart/legend.ts +++ b/packages/x-charts/src/BarChart/legend.ts @@ -12,10 +12,12 @@ const legendGetter: LegendGetter<'bar'> = (params) => { } acc.push({ + id: seriesId, + seriesId, color: series[seriesId].color, label: formattedLabel, - id: seriesId, }); + return acc; }, [] as LegendItemParams[]); }; diff --git a/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx b/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx index 4a784de3324da..e371d170bae4c 100644 --- a/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx @@ -52,6 +52,7 @@ const useUtilityClasses = (ownerState: DefaultizedChartsLegendProps & { theme: T mark: ['mark'], label: ['label'], series: ['series'], + itemBackground: ['itemBackground'], }; return composeClasses(slots, getLegendUtilityClass, classes); @@ -139,6 +140,13 @@ ChartsLegend.propTypes = { * @default 5 */ markGap: PropTypes.number, + /** + * Callback fired when a legend item is clicked. + * @param {React.MouseEvent} event The click event. + * @param {SeriesLegendItemContext} legendItem The legend item data. + * @param {number} index The index of the clicked legend item. + */ + onItemClick: PropTypes.func, /** * Legend padding (in px). * Can either be a single number, or an object with top, left, bottom, right properties. diff --git a/packages/x-charts/src/ChartsLegend/ChartsLegendItem.tsx b/packages/x-charts/src/ChartsLegend/ChartsLegendItem.tsx new file mode 100644 index 0000000000000..bf8521339daab --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/ChartsLegendItem.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { useRtl } from '@mui/system/RtlProvider'; +import { ChartsText, ChartsTextStyle } from '../ChartsText'; +import { LegendItemParams } from './chartsLegend.types'; +import { ChartsLegendClasses } from './chartsLegendClasses'; + +export interface ChartsLegendItemProps extends LegendItemParams { + positionY: number; + label: string; + positionX: number; + innerHeight: number; + innerWidth: number; + color: string; + gapX: number; + gapY: number; + legendWidth: number; + itemMarkHeight: number; + itemMarkWidth: number; + markGap: number; + labelStyle: ChartsTextStyle; + classes?: Omit, 'column' | 'row' | 'label'>; + onClick?: (event: React.MouseEvent) => void; +} + +/** + * @ignore - internal component. + */ +function ChartsLegendItem(props: ChartsLegendItemProps) { + const isRTL = useRtl(); + const { + id, + positionY, + label, + positionX, + innerHeight, + innerWidth, + legendWidth, + color, + gapX, + gapY, + itemMarkHeight, + itemMarkWidth, + markGap, + labelStyle, + classes, + onClick, + } = props; + + return ( + + + + + + ); +} + +export { ChartsLegendItem }; diff --git a/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx b/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx index 91a14334e24ca..e161bdb9a4f21 100644 --- a/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx @@ -4,14 +4,36 @@ import PropTypes from 'prop-types'; import { FormattedSeries } from '../context/SeriesProvider'; import { LegendPerItem, LegendPerItemProps } from './LegendPerItem'; import { DrawingArea } from '../context/DrawingProvider'; +import { LegendItemParams, SeriesLegendItemContext } from './chartsLegend.types'; -export interface LegendRendererProps extends Omit { +const seriesContextBuilder = (context: LegendItemParams): SeriesLegendItemContext => + ({ + type: 'series', + color: context.color, + label: context.label, + seriesId: context.seriesId!, + itemId: context.itemId, + }) as const; + +export interface LegendRendererProps + extends Omit { series: FormattedSeries; seriesToDisplay: LegendPerItemProps['itemsToDisplay']; /** * @deprecated Use the `useDrawingArea` hook instead. */ drawingArea: Omit; + /** + * Callback fired when a legend item is clicked. + * @param {React.MouseEvent} event The click event. + * @param {SeriesLegendItemContext} legendItem The legend item data. + * @param {number} index The index of the clicked legend item. + */ + onItemClick?: ( + event: React.MouseEvent, + legendItem: SeriesLegendItemContext, + index: number, + ) => void; /** * Set to true to hide the legend. * @default false @@ -20,13 +42,23 @@ export interface LegendRendererProps extends Omit; + return ( + onItemClick(e, seriesContextBuilder(seriesToDisplay[i]), i) + : undefined + } + /> + ); } DefaultChartsLegend.propTypes = { @@ -84,6 +116,13 @@ DefaultChartsLegend.propTypes = { * @default 5 */ markGap: PropTypes.number, + /** + * Callback fired when a legend item is clicked. + * @param {React.MouseEvent} event The click event. + * @param {SeriesLegendItemContext} legendItem The legend item data. + * @param {number} index The index of the clicked legend item. + */ + onItemClick: PropTypes.func, /** * Legend padding (in px). * Can either be a single number, or an object with top, left, bottom, right properties. @@ -110,7 +149,11 @@ DefaultChartsLegend.propTypes = { PropTypes.shape({ color: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + itemId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), label: PropTypes.string.isRequired, + maxValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]), + minValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]), + seriesId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }), ).isRequired, } as any; diff --git a/packages/x-charts/src/ChartsLegend/LegendPerItem.tsx b/packages/x-charts/src/ChartsLegend/LegendPerItem.tsx index ccb2fee3af100..6edd54deb32ae 100644 --- a/packages/x-charts/src/ChartsLegend/LegendPerItem.tsx +++ b/packages/x-charts/src/ChartsLegend/LegendPerItem.tsx @@ -2,15 +2,15 @@ import * as React from 'react'; import NoSsr from '@mui/material/NoSsr'; import { useTheme, styled } from '@mui/material/styles'; -import { useRtl } from '@mui/system/RtlProvider'; import { DrawingArea } from '../context/DrawingProvider'; -import { ChartsText, ChartsTextStyle } from '../ChartsText'; +import { ChartsTextStyle } from '../ChartsText'; import { CardinalDirections } from '../models/layout'; import { getWordsByLines } from '../internals/getWordsByLines'; import { GetItemSpaceType, LegendItemParams } from './chartsLegend.types'; import { legendItemPlacements } from './legendItemsPlacement'; import { useDrawingArea } from '../hooks/useDrawingArea'; import { AnchorPosition, Direction, LegendPlacement } from './legend.types'; +import { ChartsLegendItem } from './ChartsLegendItem'; import { ChartsLegendClasses } from './chartsLegendClasses'; import { DefaultizedProps } from '../models/helpers'; @@ -70,6 +70,7 @@ export interface LegendPerItemProps * @default 10 */ padding?: number | Partial>; + onItemClick?: (event: React.MouseEvent, index: number) => void; } /** @@ -110,9 +111,9 @@ export function LegendPerItem(props: LegendPerItemProps) { itemGap = 10, padding: paddingProps = 10, labelStyle: inLabelStyle, + onItemClick, } = props; const theme = useTheme(); - const isRtl = useRtl(); const drawingArea = useDrawingArea(); const labelStyle = React.useMemo( @@ -193,27 +194,20 @@ export function LegendPerItem(props: LegendPerItemProps) { return ( - {itemsWithPosition.map(({ id, label, color, positionX, positionY }) => ( - - - - + {itemsWithPosition.map((item, i) => ( + onItemClick(e, i) : undefined} + /> ))} diff --git a/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx index 250e9d3680ef5..f7df59c369524 100644 --- a/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx @@ -6,6 +6,7 @@ import { useAxis } from './useAxis'; import { ColorLegendSelector, PiecewiseLabelFormatterParams } from './legend.types'; import { LegendPerItem, LegendPerItemProps } from './LegendPerItem'; import { notNull } from '../internals/notNull'; +import { LegendItemParams, PiecewiseColorLegendItemContext } from './chartsLegend.types'; function defaultLabelFormatter(params: PiecewiseLabelFormatterParams) { if (params.min === null) { @@ -19,7 +20,7 @@ function defaultLabelFormatter(params: PiecewiseLabelFormatterParams) { export interface PiecewiseColorLegendProps extends ColorLegendSelector, - Omit { + Omit { /** * Hide the first item of the legend, corresponding to the [-infinity, min] piece. * @default false @@ -36,8 +37,28 @@ export interface PiecewiseColorLegendProps * @returns {string|null} The displayed label, or `null` to skip the item. */ labelFormatter?: (params: PiecewiseLabelFormatterParams) => string | null; + /** + * Callback fired when a legend item is clicked. + * @param {React.MouseEvent} event The click event. + * @param {PiecewiseColorLegendItemContext} legendItem The legend item data. + * @param {number} index The index of the clicked legend item. + */ + onItemClick?: ( + event: React.MouseEvent, + legendItem: PiecewiseColorLegendItemContext, + index: number, + ) => void; } +const piecewiseColorContextBuilder = (context: LegendItemParams): PiecewiseColorLegendItemContext => + ({ + type: 'piecewiseColor', + color: context.color, + label: context.label, + maxValue: context.maxValue!, + minValue: context.minValue!, + }) as const; + function PiecewiseColorLegend(props: PiecewiseColorLegendProps) { const { axisDirection, @@ -45,6 +66,7 @@ function PiecewiseColorLegend(props: PiecewiseColorLegendProps) { hideFirst, hideLast, labelFormatter = defaultLabelFormatter, + onItemClick, ...other } = props; @@ -68,24 +90,42 @@ function PiecewiseColorLegend(props: PiecewiseColorLegendProps) { return null; } - const label = labelFormatter({ - ...(index === 0 + const data = { + ...(isFirst ? { min: null, formattedMin: null } : { min: colorMap.thresholds[index - 1], formattedMin: formattedLabels[index - 1] }), - ...(index === colorMap.colors.length - 1 + ...(isLast ? { max: null, formattedMax: null } : { max: colorMap.thresholds[index], formattedMax: formattedLabels[index] }), - }); + }; + + const label = labelFormatter(data); if (label === null) { return null; } - return { id: label, color, label }; + return { + id: label, + color, + label, + minValue: data.min, + maxValue: data.max, + }; }) .filter(notNull); - return ; + return ( + onItemClick(e, piecewiseColorContextBuilder(itemsToDisplay[i]), i) + : undefined + } + /> + ); } PiecewiseColorLegend.propTypes = { @@ -153,6 +193,13 @@ PiecewiseColorLegend.propTypes = { * @default 5 */ markGap: PropTypes.number, + /** + * Callback fired when a legend item is clicked. + * @param {React.MouseEvent} event The click event. + * @param {PiecewiseColorLegendItemContext} legendItem The legend item data. + * @param {number} index The index of the clicked legend item. + */ + onItemClick: PropTypes.func, /** * Legend padding (in px). * Can either be a single number, or an object with top, left, bottom, right properties. diff --git a/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts b/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts index bf7d08c31dff2..a898c70aefbc6 100644 --- a/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts +++ b/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts @@ -1,6 +1,8 @@ import { ChartsTextStyle } from '../ChartsText'; +import { PieItemId } from '../models'; +import { SeriesId } from '../models/seriesType/common'; -export interface LegendItemParams { +interface LegendItemContextBase { /** * The color used in the legend */ @@ -9,6 +11,12 @@ export interface LegendItemParams { * The label displayed in the legend */ label: string; +} + +export interface LegendItemParams + extends Partial>, + Partial>, + LegendItemContextBase { /** * The identifier of the legend element. * Used for internal purpose such as `key` props @@ -16,6 +24,42 @@ export interface LegendItemParams { id: number | string; } +export interface SeriesLegendItemContext extends LegendItemContextBase { + /** + * The type of the legend item + * - `series` is used for series legend item + * - `piecewiseColor` is used for piecewise color legend item + */ + type: 'series'; + /** + * The identifier of the series + */ + seriesId: SeriesId; + /** + * The identifier of the pie item + */ + itemId?: PieItemId; +} + +export interface PiecewiseColorLegendItemContext extends LegendItemContextBase { + /** + * The type of the legend item + * - `series` is used for series legend item + * - `piecewiseColor` is used for piecewise color legend item + */ + type: 'piecewiseColor'; + /** + * The minimum value of the category + */ + minValue: number | Date | null; + /** + * The maximum value of the category + */ + maxValue: number | Date | null; +} + +export type LegendItemContext = SeriesLegendItemContext | PiecewiseColorLegendItemContext; + export interface LegendItemWithPosition extends LegendItemParams { positionX: number; positionY: number; diff --git a/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts b/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts index 5d710e5540569..3ec9664ff4a54 100644 --- a/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts +++ b/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts @@ -8,6 +8,8 @@ export interface ChartsLegendClasses { root: string; /** Styles applied to a series element. */ series: string; + /** Styles applied to the item background. */ + itemBackground: string; /** Styles applied to series mark element. */ mark: string; /** Styles applied to the series label. */ @@ -27,6 +29,7 @@ export function getLegendUtilityClass(slot: string) { export const legendClasses: ChartsLegendClasses = generateUtilityClasses('MuiChartsLegend', [ 'root', 'series', + 'itemBackground', 'mark', 'label', 'column', diff --git a/packages/x-charts/src/LineChart/LineChart.tsx b/packages/x-charts/src/LineChart/LineChart.tsx index 388dbee07ce23..5b501005dbdf9 100644 --- a/packages/x-charts/src/LineChart/LineChart.tsx +++ b/packages/x-charts/src/LineChart/LineChart.tsx @@ -259,6 +259,7 @@ LineChart.propTypes = { itemMarkWidth: PropTypes.number, labelStyle: PropTypes.object, markGap: PropTypes.number, + onItemClick: PropTypes.func, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ diff --git a/packages/x-charts/src/LineChart/legend.ts b/packages/x-charts/src/LineChart/legend.ts index 99038fee1a3c8..28d3ae4165461 100644 --- a/packages/x-charts/src/LineChart/legend.ts +++ b/packages/x-charts/src/LineChart/legend.ts @@ -12,9 +12,10 @@ const legendGetter: LegendGetter<'line'> = (params) => { } acc.push({ + id: seriesId, + seriesId, color: series[seriesId].color, label: formattedLabel, - id: seriesId, }); return acc; }, [] as LegendItemParams[]); diff --git a/packages/x-charts/src/PieChart/PieChart.tsx b/packages/x-charts/src/PieChart/PieChart.tsx index 8c4f4258c7aca..e976de30675df 100644 --- a/packages/x-charts/src/PieChart/PieChart.tsx +++ b/packages/x-charts/src/PieChart/PieChart.tsx @@ -284,6 +284,7 @@ PieChart.propTypes = { itemMarkWidth: PropTypes.number, labelStyle: PropTypes.object, markGap: PropTypes.number, + onItemClick: PropTypes.func, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ diff --git a/packages/x-charts/src/PieChart/legend.ts b/packages/x-charts/src/PieChart/legend.ts index a3c90e5df464b..4d39cdd7235e7 100644 --- a/packages/x-charts/src/PieChart/legend.ts +++ b/packages/x-charts/src/PieChart/legend.ts @@ -13,9 +13,11 @@ const legendGetter: LegendGetter<'pie'> = (params) => { } acc.push({ + id: item.id, + seriesId, color: item.color, label: formattedLabel, - id: item.id, + itemId: item.id, }); }); return acc; diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.tsx index 28a70b3bf0357..cbbe56dfa9e8c 100644 --- a/packages/x-charts/src/ScatterChart/ScatterChart.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterChart.tsx @@ -234,6 +234,7 @@ ScatterChart.propTypes = { itemMarkWidth: PropTypes.number, labelStyle: PropTypes.object, markGap: PropTypes.number, + onItemClick: PropTypes.func, padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ diff --git a/packages/x-charts/src/ScatterChart/legend.ts b/packages/x-charts/src/ScatterChart/legend.ts index 5e60c7db3644d..6a113642caf2b 100644 --- a/packages/x-charts/src/ScatterChart/legend.ts +++ b/packages/x-charts/src/ScatterChart/legend.ts @@ -12,9 +12,10 @@ const legendGetter: LegendGetter<'scatter'> = (params) => { } acc.push({ + id: seriesId, + seriesId, color: series[seriesId].color, label: formattedLabel, - id: seriesId, }); return acc; }, [] as LegendItemParams[]);