diff --git a/docs/data/charts/bars/ColorScaleNoSnap.js b/docs/data/charts/bars/ColorScaleNoSnap.js index a0062052c2752..810588162df1f 100644 --- a/docs/data/charts/bars/ColorScaleNoSnap.js +++ b/docs/data/charts/bars/ColorScaleNoSnap.js @@ -112,32 +112,34 @@ export default function ColorScaleNoSnap() { ...(colorX === 'None' ? [' xAxis={[{}]}'] : []), ...(colorX === 'ordinal' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'ordinal',`, ` colors: ['#ccebc5', '#a8ddb5', '#7bccc4', '#4eb3d3', '#2b8cbe', '#08589e']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'continuous' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: new Date(2019, 1, 1),`, ` max: new Date(2024, 1, 1),`, ` color: ['green', 'orange']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'piecewise' ? [ ' xAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, - ` colors: ['blue', 'red', 'blue'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, + ` colors: ['blue', 'red', 'blue'],`, + ` }`, ' }]}', ] : []), @@ -145,22 +147,24 @@ export default function ColorScaleNoSnap() { ...(colorY === 'None' ? [' yAxis={[{}]}'] : []), ...(colorY === 'continuous' ? [ - ' yAxis={[', - ` {`, + ' yAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -10,`, ` max: 10,`, ` color: ['red', 'green'],`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorY === 'piecewise' ? [ ' yAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [0],`, - ` colors: ['red', 'green'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [0],`, + ` colors: ['red', 'green'],`, + ` }`, ' }]}', ] : []), diff --git a/docs/data/charts/bars/ColorScaleNoSnap.tsx b/docs/data/charts/bars/ColorScaleNoSnap.tsx index bef053df0b9c5..2b2594dc2cee9 100644 --- a/docs/data/charts/bars/ColorScaleNoSnap.tsx +++ b/docs/data/charts/bars/ColorScaleNoSnap.tsx @@ -122,32 +122,34 @@ export default function ColorScaleNoSnap() { ...(colorX === 'None' ? [' xAxis={[{}]}'] : []), ...(colorX === 'ordinal' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'ordinal',`, ` colors: ['#ccebc5', '#a8ddb5', '#7bccc4', '#4eb3d3', '#2b8cbe', '#08589e']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'continuous' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: new Date(2019, 1, 1),`, ` max: new Date(2024, 1, 1),`, ` color: ['green', 'orange']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'piecewise' ? [ ' xAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, - ` colors: ['blue', 'red', 'blue'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, + ` colors: ['blue', 'red', 'blue'],`, + ` }`, ' }]}', ] : []), @@ -156,24 +158,24 @@ export default function ColorScaleNoSnap() { ...(colorY === 'None' ? [' yAxis={[{}]}'] : []), ...(colorY === 'continuous' ? [ - ' yAxis={[', - ` {`, - + ' yAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -10,`, ` max: 10,`, ` color: ['red', 'green'],`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorY === 'piecewise' ? [ ' yAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [0],`, - ` colors: ['red', 'green'],`, - + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [0],`, + ` colors: ['red', 'green'],`, + ` }`, ' }]}', ] : []), diff --git a/docs/data/charts/lines/ColorScaleNoSnap.js b/docs/data/charts/lines/ColorScaleNoSnap.js index 79a915a28dedb..b88e8815f1413 100644 --- a/docs/data/charts/lines/ColorScaleNoSnap.js +++ b/docs/data/charts/lines/ColorScaleNoSnap.js @@ -103,22 +103,24 @@ export default function ColorScaleNoSnap() { ...(colorX === 'None' ? [' xAxis={[{}]}'] : []), ...(colorX === 'continuous' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: new Date(2019, 1, 1),`, ` max: new Date(2024, 1, 1),`, ` color: ['green', 'orange']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'piecewise' ? [ ' xAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, - ` colors: ['blue', 'red', 'blue'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, + ` colors: ['blue', 'red', 'blue'],`, + ` }`, ' }]}', ] : []), @@ -126,22 +128,24 @@ export default function ColorScaleNoSnap() { ...(colorY === 'None' ? [' yAxis={[{}]}'] : []), ...(colorY === 'continuous' ? [ - ' yAxis={[', - ` {`, + ' yAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -10,`, ` max: 10,`, ` color: ['red', 'green'],`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorY === 'piecewise' ? [ ' yAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [0, 10],`, - ` colors: ['red', 'green', 'blue'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [0, 10],`, + ` colors: ['red', 'green', 'blue'],`, + ` }`, ' }]}', ] : []), diff --git a/docs/data/charts/lines/ColorScaleNoSnap.tsx b/docs/data/charts/lines/ColorScaleNoSnap.tsx index 687ba56d7ee99..164b1c1430e86 100644 --- a/docs/data/charts/lines/ColorScaleNoSnap.tsx +++ b/docs/data/charts/lines/ColorScaleNoSnap.tsx @@ -111,22 +111,24 @@ export default function ColorScaleNoSnap() { ...(colorX === 'None' ? [' xAxis={[{}]}'] : []), ...(colorX === 'continuous' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: new Date(2019, 1, 1),`, ` max: new Date(2024, 1, 1),`, ` color: ['green', 'orange']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'piecewise' ? [ ' xAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, - ` colors: ['blue', 'red', 'blue'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [new Date(2021, 1, 1), new Date(2023, 1, 1)],`, + ` colors: ['blue', 'red', 'blue'],`, + ` }`, ' }]}', ] : []), @@ -135,23 +137,24 @@ export default function ColorScaleNoSnap() { ...(colorY === 'None' ? [' yAxis={[{}]}'] : []), ...(colorY === 'continuous' ? [ - ' yAxis={[', - ` {`, + ' yAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -10,`, ` max: 10,`, ` color: ['red', 'green'],`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorY === 'piecewise' ? [ ' yAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [0, 10],`, - ` colors: ['red', 'green', 'blue'],`, - + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [0, 10],`, + ` colors: ['red', 'green', 'blue'],`, + ` }`, ' }]}', ] : []), diff --git a/docs/data/charts/scatter/ColorScaleNoSnap.js b/docs/data/charts/scatter/ColorScaleNoSnap.js index ea446b982627c..b51c3e27dc990 100644 --- a/docs/data/charts/scatter/ColorScaleNoSnap.js +++ b/docs/data/charts/scatter/ColorScaleNoSnap.js @@ -9,11 +9,13 @@ import HighlightedCode from 'docs/src/modules/components/HighlightedCode'; import { Chance } from 'chance'; +const POINTS_NUMBER = 50; const chance = new Chance(42); export default function ColorScaleNoSnap() { const [colorX, setColorX] = React.useState('piecewise'); const [colorY, setColorY] = React.useState('None'); + const [colorZ, setColorZ] = React.useState('None'); return ( @@ -40,6 +42,18 @@ export default function ColorScaleNoSnap() { piecewise continuous + setColorZ(event.target.value)} + > + None + piecewise + continuous + ordinal + 'A'), + ...[...Array(POINTS_NUMBER)].map(() => 'B'), + ...[...Array(POINTS_NUMBER)].map(() => 'C'), + ...[...Array(POINTS_NUMBER)].map(() => 'D'), + ] + : undefined, + colorMap: + (colorZ === 'continuous' && { + type: 'continuous', + min: -2, + max: 2, + color: ['green', 'orange'], + }) || + (colorZ === 'piecewise' && { + type: 'piecewise', + thresholds: [-1.5, 0, 1.5], + colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'], + }) || + (colorZ === 'ordinal' && { + type: 'ordinal', + values: ['A', 'B', 'C', 'D'], + colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'], + }) || + undefined, + }, + ]} /> ({...point, z: point.x + point.y})) }]}', // ColorX ...(colorX === 'None' ? [' xAxis={[{}]}'] : []), ...(colorX === 'continuous' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -2,`, ` max: 2,`, ` color: ['green', 'orange']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'piecewise' ? [ ' xAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [-1.5, 0, 1.5],`, - ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [-1.5, 0, 1.5],`, + ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` }`, ' }]}', ] : []), @@ -122,22 +170,61 @@ export default function ColorScaleNoSnap() { ...(colorY === 'None' ? [' yAxis={[{}]}'] : []), ...(colorY === 'continuous' ? [ - ' yAxis={[', - ` {`, + ' yAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -2,`, ` max: 2,`, ` color: ['blue', 'red']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorY === 'piecewise' ? [ ' yAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [-1.5, 0, 1.5],`, - ` colors: ['lightblue', 'blue', 'orange', 'red'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [-1.5, 0, 1.5],`, + ` colors: ['lightblue', 'blue', 'orange', 'red'],`, + ` }`, + ' }]}', + ] + : []), + // ColorZ + ...(colorZ === 'None' ? [' zAxis={[{}]}'] : []), + ...(colorZ === 'continuous' + ? [ + ' zAxis={[{', + ` colorMap: {`, + ` type: 'continuous',`, + ` min: -2,`, + ` max: 2,`, + ` color: ['green', 'orange'],`, + ` }`, + ' }]}', + ] + : []), + ...(colorZ === 'piecewise' + ? [ + ' zAxis={[{', + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [-1.5, 0, 1.5],`, + ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` }`, + ' }]}', + ] + : []), + ...(colorZ === 'ordinal' + ? [ + ' zAxis={[{', + ` data: ['A', ..., 'B', ..., 'C', ..., 'D', ...],`, + ` colorMap: {`, + ` type: 'ordinal',`, + ` values: ['A', 'B', 'C', 'D'],`, + ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` }`, ' }]}', ] : []), @@ -150,12 +237,21 @@ export default function ColorScaleNoSnap() { ); } -const series = [{ data: getGaussianSeriesData([0, 0], [1, 1], 200) }].map((s) => ({ +const series = [ + { + data: [ + ...getGaussianSeriesData([-1, -1]), + ...getGaussianSeriesData([-1, 1]), + ...getGaussianSeriesData([1, 1]), + ...getGaussianSeriesData([1, -1]), + ], + }, +].map((s) => ({ ...s, valueFormatter: (v) => `(${v.x.toFixed(1)}, ${v.y.toFixed(1)})`, })); -function getGaussianSeriesData(mean, stdev = [0.3, 0.4], N = 50) { +function getGaussianSeriesData(mean, stdev = [0.5, 0.5], N = 50) { return [...Array(N)].map((_, i) => { const x = Math.sqrt(-2.0 * Math.log(1 - chance.floating({ min: 0, max: 0.99 }))) * @@ -167,6 +263,6 @@ function getGaussianSeriesData(mean, stdev = [0.3, 0.4], N = 50) { Math.cos(2.0 * Math.PI * chance.floating({ min: 0, max: 0.99 })) * stdev[1] + mean[1]; - return { x, y, id: i }; + return { x, y, z: x + y, id: i }; }); } diff --git a/docs/data/charts/scatter/ColorScaleNoSnap.tsx b/docs/data/charts/scatter/ColorScaleNoSnap.tsx index 3ab5d985ef84c..db875306f935f 100644 --- a/docs/data/charts/scatter/ColorScaleNoSnap.tsx +++ b/docs/data/charts/scatter/ColorScaleNoSnap.tsx @@ -9,6 +9,7 @@ import HighlightedCode from 'docs/src/modules/components/HighlightedCode'; import { Chance } from 'chance'; +const POINTS_NUMBER = 50; const chance = new Chance(42); export default function ColorScaleNoSnap() { @@ -18,6 +19,9 @@ export default function ColorScaleNoSnap() { const [colorY, setColorY] = React.useState<'None' | 'piecewise' | 'continuous'>( 'None', ); + const [colorZ, setColorZ] = React.useState< + 'None' | 'piecewise' | 'continuous' | 'ordinal' + >('None'); return ( @@ -48,6 +52,22 @@ export default function ColorScaleNoSnap() { piecewise continuous + + setColorZ( + event.target.value as 'None' | 'piecewise' | 'continuous' | 'ordinal', + ) + } + > + None + piecewise + continuous + ordinal + 'A'), + ...[...Array(POINTS_NUMBER)].map(() => 'B'), + ...[...Array(POINTS_NUMBER)].map(() => 'C'), + ...[...Array(POINTS_NUMBER)].map(() => 'D'), + ] + : undefined, + colorMap: + (colorZ === 'continuous' && { + type: 'continuous', + min: -2, + max: 2, + color: ['green', 'orange'], + }) || + (colorZ === 'piecewise' && { + type: 'piecewise', + thresholds: [-1.5, 0, 1.5], + colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'], + }) || + (colorZ === 'ordinal' && { + type: 'ordinal', + + values: ['A', 'B', 'C', 'D'], + colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'], + }) || + undefined, + }, + ]} /> ({...point, z: point.x + point.y})) }]}', // ColorX ...(colorX === 'None' ? [' xAxis={[{}]}'] : []), ...(colorX === 'continuous' ? [ - ' xAxis={[', - ` {`, + ' xAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -2,`, ` max: 2,`, ` color: ['green', 'orange']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorX === 'piecewise' ? [ ' xAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [-1.5, 0, 1.5],`, - ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [-1.5, 0, 1.5],`, + ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` }`, ' }]}', ] : []), @@ -131,22 +186,62 @@ export default function ColorScaleNoSnap() { ...(colorY === 'None' ? [' yAxis={[{}]}'] : []), ...(colorY === 'continuous' ? [ - ' yAxis={[', - ` {`, + ' yAxis={[{', + ` colorMap: {`, ` type: 'continuous',`, ` min: -2,`, ` max: 2,`, ` color: ['blue', 'red']`, ` }`, - ' ]}', + ' }]}', ] : []), ...(colorY === 'piecewise' ? [ ' yAxis={[{', - ` type: 'piecewise',`, - ` thresholds: [-1.5, 0, 1.5],`, - ` colors: ['lightblue', 'blue', 'orange', 'red'],`, + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [-1.5, 0, 1.5],`, + ` colors: ['lightblue', 'blue', 'orange', 'red'],`, + ` }`, + ' }]}', + ] + : []), + + // ColorZ + ...(colorZ === 'None' ? [' zAxis={[{}]}'] : []), + ...(colorZ === 'continuous' + ? [ + ' zAxis={[{', + ` colorMap: {`, + ` type: 'continuous',`, + ` min: -2,`, + ` max: 2,`, + ` color: ['green', 'orange'],`, + ` }`, + ' }]}', + ] + : []), + ...(colorZ === 'piecewise' + ? [ + ' zAxis={[{', + ` colorMap: {`, + ` type: 'piecewise',`, + ` thresholds: [-1.5, 0, 1.5],`, + ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` }`, + ' }]}', + ] + : []), + ...(colorZ === 'ordinal' + ? [ + ' zAxis={[{', + ` data: ['A', ..., 'B', ..., 'C', ..., 'D', ...],`, + ` colorMap: {`, + ` type: 'ordinal',`, + ` values: ['A', 'B', 'C', 'D'],`, + ` colors: ['#d01c8b', '#f1b6da', '#b8e186', '#4dac26'],`, + ` }`, ' }]}', ] : []), @@ -159,14 +254,23 @@ export default function ColorScaleNoSnap() { ); } -const series = [{ data: getGaussianSeriesData([0, 0], [1, 1], 200) }].map((s) => ({ +const series = [ + { + data: [ + ...getGaussianSeriesData([-1, -1]), + ...getGaussianSeriesData([-1, 1]), + ...getGaussianSeriesData([1, 1]), + ...getGaussianSeriesData([1, -1]), + ], + }, +].map((s) => ({ ...s, valueFormatter: (v: ScatterValueType) => `(${v.x.toFixed(1)}, ${v.y.toFixed(1)})`, })); function getGaussianSeriesData( mean: [number, number], - stdev: [number, number] = [0.3, 0.4], + stdev: [number, number] = [0.5, 0.5], N: number = 50, ) { return [...Array(N)].map((_, i) => { @@ -180,6 +284,6 @@ function getGaussianSeriesData( Math.cos(2.0 * Math.PI * chance.floating({ min: 0, max: 0.99 })) * stdev[1] + mean[1]; - return { x, y, id: i }; + return { x, y, z: x + y, id: i }; }); } diff --git a/docs/data/charts/scatter/scatter.md b/docs/data/charts/scatter/scatter.md index 89dbca75e8a12..1ebeebf8ec9f1 100644 --- a/docs/data/charts/scatter/scatter.md +++ b/docs/data/charts/scatter/scatter.md @@ -60,9 +60,31 @@ As with other charts, you can modify the [series color](/x/react-charts/styling/ You can also modify the color by using axes `colorMap` which maps values to colors. The scatter charts use by priority: -1. The y-axis color -2. The x-axis color -3. The series color +1. The z-axis color +2. The y-axis color +3. The x-axis color +4. The series color + +:::info +The z-axis is a third axis that allows to customize scatter points independently from their position. +It can be provided with `zAxis` props, or with `ZAxisContextProvider` when using composition. + +The value to map can either come from the `z` property of series data, or from the zAxis data. +Here are three ways to set z value to 5. + +```jsx + +``` + +::: Learn more about the `colorMap` properties in the [Styling docs](/x/react-charts/styling/#values-color). diff --git a/docs/pages/x/api/charts/scatter-chart.json b/docs/pages/x/api/charts/scatter-chart.json index b0314f7238436..c2e6985c1e8ce 100644 --- a/docs/pages/x/api/charts/scatter-chart.json +++ b/docs/pages/x/api/charts/scatter-chart.json @@ -95,6 +95,12 @@ "name": "arrayOf", "description": "Array<{ axisId?: number
| string, classes?: object, colorMap?: { color: Array<string>
| func, max?: Date
| number, min?: Date
| number, type: 'continuous' }
| { colors: Array<string>, thresholds: Array<Date
| number>, type: 'piecewise' }
| { colors: Array<string>, type: 'ordinal', unknownColor?: string, values?: Array<Date
| number
| string> }, data?: array, dataKey?: string, disableLine?: bool, disableTicks?: bool, fill?: string, hideTooltip?: bool, id?: number
| string, label?: string, labelFontSize?: number, labelStyle?: object, max?: Date
| number, min?: Date
| number, position?: 'bottom'
| 'left'
| 'right'
| 'top', reverse?: bool, scaleType?: 'band'
| 'linear'
| 'log'
| 'point'
| 'pow'
| 'sqrt'
| 'time'
| 'utc', slotProps?: object, slots?: object, stroke?: string, tickFontSize?: number, tickInterval?: 'auto'
| array
| func, tickLabelInterval?: 'auto'
| func, tickLabelPlacement?: 'middle'
| 'tick', tickLabelStyle?: object, tickMaxStep?: number, tickMinStep?: number, tickNumber?: number, tickPlacement?: 'end'
| 'extremities'
| 'middle'
| 'start', tickSize?: number, valueFormatter?: func }>" } + }, + "zAxis": { + "type": { + "name": "arrayOf", + "description": "Array<{ colorMap?: { color: Array<string>
| func, max?: Date
| number, min?: Date
| number, type: 'continuous' }
| { colors: Array<string>, thresholds: Array<Date
| number>, type: 'piecewise' }
| { colors: Array<string>, type: 'ordinal', unknownColor?: string, values?: Array<Date
| number
| string> }, data?: array, dataKey?: string, id?: string }>" + } } }, "name": "ScatterChart", diff --git a/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json b/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json index ad851d29ce895..06e037d0ed66a 100644 --- a/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json +++ b/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json @@ -59,7 +59,8 @@ }, "yAxis": { "description": "The configuration of the y-axes. If not provided, a default axis config is used." - } + }, + "zAxis": { "description": "The configuration of the z-axes." } }, "classDescriptions": {}, "slotDescriptions": { diff --git a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx index 3ec415c3409d8..8eaadafc69c06 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx @@ -11,6 +11,7 @@ import { ChartsTooltipClasses } from './chartsTooltipClasses'; import { DefaultChartsAxisTooltipContent } from './DefaultChartsAxisTooltipContent'; import { isCartesianSeriesType } from './utils'; import colorGetter from '../internals/colorGetter'; +import { ZAxisContext } from '../context/ZAxisContextProvider'; type ChartSeriesDefaultizedWithColorGetter = ChartSeriesDefaultized & { getColor: (dataIndex: number) => string; @@ -59,6 +60,7 @@ function ChartsAxisTooltipContent(props: { const axisValue = isXaxis ? axisData.x && axisData.x.value : axisData.y && axisData.y.value; const { xAxisIds, xAxis, yAxisIds, yAxis } = React.useContext(CartesianContext); + const { zAxisIds, zAxis } = React.useContext(ZAxisContext); const series = React.useContext(SeriesContext); const USED_AXIS_ID = isXaxis ? xAxisIds[0] : yAxisIds[0]; @@ -74,17 +76,31 @@ function ChartsAxisTooltipContent(props: { if (axisKey === undefined || axisKey === USED_AXIS_ID) { const seriesToAdd = series[seriesType]!.series[seriesId]; - const color = colorGetter( - seriesToAdd, - xAxis[seriesToAdd.xAxisKey ?? xAxisIds[0]], - yAxis[seriesToAdd.yAxisKey ?? yAxisIds[0]], - ); - rep.push({ ...seriesToAdd, getColor: color }); + let getColor: (index: number) => string; + switch (seriesToAdd.type) { + case 'scatter': + getColor = colorGetter( + seriesToAdd, + xAxis[seriesToAdd.xAxisKey ?? xAxisIds[0]], + yAxis[seriesToAdd.yAxisKey ?? yAxisIds[0]], + zAxis[seriesToAdd.zAxisKey ?? zAxisIds[0]], + ); + break; + default: + getColor = colorGetter( + seriesToAdd, + xAxis[seriesToAdd.xAxisKey ?? xAxisIds[0]], + yAxis[seriesToAdd.yAxisKey ?? yAxisIds[0]], + ); + break; + } + + rep.push({ ...seriesToAdd, getColor }); } }); }); return rep; - }, [USED_AXIS_ID, isXaxis, series, xAxis, xAxisIds, yAxis, yAxisIds]); + }, [USED_AXIS_ID, isXaxis, series, xAxis, xAxisIds, yAxis, yAxisIds, zAxis, zAxisIds]); const relevantAxis = React.useMemo(() => { return isXaxis ? xAxis[USED_AXIS_ID] : yAxis[USED_AXIS_ID]; diff --git a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx index cdf2ff3a0105d..a61a08294ebca 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx @@ -9,6 +9,7 @@ import { ChartsTooltipClasses } from './chartsTooltipClasses'; import { DefaultChartsItemTooltipContent } from './DefaultChartsItemTooltipContent'; import { CartesianContext } from '../context/CartesianContextProvider'; import colorGetter from '../internals/colorGetter'; +import { ZAxisContext } from '../context/ZAxisContextProvider'; export type ChartsItemContentProps = { /** @@ -45,20 +46,34 @@ function ChartsItemTooltipContent(props: { itemData.seriesId ] as ChartSeriesDefaultized; - const axisData = React.useContext(CartesianContext); + const { xAxis, yAxis, xAxisIds, yAxisIds } = React.useContext(CartesianContext); + const { zAxis, zAxisIds } = React.useContext(ZAxisContext); - const { xAxis, yAxis, xAxisIds, yAxisIds } = axisData; const defaultXAxisId = xAxisIds[0]; const defaultYAxisId = yAxisIds[0]; + const defaultZAxisId = zAxisIds[0]; - const getColor = - series.type === 'pie' - ? colorGetter(series) - : colorGetter( - series, - xAxis[series.xAxisKey ?? defaultXAxisId], - yAxis[series.yAxisKey ?? defaultYAxisId], - ); + let getColor: (index: number) => string; + switch (series.type) { + case 'pie': + getColor = colorGetter(series); + break; + case 'scatter': + getColor = colorGetter( + series, + xAxis[series.xAxisKey ?? defaultXAxisId], + yAxis[series.yAxisKey ?? defaultYAxisId], + zAxis[series.zAxisKey ?? defaultZAxisId], + ); + break; + default: + getColor = colorGetter( + series, + xAxis[series.xAxisKey ?? defaultXAxisId], + yAxis[series.yAxisKey ?? defaultYAxisId], + ); + break; + } const Content = content ?? DefaultChartsItemTooltipContent; const chartTooltipContentProps = useSlotProps({ diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.tsx index 9f14ccef431e2..886cc4c33faf3 100644 --- a/packages/x-charts/src/ScatterChart/ScatterChart.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterChart.tsx @@ -32,6 +32,7 @@ import { ChartsVoronoiHandlerProps, } from '../ChartsVoronoiHandler/ChartsVoronoiHandler'; import { ChartsGrid, ChartsGridProps } from '../ChartsGrid'; +import { ZAxisContextProvider, ZAxisContextProviderProps } from '../context/ZAxisContextProvider'; export interface ScatterChartSlots extends ChartsAxisSlots, @@ -46,6 +47,7 @@ export interface ScatterChartSlotProps export interface ScatterChartProps extends Omit, + Omit, Omit, Omit { /** @@ -109,6 +111,7 @@ const ScatterChart = React.forwardRef(function ScatterChart(props: ScatterChartP const { xAxis, yAxis, + zAxis, series, tooltip, axisHighlight, @@ -142,31 +145,35 @@ const ScatterChart = React.forwardRef(function ScatterChart(props: ScatterChartP yAxis={yAxis} sx={sx} > - {!disableVoronoi && ( - - )} + + {!disableVoronoi && ( + + )} - - {grid && } - - - - - {children} + + {grid && } + + + + + {children} + ); }); @@ -583,6 +590,43 @@ ScatterChart.propTypes = { valueFormatter: PropTypes.func, }), ), + /** + * The configuration of the z-axes. + */ + zAxis: PropTypes.arrayOf( + PropTypes.shape({ + colorMap: PropTypes.oneOfType([ + PropTypes.shape({ + color: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string.isRequired), + PropTypes.func, + ]).isRequired, + max: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]), + min: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]), + type: PropTypes.oneOf(['continuous']).isRequired, + }), + PropTypes.shape({ + colors: PropTypes.arrayOf(PropTypes.string).isRequired, + thresholds: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + ).isRequired, + type: PropTypes.oneOf(['piecewise']).isRequired, + }), + PropTypes.shape({ + colors: PropTypes.arrayOf(PropTypes.string).isRequired, + type: PropTypes.oneOf(['ordinal']).isRequired, + unknownColor: PropTypes.string, + values: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, + ), + }), + ]), + data: PropTypes.array, + dataKey: PropTypes.string, + id: PropTypes.string, + }), + ), } as any; export { ScatterChart }; diff --git a/packages/x-charts/src/ScatterChart/ScatterPlot.tsx b/packages/x-charts/src/ScatterChart/ScatterPlot.tsx index 5c19c2cca9c61..f714d8fb513ae 100644 --- a/packages/x-charts/src/ScatterChart/ScatterPlot.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterPlot.tsx @@ -4,6 +4,7 @@ import { Scatter, ScatterProps } from './Scatter'; import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; import getColor from './getColor'; +import { ZAxisContext } from '../context/ZAxisContextProvider'; export interface ScatterPlotSlots { scatter?: React.JSXElementConstructor; @@ -40,6 +41,7 @@ function ScatterPlot(props: ScatterPlotProps) { const { slots, slotProps, onItemClick } = props; const seriesData = React.useContext(SeriesContext).scatter; const axisData = React.useContext(CartesianContext); + const { zAxis, zAxisIds } = React.useContext(ZAxisContext); if (seriesData === undefined) { return null; @@ -49,17 +51,20 @@ function ScatterPlot(props: ScatterPlotProps) { const defaultXAxisId = xAxisIds[0]; const defaultYAxisId = yAxisIds[0]; + const defaultZAxisId = zAxisIds[0]; + const ScatterItems = slots?.scatter ?? Scatter; return ( {seriesOrder.map((seriesId) => { - const { id, xAxisKey, yAxisKey, markerSize, color } = series[seriesId]; + const { id, xAxisKey, yAxisKey, zAxisKey, markerSize, color } = series[seriesId]; const colorGetter = getColor( series[seriesId], xAxis[xAxisKey ?? defaultXAxisId], yAxis[yAxisKey ?? defaultYAxisId], + zAxis[zAxisKey ?? defaultZAxisId], ); const xScale = xAxis[xAxisKey ?? defaultXAxisId].scale; const yScale = yAxis[yAxisKey ?? defaultYAxisId].scale; diff --git a/packages/x-charts/src/ScatterChart/getColor.ts b/packages/x-charts/src/ScatterChart/getColor.ts index 38cb1dc07f41b..79166556b1437 100644 --- a/packages/x-charts/src/ScatterChart/getColor.ts +++ b/packages/x-charts/src/ScatterChart/getColor.ts @@ -1,14 +1,33 @@ import { AxisDefaultized } from '../models/axis'; +import { ZAxisDefaultized } from '../models/z-axis'; import { DefaultizedScatterSeriesType } from '../models/seriesType/scatter'; export default function getColor( series: DefaultizedScatterSeriesType, xAxis: AxisDefaultized, yAxis: AxisDefaultized, + zAxis?: ZAxisDefaultized, ) { + const zColorScale = zAxis?.colorScale; const yColorScale = yAxis.colorScale; const xColorScale = xAxis.colorScale; + if (zColorScale) { + return (dataIndex: number) => { + if (zAxis?.data?.[dataIndex] !== undefined) { + const color = zColorScale(zAxis?.data?.[dataIndex]); + if (color !== null) { + return color; + } + } + const value = series.data[dataIndex]; + const color = value === null ? series.color : zColorScale(value.z); + if (color === null) { + return series.color; + } + return color; + }; + } if (yColorScale) { return (dataIndex: number) => { const value = series.data[dataIndex]; diff --git a/packages/x-charts/src/context/ZAxisContextProvider.tsx b/packages/x-charts/src/context/ZAxisContextProvider.tsx new file mode 100644 index 0000000000000..df992559856ee --- /dev/null +++ b/packages/x-charts/src/context/ZAxisContextProvider.tsx @@ -0,0 +1,134 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { DatasetType } from '../models/seriesType/config'; +import { MakeOptional } from '../models/helpers'; +import { getColorScale, getOrdinalColorScale } from '../internals/colorScale'; +import { ZAxisConfig, ZAxisDefaultized } from '../models/z-axis'; + +export type ZAxisContextProviderProps = { + /** + * The configuration of the z-axes. + */ + zAxis?: MakeOptional[]; + /** + * An array of objects that can be used to populate series and axes data using their `dataKey` property. + */ + dataset?: DatasetType; + children: React.ReactNode; +}; + +type DefaultizedZAxisConfig = { + [axisKey: string]: ZAxisDefaultized; +}; + +export const ZAxisContext = React.createContext<{ + /** + * Mapping from z-axis key to scaling configuration. + */ + zAxis: DefaultizedZAxisConfig; + /** + * The z-axes IDs sorted by order they got provided. + */ + zAxisIds: string[]; +}>({ zAxis: {}, zAxisIds: [] }); + +if (process.env.NODE_ENV !== 'production') { + ZAxisContext.displayName = 'ZAxisContext'; +} + +function ZAxisContextProvider(props: ZAxisContextProviderProps) { + const { zAxis: inZAxis, dataset, children } = props; + + const zAxis = React.useMemo( + () => + inZAxis?.map((axisConfig) => { + const dataKey = axisConfig.dataKey; + if (dataKey === undefined || axisConfig.data !== undefined) { + return axisConfig; + } + if (dataset === undefined) { + throw Error('MUI X Charts: z-axis uses `dataKey` but no `dataset` is provided.'); + } + return { + ...axisConfig, + data: dataset.map((d) => d[dataKey]), + }; + }), + [inZAxis, dataset], + ); + + const value = React.useMemo(() => { + const allZAxis: ZAxisConfig[] = + zAxis?.map((axis, index) => ({ id: `defaultized-z-axis-${index}`, ...axis })) ?? []; + + const completedZAxis: DefaultizedZAxisConfig = {}; + allZAxis.forEach((axis) => { + completedZAxis[axis.id] = { + ...axis, + colorScale: + axis.colorMap && + (axis.colorMap.type === 'ordinal' && axis.data + ? getOrdinalColorScale({ values: axis.data, ...axis.colorMap }) + : getColorScale(axis.colorMap)), + }; + }); + + return { + zAxis: completedZAxis, + zAxisIds: allZAxis.map(({ id }) => id), + }; + }, [zAxis]); + + return {children}; +} + +ZAxisContextProvider.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + children: PropTypes.node, + /** + * An array of objects that can be used to populate series and axes data using their `dataKey` property. + */ + dataset: PropTypes.arrayOf(PropTypes.object), + /** + * The configuration of the z-axes. + */ + zAxis: PropTypes.arrayOf( + PropTypes.shape({ + colorMap: PropTypes.oneOfType([ + PropTypes.shape({ + color: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string.isRequired), + PropTypes.func, + ]).isRequired, + max: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]), + min: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]), + type: PropTypes.oneOf(['continuous']).isRequired, + }), + PropTypes.shape({ + colors: PropTypes.arrayOf(PropTypes.string).isRequired, + thresholds: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + ).isRequired, + type: PropTypes.oneOf(['piecewise']).isRequired, + }), + PropTypes.shape({ + colors: PropTypes.arrayOf(PropTypes.string).isRequired, + type: PropTypes.oneOf(['ordinal']).isRequired, + unknownColor: PropTypes.string, + values: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, + ), + }), + ]), + data: PropTypes.array, + dataKey: PropTypes.string, + id: PropTypes.string, + }), + ), +} as any; + +export { ZAxisContextProvider }; diff --git a/packages/x-charts/src/context/index.ts b/packages/x-charts/src/context/index.ts index 58cbd7715d358..b2c53e2bb04fa 100644 --- a/packages/x-charts/src/context/index.ts +++ b/packages/x-charts/src/context/index.ts @@ -1 +1,3 @@ export type { HighlightOptions, FadeOptions, HighlightScope } from './HighlightProvider'; +export { ZAxisContextProvider } from './ZAxisContextProvider'; +export type { ZAxisContextProviderProps } from './ZAxisContextProvider'; diff --git a/packages/x-charts/src/internals/colorGetter.ts b/packages/x-charts/src/internals/colorGetter.ts index 27aa84b3c1a12..0b022ff91e258 100644 --- a/packages/x-charts/src/internals/colorGetter.ts +++ b/packages/x-charts/src/internals/colorGetter.ts @@ -9,16 +9,19 @@ import { DefaultizedScatterSeriesType, } from '../models'; import { AxisDefaultized } from '../models/axis'; +import { ZAxisDefaultized } from '../models/z-axis'; function getColor(series: DefaultizedPieSeriesType): (dataIndex: number) => string; function getColor( - series: - | DefaultizedBarSeriesType - | DefaultizedLineSeriesType - | DefaultizedScatterSeriesType - | DefaultizedPieSeriesType, + series: DefaultizedBarSeriesType | DefaultizedLineSeriesType, + xAxis: AxisDefaultized, + yAxis: AxisDefaultized, +): (dataIndex: number) => string; +function getColor( + series: DefaultizedScatterSeriesType, xAxis: AxisDefaultized, yAxis: AxisDefaultized, + zAxis?: ZAxisDefaultized, ): (dataIndex: number) => string; function getColor( series: @@ -28,6 +31,7 @@ function getColor( | DefaultizedPieSeriesType, xAxis?: AxisDefaultized, yAxis?: AxisDefaultized, + zAxis?: ZAxisDefaultized, ): (dataIndex: number) => string { if (xAxis !== undefined && yAxis !== undefined) { if (series.type === 'bar') { @@ -39,7 +43,7 @@ function getColor( } if (series.type === 'scatter') { - return getScatterColor(series, xAxis, yAxis); + return getScatterColor(series, xAxis, yAxis, zAxis); } } if (series.type === 'pie') { diff --git a/packages/x-charts/src/models/colorMapping.ts b/packages/x-charts/src/models/colorMapping.ts index 045de15faa73a..ba5dd13ecd5a6 100644 --- a/packages/x-charts/src/models/colorMapping.ts +++ b/packages/x-charts/src/models/colorMapping.ts @@ -11,7 +11,7 @@ export interface ContinuousColorConfig { */ max?: Value; /** - * The colors to render. Can either be and array with the extrem colors, or an interpolation function. + * The colors to render. It can be an array with the extremum colors, or an interpolation function. */ color: [string, string] | ((t: number) => string); } @@ -24,7 +24,7 @@ export interface PiecewiseColorConfig { thresholds: Value[]; /** * The colors used for each band defined by `thresholds`. - * Should contain N+1 colors with N the number of thresholds. + * Should contain N+1 colors, where N is the number of thresholds. */ colors: string[]; } @@ -38,6 +38,7 @@ export interface OrdinalColorConfig { values?: Value[]; /** * The color palette. + * Items equal to `values[k]` will get the color of `colors[k]`. */ colors: string[]; /** diff --git a/packages/x-charts/src/models/seriesType/scatter.ts b/packages/x-charts/src/models/seriesType/scatter.ts index 9c2d9941f95ba..48f1ec7ef98b2 100644 --- a/packages/x-charts/src/models/seriesType/scatter.ts +++ b/packages/x-charts/src/models/seriesType/scatter.ts @@ -4,6 +4,7 @@ import { CartesianSeriesType, CommonDefaultizedProps, CommonSeriesType, SeriesId export type ScatterValueType = { x: number; y: number; + z?: any; /** * A unique identifier for the scatter point */ @@ -20,6 +21,10 @@ export interface ScatterSeriesType extends CommonSeriesType, C * @default false */ disableHover?: boolean; + /** + * The id of the z-axis used to render the series. + */ + zAxisKey?: string; } /** diff --git a/packages/x-charts/src/models/z-axis.ts b/packages/x-charts/src/models/z-axis.ts new file mode 100644 index 0000000000000..0a493f5d8f680 --- /dev/null +++ b/packages/x-charts/src/models/z-axis.ts @@ -0,0 +1,20 @@ +import type { ScaleOrdinal, ScaleSequential, ScaleThreshold } from 'd3-scale'; +import { ContinuousColorConfig, OrdinalColorConfig, PiecewiseColorConfig } from './colorMapping'; + +export interface ZAxisConfig { + id: string; + data?: V[]; + /** + * The key used to retrieve `data` from the `dataset` prop. + */ + dataKey?: string; + colorMap?: OrdinalColorConfig | ContinuousColorConfig | PiecewiseColorConfig; +} + +export interface ZAxisDefaultized extends ZAxisConfig { + colorScale?: + | ScaleOrdinal + | ScaleOrdinal + | ScaleSequential + | ScaleThreshold; +} diff --git a/scripts/x-charts.exports.json b/scripts/x-charts.exports.json index a58bf1eee63c1..8e34fbddc6b84 100644 --- a/scripts/x-charts.exports.json +++ b/scripts/x-charts.exports.json @@ -267,5 +267,7 @@ { "name": "useGaugeState", "kind": "Function" }, { "name": "useSvgRef", "kind": "Function" }, { "name": "useXScale", "kind": "Function" }, - { "name": "useYScale", "kind": "Function" } + { "name": "useYScale", "kind": "Function" }, + { "name": "ZAxisContextProvider", "kind": "Function" }, + { "name": "ZAxisContextProviderProps", "kind": "TypeAlias" } ]