Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[charts] Add z-axis to colorize scatter charts #12738

Merged
merged 11 commits into from
Apr 19, 2024
60 changes: 57 additions & 3 deletions docs/data/charts/scatter/ColorScaleNoSnap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Stack direction="column" spacing={1} sx={{ width: '100%', maxWidth: 600 }}>
Expand All @@ -40,6 +42,18 @@ export default function ColorScaleNoSnap() {
<MenuItem value="piecewise">piecewise</MenuItem>
<MenuItem value="continuous">continuous</MenuItem>
</TextField>
<TextField
select
sx={{ minWidth: 150 }}
label="z-axis colorMap"
value={colorZ}
onChange={(event) => setColorZ(event.target.value)}
>
<MenuItem value="None">None</MenuItem>
<MenuItem value="piecewise">piecewise</MenuItem>
<MenuItem value="continuous">continuous</MenuItem>
<MenuItem value="ordinal">ordinal</MenuItem>
</TextField>
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
</Stack>

<ScatterChart
Expand Down Expand Up @@ -90,6 +104,37 @@ export default function ColorScaleNoSnap() {
undefined,
},
]}
zAxis={[
{
data:
colorZ === 'ordinal'
? [
...[...Array(POINTS_NUMBER)].map(() => '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,
},
]}
/>
<HighlightedCode
code={[
Expand Down Expand Up @@ -150,12 +195,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 }))) *
Expand All @@ -167,6 +221,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 };
});
}
67 changes: 64 additions & 3 deletions docs/data/charts/scatter/ColorScaleNoSnap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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 (
<Stack direction="column" spacing={1} sx={{ width: '100%', maxWidth: 600 }}>
Expand Down Expand Up @@ -48,6 +52,22 @@ export default function ColorScaleNoSnap() {
<MenuItem value="piecewise">piecewise</MenuItem>
<MenuItem value="continuous">continuous</MenuItem>
</TextField>
<TextField
select
sx={{ minWidth: 150 }}
label="z-axis colorMap"
value={colorZ}
onChange={(event) =>
setColorZ(
event.target.value as 'None' | 'piecewise' | 'continuous' | 'ordinal',
)
}
>
<MenuItem value="None">None</MenuItem>
<MenuItem value="piecewise">piecewise</MenuItem>
<MenuItem value="continuous">continuous</MenuItem>
<MenuItem value="ordinal">ordinal</MenuItem>
</TextField>
</Stack>

<ScatterChart
Expand Down Expand Up @@ -98,6 +118,38 @@ export default function ColorScaleNoSnap() {
undefined,
},
]}
zAxis={[
{
data:
colorZ === 'ordinal'
? [
...[...Array(POINTS_NUMBER)].map(() => '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,
},
]}
/>
<HighlightedCode
code={[
Expand Down Expand Up @@ -159,14 +211,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) => {
Expand All @@ -180,6 +241,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 };
});
}
28 changes: 25 additions & 3 deletions docs/data/charts/scatter/scatter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

Is this an API you saw on another library?
It's clearly a valid use-case, but I'm curious to know if this is the classic way of solving it

Copy link
Member Author

Choose a reason for hiding this comment

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

The naming is used in Rechart

https://recharts.org/en-US/examples/BubbleChart

<ZAxis type="number" dataKey="value" domain={domain} range={range} />

For Echart, they have a similar idea called visualMap

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
<ScatterChart
// First option
series={[{ data: [{ id: 0, x: 1, y: 1, z: 5 }] }]}
// Second option
zAxis={[{ data: [5] }]}
// Third option
dataset={[{ price: 5 }]}
zAxis={[{ dataKey: 'price' }]}
/>
```

:::

Learn more about the `colorMap` properties in the [Styling docs](/x/react-charts/styling/#values-color).

Expand Down
6 changes: 6 additions & 0 deletions docs/pages/x/api/charts/scatter-chart.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@
"name": "arrayOf",
"description": "Array&lt;{ axisId?: number<br>&#124;&nbsp;string, classes?: object, colorMap?: { color: Array&lt;string&gt;<br>&#124;&nbsp;func, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, type: 'continuous' }<br>&#124;&nbsp;{ colors: Array&lt;string&gt;, thresholds: Array&lt;Date<br>&#124;&nbsp;number&gt;, type: 'piecewise' }<br>&#124;&nbsp;{ colors: Array&lt;string&gt;, type: 'ordinal', unknownColor?: string, values?: Array&lt;Date<br>&#124;&nbsp;number<br>&#124;&nbsp;string&gt; }, data?: array, dataKey?: string, disableLine?: bool, disableTicks?: bool, fill?: string, hideTooltip?: bool, id?: number<br>&#124;&nbsp;string, label?: string, labelFontSize?: number, labelStyle?: object, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, position?: 'bottom'<br>&#124;&nbsp;'left'<br>&#124;&nbsp;'right'<br>&#124;&nbsp;'top', reverse?: bool, scaleType?: 'band'<br>&#124;&nbsp;'linear'<br>&#124;&nbsp;'log'<br>&#124;&nbsp;'point'<br>&#124;&nbsp;'pow'<br>&#124;&nbsp;'sqrt'<br>&#124;&nbsp;'time'<br>&#124;&nbsp;'utc', slotProps?: object, slots?: object, stroke?: string, tickFontSize?: number, tickInterval?: 'auto'<br>&#124;&nbsp;array<br>&#124;&nbsp;func, tickLabelInterval?: 'auto'<br>&#124;&nbsp;func, tickLabelPlacement?: 'middle'<br>&#124;&nbsp;'tick', tickLabelStyle?: object, tickMaxStep?: number, tickMinStep?: number, tickNumber?: number, tickPlacement?: 'end'<br>&#124;&nbsp;'extremities'<br>&#124;&nbsp;'middle'<br>&#124;&nbsp;'start', tickSize?: number, valueFormatter?: func }&gt;"
}
},
"zAxis": {
"type": {
"name": "arrayOf",
"description": "Array&lt;{ colorMap?: { color: Array&lt;string&gt;<br>&#124;&nbsp;func, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, type: 'continuous' }<br>&#124;&nbsp;{ colors: Array&lt;string&gt;, thresholds: Array&lt;Date<br>&#124;&nbsp;number&gt;, type: 'piecewise' }<br>&#124;&nbsp;{ colors: Array&lt;string&gt;, type: 'ordinal', unknownColor?: string, values?: Array&lt;Date<br>&#124;&nbsp;number<br>&#124;&nbsp;string&gt; }, data?: array, dataKey?: string, id?: string }&gt;"
}
}
},
"name": "ScatterChart",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
},
"yAxis": {
"description": "The configuration of the y-axes. If not provided, a default axis config is used."
},
"zAxis": {
"description": "The configuration of the x-axes. If not provided, a default axis config is used with id set to <code>DEFAULT_X_AXIS_KEY</code>."
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved
}
},
"classDescriptions": {},
Expand Down
24 changes: 17 additions & 7 deletions packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChartSeriesType> & {
getColor: (dataIndex: number) => string;
Expand Down Expand Up @@ -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];
Expand All @@ -74,17 +76,25 @@ 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 });
const getColor =
seriesToAdd.type === 'scatter'
? colorGetter(
seriesToAdd,
xAxis[seriesToAdd.xAxisKey ?? xAxisIds[0]],
yAxis[seriesToAdd.yAxisKey ?? yAxisIds[0]],
zAxis[seriesToAdd.zAxisKey ?? zAxisIds[0]],
)
: colorGetter(
seriesToAdd,
xAxis[seriesToAdd.xAxisKey ?? xAxisIds[0]],
yAxis[seriesToAdd.yAxisKey ?? yAxisIds[0]],
);
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
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];
Expand Down
15 changes: 12 additions & 3 deletions packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends ChartSeriesType = ChartSeriesType> = {
/**
Expand Down Expand Up @@ -45,16 +46,24 @@ function ChartsItemTooltipContent<T extends ChartSeriesType>(props: {
itemData.seriesId
] as ChartSeriesDefaultized<T>;

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.type === 'scatter' &&
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
colorGetter(
series,
xAxis[series.xAxisKey ?? defaultXAxisId],
yAxis[series.yAxisKey ?? defaultYAxisId],
zAxis[series.zAxisKey ?? defaultZAxisId],
)) ||
colorGetter(
series,
xAxis[series.xAxisKey ?? defaultXAxisId],
yAxis[series.yAxisKey ?? defaultYAxisId],
Expand Down