diff --git a/docs/data/charts/bars/BarGapNoSnap.js b/docs/data/charts/bars/BarGapNoSnap.js index 630182ccf84ee..89f97e61378d0 100644 --- a/docs/data/charts/bars/BarGapNoSnap.js +++ b/docs/data/charts/bars/BarGapNoSnap.js @@ -35,7 +35,6 @@ export default function BarGapNoSnap() { = { }, slotProps: { legend: { - direction: 'row', + direction: 'horizontal', position: { vertical: 'bottom', horizontal: 'middle' }, - padding: -5, }, }, }; diff --git a/docs/data/charts/bars/StackBars.js b/docs/data/charts/bars/StackBars.js index 1a327b699d990..8ddb6023367df 100644 --- a/docs/data/charts/bars/StackBars.js +++ b/docs/data/charts/bars/StackBars.js @@ -16,9 +16,14 @@ export default function StackBars() { { dataKey: 'treas', stack: 'equity' }, ])} xAxis={[{ scaleType: 'band', dataKey: 'year' }]} - hideLegend - width={600} - height={350} + {...config} /> ); } + +const config = { + width: 600, + height: 350, + margin: { left: 70 }, + hideLegend: true, +}; diff --git a/docs/data/charts/bars/StackBars.tsx b/docs/data/charts/bars/StackBars.tsx index 1a327b699d990..0849d4933e74e 100644 --- a/docs/data/charts/bars/StackBars.tsx +++ b/docs/data/charts/bars/StackBars.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { BarChart } from '@mui/x-charts/BarChart'; +import { BarChart, BarChartProps } from '@mui/x-charts/BarChart'; import { addLabels, balanceSheet } from './netflixsBalanceSheet'; export default function StackBars() { @@ -16,9 +16,14 @@ export default function StackBars() { { dataKey: 'treas', stack: 'equity' }, ])} xAxis={[{ scaleType: 'band', dataKey: 'year' }]} - hideLegend - width={600} - height={350} + {...config} /> ); } + +const config: Partial = { + width: 600, + height: 350, + margin: { left: 70 }, + hideLegend: true, +}; diff --git a/docs/data/charts/bars/StackBars.tsx.preview b/docs/data/charts/bars/StackBars.tsx.preview index d959a8e274dcf..22bacafb6767a 100644 --- a/docs/data/charts/bars/StackBars.tsx.preview +++ b/docs/data/charts/bars/StackBars.tsx.preview @@ -10,7 +10,5 @@ { dataKey: 'treas', stack: 'equity' }, ])} xAxis={[{ scaleType: 'band', dataKey: 'year' }]} - hideLegend - width={600} - height={350} + {...config} /> \ No newline at end of file diff --git a/docs/data/charts/components/HtmlLegend.js b/docs/data/charts/components/HtmlLegend.js index a58701b379a8f..b716c8e433055 100644 --- a/docs/data/charts/components/HtmlLegend.js +++ b/docs/data/charts/components/HtmlLegend.js @@ -1,6 +1,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; -import { unstable_useBarSeries } from '@mui/x-charts/hooks'; +import { useLegend } from '@mui/x-charts/hooks'; import { ChartDataProvider } from '@mui/x-charts/context'; import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; import { BarPlot } from '@mui/x-charts/BarChart'; @@ -8,7 +8,7 @@ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis'; function MyCustomLegend() { - const s = unstable_useBarSeries(); + const { items } = useLegend(); return ( - {Object.values(s?.series ?? []).map((v) => { + {items.map((v) => { return (
diff --git a/docs/data/charts/components/HtmlLegend.tsx b/docs/data/charts/components/HtmlLegend.tsx index a58701b379a8f..b716c8e433055 100644 --- a/docs/data/charts/components/HtmlLegend.tsx +++ b/docs/data/charts/components/HtmlLegend.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; -import { unstable_useBarSeries } from '@mui/x-charts/hooks'; +import { useLegend } from '@mui/x-charts/hooks'; import { ChartDataProvider } from '@mui/x-charts/context'; import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; import { BarPlot } from '@mui/x-charts/BarChart'; @@ -8,7 +8,7 @@ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis'; function MyCustomLegend() { - const s = unstable_useBarSeries(); + const { items } = useLegend(); return ( - {Object.values(s?.series ?? []).map((v) => { + {items.map((v) => { return (
diff --git a/docs/data/charts/components/components.md b/docs/data/charts/components/components.md index 791c338ced4fe..c89a0ef699d0e 100644 --- a/docs/data/charts/components/components.md +++ b/docs/data/charts/components/components.md @@ -83,7 +83,7 @@ With the introduction of the `ChartDataProvider` in v8, the chart data can be ac This allows you to create HTML components that interact with the charts data. In the next example, notice that `MyCustomLegend` component displays the series names and colors. -This creates an html `table` element, which handles long series names better than the default legend. +This creates an html `table` element, which can be customized in any way. {{"demo": "HtmlLegend.js"}} @@ -91,7 +91,7 @@ This creates an html `table` element, which handles long series names better tha Note that the HTML components are not part of the SVG hierarchy. Hence, they should be: -- Outside the `` component to avoid mixing HTAM and SVG. +- Outside the `` component to avoid mixing HTML and SVG. - Inside the `` component to get access to the data. ::: diff --git a/docs/data/charts/heatmap/ColorConfig.js b/docs/data/charts/heatmap/ColorConfig.js index b20e75a7a1181..16b4f95bd98a4 100644 --- a/docs/data/charts/heatmap/ColorConfig.js +++ b/docs/data/charts/heatmap/ColorConfig.js @@ -107,6 +107,7 @@ export default function ColorConfig() { xAxis={[{ data: xData }]} yAxis={[{ data: yData }]} series={[{ data }]} + margin={{ left: 70 }} zAxis={[ { min: 20, diff --git a/docs/data/charts/heatmap/ColorConfig.tsx b/docs/data/charts/heatmap/ColorConfig.tsx index ee09304de139d..d71f65a9df85e 100644 --- a/docs/data/charts/heatmap/ColorConfig.tsx +++ b/docs/data/charts/heatmap/ColorConfig.tsx @@ -110,6 +110,7 @@ export default function ColorConfig() { xAxis={[{ data: xData }]} yAxis={[{ data: yData }]} series={[{ data }]} + margin={{ left: 70 }} zAxis={[ { min: 20, diff --git a/docs/data/charts/label/PieLabel.js b/docs/data/charts/label/PieLabel.js index f363fbac7e81d..78f4259cd41de 100644 --- a/docs/data/charts/label/PieLabel.js +++ b/docs/data/charts/label/PieLabel.js @@ -21,6 +21,6 @@ export default function PieLabel() { } const props = { - width: 500, + width: 200, height: 200, }; diff --git a/docs/data/charts/label/PieLabel.tsx b/docs/data/charts/label/PieLabel.tsx index f363fbac7e81d..78f4259cd41de 100644 --- a/docs/data/charts/label/PieLabel.tsx +++ b/docs/data/charts/label/PieLabel.tsx @@ -21,6 +21,6 @@ export default function PieLabel() { } const props = { - width: 500, + width: 200, height: 200, }; diff --git a/docs/data/charts/legend/BasicColorLegend.js b/docs/data/charts/legend/BasicColorLegend.js index cb3af11c0885b..481f6e61f62e2 100644 --- a/docs/data/charts/legend/BasicColorLegend.js +++ b/docs/data/charts/legend/BasicColorLegend.js @@ -3,11 +3,12 @@ import Typography from '@mui/material/Typography'; import { LineChart } from '@mui/x-charts/LineChart'; import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine'; import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend'; +import Stack from '@mui/material/Stack'; import { dataset } from './tempAnomaly'; export default function BasicColorLegend() { return ( -
+ Global temperature anomaly relative to 1961-1990 average @@ -43,16 +44,19 @@ export default function BasicColorLegend() { ]} grid={{ horizontal: true }} height={300} - margin={{ top: 30, right: 150 }} - hideLegend + margin={{ top: 20, right: 20 }} + slotProps={{ + legend: { + axisDirection: 'x', + direction: 'vertical', + }, + }} + slots={{ + legend: PiecewiseColorLegend, + }} > - -
+ ); } diff --git a/docs/data/charts/legend/BasicColorLegend.tsx b/docs/data/charts/legend/BasicColorLegend.tsx index cb3af11c0885b..481f6e61f62e2 100644 --- a/docs/data/charts/legend/BasicColorLegend.tsx +++ b/docs/data/charts/legend/BasicColorLegend.tsx @@ -3,11 +3,12 @@ import Typography from '@mui/material/Typography'; import { LineChart } from '@mui/x-charts/LineChart'; import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine'; import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend'; +import Stack from '@mui/material/Stack'; import { dataset } from './tempAnomaly'; export default function BasicColorLegend() { return ( -
+ Global temperature anomaly relative to 1961-1990 average @@ -43,16 +44,19 @@ export default function BasicColorLegend() { ]} grid={{ horizontal: true }} height={300} - margin={{ top: 30, right: 150 }} - hideLegend + margin={{ top: 20, right: 20 }} + slotProps={{ + legend: { + axisDirection: 'x', + direction: 'vertical', + }, + }} + slots={{ + legend: PiecewiseColorLegend, + }} > - -
+ ); } diff --git a/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js b/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js index 09feb0234a322..ad79ebd83f4c5 100644 --- a/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js +++ b/docs/data/charts/legend/ContinuousInteractiveDemoNoSnap.js @@ -1,3 +1,4 @@ +// @ts-check import * as React from 'react'; import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; import { interpolateRdYlBu } from 'd3-scale-chromatic'; @@ -14,121 +15,113 @@ export default function ContinuousInteractiveDemoNoSnap() { { propName: 'direction', knob: 'select', - defaultValue: 'row', - options: ['row', 'column'], + defaultValue: 'horizontal', + options: ['horizontal', 'vertical'], + }, + { + propName: 'labelPosition', + knob: 'select', + defaultValue: 'end', + options: ['start', 'end', 'extremes'], }, { propName: 'length', knob: 'number', defaultValue: 50, min: 10, - max: 80, }, { propName: 'thickness', knob: 'number', - defaultValue: 5, + defaultValue: 12, min: 1, max: 20, }, { - propName: 'align', - knob: 'select', - defaultValue: 'middle', - options: ['start', 'middle', 'end'], - }, - { - propName: 'fontSize', - knob: 'number', - defaultValue: 10, - min: 8, - max: 20, + propName: 'reverse', + knob: 'switch', + defaultValue: false, }, ]} - renderDemo={(props) => ( -
- `${value?.toFixed(2)}°`, - }, - ]} - xAxis={[ - { - scaleType: 'time', - dataKey: 'year', - disableLine: true, - valueFormatter: (value) => value.getFullYear().toString(), + renderDemo={( + /** @type {{ direction: "horizontal" | "vertical"; length: number; thickness: number; labelPosition: 'start' | 'end' | 'extremes'; reverse: boolean; }} */ + props, + ) => ( + `${value?.toFixed(2)}°`, + }, + ]} + xAxis={[ + { + scaleType: 'time', + dataKey: 'year', + disableLine: true, + valueFormatter: (value) => value.getFullYear().toString(), + }, + ]} + yAxis={[ + { + disableLine: true, + disableTicks: true, + valueFormatter: (value) => `${value}°`, + colorMap: { + type: 'continuous', + min: -0.5, + max: 1.5, + color: (t) => interpolateRdYlBu(1 - t), }, - ]} - yAxis={[ - { - disableLine: true, - disableTicks: true, - valueFormatter: (value) => `${value}°`, - colorMap: { - type: 'continuous', - min: -0.5, - max: 1.5, - color: (t) => interpolateRdYlBu(1 - t), - }, + }, + ]} + grid={{ horizontal: true }} + height={300} + margin={{ top: 20, right: 20 }} + slots={{ legend: ContinuousColorLegend }} + slotProps={{ + legend: { + axisDirection: 'y', + direction: props.direction, + thickness: props.thickness, + labelPosition: props.labelPosition, + reverse: props.reverse, + sx: { + [props.direction === 'horizontal' ? 'width' : 'height']: + `${props.length}${props.direction === 'horizontal' ? '%' : 'px'}`, }, - ]} - grid={{ horizontal: true }} - height={300} - margin={{ - top: props.direction === 'row' ? 50 : 20, - right: props.direction === 'row' ? 20 : 50, - }} - hideLegend - > - - - -
+ }, + }} + > + + )} - getCode={({ props }) => { - return [ - `import { LineChart } from '@mui/x-charts/LineChart';`, - `import { ContinuousColorLegend } from '@mui/x-charts/ChartsLegend';`, - '', - `', - ` `, - '', - ].join('\n'); + getCode={( + /** @type {{props: { direction: "horizontal" | "vertical"; length: number; thickness: number; labelPosition: 'start' | 'end' | 'extremes'; reverse: boolean; }}} */ + { props }, + ) => { + return ` +import { ContinuousColorLegend } from '@mui/x-charts/ChartsLegend'; + + +`; }} /> ); diff --git a/docs/data/charts/legend/HiddenLegend.js b/docs/data/charts/legend/HiddenLegend.js index 80879da5f9bf5..d33b0ffa42de2 100644 --- a/docs/data/charts/legend/HiddenLegend.js +++ b/docs/data/charts/legend/HiddenLegend.js @@ -17,22 +17,20 @@ const series = [ export default function HiddenLegend() { const [isHidden, setIsHidden] = React.useState(false); + const Toggle = ( + setIsHidden(event.target.checked)} />} + label="hide the legend" + labelPlacement="end" + sx={{ margin: 'auto' }} + /> + ); + return ( - setIsHidden(event.target.checked)} /> - } - label="hide the legend" - labelPlacement="end" - /> - + {Toggle} + ); } diff --git a/docs/data/charts/legend/HiddenLegend.tsx b/docs/data/charts/legend/HiddenLegend.tsx index 80879da5f9bf5..d33b0ffa42de2 100644 --- a/docs/data/charts/legend/HiddenLegend.tsx +++ b/docs/data/charts/legend/HiddenLegend.tsx @@ -17,22 +17,20 @@ const series = [ export default function HiddenLegend() { const [isHidden, setIsHidden] = React.useState(false); + const Toggle = ( + setIsHidden(event.target.checked)} />} + label="hide the legend" + labelPlacement="end" + sx={{ margin: 'auto' }} + /> + ); + return ( - setIsHidden(event.target.checked)} /> - } - label="hide the legend" - labelPlacement="end" - /> - + {Toggle} + ); } diff --git a/docs/data/charts/legend/HiddenLegend.tsx.preview b/docs/data/charts/legend/HiddenLegend.tsx.preview index 52019cf368ffa..c20f9eec4e8c8 100644 --- a/docs/data/charts/legend/HiddenLegend.tsx.preview +++ b/docs/data/charts/legend/HiddenLegend.tsx.preview @@ -1,14 +1,2 @@ - setIsHidden(event.target.checked)} /> - } - label="hide the legend" - labelPlacement="end" -/> - \ No newline at end of file +{Toggle} + \ No newline at end of file diff --git a/docs/data/charts/legend/LegendClickNoSnap.js b/docs/data/charts/legend/LegendClickNoSnap.js index 968e601b061a7..f9ca1551987df 100644 --- a/docs/data/charts/legend/LegendClickNoSnap.js +++ b/docs/data/charts/legend/LegendClickNoSnap.js @@ -1,3 +1,5 @@ +// @ts-check + import * as React from 'react'; import Stack from '@mui/material/Stack'; import Box from '@mui/material/Box'; @@ -8,13 +10,13 @@ import UndoOutlinedIcon from '@mui/icons-material/UndoOutlined'; import { ChartsLegend, PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend'; import { HighlightedCode } from '@mui/docs/HighlightedCode'; -import { ChartContainer } from '@mui/x-charts/ChartContainer'; +import { ChartDataProvider } from '@mui/x-charts/context'; +/** @type {import('@mui/x-charts/PieChart').PieChartProps['series']} */ 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 }, @@ -22,6 +24,7 @@ const pieSeries = [ }, ]; +/** @type {import('@mui/x-charts/BarChart').BarChartProps['series']} */ const barSeries = [ { type: 'bar', @@ -37,6 +40,7 @@ const barSeries = [ }, ]; +/** @type {import('@mui/x-charts/LineChart').LineChartProps['series']} */ const lineSeries = [ { type: 'line', @@ -61,31 +65,44 @@ export default function LegendClickNoSnap() { spacing={{ xs: 0, md: 4 }} sx={{ width: '100%' }} > - + Chart Legend - + setItemData([context, index])} /> - +
Pie Chart Legend - + setItemData([context, index])} /> - - Pie Chart Legend - + Piecewise Color Legend + setItemData([context, index])} /> - + @@ -127,6 +141,7 @@ export default function LegendClickNoSnap() { aria-label="reset" size="small" onClick={() => { + // @ts-ignore setItemData(null); }} > diff --git a/docs/data/charts/legend/LegendDimensionNoSnap.js b/docs/data/charts/legend/LegendDimensionNoSnap.js index 2581092596b58..f9dd92f87bb2c 100644 --- a/docs/data/charts/legend/LegendDimensionNoSnap.js +++ b/docs/data/charts/legend/LegendDimensionNoSnap.js @@ -1,6 +1,9 @@ +// @ts-check + import * as React from 'react'; import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; import { PieChart } from '@mui/x-charts/PieChart'; +import { legendClasses } from '@mui/x-charts/ChartsLegend'; const data = [ { id: 0, value: 10, label: 'Series A' }, @@ -13,7 +16,7 @@ const data = [ { id: 7, value: 15, label: 'Series H' }, ]; -const itemsNumber = 15; +const itemsNumber = 8; export default function LegendDimensionNoSnap() { return ( @@ -23,15 +26,17 @@ export default function LegendDimensionNoSnap() { { propName: 'direction', knob: 'select', - defaultValue: 'column', - options: ['row', 'column'], + defaultValue: 'horizontal', + options: ['horizontal', 'vertical'], }, - { propName: 'itemMarkWidth', knob: 'number', defaultValue: 20 }, - { propName: 'itemMarkHeight', knob: 'number', defaultValue: 2 }, - { propName: 'markGap', knob: 'number', defaultValue: 5 }, - { propName: 'itemGap', knob: 'number', defaultValue: 10 }, + { propName: 'markSize', knob: 'number', defaultValue: 15, min: 0 }, + { propName: 'markGap', knob: 'number', defaultValue: 8, min: 0 }, + { propName: 'itemGap', knob: 'number', defaultValue: 16, min: 0 }, ]} - renderDemo={(props) => ( + renderDemo={( + /** @type {{ direction: "horizontal" | "vertical"; markSize: number; markGap: number; itemGap: number; scrollable: boolean;}} */ + props, + ) => ( )} - getCode={({ props }) => { - return [ - `import { PieChart } from '@mui/x-charts/PieChart';`, - '', - `', - ].join('\n'); + getCode={( + /** @type {{ props: { direction: "horizontal" | "vertical"; markSize: number; markGap: number; itemGap: number; scrollable: boolean;}}} */ + { props }, + ) => { + return ` +import { PieChart } from '@mui/x-charts/PieChart'; +import { legendClasses } from '@mui/x-charts/ChartsLegend'; + + +`; }} /> ); diff --git a/docs/data/charts/legend/LegendLabelPositions.js b/docs/data/charts/legend/LegendLabelPositions.js new file mode 100644 index 0000000000000..4b1817806d249 --- /dev/null +++ b/docs/data/charts/legend/LegendLabelPositions.js @@ -0,0 +1,203 @@ +import * as React from 'react'; +import { interpolateRdYlBu } from 'd3-scale-chromatic'; +import { + ContinuousColorLegend, + piecewiseColorDefaultLabelFormatter, + PiecewiseColorLegend, +} from '@mui/x-charts/ChartsLegend'; +import { ChartDataProvider } from '@mui/x-charts/context'; +import { ChartsAxesGradients } from '@mui/x-charts/internals'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; + +export default function LegendLabelPositions() { + const piecewiseFormatter = (params) => + params.index === 0 || params.index === params.length + ? piecewiseColorDefaultLabelFormatter(params) + : ''; + + return ( + `${value}°`, + colorMap: { + type: 'continuous', + min: -0.5, + max: 1.5, + color: (t) => interpolateRdYlBu(1 - t), + }, + }, + ]} + xAxis={[ + { + valueFormatter: (value) => `${value}°`, + colorMap: { + type: 'piecewise', + thresholds: [0, 1.5], + colors: [ + interpolateRdYlBu(0.9), + interpolateRdYlBu(0.5), + interpolateRdYlBu(0.1), + ], + }, + }, + ]} + > + + + Continuous + Horizontal + div': { flex: 1 } }}> +
+ start + +
+
+ end + +
+
+ extremes + +
+
+ + Vertical + div': { + flex: 1, + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + '& .MuiContinuousColorLegend-root': { flex: 1 }, + }} + > +
+ start + +
+
+ end + +
+
+ extremes + +
+
+
+ + Piecewise + Horizontal + div': { flex: 1 } }}> +
+ start + +
+
+ end + +
+
+ extremes + +
+
+ + Vertical + div': { + flex: 1, + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + }} + > +
+ start + +
+
+ end + +
+
+ extremes + +
+
+
+
+ + + +
+ ); +} diff --git a/docs/data/charts/legend/LegendLabelPositions.tsx b/docs/data/charts/legend/LegendLabelPositions.tsx new file mode 100644 index 0000000000000..9494cff502e70 --- /dev/null +++ b/docs/data/charts/legend/LegendLabelPositions.tsx @@ -0,0 +1,204 @@ +import * as React from 'react'; +import { interpolateRdYlBu } from 'd3-scale-chromatic'; +import { + ContinuousColorLegend, + piecewiseColorDefaultLabelFormatter, + PiecewiseColorLegend, + PiecewiseLabelFormatterParams, +} from '@mui/x-charts/ChartsLegend'; +import { ChartDataProvider } from '@mui/x-charts/context'; +import { ChartsAxesGradients } from '@mui/x-charts/internals'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; + +export default function LegendLabelPositions() { + const piecewiseFormatter = (params: PiecewiseLabelFormatterParams) => + params.index === 0 || params.index === params.length + ? piecewiseColorDefaultLabelFormatter(params) + : ''; + + return ( + `${value}°`, + colorMap: { + type: 'continuous', + min: -0.5, + max: 1.5, + color: (t) => interpolateRdYlBu(1 - t), + }, + }, + ]} + xAxis={[ + { + valueFormatter: (value) => `${value}°`, + colorMap: { + type: 'piecewise', + thresholds: [0, 1.5], + colors: [ + interpolateRdYlBu(0.9), + interpolateRdYlBu(0.5), + interpolateRdYlBu(0.1), + ], + }, + }, + ]} + > + + + Continuous + Horizontal + div': { flex: 1 } }}> +
+ start + +
+
+ end + +
+
+ extremes + +
+
+ + Vertical + div': { + flex: 1, + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + '& .MuiContinuousColorLegend-root': { flex: 1 }, + }} + > +
+ start + +
+
+ end + +
+
+ extremes + +
+
+
+ + Piecewise + Horizontal + div': { flex: 1 } }}> +
+ start + +
+
+ end + +
+
+ extremes + +
+
+ + Vertical + div': { + flex: 1, + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + }} + > +
+ start + +
+
+ end + +
+
+ extremes + +
+
+
+
+ + + +
+ ); +} diff --git a/docs/data/charts/legend/LegendMarkTypeNoSnap.js b/docs/data/charts/legend/LegendMarkTypeNoSnap.js new file mode 100644 index 0000000000000..a73ca40fd4adf --- /dev/null +++ b/docs/data/charts/legend/LegendMarkTypeNoSnap.js @@ -0,0 +1,64 @@ +// @ts-check + +import * as React from 'react'; +import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; +import { BarChart } from '@mui/x-charts/BarChart'; + +const seriesConfig = [ + { id: 0, data: [10], label: 'Series A' }, + { id: 1, data: [15], label: 'Series B' }, + { id: 2, data: [20], label: 'Series C' }, + { id: 3, data: [10], label: 'Series D' }, +]; + +export default function LegendMarkTypeNoSnap() { + return ( + ( + ({ + ...seriesItem, + labelMarkType: props.markType, + }))} + xAxis={[ + { + scaleType: 'band', + data: ['A'], + }, + ]} + height={200} + /> + )} + getCode={( + /** @type {{props: { markType: "square" | "circle" | "line" }}} */ + { props }, + ) => { + return ` +import { BarChart } from '@mui/x-charts/BarChart'; + + ({ + ...seriesItem, + labelMarkType: '${props.markType}', + })) + } +/> +`; + }} + /> + ); +} diff --git a/docs/data/charts/legend/LegendPositionNoSnap.js b/docs/data/charts/legend/LegendPositionNoSnap.js index ac23970b9c43e..1a96d35c09952 100644 --- a/docs/data/charts/legend/LegendPositionNoSnap.js +++ b/docs/data/charts/legend/LegendPositionNoSnap.js @@ -1,3 +1,5 @@ +// @ts-check + import * as React from 'react'; import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; import { PieChart } from '@mui/x-charts/PieChart'; @@ -28,13 +30,13 @@ export default function LegendPositionNoSnap() { { propName: 'direction', knob: 'select', - defaultValue: 'row', - options: ['row', 'column'], + defaultValue: 'vertical', + options: ['horizontal', 'vertical'], }, { propName: 'vertical', knob: 'select', - defaultValue: 'top', + defaultValue: 'middle', options: ['top', 'middle', 'bottom'], }, { @@ -43,59 +45,54 @@ export default function LegendPositionNoSnap() { defaultValue: 'middle', options: ['left', 'middle', 'right'], }, - { - propName: 'padding', - knob: 'number', - defaultValue: 0, - }, { propName: 'itemsNumber', knob: 'number', - defaultValue: 5, + defaultValue: 3, min: 1, max: data.length, }, ]} - renderDemo={(props) => ( + renderDemo={( + /** @type {{ itemsNumber: number | undefined; direction: "horizontal" | "vertical"; vertical: "top" | "middle" | "bottom"; horizontal: "left" | "middle" | "right"; }} */ + props, + ) => ( )} - getCode={({ props }) => { - return [ - `import { PieChart } from '@mui/x-charts/PieChart';`, - '', - `', - ].join('\n'); + getCode={( + /** @type {{props:{ itemsNumber: number | undefined; direction: "horizontal" | "vertical"; vertical: "top" | "middle" | "bottom"; horizontal: "left" | "middle" | "right";}}} */ + { props }, + ) => { + return ` +import { PieChart } from '@mui/x-charts/PieChart'; + + +`; }} /> ); diff --git a/docs/data/charts/legend/LegendRoundedSymbol.js b/docs/data/charts/legend/LegendRoundedSymbol.js deleted file mode 100644 index d5f9b2b864431..0000000000000 --- a/docs/data/charts/legend/LegendRoundedSymbol.js +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react'; -import { PieChart } from '@mui/x-charts/PieChart'; -import { legendClasses } from '@mui/x-charts/ChartsLegend'; - -const series = [ - { - data: [ - { id: 0, value: 10, label: 'series A' }, - { id: 1, value: 15, label: 'series B' }, - { id: 2, value: 20, label: 'series C' }, - { id: 3, value: 30, label: 'series D' }, - ], - }, -]; - -export default function LegendRoundedSymbol() { - return ( - - ); -} diff --git a/docs/data/charts/legend/LegendRoundedSymbol.tsx b/docs/data/charts/legend/LegendRoundedSymbol.tsx deleted file mode 100644 index d5f9b2b864431..0000000000000 --- a/docs/data/charts/legend/LegendRoundedSymbol.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from 'react'; -import { PieChart } from '@mui/x-charts/PieChart'; -import { legendClasses } from '@mui/x-charts/ChartsLegend'; - -const series = [ - { - data: [ - { id: 0, value: 10, label: 'series A' }, - { id: 1, value: 15, label: 'series B' }, - { id: 2, value: 20, label: 'series C' }, - { id: 3, value: 30, label: 'series D' }, - ], - }, -]; - -export default function LegendRoundedSymbol() { - return ( - - ); -} diff --git a/docs/data/charts/legend/LegendRoundedSymbol.tsx.preview b/docs/data/charts/legend/LegendRoundedSymbol.tsx.preview deleted file mode 100644 index 7ed5a28c5642d..0000000000000 --- a/docs/data/charts/legend/LegendRoundedSymbol.tsx.preview +++ /dev/null @@ -1,10 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/charts/legend/LegendTextStylingNoSnap.js b/docs/data/charts/legend/LegendTextStylingNoSnap.js index 9af402d6c16be..1ffc7be8bd4a2 100644 --- a/docs/data/charts/legend/LegendTextStylingNoSnap.js +++ b/docs/data/charts/legend/LegendTextStylingNoSnap.js @@ -1,20 +1,17 @@ +// @ts-check + import * as React from 'react'; import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; import { PieChart } from '@mui/x-charts/PieChart'; +import { labelMarkClasses } from '@mui/x-charts/ChartsLabel'; const data = [ { id: 0, value: 10, label: 'Series A' }, { id: 1, value: 15, label: 'Series B' }, { id: 2, value: 20, label: 'Series C' }, { id: 3, value: 10, label: 'Series D' }, - { id: 4, value: 15, label: 'Series E' }, - { id: 5, value: 20, label: 'Series F' }, - { id: 6, value: 10, label: 'Series G' }, - { id: 7, value: 15, label: 'Series H' }, ]; -const itemsNumber = 15; - export default function LegendTextStylingNoSnap() { return ( ( + renderDemo={( + /** @type {{ fontSize: number; color: string; markColor: string; }} */ + props, + ) => ( ({ - ...item, - label: item.label.replace(' ', props.breakLine ? '\n' : ' '), - })), + data, }, ]} + height={200} + width={200} slotProps={{ legend: { - labelStyle: { + sx: { fontSize: props.fontSize, - fill: props.fill, + color: props.color, + [`.${labelMarkClasses.fill}`]: { + fill: props.markColor, + }, }, }, }} - margin={{ - top: 10, - bottom: 10, - left: 10, - right: 200, - }} - width={400} - height={400} /> )} - getCode={({ props }) => { - return [ - `import { PieChart } from '@mui/x-charts/PieChart';`, - '', - `', - ].join('\n'); + getCode={( + /** @type {{props: { fontSize: number; color: string; markColor: string; }}} */ + { props }, + ) => { + return ` +import { PieChart } from '@mui/x-charts/PieChart'; +import { labelMarkClasses } from '@mui/x-charts/ChartsLabel'; + + +`; }} /> ); diff --git a/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js b/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js index 03ea6ccec047b..2dd1a9b8ae118 100644 --- a/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js +++ b/docs/data/charts/legend/PiecewiseInteractiveDemoNoSnap.js @@ -1,7 +1,11 @@ +// @ts-check import * as React from 'react'; import ChartsUsageDemo from 'docsx/src/modules/components/ChartsUsageDemo'; import { LineChart } from '@mui/x-charts/LineChart'; -import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend'; +import { + piecewiseColorDefaultLabelFormatter, + PiecewiseColorLegend, +} from '@mui/x-charts/ChartsLegend'; import { ChartsReferenceLine } from '@mui/x-charts/ChartsReferenceLine'; import { dataset } from './tempAnomaly'; @@ -11,109 +15,120 @@ export default function PiecewiseInteractiveDemoNoSnap() { componentName="Legend" data={[ { - propName: 'hideFirst', - knob: 'switch', + propName: 'direction', + knob: 'select', + defaultValue: 'horizontal', + options: ['horizontal', 'vertical'], }, { - propName: 'direction', + propName: 'labelPosition', knob: 'select', - defaultValue: 'row', - options: ['row', 'column'], + defaultValue: 'extremes', + options: ['start', 'end', 'extremes'], }, { - propName: 'padding', - knob: 'number', - defaultValue: 0, + propName: 'markType', + knob: 'select', + defaultValue: 'square', + options: ['square', 'circle', 'line'], }, { - propName: 'fontSize', + propName: 'onlyShowExtremes', + knob: 'switch', + defaultValue: false, + }, + { + propName: 'padding', knob: 'number', - defaultValue: 10, - min: 8, - max: 20, + defaultValue: 0, }, ]} - renderDemo={(props) => ( -
- `${value?.toFixed(2)}°`, - }, - ]} - xAxis={[ - { - scaleType: 'time', - dataKey: 'year', - disableLine: true, - valueFormatter: (value) => value.getFullYear().toString(), - colorMap: { - type: 'piecewise', - thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)], - colors: ['blue', 'gray', 'red'], - }, + renderDemo={( + /** @type {{ direction: "vertical" | "horizontal"; markType: 'square' | 'circle' | 'line'; labelPosition: 'start' | 'end' | 'extremes'; padding: number; onlyShowExtremes: boolean; }} */ + props, + ) => ( + `${value?.toFixed(2)}°`, + }, + ]} + xAxis={[ + { + scaleType: 'time', + dataKey: 'year', + disableLine: true, + valueFormatter: (value) => value.getFullYear().toString(), + colorMap: { + type: 'piecewise', + thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)], + colors: ['blue', 'gray', 'red'], }, - ]} - yAxis={[ - { - disableLine: true, - disableTicks: true, - valueFormatter: (value) => `${value}°`, + }, + ]} + yAxis={[ + { + disableLine: true, + disableTicks: true, + valueFormatter: (value) => `${value}°`, + }, + ]} + grid={{ horizontal: true }} + height={300} + margin={{ top: 20, right: 20 }} + slots={{ + legend: PiecewiseColorLegend, + }} + slotProps={{ + legend: { + axisDirection: 'x', + direction: props.direction, + markType: props.markType, + labelPosition: props.labelPosition, + labelFormatter: props.onlyShowExtremes + ? (params) => + params.index === 0 || params.index === params.length + ? piecewiseColorDefaultLabelFormatter(params) + : '' + : undefined, + sx: { + padding: props.padding, }, - ]} - grid={{ horizontal: true }} - height={300} - margin={{ - top: props.direction === 'row' ? 50 : 20, - right: props.direction === 'row' ? 20 : 150, - }} - hideLegend - > - - - -
+ }, + }} + > + + )} - getCode={({ props }) => { - return [ - `import { LineChart } from '@mui/x-charts/LineChart';`, - `import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend';`, - '', - `', - ` `, - '', - ].join('\n'); + getCode={( + /** @type {{props:{ direction: "vertical" | "horizontal"; markType: 'square' | 'circle' | 'line'; labelPosition: 'start' | 'end' | 'extremes'; padding: number; onlyShowExtremes: boolean; }}} */ + { props }, + ) => { + return ` +import { + PiecewiseColorLegend, + piecewiseColorDefaultLabelFormatter, +} from '@mui/x-charts/ChartsLegend'; + +\n params.index === 0 || params.index === params.length\n ? piecewiseColorDefaultLabelFormatter(params) \n : ''" : ''} + }, + }} +/> +`; }} /> ); diff --git a/docs/data/charts/legend/ScrollableLegend.js b/docs/data/charts/legend/ScrollableLegend.js new file mode 100644 index 0000000000000..954cf672ee553 --- /dev/null +++ b/docs/data/charts/legend/ScrollableLegend.js @@ -0,0 +1,47 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { PieChart } from '@mui/x-charts/PieChart'; + +const series = [ + { + data: [ + { id: 0, value: 10, label: 'Series A' }, + { id: 1, value: 15, label: 'Series B' }, + { id: 2, value: 20, label: 'Series C' }, + { id: 3, value: 10, label: 'Series D' }, + { id: 4, value: 15, label: 'Series E' }, + { id: 5, value: 20, label: 'Series F' }, + { id: 6, value: 10, label: 'Series G' }, + { id: 7, value: 15, label: 'Series H' }, + { id: 8, value: 20, label: 'Series I' }, + { id: 9, value: 10, label: 'Series J' }, + { id: 10, value: 15, label: 'Series K' }, + { id: 11, value: 20, label: 'Series L' }, + { id: 12, value: 10, label: 'Series M' }, + { id: 13, value: 15, label: 'Series N' }, + { id: 14, value: 20, label: 'Series O' }, + ], + }, +]; + +export default function ScrollableLegend() { + return ( + + + + ); +} diff --git a/docs/data/charts/legend/ScrollableLegend.tsx b/docs/data/charts/legend/ScrollableLegend.tsx new file mode 100644 index 0000000000000..954cf672ee553 --- /dev/null +++ b/docs/data/charts/legend/ScrollableLegend.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { PieChart } from '@mui/x-charts/PieChart'; + +const series = [ + { + data: [ + { id: 0, value: 10, label: 'Series A' }, + { id: 1, value: 15, label: 'Series B' }, + { id: 2, value: 20, label: 'Series C' }, + { id: 3, value: 10, label: 'Series D' }, + { id: 4, value: 15, label: 'Series E' }, + { id: 5, value: 20, label: 'Series F' }, + { id: 6, value: 10, label: 'Series G' }, + { id: 7, value: 15, label: 'Series H' }, + { id: 8, value: 20, label: 'Series I' }, + { id: 9, value: 10, label: 'Series J' }, + { id: 10, value: 15, label: 'Series K' }, + { id: 11, value: 20, label: 'Series L' }, + { id: 12, value: 10, label: 'Series M' }, + { id: 13, value: 15, label: 'Series N' }, + { id: 14, value: 20, label: 'Series O' }, + ], + }, +]; + +export default function ScrollableLegend() { + return ( + + + + ); +} diff --git a/docs/data/charts/legend/ScrollableLegend.tsx.preview b/docs/data/charts/legend/ScrollableLegend.tsx.preview new file mode 100644 index 0000000000000..6f7c38b28639a --- /dev/null +++ b/docs/data/charts/legend/ScrollableLegend.tsx.preview @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/docs/data/charts/legend/VeryBasicColorLegend.js b/docs/data/charts/legend/VeryBasicColorLegend.js new file mode 100644 index 0000000000000..766603bb4b903 --- /dev/null +++ b/docs/data/charts/legend/VeryBasicColorLegend.js @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { LineChart } from '@mui/x-charts/LineChart'; +import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend'; +import Stack from '@mui/material/Stack'; +import { dataset } from './tempAnomaly'; + +const data = { + dataset, + series: [ + { + label: 'Global temperature anomaly relative to 1961-1990', + dataKey: 'anomaly', + showMark: false, + valueFormatter: (value) => `${value?.toFixed(2)}°`, + }, + ], + xAxis: [ + { + scaleType: 'time', + dataKey: 'year', + disableLine: true, + valueFormatter: (value) => value.getFullYear().toString(), + colorMap: { + type: 'piecewise', + thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)], + colors: ['blue', 'gray', 'red'], + }, + }, + ], + yAxis: [ + { + disableLine: true, + disableTicks: true, + valueFormatter: (value) => `${value}°`, + }, + ], + grid: { horizontal: true }, + height: 300, + margin: { top: 20, right: 20 }, +}; + +export default function VeryBasicColorLegend() { + return ( + + + + ); +} diff --git a/docs/data/charts/legend/VeryBasicColorLegend.tsx b/docs/data/charts/legend/VeryBasicColorLegend.tsx new file mode 100644 index 0000000000000..8468e565f6f6f --- /dev/null +++ b/docs/data/charts/legend/VeryBasicColorLegend.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { LineChart, LineChartProps } from '@mui/x-charts/LineChart'; +import { PiecewiseColorLegend } from '@mui/x-charts/ChartsLegend'; +import Stack from '@mui/material/Stack'; +import { dataset } from './tempAnomaly'; + +const data: LineChartProps = { + dataset, + series: [ + { + label: 'Global temperature anomaly relative to 1961-1990', + dataKey: 'anomaly', + showMark: false, + valueFormatter: (value) => `${value?.toFixed(2)}°`, + }, + ], + xAxis: [ + { + scaleType: 'time', + dataKey: 'year', + disableLine: true, + valueFormatter: (value) => value.getFullYear().toString(), + colorMap: { + type: 'piecewise', + thresholds: [new Date(1961, 0, 1), new Date(1990, 0, 1)], + colors: ['blue', 'gray', 'red'], + }, + }, + ], + yAxis: [ + { + disableLine: true, + disableTicks: true, + valueFormatter: (value) => `${value}°`, + }, + ], + grid: { horizontal: true }, + height: 300, + margin: { top: 20, right: 20 }, +}; + +export default function VeryBasicColorLegend() { + return ( + + + + ); +} diff --git a/docs/data/charts/legend/VeryBasicColorLegend.tsx.preview b/docs/data/charts/legend/VeryBasicColorLegend.tsx.preview new file mode 100644 index 0000000000000..183ce44d8eed9 --- /dev/null +++ b/docs/data/charts/legend/VeryBasicColorLegend.tsx.preview @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/docs/data/charts/legend/legend.md b/docs/data/charts/legend/legend.md index 77c554482acaf..781c05e809656 100644 --- a/docs/data/charts/legend/legend.md +++ b/docs/data/charts/legend/legend.md @@ -1,7 +1,7 @@ --- title: Charts - Legend productId: x-charts -components: ChartsLegend, DefaultChartsLegend, ChartsText, ContinuousColorLegend, PiecewiseColorLegend +components: ChartsLegend, DefaultChartsLegend, ChartsText, ContinuousColorLegend, PiecewiseColorLegend, ChartsLabel, ChartsLabelMark, ChartsLabelGradient --- # Charts - Legend @@ -16,56 +16,69 @@ In chart components, the legend links series with `label` properties and their c ## Customization +This section explains how to customize the legend using classes and properties. + +In order to fully customize the legend with custom components, see an example at the [HTML components docs](https://mui.com/x/react-charts/components/#html-components). + +### Dimensions + +Much of the customization can be done using CSS properties. +There is a main class for the legend container, `.MuiChartsLegend-root`. +Alternatively the `legendClasses` variable can be used if using CSS-in-JS to target the elements. + +Each legend item is composed of two main elements: the `mark` and the `label`. + +The example below explains how it is possible to customize some parts the legend. +And shows how to use both the `legendClasses` variable and the CSS class directly. + +{{"demo": "LegendDimensionNoSnap.js", "hideToolbar": true, "bg": "playground"}} + ### Position -The legend can either be displayed in a `'column'` or `'row'` layout controlled with the `direction` property. +The legend can either be displayed in a `'vertical'` or `'horizontal'` layout controlled with the `direction` property. -It can also be moved with the `position: { vertical, horizontal }` property which defines how the legend aligns within the SVG. +It can also be moved with the `position: { vertical, horizontal }` property which defines how the legend aligns itself in the parent container. - `vertical` can be `'top'`, `'middle'`, or `'bottom'`. - `horizontal` can be `'left'`, `'middle'`, or `'right'`. -You can add spacing to a given `position` with the `padding` property, which can be either a number or an object `{ top, bottom, left, right }`. -This `padding` will add space between the SVG borders and the legend. - By default, the legend is placed above the charts. +:::warning +The `position` property is only available in the `slotProps`, but not in the ``. +In the second case, you are free to place the legend where you want. +::: + {{"demo": "LegendPositionNoSnap.js", "hideToolbar": true, "bg": "playground"}} ### Hiding -You can hide the legend with the property `slotProps.legend.hidden`. +You can hide the legend with the `hideLegend` property of the Chart. {{"demo": "HiddenLegend.js"}} -### Dimensions - -Inside the legend, you can customize the pixel value of the width and height of the mark with the `itemMarkWidth` and `itemMarkHeight` props. +### Label styling -You can also access the `markGap` prop to change the gap between the mark and its label, or the `itemGap` to change the gap between two legend items. -Both props impact the values defined in pixels. +Changing the `label` style can be done by targeting the root component's font properties. -{{"demo": "LegendDimensionNoSnap.js", "hideToolbar": true, "bg": "playground"}} +To change the `mark` color or shape, the `fill` class is used instead. +Keep in mind that the `mark` is an SVG element, so the `fill` property is used to change its color. -### Label styling +{{"demo": "LegendTextStylingNoSnap.js", "hideToolbar": true, "bg": "playground"}} -To break lines in legend labels, use the special `\n` character. To customize the label style, you should not use CSS. -Instead, pass a styling object to the `labelStyle` property. +### Change mark shape -{{"demo": "LegendTextStylingNoSnap.js", "hideToolbar": true, "bg": "playground"}} +To change the mark shape, you can use the `labelMarkType` property of the series item. +For the `pie` series, the `labelMarkType` property is available for each of the pie slices too. -:::info -The `labelStyle` property is needed to measure text size, and then place legend items at the correct position. -Style applied by other means will not be taken into account. -Which can lead to label overflow. -::: +{{"demo": "LegendMarkTypeNoSnap.js", "hideToolbar": true, "bg": "playground"}} -### Rounded symbol +### Scrollable legend -To create a rounded symbol, use the `legendClasses.mark` to apply CSS on marks. -Notice that SVG `rect` uses `ry` property to control the symbol's corner radius instead of `border-radius`. +The legend can be made scrollable by setting the `overflowY` for vertical legends or `overflowX` for horizontal legends. +Make sure that the legend container has a fixed height or width to enable scrolling. -{{"demo": "LegendRoundedSymbol.js"}} +{{"demo": "ScrollableLegend.js"}} ## Color legend @@ -74,6 +87,10 @@ To display legend associated to a [colorMap](https://mui.com/x/react-charts/styl - `` if you're using `colorMap.type='continuous'` - `` if you're using `colorMap.type='piecewise'`. +Then it is possible to override the `legend` slot to display the wanted legend, or use the [composition API](https://mui.com/x/react-charts/composition/) to add as many legends as needed. + +{{"demo": "VeryBasicColorLegend.js"}} + ### Select data To select the color mapping to represent, use the following props: @@ -87,16 +104,27 @@ To select the color mapping to represent, use the following props: This component position is done exactly the same way as the [legend for series](#position). +### Label position + +The labels can be positioned in relation to the marks or gradient with the `labelPosition` prop. +The values accepted are `'start'`, `'end'` or `'extremes'`. + +- With `direction='horizontal'`, using `'start'` places the labels above the visual marker, while `end` places them below. +- When `direction='vertical'`, is `'start'` or `'end'` the labels are positioned `left` and `right` of the visual markers, respectively. +- With the `'extremes'` value, the labels are positioned at both the beginning and end of the visual marker. + +{{"demo": "LegendLabelPositions.js"}} + ### Continuous color mapping To modify the shape of the gradient, use the `length` and `thickness` props. -The `length` can either be a number (in px) or a percentage string. The `"100%"` corresponds to the SVG dimension. +The `length` can either be a number (in px) or a percentage string. The `"100%"` corresponds to the parent dimension. To format labels use the `minLabel`/`maxLabel`. They accept either a string to display. Or a function `({value, formattedValue}) => string`. -The labels and gradient bar alignment can be modified by the `align` prop. +It is also possible to reverse the gradient with the `reverse` prop. {{"demo": "ContinuousInteractiveDemoNoSnap.js", "hideToolbar": true, "bg": "playground"}} @@ -105,18 +133,21 @@ The labels and gradient bar alignment can be modified by the `align` prop. The piecewise Legend is quite similar to the series legend. It accepts the same props for [customization](#dimensions). -The props `hideFirst` and `hideLast` allows to hide the two extreme pieces: values lower than the min threshold, and value higher than the max threshold. - To override labels generated by default, provide a `labelFormatter` prop. It takes the min/max of the piece and returns the label. Values can be `null` for the first and last pieces. And returning `null` removes the piece from the legend. +Returning an empty string removes any label, but still display the `mark`. ```ts -labelFormatter = ({ min, max, formattedMin, formattedMax }) => string | null; +labelFormatter = ({ index, length, min, max, formattedMin, formattedMax }) => + string | null; ``` +The `markType` can be changed with the `markType` prop. +Since the color values are based on the axis, and not the series, the `markType` has to be set directly on the legend. + {{"demo": "PiecewiseInteractiveDemoNoSnap.js", "hideToolbar": true, "bg": "playground"}} ## Click event diff --git a/docs/data/charts/pie-demo/PieChartWithCenterLabel.js b/docs/data/charts/pie-demo/PieChartWithCenterLabel.js index 41f892141c412..9e094cd1abca9 100644 --- a/docs/data/charts/pie-demo/PieChartWithCenterLabel.js +++ b/docs/data/charts/pie-demo/PieChartWithCenterLabel.js @@ -11,7 +11,7 @@ const data = [ ]; const size = { - width: 400, + width: 200, height: 200, }; diff --git a/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx b/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx index 3177a8e13c233..8d619a7e5b988 100644 --- a/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx +++ b/docs/data/charts/pie-demo/PieChartWithCenterLabel.tsx @@ -11,7 +11,7 @@ const data = [ ]; const size = { - width: 400, + width: 200, height: 200, }; diff --git a/docs/data/charts/pie-demo/StraightAnglePieChart.js b/docs/data/charts/pie-demo/StraightAnglePieChart.js index 444b0cac8fb88..9efd2ef122719 100644 --- a/docs/data/charts/pie-demo/StraightAnglePieChart.js +++ b/docs/data/charts/pie-demo/StraightAnglePieChart.js @@ -21,6 +21,7 @@ export default function StraightAnglePieChart() { }, ]} height={300} + width={300} /> ); } diff --git a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx index 444b0cac8fb88..9efd2ef122719 100644 --- a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx +++ b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx @@ -21,6 +21,7 @@ export default function StraightAnglePieChart() { }, ]} height={300} + width={300} /> ); } diff --git a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview index e75631909bfe8..7892941a06c16 100644 --- a/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview +++ b/docs/data/charts/pie-demo/StraightAnglePieChart.tsx.preview @@ -7,4 +7,5 @@ }, ]} height={300} + width={300} /> \ No newline at end of file diff --git a/docs/data/charts/pie/BasicPie.js b/docs/data/charts/pie/BasicPie.js index 2899266e0316b..97d722784cda2 100644 --- a/docs/data/charts/pie/BasicPie.js +++ b/docs/data/charts/pie/BasicPie.js @@ -13,7 +13,7 @@ export default function BasicPie() { ], }, ]} - width={400} + width={200} height={200} /> ); diff --git a/docs/data/charts/pie/BasicPie.tsx b/docs/data/charts/pie/BasicPie.tsx index 2899266e0316b..97d722784cda2 100644 --- a/docs/data/charts/pie/BasicPie.tsx +++ b/docs/data/charts/pie/BasicPie.tsx @@ -13,7 +13,7 @@ export default function BasicPie() { ], }, ]} - width={400} + width={200} height={200} /> ); diff --git a/docs/data/charts/pie/BasicPie.tsx.preview b/docs/data/charts/pie/BasicPie.tsx.preview index 61ac8ef626f20..28431a59a1325 100644 --- a/docs/data/charts/pie/BasicPie.tsx.preview +++ b/docs/data/charts/pie/BasicPie.tsx.preview @@ -8,6 +8,6 @@ ], }, ]} - width={400} + width={200} height={200} /> \ No newline at end of file diff --git a/docs/data/charts/pie/PieActiveArc.js b/docs/data/charts/pie/PieActiveArc.js index 4e151e983c04f..8e6af13f69d65 100644 --- a/docs/data/charts/pie/PieActiveArc.js +++ b/docs/data/charts/pie/PieActiveArc.js @@ -14,6 +14,7 @@ export default function PieActiveArc() { }, ]} height={200} + width={200} /> ); } diff --git a/docs/data/charts/pie/PieActiveArc.tsx b/docs/data/charts/pie/PieActiveArc.tsx index 4e151e983c04f..8e6af13f69d65 100644 --- a/docs/data/charts/pie/PieActiveArc.tsx +++ b/docs/data/charts/pie/PieActiveArc.tsx @@ -14,6 +14,7 @@ export default function PieActiveArc() { }, ]} height={200} + width={200} /> ); } diff --git a/docs/data/charts/pie/PieActiveArc.tsx.preview b/docs/data/charts/pie/PieActiveArc.tsx.preview index 6e94368e1bbb3..3e5229579966b 100644 --- a/docs/data/charts/pie/PieActiveArc.tsx.preview +++ b/docs/data/charts/pie/PieActiveArc.tsx.preview @@ -8,4 +8,5 @@ }, ]} height={200} + width={200} /> \ No newline at end of file diff --git a/docs/data/charts/pie/PieAnimation.js b/docs/data/charts/pie/PieAnimation.js index 51584d139a45e..f8b488baa6a24 100644 --- a/docs/data/charts/pie/PieAnimation.js +++ b/docs/data/charts/pie/PieAnimation.js @@ -29,6 +29,7 @@ export default function PieAnimation() { setItemData(d)} diff --git a/docs/data/charts/scatter/ScatterDataset.js b/docs/data/charts/scatter/ScatterDataset.js index 5a50fdc796762..b767d2169ac9e 100644 --- a/docs/data/charts/scatter/ScatterDataset.js +++ b/docs/data/charts/scatter/ScatterDataset.js @@ -88,6 +88,7 @@ const chartSetting = { }, width: 500, height: 300, + margin: { left: 60 }, }; export default function ScatterDataset() { diff --git a/docs/data/charts/scatter/ScatterDataset.tsx b/docs/data/charts/scatter/ScatterDataset.tsx index 5a50fdc796762..b767d2169ac9e 100644 --- a/docs/data/charts/scatter/ScatterDataset.tsx +++ b/docs/data/charts/scatter/ScatterDataset.tsx @@ -88,6 +88,7 @@ const chartSetting = { }, width: 500, height: 300, + margin: { left: 60 }, }; export default function ScatterDataset() { diff --git a/docs/data/charts/styling/BasicColor.js b/docs/data/charts/styling/BasicColor.js index 49bb1619c9a45..a1325422ac1e8 100644 --- a/docs/data/charts/styling/BasicColor.js +++ b/docs/data/charts/styling/BasicColor.js @@ -29,7 +29,7 @@ export default function BasicColor() { }; return ( - + + - + - - - - + + + + + +
); } diff --git a/docs/data/charts/styling/PatternPie.tsx b/docs/data/charts/styling/PatternPie.tsx index 02f53eebf0161..8749891afa008 100644 --- a/docs/data/charts/styling/PatternPie.tsx +++ b/docs/data/charts/styling/PatternPie.tsx @@ -1,44 +1,47 @@ import * as React from 'react'; import { PieChart } from '@mui/x-charts/PieChart'; +import { Stack } from '@mui/system'; export default function PatternPie() { return ( - - + - - - - + + + + + + ); } diff --git a/docs/data/charts/tooltip/AxisFormatter.js b/docs/data/charts/tooltip/AxisFormatter.js index ec92663310f40..ef3ff43fb3fa1 100644 --- a/docs/data/charts/tooltip/AxisFormatter.js +++ b/docs/data/charts/tooltip/AxisFormatter.js @@ -48,6 +48,7 @@ const chartParams = { dataset, width: 600, height: 400, + margin: { left: 60 }, sx: { [`.${axisClasses.left} .${axisClasses.label}`]: { transform: 'translate(-20px, 0)', diff --git a/docs/data/charts/tooltip/AxisFormatter.tsx b/docs/data/charts/tooltip/AxisFormatter.tsx index 0a687484e96b9..d0781688dbdc4 100644 --- a/docs/data/charts/tooltip/AxisFormatter.tsx +++ b/docs/data/charts/tooltip/AxisFormatter.tsx @@ -48,6 +48,7 @@ const chartParams: BarChartProps = { dataset, width: 600, height: 400, + margin: { left: 60 }, sx: { [`.${axisClasses.left} .${axisClasses.label}`]: { transform: 'translate(-20px, 0)', diff --git a/docs/data/charts/tooltip/SeriesFormatter.js b/docs/data/charts/tooltip/SeriesFormatter.js index d876c031babbe..d1a841ec1c134 100644 --- a/docs/data/charts/tooltip/SeriesFormatter.js +++ b/docs/data/charts/tooltip/SeriesFormatter.js @@ -3,7 +3,7 @@ import { PieChart } from '@mui/x-charts/PieChart'; import { legendClasses } from '@mui/x-charts/ChartsLegend'; const otherProps = { - width: 400, + width: 200, height: 200, sx: { [`.${legendClasses.root}`]: { diff --git a/docs/data/charts/tooltip/SeriesFormatter.tsx b/docs/data/charts/tooltip/SeriesFormatter.tsx index 030064774ba97..b60633f83fe75 100644 --- a/docs/data/charts/tooltip/SeriesFormatter.tsx +++ b/docs/data/charts/tooltip/SeriesFormatter.tsx @@ -3,7 +3,7 @@ import { PieChart, PieChartProps } from '@mui/x-charts/PieChart'; import { legendClasses } from '@mui/x-charts/ChartsLegend'; const otherProps: Partial = { - width: 400, + width: 200, height: 200, sx: { [`.${legendClasses.root}`]: { diff --git a/docs/data/chartsApiPages.ts b/docs/data/chartsApiPages.ts index c4ec485b59c66..dbc2961b5e3d8 100644 --- a/docs/data/chartsApiPages.ts +++ b/docs/data/chartsApiPages.ts @@ -119,10 +119,6 @@ const chartsApiPages: MuiPage[] = [ pathname: '/x/api/charts/continuous-color-legend', title: 'ContinuousColorLegend', }, - { - pathname: '/x/api/charts/default-charts-legend', - title: 'DefaultChartsLegend', - }, { pathname: '/x/api/charts/gauge', title: 'Gauge', diff --git a/docs/data/migration/migration-charts-v7/migration-charts-v7.md b/docs/data/migration/migration-charts-v7/migration-charts-v7.md index e192355a5d389..68df47f3abe3e 100644 --- a/docs/data/migration/migration-charts-v7/migration-charts-v7.md +++ b/docs/data/migration/migration-charts-v7/migration-charts-v7.md @@ -92,6 +92,25 @@ To pass props to the legend, use the `slotProps.legend`. + ``` +## Legend direction value change ✅ + +The `direction` prop of the legend has been changed to accept `'vertical'` and `'horizontal'` instead of `'column'` and `'row'`. + +```diff + +``` + +## The `getSeriesToDisplay` function was removed + +The `getSeriesToDisplay` function was removed in favor of the `useLegend` hook. You can check the [HTML Components example](/x/react-charts/components/#html-components) for usage information. + ## Removing ResponsiveChartContainer ✅ The `ResponsiveChartContainer` has been removed. diff --git a/docs/pages/x/api/charts/bar-chart.json b/docs/pages/x/api/charts/bar-chart.json index 7cb920914914d..bc3bf924ede77 100644 --- a/docs/pages/x/api/charts/bar-chart.json +++ b/docs/pages/x/api/charts/bar-chart.json @@ -149,7 +149,7 @@ { "name": "legend", "description": "Custom rendering of the legend.", - "default": "DefaultChartsLegend", + "default": "ChartsLegend", "class": null }, { @@ -173,7 +173,7 @@ ], "classes": [], "spread": true, - "themeDefaultProps": true, + "themeDefaultProps": false, "muiName": "MuiBarChart", "forwardsRefTo": "SVGSVGElement", "filename": "/packages/x-charts/src/BarChart/BarChart.tsx", diff --git a/docs/pages/x/api/charts/bar-series-type.json b/docs/pages/x/api/charts/bar-series-type.json index de9972728689c..41abd5617c88d 100644 --- a/docs/pages/x/api/charts/bar-series-type.json +++ b/docs/pages/x/api/charts/bar-series-type.json @@ -14,6 +14,7 @@ "label": { "type": { "description": "string | ((location: 'tooltip' | 'legend') => string)" } }, + "labelMarkType": { "type": { "description": "ChartsLabelMarkProps['type']" } }, "layout": { "type": { "description": "'horizontal' | 'vertical'" }, "default": "'vertical'" }, "stack": { "type": { "description": "string" } }, "stackOffset": { "type": { "description": "StackOffsetType" }, "default": "'diverging'" }, diff --git a/docs/pages/x/api/charts/charts-legend.json b/docs/pages/x/api/charts/charts-legend.json index 8e58410c7fd64..b3326b90cf78b 100644 --- a/docs/pages/x/api/charts/charts-legend.json +++ b/docs/pages/x/api/charts/charts-legend.json @@ -1,33 +1,16 @@ { "props": { "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, - "direction": { "type": { "name": "enum", "description": "'column'
| 'row'" } }, - "hidden": { "type": { "name": "bool" }, "default": "false" }, - "itemGap": { "type": { "name": "number" }, "default": "10" }, - "itemMarkHeight": { "type": { "name": "number" }, "default": "20" }, - "itemMarkWidth": { "type": { "name": "number" }, "default": "20" }, - "labelStyle": { "type": { "name": "object" }, "default": "theme.typography.subtitle1" }, - "markGap": { "type": { "name": "number" }, "default": "5" }, + "direction": { + "type": { "name": "enum", "description": "'horizontal'
| 'vertical'" } + }, "onItemClick": { "type": { "name": "func" }, "signature": { - "type": "function(event: React.MouseEvent, legendItem: SeriesLegendItemContext, index: number) => void", + "type": "function(event: React.MouseEvent, legendItem: SeriesLegendItemContext, index: number) => void", "describedArgs": ["event", "legendItem", "index"] } }, - "padding": { - "type": { - "name": "union", - "description": "number
| { bottom?: number, left?: number, right?: number, top?: number }" - }, - "default": "10" - }, - "position": { - "type": { - "name": "shape", - "description": "{ horizontal: 'left'
| 'middle'
| 'right', vertical: 'bottom'
| 'middle'
| 'top' }" - } - }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, @@ -45,21 +28,15 @@ { "name": "legend", "description": "Custom rendering of the legend.", - "default": "DefaultChartsLegend", + "default": "ChartsLegend", "class": null } ], "classes": [ { - "key": "column", - "className": "MuiChartsLegend-column", - "description": "Styles applied to the legend with column layout.", - "isGlobal": false - }, - { - "key": "itemBackground", - "className": "MuiChartsLegend-itemBackground", - "description": "Styles applied to the item background.", + "key": "horizontal", + "className": "MuiChartsLegend-horizontal", + "description": "Styles applied to the legend in row layout.", "isGlobal": false }, { @@ -80,20 +57,23 @@ "description": "Styles applied to the root element.", "isGlobal": false }, - { - "key": "row", - "className": "MuiChartsLegend-row", - "description": "Styles applied to the legend with row layout.", - "isGlobal": false - }, { "key": "series", "className": "MuiChartsLegend-series", "description": "Styles applied to a series element.", "isGlobal": false + }, + { + "key": "vertical", + "className": "MuiChartsLegend-vertical", + "description": "Styles applied to the legend in column layout.", + "isGlobal": false } ], + "spread": true, + "themeDefaultProps": true, "muiName": "MuiChartsLegend", + "forwardsRefTo": "HTMLUListElement", "filename": "/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/x/api/charts/continuous-color-legend.json b/docs/pages/x/api/charts/continuous-color-legend.json index 654a9fc15dfd1..ab6fee144fa9c 100644 --- a/docs/pages/x/api/charts/continuous-color-legend.json +++ b/docs/pages/x/api/charts/continuous-color-legend.json @@ -1,12 +1,5 @@ { "props": { - "align": { - "type": { - "name": "enum", - "description": "'end'
| 'middle'
| 'start'" - }, - "default": "'middle'" - }, "axisDirection": { "type": { "name": "enum", "description": "'x'
| 'y'
| 'z'" }, "default": "'z'" @@ -15,36 +8,30 @@ "type": { "name": "union", "description": "number
| string" }, "default": "The first axis item." }, - "direction": { "type": { "name": "enum", "description": "'column'
| 'row'" } }, - "id": { "type": { "name": "string" }, "default": "auto-generated id" }, - "labelStyle": { "type": { "name": "object" }, "default": "theme.typography.subtitle1" }, - "length": { - "type": { "name": "union", "description": "number
| string" }, - "default": "'50%'" + "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, + "direction": { + "type": { "name": "enum", "description": "'horizontal'
| 'vertical'" }, + "default": "'horizontal'" + }, + "gradientId": { "type": { "name": "string" }, "default": "auto-generated id" }, + "labelPosition": { + "type": { + "name": "enum", + "description": "'start'
| 'end'
| 'extremes'" + }, + "default": "'end'" }, "maxLabel": { "type": { "name": "union", "description": "func
| string" }, - "default": "({ formattedValue }) => formattedValue" + "default": "formattedValue" }, "minLabel": { "type": { "name": "union", "description": "func
| string" }, - "default": "({ formattedValue }) => formattedValue" - }, - "position": { - "type": { - "name": "shape", - "description": "{ horizontal: 'left'
| 'middle'
| 'right', vertical: 'bottom'
| 'middle'
| 'top' }" - } + "default": "formattedValue" }, - "scaleType": { - "type": { - "name": "enum", - "description": "'linear'
| 'log'
| 'pow'
| 'sqrt'
| 'time'
| 'utc'" - }, - "default": "'linear'" - }, - "spacing": { "type": { "name": "number" }, "default": "4" }, - "thickness": { "type": { "name": "number" }, "default": "5" } + "reverse": { "type": { "name": "bool" }, "default": "false" }, + "rotateGradient": { "type": { "name": "bool" } }, + "thickness": { "type": { "name": "number" }, "default": "12" } }, "name": "ContinuousColorLegend", "imports": [ @@ -52,8 +39,72 @@ "import { ContinuousColorLegend } from '@mui/x-charts';", "import { ContinuousColorLegend } from '@mui/x-charts-pro';" ], - "classes": [], + "classes": [ + { + "key": "end", + "className": "MuiContinuousColorLegend-end", + "description": "Styles applied to the legend with the labels after the gradient.", + "isGlobal": false + }, + { + "key": "extremes", + "className": "MuiContinuousColorLegend-extremes", + "description": "Styles applied to the legend with the labels on the extremes of the gradient.", + "isGlobal": false + }, + { + "key": "gradient", + "className": "MuiContinuousColorLegend-gradient", + "description": "Styles applied to the list item with the gradient.", + "isGlobal": false + }, + { + "key": "horizontal", + "className": "MuiContinuousColorLegend-horizontal", + "description": "Styles applied to the legend in row layout.", + "isGlobal": false + }, + { + "key": "label", + "className": "MuiContinuousColorLegend-label", + "description": "Styles applied to the series label.", + "isGlobal": false + }, + { + "key": "maxLabel", + "className": "MuiContinuousColorLegend-maxLabel", + "description": "Styles applied to the list item that renders the `maxLabel`.", + "isGlobal": false + }, + { + "key": "minLabel", + "className": "MuiContinuousColorLegend-minLabel", + "description": "Styles applied to the list item that renders the `minLabel`.", + "isGlobal": false + }, + { + "key": "root", + "className": "MuiContinuousColorLegend-root", + "description": "Styles applied to the root element.", + "isGlobal": false + }, + { + "key": "start", + "className": "MuiContinuousColorLegend-start", + "description": "Styles applied to the legend with the labels before the gradient.", + "isGlobal": false + }, + { + "key": "vertical", + "className": "MuiContinuousColorLegend-vertical", + "description": "Styles applied to the legend in column layout.", + "isGlobal": false + } + ], + "spread": true, + "themeDefaultProps": true, "muiName": "MuiContinuousColorLegend", + "forwardsRefTo": "HTMLUListElement", "filename": "/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/x/api/charts/default-charts-legend.js b/docs/pages/x/api/charts/default-charts-legend.js deleted file mode 100644 index e29bddc9b5742..0000000000000 --- a/docs/pages/x/api/charts/default-charts-legend.js +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; -import ApiPage from 'docs/src/modules/components/ApiPage'; -import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; -import jsonPageContent from './default-charts-legend.json'; - -export default function Page(props) { - const { descriptions, pageContent } = props; - return ; -} - -Page.getInitialProps = () => { - const req = require.context( - 'docsx/translations/api-docs/charts/default-charts-legend', - false, - /\.\/default-charts-legend.*.json$/, - ); - const descriptions = mapApiPageTranslations(req); - - return { - descriptions, - pageContent: jsonPageContent, - }; -}; diff --git a/docs/pages/x/api/charts/default-charts-legend.json b/docs/pages/x/api/charts/default-charts-legend.json deleted file mode 100644 index 1c55aa4c7626f..0000000000000 --- a/docs/pages/x/api/charts/default-charts-legend.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "props": { - "direction": { - "type": { "name": "enum", "description": "'column'
| 'row'" }, - "required": true - }, - "position": { - "type": { - "name": "shape", - "description": "{ horizontal: 'left'
| 'middle'
| 'right', vertical: 'bottom'
| 'middle'
| 'top' }" - }, - "required": true - }, - "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, - "hidden": { "type": { "name": "bool" }, "default": "false" }, - "itemGap": { "type": { "name": "number" }, "default": "10" }, - "itemMarkHeight": { "type": { "name": "number" }, "default": "20" }, - "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", - "description": "number
| { bottom?: number, left?: number, right?: number, top?: number }" - }, - "default": "10" - } - }, - "name": "DefaultChartsLegend", - "imports": [ - "import { DefaultChartsLegend } from '@mui/x-charts/ChartsLegend';", - "import { DefaultChartsLegend } from '@mui/x-charts';", - "import { DefaultChartsLegend } from '@mui/x-charts-pro';" - ], - "classes": [ - { - "key": "column", - "className": "MuiDefaultChartsLegend-column", - "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", - "description": "Styles applied to the series label.", - "isGlobal": false - }, - { - "key": "mark", - "className": "MuiDefaultChartsLegend-mark", - "description": "Styles applied to series mark element.", - "isGlobal": false - }, - { - "key": "root", - "className": "MuiDefaultChartsLegend-root", - "description": "Styles applied to the root element.", - "isGlobal": false - }, - { - "key": "row", - "className": "MuiDefaultChartsLegend-row", - "description": "Styles applied to the legend with row layout.", - "isGlobal": false - }, - { - "key": "series", - "className": "MuiDefaultChartsLegend-series", - "description": "Styles applied to a series element.", - "isGlobal": false - } - ], - "muiName": "MuiDefaultChartsLegend", - "filename": "/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx", - "inheritance": null, - "demos": "", - "cssComponent": false -} diff --git a/docs/pages/x/api/charts/line-chart.json b/docs/pages/x/api/charts/line-chart.json index a03a00045fa09..2fbaa24f937ff 100644 --- a/docs/pages/x/api/charts/line-chart.json +++ b/docs/pages/x/api/charts/line-chart.json @@ -136,7 +136,7 @@ { "name": "legend", "description": "Custom rendering of the legend.", - "default": "DefaultChartsLegend", + "default": "ChartsLegend", "class": null }, { @@ -168,7 +168,7 @@ ], "classes": [], "spread": true, - "themeDefaultProps": true, + "themeDefaultProps": false, "muiName": "MuiLineChart", "forwardsRefTo": "SVGSVGElement", "filename": "/packages/x-charts/src/LineChart/LineChart.tsx", diff --git a/docs/pages/x/api/charts/line-series-type.json b/docs/pages/x/api/charts/line-series-type.json index 85a484a515400..605b9697736aa 100644 --- a/docs/pages/x/api/charts/line-series-type.json +++ b/docs/pages/x/api/charts/line-series-type.json @@ -19,6 +19,7 @@ "label": { "type": { "description": "string | ((location: 'tooltip' | 'legend') => string)" } }, + "labelMarkType": { "type": { "description": "ChartsLabelMarkProps['type']" } }, "showMark": { "type": { "description": "boolean | ((params: ShowMarkParams) => boolean)" } }, "stack": { "type": { "description": "string" } }, "stackOffset": { "type": { "description": "StackOffsetType" }, "default": "'none'" }, diff --git a/docs/pages/x/api/charts/pie-chart.json b/docs/pages/x/api/charts/pie-chart.json index 090da6099a855..40c4623032918 100644 --- a/docs/pages/x/api/charts/pie-chart.json +++ b/docs/pages/x/api/charts/pie-chart.json @@ -65,7 +65,7 @@ { "name": "legend", "description": "Custom rendering of the legend.", - "default": "DefaultChartsLegend", + "default": "ChartsLegend", "class": null }, { @@ -91,7 +91,7 @@ ], "classes": [], "spread": true, - "themeDefaultProps": true, + "themeDefaultProps": false, "muiName": "MuiPieChart", "forwardsRefTo": "SVGSVGElement", "filename": "/packages/x-charts/src/PieChart/PieChart.tsx", diff --git a/docs/pages/x/api/charts/pie-series-type.json b/docs/pages/x/api/charts/pie-series-type.json index b644991437236..598ad4c27b0df 100644 --- a/docs/pages/x/api/charts/pie-series-type.json +++ b/docs/pages/x/api/charts/pie-series-type.json @@ -35,6 +35,7 @@ "highlightScope": { "type": { "description": "Partial<HighlightScope>" } }, "id": { "type": { "description": "SeriesId" } }, "innerRadius": { "type": { "description": "number | string" }, "default": "0" }, + "labelMarkType": { "type": { "description": "ChartsLabelMarkProps['type']" } }, "outerRadius": { "type": { "description": "number | string" }, "default": "'100%'" }, "paddingAngle": { "type": { "description": "number" }, "default": "0" }, "sortingValues": { "type": { "description": "ChartsPieSorting" }, "default": "'none'" }, diff --git a/docs/pages/x/api/charts/piecewise-color-legend.json b/docs/pages/x/api/charts/piecewise-color-legend.json index 18c41729f90c5..d4651729c8b1d 100644 --- a/docs/pages/x/api/charts/piecewise-color-legend.json +++ b/docs/pages/x/api/charts/piecewise-color-legend.json @@ -1,16 +1,5 @@ { "props": { - "direction": { - "type": { "name": "enum", "description": "'column'
| 'row'" }, - "required": true - }, - "position": { - "type": { - "name": "shape", - "description": "{ horizontal: 'left'
| 'middle'
| 'right', vertical: 'bottom'
| 'middle'
| 'top' }" - }, - "required": true - }, "axisDirection": { "type": { "name": "enum", "description": "'x'
| 'y'
| 'z'" }, "default": "'z'" @@ -20,11 +9,10 @@ "default": "The first axis item." }, "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } }, - "hideFirst": { "type": { "name": "bool" }, "default": "false" }, - "hideLast": { "type": { "name": "bool" }, "default": "false" }, - "itemGap": { "type": { "name": "number" }, "default": "10" }, - "itemMarkHeight": { "type": { "name": "number" }, "default": "20" }, - "itemMarkWidth": { "type": { "name": "number" }, "default": "20" }, + "direction": { + "type": { "name": "enum", "description": "'horizontal'
| 'vertical'" }, + "default": "'horizontal'" + }, "labelFormatter": { "type": { "name": "func" }, "signature": { @@ -33,21 +21,26 @@ "returned": "string | null" } }, - "labelStyle": { "type": { "name": "object" }, "default": "theme.typography.subtitle1" }, - "markGap": { "type": { "name": "number" }, "default": "5" }, + "labelPosition": { + "type": { + "name": "enum", + "description": "'start'
| 'end'
| 'extremes'" + }, + "default": "'extremes'" + }, + "markType": { + "type": { + "name": "enum", + "description": "'square'
| 'circle'
| 'line'" + }, + "default": "'square'" + }, "onItemClick": { "type": { "name": "func" }, "signature": { - "type": "function(event: React.MouseEvent, legendItem: PiecewiseColorLegendItemContext, index: number) => void", + "type": "function(event: React.MouseEvent, legendItem: PiecewiseColorLegendItemContext, index: number) => void", "describedArgs": ["event", "legendItem", "index"] } - }, - "padding": { - "type": { - "name": "union", - "description": "number
| { bottom?: number, left?: number, right?: number, top?: number }" - }, - "default": "10" } }, "name": "PiecewiseColorLegend", @@ -58,15 +51,27 @@ ], "classes": [ { - "key": "column", - "className": "MuiPiecewiseColorLegend-column", - "description": "Styles applied to the legend with column layout.", + "key": "end", + "className": "MuiPiecewiseColorLegend-end", + "description": "Styles applied to the legend with the labels after the color marks.", + "isGlobal": false + }, + { + "key": "extremes", + "className": "MuiPiecewiseColorLegend-extremes", + "description": "Styles applied to the legend with the labels on the extremes of the color marks.", + "isGlobal": false + }, + { + "key": "horizontal", + "className": "MuiPiecewiseColorLegend-horizontal", + "description": "Styles applied to the legend in row layout.", "isGlobal": false }, { - "key": "itemBackground", - "className": "MuiPiecewiseColorLegend-itemBackground", - "description": "Styles applied to the item background.", + "key": "item", + "className": "MuiPiecewiseColorLegend-item", + "description": "Styles applied to the list items.", "isGlobal": false }, { @@ -78,7 +83,19 @@ { "key": "mark", "className": "MuiPiecewiseColorLegend-mark", - "description": "Styles applied to series mark element.", + "description": "Styles applied to the marks.", + "isGlobal": false + }, + { + "key": "maxLabel", + "className": "MuiPiecewiseColorLegend-maxLabel", + "description": "Styles applied to the list item that renders the `maxLabel`.", + "isGlobal": false + }, + { + "key": "minLabel", + "className": "MuiPiecewiseColorLegend-minLabel", + "description": "Styles applied to the list item that renders the `minLabel`.", "isGlobal": false }, { @@ -88,19 +105,22 @@ "isGlobal": false }, { - "key": "row", - "className": "MuiPiecewiseColorLegend-row", - "description": "Styles applied to the legend with row layout.", + "key": "start", + "className": "MuiPiecewiseColorLegend-start", + "description": "Styles applied to the legend with the labels before the color marks.", "isGlobal": false }, { - "key": "series", - "className": "MuiPiecewiseColorLegend-series", - "description": "Styles applied to a series element.", + "key": "vertical", + "className": "MuiPiecewiseColorLegend-vertical", + "description": "Styles applied to the legend in column layout.", "isGlobal": false } ], + "spread": true, + "themeDefaultProps": true, "muiName": "MuiPiecewiseColorLegend", + "forwardsRefTo": "HTMLUListElement", "filename": "/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/x/api/charts/scatter-chart.json b/docs/pages/x/api/charts/scatter-chart.json index 9826eff4a7fea..0c6730fcbd93e 100644 --- a/docs/pages/x/api/charts/scatter-chart.json +++ b/docs/pages/x/api/charts/scatter-chart.json @@ -133,7 +133,7 @@ { "name": "legend", "description": "Custom rendering of the legend.", - "default": "DefaultChartsLegend", + "default": "ChartsLegend", "class": null }, { @@ -158,7 +158,7 @@ ], "classes": [], "spread": true, - "themeDefaultProps": true, + "themeDefaultProps": false, "muiName": "MuiScatterChart", "forwardsRefTo": "SVGSVGElement", "filename": "/packages/x-charts/src/ScatterChart/ScatterChart.tsx", diff --git a/docs/pages/x/api/charts/scatter-series-type.json b/docs/pages/x/api/charts/scatter-series-type.json index 436b9311ec3f8..8fd3e00be1694 100644 --- a/docs/pages/x/api/charts/scatter-series-type.json +++ b/docs/pages/x/api/charts/scatter-series-type.json @@ -19,6 +19,7 @@ "label": { "type": { "description": "string | ((location: 'tooltip' | 'legend') => string)" } }, + "labelMarkType": { "type": { "description": "ChartsLabelMarkProps['type']" } }, "markerSize": { "type": { "description": "number" } }, "valueFormatter": { "type": { "description": "SeriesValueFormatter<TValue>" } }, "xAxisId": { "type": { "description": "string" } }, diff --git a/docs/translations/api-docs/charts/bar-series-type.json b/docs/translations/api-docs/charts/bar-series-type.json index ebb278d10e61c..d84b4ba782c5a 100644 --- a/docs/translations/api-docs/charts/bar-series-type.json +++ b/docs/translations/api-docs/charts/bar-series-type.json @@ -10,6 +10,9 @@ "label": { "description": "The label to display on the tooltip or the legend. It can be a string or a function." }, + "labelMarkType": { + "description": "Defines the mark type for the series.

There is a default mark type for each series type.

It allows custom values which will be passed to the mark component if it was customized." + }, "layout": { "description": "Layout of the bars. All bar should have the same layout." }, "stack": { "description": "The key that identifies the stacking group.
Series with the same stack property will be stacked together." 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 697f19e692d3b..b558ebc1c7936 100644 --- a/docs/translations/api-docs/charts/charts-legend/charts-legend.json +++ b/docs/translations/api-docs/charts/charts-legend/charts-legend.json @@ -5,12 +5,6 @@ "direction": { "description": "The direction of the legend layout. The default depends on the chart." }, - "hidden": { "description": "Set to true to hide the legend." }, - "itemGap": { "description": "Space between two legend items (in px)." }, - "itemMarkHeight": { "description": "Height of the item mark (in px)." }, - "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": { @@ -19,30 +13,22 @@ "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." - }, - "position": { "description": "The position of the legend." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, "classDescriptions": { - "column": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the legend with column layout" - }, - "itemBackground": { + "horizontal": { "description": "Styles applied to {{nodeName}}.", - "nodeName": "the item background" + "nodeName": "the legend in row layout" }, "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." }, - "row": { + "series": { "description": "Styles applied to {{nodeName}}.", "nodeName": "a series element" }, + "vertical": { "description": "Styles applied to {{nodeName}}.", - "nodeName": "the legend with row layout" - }, - "series": { "description": "Styles applied to {{nodeName}}.", "nodeName": "a series element" } + "nodeName": "the legend in column layout" + } }, "slotDescriptions": { "legend": "Custom rendering of the legend." } } diff --git a/docs/translations/api-docs/charts/continuous-color-legend/continuous-color-legend.json b/docs/translations/api-docs/charts/continuous-color-legend/continuous-color-legend.json index 50156c88b2edb..9505bcab4804d 100644 --- a/docs/translations/api-docs/charts/continuous-color-legend/continuous-color-legend.json +++ b/docs/translations/api-docs/charts/continuous-color-legend/continuous-color-legend.json @@ -1,31 +1,64 @@ { "componentDescription": "", "propDescriptions": { - "align": { "description": "The alignment of the texts with the gradient bar." }, "axisDirection": { "description": "The axis direction containing the color configuration to represent." }, "axisId": { "description": "The id of the axis item with the color configuration to represent." }, - "direction": { - "description": "The direction of the legend layout. The default depends on the chart." - }, - "id": { "description": "A unique identifier for the gradient." }, - "labelStyle": { "description": "The style applied to labels." }, - "length": { - "description": "The length of the gradient bar. Can be a number (in px) or a string with a percentage such as '50%'. The '100%' is the length of the svg." + "classes": { "description": "Override or extend the styles applied to the component." }, + "direction": { "description": "The direction of the legend layout." }, + "gradientId": { + "description": "The id for the gradient to use. If not provided, it will use the generated gradient from the axis configuration. The gradientId will be used as fill="url(#gradientId)"." }, + "labelPosition": { "description": "Where to position the labels relative to the gradient." }, "maxLabel": { "description": "The label to display at the maximum side of the gradient. Can either be a string, or a function. If not defined, the formatted maximal value is display." }, "minLabel": { "description": "The label to display at the minimum side of the gradient. Can either be a string, or a function." }, - "position": { "description": "The position of the legend." }, - "scaleType": { "description": "The scale used to display gradient colors." }, - "spacing": { "description": "The space between the gradient bar and the labels." }, - "thickness": { "description": "The thickness of the gradient bar." } + "reverse": { "description": "If true, the gradient and labels will be reversed." }, + "rotateGradient": { + "description": "If provided, the gradient will be rotated by 90deg. Useful for linear gradients that are not in the correct orientation." + }, + "thickness": { "description": "The thickness of the gradient" } }, - "classDescriptions": {} + "classDescriptions": { + "end": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the legend with the labels after the gradient" + }, + "extremes": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the legend with the labels on the extremes of the gradient" + }, + "gradient": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the list item with the gradient" + }, + "horizontal": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the legend in row layout" + }, + "label": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the series label" }, + "maxLabel": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the list item that renders the maxLabel" + }, + "minLabel": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the list item that renders the minLabel" + }, + "root": { "description": "Styles applied to the root element." }, + "start": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the legend with the labels before the gradient" + }, + "vertical": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the legend in column layout" + } + } } 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 deleted file mode 100644 index eea8fa28c58f3..0000000000000 --- a/docs/translations/api-docs/charts/default-charts-legend/default-charts-legend.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "componentDescription": "", - "propDescriptions": { - "classes": { "description": "Override or extend the styles applied to the component." }, - "direction": { - "description": "The direction of the legend layout. The default depends on the chart." - }, - "hidden": { "description": "Set to true to hide the legend." }, - "itemGap": { "description": "Space between two legend items (in px)." }, - "itemMarkHeight": { "description": "Height of the item mark (in px)." }, - "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." - }, - "position": { "description": "The position of the legend." } - }, - "classDescriptions": { - "column": { - "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." }, - "row": { - "description": "Styles applied to {{nodeName}}.", - "nodeName": "the legend with row layout" - }, - "series": { "description": "Styles applied to {{nodeName}}.", "nodeName": "a series element" } - } -} diff --git a/docs/translations/api-docs/charts/line-series-type.json b/docs/translations/api-docs/charts/line-series-type.json index 62a540a7437ad..7855b3617b0ab 100644 --- a/docs/translations/api-docs/charts/line-series-type.json +++ b/docs/translations/api-docs/charts/line-series-type.json @@ -23,6 +23,9 @@ "label": { "description": "The label to display on the tooltip or the legend. It can be a string or a function." }, + "labelMarkType": { + "description": "Defines the mark type for the series.

There is a default mark type for each series type.

It allows custom values which will be passed to the mark component if it was customized." + }, "showMark": { "description": "Define which items of the series should display a mark.
If can be a boolean that applies to all items.
Or a callback that gets some item properties and returns true if the item should be displayed." }, diff --git a/docs/translations/api-docs/charts/pie-series-type.json b/docs/translations/api-docs/charts/pie-series-type.json index ecf2084ab1bc9..6624fb549e8de 100644 --- a/docs/translations/api-docs/charts/pie-series-type.json +++ b/docs/translations/api-docs/charts/pie-series-type.json @@ -26,6 +26,9 @@ "innerRadius": { "description": "The radius between circle center and the beginning of the arc.
Can be a number (in px) or a string with a percentage such as '50%'.
The '100%' is the maximal radius that fit into the drawing area." }, + "labelMarkType": { + "description": "Defines the mark type for the series.

There is a default mark type for each series type.

It allows custom values which will be passed to the mark component if it was customized." + }, "outerRadius": { "description": "The radius between circle center and the end of the arc.
Can be a number (in px) or a string with a percentage such as '50%'.
The '100%' is the maximal radius that fit into the drawing area." }, 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 eb233fb379f1f..0e5ee41db27e6 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 @@ -8,27 +8,16 @@ "description": "The id of the axis item with the color configuration to represent." }, "classes": { "description": "Override or extend the styles applied to the component." }, - "direction": { - "description": "The direction of the legend layout. The default depends on the chart." - }, - "hideFirst": { - "description": "Hide the first item of the legend, corresponding to the [-infinity, min] piece." - }, - "hideLast": { - "description": "Hide the last item of the legend, corresponding to the [max, +infinity] piece." - }, - "itemGap": { "description": "Space between two legend items (in px)." }, - "itemMarkHeight": { "description": "Height of the item mark (in px)." }, - "itemMarkWidth": { "description": "Width of the item mark (in px)." }, + "direction": { "description": "The direction of the legend layout." }, "labelFormatter": { "description": "Format the legend labels.", "typeDescriptions": { "params": "The bound of the piece to format.", - "string | null": "The displayed label, or null to skip the item." + "string | null": "The displayed label, '' to skip the label but show the color mark, or null to skip it entirely." } }, - "labelStyle": { "description": "Style applied to legend labels." }, - "markGap": { "description": "Space between the mark and the label (in px)." }, + "labelPosition": { "description": "Where to position the labels relative to the gradient." }, + "markType": { "description": "The type of the mark." }, "onItemClick": { "description": "Callback fired when a legend item is clicked.", "typeDescriptions": { @@ -36,28 +25,40 @@ "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." - }, - "position": { "description": "The position of the legend." } + } }, "classDescriptions": { - "column": { + "end": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the legend with the labels after the color marks" + }, + "extremes": { "description": "Styles applied to {{nodeName}}.", - "nodeName": "the legend with column layout" + "nodeName": "the legend with the labels on the extremes of the color marks" }, - "itemBackground": { + "horizontal": { "description": "Styles applied to {{nodeName}}.", - "nodeName": "the item background" + "nodeName": "the legend in row layout" }, + "item": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the list items" }, "label": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the series label" }, - "mark": { "description": "Styles applied to {{nodeName}}.", "nodeName": "series mark element" }, + "mark": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the marks" }, + "maxLabel": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the list item that renders the maxLabel" + }, + "minLabel": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the list item that renders the minLabel" + }, "root": { "description": "Styles applied to the root element." }, - "row": { + "start": { "description": "Styles applied to {{nodeName}}.", - "nodeName": "the legend with row layout" + "nodeName": "the legend with the labels before the color marks" }, - "series": { "description": "Styles applied to {{nodeName}}.", "nodeName": "a series element" } + "vertical": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the legend in column layout" + } } } diff --git a/docs/translations/api-docs/charts/scatter-series-type.json b/docs/translations/api-docs/charts/scatter-series-type.json index 58a593f0bdbf6..8a20f810ad506 100644 --- a/docs/translations/api-docs/charts/scatter-series-type.json +++ b/docs/translations/api-docs/charts/scatter-series-type.json @@ -15,6 +15,9 @@ "label": { "description": "The label to display on the tooltip or the legend. It can be a string or a function." }, + "labelMarkType": { + "description": "Defines the mark type for the series.

There is a default mark type for each series type.

It allows custom values which will be passed to the mark component if it was customized." + }, "markerSize": { "description": "" }, "valueFormatter": { "description": "Formatter used to render values in tooltip or other data display." diff --git a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx index e05075227db07..75f4dc6e8a34a 100644 --- a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx +++ b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx @@ -11,11 +11,13 @@ import { ChartsLegend } from '@mui/x-charts/ChartsLegend'; import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight'; import { ChartsTooltip } from '@mui/x-charts/ChartsTooltip'; import { ChartsClipPath } from '@mui/x-charts/ChartsClipPath'; -import { useBarChartProps } from '@mui/x-charts/internals'; -import { ChartContainerPro } from '../ChartContainerPro'; +import { useBarChartProps, ChartsWrapper } from '@mui/x-charts/internals'; +import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup'; import { useZoom } from '../context/ZoomProvider/useZoom'; import { ZoomProps } from '../context/ZoomProvider'; +import { useChartContainerProProps } from '../ChartContainerPro/useChartContainerProProps'; +import { ChartDataProviderPro } from '../context/ChartDataProviderPro'; function BarChartPlotZoom(props: BarPlotProps) { const { isInteracting } = useZoom(); @@ -83,6 +85,7 @@ const BarChartPro = React.forwardRef(function BarChartPro( const props = useThemeProps({ props: inProps, name: 'MuiBarChartPro' }); const { zoom, onZoomChange, ...other } = props; const { + chartsWrapperProps, chartContainerProps, barPlotProps, axisClickHandlerProps, @@ -95,25 +98,33 @@ const BarChartPro = React.forwardRef(function BarChartPro( legendProps, children, } = useBarChartProps(other); + const { chartDataProviderProProps, chartsSurfaceProps } = useChartContainerProProps( + chartContainerProps, + ref, + ); const Tooltip = props.slots?.tooltip ?? ChartsTooltip; return ( - - {props.onAxisClick && } - - - - - - - - {!props.hideLegend && } - {!props.loading && } - - - {children} - + + + {!props.hideLegend && } + + {props.onAxisClick && } + + + + + + + + {!props.loading && } + + + {children} + + + ); }); diff --git a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx index ff78f27a7b710..b56f0cc3ff725 100644 --- a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx +++ b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx @@ -20,11 +20,13 @@ import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight'; import { ChartsLegend } from '@mui/x-charts/ChartsLegend'; import { ChartsTooltip } from '@mui/x-charts/ChartsTooltip'; import { ChartsClipPath } from '@mui/x-charts/ChartsClipPath'; -import { useLineChartProps } from '@mui/x-charts/internals'; -import { ChartContainerPro } from '../ChartContainerPro'; +import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; +import { useLineChartProps, ChartsWrapper } from '@mui/x-charts/internals'; import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup'; import { useZoom } from '../context/ZoomProvider/useZoom'; import { ZoomProps } from '../context/ZoomProvider'; +import { ChartDataProviderPro } from '../context/ChartDataProviderPro'; +import { useChartContainerProProps } from '../ChartContainerPro/useChartContainerProProps'; function AreaPlotZoom(props: AreaPlotProps) { const { isInteracting } = useZoom(); @@ -150,6 +152,7 @@ const LineChartPro = React.forwardRef(function LineChartPro( const props = useThemeProps({ props: inProps, name: 'MuiLineChartPro' }); const { zoom, onZoomChange, ...other } = props; const { + chartsWrapperProps, chartContainerProps, axisClickHandlerProps, gridProps, @@ -165,31 +168,39 @@ const LineChartPro = React.forwardRef(function LineChartPro( legendProps, children, } = useLineChartProps(other); + const { chartDataProviderProProps, chartsSurfaceProps } = useChartContainerProProps( + chartContainerProps, + ref, + ); const Tooltip = props.slots?.tooltip ?? ChartsTooltip; return ( - - {props.onAxisClick && } - - - - - - - - - - {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} - - - - {!props.hideLegend && } - {!props.loading && } - - - {children} - + + + {!props.hideLegend && } + + {props.onAxisClick && } + + + + + + + + + + {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} + + + + {!props.loading && } + + + {children} + + + ); }); diff --git a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx index 5ec98b8eb79fe..f189dbffa3586 100644 --- a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx +++ b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx @@ -9,12 +9,14 @@ import { ChartsVoronoiHandler } from '@mui/x-charts/ChartsVoronoiHandler'; import { ChartsAxis } from '@mui/x-charts/ChartsAxis'; import { ChartsGrid } from '@mui/x-charts/ChartsGrid'; import { ChartsLegend } from '@mui/x-charts/ChartsLegend'; +import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight'; import { ChartsTooltip } from '@mui/x-charts/ChartsTooltip'; -import { useScatterChartProps } from '@mui/x-charts/internals'; -import { ChartContainerPro } from '../ChartContainerPro'; +import { useScatterChartProps, ChartsWrapper } from '@mui/x-charts/internals'; import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup'; import { ZoomProps } from '../context/ZoomProvider'; +import { ChartDataProviderPro } from '../context/ChartDataProviderPro'; +import { useChartContainerProProps } from '../ChartContainerPro/useChartContainerProProps'; export interface ScatterChartProProps extends ScatterChartProps, ZoomProps {} @@ -35,6 +37,7 @@ const ScatterChartPro = React.forwardRef(function ScatterChartPro( const props = useThemeProps({ props: inProps, name: 'MuiScatterChartPro' }); const { zoom, onZoomChange, ...other } = props; const { + chartsWrapperProps, chartContainerProps, zAxisProps, voronoiHandlerProps, @@ -46,27 +49,35 @@ const ScatterChartPro = React.forwardRef(function ScatterChartPro( axisHighlightProps, children, } = useScatterChartProps(other); + const { chartDataProviderProProps, chartsSurfaceProps } = useChartContainerProProps( + chartContainerProps, + ref, + ); const Tooltip = props.slots?.tooltip ?? ChartsTooltip; return ( - - - {!props.disableVoronoi && } - - - - {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} - - - + + {!props.hideLegend && } - - {!props.loading && } - - {children} - - + + + {!props.disableVoronoi && } + + + + {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} + + + + + {!props.loading && } + + {children} + + + + ); }); diff --git a/packages/x-charts/src/BarChart/BarChart.test.tsx b/packages/x-charts/src/BarChart/BarChart.test.tsx index f93e1352ca788..8f598d3d21d89 100644 --- a/packages/x-charts/src/BarChart/BarChart.test.tsx +++ b/packages/x-charts/src/BarChart/BarChart.test.tsx @@ -24,6 +24,7 @@ describe('', () => { 'themeStyleOverrides', 'themeVariants', 'themeCustomPalette', + 'themeDefaultProps', ], }), ); diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx index 5722559367505..57a0504989463 100644 --- a/packages/x-charts/src/BarChart/BarChart.tsx +++ b/packages/x-charts/src/BarChart/BarChart.tsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import { useThemeProps } from '@mui/material/styles'; import { MakeOptional } from '@mui/x-internals/types'; import { BarPlot, BarPlotProps, BarPlotSlotProps, BarPlotSlots } from './BarPlot'; -import { ChartContainer, ChartContainerProps } from '../ChartContainer'; +import { ChartContainerProps } from '../ChartContainer'; import { ChartsAxis, ChartsAxisProps } from '../ChartsAxis'; import { BarSeriesType } from '../models/seriesType/bar'; import { ChartsTooltip } from '../ChartsTooltip'; @@ -25,6 +25,10 @@ import { ChartsOverlaySlots, } from '../ChartsOverlay/ChartsOverlay'; import { useBarChartProps } from './useBarChartProps'; +import { ChartDataProvider } from '../context'; +import { ChartsSurface } from '../ChartsSurface'; +import { useChartContainerProps } from '../ChartContainer/useChartContainerProps'; +import { ChartsWrapper } from '../internals/components/ChartsWrapper'; export interface BarChartSlots extends ChartsAxisSlots, @@ -100,6 +104,7 @@ const BarChart = React.forwardRef(function BarChart( ) { const props = useThemeProps({ props: inProps, name: 'MuiBarChart' }); const { + chartsWrapperProps, chartContainerProps, barPlotProps, axisClickHandlerProps, @@ -112,24 +117,32 @@ const BarChart = React.forwardRef(function BarChart( legendProps, children, } = useBarChartProps(props); + const { chartDataProviderProps, chartsSurfaceProps } = useChartContainerProps( + chartContainerProps, + ref, + ); const Tooltip = props.slots?.tooltip ?? ChartsTooltip; return ( - - {props.onAxisClick && } - - - - - - - - {!props.hideLegend && } - {!props.loading && } - - {children} - + + + {!props.hideLegend && } + + {props.onAxisClick && } + + + + + + + + {!props.loading && } + + {children} + + + ); }); diff --git a/packages/x-charts/src/BarChart/legend.ts b/packages/x-charts/src/BarChart/legend.ts index e2ee7ae8c7e92..254e34c6a1170 100644 --- a/packages/x-charts/src/BarChart/legend.ts +++ b/packages/x-charts/src/BarChart/legend.ts @@ -1,4 +1,4 @@ -import { LegendItemParams } from '../ChartsLegend/chartsLegend.types'; +import type { LegendItemParams } from '../ChartsLegend'; import { LegendGetter } from '../context/PluginProvider'; import { getLabel } from '../internals/getLabel'; @@ -12,6 +12,7 @@ const legendGetter: LegendGetter<'bar'> = (params) => { } acc.push({ + markType: series[seriesId].labelMarkType ?? 'square', id: seriesId, seriesId, color: series[seriesId].color, diff --git a/packages/x-charts/src/BarChart/useBarChartProps.ts b/packages/x-charts/src/BarChart/useBarChartProps.ts index ab65c93ad40e0..a1194f7249804 100644 --- a/packages/x-charts/src/BarChart/useBarChartProps.ts +++ b/packages/x-charts/src/BarChart/useBarChartProps.ts @@ -10,7 +10,9 @@ import { ChartsClipPathProps } from '../ChartsClipPath'; import { ChartsOverlayProps } from '../ChartsOverlay'; import { ChartsAxisProps } from '../ChartsAxis'; import { ChartsAxisHighlightProps } from '../ChartsAxisHighlight'; -import { ChartsLegendProps } from '../ChartsLegend'; +import { ChartsLegendSlotExtension } from '../ChartsLegend'; +import type { ChartsWrapperProps } from '../internals/components/ChartsWrapper'; +import { calculateMargins } from '../internals/calculateMargins'; /** * A helper function that extracts BarChartProps from the input props @@ -77,7 +79,7 @@ export const useBarChartProps = (props: BarChartProps) => { })), width, height, - margin, + margin: calculateMargins({ margin, hideLegend, slotProps, series }), colors, dataset, xAxis: @@ -86,7 +88,6 @@ export const useBarChartProps = (props: BarChartProps) => { yAxis: yAxis ?? (hasHorizontalSeries ? [{ id: DEFAULT_Y_AXIS_KEY, ...defaultAxisConfig }] : undefined), - sx, highlightedItem, onHighlightChange, disableAxisListener: @@ -143,12 +144,19 @@ export const useBarChartProps = (props: BarChartProps) => { ...axisHighlight, }; - const legendProps: ChartsLegendProps = { + const legendProps: ChartsLegendSlotExtension = { slots, slotProps, }; + const chartsWrapperProps: Omit = { + sx, + legendPosition: props.slotProps?.legend?.position, + legendDirection: props.slotProps?.legend?.direction, + }; + return { + chartsWrapperProps, chartContainerProps, barPlotProps, axisClickHandlerProps, diff --git a/packages/x-charts/src/ChartsLabel/ChartsLabel.test.tsx b/packages/x-charts/src/ChartsLabel/ChartsLabel.test.tsx index ab8d649ac28fa..50692b9412d23 100644 --- a/packages/x-charts/src/ChartsLabel/ChartsLabel.test.tsx +++ b/packages/x-charts/src/ChartsLabel/ChartsLabel.test.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { createRenderer } from '@mui/internal-test-utils/createRenderer'; import { describeConformance } from 'test/utils/describeConformance'; import { ChartsLabel } from '@mui/x-charts/ChartsLabel/ChartsLabel'; -import { labelClasses } from '@mui/x-charts/ChartsLabel/labelClasses'; +import { labelClasses } from '@mui/x-charts/ChartsLabel'; import { createTheme, ThemeProvider } from '@mui/material/styles'; describe('', () => { @@ -18,6 +18,13 @@ describe('', () => { ThemeProvider, createTheme, // SKIP - skip: ['themeVariants', 'componentProp', 'componentsProp'], + skip: [ + 'themeVariants', + 'themeStyleOverrides', + 'themeCustomPalette', + 'themeDefaultProps', + 'componentProp', + 'componentsProp', + ], })); }); diff --git a/packages/x-charts/src/ChartsLabel/ChartsLabel.tsx b/packages/x-charts/src/ChartsLabel/ChartsLabel.tsx index 92989a60b2027..e133765cebb3c 100644 --- a/packages/x-charts/src/ChartsLabel/ChartsLabel.tsx +++ b/packages/x-charts/src/ChartsLabel/ChartsLabel.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { styled, SxProps, Theme } from '@mui/material/styles'; +import { SxProps, Theme } from '@mui/material/styles'; import clsx from 'clsx'; import { ChartsLabelClasses, useUtilityClasses } from './labelClasses'; import { consumeThemeProps } from '../internals/consumeThemeProps'; @@ -16,21 +16,9 @@ export interface ChartsLabelProps { sx?: SxProps; } -const Root = styled('span', { - name: 'MuiChartsLabel', - slot: 'Root', - overridesResolver: (props, styles) => styles.root, -})<{ ownerState: ChartsLabelProps }>(({ theme }) => ({ - ...theme.typography.caption, - color: (theme.vars || theme).palette.text.primary, - lineHeight: undefined, - display: 'flex', -})); - /** - * @ignore - internal component. - * * Generates the label mark for the tooltip and legend. + * @ignore - internal component. */ const ChartsLabel = consumeThemeProps( 'MuiChartsLabel', @@ -41,9 +29,9 @@ const ChartsLabel = consumeThemeProps( const { children, className, classes, ...other } = props; return ( - + {children} - + ); }, ); diff --git a/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.test.tsx b/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.test.tsx index 637a0b5763754..9e5ae7a6d0491 100644 --- a/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.test.tsx +++ b/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.test.tsx @@ -3,7 +3,7 @@ import { createRenderer } from '@mui/internal-test-utils/createRenderer'; import { describeConformance } from 'test/utils/describeConformance'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { ChartsLabelGradient } from '@mui/x-charts/ChartsLabel/ChartsLabelGradient'; -import { labelGradientClasses } from '@mui/x-charts/ChartsLabel/labelGradientClasses'; +import { labelGradientClasses } from '@mui/x-charts/ChartsLabel'; describe('', () => { const { render } = createRenderer(); diff --git a/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.tsx b/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.tsx index 65975624744f2..c842ab1ad7d8e 100644 --- a/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.tsx +++ b/packages/x-charts/src/ChartsLabel/ChartsLabelGradient.tsx @@ -13,26 +13,28 @@ import { consumeThemeProps } from '../internals/consumeThemeProps'; export interface ChartsLabelGradientProps { /** * A unique identifier for the gradient. - * * The `gradientId` will be used as `fill="url(#gradientId)"`. */ gradientId: string; /** * The direction of the gradient. - * - * @default 'row' + * @default 'horizontal' */ - direction?: 'column' | 'row'; + direction?: 'vertical' | 'horizontal'; /** * If `true`, the gradient will be reversed. */ reverse?: boolean; /** * If provided, the gradient will be rotated by 90deg. - * * Useful for linear gradients that are not in the correct orientation. */ rotate?: boolean; + /** + * The thickness of the gradient + * @default 12 + */ + thickness?: number; /** * Override or extend the styles applied to the component. */ @@ -41,20 +43,24 @@ export interface ChartsLabelGradientProps { sx?: SxProps; } -const getRotation = (direction?: 'column' | 'row', reverse?: boolean, rotate?: boolean) => { +const getRotation = ( + direction?: 'vertical' | 'horizontal', + reverse?: boolean, + rotate?: boolean, +) => { if (!rotate && reverse) { - return direction === 'column' ? 90 : 180; + return direction === 'vertical' ? 90 : 180; } if (rotate && !reverse) { - return direction === 'column' ? 0 : 90; + return direction === 'vertical' ? 0 : 90; } if (rotate && reverse) { - return direction === 'column' ? 180 : -90; + return direction === 'vertical' ? 180 : -90; } - return direction === 'column' ? -90 : 0; + return direction === 'vertical' ? -90 : 0; }; const Root = styled('div', { @@ -72,17 +78,17 @@ const Root = styled('div', { borderRadius: 2, overflow: 'hidden', }, - [`&.${labelGradientClasses.row}`]: { + [`&.${labelGradientClasses.horizontal}`]: { width: '100%', [`.${labelGradientClasses.mask}`]: { - height: 12, + height: ownerState.thickness, width: '100%', }, }, - [`&.${labelGradientClasses.column}`]: { + [`&.${labelGradientClasses.vertical}`]: { height: '100%', [`.${labelGradientClasses.mask}`]: { - width: 12, + width: ownerState.thickness, height: '100%', '> svg': { height: '100%', @@ -97,20 +103,21 @@ const Root = styled('div', { }); /** - * @ignore - internal component. - * * Generates the label Gradient for the tooltip and legend. + * @ignore - internal component. */ const ChartsLabelGradient = consumeThemeProps( 'MuiChartsLabelGradient', { defaultProps: { - direction: 'row', + direction: 'horizontal', + thickness: 12, }, classesResolver: useUtilityClasses, }, function ChartsLabelGradient(props: ChartsLabelGradientProps, ref: React.Ref) { - const { gradientId, direction, classes, className, ...other } = props; + const { gradientId, direction, classes, className, rotate, reverse, thickness, ...other } = + props; return (
- +
@@ -141,13 +148,11 @@ ChartsLabelGradient.propTypes = { classes: PropTypes.object, /** * The direction of the gradient. - * - * @default 'row' + * @default 'horizontal' */ - direction: PropTypes.oneOf(['column', 'row']), + direction: PropTypes.oneOf(['vertical', 'horizontal']), /** * A unique identifier for the gradient. - * * The `gradientId` will be used as `fill="url(#gradientId)"`. */ gradientId: PropTypes.string.isRequired, @@ -157,10 +162,14 @@ ChartsLabelGradient.propTypes = { reverse: PropTypes.bool, /** * If provided, the gradient will be rotated by 90deg. - * * Useful for linear gradients that are not in the correct orientation. */ rotate: PropTypes.bool, + /** + * The thickness of the gradient + * @default 12 + */ + thickness: PropTypes.number, } as any; export { ChartsLabelGradient }; diff --git a/packages/x-charts/src/ChartsLabel/ChartsLabelMark.test.tsx b/packages/x-charts/src/ChartsLabel/ChartsLabelMark.test.tsx index 25d954881b7f6..66fa1d0ee9d0c 100644 --- a/packages/x-charts/src/ChartsLabel/ChartsLabelMark.test.tsx +++ b/packages/x-charts/src/ChartsLabel/ChartsLabelMark.test.tsx @@ -3,7 +3,7 @@ import { createRenderer } from '@mui/internal-test-utils/createRenderer'; import { describeConformance } from 'test/utils/describeConformance'; import { createTheme, ThemeProvider } from '@mui/material/styles'; import { ChartsLabelMark } from '@mui/x-charts/ChartsLabel/ChartsLabelMark'; -import { labelMarkClasses } from '@mui/x-charts/ChartsLabel/labelMarkClasses'; +import { labelMarkClasses } from '@mui/x-charts/ChartsLabel'; describe('', () => { const { render } = createRenderer(); diff --git a/packages/x-charts/src/ChartsLabel/ChartsLabelMark.tsx b/packages/x-charts/src/ChartsLabel/ChartsLabelMark.tsx index f8d6ad54dc04e..fbe440fc35cc1 100644 --- a/packages/x-charts/src/ChartsLabel/ChartsLabelMark.tsx +++ b/packages/x-charts/src/ChartsLabel/ChartsLabelMark.tsx @@ -66,13 +66,13 @@ const Root = styled('div', { }); /** - * @ignore - internal component. - * * Generates the label mark for the tooltip and legend. + * @ignore - internal component. */ const ChartsLabelMark = consumeThemeProps( 'MuiChartsLabelMark', { + defaultProps: { type: 'square' }, classesResolver: useUtilityClasses, }, function ChartsLabelMark(props: ChartsLabelMarkProps, ref: React.Ref) { @@ -88,7 +88,7 @@ const ChartsLabelMark = consumeThemeProps( >
- +
diff --git a/packages/x-charts/src/ChartsLabel/index.ts b/packages/x-charts/src/ChartsLabel/index.ts new file mode 100644 index 0000000000000..e50d5aba4022a --- /dev/null +++ b/packages/x-charts/src/ChartsLabel/index.ts @@ -0,0 +1,9 @@ +// export * from './ChartsLabel'; +export type { ChartsLabelMarkProps } from './ChartsLabelMark'; +// export * from './ChartsLabelGradient'; +export { labelClasses } from './labelClasses'; +export type { ChartsLabelClasses } from './labelClasses'; +export { labelMarkClasses } from './labelMarkClasses'; +export type { ChartsLabelMarkClasses } from './labelMarkClasses'; +export { labelGradientClasses } from './labelGradientClasses'; +export type { ChartsLabelGradientClasses } from './labelGradientClasses'; diff --git a/packages/x-charts/src/ChartsLabel/labelClasses.ts b/packages/x-charts/src/ChartsLabel/labelClasses.ts index 236d118902902..b0b89d5d69efe 100644 --- a/packages/x-charts/src/ChartsLabel/labelClasses.ts +++ b/packages/x-charts/src/ChartsLabel/labelClasses.ts @@ -8,8 +8,6 @@ export interface ChartsLabelClasses { root: string; } -export type ChartsLabelClassKey = keyof ChartsLabelClasses; - export function getLabelUtilityClass(slot: string) { return generateUtilityClass('MuiChartsLabel', slot); } diff --git a/packages/x-charts/src/ChartsLabel/labelGradientClasses.tsx b/packages/x-charts/src/ChartsLabel/labelGradientClasses.tsx index 808c653434bcc..03031ee6b7eca 100644 --- a/packages/x-charts/src/ChartsLabel/labelGradientClasses.tsx +++ b/packages/x-charts/src/ChartsLabel/labelGradientClasses.tsx @@ -9,20 +9,20 @@ export interface ChartsLabelGradientClasses { /** Styles applied to the "mask" that gives shape to the gradient. */ mask: string; /** Styles applied when direction is "column". */ - column: string; + vertical: string; /** Styles applied when direction is "row". */ - row: string; + horizontal: string; + /** Styles applied to the element filled by the gradient */ + fill: string; } -export type ChartsLabelGradientClassKey = keyof ChartsLabelGradientClasses; - export function getLabelGradientUtilityClass(slot: string) { return generateUtilityClass('MuiChartsLabelGradient', slot); } export const labelGradientClasses: ChartsLabelGradientClasses = generateUtilityClasses( 'MuiChartsLabelGradient', - ['root', 'column', 'row', 'mask'], + ['root', 'vertical', 'horizontal', 'mask', 'fill'], ); export const useUtilityClasses = (props: ChartsLabelGradientProps) => { @@ -31,6 +31,7 @@ export const useUtilityClasses = (props: ChartsLabelGradientProps) => { const slots = { root: ['root', direction], mask: ['mask'], + fill: ['fill'], }; return composeClasses(slots, getLabelGradientUtilityClass, props.classes); diff --git a/packages/x-charts/src/ChartsLabel/labelMarkClasses.ts b/packages/x-charts/src/ChartsLabel/labelMarkClasses.ts index 85004c38725a6..e0f54754d2e00 100644 --- a/packages/x-charts/src/ChartsLabel/labelMarkClasses.ts +++ b/packages/x-charts/src/ChartsLabel/labelMarkClasses.ts @@ -14,17 +14,17 @@ export interface ChartsLabelMarkClasses { square: string; /** Styles applied to the mark type "circle". */ circle: string; + /** Styles applied to the element with fill={color} attribute. */ + fill: string; } -export type ChartsLabelMarkClassKey = keyof ChartsLabelMarkClasses; - export function getLabelMarkUtilityClass(slot: string) { return generateUtilityClass('MuiChartsLabelMark', slot); } export const labelMarkClasses: ChartsLabelMarkClasses = generateUtilityClasses( 'MuiChartsLabelMark', - ['root', 'line', 'square', 'circle', 'mask'], + ['root', 'line', 'square', 'circle', 'mask', 'fill'], ); export const useUtilityClasses = (props: ChartsLabelMarkProps) => { @@ -32,6 +32,7 @@ export const useUtilityClasses = (props: ChartsLabelMarkProps) => { const slots = { root: ['root', type], mask: ['mask'], + fill: ['fill'], }; return composeClasses(slots, getLabelMarkUtilityClass, props.classes); diff --git a/packages/x-charts/src/ChartsLegend/ChartsLegend.test.tsx b/packages/x-charts/src/ChartsLegend/ChartsLegend.test.tsx new file mode 100644 index 0000000000000..b943131348948 --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/ChartsLegend.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { createRenderer, describeConformance } from '@mui/internal-test-utils'; +import { ChartsLegend, legendClasses } from '@mui/x-charts/ChartsLegend'; +import { ChartDataProvider } from '@mui/x-charts/context'; +import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + classes: legendClasses, + inheritComponent: 'ul', + render: (node) => + render(node, { + wrapper: ({ children }) => ( + + {/* Has to be first as describeConformance picks the "first child" */} + {/* https://github.com/mui/material-ui/blob/c0620e333641deda56f3cd68c7c3736098ee818c/packages-internal/test-utils/src/describeConformance.tsx#L257 */} + {children} + + + ), + }), + muiName: 'MuiChartsLegend', + testComponentPropWith: 'ul', + refInstanceof: window.HTMLUListElement, + ThemeProvider, + createTheme, + // SKIP + skip: ['themeVariants', 'componentProp', 'componentsProp'], + })); +}); diff --git a/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx b/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx index 1a68804b785fe..a6c4c0a24267e 100644 --- a/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/ChartsLegend.tsx @@ -1,97 +1,136 @@ 'use client'; import * as React from 'react'; +import { styled, SxProps, Theme } from '@mui/material/styles'; import PropTypes from 'prop-types'; -import useSlotProps from '@mui/utils/useSlotProps'; -import composeClasses from '@mui/utils/composeClasses'; -import { DefaultizedProps } from '@mui/x-internals/types'; -import { useThemeProps, useTheme, Theme } from '@mui/material/styles'; -import { getSeriesToDisplay } from './utils'; -import { getLegendUtilityClass } from './chartsLegendClasses'; -import { DefaultChartsLegend, LegendRendererProps } from './DefaultChartsLegend'; -import { useSeries } from '../hooks/useSeries'; -import { LegendPlacement } from './legend.types'; - -export type ChartsLegendPropsBase = Omit< - LegendRendererProps, - keyof LegendPlacement | 'series' | 'seriesToDisplay' | 'drawingArea' -> & - LegendPlacement; - -export interface ChartsLegendSlots { +import clsx from 'clsx'; +import { useLegend } from '../hooks/useLegend'; +import type { Direction } from './direction'; +import { SeriesLegendItemContext } from './legendContext.types'; +import { ChartsLabelMark } from '../ChartsLabel/ChartsLabelMark'; +import { seriesContextBuilder } from './onClickContextBuilder'; +import { legendClasses, useUtilityClasses, type ChartsLegendClasses } from './chartsLegendClasses'; +import { consumeSlots } from '../internals/consumeSlots'; +import { ChartsLabel } from '../ChartsLabel/ChartsLabel'; + +export interface ChartsLegendProps { /** - * Custom rendering of the legend. - * @default DefaultChartsLegend + * 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. */ - legend?: React.JSXElementConstructor; -} - -export interface ChartsLegendSlotProps { - legend?: Partial; -} - -export interface ChartsLegendProps extends ChartsLegendPropsBase { + onItemClick?: ( + event: React.MouseEvent, + legendItem: SeriesLegendItemContext, + index: number, + ) => void; /** - * Overridable component slots. - * @default {} + * The direction of the legend layout. + * The default depends on the chart. */ - slots?: ChartsLegendSlots; + direction?: Direction; /** - * The props used for each component slot. - * @default {} + * Override or extend the styles applied to the component. */ - slotProps?: ChartsLegendSlotProps; + classes?: Partial; + className?: string; + sx?: SxProps; } -type DefaultizedChartsLegendProps = DefaultizedProps; - -const useUtilityClasses = (ownerState: DefaultizedChartsLegendProps & { theme: Theme }) => { - const { classes, direction } = ownerState; - const slots = { - root: ['root', direction], - mark: ['mark'], - label: ['label'], - series: ['series'], - itemBackground: ['itemBackground'], - }; - - return composeClasses(slots, getLegendUtilityClass, classes); -}; - -function ChartsLegend(inProps: ChartsLegendProps) { - const props = useThemeProps({ - props: inProps, - name: 'MuiChartsLegend', - }); - - const defaultizedProps: DefaultizedChartsLegendProps = { - direction: 'row', - ...props, - position: { horizontal: 'middle', vertical: 'top', ...props.position }, - }; - const { slots, slotProps, ...other } = defaultizedProps; - - const theme = useTheme(); - const classes = useUtilityClasses({ ...defaultizedProps, theme }); - - const series = useSeries(); - - const seriesToDisplay = getSeriesToDisplay(series); - - const ChartLegendRender = slots?.legend ?? DefaultChartsLegend; - const chartLegendRenderProps = useSlotProps({ - elementType: ChartLegendRender, - externalSlotProps: slotProps?.legend, - additionalProps: { - ...other, - classes, - series, - seriesToDisplay, - }, - ownerState: {}, - }); - - return ; -} +const RootElement = styled('ul', { + name: 'MuiChartsLegend', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: ChartsLegendProps }>(({ ownerState, theme }) => ({ + ...theme.typography.caption, + color: (theme.vars || theme).palette.text.primary, + lineHeight: '100%', + display: 'flex', + flexDirection: ownerState.direction === 'vertical' ? 'column' : 'row', + alignItems: ownerState.direction === 'vertical' ? undefined : 'center', + flexShrink: 0, + gap: theme.spacing(2), + listStyleType: 'none', + paddingInlineStart: 0, + marginBlock: theme.spacing(1), + marginInline: theme.spacing(1), + flexWrap: 'wrap', + li: { + display: ownerState.direction === 'horizontal' ? 'inline-flex' : undefined, + }, + [`button.${legendClasses.series}`]: { + // Reset button styles + background: 'none', + border: 'none', + padding: 0, + fontFamily: 'inherit', + fontWeight: 'inherit', + fontSize: 'inherit', + letterSpacing: 'inherit', + color: 'inherit', + }, + [`& .${legendClasses.series}`]: { + display: ownerState.direction === 'vertical' ? 'flex' : 'inline-flex', + alignItems: 'center', + gap: theme.spacing(1), + }, +})); + +const ChartsLegend = consumeSlots( + 'MuiChartsLegend', + 'legend', + { + defaultProps: { direction: 'horizontal' }, + // @ts-expect-error position is used only in the slots, but it is passed to the SVG wrapper. + // We omit it here to avoid passing to slots. + omitProps: ['position'], + classesResolver: useUtilityClasses, + }, + function ChartsLegend(props: ChartsLegendProps, ref: React.Ref) { + const data = useLegend(); + const { direction, onItemClick, className, classes, ...other } = props; + + if (data.items.length === 0) { + return null; + } + + const Element = onItemClick ? 'button' : 'div'; + + return ( + + {data.items.map((item, i) => { + return ( +
  • + onItemClick(event, seriesContextBuilder(item), i) + : undefined + } + > + + {item.label} + +
  • + ); + })} +
    + ); + }, +); ChartsLegend.propTypes = { // ----------------------------- Warning -------------------------------- @@ -102,69 +141,19 @@ ChartsLegend.propTypes = { * Override or extend the styles applied to the component. */ classes: PropTypes.object, + className: PropTypes.string, /** * The direction of the legend layout. * The default depends on the chart. */ - direction: PropTypes.oneOf(['column', 'row']), - /** - * Set to true to hide the legend. - * @default false - */ - hidden: PropTypes.bool, - /** - * Space between two legend items (in px). - * @default 10 - */ - itemGap: PropTypes.number, - /** - * Height of the item mark (in px). - * @default 20 - */ - itemMarkHeight: PropTypes.number, - /** - * Width of the item mark (in px). - * @default 20 - */ - itemMarkWidth: PropTypes.number, - /** - * Style applied to legend labels. - * @default theme.typography.subtitle1 - */ - labelStyle: PropTypes.object, - /** - * Space between the mark and the label (in px). - * @default 5 - */ - markGap: PropTypes.number, + direction: PropTypes.oneOf(['horizontal', 'vertical']), /** * Callback fired when a legend item is clicked. - * @param {React.MouseEvent} event The click event. + * @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. - * @default 10 - */ - padding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - top: PropTypes.number, - }), - ]), - /** - * The position of the legend. - */ - position: PropTypes.shape({ - horizontal: PropTypes.oneOf(['left', 'middle', 'right']).isRequired, - vertical: PropTypes.oneOf(['bottom', 'middle', 'top']).isRequired, - }), /** * The props used for each component slot. * @default {} @@ -175,6 +164,11 @@ ChartsLegend.propTypes = { * @default {} */ slots: PropTypes.object, + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), } as any; export { ChartsLegend }; diff --git a/packages/x-charts/src/ChartsLegend/ChartsLegendItem.tsx b/packages/x-charts/src/ChartsLegend/ChartsLegendItem.tsx deleted file mode 100644 index bf8521339daab..0000000000000 --- a/packages/x-charts/src/ChartsLegend/ChartsLegendItem.tsx +++ /dev/null @@ -1,87 +0,0 @@ -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/ContinuousColorLegend.test.tsx b/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.test.tsx new file mode 100644 index 0000000000000..baf93faeb0e46 --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.test.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { createRenderer, describeConformance } from '@mui/internal-test-utils'; +import { ContinuousColorLegend, continuousColorLegendClasses } from '@mui/x-charts/ChartsLegend'; +import { ChartDataProvider } from '@mui/x-charts/context'; +import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + classes: continuousColorLegendClasses, + inheritComponent: 'ul', + render: (node) => + render(node, { + wrapper: ({ children }) => ( + `${t}`, + }, + }, + ]} + > + {/* Has to be first as describeConformance picks the "first child" */} + {/* https://github.com/mui/material-ui/blob/c0620e333641deda56f3cd68c7c3736098ee818c/packages-internal/test-utils/src/describeConformance.tsx#L257 */} + {children} + + + ), + }), + muiName: 'MuiContinuousColorLegend', + testComponentPropWith: 'ul', + refInstanceof: window.HTMLUListElement, + ThemeProvider, + createTheme, + // SKIP + skip: ['themeVariants', 'componentProp', 'componentsProp'], + })); +}); diff --git a/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx b/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx index e45d1480d8150..98c1cb1ed7c09 100644 --- a/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/ContinuousColorLegend.tsx @@ -1,352 +1,280 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { ScaleSequential } from '@mui/x-charts-vendor/d3-scale'; -import { useTheme } from '@mui/material/styles'; -import { useRtl } from '@mui/system/RtlProvider'; -import ChartsContinuousGradient from '../internals/components/ChartsAxesGradients/ChartsContinuousGradient'; -import { AxisDefaultized, ContinuousScaleName } from '../models/axis'; -import { useChartId, useDrawingArea } from '../hooks'; -import { getScale } from '../internals/getScale'; -import { getPercentageValue } from '../internals/getPercentageValue'; -import { ChartsText, ChartsTextProps } from '../ChartsText'; -import { getStringSize } from '../internals/domUtils'; +import { styled, SxProps, Theme } from '@mui/material/styles'; +import clsx from 'clsx'; +import { AppendKeys } from '@mui/x-internals/types'; +import { AxisDefaultized } from '../models/axis'; import { useAxis } from './useAxis'; +import { ColorLegendSelector } from './colorLegend.types'; +import { ChartsLabel } from '../ChartsLabel/ChartsLabel'; +import { ChartsLabelGradient, ChartsLabelGradientProps } from '../ChartsLabel/ChartsLabelGradient'; +import { Direction } from './direction'; +import { consumeThemeProps } from '../internals/consumeThemeProps'; import { - AnchorPosition, - BoundingBox, - ColorLegendSelector, - LegendPlacement, - Position, - TextPosition, -} from './legend.types'; - -function getPositionOffset(position: AnchorPosition, legendBox: BoundingBox, svgBox: BoundingBox) { - let offsetX = 0; - let offsetY = 0; - - switch (position.horizontal) { - case 'left': - offsetX = 0; - break; - case 'middle': - offsetX = (svgBox.width - legendBox.width) / 2; - break; - case 'right': - default: - offsetX = svgBox.width - legendBox.width; - break; - } - switch (position.vertical) { - case 'top': - offsetY = 0; - break; - case 'middle': - offsetY = (svgBox.height - legendBox.height) / 2; - break; - case 'bottom': - default: - offsetY = svgBox.height - legendBox.height; - break; - } - - return { offsetX, offsetY }; -} - -/** - * Takes placement parameters and element bounding boxes. - * Returns the x, y coordinates of the elements. And the textAnchor, dominantBaseline for texts. - */ -function getElementPositions( - text1Box: BoundingBox, - barBox: BoundingBox, - text2Box: BoundingBox, - params: { - spacing: number; - align: ContinuousColorLegendProps['align']; - direction: ContinuousColorLegendProps['direction']; - }, -): { - text1: TextPosition; - text2: TextPosition; - bar: Position; - boundingBox: BoundingBox; -} { - if (params.direction === 'column') { - const text1 = { y: text1Box.height, dominantBaseline: 'auto' } as const; - const text2 = { - y: text1Box.height + 2 * params.spacing + barBox.height, - dominantBaseline: 'hanging', - } as const; - const bar = { y: text1Box.height + params.spacing }; - - const totalWidth = Math.max(text1Box.width, barBox.width, text2Box.width); - const totalHeight = text1Box.height + barBox.height + text2Box.height + 2 * params.spacing; - - const boundingBox = { width: totalWidth, height: totalHeight }; - switch (params.align) { - case 'start': - return { - text1: { ...text1, textAnchor: 'start', x: 0 }, - text2: { ...text2, textAnchor: 'start', x: 0 }, - bar: { ...bar, x: 0 }, - boundingBox, - }; - case 'end': - return { - text1: { ...text1, textAnchor: 'end', x: totalWidth }, - text2: { ...text2, textAnchor: 'end', x: totalWidth }, - bar: { ...bar, x: totalWidth - barBox.width }, - boundingBox, - }; - case 'middle': - default: - return { - text1: { ...text1, textAnchor: 'middle', x: totalWidth / 2 }, - text2: { ...text2, textAnchor: 'middle', x: totalWidth / 2 }, - bar: { ...bar, x: totalWidth / 2 - barBox.width / 2 }, - boundingBox, - }; - } - } else { - const text1 = { x: text1Box.width, textAnchor: 'end' } as const; - const text2 = { - x: text1Box.width + 2 * params.spacing + barBox.width, - textAnchor: 'start', - } as const; - const bar = { x: text1Box.width + params.spacing }; - - const totalHeight = Math.max(text1Box.height, barBox.height, text2Box.height); - const totalWidth = text1Box.width + barBox.width + text2Box.width + 2 * params.spacing; - - const boundingBox = { width: totalWidth, height: totalHeight }; - - switch (params.align) { - case 'start': - return { - text1: { ...text1, dominantBaseline: 'hanging', y: 0 }, - text2: { ...text2, dominantBaseline: 'hanging', y: 0 }, - bar: { ...bar, y: 0 }, - boundingBox, - }; - case 'end': - return { - text1: { ...text1, dominantBaseline: 'auto', y: totalHeight }, - text2: { ...text2, dominantBaseline: 'auto', y: totalHeight }, - bar: { ...bar, y: totalHeight - barBox.height }, - boundingBox, - }; - case 'middle': - default: - return { - text1: { ...text1, dominantBaseline: 'central', y: totalHeight / 2 }, - text2: { ...text2, dominantBaseline: 'central', y: totalHeight / 2 }, - bar: { ...bar, y: totalHeight / 2 - barBox.height / 2 }, - boundingBox, - }; - } - } -} + continuousColorLegendClasses, + ContinuousColorLegendClasses, + useUtilityClasses, +} from './continuousColorLegendClasses'; +import { useChartGradientObjectBound } from '../internals/components/ChartsAxesGradients'; type LabelFormatter = (params: { value: number | Date; formattedValue: string }) => string; -export interface ContinuousColorLegendProps extends LegendPlacement, ColorLegendSelector { +export interface ContinuousColorLegendProps + extends ColorLegendSelector, + AppendKeys, 'gradient'>, + Pick { + /** + * The direction of the legend layout. + * @default 'horizontal' + */ + direction?: Direction; /** * The label to display at the minimum side of the gradient. * Can either be a string, or a function. - * @default ({ formattedValue }) => formattedValue + * @default formattedValue */ minLabel?: string | LabelFormatter; /** * The label to display at the maximum side of the gradient. * Can either be a string, or a function. * If not defined, the formatted maximal value is display. - * @default ({ formattedValue }) => formattedValue + * @default formattedValue */ maxLabel?: string | LabelFormatter; /** - * A unique identifier for the gradient. + * The id for the gradient to use. + * If not provided, it will use the generated gradient from the axis configuration. + * The `gradientId` will be used as `fill="url(#gradientId)"`. * @default auto-generated id */ - id?: string; - /** - * The scale used to display gradient colors. - * @default 'linear' - */ - scaleType?: ContinuousScaleName; - /** - * The length of the gradient bar. - * Can be a number (in px) or a string with a percentage such as '50%'. - * The '100%' is the length of the svg. - * @default '50%' - */ - length?: number | string; + gradientId?: string; /** - * The thickness of the gradient bar. - * @default 5 + * Where to position the labels relative to the gradient. + * @default 'end' */ - thickness?: number; + labelPosition?: 'start' | 'end' | 'extremes'; /** - * The alignment of the texts with the gradient bar. - * @default 'middle' + * If `true`, the gradient and labels will be reversed. + * @default false */ - align?: 'start' | 'middle' | 'end'; + reverse?: boolean; /** - * The space between the gradient bar and the labels. - * @default 4 + * Override or extend the styles applied to the component. */ - spacing?: number; - /** - * The style applied to labels. - * @default theme.typography.subtitle1 - */ - labelStyle?: ChartsTextProps['style']; + classes?: Partial; + className?: string; + sx?: SxProps; } -const defaultLabelFormatter: LabelFormatter = ({ formattedValue }) => formattedValue; - -function ContinuousColorLegend(props: ContinuousColorLegendProps) { - const theme = useTheme(); - const isRtl = useRtl(); - const { - id: idProp, - minLabel = defaultLabelFormatter, - maxLabel = defaultLabelFormatter, - scaleType = 'linear', - direction, - length = '50%', - thickness = 5, - spacing = 4, - align = 'middle', - labelStyle = theme.typography.subtitle1 as ChartsTextProps['style'], - position, - axisDirection, - axisId, - } = props; - - const chartId = useChartId(); - const id = idProp ?? `gradient-legend-${chartId}`; - - const axisItem = useAxis({ axisDirection, axisId }); - const { width, height, left, right, top, bottom } = useDrawingArea(); - - const refLength = direction === 'column' ? height + top + bottom : width + left + right; - const size = getPercentageValue(length, refLength); - - const isReversed = direction === 'column'; - - const colorMap = axisItem?.colorMap; - if (!colorMap || !colorMap.type || colorMap.type !== 'continuous') { - return null; +const templateAreas = (reverse?: boolean) => { + const startLabel = reverse ? 'max-label' : 'min-label'; + const endLabel = reverse ? 'min-label' : 'max-label'; + + return { + row: { + start: ` + '${startLabel} . ${endLabel}' + 'gradient gradient gradient' + `, + end: ` + 'gradient gradient gradient' + '${startLabel} . ${endLabel}' + `, + extremes: ` + '${startLabel} gradient ${endLabel}' + `, + }, + column: { + start: ` + '${endLabel} gradient' + '. gradient' + '${startLabel} gradient' + `, + end: ` + 'gradient ${endLabel}' + 'gradient .' + 'gradient ${startLabel}' + `, + extremes: ` + '${endLabel}' + 'gradient' + '${startLabel}' + `, + }, + }; +}; + +const RootElement = styled('ul', { + name: 'MuiContinuousColorLegend', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: ContinuousColorLegendProps }>(({ theme, ownerState }) => ({ + ...theme.typography.caption, + color: (theme.vars || theme).palette.text.primary, + lineHeight: '100%', + display: 'grid', + flexShrink: 0, + gap: theme.spacing(0.5), + listStyleType: 'none', + paddingInlineStart: 0, + marginBlock: theme.spacing(1), + marginInline: theme.spacing(1), + [`&.${continuousColorLegendClasses.horizontal}`]: { + gridTemplateRows: 'min-content min-content', + gridTemplateColumns: 'min-content auto min-content', + [`&.${continuousColorLegendClasses.start}`]: { + gridTemplateAreas: templateAreas(ownerState.reverse).row.start, + }, + [`&.${continuousColorLegendClasses.end}`]: { + gridTemplateAreas: templateAreas(ownerState.reverse).row.end, + }, + [`&.${continuousColorLegendClasses.extremes}`]: { + gridTemplateAreas: templateAreas(ownerState.reverse).row.extremes, + gridTemplateRows: 'min-content', + alignItems: 'center', + }, + }, + [`&.${continuousColorLegendClasses.vertical}`]: { + gridTemplateRows: 'min-content auto min-content', + gridTemplateColumns: 'min-content min-content', + [`&.${continuousColorLegendClasses.start}`]: { + gridTemplateAreas: templateAreas(ownerState.reverse).column.start, + [`.${continuousColorLegendClasses.maxLabel}, .${continuousColorLegendClasses.minLabel}`]: { + justifySelf: 'end', + }, + }, + [`&.${continuousColorLegendClasses.end}`]: { + gridTemplateAreas: templateAreas(ownerState.reverse).column.end, + [`.${continuousColorLegendClasses.maxLabel}, .${continuousColorLegendClasses.minLabel}`]: { + justifySelf: 'start', + }, + }, + [`&.${continuousColorLegendClasses.extremes}`]: { + gridTemplateAreas: templateAreas(ownerState.reverse).column.extremes, + gridTemplateColumns: 'min-content', + [`.${continuousColorLegendClasses.maxLabel}, .${continuousColorLegendClasses.minLabel}`]: { + justifySelf: 'center', + }, + }, + }, + [`.${continuousColorLegendClasses.gradient}`]: { + gridArea: 'gradient', + }, + [`.${continuousColorLegendClasses.maxLabel}`]: { + gridArea: 'max-label', + }, + [`.${continuousColorLegendClasses.minLabel}`]: { + gridArea: 'min-label', + }, +})); + +const getText = ( + label: string | LabelFormatter | undefined, + value: number | Date, + formattedValue: string, +) => { + if (typeof label === 'string') { + return label; } + return label?.({ value, formattedValue }) ?? formattedValue; +}; + +const ContinuousColorLegend = consumeThemeProps( + 'MuiContinuousColorLegend', + { + defaultProps: { + direction: 'horizontal', + labelPosition: 'end', + axisDirection: 'z', + }, + classesResolver: useUtilityClasses, + }, + function ContinuousColorLegend( + props: ContinuousColorLegendProps, + ref: React.Ref, + ) { + const { + minLabel, + maxLabel, + direction, + axisDirection, + axisId, + rotateGradient, + reverse, + classes, + className, + gradientId, + labelPosition, + thickness, + ...other + } = props; + + const generateGradientId = useChartGradientObjectBound(); + const axisItem = useAxis({ axisDirection, axisId }); + + const colorMap = axisItem?.colorMap; + if (!colorMap || !colorMap.type || colorMap.type !== 'continuous') { + return null; + } - // Define the coordinate to color mapping - - const colorScale = axisItem.colorScale as ScaleSequential; - - const minValue = colorMap.min ?? 0; - const maxValue = colorMap.max ?? 100; - - const scale = getScale(scaleType, [minValue, maxValue], isReversed ? [size, 0] : [0, size]); - - // Get texts to display - - const formattedMin = - (axisItem as AxisDefaultized).valueFormatter?.(minValue, { location: 'legend' }) ?? - minValue.toLocaleString(); - - const formattedMax = - (axisItem as AxisDefaultized).valueFormatter?.(maxValue, { location: 'legend' }) ?? - maxValue.toLocaleString(); - - const minText = - typeof minLabel === 'string' - ? minLabel - : minLabel({ value: minValue ?? 0, formattedValue: formattedMin }); - - const maxText = - typeof maxLabel === 'string' - ? maxLabel - : maxLabel({ value: maxValue ?? 0, formattedValue: formattedMax }); - - const text1 = isReversed ? maxText : minText; - const text2 = isReversed ? minText : maxText; - - const text1Box = getStringSize(text1, { ...labelStyle }); - const text2Box = getStringSize(text2, { ...labelStyle }); - - // Place bar and texts - - const barBox = - direction === 'column' || (isRtl && direction === 'row') - ? { width: thickness, height: size } - : { width: size, height: thickness }; - - const legendPositions = getElementPositions(text1Box, barBox, text2Box, { - spacing, - align, - direction, - }); - const svgBoundingBox = { width: width + left + right, height: height + top + bottom }; - - const positionOffset = getPositionOffset( - { horizontal: 'middle', vertical: 'top', ...position }, - legendPositions.boundingBox, - svgBoundingBox, - ); - - return ( - - - - - - - ); -} + const minValue = colorMap.min ?? 0; + const maxValue = colorMap.max ?? 100; + + // Get texts to display + + const valueFormatter = (axisItem as AxisDefaultized)?.valueFormatter; + const formattedMin = valueFormatter + ? valueFormatter(minValue, { location: 'legend' }) + : minValue.toLocaleString(); + + const formattedMax = valueFormatter + ? valueFormatter(maxValue, { location: 'legend' }) + : maxValue.toLocaleString(); + + const minText = getText(minLabel, minValue, formattedMin); + const maxText = getText(maxLabel, maxValue, formattedMax); + + const minComponent = ( +
  • + {minText} +
  • + ); + + const maxComponent = ( +
  • + {maxText} +
  • + ); + + return ( + + {reverse ? maxComponent : minComponent} +
  • + +
  • + {reverse ? minComponent : maxComponent} +
    + ); + }, +); ContinuousColorLegend.propTypes = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- - /** - * The alignment of the texts with the gradient bar. - * @default 'middle' - */ - align: PropTypes.oneOf(['end', 'middle', 'start']), /** * The axis direction containing the color configuration to represent. * @default 'z' @@ -358,62 +286,60 @@ ContinuousColorLegend.propTypes = { */ axisId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), /** - * The direction of the legend layout. - * The default depends on the chart. + * Override or extend the styles applied to the component. */ - direction: PropTypes.oneOf(['column', 'row']), + classes: PropTypes.object, + className: PropTypes.string, /** - * A unique identifier for the gradient. - * @default auto-generated id + * The direction of the legend layout. + * @default 'horizontal' */ - id: PropTypes.string, + direction: PropTypes.oneOf(['horizontal', 'vertical']), /** - * The style applied to labels. - * @default theme.typography.subtitle1 + * The id for the gradient to use. + * If not provided, it will use the generated gradient from the axis configuration. + * The `gradientId` will be used as `fill="url(#gradientId)"`. + * @default auto-generated id */ - labelStyle: PropTypes.object, + gradientId: PropTypes.string, /** - * The length of the gradient bar. - * Can be a number (in px) or a string with a percentage such as '50%'. - * The '100%' is the length of the svg. - * @default '50%' + * Where to position the labels relative to the gradient. + * @default 'end' */ - length: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + labelPosition: PropTypes.oneOf(['start', 'end', 'extremes']), /** * The label to display at the maximum side of the gradient. * Can either be a string, or a function. * If not defined, the formatted maximal value is display. - * @default ({ formattedValue }) => formattedValue + * @default formattedValue */ maxLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** * The label to display at the minimum side of the gradient. * Can either be a string, or a function. - * @default ({ formattedValue }) => formattedValue + * @default formattedValue */ minLabel: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), /** - * The position of the legend. - */ - position: PropTypes.shape({ - horizontal: PropTypes.oneOf(['left', 'middle', 'right']).isRequired, - vertical: PropTypes.oneOf(['bottom', 'middle', 'top']).isRequired, - }), - /** - * The scale used to display gradient colors. - * @default 'linear' + * If `true`, the gradient and labels will be reversed. + * @default false */ - scaleType: PropTypes.oneOf(['linear', 'log', 'pow', 'sqrt', 'time', 'utc']), + reverse: PropTypes.bool, /** - * The space between the gradient bar and the labels. - * @default 4 + * If provided, the gradient will be rotated by 90deg. + * Useful for linear gradients that are not in the correct orientation. */ - spacing: PropTypes.number, + rotateGradient: PropTypes.bool, /** - * The thickness of the gradient bar. - * @default 5 + * The thickness of the gradient + * @default 12 */ thickness: PropTypes.number, + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, + ]), } as any; export { ContinuousColorLegend }; diff --git a/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx b/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx deleted file mode 100644 index ff2c62ade76bc..0000000000000 --- a/packages/x-charts/src/ChartsLegend/DefaultChartsLegend.tsx +++ /dev/null @@ -1,145 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedSeries } from '../context/SeriesProvider'; -import { LegendPerItem, LegendPerItemProps } from './LegendPerItem'; -import { LegendItemParams, SeriesLegendItemContext } from './chartsLegend.types'; - -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']; - /** - * 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 - */ - hidden?: boolean; -} - -function DefaultChartsLegend(props: LegendRendererProps) { - const { seriesToDisplay, hidden, onItemClick, ...other } = props; - - if (hidden) { - return null; - } - - return ( - onItemClick(event, seriesContextBuilder(seriesToDisplay[i]), i) - : undefined - } - /> - ); -} - -DefaultChartsLegend.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the TypeScript types and run "pnpm proptypes" | - // ---------------------------------------------------------------------- - /** - * Override or extend the styles applied to the component. - */ - classes: PropTypes.object, - /** - * The direction of the legend layout. - * The default depends on the chart. - */ - direction: PropTypes.oneOf(['column', 'row']).isRequired, - /** - * Set to true to hide the legend. - * @default false - */ - hidden: PropTypes.bool, - /** - * Space between two legend items (in px). - * @default 10 - */ - itemGap: PropTypes.number, - /** - * Height of the item mark (in px). - * @default 20 - */ - itemMarkHeight: PropTypes.number, - /** - * Width of the item mark (in px). - * @default 20 - */ - itemMarkWidth: PropTypes.number, - /** - * Style applied to legend labels. - * @default theme.typography.subtitle1 - */ - labelStyle: PropTypes.object, - /** - * Space between the mark and the label (in px). - * @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. - * @default 10 - */ - padding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - top: PropTypes.number, - }), - ]), - /** - * The position of the legend. - */ - position: PropTypes.shape({ - horizontal: PropTypes.oneOf(['left', 'middle', 'right']).isRequired, - vertical: PropTypes.oneOf(['bottom', 'middle', 'top']).isRequired, - }).isRequired, - series: PropTypes.object.isRequired, - seriesToDisplay: PropTypes.arrayOf( - 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; - -export { DefaultChartsLegend }; diff --git a/packages/x-charts/src/ChartsLegend/LegendPerItem.tsx b/packages/x-charts/src/ChartsLegend/LegendPerItem.tsx deleted file mode 100644 index a144358e6d875..0000000000000 --- a/packages/x-charts/src/ChartsLegend/LegendPerItem.tsx +++ /dev/null @@ -1,215 +0,0 @@ -'use client'; -import * as React from 'react'; -import { DefaultizedProps } from '@mui/x-internals/types'; -import NoSsr from '@mui/material/NoSsr'; -import { useTheme, styled } from '@mui/material/styles'; -import { DrawingAreaState } from '../context/DrawingAreaProvider'; -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'; - -export type ChartsLegendRootOwnerState = { - position: AnchorPosition; - direction: Direction; - drawingArea: DrawingAreaState; - offsetX?: number; - offsetY?: number; - seriesNumber: number; -}; - -export const ChartsLegendRoot = styled('g', { - name: 'MuiChartsLegend', - slot: 'Root', - overridesResolver: (props, styles) => styles.root, -})({}); - -export interface LegendPerItemProps - extends DefaultizedProps { - /** - * The ordered array of item to display in the legend. - */ - itemsToDisplay: LegendItemParams[]; - /** - * Override or extend the styles applied to the component. - */ - classes?: Partial; - /** - * Style applied to legend labels. - * @default theme.typography.subtitle1 - */ - labelStyle?: ChartsTextStyle; - /** - * Width of the item mark (in px). - * @default 20 - */ - itemMarkWidth?: number; - /** - * Height of the item mark (in px). - * @default 20 - */ - itemMarkHeight?: number; - /** - * Space between the mark and the label (in px). - * @default 5 - */ - markGap?: number; - /** - * Space between two legend items (in px). - * @default 10 - */ - itemGap?: number; - /** - * Legend padding (in px). - * Can either be a single number, or an object with top, left, bottom, right properties. - * @default 10 - */ - padding?: number | Partial>; - onItemClick?: (event: React.MouseEvent, index: number) => void; -} - -/** - * Transforms number or partial padding object to a defaultized padding object. - */ -const getStandardizedPadding = (padding: LegendPerItemProps['padding']) => { - if (typeof padding === 'number') { - return { - left: padding, - right: padding, - top: padding, - bottom: padding, - }; - } - return { - left: 0, - right: 0, - top: 0, - bottom: 0, - ...padding, - }; -}; - -/** - * Internal component to display an array of items as a legend. - * Used for series legend, and threshold color legend. - * @ignore - Do not document - */ -export function LegendPerItem(props: LegendPerItemProps) { - const { - position, - direction, - itemsToDisplay, - classes, - itemMarkWidth = 20, - itemMarkHeight = 20, - markGap = 5, - itemGap = 10, - padding: paddingProps = 10, - labelStyle: inLabelStyle, - onItemClick, - } = props; - const theme = useTheme(); - const drawingArea = useDrawingArea(); - - const labelStyle = React.useMemo( - () => - ({ - ...theme.typography.subtitle1, - color: 'inherit', - dominantBaseline: 'central', - textAnchor: 'start', - fill: (theme.vars || theme).palette.text.primary, - lineHeight: 1, - ...inLabelStyle, - }) as ChartsTextStyle, // To say to TS that the dominantBaseline and textAnchor are correct - [inLabelStyle, theme], - ); - - const padding = React.useMemo(() => getStandardizedPadding(paddingProps), [paddingProps]); - - const getItemSpace: GetItemSpaceType = React.useCallback( - (label, inStyle = {}) => { - const { rotate, dominantBaseline, ...style } = inStyle; - const linesSize = getWordsByLines({ style, needsComputation: true, text: label }); - const innerSize = { - innerWidth: itemMarkWidth + markGap + Math.max(...linesSize.map((size) => size.width)), - innerHeight: Math.max(itemMarkHeight, linesSize.length * linesSize[0].height), - }; - - return { - ...innerSize, - outerWidth: innerSize.innerWidth + itemGap, - outerHeight: innerSize.innerHeight + itemGap, - }; - }, - [itemGap, itemMarkHeight, itemMarkWidth, markGap], - ); - - const totalWidth = drawingArea.left + drawingArea.width + drawingArea.right; - const totalHeight = drawingArea.top + drawingArea.height + drawingArea.bottom; - const availableWidth = totalWidth - padding.left - padding.right; - const availableHeight = totalHeight - padding.top - padding.bottom; - - const [itemsWithPosition, legendWidth, legendHeight] = React.useMemo( - () => - legendItemPlacements( - itemsToDisplay, - getItemSpace, - labelStyle, - direction, - availableWidth, - availableHeight, - itemGap, - ), - [itemsToDisplay, getItemSpace, labelStyle, direction, availableWidth, availableHeight, itemGap], - ); - - const gapX = React.useMemo(() => { - switch (position.horizontal) { - case 'left': - return padding.left; - case 'right': - return totalWidth - padding.right - legendWidth; - default: - return (totalWidth - legendWidth) / 2; - } - }, [position.horizontal, padding.left, padding.right, totalWidth, legendWidth]); - - const gapY = React.useMemo(() => { - switch (position.vertical) { - case 'top': - return padding.top; - case 'bottom': - return totalHeight - padding.bottom - legendHeight; - default: - return (totalHeight - legendHeight) / 2; - } - }, [position.vertical, padding.top, padding.bottom, totalHeight, legendHeight]); - - return ( - - - {itemsWithPosition.map((item, i) => ( - onItemClick(event, i) : undefined} - /> - ))} - - - ); -} diff --git a/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.test.tsx b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.test.tsx new file mode 100644 index 0000000000000..b42baa978eb5a --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.test.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { createRenderer, describeConformance } from '@mui/internal-test-utils'; +import { PiecewiseColorLegend, piecewiseColorLegendClasses } from '@mui/x-charts/ChartsLegend'; +import { ChartDataProvider } from '@mui/x-charts/context'; +import { ChartsSurface } from '@mui/x-charts/ChartsSurface'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + classes: piecewiseColorLegendClasses, + inheritComponent: 'ul', + render: (node) => + render(node, { + wrapper: ({ children }) => ( + + {/* Has to be first as describeConformance picks the "first child" */} + {/* https://github.com/mui/material-ui/blob/c0620e333641deda56f3cd68c7c3736098ee818c/packages-internal/test-utils/src/describeConformance.tsx#L257 */} + {children} + + + ), + }), + muiName: 'MuiPiecewiseColorLegend', + testComponentPropWith: 'ul', + refInstanceof: window.HTMLUListElement, + ThemeProvider, + createTheme, + // SKIP + skip: ['themeVariants', 'componentProp', 'componentsProp'], + })); +}); diff --git a/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx index fa94832fc4d38..c094ddf44173c 100644 --- a/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx +++ b/packages/x-charts/src/ChartsLegend/PiecewiseColorLegend.tsx @@ -1,132 +1,280 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; +import { styled, SxProps, Theme } from '@mui/material/styles'; +import clsx from 'clsx'; +import { PrependKeys } from '@mui/x-internals/types'; +import { ChartsLabel } from '../ChartsLabel/ChartsLabel'; +import { ChartsLabelMark, ChartsLabelMarkProps } from '../ChartsLabel/ChartsLabelMark'; +import { Direction } from './direction'; +import { consumeThemeProps } from '../internals/consumeThemeProps'; +import { + piecewiseColorLegendClasses, + PiecewiseColorLegendClasses, + useUtilityClasses, +} from './piecewiseColorLegendClasses'; +import { ColorLegendSelector } from './colorLegend.types'; +import { PiecewiseLabelFormatterParams } from './piecewiseColorLegend.types'; import { AxisDefaultized } from '../models/axis'; 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) { - return `<${params.formattedMax}`; - } - if (params.max === null) { - return `>${params.formattedMin}`; - } - return `${params.formattedMin}-${params.formattedMax}`; -} +import { PiecewiseColorLegendItemContext } from './legendContext.types'; +import { piecewiseColorDefaultLabelFormatter } from './piecewiseColorDefaultLabelFormatter'; export interface PiecewiseColorLegendProps extends ColorLegendSelector, - Omit { - /** - * Hide the first item of the legend, corresponding to the [-infinity, min] piece. - * @default false - */ - hideFirst?: boolean; + PrependKeys, 'mark'> { /** - * Hide the last item of the legend, corresponding to the [max, +infinity] piece. - * @default false + * The direction of the legend layout. + * @default 'horizontal' */ - hideLast?: boolean; + direction?: Direction; /** * Format the legend labels. * @param {PiecewiseLabelFormatterParams} params The bound of the piece to format. - * @returns {string|null} The displayed label, or `null` to skip the item. + * @returns {string|null} The displayed label, `''` to skip the label but show the color mark, or `null` to skip it entirely. */ labelFormatter?: (params: PiecewiseLabelFormatterParams) => string | null; + /** + * Where to position the labels relative to the gradient. + * @default 'extremes' + */ + labelPosition?: 'start' | 'end' | 'extremes'; /** * Callback fired when a legend item is clicked. - * @param {React.MouseEvent} event The click event. + * @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, + event: React.MouseEvent, legendItem: PiecewiseColorLegendItemContext, index: number, ) => void; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + className?: string; + sx?: SxProps; } -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, - axisId, - hideFirst, - hideLast, - labelFormatter = defaultLabelFormatter, - onItemClick, - ...other - } = props; - - const axisItem = useAxis({ axisDirection, axisId }); - - const colorMap = axisItem?.colorMap; - if (!colorMap || !colorMap.type || colorMap.type !== 'piecewise') { - return null; - } - const valueFormatter = (v: number | Date) => - (axisItem as AxisDefaultized).valueFormatter?.(v, { location: 'legend' }) ?? v.toLocaleString(); - - const formattedLabels = colorMap.thresholds.map(valueFormatter); - - const itemsToDisplay = colorMap.colors - .map((color, index) => { - const isFirst = index === 0; - const isLast = index === colorMap.colors.length - 1; - - if ((hideFirst && isFirst) || (hideLast && isLast)) { - return null; - } - - const data = { - ...(isFirst - ? { min: null, formattedMin: null } - : { min: colorMap.thresholds[index - 1], formattedMin: formattedLabels[index - 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, - minValue: data.min, - maxValue: data.max, - }; - }) - .filter(notNull); - - return ( - onItemClick(event, piecewiseColorContextBuilder(itemsToDisplay[i]), i) - : undefined - } - /> - ); -} +const RootElement = styled('ul', { + name: 'MuiPiecewiseColorLegend', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: PiecewiseColorLegendProps }>(({ theme, ownerState }) => { + return { + ...theme.typography.caption, + color: (theme.vars || theme).palette.text.primary, + lineHeight: '100%', + display: 'flex', + flexDirection: ownerState.direction === 'vertical' ? 'column' : 'row', + flexShrink: 0, + gap: theme.spacing(0.5), + listStyleType: 'none', + paddingInlineStart: 0, + marginBlock: theme.spacing(1), + marginInline: theme.spacing(1), + width: 'max-content', + [`button.${piecewiseColorLegendClasses.item}`]: { + // Reset button styles + background: 'none', + border: 'none', + padding: 0, + cursor: ownerState.onItemClick ? 'pointer' : 'unset', + fontFamily: 'inherit', + fontWeight: 'inherit', + fontSize: 'inherit', + letterSpacing: 'inherit', + color: 'inherit', + }, + [`.${piecewiseColorLegendClasses.item}`]: { + display: 'flex', + gap: theme.spacing(0.5), + }, + [`li :not(.${piecewiseColorLegendClasses.minLabel}, .${piecewiseColorLegendClasses.maxLabel}) .${piecewiseColorLegendClasses?.mark}`]: + { + alignSelf: 'center', + }, + [`&.${piecewiseColorLegendClasses.start}`]: { + alignItems: 'end', + }, + [`&.${piecewiseColorLegendClasses.end}`]: { + alignItems: 'start', + }, + [`&.${piecewiseColorLegendClasses.horizontal}`]: { + alignItems: 'center', + [`.${piecewiseColorLegendClasses.item}`]: { + flexDirection: 'column', + }, + [`&.${piecewiseColorLegendClasses.start}`]: { + alignItems: 'end', + }, + [`&.${piecewiseColorLegendClasses.end}`]: { + alignItems: 'start', + }, + [`.${piecewiseColorLegendClasses.minLabel}`]: { + alignItems: 'end', + }, + [`&.${piecewiseColorLegendClasses.extremes}`]: { + [`.${piecewiseColorLegendClasses.minLabel}, .${piecewiseColorLegendClasses.maxLabel}`]: { + alignItems: 'center', + display: 'flex', + flexDirection: 'row', + }, + }, + }, + [`&.${piecewiseColorLegendClasses.vertical}`]: { + [`.${piecewiseColorLegendClasses.item}`]: { + flexDirection: 'row', + alignItems: 'center', + }, + [`&.${piecewiseColorLegendClasses.start}`]: { + alignItems: 'end', + }, + [`&.${piecewiseColorLegendClasses.end}`]: { + alignItems: 'start', + }, + [`&.${piecewiseColorLegendClasses.extremes}`]: { + alignItems: 'center', + [`.${piecewiseColorLegendClasses.minLabel}, .${piecewiseColorLegendClasses.maxLabel}`]: { + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + }, + }, + }, + }; +}); + +const PiecewiseColorLegend = consumeThemeProps( + 'MuiPiecewiseColorLegend', + { + defaultProps: { + direction: 'horizontal', + labelPosition: 'extremes', + labelFormatter: piecewiseColorDefaultLabelFormatter, + }, + classesResolver: useUtilityClasses, + }, + function PiecewiseColorLegend( + props: PiecewiseColorLegendProps, + ref: React.Ref, + ) { + const { + direction, + classes, + className, + markType, + labelPosition, + axisDirection, + axisId, + labelFormatter, + onItemClick, + ...other + } = props; + + const isVertical = direction === 'vertical'; + const isReverse = isVertical; + const axisItem = useAxis({ axisDirection, axisId }); + + const colorMap = axisItem?.colorMap; + if (!colorMap || !colorMap.type || colorMap.type !== 'piecewise') { + return null; + } + const valueFormatter = (v: number | Date) => + (axisItem as AxisDefaultized).valueFormatter?.(v, { location: 'legend' }) ?? + v.toLocaleString(); + + const formattedLabels = colorMap.thresholds.map(valueFormatter); + + const startClass = isReverse ? classes?.maxLabel : classes?.minLabel; + const endClass = isReverse ? classes?.minLabel : classes?.maxLabel; + + const colors = colorMap.colors.map((color, colorIndex) => ({ + color, + colorIndex, + })); + const orderedColors = isReverse ? colors.reverse() : colors; + + const isStart = labelPosition === 'start'; + const isEnd = labelPosition === 'end'; + const isExtremes = labelPosition === 'extremes'; + + return ( + + {orderedColors.map(({ color, colorIndex }, index) => { + const isFirst = index === 0; + const isLast = index === colorMap.colors.length - 1; + const isFirstColor = colorIndex === 0; + const isLastColor = colorIndex === colorMap.colors.length - 1; + + const data = { + index: colorIndex, + length: formattedLabels.length, + ...(isFirstColor + ? { min: null, formattedMin: null } + : { + min: colorMap.thresholds[colorIndex - 1], + formattedMin: formattedLabels[colorIndex - 1], + }), + ...(isLastColor + ? { max: null, formattedMax: null } + : { + max: colorMap.thresholds[colorIndex], + formattedMax: formattedLabels[colorIndex], + }), + }; + + const label = labelFormatter?.(data); + + if (label === null || label === undefined) { + return null; + } + + const isTextBefore = isStart || (isExtremes && isFirst); + const isTextAfter = isEnd || (isExtremes && isLast); + + const clickObject = { + type: 'piecewiseColor', + color, + label, + minValue: data.min, + maxValue: data.max, + } as const; + + const Element = onItemClick ? 'button' : 'div'; + + return ( +
  • + onItemClick(event, clickObject, index) : undefined + } + className={clsx(classes?.item, { + [`${startClass}`]: index === 0, + [`${endClass}`]: index === orderedColors.length - 1, + })} + > + {isTextBefore && {label}} + + {isTextAfter && {label}} + +
  • + ); + })} +
    + ); + }, +); PiecewiseColorLegend.propTypes = { // ----------------------------- Warning -------------------------------- @@ -147,80 +295,40 @@ PiecewiseColorLegend.propTypes = { * Override or extend the styles applied to the component. */ classes: PropTypes.object, + className: PropTypes.string, /** * The direction of the legend layout. - * The default depends on the chart. + * @default 'horizontal' */ - direction: PropTypes.oneOf(['column', 'row']).isRequired, - /** - * Hide the first item of the legend, corresponding to the [-infinity, min] piece. - * @default false - */ - hideFirst: PropTypes.bool, - /** - * Hide the last item of the legend, corresponding to the [max, +infinity] piece. - * @default false - */ - hideLast: PropTypes.bool, - /** - * Space between two legend items (in px). - * @default 10 - */ - itemGap: PropTypes.number, - /** - * Height of the item mark (in px). - * @default 20 - */ - itemMarkHeight: PropTypes.number, - /** - * Width of the item mark (in px). - * @default 20 - */ - itemMarkWidth: PropTypes.number, + direction: PropTypes.oneOf(['horizontal', 'vertical']), /** * Format the legend labels. * @param {PiecewiseLabelFormatterParams} params The bound of the piece to format. - * @returns {string|null} The displayed label, or `null` to skip the item. + * @returns {string|null} The displayed label, `''` to skip the label but show the color mark, or `null` to skip it entirely. */ labelFormatter: PropTypes.func, /** - * Style applied to legend labels. - * @default theme.typography.subtitle1 + * Where to position the labels relative to the gradient. + * @default 'extremes' */ - labelStyle: PropTypes.object, + labelPosition: PropTypes.oneOf(['start', 'end', 'extremes']), /** - * Space between the mark and the label (in px). - * @default 5 + * The type of the mark. + * @default 'square' */ - markGap: PropTypes.number, + markType: PropTypes.oneOf(['square', 'circle', 'line']), /** * Callback fired when a legend item is clicked. - * @param {React.MouseEvent} event The click event. + * @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. - * @default 10 - */ - padding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - top: PropTypes.number, - }), + sx: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), + PropTypes.func, + PropTypes.object, ]), - /** - * The position of the legend. - */ - position: PropTypes.shape({ - horizontal: PropTypes.oneOf(['left', 'middle', 'right']).isRequired, - vertical: PropTypes.oneOf(['bottom', 'middle', 'top']).isRequired, - }).isRequired, } as any; export { PiecewiseColorLegend }; diff --git a/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts b/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts index a898c70aefbc6..3305069e26fb1 100644 --- a/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts +++ b/packages/x-charts/src/ChartsLegend/chartsLegend.types.ts @@ -1,81 +1,34 @@ -import { ChartsTextStyle } from '../ChartsText'; -import { PieItemId } from '../models'; -import { SeriesId } from '../models/seriesType/common'; +import type { ChartsLegendProps } from './ChartsLegend'; +import { ContinuousColorLegendProps } from './ContinuousColorLegend'; +import { ChartsLegendPosition } from './legend.types'; +import { PiecewiseColorLegendProps } from './PiecewiseColorLegend'; -interface LegendItemContextBase { +export interface ChartsLegendSlots { /** - * The color used in the legend + * Custom rendering of the legend. + * @default ChartsLegend */ - color: string; - /** - * The label displayed in the legend - */ - label: string; + legend?: + | React.JSXElementConstructor + | React.JSXElementConstructor + | React.JSXElementConstructor; } -export interface LegendItemParams - extends Partial>, - Partial>, - LegendItemContextBase { - /** - * The identifier of the legend element. - * Used for internal purpose such as `key` props - */ - 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 ChartsLegendSlotProps { + legend?: Partial & + // We allow position only on slots. + ChartsLegendPosition; } -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'; +export interface ChartsLegendSlotExtension { /** - * The minimum value of the category + * Overridable component slots. + * @default {} */ - minValue: number | Date | null; + slots?: ChartsLegendSlots; /** - * The maximum value of the category + * The props used for each component slot. + * @default {} */ - maxValue: number | Date | null; + slotProps?: ChartsLegendSlotProps; } - -export type LegendItemContext = SeriesLegendItemContext | PiecewiseColorLegendItemContext; - -export interface LegendItemWithPosition extends LegendItemParams { - positionX: number; - positionY: number; - innerHeight: number; - innerWidth: number; - outerHeight: number; - outerWidth: number; - rowIndex: number; -} - -export type GetItemSpaceType = ( - label: string, - inStyle?: ChartsTextStyle, -) => { - innerWidth: number; - innerHeight: number; - outerWidth: number; - outerHeight: number; -}; diff --git a/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts b/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts index 7539c5caf6ac0..c36d3ac80a1ed 100644 --- a/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts +++ b/packages/x-charts/src/ChartsLegend/chartsLegendClasses.ts @@ -1,35 +1,45 @@ import generateUtilityClass from '@mui/utils/generateUtilityClass'; import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; +import composeClasses from '@mui/utils/composeClasses'; +import type { ChartsLegendProps } from './ChartsLegend'; +import type { ChartsLegendSlotExtension } from './chartsLegend.types'; export interface ChartsLegendClasses { /** Styles applied to the root element. */ 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. */ label: string; - /** Styles applied to the legend with column layout. */ - column: string; - /** Styles applied to the legend with row layout. */ - row: string; + /** Styles applied to the legend in column layout. */ + vertical: string; + /** Styles applied to the legend in row layout. */ + horizontal: string; } -export type ChartsLegendClassKey = keyof ChartsLegendClasses; - -export function getLegendUtilityClass(slot: string) { +function getLegendUtilityClass(slot: string) { return generateUtilityClass('MuiChartsLegend', slot); } +export const useUtilityClasses = (props: ChartsLegendProps & ChartsLegendSlotExtension) => { + const { classes, direction } = props; + const slots = { + root: ['root', direction], + mark: ['mark'], + label: ['label'], + series: ['series'], + }; + + return composeClasses(slots, getLegendUtilityClass, classes); +}; + export const legendClasses: ChartsLegendClasses = generateUtilityClasses('MuiChartsLegend', [ 'root', 'series', - 'itemBackground', 'mark', 'label', - 'column', - 'row', + 'vertical', + 'horizontal', ]); diff --git a/packages/x-charts/src/ChartsLegend/colorLegend.types.ts b/packages/x-charts/src/ChartsLegend/colorLegend.types.ts new file mode 100644 index 0000000000000..7b0fdb1655b25 --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/colorLegend.types.ts @@ -0,0 +1,14 @@ +import { AxisId } from '../models/axis'; + +export interface ColorLegendSelector { + /** + * The axis direction containing the color configuration to represent. + * @default 'z' + */ + axisDirection?: 'x' | 'y' | 'z'; + /** + * The id of the axis item with the color configuration to represent. + * @default The first axis item. + */ + axisId?: AxisId; +} diff --git a/packages/x-charts/src/ChartsLegend/continuousColorLegendClasses.ts b/packages/x-charts/src/ChartsLegend/continuousColorLegendClasses.ts new file mode 100644 index 0000000000000..41ed0222e1749 --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/continuousColorLegendClasses.ts @@ -0,0 +1,64 @@ +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; +import composeClasses from '@mui/utils/composeClasses'; +import type { ContinuousColorLegendProps } from './ContinuousColorLegend'; +import type { ChartsLegendSlotExtension } from './chartsLegend.types'; + +export interface ContinuousColorLegendClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the list item that renders the `minLabel`. */ + minLabel: string; + /** Styles applied to the list item that renders the `maxLabel`. */ + maxLabel: string; + /** Styles applied to the list item with the gradient. */ + gradient: string; + /** Styles applied to the legend in column layout. */ + vertical: string; + /** Styles applied to the legend in row layout. */ + horizontal: string; + /** Styles applied to the legend with the labels before the gradient. */ + start: string; + /** Styles applied to the legend with the labels after the gradient. */ + end: string; + /** Styles applied to the legend with the labels on the extremes of the gradient. */ + extremes: string; + /** Styles applied to the series label. */ + label: string; +} + +function getLegendUtilityClass(slot: string) { + return generateUtilityClass('MuiContinuousColorLegend', slot); +} + +export const useUtilityClasses = ( + props: ContinuousColorLegendProps & ChartsLegendSlotExtension, +) => { + const { classes, direction, labelPosition } = props; + const slots = { + root: ['root', direction, labelPosition], + minLabel: ['minLabel'], + maxLabel: ['maxLabel'], + gradient: ['gradient'], + mark: ['mark'], + label: ['label'], + }; + + return composeClasses(slots, getLegendUtilityClass, classes); +}; + +export const continuousColorLegendClasses: ContinuousColorLegendClasses = generateUtilityClasses( + 'MuiContinuousColorLegend', + [ + 'root', + 'minLabel', + 'maxLabel', + 'gradient', + 'vertical', + 'horizontal', + 'start', + 'end', + 'extremes', + 'label', + ], +); diff --git a/packages/x-charts/src/ChartsLegend/direction.ts b/packages/x-charts/src/ChartsLegend/direction.ts new file mode 100644 index 0000000000000..e52780972852f --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/direction.ts @@ -0,0 +1 @@ +export type Direction = 'vertical' | 'horizontal'; diff --git a/packages/x-charts/src/ChartsLegend/index.ts b/packages/x-charts/src/ChartsLegend/index.ts index 24c38e1b8e17d..f8a0057266c23 100644 --- a/packages/x-charts/src/ChartsLegend/index.ts +++ b/packages/x-charts/src/ChartsLegend/index.ts @@ -1,7 +1,16 @@ export * from './ChartsLegend'; -export * from './DefaultChartsLegend'; +export * from './chartsLegend.types'; +export * from './direction'; +export * from './legendContext.types'; +export { legendClasses } from './chartsLegendClasses'; +export type { ChartsLegendClasses } from './chartsLegendClasses'; export * from './ContinuousColorLegend'; +export * from './colorLegend.types'; +export { continuousColorLegendClasses } from './continuousColorLegendClasses'; +export type { ContinuousColorLegendClasses } from './continuousColorLegendClasses'; export * from './PiecewiseColorLegend'; -export * from './chartsLegendClasses'; - -export * from './utils'; +export { piecewiseColorLegendClasses } from './piecewiseColorLegendClasses'; +export type { PiecewiseColorLegendClasses } from './piecewiseColorLegendClasses'; +export { piecewiseColorDefaultLabelFormatter } from './piecewiseColorDefaultLabelFormatter'; +export type { PiecewiseLabelFormatterParams } from './piecewiseColorLegend.types'; +export * from './legend.types'; diff --git a/packages/x-charts/src/ChartsLegend/legend.types.ts b/packages/x-charts/src/ChartsLegend/legend.types.ts index 84fa17530d739..fc180fae2013f 100644 --- a/packages/x-charts/src/ChartsLegend/legend.types.ts +++ b/packages/x-charts/src/ChartsLegend/legend.types.ts @@ -1,67 +1,19 @@ -import { ChartsTextBaseline, ChartsTextStyle } from '../internals/getWordsByLines'; -import { AxisId } from '../models/axis'; - -export type AnchorX = 'left' | 'right' | 'middle'; -export type AnchorY = 'top' | 'bottom' | 'middle'; - -export type AnchorPosition = { horizontal: AnchorX; vertical: AnchorY }; - -export type Direction = 'row' | 'column'; - -export interface ColorLegendSelector { - /** - * The axis direction containing the color configuration to represent. - * @default 'z' - */ - axisDirection?: 'x' | 'y' | 'z'; - /** - * The id of the axis item with the color configuration to represent. - * @default The first axis item. - */ - axisId?: AxisId; -} - -export interface LegendPlacement { +export type LegendPosition = { /** - * The position of the legend. + * The vertical position of the legend. */ - position?: AnchorPosition; + vertical?: 'top' | 'middle' | 'bottom'; /** - * The direction of the legend layout. - * The default depends on the chart. + * The horizontal position of the legend. */ - direction?: Direction; -} - -export type BoundingBox = { - width: number; - height: number; + horizontal?: 'left' | 'middle' | 'right'; }; -export interface Position { - x: number; - y: number; -} -export interface TextPosition extends Position { - dominantBaseline: ChartsTextBaseline; - textAnchor: ChartsTextStyle['textAnchor']; -} - -export type PiecewiseLabelFormatterParams = { - /** - * The min value of the piece. `null` is infinite. - */ - min: number | Date | null; - /** - * The max value of the piece. `null` is infinite. - */ - max: number | Date | null; - /** - * The formatted min value of the piece. `null` is infinite. - */ - formattedMin: string | null; +export type ChartsLegendPosition = { /** - * The formatted max value of the piece. `null` is infinite. + * The position of the legend in relation to the chart. + * This property is only passed to the Chart components, e.g. `ScatterChart`, and not the slots themselves. + * If customization is needed, simply use the composition pattern. */ - formattedMax: string | null; + position?: LegendPosition; }; diff --git a/packages/x-charts/src/ChartsLegend/legendContext.types.ts b/packages/x-charts/src/ChartsLegend/legendContext.types.ts new file mode 100644 index 0000000000000..7d8612ad5161f --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/legendContext.types.ts @@ -0,0 +1,62 @@ +import { ChartsLabelMarkProps } from '../ChartsLabel/ChartsLabelMark'; +import { PieItemId } from '../models'; +import { SeriesId } from '../models/seriesType/common'; + +interface LegendItemContextBase { + /** + * The color used in the legend + */ + color: string; + /** + * 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 + */ + id: number | string; + markType: ChartsLabelMarkProps['type']; +} + +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; diff --git a/packages/x-charts/src/ChartsLegend/legendItemsPlacement.ts b/packages/x-charts/src/ChartsLegend/legendItemsPlacement.ts deleted file mode 100644 index f482e3b9a244c..0000000000000 --- a/packages/x-charts/src/ChartsLegend/legendItemsPlacement.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ChartsTextStyle } from '../ChartsText'; -import { GetItemSpaceType, LegendItemParams, LegendItemWithPosition } from './chartsLegend.types'; - -export function legendItemPlacements( - itemsToDisplay: LegendItemParams[], - getItemSpace: GetItemSpaceType, - labelStyle: ChartsTextStyle, - direction: string, - availableWidth: number, - availableHeight: number, - itemGap: number, -): [LegendItemWithPosition[], number, number] { - // Start at 0, 0. Will be modified later by padding and position. - let x = 0; - let y = 0; - - // total values used to align legend later. - let totalWidthUsed = 0; - let totalHeightUsed = 0; - let rowIndex = 0; - const rowMaxHeight = [0]; - - const seriesWithRawPosition = itemsToDisplay.map(({ label, ...other }) => { - const itemSpace = getItemSpace(label, labelStyle); - const rep = { - ...other, - label, - positionX: x, - positionY: y, - innerHeight: itemSpace.innerHeight, - innerWidth: itemSpace.innerWidth, - outerHeight: itemSpace.outerHeight, - outerWidth: itemSpace.outerWidth, - rowIndex, - }; - - if (direction === 'row') { - if (x + itemSpace.innerWidth > availableWidth) { - // This legend item would create overflow along the x-axis, so we start a new row. - x = 0; - y += rowMaxHeight[rowIndex]; - rowIndex += 1; - if (rowMaxHeight.length <= rowIndex) { - rowMaxHeight.push(0); - } - rep.positionX = x; - rep.positionY = y; - rep.rowIndex = rowIndex; - } - totalWidthUsed = Math.max(totalWidthUsed, x + itemSpace.outerWidth); - totalHeightUsed = Math.max(totalHeightUsed, y + itemSpace.outerHeight); - rowMaxHeight[rowIndex] = Math.max(rowMaxHeight[rowIndex], itemSpace.outerHeight); - - x += itemSpace.outerWidth; - } - - if (direction === 'column') { - if (y + itemSpace.innerHeight > availableHeight) { - // This legend item would create overflow along the y-axis, so we start a new column. - x = totalWidthUsed + itemGap; - y = 0; - rowIndex = 0; - rep.positionX = x; - rep.positionY = y; - rep.rowIndex = rowIndex; - } - if (rowMaxHeight.length <= rowIndex) { - rowMaxHeight.push(0); - } - totalWidthUsed = Math.max(totalWidthUsed, x + itemSpace.outerWidth); - totalHeightUsed = Math.max(totalHeightUsed, y + itemSpace.outerHeight); - - rowIndex += 1; - y += itemSpace.outerHeight; - } - - return rep; - }); - - return [ - seriesWithRawPosition.map((item) => ({ - ...item, - positionY: - item.positionY + - (direction === 'row' - ? rowMaxHeight[item.rowIndex] / 2 // Get the center of the entire row - : item.outerHeight / 2), // Get the center of the item - })), - totalWidthUsed, - totalHeightUsed, - ]; -} diff --git a/packages/x-charts/src/ChartsLegend/onClickContextBuilder.ts b/packages/x-charts/src/ChartsLegend/onClickContextBuilder.ts new file mode 100644 index 0000000000000..09cd72ce4f54c --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/onClickContextBuilder.ts @@ -0,0 +1,10 @@ +import { LegendItemParams, SeriesLegendItemContext } from './legendContext.types'; + +export const seriesContextBuilder = (context: LegendItemParams): SeriesLegendItemContext => + ({ + type: 'series', + color: context.color, + label: context.label, + seriesId: context.seriesId!, + itemId: context.itemId, + }) as const; diff --git a/packages/x-charts/src/ChartsLegend/piecewiseColorDefaultLabelFormatter.ts b/packages/x-charts/src/ChartsLegend/piecewiseColorDefaultLabelFormatter.ts new file mode 100644 index 0000000000000..61a852dec16c0 --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/piecewiseColorDefaultLabelFormatter.ts @@ -0,0 +1,11 @@ +import { PiecewiseLabelFormatterParams } from './piecewiseColorLegend.types'; + +export function piecewiseColorDefaultLabelFormatter(params: PiecewiseLabelFormatterParams) { + if (params.min === null) { + return `<${params.formattedMax}`; + } + if (params.max === null) { + return `>${params.formattedMin}`; + } + return `${params.formattedMin}-${params.formattedMax}`; +} diff --git a/packages/x-charts/src/ChartsLegend/piecewiseColorLegend.types.ts b/packages/x-charts/src/ChartsLegend/piecewiseColorLegend.types.ts new file mode 100644 index 0000000000000..ee935dde1278f --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/piecewiseColorLegend.types.ts @@ -0,0 +1,26 @@ +export type PiecewiseLabelFormatterParams = { + /** + * The index of the entry. + */ + index: number; + /** + * The length of the entries array. + */ + length: number; + /** + * The min value of the piece. `null` is infinite. + */ + min: number | Date | null; + /** + * The max value of the piece. `null` is infinite. + */ + max: number | Date | null; + /** + * The formatted min value of the piece. `null` is infinite. + */ + formattedMin: string | null; + /** + * The formatted max value of the piece. `null` is infinite. + */ + formattedMax: string | null; +}; diff --git a/packages/x-charts/src/ChartsLegend/piecewiseColorLegendClasses.ts b/packages/x-charts/src/ChartsLegend/piecewiseColorLegendClasses.ts new file mode 100644 index 0000000000000..b5b55527633b9 --- /dev/null +++ b/packages/x-charts/src/ChartsLegend/piecewiseColorLegendClasses.ts @@ -0,0 +1,65 @@ +import generateUtilityClass from '@mui/utils/generateUtilityClass'; +import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; +import composeClasses from '@mui/utils/composeClasses'; +import type { PiecewiseColorLegendProps } from './PiecewiseColorLegend'; +import type { ChartsLegendSlotExtension } from './chartsLegend.types'; + +export interface PiecewiseColorLegendClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the list item that renders the `minLabel`. */ + minLabel: string; + /** Styles applied to the list item that renders the `maxLabel`. */ + maxLabel: string; + /** Styles applied to the list items. */ + item: string; + /** Styles applied to the legend in column layout. */ + vertical: string; + /** Styles applied to the legend in row layout. */ + horizontal: string; + /** Styles applied to the legend with the labels before the color marks. */ + start: string; + /** Styles applied to the legend with the labels after the color marks. */ + end: string; + /** Styles applied to the legend with the labels on the extremes of the color marks. */ + extremes: string; + /** Styles applied to the marks. */ + mark: string; + /** Styles applied to the series label. */ + label: string; +} + +function getLegendUtilityClass(slot: string) { + return generateUtilityClass('MuiPiecewiseColorLegendClasses', slot); +} + +export const useUtilityClasses = (props: PiecewiseColorLegendProps & ChartsLegendSlotExtension) => { + const { classes, direction, labelPosition } = props; + const slots = { + root: ['root', direction, labelPosition], + minLabel: ['minLabel'], + maxLabel: ['maxLabel'], + item: ['item'], + mark: ['mark'], + label: ['label'], + }; + + return composeClasses(slots, getLegendUtilityClass, classes); +}; + +export const piecewiseColorLegendClasses: PiecewiseColorLegendClasses = generateUtilityClasses( + 'MuiPiecewiseColorLegendClasses', + [ + 'root', + 'minLabel', + 'maxLabel', + 'item', + 'vertical', + 'horizontal', + 'start', + 'end', + 'extremes', + 'mark', + 'label', + ], +); diff --git a/packages/x-charts/src/ChartsLegend/useAxis.ts b/packages/x-charts/src/ChartsLegend/useAxis.ts index 4f80ddd8f3e1a..7cba0d0069377 100644 --- a/packages/x-charts/src/ChartsLegend/useAxis.ts +++ b/packages/x-charts/src/ChartsLegend/useAxis.ts @@ -3,8 +3,8 @@ import { AxisDefaultized } from '../models/axis'; import { useCartesianContext } from '../context/CartesianProvider/useCartesianContext'; import { ZAxisDefaultized } from '../models/z-axis'; -import { ColorLegendSelector } from './legend.types'; import { useZAxis } from '../hooks/useZAxis'; +import { ColorLegendSelector } from './colorLegend.types'; /** * Helper to select an axis definition according to its direction and id. diff --git a/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx b/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx index 2403575a9b649..207a159aebc9f 100644 --- a/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx +++ b/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx @@ -31,7 +31,6 @@ const ChartsSurfaceStyles = styled('svg', { height: ownerState.height ?? '100%', display: 'flex', position: 'relative', - flexGrow: 1, flexDirection: 'column', alignItems: 'center', justifyContent: 'center', diff --git a/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx b/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx index 2015a303c3dc6..ad25838c72d63 100644 --- a/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx +++ b/packages/x-charts/src/ChartsTooltip/contentDisplayed.test.tsx @@ -1,15 +1,16 @@ import * as React from 'react'; import { expect } from 'chai'; import { createRenderer, fireEvent } from '@mui/internal-test-utils'; -import { BarChart } from '@mui/x-charts/BarChart'; +import { BarChart, BarChartProps } from '@mui/x-charts/BarChart'; import { describeSkipIf, isJSDOM } from 'test/utils/skipIf'; -const config = { +const config: Partial = { dataset: [ { x: 'A', v1: 4, v2: 2 }, { x: 'B', v1: 1, v2: 1 }, ], margin: { top: 0, left: 0, bottom: 0, right: 0 }, + hideLegend: true, width: 400, height: 400, }; diff --git a/packages/x-charts/src/LineChart/LineChart.test.tsx b/packages/x-charts/src/LineChart/LineChart.test.tsx index 1ec0c3f8ac2a5..441d32c31e96c 100644 --- a/packages/x-charts/src/LineChart/LineChart.test.tsx +++ b/packages/x-charts/src/LineChart/LineChart.test.tsx @@ -23,6 +23,7 @@ describe('', () => { 'themeStyleOverrides', 'themeVariants', 'themeCustomPalette', + 'themeDefaultProps', ], }), ); diff --git a/packages/x-charts/src/LineChart/LineChart.tsx b/packages/x-charts/src/LineChart/LineChart.tsx index 80fa8dc686671..b6887f8937299 100644 --- a/packages/x-charts/src/LineChart/LineChart.tsx +++ b/packages/x-charts/src/LineChart/LineChart.tsx @@ -5,7 +5,7 @@ import { useThemeProps } from '@mui/material/styles'; import { MakeOptional } from '@mui/x-internals/types'; import { AreaPlot, AreaPlotProps, AreaPlotSlotProps, AreaPlotSlots } from './AreaPlot'; import { LinePlot, LinePlotProps, LinePlotSlotProps, LinePlotSlots } from './LinePlot'; -import { ChartContainer, ChartContainerProps } from '../ChartContainer'; +import { ChartContainerProps } from '../ChartContainer'; import { MarkPlot, MarkPlotProps, MarkPlotSlotProps, MarkPlotSlots } from './MarkPlot'; import { ChartsAxis, ChartsAxisProps } from '../ChartsAxis/ChartsAxis'; import { LineSeriesType } from '../models/seriesType/line'; @@ -32,6 +32,10 @@ import { ChartsOverlaySlots, } from '../ChartsOverlay'; import { useLineChartProps } from './useLineChartProps'; +import { useChartContainerProps } from '../ChartContainer/useChartContainerProps'; +import { ChartDataProvider } from '../context'; +import { ChartsSurface } from '../ChartsSurface'; +import { ChartsWrapper } from '../internals/components/ChartsWrapper'; export interface LineChartSlots extends ChartsAxisSlots, @@ -129,6 +133,7 @@ const LineChart = React.forwardRef(function LineChart( ) { const props = useThemeProps({ props: inProps, name: 'MuiLineChart' }); const { + chartsWrapperProps, chartContainerProps, axisClickHandlerProps, gridProps, @@ -144,30 +149,38 @@ const LineChart = React.forwardRef(function LineChart( legendProps, children, } = useLineChartProps(props); + const { chartDataProviderProps, chartsSurfaceProps } = useChartContainerProps( + chartContainerProps, + ref, + ); const Tooltip = props.slots?.tooltip ?? ChartsTooltip; return ( - - {props.onAxisClick && } - - - - - - - - - - {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} - - - - {!props.hideLegend && } - {!props.loading && } - - {children} - + + + {!props.hideLegend && } + + {props.onAxisClick && } + + + + + + + + + + {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} + + + + {!props.loading && } + + {children} + + + ); }); diff --git a/packages/x-charts/src/LineChart/legend.ts b/packages/x-charts/src/LineChart/legend.ts index 28d3ae4165461..0dcd25e97ae22 100644 --- a/packages/x-charts/src/LineChart/legend.ts +++ b/packages/x-charts/src/LineChart/legend.ts @@ -1,4 +1,4 @@ -import { LegendItemParams } from '../ChartsLegend/chartsLegend.types'; +import type { LegendItemParams } from '../ChartsLegend'; import { getLabel } from '../internals/getLabel'; import { LegendGetter } from '../context/PluginProvider'; @@ -12,6 +12,7 @@ const legendGetter: LegendGetter<'line'> = (params) => { } acc.push({ + markType: series[seriesId].labelMarkType ?? 'line', id: seriesId, seriesId, color: series[seriesId].color, diff --git a/packages/x-charts/src/LineChart/useLineChartProps.ts b/packages/x-charts/src/LineChart/useLineChartProps.ts index 05cef6fc627bc..09f084b96b962 100644 --- a/packages/x-charts/src/LineChart/useLineChartProps.ts +++ b/packages/x-charts/src/LineChart/useLineChartProps.ts @@ -4,7 +4,7 @@ import { ChartsAxisProps } from '../ChartsAxis'; import { ChartsAxisHighlightProps } from '../ChartsAxisHighlight'; import { ChartsClipPathProps } from '../ChartsClipPath'; import { ChartsGridProps } from '../ChartsGrid'; -import { ChartsLegendProps } from '../ChartsLegend'; +import { ChartsLegendSlotExtension } from '../ChartsLegend'; import { ChartsOnAxisClickHandlerProps } from '../ChartsOnAxisClickHandler'; import { ChartsOverlayProps } from '../ChartsOverlay'; import { DEFAULT_X_AXIS_KEY } from '../constants'; @@ -14,6 +14,8 @@ import type { LineChartProps } from './LineChart'; import { LineHighlightPlotProps } from './LineHighlightPlot'; import { LinePlotProps } from './LinePlot'; import { MarkPlotProps } from './MarkPlot'; +import type { ChartsWrapperProps } from '../internals/components/ChartsWrapper'; +import { calculateMargins } from '../internals/calculateMargins'; /** * A helper function that extracts LineChartProps from the input props @@ -69,7 +71,7 @@ export const useLineChartProps = (props: LineChartProps) => { })), width, height, - margin, + margin: calculateMargins({ margin, hideLegend, slotProps, series }), colors, dataset, xAxis: xAxis ?? [ @@ -83,7 +85,6 @@ export const useLineChartProps = (props: LineChartProps) => { }, ], yAxis, - sx, highlightedItem, onHighlightChange, disableAxisListener: @@ -157,12 +158,19 @@ export const useLineChartProps = (props: LineChartProps) => { slotProps, }; - const legendProps: ChartsLegendProps = { + const legendProps: ChartsLegendSlotExtension = { slots, slotProps, }; + const chartsWrapperProps: Omit = { + sx, + legendPosition: props.slotProps?.legend?.position, + legendDirection: props.slotProps?.legend?.direction, + }; + return { + chartsWrapperProps, chartContainerProps, axisClickHandlerProps, gridProps, diff --git a/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx b/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx index cd37e91b98813..a2adde4ea9e7f 100644 --- a/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx +++ b/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx @@ -206,6 +206,7 @@ PieArcLabelPlot.propTypes = { id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, index: PropTypes.number.isRequired, label: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + labelMarkType: PropTypes.oneOf(['circle', 'line', 'square']), padAngle: PropTypes.number.isRequired, startAngle: PropTypes.number.isRequired, value: PropTypes.number.isRequired, diff --git a/packages/x-charts/src/PieChart/PieArcPlot.tsx b/packages/x-charts/src/PieChart/PieArcPlot.tsx index a1cbd0e955f7e..2c43b0879966b 100644 --- a/packages/x-charts/src/PieChart/PieArcPlot.tsx +++ b/packages/x-charts/src/PieChart/PieArcPlot.tsx @@ -168,6 +168,7 @@ PieArcPlot.propTypes = { id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, index: PropTypes.number.isRequired, label: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + labelMarkType: PropTypes.oneOf(['circle', 'line', 'square']), padAngle: PropTypes.number.isRequired, startAngle: PropTypes.number.isRequired, value: PropTypes.number.isRequired, diff --git a/packages/x-charts/src/PieChart/PieChart.test.tsx b/packages/x-charts/src/PieChart/PieChart.test.tsx index d1d5c3fd55ff9..4eebddfa285cc 100644 --- a/packages/x-charts/src/PieChart/PieChart.test.tsx +++ b/packages/x-charts/src/PieChart/PieChart.test.tsx @@ -34,6 +34,7 @@ describe('', () => { 'themeStyleOverrides', 'themeVariants', 'themeCustomPalette', + 'themeDefaultProps', ], }), ); diff --git a/packages/x-charts/src/PieChart/PieChart.tsx b/packages/x-charts/src/PieChart/PieChart.tsx index 4af810a6ed04d..4a04bd20e7cb4 100644 --- a/packages/x-charts/src/PieChart/PieChart.tsx +++ b/packages/x-charts/src/PieChart/PieChart.tsx @@ -1,10 +1,9 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { useRtl } from '@mui/system/RtlProvider'; import { useThemeProps } from '@mui/material/styles'; import { MakeOptional } from '@mui/x-internals/types'; -import { ChartContainer, ChartContainerProps } from '../ChartContainer'; +import { ChartContainerProps } from '../ChartContainer'; import { PieSeriesType } from '../models/seriesType'; import { ChartsTooltip } from '../ChartsTooltip'; import { ChartsTooltipSlots, ChartsTooltipSlotProps } from '../ChartsTooltip/ChartTooltip.types'; @@ -17,6 +16,10 @@ import { ChartsOverlaySlotProps, ChartsOverlaySlots, } from '../ChartsOverlay'; +import { ChartsSurface } from '../ChartsSurface'; +import { ChartDataProvider } from '../context'; +import { useChartContainerProps } from '../ChartContainer/useChartContainerProps'; +import { ChartsWrapper } from '../internals/components/ChartsWrapper'; export interface PieChartSlots extends PiePlotSlots, @@ -59,8 +62,7 @@ export interface PieChartProps slotProps?: PieChartSlotProps; } -const defaultMargin = { top: 5, bottom: 5, left: 5, right: 100 }; -const defaultRTLMargin = { top: 5, bottom: 5, left: 100, right: 5 }; +const defaultMargin = { top: 5, bottom: 5, left: 5, right: 5 }; /** * Demos: @@ -98,40 +100,48 @@ const PieChart = React.forwardRef(function PieChart( className, ...other } = props; - const isRtl = useRtl(); + const margin = { ...defaultMargin, ...marginProps }; - const margin = { ...(isRtl ? defaultRTLMargin : defaultMargin), ...marginProps }; + const { chartDataProviderProps, chartsSurfaceProps } = useChartContainerProps( + { + ...other, + series: series.map((s) => ({ type: 'pie', ...s })), + width, + height, + margin, + colors, + disableAxisListener: true, + highlightedItem, + onHighlightChange, + className, + skipAnimation, + }, + ref, + ); const Tooltip = slots?.tooltip ?? ChartsTooltip; return ( - ({ type: 'pie', ...s }))} - width={width} - height={height} - margin={margin} - colors={colors} - sx={sx} - disableAxisListener - highlightedItem={highlightedItem} - onHighlightChange={onHighlightChange} - className={className} - skipAnimation={skipAnimation} - > - - - {!hideLegend && ( - - )} - {!loading && } - {children} - + + + {!hideLegend && ( + + )} + + + + {!loading && } + {children} + + + ); }); diff --git a/packages/x-charts/src/PieChart/legend.ts b/packages/x-charts/src/PieChart/legend.ts index 4d39cdd7235e7..89ed3254d4f6b 100644 --- a/packages/x-charts/src/PieChart/legend.ts +++ b/packages/x-charts/src/PieChart/legend.ts @@ -1,4 +1,4 @@ -import { LegendItemParams } from '../ChartsLegend/chartsLegend.types'; +import type { LegendItemParams } from '../ChartsLegend'; import { getLabel } from '../internals/getLabel'; import { LegendGetter } from '../context/PluginProvider'; @@ -13,6 +13,7 @@ const legendGetter: LegendGetter<'pie'> = (params) => { } acc.push({ + markType: item.labelMarkType ?? series[seriesId].labelMarkType ?? 'circle', id: item.id, seriesId, color: item.color, diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.test.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.test.tsx index 0648c513feb83..4528f7f1cb479 100644 --- a/packages/x-charts/src/ScatterChart/ScatterChart.test.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterChart.test.tsx @@ -37,6 +37,7 @@ describe('', () => { 'themeStyleOverrides', 'themeVariants', 'themeCustomPalette', + 'themeDefaultProps', ], }), ); diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.tsx index 65d5e8bc460ee..553a948d17370 100644 --- a/packages/x-charts/src/ScatterChart/ScatterChart.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterChart.tsx @@ -9,7 +9,7 @@ import { ScatterPlotSlotProps, ScatterPlotSlots, } from './ScatterPlot'; -import { ChartContainer, ChartContainerProps } from '../ChartContainer'; +import { ChartContainerProps } from '../ChartContainer'; import { ChartsAxis, ChartsAxisProps } from '../ChartsAxis'; import { ScatterSeriesType } from '../models/seriesType/scatter'; import { ChartsTooltip } from '../ChartsTooltip'; @@ -30,6 +30,10 @@ import { import { ChartsGrid, ChartsGridProps } from '../ChartsGrid'; import { ZAxisContextProvider, ZAxisContextProviderProps } from '../context/ZAxisContextProvider'; import { useScatterChartProps } from './useScatterChartProps'; +import { useChartContainerProps } from '../ChartContainer/useChartContainerProps'; +import { ChartDataProvider } from '../context'; +import { ChartsSurface } from '../ChartsSurface'; +import { ChartsWrapper } from '../internals/components/ChartsWrapper'; export interface ScatterChartSlots extends ChartsAxisSlots, @@ -108,6 +112,7 @@ const ScatterChart = React.forwardRef(function ScatterChart( ) { const props = useThemeProps({ props: inProps, name: 'MuiScatterChart' }); const { + chartsWrapperProps, chartContainerProps, zAxisProps, voronoiHandlerProps, @@ -119,26 +124,34 @@ const ScatterChart = React.forwardRef(function ScatterChart( axisHighlightProps, children, } = useScatterChartProps(props); + const { chartDataProviderProps, chartsSurfaceProps } = useChartContainerProps( + chartContainerProps, + ref, + ); const Tooltip = props.slots?.tooltip ?? ChartsTooltip; return ( - - - {!props.disableVoronoi && } - - - - {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} - - - + + {!props.hideLegend && } - - {!props.loading && } - {children} - - + + + {!props.disableVoronoi && } + + + + {/* The `data-drawing-container` indicates that children are part of the drawing area. Ref: https://github.com/mui/mui-x/issues/13659 */} + + + + + {!props.loading && } + {children} + + + + ); }); diff --git a/packages/x-charts/src/ScatterChart/legend.ts b/packages/x-charts/src/ScatterChart/legend.ts index 6a113642caf2b..c08d7e9bd5a65 100644 --- a/packages/x-charts/src/ScatterChart/legend.ts +++ b/packages/x-charts/src/ScatterChart/legend.ts @@ -1,4 +1,4 @@ -import { LegendItemParams } from '../ChartsLegend/chartsLegend.types'; +import type { LegendItemParams } from '../ChartsLegend'; import { getLabel } from '../internals/getLabel'; import { LegendGetter } from '../context/PluginProvider'; @@ -12,6 +12,7 @@ const legendGetter: LegendGetter<'scatter'> = (params) => { } acc.push({ + markType: series[seriesId].labelMarkType ?? 'circle', id: seriesId, seriesId, color: series[seriesId].color, diff --git a/packages/x-charts/src/ScatterChart/useScatterChartProps.ts b/packages/x-charts/src/ScatterChart/useScatterChartProps.ts index 0399f0543a968..38f06b5ddd994 100644 --- a/packages/x-charts/src/ScatterChart/useScatterChartProps.ts +++ b/packages/x-charts/src/ScatterChart/useScatterChartProps.ts @@ -2,13 +2,15 @@ import { ChartsAxisProps } from '../ChartsAxis'; import { ChartsAxisHighlightProps } from '../ChartsAxisHighlight'; import { ChartsGridProps } from '../ChartsGrid'; -import { ChartsLegendProps } from '../ChartsLegend'; +import { ChartsLegendSlotExtension } from '../ChartsLegend'; import { ChartsOverlayProps } from '../ChartsOverlay'; import type { ChartsVoronoiHandlerProps } from '../ChartsVoronoiHandler'; import { ChartContainerProps } from '../ChartContainer'; import { ZAxisContextProviderProps } from '../context'; import type { ScatterChartProps } from './ScatterChart'; import type { ScatterPlotProps } from './ScatterPlot'; +import type { ChartsWrapperProps } from '../internals/components/ChartsWrapper'; +import { calculateMargins } from '../internals/calculateMargins'; /** * A helper function that extracts ScatterChartProps from the input props @@ -53,11 +55,10 @@ export const useScatterChartProps = (props: ScatterChartProps) => { series: series.map((s) => ({ type: 'scatter' as const, ...s })), width, height, - margin, + margin: calculateMargins({ margin, hideLegend, slotProps, series }), colors, xAxis, yAxis, - sx, highlightedItem, onHighlightChange, className, @@ -95,7 +96,7 @@ export const useScatterChartProps = (props: ScatterChartProps) => { slotProps, }; - const legendProps: ChartsLegendProps = { + const legendProps: ChartsLegendSlotExtension = { slots, slotProps, }; @@ -106,7 +107,14 @@ export const useScatterChartProps = (props: ScatterChartProps) => { ...axisHighlight, }; + const chartsWrapperProps: Omit = { + sx, + legendPosition: props.slotProps?.legend?.position, + legendDirection: props.slotProps?.legend?.direction, + }; + return { + chartsWrapperProps, chartContainerProps, zAxisProps, voronoiHandlerProps, diff --git a/packages/x-charts/src/constants/index.ts b/packages/x-charts/src/constants/index.ts index a198b2fa8f8ff..67382090061a5 100644 --- a/packages/x-charts/src/constants/index.ts +++ b/packages/x-charts/src/constants/index.ts @@ -6,3 +6,4 @@ export const DEFAULT_MARGINS = { left: 50, right: 50, }; +export const DEFAULT_LEGEND_FACING_MARGIN = 20; diff --git a/packages/x-charts/src/context/PluginProvider/SeriesFormatter.types.ts b/packages/x-charts/src/context/PluginProvider/SeriesFormatter.types.ts index 6462da81a53b8..7bf783b923f9d 100644 --- a/packages/x-charts/src/context/PluginProvider/SeriesFormatter.types.ts +++ b/packages/x-charts/src/context/PluginProvider/SeriesFormatter.types.ts @@ -6,7 +6,7 @@ import type { } from '../../models/seriesType/config'; import type { SeriesId } from '../../models/seriesType/common'; import type { StackingGroupsType } from '../../internals/stackSeries'; -import type { LegendItemParams } from '../../ChartsLegend/chartsLegend.types'; +import type { LegendItemParams } from '../../ChartsLegend'; export type SeriesFormatterParams = { series: Record; diff --git a/packages/x-charts/src/hooks/index.ts b/packages/x-charts/src/hooks/index.ts index 7f6e5dcdbdafe..9ba0336f9586c 100644 --- a/packages/x-charts/src/hooks/index.ts +++ b/packages/x-charts/src/hooks/index.ts @@ -11,3 +11,4 @@ export { useBarSeries as unstable_useBarSeries, useScatterSeries as unstable_useScatterSeries, } from './useSeries'; +export * from './useLegend'; diff --git a/packages/x-charts/src/ChartsLegend/utils.ts b/packages/x-charts/src/hooks/useLegend.ts similarity index 57% rename from packages/x-charts/src/ChartsLegend/utils.ts rename to packages/x-charts/src/hooks/useLegend.ts index 0e4e0bf973c85..362ace3361eea 100644 --- a/packages/x-charts/src/ChartsLegend/utils.ts +++ b/packages/x-charts/src/hooks/useLegend.ts @@ -1,3 +1,4 @@ +'use client'; import { FormattedSeries } from '../context/SeriesProvider'; import { ChartSeriesType } from '../models/seriesType/config'; import { LegendGetter } from '../context/PluginProvider'; @@ -6,6 +7,8 @@ import getBarLegend from '../BarChart/legend'; import getScatterLegend from '../ScatterChart/legend'; import getLineLegend from '../LineChart/legend'; import getPieLegend from '../PieChart/legend'; +import { useSeries } from './useSeries'; +import type { LegendItemParams } from '../ChartsLegend'; const legendGetter: { [T in ChartSeriesType]?: LegendGetter } = { bar: getBarLegend, @@ -14,7 +17,7 @@ const legendGetter: { [T in ChartSeriesType]?: LegendGetter } = { pie: getPieLegend, }; -export function getSeriesToDisplay(series: FormattedSeries) { +function getSeriesToDisplay(series: FormattedSeries) { return (Object.keys(series) as ChartSeriesType[]).flatMap( (seriesType: T) => { const getter = legendGetter[seriesType as T]; @@ -22,3 +25,19 @@ export function getSeriesToDisplay(series: FormattedSeries) { }, ); } + +/** + * Get the legend items to display. + * + * This hook is used by the `ChartsLegend` component. And will return the legend items formatted for display. + * + * An alternative is to use the `useSeries` hook and format the legend items yourself. + * + * @returns legend data + */ +export function useLegend(): { items: LegendItemParams[] } { + const series = useSeries(); + return { + items: getSeriesToDisplay(series), + }; +} diff --git a/packages/x-charts/src/index.ts b/packages/x-charts/src/index.ts index 0b76b3b75b74f..485077ceba010 100644 --- a/packages/x-charts/src/index.ts +++ b/packages/x-charts/src/index.ts @@ -11,6 +11,7 @@ export * from './ChartsYAxis'; export * from './ChartsGrid'; export * from './ChartsText'; export * from './ChartsTooltip'; +export * from './ChartsLabel'; export * from './ChartsLegend'; export * from './ChartsAxisHighlight'; export * from './ChartsVoronoiHandler'; diff --git a/packages/x-charts/src/internals/calculateMargins.ts b/packages/x-charts/src/internals/calculateMargins.ts new file mode 100644 index 0000000000000..f2984dac071b6 --- /dev/null +++ b/packages/x-charts/src/internals/calculateMargins.ts @@ -0,0 +1,51 @@ +import type { ChartsLegendSlotExtension } from '../ChartsLegend'; +import { DEFAULT_MARGINS, DEFAULT_LEGEND_FACING_MARGIN } from '../constants'; +import type { LayoutConfig } from '../models'; +import type { CartesianChartSeriesType, ChartsSeriesConfig } from '../models/seriesType/config'; + +export const calculateMargins = < + T extends ChartsLegendSlotExtension & + Pick & { + hideLegend?: boolean; + series?: Partial[]; + }, +>( + props: T, +): Required => { + if (props.hideLegend || !props.series?.some((s) => s.label)) { + return { + ...DEFAULT_MARGINS, + ...props.margin, + }; + } + + if (props.slotProps?.legend?.direction === 'vertical') { + if (props.slotProps?.legend?.position?.horizontal === 'left') { + return { + ...DEFAULT_MARGINS, + left: DEFAULT_LEGEND_FACING_MARGIN, + ...props.margin, + }; + } + + return { + ...DEFAULT_MARGINS, + right: DEFAULT_LEGEND_FACING_MARGIN, + ...props.margin, + }; + } + + if (props.slotProps?.legend?.position?.vertical === 'bottom') { + return { + ...DEFAULT_MARGINS, + bottom: DEFAULT_LEGEND_FACING_MARGIN, + ...props.margin, + }; + } + + return { + ...DEFAULT_MARGINS, + top: DEFAULT_LEGEND_FACING_MARGIN, + ...props.margin, + }; +}; diff --git a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx index 7443e5754dc5c..d7edb5b7f60fc 100644 --- a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx +++ b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsAxesGradients.tsx @@ -4,11 +4,23 @@ import { useChartId, useDrawingArea } from '../../../hooks'; import ChartsPiecewiseGradient from './ChartsPiecewiseGradient'; import ChartsContinuousGradient from './ChartsContinuousGradient'; import { AxisId } from '../../../models/axis'; +import ChartsContinuousGradientObjectBound from './ChartsContinuousGradientObjectBound'; +import { useZAxis } from '../../../hooks/useZAxis'; export function useChartGradient() { const chartId = useChartId(); return React.useCallback( - (axisId: AxisId, direction: 'x' | 'y') => `${chartId}-gradient-${direction}-${axisId}`, + (axisId: AxisId, direction: 'x' | 'y' | 'z') => `${chartId}-gradient-${direction}-${axisId}`, + [chartId], + ); +} + +// TODO: make public? +export function useChartGradientObjectBound() { + const chartId = useChartId(); + return React.useCallback( + (axisId: AxisId, direction: 'x' | 'y' | 'z') => + `${chartId}-gradient-${direction}-${axisId}-object-bound`, [chartId], ); } @@ -19,12 +31,19 @@ export function ChartsAxesGradients() { const svgHeight = top + height + bottom; const svgWidth = left + width + right; const getGradientId = useChartGradient(); + const getObjectBoundGradientId = useChartGradientObjectBound(); const { xAxisIds, xAxis, yAxisIds, yAxis } = useCartesianContext(); + const { zAxisIds, zAxis } = useZAxis(); const filteredYAxisIds = yAxisIds.filter((axisId) => yAxis[axisId].colorMap !== undefined); const filteredXAxisIds = xAxisIds.filter((axisId) => xAxis[axisId].colorMap !== undefined); + const filteredZAxisIds = zAxisIds.filter((axisId) => zAxis[axisId].colorMap !== undefined); - if (filteredYAxisIds.length === 0 && filteredXAxisIds.length === 0) { + if ( + filteredYAxisIds.length === 0 && + filteredXAxisIds.length === 0 && + filteredZAxisIds.length === 0 + ) { return null; } @@ -32,6 +51,7 @@ export function ChartsAxesGradients() { {filteredYAxisIds.map((axisId) => { const gradientId = getGradientId(axisId, 'y'); + const objectBoundGradientId = getObjectBoundGradientId(axisId, 'y'); const { colorMap, scale, colorScale, reverse } = yAxis[axisId]; if (colorMap?.type === 'piecewise') { return ( @@ -48,22 +68,31 @@ export function ChartsAxesGradients() { } if (colorMap?.type === 'continuous') { return ( - + + + + ); } return null; })} {filteredXAxisIds.map((axisId) => { const gradientId = getGradientId(axisId, 'x'); + const objectBoundGradientId = getObjectBoundGradientId(axisId, 'x'); + const { colorMap, scale, reverse, colorScale } = xAxis[axisId]; if (colorMap?.type === 'piecewise') { return ( @@ -80,15 +109,37 @@ export function ChartsAxesGradients() { } if (colorMap?.type === 'continuous') { return ( - + + + + ); + } + return null; + })} + {filteredZAxisIds.map((axisId) => { + const objectBoundGradientId = getObjectBoundGradientId(axisId, 'z'); + const { colorMap, colorScale } = zAxis[axisId]; + if (colorMap?.type === 'continuous') { + return ( + ); } diff --git a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsContinuousGradient.tsx b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsContinuousGradient.tsx index f78518a478c53..5eb3915bdf214 100644 --- a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsContinuousGradient.tsx +++ b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsContinuousGradient.tsx @@ -24,22 +24,24 @@ export default function ChartsContinuousGradient(props: ChartsContinuousGradient const { gradientUnits, isReversed, gradientId, size, direction, scale, colorScale, colorMap } = props; - const extremValues = [colorMap.min ?? 0, colorMap.max ?? 100] as [number, number] | [Date, Date]; - const extremPositions = extremValues.map(scale).filter((p): p is number => p !== undefined); + const extremumValues = [colorMap.min ?? 0, colorMap.max ?? 100] as + | [number, number] + | [Date, Date]; + const extremumPositions = extremumValues.map(scale).filter((p): p is number => p !== undefined); - if (extremPositions.length !== 2) { + if (extremumPositions.length !== 2) { return null; } const interpolator = - typeof extremValues[0] === 'number' - ? interpolateNumber(extremValues[0], extremValues[1]) - : interpolateDate(extremValues[0], extremValues[1] as Date); + typeof extremumValues[0] === 'number' + ? interpolateNumber(extremumValues[0], extremumValues[1]) + : interpolateDate(extremumValues[0], extremumValues[1] as Date); const numberOfPoints = Math.round( - (Math.max(...extremPositions) - Math.min(...extremPositions)) / PX_PRECISION, + (Math.max(...extremumPositions) - Math.min(...extremumPositions)) / PX_PRECISION, ); - const keyPrefix = `${extremValues[0]}-${extremValues[1]}-`; + const keyPrefix = `${extremumValues[0]}-${extremumValues[1]}-`; return ( string | null; +}; + +const getDirection = (isReversed?: boolean): Record<'x1' | 'x2' | 'y1' | 'y2', '0' | '1'> => { + if (isReversed) { + return { x1: '1', x2: '0', y1: '0', y2: '0' }; + } + return { x1: '0', x2: '1', y1: '0', y2: '0' }; +}; + +/** + * Generates gradients to be used in tooltips and legends. + */ +export default function ChartsContinuousGradientObjectBound( + props: ChartsContinuousGradientObjectBoundProps, +) { + const { isReversed, gradientId, colorScale, colorMap } = props; + + const extremumValues = [colorMap.min ?? 0, colorMap.max ?? 100] as + | [number, number] + | [Date, Date]; + + const interpolator = + typeof extremumValues[0] === 'number' + ? interpolateNumber(extremumValues[0], extremumValues[1]) + : interpolateDate(extremumValues[0], extremumValues[1] as Date); + const numberOfPoints = PX_PRECISION; + + const keyPrefix = `${extremumValues[0]}-${extremumValues[1]}-`; + return ( + + {Array.from({ length: numberOfPoints + 1 }, (_, index) => { + const offset = index / numberOfPoints; + const value = interpolator(offset); + if (value === undefined) { + return null; + } + + const color = colorScale(value); + + if (color === null) { + return null; + } + return ; + })} + + ); +} diff --git a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsPiecewiseGradient.tsx b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsPiecewiseGradient.tsx index 6b6957ab154d3..cd792c6c72d53 100644 --- a/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsPiecewiseGradient.tsx +++ b/packages/x-charts/src/internals/components/ChartsAxesGradients/ChartsPiecewiseGradient.tsx @@ -31,6 +31,10 @@ export default function ChartsPiecewiseGradient(props: ChartsPiecewiseGradientPr } const offset = isReversed ? 1 - x / size : x / size; + if (Number.isNaN(offset)) { + return null; + } + return ( diff --git a/packages/x-charts/src/internals/components/ChartsWrapper/ChartsWrapper.tsx b/packages/x-charts/src/internals/components/ChartsWrapper/ChartsWrapper.tsx new file mode 100644 index 0000000000000..a1b80b46ac0df --- /dev/null +++ b/packages/x-charts/src/internals/components/ChartsWrapper/ChartsWrapper.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { styled, SxProps, Theme } from '@mui/material/styles'; +import { Direction, LegendPosition } from '../../../ChartsLegend'; + +export interface ChartsWrapperProps { + // eslint-disable-next-line react/no-unused-prop-types + legendPosition?: LegendPosition; + // eslint-disable-next-line react/no-unused-prop-types + legendDirection?: Direction; + children: React.ReactNode; + sx?: SxProps; +} + +const getDirection = (direction?: Direction, position?: LegendPosition) => { + if (direction === 'vertical') { + if (position?.horizontal === 'left') { + return 'row'; + } + + return 'row-reverse'; + } + + if (position?.vertical === 'bottom') { + return 'column-reverse'; + } + + return 'column'; +}; + +const getAlign = (direction?: Direction, position?: LegendPosition) => { + if (direction === 'vertical') { + if (position?.vertical === 'top') { + return 'flex-start'; + } + + if (position?.vertical === 'bottom') { + return 'flex-end'; + } + } + + if (direction === 'horizontal') { + if (position?.horizontal === 'left') { + return 'flex-start'; + } + + if (position?.horizontal === 'right') { + return 'flex-end'; + } + } + + return 'center'; +}; + +const Root = styled('div', { + name: 'MuiChartsWrapper', + slot: 'Root', + overridesResolver: (props, styles) => styles.root, +})<{ ownerState: ChartsWrapperProps }>(({ ownerState }) => ({ + display: 'flex', + flexDirection: getDirection(ownerState.legendDirection, ownerState.legendPosition), + flex: 1, + justifyContent: 'center', + alignItems: getAlign(ownerState.legendDirection, ownerState.legendPosition), +})); + +/** + * @ignore - internal component. + * + * Wrapper for the charts components. + * Its main purpose is to position the HTML legend in the correct place. + */ +function ChartsWrapper(props: ChartsWrapperProps) { + const { children, sx } = props; + + return ( + + {children} + + ); +} + +export { ChartsWrapper }; diff --git a/packages/x-charts/src/internals/components/ChartsWrapper/index.ts b/packages/x-charts/src/internals/components/ChartsWrapper/index.ts new file mode 100644 index 0000000000000..42c70e39f34fb --- /dev/null +++ b/packages/x-charts/src/internals/components/ChartsWrapper/index.ts @@ -0,0 +1 @@ +export * from './ChartsWrapper'; diff --git a/packages/x-charts/src/internals/consumeSlots.test.tsx b/packages/x-charts/src/internals/consumeSlots.test.tsx new file mode 100644 index 0000000000000..2ff5ac4003ee2 --- /dev/null +++ b/packages/x-charts/src/internals/consumeSlots.test.tsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { createRenderer, screen } from '@mui/internal-test-utils'; +import { consumeSlots } from './consumeSlots'; + +type WrapperProps = { + data?: string; + shouldOmit?: boolean; + classes?: Record<'root', string>; + slots?: { + wrapper?: + | React.ElementType> + | React.ForwardRefRenderFunction>; + }; +}; + +const SlotsWrapper = consumeSlots( + 'MuiSlotsWrapper', + 'wrapper', + { + defaultProps: { data: 'test' }, + omitProps: ['shouldOmit'], + classesResolver: (props: WrapperProps) => ({ + root: ['wrapper-root', props.data, props.shouldOmit ? 'shouldOmit' : ''].join(' '), + }), + }, + function SlotsWrapper(props: WrapperProps, ref: React.Ref) { + return ( +
    +
    {props.data}
    +
    {props.shouldOmit ? 'not-omitted' : 'omitted'}
    +
    {props.classes?.root}
    +
    + ); + }, +); + +describe('consumeSlots', () => { + const { render } = createRenderer(); + + it('should render default props', async () => { + render(); + + await screen.findByText('test', { selector: '.data' }); + }); + + it('should render passed props', async () => { + render(); + + await screen.findByText('new', { selector: '.data' }); + }); + + it('should render omit props in omitProps', async () => { + render(); + + await screen.findByText('omitted', { selector: '.shouldOmit' }); + }); + + it('should resolve classes', async () => { + render(); + + await screen.findByText('wrapper-root test shouldOmit', { selector: '.classes' }); + }); + + it('should render function component passed as slot', async () => { + render( +
    function component
    , + }} + />, + ); + + await screen.findByText('function component'); + }); + + it('should render forward ref function passed as slot', async () => { + render( + ) => ( +
    forward ref
    + )), + }} + />, + ); + + await screen.findByText('forward ref'); + }); + + it('should render function with props and ref arguments passed as slot', async () => { + render( + ) => ( +
    props and ref arguments
    + ), + }} + />, + ); + + await screen.findByText('props and ref arguments'); + }); +}); diff --git a/packages/x-charts/src/internals/consumeSlots.tsx b/packages/x-charts/src/internals/consumeSlots.tsx new file mode 100644 index 0000000000000..178e2f40b4cdb --- /dev/null +++ b/packages/x-charts/src/internals/consumeSlots.tsx @@ -0,0 +1,121 @@ +import { useTheme, useThemeProps } from '@mui/material/styles'; +import resolveProps from '@mui/utils/resolveProps'; +import useSlotProps from '@mui/utils/useSlotProps'; +import * as React from 'react'; + +/** + * A higher order component that consumes a slot from the props and renders the component provided in the slot. + * + * This HOC will wrap a single component, and will render the component provided in the slot, if it exists. + * + * If you need to render multiple components, you can manually consume the slots from the props and render them in your component instead of using this HOC. + * + * In the example below, `MyComponent` will render the component provided in `mySlot` slot, if it exists. Otherwise, it will render the `DefaultComponent`. + * + * @example + * + * ```tsx + * type MyComponentProps = { + * direction: 'row' | 'column'; + * slots?: { + * mySlot?: React.JSXElementConstructor<{ direction: 'row' | 'column' }>; + * } + * }; + * + * const MyComponent = consumeSlots( + * 'MuiMyComponent', + * 'mySlot', + * function DefaultComponent(props: MyComponentProps) { + * return ( + *
    + * {props.direction} + *
    + * ); + * } + * ); + * ``` + * + * @param {string} name The mui component name. + * @param {string} slotPropName The name of the prop to retrieve the slot from. + * @param {object} options Options for the HOC. + * @param {boolean} options.propagateSlots Whether to propagate the slots to the component, this is always false if the slot is provided. + * @param {Record} options.defaultProps A set of defaults for the component, will be deep merged with the props. + * @param {Array} options.omitProps An array of props to omit from the component. + * @param {Function} options.classesResolver A function that returns the classes for the component. It receives the props, after theme props and defaults have been applied. And the theme object as the second argument. + * @param InComponent The component to render if the slot is not provided. + */ +export const consumeSlots = < + Props extends {}, + Ref extends {}, + RenderFunction = (props: Props, ref: React.Ref) => React.ElementType, +>( + name: string, + slotPropName: string, + options: { + propagateSlots?: boolean; + defaultProps?: + | Omit, 'slots' | 'slotProps'> + | ((props: Props) => Omit, 'slots' | 'slotProps'>); + omitProps?: Array; + classesResolver?: (props: Props, theme: any) => Record; + }, + InComponent: RenderFunction, +) => { + function ConsumeSlotsInternal(props: React.PropsWithoutRef, ref: React.ForwardedRef) { + const themedProps = useThemeProps({ + props, + // eslint-disable-next-line material-ui/mui-name-matches-component-name + name, + }); + + const defaultProps = + typeof options.defaultProps === 'function' + ? options.defaultProps(themedProps as Props) + : (options.defaultProps ?? {}); + + const defaultizedProps = resolveProps(defaultProps, themedProps) as Props; + const { slots, slotProps, ...other } = defaultizedProps as { + slots?: Record; + slotProps?: Record; + }; + + const theme = useTheme(); + const classes = options.classesResolver?.(defaultizedProps, theme); + + // Can be a function component or a forward ref component. + const Component = slots?.[slotPropName] ?? InComponent; + + const propagateSlots = options.propagateSlots && !slots?.[slotPropName]; + + const { ownerState, ...originalOutProps } = useSlotProps({ + elementType: Component, + externalSlotProps: slotProps?.[slotPropName], + additionalProps: { + ...other, + classes, + ...(propagateSlots && { slots, slotProps }), + }, + ownerState: {}, + }); + + const outProps = { ...originalOutProps } as unknown as Props; + + for (const prop of options.omitProps ?? []) { + delete (outProps as unknown as Props)[prop]; + } + + // Component can be wrapped in React.forwardRef or just a function that accepts (props, ref). + // If it is a plain function which accepts two arguments, we need to wrap it in React.forwardRef. + const OutComponent = ( + Component.length >= 2 ? React.forwardRef(Component) : Component + ) as React.FunctionComponent; + + if (process.env.NODE_ENV !== 'production') { + OutComponent.displayName = `${name}.slots.${slotPropName}`; + } + + return ; + } + + return React.forwardRef(ConsumeSlotsInternal); +}; diff --git a/packages/x-charts/src/internals/consumeThemeProps.test.tsx b/packages/x-charts/src/internals/consumeThemeProps.test.tsx new file mode 100644 index 0000000000000..f5b2d28dd7185 --- /dev/null +++ b/packages/x-charts/src/internals/consumeThemeProps.test.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { createRenderer, screen } from '@mui/internal-test-utils'; +import { consumeThemeProps } from './consumeThemeProps'; + +type WrapperProps = { + data?: string; + shouldOmit?: boolean; + classes?: Record<'root', string>; + slots?: { + wrapper?: React.ElementType>; + }; +}; + +const SlotsWrapper = consumeThemeProps( + 'MuiSlotsWrapper', + { + defaultProps: { data: 'test' }, + classesResolver: (props: WrapperProps) => ({ + root: ['wrapper-root', props.data, props.shouldOmit ? 'shouldOmit' : ''].join(' '), + }), + }, + function SlotsWrapper(props: WrapperProps, ref: React.Ref) { + return ( +
    +
    {props.data}
    +
    {props.classes?.root}
    +
    + ); + }, +); + +describe('consumeThemeProps', () => { + const { render } = createRenderer(); + + it('should render default props', async function test() { + render(); + + await screen.findByText('test', { selector: '.data' }); + }); + + it('should render passed props', async function test() { + render(); + + await screen.findByText('new', { selector: '.data' }); + }); + + it('should resolve classes', async () => { + render(); + + await screen.findByText('wrapper-root test shouldOmit', { selector: '.classes' }); + }); +}); diff --git a/packages/x-charts/src/internals/consumeThemeProps.tsx b/packages/x-charts/src/internals/consumeThemeProps.tsx index 217550c1b07ad..29524a2ddb794 100644 --- a/packages/x-charts/src/internals/consumeThemeProps.tsx +++ b/packages/x-charts/src/internals/consumeThemeProps.tsx @@ -1,7 +1,6 @@ import { useTheme, useThemeProps } from '@mui/material/styles'; import resolveProps from '@mui/utils/resolveProps'; import * as React from 'react'; -import * as ReactIs from 'react-is'; /** * A higher order component that consumes and merges the theme `defaultProps` and handles the `classes` and renders the component. @@ -49,22 +48,23 @@ import * as ReactIs from 'react-is'; * @param InComponent The component to render if the slot is not provided. */ export const consumeThemeProps = < - Props extends { - slots?: Record; - slotProps?: Record; - classes?: Record; - }, + Props extends {}, + Ref extends {}, + RenderFunction = (props: Props, ref: React.Ref) => React.ElementType, >( name: string, options: { defaultProps?: | Omit, 'slots' | 'slotProps'> - | ((props: Props) => Omit, 'slots' | 'slotProps'>); + | ((props: T) => Omit, 'slots' | 'slotProps'>); classesResolver?: (props: Props, theme: any) => Record; }, - InComponent: React.ForwardRefRenderFunction, -) => { - function InternalComponent(props: React.PropsWithoutRef, ref: React.Ref) { + InComponent: RenderFunction, +) => + React.forwardRef(function ConsumeThemeInternal( + props: React.PropsWithoutRef, + ref: React.ForwardedRef, + ) { const themedProps = useThemeProps({ props, // eslint-disable-next-line material-ui/mui-name-matches-component-name @@ -76,24 +76,18 @@ export const consumeThemeProps = < ? options.defaultProps(themedProps as Props) : (options.defaultProps ?? {}); - const outProps = resolveProps(defaultProps, themedProps) as Props; + const outProps = resolveProps(defaultProps, themedProps) as React.PropsWithoutRef; const theme = useTheme(); - const classes = options.classesResolver?.(outProps, theme); + const classes = options.classesResolver?.(outProps as Props, theme); + + const OutComponent = React.forwardRef( + InComponent as React.ForwardRefRenderFunction>, + ); if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line react-compiler/react-compiler - (InComponent as any).displayName = name; + OutComponent.displayName = `consumeThemeProps(${name})`; } - const OutComponent = ReactIs.isForwardRef(InComponent) - ? (InComponent as unknown as React.ElementType) - : // InComponent needs to be a function that accepts `(props, ref)` - // @ts-expect-error - React.forwardRef(InComponent); - return ; - } - - return React.forwardRef(InternalComponent); -}; + }); diff --git a/packages/x-charts/src/internals/index.ts b/packages/x-charts/src/internals/index.ts index b3079186b4810..93f32cab9e1e9 100644 --- a/packages/x-charts/src/internals/index.ts +++ b/packages/x-charts/src/internals/index.ts @@ -1,5 +1,6 @@ // Components export * from './components/ChartsAxesGradients'; +export * from './components/ChartsWrapper'; // hooks export { useSeries } from '../hooks/useSeries'; diff --git a/packages/x-charts/src/models/seriesType/common.ts b/packages/x-charts/src/models/seriesType/common.ts index 356c8a4b4b23b..084d3d302dd1a 100644 --- a/packages/x-charts/src/models/seriesType/common.ts +++ b/packages/x-charts/src/models/seriesType/common.ts @@ -1,3 +1,4 @@ +import type { ChartsLabelMarkProps } from '../../ChartsLabel'; import type { HighlightScope } from '../../context'; import type { StackOffsetType, StackOrderType } from '../stacking'; @@ -29,6 +30,14 @@ export type CommonSeriesType = { * The scope to apply when the series is highlighted. */ highlightScope?: Partial; + /** + * Defines the mark type for the series. + * + * There is a default mark type for each series type. + * + * It allows custom values which will be passed to the mark component if it was customized. + */ + labelMarkType?: ChartsLabelMarkProps['type']; }; export type CommonDefaultizedProps = 'id' | 'valueFormatter' | 'data'; diff --git a/packages/x-charts/src/models/seriesType/pie.ts b/packages/x-charts/src/models/seriesType/pie.ts index 1ab1a6a4b5a87..50d2447ec0cfc 100644 --- a/packages/x-charts/src/models/seriesType/pie.ts +++ b/packages/x-charts/src/models/seriesType/pie.ts @@ -1,6 +1,7 @@ import { PieArcDatum as D3PieArcDatum } from '@mui/x-charts-vendor/d3-shape'; import { DefaultizedProps } from '@mui/x-internals/types'; import { CommonDefaultizedProps, CommonSeriesType, SeriesId } from './common'; +import type { ChartsLabelMarkProps } from '../../ChartsLabel'; export type PieItemId = string | number; @@ -15,6 +16,14 @@ export type PieValueType = { */ label?: string | ((location: 'tooltip' | 'legend' | 'arc') => string); color?: string; + /** + * Defines the mark type for the pie item. + * + * It allows custom values which will be passed to the mark component if it was customized. + * + * @default 'circle' + */ + labelMarkType?: ChartsLabelMarkProps['type']; }; export type DefaultizedPieValueType = PieValueType & diff --git a/packages/x-charts/src/themeAugmentation/themeAugmentation.spec.ts b/packages/x-charts/src/themeAugmentation/themeAugmentation.spec.ts index 8412c5c669cf7..4e55e2684ac42 100644 --- a/packages/x-charts/src/themeAugmentation/themeAugmentation.spec.ts +++ b/packages/x-charts/src/themeAugmentation/themeAugmentation.spec.ts @@ -42,7 +42,7 @@ createTheme({ }, MuiChartsLegend: { defaultProps: { - direction: 'row', + direction: 'vertical', // @ts-expect-error invalid MuiChartsLegend prop someRandomProp: true, }, diff --git a/packages/x-codemod/README.md b/packages/x-codemod/README.md index 75d44cf98ab90..2ca0c8be18edc 100644 --- a/packages/x-codemod/README.md +++ b/packages/x-codemod/README.md @@ -143,6 +143,7 @@ npx @mui/x-codemod@next v8.0.0/charts/preset-safe The list includes these transformers - [`rename-legend-to-slots-legend`](#rename-legend-to-slots-legend) +- [`replace-legend-direction-values`](#replace-legend-direction-values) - [`rename-responsive-chart-container`](#rename-responsive-chart-container) - [`rename-label-and-tick-font-size`](#rename-label-and-tick-font-size) @@ -157,6 +158,21 @@ Renames legend props to the corresponding slotProps. /> ``` +#### `replace-legend-direction-values` + +Replace `row` and `column` values by `horizontal` and `vertical` respectively. + +```diff + +``` + #### `rename-responsive-chart-container` Renames `ResponsiveChartContainer` and `ResponsiveChartContainerPro` by `ChartContainer` and `ChartContainerPro` which have the same behavior in v8. diff --git a/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx index 849585a97e2fb..9e949121e03c7 100644 --- a/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx +++ b/packages/x-codemod/src/v8.0.0/charts/preset-safe/actual.spec.tsx @@ -1,7 +1,7 @@ // @ts-nocheck import * as React from 'react'; import { PieChart } from '@mui/x-charts/PieChart'; -import { BarPlot } from '@mui/x-charts/BarChart'; +import { BarPlot, BarChart } from '@mui/x-charts/BarChart'; import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer'; import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; @@ -25,4 +25,7 @@ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; labelStyle={{ fontWeight: 'bold', fontSize: 10 }} tickStyle={{ fontWeight: 'bold', fontSize: 12 }} /> + + + ; diff --git a/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx index 139cad04ca12c..b674eb48cbe82 100644 --- a/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx +++ b/packages/x-codemod/src/v8.0.0/charts/preset-safe/expected.spec.tsx @@ -1,7 +1,7 @@ // @ts-nocheck import * as React from 'react'; import { PieChart } from '@mui/x-charts/PieChart'; -import { BarPlot } from '@mui/x-charts/BarChart'; +import { BarPlot, BarChart } from '@mui/x-charts/BarChart'; import { ChartContainer } from '@mui/x-charts/ChartContainer'; import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; @@ -44,4 +44,23 @@ import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; fontWeight: 'bold', fontSize: 12 }} /> + + + ; diff --git a/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts b/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts index 3fdc6150db550..54cc36d4595ab 100644 --- a/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts +++ b/packages/x-codemod/src/v8.0.0/charts/preset-safe/index.ts @@ -1,6 +1,7 @@ import transformLegendToSlots from '../rename-legend-to-slots-legend'; import transformRemoveResponsiveContainer from '../rename-responsive-chart-container'; import transformRenameLabelAndTickFontSize from '../rename-label-and-tick-font-size'; +import transformReplaceLegendDirectionValues from '../replace-legend-direction-values'; import { JsCodeShiftAPI, JsCodeShiftFileInfo } from '../../../types'; @@ -8,6 +9,8 @@ export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftA file.source = transformLegendToSlots(file, api, options); file.source = transformRemoveResponsiveContainer(file, api, options); file.source = transformRenameLabelAndTickFontSize(file, api, options); + file.source = transformRenameLabelAndTickFontSize(file, api, options); + file.source = transformReplaceLegendDirectionValues(file, api, options); return file.source; } diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/actual.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/actual.spec.tsx new file mode 100644 index 0000000000000..930f06082a354 --- /dev/null +++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/actual.spec.tsx @@ -0,0 +1,10 @@ +// @ts-nocheck +import * as React from 'react'; +import { BarChart } from '@mui/x-charts/BarChart'; + +// prettier-ignore +
    + + + +
    ; diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/expected.spec.tsx b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/expected.spec.tsx new file mode 100644 index 0000000000000..13791eaec74f8 --- /dev/null +++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/expected.spec.tsx @@ -0,0 +1,26 @@ +// @ts-nocheck +import * as React from 'react'; +import { BarChart } from '@mui/x-charts/BarChart'; + +// prettier-ignore +
    + + + +
    ; diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/index.ts b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/index.ts new file mode 100644 index 0000000000000..2c1f748a75a3f --- /dev/null +++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/index.ts @@ -0,0 +1,75 @@ +import { JSXAttribute, JSXExpressionContainer, ObjectExpression } from 'jscodeshift'; +import { JsCodeShiftAPI, JsCodeShiftFileInfo } from '../../../types'; +import { transformNestedProp } from '../../../util/addComponentsSlots'; +/** + * @param {import('jscodeshift').FileInfo} file + * @param {import('jscodeshift').API} api + */ +export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftAPI, options: any) { + const j = api.jscodeshift; + + const printOptions = options.printOptions; + + const root = j(file.source); + + root + .find(j.ImportDeclaration) + .filter(({ node }) => { + return typeof node.source.value === 'string' && node.source.value.startsWith('@mui/x-charts'); + }) + .forEach((path) => { + path.node.specifiers?.forEach((node) => { + root.findJSXElements(node.local?.name).forEach((elementPath) => { + if (elementPath.node.type !== 'JSXElement') { + return; + } + + const slotProps = elementPath.node.openingElement.attributes?.find( + (elementNode) => + elementNode.type === 'JSXAttribute' && elementNode.name.name === 'slotProps', + ) as JSXAttribute | null; + + if (slotProps === null) { + // No slotProps to manage + return; + } + + const direction = ( + (slotProps?.value as JSXExpressionContainer | null)?.expression as ObjectExpression + )?.properties + // @ts-expect-error + ?.find((v) => v?.key?.name === 'legend') + // @ts-expect-error + ?.value?.properties?.find((v) => v?.key?.name === 'direction'); + + if ( + direction === undefined || + direction?.value === undefined || + direction?.value?.value === undefined + ) { + return; + } + const directionValue = direction.value; + + directionValue.value = mapFix(directionValue.value); + + transformNestedProp(elementPath, 'slotProps', 'legend.direction', directionValue, j); + }); + }); + }); + + const transformed = root.findJSXElements(); + + return transformed.toSource(printOptions); +} + +function mapFix(v?: string) { + switch (v) { + case 'row': + return 'horizontal'; + case 'column': + return 'vertical'; + default: + return v; + } +} diff --git a/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/replace-legend-direction-values.test.ts b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/replace-legend-direction-values.test.ts new file mode 100644 index 0000000000000..0d3f4daddffaa --- /dev/null +++ b/packages/x-codemod/src/v8.0.0/charts/replace-legend-direction-values/replace-legend-direction-values.test.ts @@ -0,0 +1,38 @@ +import path from 'path'; +import { expect } from 'chai'; +import jscodeshift from 'jscodeshift'; +import transform from '.'; +import readFile from '../../../util/readFile'; + +function read(fileName) { + return readFile(path.join(__dirname, fileName)); +} + +describe('v8.0.0/charts', () => { + describe('rename-label-and-tick-font-size.test', () => { + const actualPath = `./actual.spec.tsx`; + const expectedPath = `./expected.spec.tsx`; + + it('transforms imports as needed', () => { + const actual = transform( + { source: read(actualPath) }, + { jscodeshift: jscodeshift.withParser('tsx') }, + {}, + ); + + const expected = read(expectedPath); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + + it('should be idempotent', () => { + const actual = transform( + { source: read(expectedPath) }, + { jscodeshift: jscodeshift.withParser('tsx') }, + {}, + ); + + const expected = read(expectedPath); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + }); +}); diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts index 0e91f89ecee6a..4294bf38db5ca 100644 --- a/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts +++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/createAggregationLookup.ts @@ -19,23 +19,20 @@ import { import { getAggregationRules } from './gridAggregationUtils'; import { gridAggregationModelSelector } from './gridAggregationSelectors'; -const getAggregationCellValue = ({ - apiRef, - groupId, - field, - aggregationFunction, - aggregationRowsScope, -}: { - apiRef: React.MutableRefObject; - groupId: GridRowId; - field: string; - aggregationFunction: GridAggregationFunction; - aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope']; -}) => { +const getGroupAggregatedValue = ( + groupId: GridRowId, + apiRef: React.MutableRefObject, + aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope'], + aggregatedFields: string[], + aggregationRules: GridAggregationRules, + position: GridAggregationPosition, +) => { + const groupAggregationLookup: GridAggregationLookup[GridRowId] = {}; + const aggregatedValues: { aggregatedField: string; values: any[] }[] = []; + + const rowIds = apiRef.current.getRowGroupChildren({ groupId }); const filteredRowsLookup = gridFilteredRowsLookupSelector(apiRef); - const rowIds: GridRowId[] = apiRef.current.getRowGroupChildren({ groupId }); - const values: any[] = []; rowIds.forEach((rowId) => { if (aggregationRowsScope === 'filtered' && filteredRowsLookup[rowId] === false) { return; @@ -53,51 +50,43 @@ const getAggregationCellValue = ({ return; } - if (typeof aggregationFunction.getCellValue === 'function') { - const row = apiRef.current.getRow(rowId); - values.push(aggregationFunction.getCellValue({ row })); - } else { - values.push(apiRef.current.getCellValue(rowId, field)); - } - }); + const row = apiRef.current.getRow(rowId); - return aggregationFunction.apply({ - values, - groupId, - field, // Added per user request in https://github.com/mui/mui-x/issues/6995#issuecomment-1327423455 - }); -}; + for (let j = 0; j < aggregatedFields.length; j += 1) { + const aggregatedField = aggregatedFields[j]; + const columnAggregationRules = aggregationRules[aggregatedField]; -const getGroupAggregatedValue = ({ - groupId, - apiRef, - aggregationRowsScope, - aggregatedFields, - aggregationRules, - position, -}: { - groupId: GridRowId; - apiRef: React.MutableRefObject; - aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope']; - aggregatedFields: string[]; - aggregationRules: GridAggregationRules; - position: GridAggregationPosition; -}) => { - const groupAggregationLookup: GridAggregationLookup[GridRowId] = {}; + const aggregationFunction = columnAggregationRules.aggregationFunction; + const field = aggregatedField; - for (let j = 0; j < aggregatedFields.length; j += 1) { - const aggregatedField = aggregatedFields[j]; - const columnAggregationRules = aggregationRules[aggregatedField]; + if (aggregatedValues[j] === undefined) { + aggregatedValues[j] = { + aggregatedField, + values: [], + }; + } + + if (typeof aggregationFunction.getCellValue === 'function') { + aggregatedValues[j].values.push(aggregationFunction.getCellValue({ row })); + } else { + const colDef = apiRef.current.getColumn(field); + aggregatedValues[j].values.push(apiRef.current.getRowValue(row, colDef)); + } + } + }); + + for (let i = 0; i < aggregatedValues.length; i += 1) { + const { aggregatedField, values } = aggregatedValues[i]; + const aggregationFunction = aggregationRules[aggregatedField].aggregationFunction; + const value = aggregationFunction.apply({ + values, + groupId, + field: aggregatedField, // Added per user request in https://github.com/mui/mui-x/issues/6995#issuecomment-1327423455 + }); groupAggregationLookup[aggregatedField] = { position, - value: getAggregationCellValue({ - apiRef, - groupId, - field: aggregatedField, - aggregationFunction: columnAggregationRules.aggregationFunction, - aggregationRowsScope, - }), + value, }; } @@ -115,11 +104,11 @@ export const createAggregationLookup = ({ aggregationRowsScope: DataGridPremiumProcessedProps['aggregationRowsScope']; getAggregationPosition: DataGridPremiumProcessedProps['getAggregationPosition']; }): GridAggregationLookup => { - const aggregationRules = getAggregationRules({ - columnsLookup: gridColumnLookupSelector(apiRef), - aggregationModel: gridAggregationModelSelector(apiRef), + const aggregationRules = getAggregationRules( + gridColumnLookupSelector(apiRef), + gridAggregationModelSelector(apiRef), aggregationFunctions, - }); + ); const aggregatedFields = Object.keys(aggregationRules); if (aggregatedFields.length === 0) { @@ -143,14 +132,14 @@ export const createAggregationLookup = ({ if (hasAggregableChildren) { const position = getAggregationPosition(groupNode); if (position != null) { - aggregationLookup[groupNode.id] = getGroupAggregatedValue({ - groupId: groupNode.id, + aggregationLookup[groupNode.id] = getGroupAggregatedValue( + groupNode.id, apiRef, - aggregatedFields, aggregationRowsScope, + aggregatedFields, aggregationRules, position, - }); + ); } } }; diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts index 0f15aa74136b4..c84aae5d5e30b 100644 --- a/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts +++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/gridAggregationUtils.ts @@ -86,18 +86,16 @@ export const mergeStateWithAggregationModel = aggregation: { ...state.aggregation, model: aggregationModel }, }); -export const getAggregationRules = ({ - columnsLookup, - aggregationModel, - aggregationFunctions, -}: { - columnsLookup: GridColumnRawLookup; - aggregationModel: GridAggregationModel; - aggregationFunctions: Record; -}) => { +export const getAggregationRules = ( + columnsLookup: GridColumnRawLookup, + aggregationModel: GridAggregationModel, + aggregationFunctions: Record, +) => { const aggregationRules: GridAggregationRules = {}; - Object.entries(aggregationModel).forEach(([field, columnItem]) => { + // eslint-disable-next-line guard-for-in + for (const field in aggregationModel) { + const columnItem = aggregationModel[field]; if ( columnsLookup[field] && canColumnHaveAggregationFunction({ @@ -111,7 +109,7 @@ export const getAggregationRules = ({ aggregationFunction: aggregationFunctions[columnItem], }; } - }); + } return aggregationRules; }; diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts index 2a9e12601509a..8b41493cc2b96 100644 --- a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts +++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregation.ts @@ -103,11 +103,11 @@ export const useGridAggregation = ( const aggregationRules = props.disableAggregation ? {} - : getAggregationRules({ - columnsLookup: gridColumnLookupSelector(apiRef), - aggregationModel: gridAggregationModelSelector(apiRef), - aggregationFunctions: props.aggregationFunctions, - }); + : getAggregationRules( + gridColumnLookupSelector(apiRef), + gridAggregationModelSelector(apiRef), + props.aggregationFunctions, + ); // Re-apply the row hydration to add / remove the aggregation footers if (!areAggregationRulesEqual(rulesOnLastRowHydration, aggregationRules)) { diff --git a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx index b0f1ab6933ae0..5ae196fb91ce1 100644 --- a/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx +++ b/packages/x-data-grid-premium/src/hooks/features/aggregation/useGridAggregationPreProcessors.tsx @@ -36,11 +36,11 @@ export const useGridAggregationPreProcessors = ( (columnsState) => { const aggregationRules = props.disableAggregation ? {} - : getAggregationRules({ - columnsLookup: columnsState.lookup, - aggregationModel: gridAggregationModelSelector(apiRef), - aggregationFunctions: props.aggregationFunctions, - }); + : getAggregationRules( + columnsState.lookup, + gridAggregationModelSelector(apiRef), + props.aggregationFunctions, + ); columnsState.orderedFields.forEach((field) => { const shouldHaveAggregationValue = !!aggregationRules[field]; @@ -76,11 +76,11 @@ export const useGridAggregationPreProcessors = ( (value) => { const aggregationRules = props.disableAggregation ? {} - : getAggregationRules({ - columnsLookup: gridColumnLookupSelector(apiRef), - aggregationModel: gridAggregationModelSelector(apiRef), - aggregationFunctions: props.aggregationFunctions, - }); + : getAggregationRules( + gridColumnLookupSelector(apiRef), + gridAggregationModelSelector(apiRef), + props.aggregationFunctions, + ); const hasAggregationRule = Object.keys(aggregationRules).length > 0; diff --git a/packages/x-internals/src/types/AppendKeys.ts b/packages/x-internals/src/types/AppendKeys.ts new file mode 100644 index 0000000000000..49cf66082f6c8 --- /dev/null +++ b/packages/x-internals/src/types/AppendKeys.ts @@ -0,0 +1,16 @@ +// Uppercase first letter of a string +type CapitalizeFirstLetter = S extends `${infer First}${infer Rest}` + ? `${Uppercase}${Rest}` + : S; + +/** + * Append string P to all keys in T. + * If K is provided, only append P to keys in K. + * + * @template T - The type to append keys to. + * @template P - The string to append. + * @template K - The keys to append P to. + */ +export type AppendKeys = { + [key in keyof T as key extends K ? `${key}${CapitalizeFirstLetter

    }` : key]: T[key]; +}; diff --git a/packages/x-internals/src/types/PrependKeys.ts b/packages/x-internals/src/types/PrependKeys.ts new file mode 100644 index 0000000000000..f6eddd8ce5c75 --- /dev/null +++ b/packages/x-internals/src/types/PrependKeys.ts @@ -0,0 +1,16 @@ +// Uppercase first letter of a string +type CapitalizeFirstLetter = S extends `${infer First}${infer Rest}` + ? `${Uppercase}${Rest}` + : S; + +/** + * Prepend string P to all keys in T. + * If K is provided, only prepend P to keys in K. + * + * @template T - The type to prepend keys to. + * @template P - The string to prepend. + * @template K - The keys to prepend P to. + */ +export type PrependKeys = { + [key in keyof T as key extends K ? `${P}${CapitalizeFirstLetter}` : key]: T[key]; +}; diff --git a/packages/x-internals/src/types/index.ts b/packages/x-internals/src/types/index.ts index 43d0d70371584..8219e4271846e 100644 --- a/packages/x-internals/src/types/index.ts +++ b/packages/x-internals/src/types/index.ts @@ -1,4 +1,6 @@ +export * from './AppendKeys'; export * from './DefaultizedProps'; export * from './MakeOptional'; export * from './MakeRequired'; +export * from './PrependKeys'; export * from './SlotComponentPropsFromProps'; diff --git a/scripts/buildApiDocs/chartsSettings/index.ts b/scripts/buildApiDocs/chartsSettings/index.ts index 2c9dabdcb7e77..2ba7315b2f302 100644 --- a/scripts/buildApiDocs/chartsSettings/index.ts +++ b/scripts/buildApiDocs/chartsSettings/index.ts @@ -68,7 +68,6 @@ export default chartsApiPages; 'x-charts/src/ChartsOverlay/ChartsOverlay.tsx', 'x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx', 'x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx', - 'x-charts/src/ChartsLegend/LegendPerItem.tsx', 'x-charts/src/LineChart/CircleMarkElement.tsx', 'x-charts/src/BarChart/AnimatedBarElement.tsx', ].some((invalidPath) => filename.endsWith(invalidPath)); diff --git a/scripts/x-charts-pro.exports.json b/scripts/x-charts-pro.exports.json index 2404d9305ef6c..fcd6d9d961a59 100644 --- a/scripts/x-charts-pro.exports.json +++ b/scripts/x-charts-pro.exports.json @@ -82,11 +82,11 @@ { "name": "ChartsGridProps", "kind": "Interface" }, { "name": "ChartsItemTooltipContent", "kind": "Function" }, { "name": "ChartsItemTooltipContentProps", "kind": "Interface" }, - { "name": "ChartsLegend", "kind": "Function" }, + { "name": "ChartsLegend", "kind": "Variable" }, { "name": "ChartsLegendClasses", "kind": "Interface" }, - { "name": "ChartsLegendClassKey", "kind": "TypeAlias" }, + { "name": "ChartsLegendPosition", "kind": "TypeAlias" }, { "name": "ChartsLegendProps", "kind": "Interface" }, - { "name": "ChartsLegendPropsBase", "kind": "TypeAlias" }, + { "name": "ChartsLegendSlotExtension", "kind": "Interface" }, { "name": "ChartsLegendSlotProps", "kind": "Interface" }, { "name": "ChartsLegendSlots", "kind": "Interface" }, { "name": "ChartsOnAxisClickHandler", "kind": "Function" }, @@ -121,15 +121,18 @@ { "name": "cheerfulFiestaPalette", "kind": "Variable" }, { "name": "cheerfulFiestaPaletteDark", "kind": "Variable" }, { "name": "cheerfulFiestaPaletteLight", "kind": "Variable" }, + { "name": "ColorLegendSelector", "kind": "Interface" }, { "name": "ComputedPieRadius", "kind": "Interface" }, - { "name": "ContinuousColorLegend", "kind": "Function" }, + { "name": "ContinuousColorLegend", "kind": "Variable" }, + { "name": "continuousColorLegendClasses", "kind": "Variable" }, + { "name": "ContinuousColorLegendClasses", "kind": "Interface" }, { "name": "ContinuousColorLegendProps", "kind": "Interface" }, { "name": "ContinuousScaleName", "kind": "TypeAlias" }, { "name": "CurveType", "kind": "TypeAlias" }, + { "name": "DEFAULT_LEGEND_FACING_MARGIN", "kind": "Variable" }, { "name": "DEFAULT_MARGINS", "kind": "Variable" }, { "name": "DEFAULT_X_AXIS_KEY", "kind": "Variable" }, { "name": "DEFAULT_Y_AXIS_KEY", "kind": "Variable" }, - { "name": "DefaultChartsLegend", "kind": "Function" }, { "name": "DefaultizedBarSeriesType", "kind": "Interface" }, { "name": "DefaultizedCartesianSeriesType", "kind": "TypeAlias" }, { "name": "DefaultizedLineSeriesType", "kind": "Interface" }, @@ -137,6 +140,7 @@ { "name": "DefaultizedPieValueType", "kind": "TypeAlias" }, { "name": "DefaultizedScatterSeriesType", "kind": "Interface" }, { "name": "DefaultizedSeriesType", "kind": "TypeAlias" }, + { "name": "Direction", "kind": "TypeAlias" }, { "name": "FadeOptions", "kind": "TypeAlias" }, { "name": "Gauge", "kind": "Variable" }, { "name": "gaugeClasses", "kind": "Variable" }, @@ -160,14 +164,12 @@ { "name": "getGaugeUtilityClass", "kind": "Function" }, { "name": "getHeatmapUtilityClass", "kind": "Function" }, { "name": "getHighlightElementUtilityClass", "kind": "Function" }, - { "name": "getLegendUtilityClass", "kind": "Function" }, { "name": "getLineElementUtilityClass", "kind": "Function" }, { "name": "getMarkElementUtilityClass", "kind": "Function" }, { "name": "getPieArcLabelUtilityClass", "kind": "Function" }, { "name": "getPieArcUtilityClass", "kind": "Function" }, { "name": "getPieCoordinates", "kind": "Function" }, { "name": "getReferenceLineUtilityClass", "kind": "Function" }, - { "name": "getSeriesToDisplay", "kind": "Function" }, { "name": "getValueToPositionMapper", "kind": "Function" }, { "name": "Heatmap", "kind": "Variable" }, { "name": "heatmapClasses", "kind": "Variable" }, @@ -189,7 +191,9 @@ { "name": "ItemHighlightedState", "kind": "TypeAlias" }, { "name": "LayoutConfig", "kind": "TypeAlias" }, { "name": "legendClasses", "kind": "Variable" }, - { "name": "LegendRendererProps", "kind": "Interface" }, + { "name": "LegendItemContext", "kind": "TypeAlias" }, + { "name": "LegendItemParams", "kind": "Interface" }, + { "name": "LegendPosition", "kind": "TypeAlias" }, { "name": "LineChart", "kind": "Variable" }, { "name": "LineChartPro", "kind": "Variable" }, { "name": "LineChartProProps", "kind": "Interface" }, @@ -248,8 +252,13 @@ { "name": "PieArcPlotSlotProps", "kind": "Interface" }, { "name": "PieArcPlotSlots", "kind": "Interface" }, { "name": "PieArcProps", "kind": "TypeAlias" }, - { "name": "PiecewiseColorLegend", "kind": "Function" }, + { "name": "piecewiseColorDefaultLabelFormatter", "kind": "Function" }, + { "name": "PiecewiseColorLegend", "kind": "Variable" }, + { "name": "piecewiseColorLegendClasses", "kind": "Variable" }, + { "name": "PiecewiseColorLegendClasses", "kind": "Interface" }, + { "name": "PiecewiseColorLegendItemContext", "kind": "Interface" }, { "name": "PiecewiseColorLegendProps", "kind": "Interface" }, + { "name": "PiecewiseLabelFormatterParams", "kind": "TypeAlias" }, { "name": "PieChart", "kind": "Variable" }, { "name": "PieChartProps", "kind": "Interface" }, { "name": "PieChartSlotProps", "kind": "Interface" }, @@ -281,6 +290,7 @@ { "name": "ScatterSeriesType", "kind": "Interface" }, { "name": "ScatterValueType", "kind": "TypeAlias" }, { "name": "SeriesItemIdentifier", "kind": "TypeAlias" }, + { "name": "SeriesLegendItemContext", "kind": "Interface" }, { "name": "ShowMarkParams", "kind": "Interface" }, { "name": "SparkLineChart", "kind": "Variable" }, { "name": "SparkLineChartProps", "kind": "Interface" }, @@ -303,6 +313,7 @@ { "name": "useItemHighlighted", "kind": "Function" }, { "name": "useItemTooltip", "kind": "Function" }, { "name": "UseItemTooltipReturnValue", "kind": "Interface" }, + { "name": "useLegend", "kind": "Function" }, { "name": "useMouseTracker", "kind": "Function" }, { "name": "useSvgRef", "kind": "Function" }, { "name": "useXAxis", "kind": "Function" }, diff --git a/scripts/x-charts.exports.json b/scripts/x-charts.exports.json index 4081f5bdcb116..9ef2b3170bc35 100644 --- a/scripts/x-charts.exports.json +++ b/scripts/x-charts.exports.json @@ -80,11 +80,15 @@ { "name": "ChartsGridProps", "kind": "Interface" }, { "name": "ChartsItemTooltipContent", "kind": "Function" }, { "name": "ChartsItemTooltipContentProps", "kind": "Interface" }, - { "name": "ChartsLegend", "kind": "Function" }, + { "name": "ChartsLabelClasses", "kind": "Interface" }, + { "name": "ChartsLabelGradientClasses", "kind": "Interface" }, + { "name": "ChartsLabelMarkClasses", "kind": "Interface" }, + { "name": "ChartsLabelMarkProps", "kind": "Interface" }, + { "name": "ChartsLegend", "kind": "Variable" }, { "name": "ChartsLegendClasses", "kind": "Interface" }, - { "name": "ChartsLegendClassKey", "kind": "TypeAlias" }, + { "name": "ChartsLegendPosition", "kind": "TypeAlias" }, { "name": "ChartsLegendProps", "kind": "Interface" }, - { "name": "ChartsLegendPropsBase", "kind": "TypeAlias" }, + { "name": "ChartsLegendSlotExtension", "kind": "Interface" }, { "name": "ChartsLegendSlotProps", "kind": "Interface" }, { "name": "ChartsLegendSlots", "kind": "Interface" }, { "name": "ChartsOnAxisClickHandler", "kind": "Function" }, @@ -119,15 +123,18 @@ { "name": "cheerfulFiestaPalette", "kind": "Variable" }, { "name": "cheerfulFiestaPaletteDark", "kind": "Variable" }, { "name": "cheerfulFiestaPaletteLight", "kind": "Variable" }, + { "name": "ColorLegendSelector", "kind": "Interface" }, { "name": "ComputedPieRadius", "kind": "Interface" }, - { "name": "ContinuousColorLegend", "kind": "Function" }, + { "name": "ContinuousColorLegend", "kind": "Variable" }, + { "name": "continuousColorLegendClasses", "kind": "Variable" }, + { "name": "ContinuousColorLegendClasses", "kind": "Interface" }, { "name": "ContinuousColorLegendProps", "kind": "Interface" }, { "name": "ContinuousScaleName", "kind": "TypeAlias" }, { "name": "CurveType", "kind": "TypeAlias" }, + { "name": "DEFAULT_LEGEND_FACING_MARGIN", "kind": "Variable" }, { "name": "DEFAULT_MARGINS", "kind": "Variable" }, { "name": "DEFAULT_X_AXIS_KEY", "kind": "Variable" }, { "name": "DEFAULT_Y_AXIS_KEY", "kind": "Variable" }, - { "name": "DefaultChartsLegend", "kind": "Function" }, { "name": "DefaultizedBarSeriesType", "kind": "Interface" }, { "name": "DefaultizedCartesianSeriesType", "kind": "TypeAlias" }, { "name": "DefaultizedLineSeriesType", "kind": "Interface" }, @@ -135,6 +142,7 @@ { "name": "DefaultizedPieValueType", "kind": "TypeAlias" }, { "name": "DefaultizedScatterSeriesType", "kind": "Interface" }, { "name": "DefaultizedSeriesType", "kind": "TypeAlias" }, + { "name": "Direction", "kind": "TypeAlias" }, { "name": "FadeOptions", "kind": "TypeAlias" }, { "name": "Gauge", "kind": "Variable" }, { "name": "gaugeClasses", "kind": "Variable" }, @@ -157,14 +165,12 @@ { "name": "getChartsTooltipUtilityClass", "kind": "Function" }, { "name": "getGaugeUtilityClass", "kind": "Function" }, { "name": "getHighlightElementUtilityClass", "kind": "Function" }, - { "name": "getLegendUtilityClass", "kind": "Function" }, { "name": "getLineElementUtilityClass", "kind": "Function" }, { "name": "getMarkElementUtilityClass", "kind": "Function" }, { "name": "getPieArcLabelUtilityClass", "kind": "Function" }, { "name": "getPieArcUtilityClass", "kind": "Function" }, { "name": "getPieCoordinates", "kind": "Function" }, { "name": "getReferenceLineUtilityClass", "kind": "Function" }, - { "name": "getSeriesToDisplay", "kind": "Function" }, { "name": "getValueToPositionMapper", "kind": "Function" }, { "name": "HighlightedContext", "kind": "Variable" }, { "name": "HighlightedProvider", "kind": "Function" }, @@ -177,9 +183,14 @@ { "name": "isBarSeries", "kind": "Function" }, { "name": "isDefaultizedBarSeries", "kind": "Function" }, { "name": "ItemHighlightedState", "kind": "TypeAlias" }, + { "name": "labelClasses", "kind": "Variable" }, + { "name": "labelGradientClasses", "kind": "Variable" }, + { "name": "labelMarkClasses", "kind": "Variable" }, { "name": "LayoutConfig", "kind": "TypeAlias" }, { "name": "legendClasses", "kind": "Variable" }, - { "name": "LegendRendererProps", "kind": "Interface" }, + { "name": "LegendItemContext", "kind": "TypeAlias" }, + { "name": "LegendItemParams", "kind": "Interface" }, + { "name": "LegendPosition", "kind": "TypeAlias" }, { "name": "LineChart", "kind": "Variable" }, { "name": "LineChartProps", "kind": "Interface" }, { "name": "LineChartSlotProps", "kind": "Interface" }, @@ -236,8 +247,13 @@ { "name": "PieArcPlotSlotProps", "kind": "Interface" }, { "name": "PieArcPlotSlots", "kind": "Interface" }, { "name": "PieArcProps", "kind": "TypeAlias" }, - { "name": "PiecewiseColorLegend", "kind": "Function" }, + { "name": "piecewiseColorDefaultLabelFormatter", "kind": "Function" }, + { "name": "PiecewiseColorLegend", "kind": "Variable" }, + { "name": "piecewiseColorLegendClasses", "kind": "Variable" }, + { "name": "PiecewiseColorLegendClasses", "kind": "Interface" }, + { "name": "PiecewiseColorLegendItemContext", "kind": "Interface" }, { "name": "PiecewiseColorLegendProps", "kind": "Interface" }, + { "name": "PiecewiseLabelFormatterParams", "kind": "TypeAlias" }, { "name": "PieChart", "kind": "Variable" }, { "name": "PieChartProps", "kind": "Interface" }, { "name": "PieChartSlotProps", "kind": "Interface" }, @@ -267,6 +283,7 @@ { "name": "ScatterSeriesType", "kind": "Interface" }, { "name": "ScatterValueType", "kind": "TypeAlias" }, { "name": "SeriesItemIdentifier", "kind": "TypeAlias" }, + { "name": "SeriesLegendItemContext", "kind": "Interface" }, { "name": "ShowMarkParams", "kind": "Interface" }, { "name": "SparkLineChart", "kind": "Variable" }, { "name": "SparkLineChartProps", "kind": "Interface" }, @@ -289,6 +306,7 @@ { "name": "useItemHighlighted", "kind": "Function" }, { "name": "useItemTooltip", "kind": "Function" }, { "name": "UseItemTooltipReturnValue", "kind": "Interface" }, + { "name": "useLegend", "kind": "Function" }, { "name": "useMouseTracker", "kind": "Function" }, { "name": "useSvgRef", "kind": "Function" }, { "name": "useXAxis", "kind": "Function" },