Skip to content

Commit

Permalink
[charts] Expand line with step interpolation (#16229)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfauquette authored Jan 21, 2025
1 parent 4497258 commit 00a0483
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 21 deletions.
101 changes: 101 additions & 0 deletions docs/data/charts/lines/ExpandingStep.js
Original file line number Diff line number Diff line change
@@ -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 (
<Stack sx={{ width: '100%' }}>
<Stack direction="row" justifyContent="space-between">
<Stack>
<FormControlLabel
checked={connectNulls}
control={
<Checkbox
onChange={(event) => setConnectNulls(event.target.checked)}
/>
}
label="connectNulls"
labelPlacement="end"
/>
<FormControlLabel
checked={strictStepCurve}
control={
<Checkbox
onChange={(event) => setStrictStepCurve(event.target.checked)}
/>
}
label="strictStepCurve"
labelPlacement="end"
/>
</Stack>
<TextField
select
label="curve"
value={curve}
sx={{ minWidth: 200, mb: 2 }}
onChange={(event) => setCurve(event.target.value)}
>
{stepCurves.map((curveType) => (
<MenuItem key={curveType} value={curveType}>
{curveType}
</MenuItem>
))}
</TextField>
</Stack>

<ChartContainer
xAxis={[{ scaleType: 'band', data: weekDay }]}
series={[
{
type: 'line',
curve,
connectNulls,
strictStepCurve,
data: [5, 10, 16, 9, null, 6],
showMark: true,
color: 'blue',
},
{
type: 'line',
curve,
connectNulls,
strictStepCurve,
data: [null, 15, 9, 6, 8, 3, 10],
showMark: true,
color: 'red',
},
{
data: [1, 2, 3, 4, 3, 2, 1],
type: 'bar',
},
]}
height={200}
margin={{ top: 10, bottom: 20 }}
skipAnimation
>
<ChartsAxisHighlight x="band" />
<BarPlot />
<LinePlot />
<MarkPlot />
<ChartsXAxis />
<ChartsYAxis />
</ChartContainer>
</Stack>
);
}
102 changes: 102 additions & 0 deletions docs/data/charts/lines/ExpandingStep.tsx
Original file line number Diff line number Diff line change
@@ -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<StepCurve>('step');

return (
<Stack sx={{ width: '100%' }}>
<Stack direction="row" justifyContent="space-between">
<Stack>
<FormControlLabel
checked={connectNulls}
control={
<Checkbox
onChange={(event) => setConnectNulls(event.target.checked)}
/>
}
label="connectNulls"
labelPlacement="end"
/>
<FormControlLabel
checked={strictStepCurve}
control={
<Checkbox
onChange={(event) => setStrictStepCurve(event.target.checked)}
/>
}
label="strictStepCurve"
labelPlacement="end"
/>
</Stack>
<TextField
select
label="curve"
value={curve}
sx={{ minWidth: 200, mb: 2 }}
onChange={(event) => setCurve(event.target.value as StepCurve)}
>
{stepCurves.map((curveType) => (
<MenuItem key={curveType} value={curveType}>
{curveType}
</MenuItem>
))}
</TextField>
</Stack>

<ChartContainer
xAxis={[{ scaleType: 'band', data: weekDay }]}
series={[
{
type: 'line',
curve,
connectNulls,
strictStepCurve,
data: [5, 10, 16, 9, null, 6],
showMark: true,
color: 'blue',
},
{
type: 'line',
curve,
connectNulls,
strictStepCurve,
data: [null, 15, 9, 6, 8, 3, 10],
showMark: true,
color: 'red',
},
{
data: [1, 2, 3, 4, 3, 2, 1],
type: 'bar',
},
]}
height={200}
margin={{ top: 10, bottom: 20 }}
skipAnimation
>
<ChartsAxisHighlight x="band" />
<BarPlot />
<LinePlot />
<MarkPlot />
<ChartsXAxis />
<ChartsYAxis />
</ChartContainer>
</Stack>
);
}
4 changes: 2 additions & 2 deletions docs/data/charts/lines/InterpolationDemoNoSnap.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const curveTypes = [
function getExample(curveType) {
return `<LineChart
series={[
{ curve: "${curveType}", data: [0, 5, 2, 6, 3, 9.3] },
{ curve: "${curveType}", data: [1, 5, 2, 6, 3, 9.3] },
{ curve: "${curveType}", data: [6, 3, 7, 9.5, 4, 2] },
]}
{/* ... */}
Expand All @@ -47,7 +47,7 @@ export default function InterpolationDemoNoSnap() {
<LineChart
xAxis={[{ data: [1, 3, 5, 6, 7, 9], min: 0, max: 10 }]}
series={[
{ curve: curveType, data: [0, 5, 2, 6, 3, 9.3] },
{ curve: curveType, data: [1, 5, 2, 6, 3, 9.3] },
{ curve: curveType, data: [6, 3, 7, 9.5, 4, 2] },
]}
height={300}
Expand Down
8 changes: 8 additions & 0 deletions docs/data/charts/lines/lines.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ Different series could even have different interpolations.

{{"demo": "InterpolationDemoNoSnap.js", "hideToolbar": true}}

#### Expanding steps

To simplify the composition of line and chart, the step interpolations (when `curve` property is `'step'`, `'stepBefore'`, or `'stepAfter'`) expand to cover the full band width.

You can disable this behavior with `strictStepCurve` series property.

{{"demo": "ExpandingStep.js"}}

### Baseline

The area chart draws a `baseline` on the Y axis `0`.
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/charts/line-series-type.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"stack": { "type": { "description": "string" } },
"stackOffset": { "type": { "description": "StackOffsetType" }, "default": "'none'" },
"stackOrder": { "type": { "description": "StackOrderType" }, "default": "'none'" },
"strictStepCurve": { "type": { "description": "boolean" } },
"valueFormatter": { "type": { "description": "SeriesValueFormatter&lt;TValue&gt;" } },
"xAxisId": { "type": { "description": "string" } },
"yAxisId": { "type": { "description": "string" } }
Expand Down
3 changes: 3 additions & 0 deletions docs/translations/api-docs/charts/line-series-type.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"stackOrder": {
"description": "The order in which series&#39; of the same group are stacked together."
},
"strictStepCurve": {
"description": "If <code>true</code>, step curve starts and end at the first and last point.<br />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."
},
Expand Down
55 changes: 45 additions & 10 deletions packages/x-charts/src/LineChart/AreaPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand All @@ -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)!;
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 00a0483

Please sign in to comment.