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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[charts] Use plugins to define series extremum and colors #13397

Merged
merged 8 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 38 additions & 28 deletions packages/x-charts/src/ChartContainer/ChartContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SeriesContextProviderProps,
} from '../context/SeriesContextProvider';
import { InteractionProvider } from '../context/InteractionProvider';
import { ColorProvider } from '../context/ColorProvider';
import { useReducedMotion } from '../hooks/useReducedMotion';
import { ChartsSurface, ChartsSurfaceProps } from '../ChartsSurface';
import {
Expand Down Expand Up @@ -57,39 +58,48 @@ const ChartContainer = React.forwardRef(function ChartContainer(props: ChartCont
const svgRef = React.useRef<SVGSVGElement>(null);
const handleRef = useForkRef(ref, svgRef);

const { seriesFormatters } = usePluginsMerge(plugins);
const { xExtremumGetters, yExtremumGetters, seriesFormatters, colorProcessors } =
usePluginsMerge(plugins);
useReducedMotion(); // a11y reduce motion (see: https://react-spring.dev/docs/utilities/use-reduced-motion)

return (
<DrawingProvider width={width} height={height} margin={margin} svgRef={svgRef}>
<SeriesContextProvider
series={series}
colors={colors}
dataset={dataset}
seriesFormatters={seriesFormatters}
>
<CartesianContextProvider xAxis={xAxis} yAxis={yAxis} dataset={dataset}>
<InteractionProvider>
<HighlightedProvider
highlightedItem={highlightedItem}
onHighlightChange={onHighlightChange}
>
<ChartsSurface
width={width}
height={height}
ref={handleRef}
sx={sx}
title={title}
desc={desc}
disableAxisListener={disableAxisListener}
<ColorProvider colorProcessors={colorProcessors}>
<SeriesContextProvider
series={series}
colors={colors}
dataset={dataset}
seriesFormatters={seriesFormatters}
>
<CartesianContextProvider
xAxis={xAxis}
yAxis={yAxis}
dataset={dataset}
xExtremumGetters={xExtremumGetters}
yExtremumGetters={yExtremumGetters}
>
<InteractionProvider>
<HighlightedProvider
highlightedItem={highlightedItem}
onHighlightChange={onHighlightChange}
>
<ChartsAxesGradients />
{children}
</ChartsSurface>
</HighlightedProvider>
</InteractionProvider>
</CartesianContextProvider>
</SeriesContextProvider>
<ChartsSurface
width={width}
height={height}
ref={handleRef}
sx={sx}
title={title}
desc={desc}
disableAxisListener={disableAxisListener}
>
<ChartsAxesGradients />
{children}
</ChartsSurface>
</HighlightedProvider>
</InteractionProvider>
</CartesianContextProvider>
</SeriesContextProvider>
</ColorProvider>
</DrawingProvider>
);
});
Expand Down
44 changes: 24 additions & 20 deletions packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AxisDefaultized } from '../models/axis';
import { ChartsTooltipClasses } from './chartsTooltipClasses';
import { DefaultChartsAxisTooltipContent } from './DefaultChartsAxisTooltipContent';
import { isCartesianSeriesType } from '../internals/isCartesian';
import colorGetter from '../internals/colorGetter';
import { useColorProcessor } from '../hooks/useColor';
import { ZAxisContext } from '../context/ZAxisContextProvider';
import { useSeries } from '../hooks/useSeries';

Expand Down Expand Up @@ -63,6 +63,8 @@ function ChartsAxisTooltipContent(props: {
const { zAxisIds, zAxis } = React.useContext(ZAxisContext);
const series = useSeries();

const colorProcessors = useColorProcessor();

const USED_AXIS_ID = isXaxis ? xAxisIds[0] : yAxisIds[0];

const relevantSeries = React.useMemo(() => {
Expand All @@ -76,31 +78,33 @@ function ChartsAxisTooltipContent(props: {
if (axisKey === undefined || axisKey === USED_AXIS_ID) {
const seriesToAdd = series[seriesType]!.series[seriesId];

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;
}
const zAxisKey = (seriesToAdd as any).zAxisKey ?? zAxisIds[0];

const getColor =
colorProcessors[seriesType]?.(
seriesToAdd as any,
xAxis[seriesToAdd.xAxisKey ?? xAxisIds[0]],
yAxis[seriesToAdd.yAxisKey ?? yAxisIds[0]],
zAxisKey && zAxis[zAxisKey],
) ?? (() => '');

rep.push({ ...seriesToAdd, getColor });
}
});
});
return rep;
}, [USED_AXIS_ID, isXaxis, series, xAxis, xAxisIds, yAxis, yAxisIds, zAxis, zAxisIds]);
}, [
USED_AXIS_ID,
colorProcessors,
isXaxis,
series,
xAxis,
xAxisIds,
yAxis,
yAxisIds,
zAxis,
zAxisIds,
]);

const relevantAxis = React.useMemo(() => {
return isXaxis ? xAxis[USED_AXIS_ID] : yAxis[USED_AXIS_ID];
Expand Down
37 changes: 12 additions & 25 deletions packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { ChartSeriesDefaultized, ChartSeriesType } from '../models/seriesType/co
import { ChartsTooltipClasses } from './chartsTooltipClasses';
import { DefaultChartsItemTooltipContent } from './DefaultChartsItemTooltipContent';
import { CartesianContext } from '../context/CartesianContextProvider';
import colorGetter from '../internals/colorGetter';
import { ZAxisContext } from '../context/ZAxisContextProvider';
import { useColorProcessor } from '../hooks/useColor';
import { useSeries } from '../hooks/useSeries';

export type ChartsItemContentProps<T extends ChartSeriesType = ChartSeriesType> = {
Expand Down Expand Up @@ -46,32 +46,19 @@ function ChartsItemTooltipContent<T extends ChartSeriesType>(props: {

const { xAxis, yAxis, xAxisIds, yAxisIds } = React.useContext(CartesianContext);
const { zAxis, zAxisIds } = React.useContext(ZAxisContext);
const colorProcessors = useColorProcessor();

const defaultXAxisId = xAxisIds[0];
const defaultYAxisId = yAxisIds[0];
const defaultZAxisId = zAxisIds[0];
const xAxisKey = (series as any).xAxisKey ?? xAxisIds[0];
const yAxisKey = (series as any).yAxisKey ?? yAxisIds[0];
const zAxisKey = (series as any).zAxisKey ?? zAxisIds[0];

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 getColor =
colorProcessors[series.type]?.(
series as any,
xAxisKey && xAxis[xAxisKey],
yAxisKey && yAxis[yAxisKey],
zAxisKey && zAxis[zAxisKey],
) ?? (() => '');

const Content = content ?? DefaultChartsItemTooltipContent;
const chartTooltipContentProps = useSlotProps({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,28 @@ function DefaultChartsAxisTooltipContent(props: ChartsAxisContentProps) {
)}

<tbody>
{series
.filter(isCartesianSeries)
.map(({ color, id, label, valueFormatter, data, getColor }) => {
// @ts-ignore
const formattedValue = valueFormatter(data[dataIndex] ?? null, { dataIndex });
if (formattedValue == null) {
return null;
}
const formattedLabel = getLabel(label, 'tooltip');
return (
<ChartsTooltipRow key={id} className={classes.row}>
<ChartsTooltipCell className={clsx(classes.markCell, classes.cell)}>
<ChartsTooltipMark
color={getColor(dataIndex) ?? color}
className={classes.mark}
/>
</ChartsTooltipCell>
<ChartsTooltipCell className={clsx(classes.labelCell, classes.cell)}>
{formattedLabel ? <Typography>{formattedLabel}</Typography> : null}
</ChartsTooltipCell>
<ChartsTooltipCell className={clsx(classes.valueCell, classes.cell)}>
<Typography>{formattedValue}</Typography>
</ChartsTooltipCell>
</ChartsTooltipRow>
);
})}
{series.filter(isCartesianSeries).map(({ id, label, valueFormatter, data, getColor }) => {
// @ts-ignore
const formattedValue = valueFormatter(data[dataIndex] ?? null, { dataIndex });
if (formattedValue == null) {
return null;
}
const formattedLabel = getLabel(label, 'tooltip');
const color = getColor(dataIndex);
return (
<ChartsTooltipRow key={id} className={classes.row}>
<ChartsTooltipCell className={clsx(classes.markCell, classes.cell)}>
{color && <ChartsTooltipMark color={color} className={classes.mark} />}
</ChartsTooltipCell>
<ChartsTooltipCell className={clsx(classes.labelCell, classes.cell)}>
{formattedLabel ? <Typography>{formattedLabel}</Typography> : null}
</ChartsTooltipCell>
<ChartsTooltipCell className={clsx(classes.valueCell, classes.cell)}>
<Typography>{formattedValue}</Typography>
</ChartsTooltipCell>
</ChartsTooltipRow>
);
})}
</tbody>
</ChartsTooltipTable>
</ChartsTooltipPaper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function DefaultChartsItemTooltipContent<T extends ChartSeriesType = ChartSeries
displayedLabel: getLabel(series.data[itemData.dataIndex].label, 'tooltip'),
}
: {
color: getColor(itemData.dataIndex) ?? series.color,
color: getColor(itemData.dataIndex),
displayedLabel: getLabel(series.label, 'tooltip'),
};

Expand Down
52 changes: 22 additions & 30 deletions packages/x-charts/src/context/CartesianContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import * as React from 'react';
import { scaleBand, scalePoint } from 'd3-scale';
import {
getExtremumX as getBarExtremumX,
getExtremumY as getBarExtremumY,
} from '../BarChart/extremums';
import {
getExtremumX as getScatterExtremumX,
getExtremumY as getScatterExtremumY,
} from '../ScatterChart/extremums';
import {
getExtremumX as getLineExtremumX,
getExtremumY as getLineExtremumY,
} from '../LineChart/extremums';
import {
AxisConfig,
AxisDefaultized,
Expand Down Expand Up @@ -59,25 +47,20 @@ export type CartesianContextProviderProps = {
* An array of objects that can be used to populate series and axes data using their `dataKey` property.
*/
dataset?: DatasetType;
/**
* An object with x-axis extremum getters per series type.
*/
xExtremumGetters: ExtremumGettersConfig;
/**
* An object with y-axis extremum getters per series type.
*/
yExtremumGetters: ExtremumGettersConfig;
children: React.ReactNode;
};

const DEFAULT_CATEGORY_GAP_RATIO = 0.2;
const DEFAULT_BAR_GAP_RATIO = 0.1;

// TODO: those might be better placed in a distinct file
const xExtremumGetters: { [T in CartesianChartSeriesType]: ExtremumGetter<T> } = {
bar: getBarExtremumX,
scatter: getScatterExtremumX,
line: getLineExtremumX,
};

const yExtremumGetters: { [T in CartesianChartSeriesType]: ExtremumGetter<T> } = {
bar: getBarExtremumY,
scatter: getScatterExtremumY,
line: getLineExtremumY,
};

type DefaultizedAxisConfig<AxisProps> = {
[axisKey: string]: AxisDefaultized<ScaleName, any, AxisProps>;
};
Expand Down Expand Up @@ -111,7 +94,14 @@ if (process.env.NODE_ENV !== 'production') {
}

function CartesianContextProvider(props: CartesianContextProviderProps) {
const { xAxis: inXAxis, yAxis: inYAxis, dataset, children } = props;
const {
xAxis: inXAxis,
yAxis: inYAxis,
dataset,
xExtremumGetters,
yExtremumGetters,
children,
} = props;
const formattedSeries = useSeries();
const drawingArea = useDrawingArea();

Expand Down Expand Up @@ -156,17 +146,17 @@ function CartesianContextProvider(props: CartesianContextProviderProps) {
acc: ExtremumGetterResult,
chartType: T,
axis: AxisConfig,
getters: { [T2 in CartesianChartSeriesType]: ExtremumGetter<T2> },
getters: { [T2 in CartesianChartSeriesType]?: ExtremumGetter<T2> },
isDefaultAxis: boolean,
): ExtremumGetterResult => {
const getter = getters[chartType];
const series = (formattedSeries[chartType]?.series as Record<SeriesId, ChartSeries<T>>) ?? {};

const [minChartTypeData, maxChartTypeData] = getter({
const [minChartTypeData, maxChartTypeData] = getter?.({
series,
axis,
isDefaultAxis,
});
}) ?? [null, null];

const [minData, maxData] = acc;

Expand All @@ -183,7 +173,7 @@ function CartesianContextProvider(props: CartesianContextProviderProps) {

const getAxisExtremum = (
axis: AxisConfig,
getters: { [T in CartesianChartSeriesType]: ExtremumGetter<T> },
getters: { [T in CartesianChartSeriesType]?: ExtremumGetter<T> },
isDefaultAxis: boolean,
) => {
const charTypes = Object.keys(getters) as CartesianChartSeriesType[];
Expand Down Expand Up @@ -356,7 +346,9 @@ function CartesianContextProvider(props: CartesianContextProviderProps) {
drawingArea.width,
formattedSeries,
xAxis,
xExtremumGetters,
yAxis,
yExtremumGetters,
]);

// @ts-ignore
Expand Down
Loading