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];