From 52330d2703d492c6801c2ceedc7520f48daadd14 Mon Sep 17 00:00:00 2001
From: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com>
Date: Tue, 16 May 2023 20:06:10 +0200
Subject: [PATCH] [charts] Clean the axis rendering (#8948)
---
packages/x-charts/src/Axis/axisClasses.ts | 52 +++++++++
packages/x-charts/src/BarChart/BarChart.tsx | 2 +-
packages/x-charts/src/LineChart/LineChart.tsx | 2 +-
.../src/ScatterChart/ScatterChart.tsx | 2 +-
packages/x-charts/src/XAxis/XAxis.tsx | 99 ++++++++++-------
packages/x-charts/src/YAxis/YAxis.tsx | 101 +++++++++++-------
.../src/context/CartesianContextProvider.tsx | 45 +++++---
packages/x-charts/src/hooks/useScale.ts | 41 ++++---
packages/x-charts/src/hooks/useTicks.ts | 31 +++---
.../components/AxisSharedComponents.tsx | 39 +++++++
packages/x-charts/src/models/axis.ts | 99 +++++++++--------
11 files changed, 350 insertions(+), 163 deletions(-)
create mode 100644 packages/x-charts/src/Axis/axisClasses.ts
create mode 100644 packages/x-charts/src/internals/components/AxisSharedComponents.tsx
diff --git a/packages/x-charts/src/Axis/axisClasses.ts b/packages/x-charts/src/Axis/axisClasses.ts
new file mode 100644
index 0000000000000..3f4188795c46d
--- /dev/null
+++ b/packages/x-charts/src/Axis/axisClasses.ts
@@ -0,0 +1,52 @@
+import {
+ unstable_generateUtilityClass as generateUtilityClass,
+ unstable_generateUtilityClasses as generateUtilityClasses,
+} from '@mui/utils';
+
+export interface AxisClasses {
+ /** Styles applied to the root element. */
+ root: string;
+ /** Styles applied to the main line element. */
+ line: string;
+ /** Styles applied to group ingruding the tick and its label. */
+ tickContainer: string;
+ /** Styles applied to ticks. */
+ tick: string;
+ /** Styles applied to ticks label. */
+ tickLabel: string;
+ /** Styles applied to the axis label. */
+ label: string;
+ /** Styles applied to x axes. */
+ directionX: string;
+ /** Styles applied to y axes. */
+ directionY: string;
+ /** Styles applied to the top axis. */
+ top: string;
+ /** Styles applied to the bottom axis. */
+ bottom: string;
+ /** Styles applied to the left axis. */
+ left: string;
+ /** Styles applied to the right axis. */
+ right: string;
+}
+
+export type XAxisClassKey = keyof AxisClasses;
+
+export function getAxisUtilityClass(slot: string) {
+ return generateUtilityClass('MuiAxis', slot);
+}
+
+export const axisClasses: AxisClasses = generateUtilityClasses('MuiAxis', [
+ 'root',
+ 'line',
+ 'tickContainer',
+ 'tick',
+ 'tickLabel',
+ 'label',
+ 'directionX',
+ 'directionY',
+ 'top',
+ 'bottom',
+ 'left',
+ 'right',
+]);
diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx
index b346dcb40cda1..3b2402348b16f 100644
--- a/packages/x-charts/src/BarChart/BarChart.tsx
+++ b/packages/x-charts/src/BarChart/BarChart.tsx
@@ -57,8 +57,8 @@ export function BarChart(props: BarChartProps) {
tooltip?.trigger !== 'axis' && highlight?.x === 'none' && highlight?.y === 'none'
}
>
-
+
{children}
diff --git a/packages/x-charts/src/LineChart/LineChart.tsx b/packages/x-charts/src/LineChart/LineChart.tsx
index 59e0eaf2ab6d3..a9980eab9a7f6 100644
--- a/packages/x-charts/src/LineChart/LineChart.tsx
+++ b/packages/x-charts/src/LineChart/LineChart.tsx
@@ -56,8 +56,8 @@ export function LineChart(props: LineChartProps) {
tooltip?.trigger !== 'axis' && highlight?.x === 'none' && highlight?.y === 'none'
}
>
-
+
diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.tsx
index 0447f8c085b54..57bdb09bec524 100644
--- a/packages/x-charts/src/ScatterChart/ScatterChart.tsx
+++ b/packages/x-charts/src/ScatterChart/ScatterChart.tsx
@@ -43,8 +43,8 @@ export function ScatterChart(props: ScatterChartProps) {
yAxis={yAxis}
sx={sx}
>
-
+
{children}
diff --git a/packages/x-charts/src/XAxis/XAxis.tsx b/packages/x-charts/src/XAxis/XAxis.tsx
index 748040941c9a3..42d8c41fa75a1 100644
--- a/packages/x-charts/src/XAxis/XAxis.tsx
+++ b/packages/x-charts/src/XAxis/XAxis.tsx
@@ -1,71 +1,98 @@
import * as React from 'react';
+import { unstable_composeClasses as composeClasses } from '@mui/utils';
+import { useThemeProps, useTheme, Theme } from '@mui/material/styles';
import { CartesianContext } from '../context/CartesianContextProvider';
import { DrawingContext } from '../context/DrawingProvider';
import useTicks from '../hooks/useTicks';
import { XAxisProps } from '../models/axis';
+import { getAxisUtilityClass } from '../Axis/axisClasses';
+import { Line, Tick, TickLabel, Label } from '../internals/components/AxisSharedComponents';
-export function XAxis(props: XAxisProps) {
+const useUtilityClasses = (ownerState: XAxisProps & { theme: Theme }) => {
+ const { classes, position } = ownerState;
+ const slots = {
+ root: ['root', 'directionX', position],
+ line: ['line'],
+ tickContainer: ['tickContainer'],
+ tick: ['tick'],
+ tickLabel: ['tickLabel'],
+ label: ['label'],
+ };
+
+ return composeClasses(slots, getAxisUtilityClass, classes);
+};
+const defaultProps = {
+ position: 'bottom',
+ disableLine: false,
+ disableTicks: false,
+ tickFontSize: 10,
+ labelFontSize: 14,
+ tickSize: 6,
+} as const;
+
+export function XAxis(inProps: XAxisProps) {
+ const props = useThemeProps({ props: { ...defaultProps, ...inProps }, name: 'MuiXAxis' });
const {
xAxis: {
- [props.axisId]: { scale: xScale, ...settings },
+ [props.axisId]: { scale: xScale, ticksNumber, ...settings },
},
} = React.useContext(CartesianContext);
+
+ const defaultizedProps = { ...defaultProps, ...settings, ...props };
const {
- position = 'bottom',
- disableLine = false,
- disableTicks = false,
- fill = 'currentColor',
- fontSize = 10,
+ position,
+ disableLine,
+ disableTicks,
+ tickFontSize,
label,
- labelFontSize = 14,
- stroke = 'currentColor',
- tickSize: tickSizeProp = 6,
- } = { ...settings, ...props };
+ labelFontSize,
+ tickSize: tickSizeProp,
+ } = defaultizedProps;
+
+ const theme = useTheme();
+ const classes = useUtilityClasses({ ...defaultizedProps, theme });
const { left, top, width, height } = React.useContext(DrawingContext);
const tickSize = disableTicks ? 4 : tickSizeProp;
- const xTicks = useTicks({ scale: xScale });
-
+ const xTicks = useTicks({ scale: xScale, ticksNumber });
const positionSigne = position === 'bottom' ? 1 : -1;
return (
-
+
{!disableLine && (
-
+
)}
{xTicks.map(({ value, offset }, index) => (
-
- {!disableTicks && (
-
- )}
-
+ {!disableTicks && }
+
{value.toLocaleString()}
-
+
))}
{label && (
-
{label}
-
+
)}
);
diff --git a/packages/x-charts/src/YAxis/YAxis.tsx b/packages/x-charts/src/YAxis/YAxis.tsx
index 56949b3279640..2fe52256926bd 100644
--- a/packages/x-charts/src/YAxis/YAxis.tsx
+++ b/packages/x-charts/src/YAxis/YAxis.tsx
@@ -1,71 +1,100 @@
import * as React from 'react';
+import { unstable_composeClasses as composeClasses } from '@mui/utils';
+import { useThemeProps, useTheme, Theme } from '@mui/material/styles';
import { CartesianContext } from '../context/CartesianContextProvider';
import { DrawingContext } from '../context/DrawingProvider';
import useTicks from '../hooks/useTicks';
import { YAxisProps } from '../models/axis';
+import { Line, Tick, TickLabel, Label } from '../internals/components/AxisSharedComponents';
+import { getAxisUtilityClass } from '../Axis/axisClasses';
-export function YAxis(props: YAxisProps) {
+const useUtilityClasses = (ownerState: YAxisProps & { theme: Theme }) => {
+ const { classes, position } = ownerState;
+ const slots = {
+ root: ['root', 'directionY', position],
+ line: ['line'],
+ tickContainer: ['tickContainer'],
+ tick: ['tick'],
+ tickLabel: ['tickLabel'],
+ label: ['label'],
+ };
+
+ return composeClasses(slots, getAxisUtilityClass, classes);
+};
+
+const defaultProps = {
+ position: 'left',
+ disableLine: false,
+ disableTicks: false,
+ tickFontSize: 10,
+ labelFontSize: 14,
+ tickSize: 6,
+} as const;
+
+export function YAxis(inProps: YAxisProps) {
+ const props = useThemeProps({ props: { ...defaultProps, ...inProps }, name: 'MuiYAxis' });
const {
yAxis: {
- [props.axisId]: { scale: yScale, ...settings },
+ [props.axisId]: { scale: yScale, ticksNumber, ...settings },
},
} = React.useContext(CartesianContext);
+
+ const defaultizedProps = { ...defaultProps, ...settings, ...props };
const {
- position = 'left',
- disableLine = false,
- disableTicks = false,
- fill = 'currentColor',
- fontSize = 10,
+ position,
+ disableLine,
+ disableTicks,
+ tickFontSize,
label,
- labelFontSize = 14,
- stroke = 'currentColor',
- tickSize: tickSizeProp = 6,
- } = { ...settings, ...props };
+ labelFontSize,
+ tickSize: tickSizeProp,
+ } = defaultizedProps;
+
+ const theme = useTheme();
+ const classes = useUtilityClasses({ ...defaultizedProps, theme });
const { left, top, width, height } = React.useContext(DrawingContext);
const tickSize = disableTicks ? 4 : tickSizeProp;
- const yTicks = useTicks({ scale: yScale });
+ const yTicks = useTicks({ scale: yScale, ticksNumber });
const positionSigne = position === 'right' ? 1 : -1;
+
return (
-
+
{!disableLine && (
-
+
)}
{yTicks.map(({ value, offset }, index) => (
-
- {!disableTicks && (
-
- )}
-
+ {!disableTicks && }
+
{value}
-
+
))}
{label && (
-
{label}
-
+
)}
);
diff --git a/packages/x-charts/src/context/CartesianContextProvider.tsx b/packages/x-charts/src/context/CartesianContextProvider.tsx
index 546a22bde47e1..7af63eb7b3fc4 100644
--- a/packages/x-charts/src/context/CartesianContextProvider.tsx
+++ b/packages/x-charts/src/context/CartesianContextProvider.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import { scaleBand } from 'd3-scale';
import {
getExtremumX as getBarExtremumX,
getExtremumY as getBarExtremumY,
@@ -23,6 +24,7 @@ import {
ExtremumGetterResult,
} from '../models/seriesType/config';
import { MakeOptional } from '../models/helpers';
+import { getTicksNumber } from '../hooks/useTicks';
export type CartesianContextProviderProps = {
xAxis?: MakeOptional[];
@@ -127,15 +129,25 @@ export function CartesianContextProvider({
const [minData, maxData] = getAxisExtremum(axis, xExtremumGetters, isDefaultAxis);
const scaleType = axis.scaleType ?? 'linear';
+ const domain = [drawingArea.left, drawingArea.left + drawingArea.width];
+
+ if (scaleType === 'band') {
+ completedXAxis[axis.id] = {
+ ...axis,
+ scaleType,
+ scale: scaleBand(axis.data!, domain),
+ ticksNumber: axis.data!.length,
+ };
+ return;
+ }
+ const extremums = [axis.min ?? minData, axis.max ?? maxData];
+ const ticksNumber = getTicksNumber({ ...axis, domain });
completedXAxis[axis.id] = {
...axis,
scaleType,
- scale: getScale(scaleType)
- // @ts-ignore
- .domain(scaleType === 'band' ? axis.data : [axis.min ?? minData, axis.max ?? maxData])
- // @ts-ignore
- .range([drawingArea.left, drawingArea.left + drawingArea.width]),
- };
+ scale: getScale(scaleType, extremums, domain).nice(ticksNumber),
+ ticksNumber,
+ } as AxisDefaultized;
});
const allYAxis: AxisConfig[] = [
@@ -149,17 +161,26 @@ export function CartesianContextProvider({
allYAxis.forEach((axis, axisIndex) => {
const isDefaultAxis = axisIndex === 0;
const [minData, maxData] = getAxisExtremum(axis, yExtremumGetters, isDefaultAxis);
+ const domain = [drawingArea.top + drawingArea.height, drawingArea.top];
const scaleType: ScaleName = axis.scaleType ?? 'linear';
+ if (scaleType === 'band') {
+ completedYAxis[axis.id] = {
+ ...axis,
+ scaleType,
+ scale: scaleBand(axis.data!, domain),
+ ticksNumber: axis.data!.length,
+ };
+ return;
+ }
+ const extremums = [axis.min ?? minData, axis.max ?? maxData];
+ const ticksNumber = getTicksNumber({ ...axis, domain });
completedYAxis[axis.id] = {
...axis,
scaleType,
- scale: getScale(scaleType)
- // @ts-ignore
- .domain(scaleType === 'band' ? axis.data : [axis.min ?? minData, axis.max ?? maxData])
- // @ts-ignore
- .range([drawingArea.top + drawingArea.height, drawingArea.top]),
- };
+ scale: getScale(scaleType, extremums, domain).nice(ticksNumber),
+ ticksNumber,
+ } as AxisDefaultized;
});
return {
diff --git a/packages/x-charts/src/hooks/useScale.ts b/packages/x-charts/src/hooks/useScale.ts
index 394bed1b7ca9f..7905af820b67c 100644
--- a/packages/x-charts/src/hooks/useScale.ts
+++ b/packages/x-charts/src/hooks/useScale.ts
@@ -1,13 +1,4 @@
-import {
- scaleBand,
- scaleLog,
- scalePoint,
- scalePow,
- scaleSqrt,
- scaleTime,
- scaleUtc,
- scaleLinear,
-} from 'd3-scale';
+import { scaleLog, scalePow, scaleSqrt, scaleTime, scaleUtc, scaleLinear } from 'd3-scale';
import type {
ScaleBand,
ScaleLogarithmic,
@@ -16,7 +7,7 @@ import type {
ScaleTime,
ScaleLinear,
} from 'd3-scale';
-import { ScaleName } from '../models/axis';
+import { ContinuouseScaleName } from '../models/axis';
export type D3Scale =
| ScaleBand
@@ -26,24 +17,30 @@ export type D3Scale =
| ScaleTime
| ScaleLinear;
-export function getScale(scaleType: ScaleName | undefined): D3Scale {
+export type D3ContinuouseScale =
+ | ScaleLogarithmic
+ | ScalePower
+ | ScaleTime
+ | ScaleLinear;
+
+export function getScale(
+ scaleType: ContinuouseScaleName,
+ domain: any[],
+ range: any[],
+): D3ContinuouseScale {
switch (scaleType) {
- case 'band':
- return scaleBand();
case 'log':
- return scaleLog();
- case 'point':
- return scalePoint();
+ return scaleLog(domain, range);
case 'pow':
- return scalePow();
+ return scalePow(domain, range);
case 'sqrt':
- return scaleSqrt();
+ return scaleSqrt(domain, range);
case 'time':
- return scaleTime();
+ return scaleTime(domain, range);
case 'utc':
- return scaleUtc();
+ return scaleUtc(domain, range);
default:
- return scaleLinear();
+ return scaleLinear(domain, range);
}
}
diff --git a/packages/x-charts/src/hooks/useTicks.ts b/packages/x-charts/src/hooks/useTicks.ts
index d5919ceff69c0..3c5b2a5b7b50f 100644
--- a/packages/x-charts/src/hooks/useTicks.ts
+++ b/packages/x-charts/src/hooks/useTicks.ts
@@ -1,13 +1,25 @@
import * as React from 'react';
import { D3Scale, isBandScale } from './useScale';
-function useTicks(options: {
- scale: D3Scale;
+export type TickParams = {
maxTicks?: number;
minTicks?: number;
tickSpacing?: number;
-}) {
- const { maxTicks = 999, minTicks = 2, tickSpacing = 50, scale } = options;
+};
+
+export function getTicksNumber(
+ params: TickParams & {
+ domain: any[];
+ },
+) {
+ const { maxTicks = 999, minTicks = 2, tickSpacing = 50, domain } = params;
+
+ const estimatedTickNumber = Math.floor(Math.abs(domain[1] - domain[0]) / tickSpacing);
+ return Math.min(maxTicks, Math.max(minTicks, estimatedTickNumber));
+}
+
+function useTicks(options: { scale: D3Scale; ticksNumber?: number }) {
+ const { scale, ticksNumber } = options;
return React.useMemo(() => {
// band scale
@@ -17,16 +29,11 @@ function useTicks(options: {
.map((d) => ({ value: d, offset: (scale(d) ?? 0) + scale.bandwidth() / 2 }));
}
- const numberOfTicksTarget = Math.min(
- maxTicks,
- Math.max(minTicks, Math.floor((scale.range()[1] - scale.range()[0]) / tickSpacing)),
- );
-
- return scale.ticks(numberOfTicksTarget).map((value: any) => ({
- value: scale.tickFormat(numberOfTicksTarget)(value),
+ return scale.ticks(ticksNumber).map((value: any) => ({
+ value: scale.tickFormat(ticksNumber)(value),
offset: scale(value),
}));
- }, [tickSpacing, minTicks, maxTicks, scale]);
+ }, [ticksNumber, scale]);
}
export default useTicks;
diff --git a/packages/x-charts/src/internals/components/AxisSharedComponents.tsx b/packages/x-charts/src/internals/components/AxisSharedComponents.tsx
new file mode 100644
index 0000000000000..d25b2f73e0730
--- /dev/null
+++ b/packages/x-charts/src/internals/components/AxisSharedComponents.tsx
@@ -0,0 +1,39 @@
+import { styled } from '@mui/material/styles';
+
+export const Line = styled('line', {
+ name: 'MuiChartsAxis',
+ slot: 'Line',
+ overridesResolver: (props, styles) => styles.root,
+})(({ theme }) => ({
+ stroke: theme.palette.text.primary,
+ shapeRendering: 'crispEdges',
+}));
+
+export const Tick = styled('line', {
+ name: 'MuiChartsAxis',
+ slot: 'Tick',
+ overridesResolver: (props, styles) => styles.root,
+})(({ theme }) => ({
+ stroke: theme.palette.text.primary,
+ shapeRendering: 'crispEdges',
+}));
+
+export const TickLabel = styled('text', {
+ name: 'MuiChartsAxis',
+ slot: 'TickLabel',
+ overridesResolver: (props, styles) => styles.root,
+})(({ theme }) => ({
+ ...theme.typography.caption,
+ fill: theme.palette.text.primary,
+ textAnchor: 'middle',
+}));
+
+export const Label = styled('text', {
+ name: 'MuiChartsAxis',
+ slot: 'Label',
+ overridesResolver: (props, styles) => styles.root,
+})(({ theme }) => ({
+ ...theme.typography.body1,
+ fill: theme.palette.text.primary,
+ textAnchor: 'middle',
+}));
diff --git a/packages/x-charts/src/models/axis.ts b/packages/x-charts/src/models/axis.ts
index e993401daaed9..642bcb1943319 100644
--- a/packages/x-charts/src/models/axis.ts
+++ b/packages/x-charts/src/models/axis.ts
@@ -1,12 +1,6 @@
-import type {
- ScaleBand,
- ScaleLogarithmic,
- ScalePoint,
- ScalePower,
- ScaleTime,
- ScaleLinear,
-} from 'd3-scale';
-import { DefaultizedProps } from './helpers';
+import type { ScaleBand, ScaleLogarithmic, ScalePower, ScaleTime, ScaleLinear } from 'd3-scale';
+import { AxisClasses } from '../Axis/axisClasses';
+import type { TickParams } from '../hooks/useTicks';
export interface AxisProps {
/**
@@ -29,10 +23,10 @@ export interface AxisProps {
*/
fill?: string;
/**
- * The font size of the axis text.
+ * The font size of the axis ticks text.
* @default 12
*/
- fontSize?: number;
+ tickFontSize?: number;
/**
* The label of the axis.
*/
@@ -52,6 +46,10 @@ export interface AxisProps {
* @default 6
*/
tickSize?: number;
+ /**
+ * Override or extend the styles applied to the component.
+ */
+ classes?: Partial;
}
export interface YAxisProps extends AxisProps {
@@ -68,42 +66,59 @@ export interface XAxisProps extends AxisProps {
position?: 'top' | 'bottom';
}
-export type ScaleName = 'linear' | 'band' | 'log' | 'point' | 'pow' | 'sqrt' | 'time' | 'utc';
+export type ScaleName = 'linear' | 'band' | 'log' | 'pow' | 'sqrt' | 'time' | 'utc';
+export type ContinuouseScaleName = 'linear' | 'log' | 'pow' | 'sqrt' | 'time' | 'utc';
-export type AxisScaleMapping =
- | {
- scaleType: 'band';
- scale: ScaleBand;
- }
- | {
- scaleType: 'log';
- scale: ScaleLogarithmic;
- }
- | {
- scaleType: 'point';
- scale: ScalePoint;
- }
- | {
- scaleType: 'pow' | 'sqrt';
- scale: ScalePower;
- }
- | {
- scaleType: 'time' | 'utc';
- scale: ScaleTime;
- }
- | {
- scaleType: 'linear';
- scale: ScaleLinear;
- };
+interface AxisScaleConfig {
+ band: {
+ scaleType: 'band';
+ scale: ScaleBand;
+ ticksNumber: number;
+ };
+ log: {
+ scaleType: 'log';
+ scale: ScaleLogarithmic;
+ ticksNumber: number;
+ };
+ pow: {
+ scaleType: 'pow';
+ scale: ScalePower;
+ ticksNumber: number;
+ };
+ sqrt: {
+ scaleType: 'sqrt';
+ scale: ScalePower;
+ ticksNumber: number;
+ };
+ time: {
+ scaleType: 'time';
+ scale: ScaleTime;
+ ticksNumber: number;
+ };
+ utc: {
+ scaleType: 'utc';
+ scale: ScaleTime;
+ ticksNumber: number;
+ };
+ linear: {
+ scaleType: 'linear';
+ scale: ScaleLinear;
+ ticksNumber: number;
+ };
+}
-export type AxisConfig = {
+export type AxisConfig = {
id: string;
- scaleType?: ScaleName;
+ scaleType?: S;
min?: number;
max?: number;
data?: V[];
valueFormatter?: (value: V) => string;
-} & Partial;
+} & Partial &
+ TickParams;
-export type AxisDefaultized = DefaultizedProps, 'scaleType'> &
- AxisScaleMapping;
+export type AxisDefaultized = Omit<
+ AxisConfig,
+ 'scaleType'
+> &
+ AxisScaleConfig[S];