Skip to content

[charts-pro] Add a borderRadius property to FunnelChart #17660

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 24 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from 15 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
29 changes: 29 additions & 0 deletions docs/data/charts/funnel/FunnelBorderRadius.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { Unstable_FunnelChart as FunnelChart } from '@mui/x-charts-pro/FunnelChart';

export default function FunnelBorderRadius() {
return (
<Box sx={{ width: '100%', maxWidth: 400 }}>
<FunnelChart
series={[
{
data: [{ value: 200 }, { value: 180 }, { value: 90 }, { value: 50 }],
borderRadius: 10,
},
]}
height={300}
/>
<FunnelChart
series={[
{
data: [{ value: 200 }, { value: 180 }, { value: 90 }, { value: 50 }],
borderRadius: 10,
},
]}
height={300}
gap={10}
/>
</Box>
);
}
29 changes: 29 additions & 0 deletions docs/data/charts/funnel/FunnelBorderRadius.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { Unstable_FunnelChart as FunnelChart } from '@mui/x-charts-pro/FunnelChart';

export default function FunnelBorderRadius() {
return (
<Box sx={{ width: '100%', maxWidth: 400 }}>
<FunnelChart
series={[
{
data: [{ value: 200 }, { value: 180 }, { value: 90 }, { value: 50 }],
borderRadius: 10,
},
]}
height={300}
/>
<FunnelChart
series={[
{
data: [{ value: 200 }, { value: 180 }, { value: 90 }, { value: 50 }],
borderRadius: 10,
},
]}
height={300}
gap={10}
/>
</Box>
);
}
9 changes: 9 additions & 0 deletions docs/data/charts/funnel/FunnelCurves.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export default function FunnelCurves() {
min: 0,
max: 20,
},
borderRadius: {
knob: 'slider',
defaultValue: 0,
min: 0,
max: 20,
},
}}
renderDemo={(props) => (
<Stack sx={{ width: '100%' }}>
Expand All @@ -30,6 +36,7 @@ export default function FunnelCurves() {
{
curve: props.curveType,
layout: 'vertical',
borderRadius: props.borderRadius,
...populationByEducationLevelPercentageSeries,
},
]}
Expand All @@ -42,6 +49,7 @@ export default function FunnelCurves() {
{
curve: props.curveType,
layout: 'horizontal',
borderRadius: props.borderRadius,
...populationByEducationLevelPercentageSeries,
},
]}
Expand All @@ -57,6 +65,7 @@ export default function FunnelCurves() {
<FunnelChart
series={[{ curve: '${props.curveType}' }]}
gap={${props.gap}}
${props.curveType === 'bump' ? '// ' : ''}borderRadius={${props.borderRadius}}
/>
`;
}}
Expand Down
13 changes: 12 additions & 1 deletion docs/data/charts/funnel/FunnelCurves.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export default function FunnelCurves() {
min: 0,
max: 20,
},
borderRadius: {
knob: 'slider',
defaultValue: 0,
min: 0,
max: 20,
},
} as const
}
renderDemo={(props) => (
Expand All @@ -31,6 +37,7 @@ export default function FunnelCurves() {
series={[
{
curve: props.curveType,
borderRadius: props.borderRadius,
layout: 'vertical',
...populationByEducationLevelPercentageSeries,
},
Expand All @@ -43,6 +50,7 @@ export default function FunnelCurves() {
series={[
{
curve: props.curveType,
borderRadius: props.borderRadius,
layout: 'horizontal',
...populationByEducationLevelPercentageSeries,
},
Expand All @@ -57,7 +65,10 @@ export default function FunnelCurves() {
return `import { FunnelChart } from '@mui/x-charts-pro/FunnelChart';

<FunnelChart
series={[{ curve: '${props.curveType}' }]}
series={[{
curve: '${props.curveType}',
${props.curveType === 'bump' ? '// ' : ''}borderRadius: ${props.borderRadius},
}]}
gap={${props.gap}}
/>
`;
Expand Down
15 changes: 15 additions & 0 deletions docs/data/charts/funnel/funnel.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@

{{"demo": "FunnelGap.js"}}

### Border radius

The border radius of the sections can be customized by the `borderRadius` property.
It accepts a number that represents the radius in pixels.

- The `bump` curve interpolation will not respect the border radius.

Check warning on line 75 in docs/data/charts/funnel/funnel.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/charts/funnel/funnel.md", "range": {"start": {"line": 75, "column": 34}}}, "severity": "WARNING"}
- The `linear` curve will respect the border radius to some extent due to the angle of the sections.

Check warning on line 76 in docs/data/charts/funnel/funnel.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/charts/funnel/funnel.md", "range": {"start": {"line": 76, "column": 22}}}, "severity": "WARNING"}
- The `step` curve will respect the border radius.

Check warning on line 77 in docs/data/charts/funnel/funnel.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/charts/funnel/funnel.md", "range": {"start": {"line": 77, "column": 20}}}, "severity": "WARNING"}

You can play with this in the [curve interpolation example](/x/react-charts/funnel/#curve-interpolation) above.

The `borderRadius` property will also behave differently depending if the `gap` property is bigger than 0 or not.

Check warning on line 81 in docs/data/charts/funnel/funnel.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/charts/funnel/funnel.md", "range": {"start": {"line": 81, "column": 29}}}, "severity": "WARNING"}

{{"demo": "FunnelBorderRadius.js"}}

### Colors

The funnel colors can be customized in two ways.
Expand Down
11 changes: 9 additions & 2 deletions packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ const useAggregatedData = (gap: number | undefined) => {
const xScale = xAxis[xAxisId].scale;
const yScale = yAxis[yAxisId].scale;

const curve = getFunnelCurve(currentSeries.curve, isHorizontal, gap);

const xPosition = (
value: number,
bandIndex: number,
Expand Down Expand Up @@ -107,6 +105,15 @@ const useAggregatedData = (gap: number | undefined) => {
})
: currentSeries.sectionLabel;

const curve = getFunnelCurve(
currentSeries.curve,
isHorizontal,
gap,
dataIndex,
currentSeries.dataPoints.length,
currentSeries.borderRadius,
);

const line = d3Line<FunnelDataPoints>()
.x((d) =>
xPosition(d.x, baseScaleConfig.data?.[dataIndex], d.stackOffset, d.useBandWidth),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Point } from './curve.types';

const distance = (p1: Point, p2: Point) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
const lerp = (a: number, b: number, x: number) => a + (b - a) * x;
const lerp2D = (p1: Point, p2: Point, t: number) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t),
});

/**
* Draws a polygon with rounded corners
* @param {CanvasRenderingContext2D} ctx The canvas context
* @param {Array} points A list of `{x, y}` points
* @radius {number} how much to round the corners
*/
export function borderRadiusPolygon(
ctx: CanvasRenderingContext2D,
points: Point[],
radius: number | number[],
): void {
const numPoints = points.length;

radius = Array.isArray(radius) ? radius : Array(numPoints).fill(radius);

const corners: Point[][] = [];
for (let i = 0; i < numPoints; i += 1) {
const lastPoint = points[i];
const thisPoint = points[(i + 1) % numPoints];
const nextPoint = points[(i + 2) % numPoints];

const lastEdgeLength = distance(lastPoint, thisPoint);
const lastOffsetDistance = Math.min(lastEdgeLength / 2, radius[i] ?? 0);
const start = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength);

const nextEdgeLength = distance(nextPoint, thisPoint);
const nextOffsetDistance = Math.min(nextEdgeLength / 2, radius[i] ?? 0);
const end = lerp2D(thisPoint, nextPoint, nextOffsetDistance / nextEdgeLength);

corners.push([start, thisPoint, end]);
}

ctx.moveTo(corners[0][0].x, corners[0][0].y);
for (const [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y);
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y);
}

ctx.closePath();
}
41 changes: 19 additions & 22 deletions packages/x-charts-pro/src/FunnelChart/curves/bump.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/* eslint-disable class-methods-use-this */
import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape';

/**
* This is a custom "bump" curve generator.
* It draws smooth curves for the 4 provided points,
* with the option to add a gap between sections while also properly handling the border radius.
*
* It takes into account the gap between the points and draws a smooth curve between them.
*
* It is based on the d3-shape bump curve generator.
* The implementation is based on the d3-shape bump curve generator.
* https://github.com/d3/d3-shape/blob/a82254af78f08799c71d7ab25df557c4872a3c51/src/curve/bump.js
*/
export class Bump implements CurveGenerator {
private context: CanvasRenderingContext2D;

private line: number = NaN;

private x: number = NaN;

private y: number = NaN;
Expand All @@ -23,30 +22,22 @@ export class Bump implements CurveGenerator {

private gap: number = 0;

constructor(context: CanvasRenderingContext2D, isHorizontal: boolean, gap: number = 0) {
constructor(
context: CanvasRenderingContext2D,
{ isHorizontal, gap }: { isHorizontal: boolean; gap?: number },
) {
this.context = context;
this.isHorizontal = isHorizontal;
this.gap = gap / 2;
this.gap = (gap ?? 0) / 2;
}

areaStart(): void {
this.line = 0;
}
areaStart(): void {}

areaEnd(): void {
this.line = NaN;
}
areaEnd(): void {}

lineStart(): void {
this.currentPoint = 0;
}
lineStart(): void {}

lineEnd() {
if (this.line || (this.line !== 0 && this.currentPoint === 1)) {
this.context.closePath();
}
this.line = 1 - this.line;
}
lineEnd(): void {}

point(x: number, y: number): void {
x = +x;
Expand All @@ -65,6 +56,9 @@ export class Bump implements CurveGenerator {
this.context.bezierCurveTo((this.x + x) / 2, this.y, (this.x + x) / 2, y, x + this.gap, y);
}

if (this.currentPoint === 3) {
this.context.closePath();
}
this.currentPoint += 1;
this.x = x;
this.y = y;
Expand All @@ -84,6 +78,9 @@ export class Bump implements CurveGenerator {
this.context.bezierCurveTo(this.x, (this.y + y) / 2, x, (this.y + y) / 2, x, y + this.gap);
}

if (this.currentPoint === 3) {
this.context.closePath();
}
this.currentPoint += 1;
this.x = x;
this.y = y;
Expand Down
5 changes: 5 additions & 0 deletions packages/x-charts-pro/src/FunnelChart/curves/curve.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ export type FunnelCurveOptions = {
gap?: number;
};
export type FunnelCurveType = 'linear' | 'step' | 'bump';

export type Point = {
x: number;
y: number;
};
Loading
Loading