Skip to content

[charts] Expand line with step interpolation #16229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -24,6 +24,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 @@ -36,6 +36,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
Loading