diff --git a/docs/data/charts/lines/ExpandingStep.js b/docs/data/charts/lines/ExpandingStep.js new file mode 100644 index 0000000000000..04f959a28d416 --- /dev/null +++ b/docs/data/charts/lines/ExpandingStep.js @@ -0,0 +1,101 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import TextField from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; +import { LinePlot, MarkPlot } from '@mui/x-charts/LineChart'; +import { ChartContainer } from '@mui/x-charts/ChartContainer'; +import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; +import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis'; +import { BarPlot } from '@mui/x-charts/BarChart'; +import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight'; + +const weekDay = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; +const stepCurves = ['step', 'stepBefore', 'stepAfter']; + +export default function ExpandingStep() { + const [strictStepCurve, setStrictStepCurve] = React.useState(false); + const [connectNulls, setConnectNulls] = React.useState(false); + const [curve, setCurve] = React.useState('step'); + + return ( + + + + setConnectNulls(event.target.checked)} + /> + } + label="connectNulls" + labelPlacement="end" + /> + setStrictStepCurve(event.target.checked)} + /> + } + label="strictStepCurve" + labelPlacement="end" + /> + + setCurve(event.target.value)} + > + {stepCurves.map((curveType) => ( + + {curveType} + + ))} + + + + + + + + + + + + + ); +} diff --git a/docs/data/charts/lines/ExpandingStep.tsx b/docs/data/charts/lines/ExpandingStep.tsx new file mode 100644 index 0000000000000..6e301e9aeefbc --- /dev/null +++ b/docs/data/charts/lines/ExpandingStep.tsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import TextField from '@mui/material/TextField'; +import MenuItem from '@mui/material/MenuItem'; +import { LinePlot, MarkPlot } from '@mui/x-charts/LineChart'; +import { ChartContainer } from '@mui/x-charts/ChartContainer'; +import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis'; +import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis'; +import { BarPlot } from '@mui/x-charts/BarChart'; +import { ChartsAxisHighlight } from '@mui/x-charts/ChartsAxisHighlight'; + +const weekDay = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; +const stepCurves = ['step', 'stepBefore', 'stepAfter']; +type StepCurve = 'step' | 'stepBefore' | 'stepAfter'; + +export default function ExpandingStep() { + const [strictStepCurve, setStrictStepCurve] = React.useState(false); + const [connectNulls, setConnectNulls] = React.useState(false); + const [curve, setCurve] = React.useState('step'); + + return ( + + + + setConnectNulls(event.target.checked)} + /> + } + label="connectNulls" + labelPlacement="end" + /> + setStrictStepCurve(event.target.checked)} + /> + } + label="strictStepCurve" + labelPlacement="end" + /> + + setCurve(event.target.value as StepCurve)} + > + {stepCurves.map((curveType) => ( + + {curveType} + + ))} + + + + + + + + + + + + + ); +} diff --git a/docs/data/charts/lines/InterpolationDemoNoSnap.js b/docs/data/charts/lines/InterpolationDemoNoSnap.js index 2560facdf22de..4d7df7b365c81 100644 --- a/docs/data/charts/lines/InterpolationDemoNoSnap.js +++ b/docs/data/charts/lines/InterpolationDemoNoSnap.js @@ -20,7 +20,7 @@ const curveTypes = [ function getExample(curveType) { return `true, step curve starts and end at the first and last point.
By default the line is extended to fill the space before and after." + }, "valueFormatter": { "description": "Formatter used to render values in tooltip or other data display." }, diff --git a/packages/x-charts/src/LineChart/AreaPlot.tsx b/packages/x-charts/src/LineChart/AreaPlot.tsx index cb1e500013376..ea2101ed33977 100644 --- a/packages/x-charts/src/LineChart/AreaPlot.tsx +++ b/packages/x-charts/src/LineChart/AreaPlot.tsx @@ -12,6 +12,7 @@ import { } from './AreaElement'; import { getValueToPositionMapper } from '../hooks/useScale'; import getCurveFactory from '../internals/getCurve'; +import { isBandScale } from '../internals/isBandScale'; import { DEFAULT_X_AXIS_KEY } from '../constants'; import { LineItemIdentifier } from '../models/seriesType/line'; import { useLineSeries } from '../hooks/useSeries'; @@ -75,9 +76,12 @@ const useAggregatedData = () => { data, connectNulls, baseline, + curve, + strictStepCurve, } = series[seriesId]; - const xScale = getValueToPositionMapper(xAxis[xAxisId].scale); + const xScale = xAxis[xAxisId].scale; + const xPosition = getValueToPositionMapper(xScale); const yScale = yAxis[yAxisId].scale; const xData = xAxis[xAxisId].data; @@ -103,12 +107,49 @@ const useAggregatedData = () => { } } + const shouldExpand = curve?.includes('step') && !strictStepCurve && isBandScale(xScale); + + const formattedData: { + x: any; + y: [number, number]; + nullData: boolean; + isExtension?: boolean; + }[] = + xData?.flatMap((x, index) => { + const nullData = data[index] == null; + if (shouldExpand) { + const rep = [{ x, y: stackedData[index], nullData, isExtension: false }]; + if (!nullData && (index === 0 || data[index - 1] == null)) { + rep.unshift({ + x: (xScale(x) ?? 0) - (xScale.step() - xScale.bandwidth()) / 2, + y: stackedData[index], + nullData, + isExtension: true, + }); + } + if (!nullData && (index === data.length - 1 || data[index + 1] == null)) { + rep.push({ + x: (xScale(x) ?? 0) + (xScale.step() + xScale.bandwidth()) / 2, + y: stackedData[index], + nullData, + isExtension: true, + }); + } + return rep; + } + return { x, y: stackedData[index], nullData }; + }) ?? []; + + const d3Data = connectNulls ? formattedData.filter((d) => !d.nullData) : formattedData; + const areaPath = d3Area<{ x: any; y: [number, number]; + nullData: boolean; + isExtension?: boolean; }>() - .x((d) => xScale(d.x)) - .defined((_, i) => connectNulls || data[i] != null) + .x((d) => (d.isExtension ? d.x : xPosition(d.x))) + .defined((d) => connectNulls || !d.nullData || !!d.isExtension) .y0((d) => { if (typeof baseline === 'number') { return yScale(baseline)!; @@ -128,13 +169,7 @@ const useAggregatedData = () => { }) .y1((d) => d.y && yScale(d.y[1])!); - const curve = getCurveFactory(series[seriesId].curve); - const formattedData = xData?.map((x, index) => ({ x, y: stackedData[index] })) ?? []; - const d3Data = connectNulls - ? formattedData.filter((_, i) => data[i] != null) - : formattedData; - - const d = areaPath.curve(curve)(d3Data) || ''; + const d = areaPath.curve(getCurveFactory(curve))(d3Data) || ''; return { ...series[seriesId], gradientId, diff --git a/packages/x-charts/src/LineChart/LinePlot.tsx b/packages/x-charts/src/LineChart/LinePlot.tsx index 27b0c1794df4d..8f02667ac176e 100644 --- a/packages/x-charts/src/LineChart/LinePlot.tsx +++ b/packages/x-charts/src/LineChart/LinePlot.tsx @@ -12,6 +12,7 @@ import { } from './LineElement'; import { getValueToPositionMapper } from '../hooks/useScale'; import getCurveFactory from '../internals/getCurve'; +import { isBandScale } from '../internals/isBandScale'; import { DEFAULT_X_AXIS_KEY } from '../constants'; import { LineItemIdentifier } from '../models/seriesType/line'; import { useLineSeries } from '../hooks/useSeries'; @@ -72,9 +73,12 @@ const useAggregatedData = () => { stackedData, data, connectNulls, + curve, + strictStepCurve, } = series[seriesId]; - const xScale = getValueToPositionMapper(xAxis[xAxisId].scale); + const xScale = xAxis[xAxisId].scale; + const xPosition = getValueToPositionMapper(xScale); const yScale = yAxis[yAxisId].scale; const xData = xAxis[xAxisId].data; @@ -100,20 +104,52 @@ const useAggregatedData = () => { } } + const shouldExpand = curve?.includes('step') && !strictStepCurve && isBandScale(xScale); + + const formattedData: { + x: any; + y: [number, number]; + nullData: boolean; + isExtension?: boolean; + }[] = + xData?.flatMap((x, index) => { + const nullData = data[index] == null; + if (shouldExpand) { + const rep = [{ x, y: stackedData[index], nullData, isExtension: false }]; + if (!nullData && (index === 0 || data[index - 1] == null)) { + rep.unshift({ + x: (xScale(x) ?? 0) - (xScale.step() - xScale.bandwidth()) / 2, + y: stackedData[index], + nullData, + isExtension: true, + }); + } + if (!nullData && (index === data.length - 1 || data[index + 1] == null)) { + rep.push({ + x: (xScale(x) ?? 0) + (xScale.step() + xScale.bandwidth()) / 2, + y: stackedData[index], + nullData, + isExtension: true, + }); + } + return rep; + } + return { x, y: stackedData[index], nullData }; + }) ?? []; + + const d3Data = connectNulls ? formattedData.filter((d) => !d.nullData) : formattedData; + const linePath = d3Line<{ x: any; y: [number, number]; + nullData: boolean; + isExtension?: boolean; }>() - .x((d) => xScale(d.x)) - .defined((_, i) => connectNulls || data[i] != null) + .x((d) => (d.isExtension ? d.x : xPosition(d.x))) + .defined((d) => connectNulls || !d.nullData || !!d.isExtension) .y((d) => yScale(d.y[1])!); - const formattedData = xData?.map((x, index) => ({ x, y: stackedData[index] })) ?? []; - const d3Data = connectNulls - ? formattedData.filter((_, i) => data[i] != null) - : formattedData; - - const d = linePath.curve(getCurveFactory(series[seriesId].curve))(d3Data) || ''; + const d = linePath.curve(getCurveFactory(curve))(d3Data) || ''; return { ...series[seriesId], gradientId, diff --git a/packages/x-charts/src/models/seriesType/line.ts b/packages/x-charts/src/models/seriesType/line.ts index b2477ea62628d..e38902b34bb13 100644 --- a/packages/x-charts/src/models/seriesType/line.ts +++ b/packages/x-charts/src/models/seriesType/line.ts @@ -66,6 +66,11 @@ export interface LineSeriesType * @default 'monotoneX' */ curve?: CurveType; + /** + * If `true`, step curve starts and end at the first and last point. + * By default the line is extended to fill the space before and after. + */ + strictStepCurve?: boolean; /** * Define which items of the series should display a mark. * If can be a boolean that applies to all items.