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) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
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) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
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.