From 7c21f66d7d1ddc5e58978109ef97d51ee9ad1237 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 13:37:24 +0200 Subject: [PATCH 01/20] initial work --- docs/data/charts/funnel/FunnelCurves.tsx | 9 ++ .../src/FunnelChart/FunnelPlot.tsx | 24 ++- .../FunnelChart/curves/borderRadiusPolygon.ts | 49 ++++++ .../src/FunnelChart/curves/bump.ts | 29 ++-- .../src/FunnelChart/curves/curve.types.ts | 5 + .../src/FunnelChart/curves/funnelStep.ts | 147 +++++++++-------- .../src/FunnelChart/curves/getFunnelCurve.ts | 14 +- .../src/FunnelChart/curves/linear.ts | 152 ++++++++++-------- .../src/FunnelChart/useFunnelChartProps.ts | 2 + 9 files changed, 272 insertions(+), 159 deletions(-) create mode 100644 packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts diff --git a/docs/data/charts/funnel/FunnelCurves.tsx b/docs/data/charts/funnel/FunnelCurves.tsx index be4827ecef762..107f69a6aeab0 100644 --- a/docs/data/charts/funnel/FunnelCurves.tsx +++ b/docs/data/charts/funnel/FunnelCurves.tsx @@ -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) => ( @@ -36,6 +42,7 @@ export default function FunnelCurves() { }, ]} gap={props.gap} + borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> @@ -48,6 +55,7 @@ export default function FunnelCurves() { }, ]} gap={props.gap} + borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> @@ -59,6 +67,7 @@ export default function FunnelCurves() { `; }} diff --git a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx index ce2b461536bff..53d00cba00d92 100644 --- a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx +++ b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx @@ -20,6 +20,11 @@ export interface FunnelPlotProps extends FunnelPlotSlotExtension { * @default 0 */ gap?: number; + /** + * The radius of the corners of the funnel sections. + * @default 0 + */ + borderRadius?: number; /** * Callback fired when a funnel item is clicked. * @param {React.MouseEvent} event The event source of the callback. @@ -31,7 +36,7 @@ export interface FunnelPlotProps extends FunnelPlotSlotExtension { ) => void; } -const useAggregatedData = (gap: number | undefined) => { +const useAggregatedData = (gap: number | undefined, borderRadius: number | undefined) => { const seriesData = useFunnelSeriesContext(); const { xAxis, xAxisIds } = useXAxes(); const { yAxis, yAxisIds } = useYAxes(); @@ -67,8 +72,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, @@ -107,6 +110,15 @@ const useAggregatedData = (gap: number | undefined) => { }) : currentSeries.sectionLabel; + const curve = getFunnelCurve( + currentSeries.curve, + isHorizontal, + gap, + dataIndex, + currentSeries.dataPoints.length, + borderRadius, + ); + const line = d3Line() .x((d) => xPosition(d.x, baseScaleConfig.data?.[dataIndex], d.stackOffset, d.useBandWidth), @@ -142,16 +154,16 @@ const useAggregatedData = (gap: number | undefined) => { }); return result.flat(); - }, [seriesData, xAxis, xAxisIds, yAxis, yAxisIds, gap]); + }, [seriesData, xAxis, xAxisIds, yAxis, yAxisIds, gap, borderRadius]); return allData; }; function FunnelPlot(props: FunnelPlotProps) { - const { onItemClick, gap, ...other } = props; + const { onItemClick, gap, borderRadius, ...other } = props; const theme = useTheme(); - const data = useAggregatedData(gap); + const data = useAggregatedData(gap, borderRadius); return ( diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts new file mode 100644 index 0000000000000..0217feb7e51ad --- /dev/null +++ b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts @@ -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(); +} diff --git a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts index c20c58f9de25f..e4061a425f2bc 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts @@ -1,3 +1,4 @@ +/* eslint-disable class-methods-use-this */ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; /** @@ -11,8 +12,6 @@ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; export class Bump implements CurveGenerator { private context: CanvasRenderingContext2D; - private line: number = NaN; - private x: number = NaN; private y: number = NaN; @@ -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; diff --git a/packages/x-charts-pro/src/FunnelChart/curves/curve.types.ts b/packages/x-charts-pro/src/FunnelChart/curves/curve.types.ts index 572047b231d55..e3f231ff8173c 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/curve.types.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/curve.types.ts @@ -6,3 +6,8 @@ export type FunnelCurveOptions = { gap?: number; }; export type FunnelCurveType = 'linear' | 'step' | 'bump'; + +export type Point = { + x: number; + y: number; +}; diff --git a/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts b/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts index ff03a34e9bd55..becd6c9350a0c 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts @@ -1,4 +1,7 @@ +/* eslint-disable class-methods-use-this */ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; +import { Point } from './curve.types'; +import { borderRadiusPolygon } from './borderRadiusPolygon'; /** * This is a custom "step" curve generator for the funnel chart. @@ -12,85 +15,103 @@ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; export class FunnelStep implements CurveGenerator { private context: CanvasRenderingContext2D; - private line: number = NaN; - - private x: number = NaN; - - private y: number = NaN; - - private currentPoint: number = 0; - private isHorizontal: boolean = false; private gap: number = 0; - constructor(context: CanvasRenderingContext2D, isHorizontal: boolean, gap: number = 0) { + private borderRadius: number = 0; + + private position: number = 0; + + private sections: number = 0; + + private points: Point[] = []; + + constructor( + context: CanvasRenderingContext2D, + { + isHorizontal, + gap, + position, + sections, + borderRadius, + }: { + isHorizontal: boolean; + gap?: number; + position?: number; + sections?: number; + borderRadius?: number; + }, + ) { this.context = context; this.isHorizontal = isHorizontal; - this.gap = gap / 2; + this.gap = (gap ?? 0) / 2; + this.position = position ?? 0; + this.sections = sections ?? 1; + this.borderRadius = borderRadius ?? 0; } - areaStart(): void { - this.line = 0; - } - - areaEnd(): void { - this.line = NaN; - } + areaStart(): void {} - lineStart(): void { - this.x = NaN; - this.y = NaN; - this.currentPoint = 0; - } + areaEnd(): void {} - lineEnd(): void { - if (this.currentPoint === 2) { - this.context.lineTo(this.x, this.y); - } - if (this.line || (this.line !== 0 && this.currentPoint === 1)) { - this.context.closePath(); - } - if (this.line >= 0) { - this.line = 1 - this.line; - } - } + lineStart(): void {} - point(x: number, y: number): void { - x = +x; - y = +y; - - // 0 is the top-left corner. - if (this.isHorizontal) { - if (this.currentPoint === 0) { - this.context.moveTo(x + this.gap, y); - } else if (this.currentPoint === 1 || this.currentPoint === 2) { - this.context.lineTo(x - this.gap, this.y); - this.context.lineTo(x - this.gap, y); - } else { - this.context.lineTo(this.x - this.gap, y); - this.context.lineTo(x + this.gap, y); - } + lineEnd(): void {} - this.currentPoint += 1; - this.x = x; - this.y = y; + point(xIn: number, yIn: number): void { + this.points.push({ x: xIn, y: yIn }); + if (this.points.length < 4) { return; } - // 0 is the top-right corner. - if (this.currentPoint === 0) { - this.context.moveTo(x, y + this.gap); - } else if (this.currentPoint === 3) { - this.context.lineTo(x, this.y - this.gap); - this.context.lineTo(x, y + this.gap); + // Ensure we have rectangles instead of trapezoids. + this.points = this.points.map((point, index) => { + if (this.isHorizontal) { + return { + x: point.x, + y: index <= 1 ? this.points.at(0)!.y : this.points.at(-1)!.y, + }; + } + return { + x: index <= 1 ? this.points.at(0)!.x : this.points.at(-1)!.x, + y: point.y, + }; + }); + + // Add gaps where they are needed. + this.points = this.points.map((point, index) => { + if (this.isHorizontal) { + return { + x: point.x + (index === 0 || index === 3 ? this.gap : -this.gap), + y: point.y, + }; + } + return { + x: point.x, + y: point.y + (index === 0 || index === 3 ? this.gap : -this.gap), + }; + }); + + if (this.borderRadius > 0) { + borderRadiusPolygon( + this.context, + this.points, + this.gap > 0 || this.position === 0 + ? this.borderRadius + : [this.borderRadius, this.borderRadius], + ); } else { - this.context.lineTo(this.x, y - this.gap); - this.context.lineTo(x, y - this.gap); + this.context.moveTo(this.points[0].x, this.points[0].y); + this.points.forEach((point, index) => { + if (index === 0) { + this.context.moveTo(point.x, point.y); + } + this.context.lineTo(point.x, point.y); + if (index === this.points.length - 1) { + this.context.closePath(); + } + }); } - - this.currentPoint += 1; - this.x = x; - this.y = y; } } diff --git a/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts b/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts index bfde8fcb7c422..d706ae5f10027 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts @@ -17,7 +17,17 @@ const curveConstructor = (curve: FunnelCurveType | undefined) => { export const getFunnelCurve = ( curve: FunnelCurveType | undefined, isHorizontal: boolean, - gap: number = 0, + gap: number | undefined, + dataIndex: number, + totalDataPoints: number, + borderRadius: number | undefined, ): CurveFactory => { - return (context) => new (curveConstructor(curve))(context as any, isHorizontal, gap); + return (context) => + new (curveConstructor(curve))(context as any, { + isHorizontal, + gap, + position: dataIndex, + sections: totalDataPoints, + borderRadius, + }); }; diff --git a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts index 3ad11b75c5f8b..86f5317d87b29 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts @@ -1,4 +1,7 @@ +/* eslint-disable class-methods-use-this */ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; +import { Point } from './curve.types'; +import { borderRadiusPolygon } from './borderRadiusPolygon'; // From point1 to point2, get the x value from y const xFromY = @@ -35,93 +38,104 @@ const yFromX = export class Linear implements CurveGenerator { private context: CanvasRenderingContext2D; - private line: number = NaN; + private position: number = 0; - private x: number = NaN; - - private y: number = NaN; - - private currentPoint: number = 0; + private sections: number = 0; private isHorizontal: boolean = false; private gap: number = 0; - constructor(context: CanvasRenderingContext2D, isHorizontal: boolean, gap: number = 0) { + private borderRadius: number = 0; + + private points: Point[] = []; + + constructor( + context: CanvasRenderingContext2D, + { + isHorizontal, + gap, + position, + sections, + borderRadius, + }: { + isHorizontal: boolean; + gap?: number; + position?: number; + sections?: number; + borderRadius?: number; + }, + ) { this.context = context; this.isHorizontal = isHorizontal; - this.gap = gap / 2; + this.gap = (gap ?? 0) / 2; + this.position = position ?? 0; + this.sections = sections ?? 1; + this.borderRadius = borderRadius ?? 0; } - areaStart(): void { - this.line = 0; - } + areaStart(): void {} - areaEnd(): void { - this.line = NaN; - } + areaEnd(): void {} - lineStart(): void { - this.currentPoint = 0; - } + lineStart(): void {} + + lineEnd(): void {} - lineEnd() { - if (this.line || (this.line !== 0 && this.currentPoint === 1)) { - this.context.closePath(); + point(xIn: number, yIn: number): void { + this.points.push({ x: xIn, y: yIn }); + if (this.points.length < 4) { + return; } - this.line = 1 - this.line; - } - point(x: number, y: number): void { - x = +x; - y = +y; - - // We draw the lines only at currentPoint 1 & 3 because we need - // The data of a pair of points to draw the lines. - // Hence currentPoint 1 draws a line from point 0 to point 1 and point 1 to point 2. - // currentPoint 3 draws a line from point 2 to point 3 and point 3 to point 0. - - if (this.isHorizontal) { - const yGetter = yFromX(this.x, this.y, x, y); - let xGap = 0; - - // 0 is the top-left corner. - if (this.currentPoint === 1) { - xGap = this.x + this.gap; - this.context.moveTo(xGap, yGetter(xGap)); - this.context.lineTo(xGap, yGetter(xGap)); - xGap = x - this.gap; - this.context.lineTo(xGap, yGetter(xGap)); - } else if (this.currentPoint === 3) { - xGap = this.x - this.gap; - this.context.lineTo(xGap, yGetter(xGap)); - xGap = x + this.gap; - this.context.lineTo(xGap, yGetter(xGap)); + // Add gaps where they are needed. + this.points = this.points.map((point, index) => { + const slopeStart = this.points.at(index <= 1 ? 0 : 2)!; + const slopeEnd = this.points.at(index <= 1 ? 1 : 3)!; + const yGetter = yFromX(slopeStart.x, slopeStart.y, slopeEnd.x, slopeEnd.y); + if (this.isHorizontal) { + const xGap = point.x + (index === 0 || index === 3 ? this.gap : -this.gap); + + return { + x: xGap, + y: yGetter(xGap), + }; } - } - if (!this.isHorizontal) { - const xGetter = xFromY(this.x, this.y, x, y); - let yGap = 0; - - // 0 is the top-right corner. - if (this.currentPoint === 1) { - yGap = this.y + this.gap; - this.context.moveTo(xGetter(yGap), yGap); - this.context.lineTo(xGetter(yGap), yGap); - yGap = y - this.gap; - this.context.lineTo(xGetter(yGap), yGap); - } else if (this.currentPoint === 3) { - yGap = this.y - this.gap; - this.context.lineTo(xGetter(yGap), yGap); - yGap = y + this.gap; - this.context.lineTo(xGetter(yGap), yGap); + const xGetter = xFromY(slopeStart.x, slopeStart.y, slopeEnd.x, slopeEnd.y); + const yGap = point.y + (index === 0 || index === 3 ? this.gap : -this.gap); + return { + x: xGetter(yGap), + y: yGap, + }; + }); + + const getBorderRadius = () => { + if (this.gap > 0) { + return this.borderRadius; + } + if (this.position === 0) { + return [0, 0, this.borderRadius, this.borderRadius]; } + if (this.position === this.sections - 1) { + return [this.borderRadius, this.borderRadius]; + } + return 0; + }; + + if (this.borderRadius > 0) { + borderRadiusPolygon(this.context, this.points, getBorderRadius()); + } else { + this.context.moveTo(this.points[0].x, this.points[0].y); + this.points.forEach((point, index) => { + if (index === 0) { + this.context.moveTo(point.x, point.y); + } + this.context.lineTo(point.x, point.y); + if (index === this.points.length - 1) { + this.context.closePath(); + } + }); } - - // Increment the values - this.currentPoint += 1; - this.x = x; - this.y = y; } } diff --git a/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts b/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts index 66db4c7ce4e25..534778d986d80 100644 --- a/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts +++ b/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts @@ -127,6 +127,7 @@ export const useFunnelChartProps = (props: FunnelChartProps) => { axisHighlight, apiRef, gap, + borderRadius, ...rest } = props; const margin = defaultizeMargin(marginProps, DEFAULT_MARGINS); @@ -172,6 +173,7 @@ export const useFunnelChartProps = (props: FunnelChartProps) => { const funnelPlotProps: FunnelPlotProps = { gap, + borderRadius, onItemClick, slots, slotProps, From 1684bf1866e8ee6973e30680719d4e4e8ca9c6f5 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 13:45:10 +0200 Subject: [PATCH 02/20] logic --- .../src/FunnelChart/curves/funnelStep.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts b/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts index becd6c9350a0c..3fa360b283e51 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts @@ -3,11 +3,18 @@ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; import { Point } from './curve.types'; import { borderRadiusPolygon } from './borderRadiusPolygon'; +const max = (numbers: number[]) => Math.max(...numbers, -Infinity); +const min = (numbers: number[]) => Math.min(...numbers, Infinity); + /** - * This is a custom "step" curve generator for the funnel chart. - * It is used to draw the funnel using "rectangles" without having to rework the rendering logic. + * This is a custom "step" curve generator. + * It is used to draw "rectangles" from 4 points without having to rework the rendering logic. + * + * It expects points to be passed in the following order: top-left, top-right, bottom-right, bottom-left. + * + * It takes the min and max of the x and y coordinates of the points to create a rectangle. * - * It takes into account the gap between the points and draws a smooth curve between them. + * It takes into account the gap between sections and the border radius. * * It is based on the d3-shape step curve generator. * https://github.com/d3/d3-shape/blob/a82254af78f08799c71d7ab25df557c4872a3c51/src/curve/step.js @@ -23,8 +30,6 @@ export class FunnelStep implements CurveGenerator { private position: number = 0; - private sections: number = 0; - private points: Point[] = []; constructor( @@ -33,7 +38,6 @@ export class FunnelStep implements CurveGenerator { isHorizontal, gap, position, - sections, borderRadius, }: { isHorizontal: boolean; @@ -47,7 +51,6 @@ export class FunnelStep implements CurveGenerator { this.isHorizontal = isHorizontal; this.gap = (gap ?? 0) / 2; this.position = position ?? 0; - this.sections = sections ?? 1; this.borderRadius = borderRadius ?? 0; } @@ -66,16 +69,18 @@ export class FunnelStep implements CurveGenerator { } // Ensure we have rectangles instead of trapezoids. - this.points = this.points.map((point, index) => { + this.points = this.points.map((_, index) => { + const allX = this.points.map((p) => p.x); + const allY = this.points.map((p) => p.y); if (this.isHorizontal) { return { - x: point.x, - y: index <= 1 ? this.points.at(0)!.y : this.points.at(-1)!.y, + x: index === 1 || index === 2 ? max(allX) : min(allX), + y: index <= 1 ? max(allY) : min(allY), }; } return { - x: index <= 1 ? this.points.at(0)!.x : this.points.at(-1)!.x, - y: point.y, + x: index <= 1 ? min(allX) : max(allX), + y: index === 1 || index === 2 ? max(allY) : min(allY), }; }); From 9db8c25f6ac8d28e25a1ec7dd9014e8c50acbd4d Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 13:45:57 +0200 Subject: [PATCH 03/20] rename --- .../x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts | 4 ++-- .../src/FunnelChart/curves/{funnelStep.ts => step.ts} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename packages/x-charts-pro/src/FunnelChart/curves/{funnelStep.ts => step.ts} (98%) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts b/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts index d706ae5f10027..85c874060bc8f 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/getFunnelCurve.ts @@ -1,12 +1,12 @@ import { CurveFactory } from '@mui/x-charts-vendor/d3-shape'; import { FunnelCurveType } from './curve.types'; -import { FunnelStep } from './funnelStep'; +import { Step } from './step'; import { Linear } from './linear'; import { Bump } from './bump'; const curveConstructor = (curve: FunnelCurveType | undefined) => { if (curve === 'step') { - return FunnelStep; + return Step; } if (curve === 'bump') { return Bump; diff --git a/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts b/packages/x-charts-pro/src/FunnelChart/curves/step.ts similarity index 98% rename from packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts rename to packages/x-charts-pro/src/FunnelChart/curves/step.ts index 3fa360b283e51..e9139cc006a81 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/funnelStep.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/step.ts @@ -19,7 +19,7 @@ const min = (numbers: number[]) => Math.min(...numbers, Infinity); * It is based on the d3-shape step curve generator. * https://github.com/d3/d3-shape/blob/a82254af78f08799c71d7ab25df557c4872a3c51/src/curve/step.js */ -export class FunnelStep implements CurveGenerator { +export class Step implements CurveGenerator { private context: CanvasRenderingContext2D; private isHorizontal: boolean = false; From df94cbb147f6bd13a405a70be4eee3e8c1cf7c62 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 13:54:08 +0200 Subject: [PATCH 04/20] improve docs --- packages/x-charts-pro/src/FunnelChart/curves/bump.ts | 6 +++--- packages/x-charts-pro/src/FunnelChart/curves/linear.ts | 6 +++--- packages/x-charts-pro/src/FunnelChart/curves/step.ts | 9 +++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts index e4061a425f2bc..8052662fadb94 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts @@ -3,10 +3,10 @@ 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 { diff --git a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts index 86f5317d87b29..d023c313efdde 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts @@ -29,10 +29,10 @@ const yFromX = /** * This is a custom "linear" curve generator. + * It draws straight lines 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 linear curve generator. + * The implementation is based on the d3-shape linear curve generator. * https://github.com/d3/d3-shape/blob/a82254af78f08799c71d7ab25df557c4872a3c51/src/curve/linear.js */ export class Linear implements CurveGenerator { diff --git a/packages/x-charts-pro/src/FunnelChart/curves/step.ts b/packages/x-charts-pro/src/FunnelChart/curves/step.ts index e9139cc006a81..7d3c459eb1f36 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/step.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/step.ts @@ -8,15 +8,12 @@ const min = (numbers: number[]) => Math.min(...numbers, Infinity); /** * This is a custom "step" curve generator. - * It is used to draw "rectangles" from 4 points without having to rework the rendering logic. - * - * It expects points to be passed in the following order: top-left, top-right, bottom-right, bottom-left. + * It is used to draw "rectangles" from 4 points without having to rework the rendering logic, + * with the option to add a gap between sections while also properly handling the border radius. * * It takes the min and max of the x and y coordinates of the points to create a rectangle. * - * It takes into account the gap between sections and the border radius. - * - * It is based on the d3-shape step curve generator. + * The implementation is based on the d3-shape step curve generator. * https://github.com/d3/d3-shape/blob/a82254af78f08799c71d7ab25df557c4872a3c51/src/curve/step.js */ export class Step implements CurveGenerator { From 81e03b7dc40e6a2eed3826da3f7572330ecdc93a Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 16:59:42 +0200 Subject: [PATCH 05/20] changes yolo --- .../curves/borderRadiusBumpPolygon.ts | 131 ++++++++++++++++++ .../FunnelChart/curves/borderRadiusPolygon.ts | 3 + .../src/FunnelChart/curves/bump.ts | 71 ++++------ .../src/FunnelChart/curves/linear.ts | 15 +- .../src/FunnelChart/curves/step.ts | 27 +--- 5 files changed, 171 insertions(+), 76 deletions(-) create mode 100644 packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts new file mode 100644 index 0000000000000..8b24917ac94cf --- /dev/null +++ b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts @@ -0,0 +1,131 @@ +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), +}); + +/** + * Same as `borderRadiusPolygon` but with a bump in the middle + * of the relevant edges. + * + * @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 borderRadiusBumpPolygon( + 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]; + + // Regular corners + if (i === 0 || i === 2) { + 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]); + } else { + // Start bump, from last point to the middle of the edge + const lastEdgeLength = distance(lastPoint, thisPoint); + const lastOffsetDistance = Math.min(lastEdgeLength / 2, radius[i] ?? 0); + const startBumpStart = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength); + + const midPoint = { + x: (thisPoint.x + lastPoint.x) / 2, + y: (thisPoint.y + lastPoint.y) / 2, + }; + const startBumpEdgeLength = distance(midPoint, thisPoint); + const startBumpOffsetDistance = Math.min(startBumpEdgeLength / 2, radius[i] ?? 0); + const startBumpEnd = lerp2D( + thisPoint, + nextPoint, + startBumpOffsetDistance / startBumpEdgeLength, + ); + + const angle = Math.abs( + (Math.atan2(thisPoint.x - lastPoint.x, thisPoint.y - lastPoint.y) * 180) / Math.PI, + ); + const isHorizontal = angle < 45 || angle > 135; + + const controlPoint = { + x: isHorizontal ? midPoint.x : lastPoint.x, + y: isHorizontal ? lastPoint.y : midPoint.y, + }; + + corners.push([startBumpStart, controlPoint, startBumpEnd]); + + // End bump, from the middle of the edge to the next point + const lastBumpEdgeLength = distance(thisPoint, nextPoint); + + // const edgeCp2 = { + // x: isHorizontal ? (lastPoint.x + thisPoint.x) / 2 : thisPoint.x, + // y: isHorizontal ? thisPoint.y : (lastPoint.y + thisPoint.y) / 2, + // }; + } + + // } else if (this.currentPoint === 1) { + // this.context.bezierCurveTo( + // (this.x + x - this.gap) / 2, + // this.y, + // (this.x + x - this.gap) / 2, + // y, + // x - this.gap, + // y, + // ); + // } else if (this.currentPoint === 3) { + // this.context.bezierCurveTo( + // (this.x + x - this.gap) / 2, + // this.y, + // (this.x + x - this.gap) / 2, + // y, + // x + this.gap, + // y, + // ); + // } + + // } else if (this.currentPoint === 1) { + // this.context.bezierCurveTo( + // this.x, + // (this.y + y - this.gap) / 2, + // x, + // (this.y + y - this.gap) / 2, + // x, + // y - this.gap, + // ); + // } else if (this.currentPoint === 3) { + // this.context.bezierCurveTo( + // this.x, + // (this.y + y - this.gap) / 2, + // x, + // (this.y + y - this.gap) / 2, + // x, + // y + this.gap, + // ); + // } + } + + 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(); +} diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts index 0217feb7e51ad..d96ea2de1c72d 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts @@ -9,6 +9,9 @@ const lerp2D = (p1: Point, p2: Point, t: number) => ({ /** * Draws a polygon with rounded corners + * + * Take from https://stackoverflow.com/a/56214413/24269134 + * * @param {CanvasRenderingContext2D} ctx The canvas context * @param {Array} points A list of `{x, y}` points * @radius {number} how much to round the corners diff --git a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts index 8052662fadb94..9fd884db814e7 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts @@ -1,5 +1,7 @@ /* eslint-disable class-methods-use-this */ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; +import { Point } from './curve.types'; +import { borderRadiusBumpPolygon } from './borderRadiusBumpPolygon'; /** * This is a custom "bump" curve generator. @@ -12,23 +14,26 @@ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; export class Bump implements CurveGenerator { private context: CanvasRenderingContext2D; - private x: number = NaN; - - private y: number = NaN; - - private currentPoint: number = 0; - private isHorizontal: boolean = false; private gap: number = 0; + private borderRadius: number = 0; + + private points: Point[] = []; + constructor( context: CanvasRenderingContext2D, - { isHorizontal, gap }: { isHorizontal: boolean; gap?: number }, + { + isHorizontal, + gap, + borderRadius, + }: { isHorizontal: boolean; gap?: number; borderRadius?: number }, ) { this.context = context; this.isHorizontal = isHorizontal; this.gap = (gap ?? 0) / 2; + this.borderRadius = borderRadius ?? 0; } areaStart(): void {} @@ -39,44 +44,26 @@ export class Bump implements CurveGenerator { lineEnd(): void {} - point(x: number, y: number): void { - x = +x; - y = +y; - - // 0 is the top-left corner. - if (this.isHorizontal) { - if (this.currentPoint === 0) { - this.context.moveTo(x + this.gap, y); - this.context.lineTo(x + this.gap, y); - } else if (this.currentPoint === 1) { - this.context.bezierCurveTo((this.x + x) / 2, this.y, (this.x + x) / 2, y, x - this.gap, y); - } else if (this.currentPoint === 2) { - this.context.lineTo(x - this.gap, y); - } else { - this.context.bezierCurveTo((this.x + x) / 2, this.y, (this.x + x) / 2, y, x + this.gap, y); - } - - this.currentPoint += 1; - this.x = x; - this.y = y; + point(xIn: number, yIn: number): void { + this.points.push({ x: xIn, y: yIn }); + if (this.points.length < 4) { return; } - // 0 is the top-right corner. - if (this.currentPoint === 0) { - // X from Y - this.context.moveTo(x, y + this.gap); - this.context.lineTo(x, y + this.gap); - } else if (this.currentPoint === 1) { - this.context.bezierCurveTo(this.x, (this.y + y) / 2, x, (this.y + y) / 2, x, y - this.gap); - } else if (this.currentPoint === 2) { - this.context.lineTo(x, y - this.gap); - } else { - this.context.bezierCurveTo(this.x, (this.y + y) / 2, x, (this.y + y) / 2, x, y + this.gap); - } + // Add gaps where they are needed. + this.points = this.points.map((point, index) => { + if (this.isHorizontal) { + return { + x: point.x + (index === 0 || index === 3 ? this.gap : -this.gap), + y: point.y, + }; + } + return { + x: point.x, + y: point.y + (index === 0 || index === 3 ? this.gap : -this.gap), + }; + }); - this.currentPoint += 1; - this.x = x; - this.y = y; + borderRadiusBumpPolygon(this.context, this.points, this.borderRadius); } } diff --git a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts index d023c313efdde..6fbc31ff5b22e 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts @@ -123,19 +123,6 @@ export class Linear implements CurveGenerator { return 0; }; - if (this.borderRadius > 0) { - borderRadiusPolygon(this.context, this.points, getBorderRadius()); - } else { - this.context.moveTo(this.points[0].x, this.points[0].y); - this.points.forEach((point, index) => { - if (index === 0) { - this.context.moveTo(point.x, point.y); - } - this.context.lineTo(point.x, point.y); - if (index === this.points.length - 1) { - this.context.closePath(); - } - }); - } + borderRadiusPolygon(this.context, this.points, getBorderRadius()); } } diff --git a/packages/x-charts-pro/src/FunnelChart/curves/step.ts b/packages/x-charts-pro/src/FunnelChart/curves/step.ts index 7d3c459eb1f36..932c54ffc4b1e 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/step.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/step.ts @@ -95,25 +95,12 @@ export class Step implements CurveGenerator { }; }); - if (this.borderRadius > 0) { - borderRadiusPolygon( - this.context, - this.points, - this.gap > 0 || this.position === 0 - ? this.borderRadius - : [this.borderRadius, this.borderRadius], - ); - } else { - this.context.moveTo(this.points[0].x, this.points[0].y); - this.points.forEach((point, index) => { - if (index === 0) { - this.context.moveTo(point.x, point.y); - } - this.context.lineTo(point.x, point.y); - if (index === this.points.length - 1) { - this.context.closePath(); - } - }); - } + borderRadiusPolygon( + this.context, + this.points, + this.gap > 0 || this.position === 0 + ? this.borderRadius + : [this.borderRadius, this.borderRadius], + ); } } From 032abd7b9121418efc732a1555d49abd079e7c82 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 18:31:43 +0200 Subject: [PATCH 06/20] doesnt work --- .../curves/borderRadiusBumpPolygon.ts | 165 ++++++++---------- 1 file changed, 73 insertions(+), 92 deletions(-) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts index 8b24917ac94cf..8f3fdc0d6090d 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts @@ -24,107 +24,88 @@ export function borderRadiusBumpPolygon( radius = Array.isArray(radius) ? radius : Array(numPoints).fill(radius); - const corners: Point[][] = []; + const corners: [Point, Point, Point, Point | null, Point | null][] = []; for (let i = 0; i < numPoints; i += 1) { + const secondToLastPoint = points.at(i - 1)!; const lastPoint = points[i]; const thisPoint = points[(i + 1) % numPoints]; const nextPoint = points[(i + 2) % numPoints]; - // Regular corners - if (i === 0 || i === 2) { - const lastEdgeLength = distance(lastPoint, thisPoint); - const lastOffsetDistance = Math.min(lastEdgeLength / 2, radius[i] ?? 0); - const start = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength); + 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); + + const lastEndEdgeLength = distance(lastPoint, secondToLastPoint); + const lastEndOffsetDistance = Math.min(lastEndEdgeLength / 2, radius[i] ?? 0); + const lastEndPoint = lerp2D( + lastPoint, + secondToLastPoint, + lastEndOffsetDistance / lastEndEdgeLength, + ); + + const angle = Math.abs( + (Math.atan2(thisPoint.x - nextPoint.x, thisPoint.y - nextPoint.y) * 180) / Math.PI, + ); + const isHorizontal = angle < 45 || angle > 135; + + const midPoint = { + x: (lastPoint.x + thisPoint.x) / 2, + y: (lastPoint.y + thisPoint.y) / 2, + }; + + const controlPoint1 = { + x: isHorizontal ? midPoint.x : lastEndPoint.x, + y: isHorizontal ? lastEndPoint.y : midPoint.y, + }; + + const controlPoint2 = { + x: isHorizontal ? midPoint.x : start.x, + y: isHorizontal ? start.y : midPoint.y, + }; + + const isBumpEdge = i === 0 || i === 2; + corners.push([ + start, + thisPoint, + end, + isBumpEdge ? controlPoint1 : null, + isBumpEdge ? controlPoint2 : null, + ]); + } + + // this.context.bezierCurveTo( + // (this.x + x ) / 2, + // this.y, + // (this.x + x ) / 2, + // y, + // x, + // y, + // ); + + // this.context.bezierCurveTo( + // this.x, + // (this.y + y ) / 2, + // x, + // (this.y + y ) / 2, + // x, + // y, + // ); - const nextEdgeLength = distance(nextPoint, thisPoint); - const nextOffsetDistance = Math.min(nextEdgeLength / 2, radius[i] ?? 0); - const end = lerp2D(thisPoint, nextPoint, nextOffsetDistance / nextEdgeLength); + ctx.moveTo(corners[0][0].x, corners[0][0].y); + for (let i = 0; i < corners.length; i += 1) { + const [start, ctrl, end, cp1, cp2] = corners[i]; - corners.push([start, thisPoint, end]); + if (cp1 === null || cp2 === null) { + ctx.lineTo(start.x, start.y); + ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y); } else { - // Start bump, from last point to the middle of the edge - const lastEdgeLength = distance(lastPoint, thisPoint); - const lastOffsetDistance = Math.min(lastEdgeLength / 2, radius[i] ?? 0); - const startBumpStart = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength); - - const midPoint = { - x: (thisPoint.x + lastPoint.x) / 2, - y: (thisPoint.y + lastPoint.y) / 2, - }; - const startBumpEdgeLength = distance(midPoint, thisPoint); - const startBumpOffsetDistance = Math.min(startBumpEdgeLength / 2, radius[i] ?? 0); - const startBumpEnd = lerp2D( - thisPoint, - nextPoint, - startBumpOffsetDistance / startBumpEdgeLength, - ); - - const angle = Math.abs( - (Math.atan2(thisPoint.x - lastPoint.x, thisPoint.y - lastPoint.y) * 180) / Math.PI, - ); - const isHorizontal = angle < 45 || angle > 135; - - const controlPoint = { - x: isHorizontal ? midPoint.x : lastPoint.x, - y: isHorizontal ? lastPoint.y : midPoint.y, - }; - - corners.push([startBumpStart, controlPoint, startBumpEnd]); - - // End bump, from the middle of the edge to the next point - const lastBumpEdgeLength = distance(thisPoint, nextPoint); - - // const edgeCp2 = { - // x: isHorizontal ? (lastPoint.x + thisPoint.x) / 2 : thisPoint.x, - // y: isHorizontal ? thisPoint.y : (lastPoint.y + thisPoint.y) / 2, - // }; + ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, start.x, start.y); + ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y); } - - // } else if (this.currentPoint === 1) { - // this.context.bezierCurveTo( - // (this.x + x - this.gap) / 2, - // this.y, - // (this.x + x - this.gap) / 2, - // y, - // x - this.gap, - // y, - // ); - // } else if (this.currentPoint === 3) { - // this.context.bezierCurveTo( - // (this.x + x - this.gap) / 2, - // this.y, - // (this.x + x - this.gap) / 2, - // y, - // x + this.gap, - // y, - // ); - // } - - // } else if (this.currentPoint === 1) { - // this.context.bezierCurveTo( - // this.x, - // (this.y + y - this.gap) / 2, - // x, - // (this.y + y - this.gap) / 2, - // x, - // y - this.gap, - // ); - // } else if (this.currentPoint === 3) { - // this.context.bezierCurveTo( - // this.x, - // (this.y + y - this.gap) / 2, - // x, - // (this.y + y - this.gap) / 2, - // x, - // y + this.gap, - // ); - // } - } - - 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(); From 678c2a5a0bd29396b7fd72b909601aff47d16c79 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 18:31:53 +0200 Subject: [PATCH 07/20] Revert "doesnt work" This reverts commit 032abd7b9121418efc732a1555d49abd079e7c82. --- .../curves/borderRadiusBumpPolygon.ts | 165 ++++++++++-------- 1 file changed, 92 insertions(+), 73 deletions(-) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts index 8f3fdc0d6090d..8b24917ac94cf 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts @@ -24,88 +24,107 @@ export function borderRadiusBumpPolygon( radius = Array.isArray(radius) ? radius : Array(numPoints).fill(radius); - const corners: [Point, Point, Point, Point | null, Point | null][] = []; + const corners: Point[][] = []; for (let i = 0; i < numPoints; i += 1) { - const secondToLastPoint = points.at(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); - - const lastEndEdgeLength = distance(lastPoint, secondToLastPoint); - const lastEndOffsetDistance = Math.min(lastEndEdgeLength / 2, radius[i] ?? 0); - const lastEndPoint = lerp2D( - lastPoint, - secondToLastPoint, - lastEndOffsetDistance / lastEndEdgeLength, - ); - - const angle = Math.abs( - (Math.atan2(thisPoint.x - nextPoint.x, thisPoint.y - nextPoint.y) * 180) / Math.PI, - ); - const isHorizontal = angle < 45 || angle > 135; - - const midPoint = { - x: (lastPoint.x + thisPoint.x) / 2, - y: (lastPoint.y + thisPoint.y) / 2, - }; - - const controlPoint1 = { - x: isHorizontal ? midPoint.x : lastEndPoint.x, - y: isHorizontal ? lastEndPoint.y : midPoint.y, - }; - - const controlPoint2 = { - x: isHorizontal ? midPoint.x : start.x, - y: isHorizontal ? start.y : midPoint.y, - }; - - const isBumpEdge = i === 0 || i === 2; - corners.push([ - start, - thisPoint, - end, - isBumpEdge ? controlPoint1 : null, - isBumpEdge ? controlPoint2 : null, - ]); - } - - // this.context.bezierCurveTo( - // (this.x + x ) / 2, - // this.y, - // (this.x + x ) / 2, - // y, - // x, - // y, - // ); - - // this.context.bezierCurveTo( - // this.x, - // (this.y + y ) / 2, - // x, - // (this.y + y ) / 2, - // x, - // y, - // ); + // Regular corners + if (i === 0 || i === 2) { + const lastEdgeLength = distance(lastPoint, thisPoint); + const lastOffsetDistance = Math.min(lastEdgeLength / 2, radius[i] ?? 0); + const start = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength); - ctx.moveTo(corners[0][0].x, corners[0][0].y); - for (let i = 0; i < corners.length; i += 1) { - const [start, ctrl, end, cp1, cp2] = corners[i]; + const nextEdgeLength = distance(nextPoint, thisPoint); + const nextOffsetDistance = Math.min(nextEdgeLength / 2, radius[i] ?? 0); + const end = lerp2D(thisPoint, nextPoint, nextOffsetDistance / nextEdgeLength); - if (cp1 === null || cp2 === null) { - ctx.lineTo(start.x, start.y); - ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y); + corners.push([start, thisPoint, end]); } else { - ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, start.x, start.y); - ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y); + // Start bump, from last point to the middle of the edge + const lastEdgeLength = distance(lastPoint, thisPoint); + const lastOffsetDistance = Math.min(lastEdgeLength / 2, radius[i] ?? 0); + const startBumpStart = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength); + + const midPoint = { + x: (thisPoint.x + lastPoint.x) / 2, + y: (thisPoint.y + lastPoint.y) / 2, + }; + const startBumpEdgeLength = distance(midPoint, thisPoint); + const startBumpOffsetDistance = Math.min(startBumpEdgeLength / 2, radius[i] ?? 0); + const startBumpEnd = lerp2D( + thisPoint, + nextPoint, + startBumpOffsetDistance / startBumpEdgeLength, + ); + + const angle = Math.abs( + (Math.atan2(thisPoint.x - lastPoint.x, thisPoint.y - lastPoint.y) * 180) / Math.PI, + ); + const isHorizontal = angle < 45 || angle > 135; + + const controlPoint = { + x: isHorizontal ? midPoint.x : lastPoint.x, + y: isHorizontal ? lastPoint.y : midPoint.y, + }; + + corners.push([startBumpStart, controlPoint, startBumpEnd]); + + // End bump, from the middle of the edge to the next point + const lastBumpEdgeLength = distance(thisPoint, nextPoint); + + // const edgeCp2 = { + // x: isHorizontal ? (lastPoint.x + thisPoint.x) / 2 : thisPoint.x, + // y: isHorizontal ? thisPoint.y : (lastPoint.y + thisPoint.y) / 2, + // }; } + + // } else if (this.currentPoint === 1) { + // this.context.bezierCurveTo( + // (this.x + x - this.gap) / 2, + // this.y, + // (this.x + x - this.gap) / 2, + // y, + // x - this.gap, + // y, + // ); + // } else if (this.currentPoint === 3) { + // this.context.bezierCurveTo( + // (this.x + x - this.gap) / 2, + // this.y, + // (this.x + x - this.gap) / 2, + // y, + // x + this.gap, + // y, + // ); + // } + + // } else if (this.currentPoint === 1) { + // this.context.bezierCurveTo( + // this.x, + // (this.y + y - this.gap) / 2, + // x, + // (this.y + y - this.gap) / 2, + // x, + // y - this.gap, + // ); + // } else if (this.currentPoint === 3) { + // this.context.bezierCurveTo( + // this.x, + // (this.y + y - this.gap) / 2, + // x, + // (this.y + y - this.gap) / 2, + // x, + // y + this.gap, + // ); + // } + } + + 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(); From c9bc9a91c57ec752b042ffd40d19c516fe1a0d1d Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 18:32:22 +0200 Subject: [PATCH 08/20] Revert "changes yolo" This reverts commit 81e03b7dc40e6a2eed3826da3f7572330ecdc93a. --- .../curves/borderRadiusBumpPolygon.ts | 131 ------------------ .../FunnelChart/curves/borderRadiusPolygon.ts | 3 - .../src/FunnelChart/curves/bump.ts | 71 ++++++---- .../src/FunnelChart/curves/linear.ts | 15 +- .../src/FunnelChart/curves/step.ts | 27 +++- 5 files changed, 76 insertions(+), 171 deletions(-) delete mode 100644 packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts deleted file mode 100644 index 8b24917ac94cf..0000000000000 --- a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusBumpPolygon.ts +++ /dev/null @@ -1,131 +0,0 @@ -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), -}); - -/** - * Same as `borderRadiusPolygon` but with a bump in the middle - * of the relevant edges. - * - * @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 borderRadiusBumpPolygon( - 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]; - - // Regular corners - if (i === 0 || i === 2) { - 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]); - } else { - // Start bump, from last point to the middle of the edge - const lastEdgeLength = distance(lastPoint, thisPoint); - const lastOffsetDistance = Math.min(lastEdgeLength / 2, radius[i] ?? 0); - const startBumpStart = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength); - - const midPoint = { - x: (thisPoint.x + lastPoint.x) / 2, - y: (thisPoint.y + lastPoint.y) / 2, - }; - const startBumpEdgeLength = distance(midPoint, thisPoint); - const startBumpOffsetDistance = Math.min(startBumpEdgeLength / 2, radius[i] ?? 0); - const startBumpEnd = lerp2D( - thisPoint, - nextPoint, - startBumpOffsetDistance / startBumpEdgeLength, - ); - - const angle = Math.abs( - (Math.atan2(thisPoint.x - lastPoint.x, thisPoint.y - lastPoint.y) * 180) / Math.PI, - ); - const isHorizontal = angle < 45 || angle > 135; - - const controlPoint = { - x: isHorizontal ? midPoint.x : lastPoint.x, - y: isHorizontal ? lastPoint.y : midPoint.y, - }; - - corners.push([startBumpStart, controlPoint, startBumpEnd]); - - // End bump, from the middle of the edge to the next point - const lastBumpEdgeLength = distance(thisPoint, nextPoint); - - // const edgeCp2 = { - // x: isHorizontal ? (lastPoint.x + thisPoint.x) / 2 : thisPoint.x, - // y: isHorizontal ? thisPoint.y : (lastPoint.y + thisPoint.y) / 2, - // }; - } - - // } else if (this.currentPoint === 1) { - // this.context.bezierCurveTo( - // (this.x + x - this.gap) / 2, - // this.y, - // (this.x + x - this.gap) / 2, - // y, - // x - this.gap, - // y, - // ); - // } else if (this.currentPoint === 3) { - // this.context.bezierCurveTo( - // (this.x + x - this.gap) / 2, - // this.y, - // (this.x + x - this.gap) / 2, - // y, - // x + this.gap, - // y, - // ); - // } - - // } else if (this.currentPoint === 1) { - // this.context.bezierCurveTo( - // this.x, - // (this.y + y - this.gap) / 2, - // x, - // (this.y + y - this.gap) / 2, - // x, - // y - this.gap, - // ); - // } else if (this.currentPoint === 3) { - // this.context.bezierCurveTo( - // this.x, - // (this.y + y - this.gap) / 2, - // x, - // (this.y + y - this.gap) / 2, - // x, - // y + this.gap, - // ); - // } - } - - 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(); -} diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts index d96ea2de1c72d..0217feb7e51ad 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts @@ -9,9 +9,6 @@ const lerp2D = (p1: Point, p2: Point, t: number) => ({ /** * Draws a polygon with rounded corners - * - * Take from https://stackoverflow.com/a/56214413/24269134 - * * @param {CanvasRenderingContext2D} ctx The canvas context * @param {Array} points A list of `{x, y}` points * @radius {number} how much to round the corners diff --git a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts index 9fd884db814e7..8052662fadb94 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts @@ -1,7 +1,5 @@ /* eslint-disable class-methods-use-this */ import { CurveGenerator } from '@mui/x-charts-vendor/d3-shape'; -import { Point } from './curve.types'; -import { borderRadiusBumpPolygon } from './borderRadiusBumpPolygon'; /** * This is a custom "bump" curve generator. @@ -14,26 +12,23 @@ import { borderRadiusBumpPolygon } from './borderRadiusBumpPolygon'; export class Bump implements CurveGenerator { private context: CanvasRenderingContext2D; - private isHorizontal: boolean = false; + private x: number = NaN; - private gap: number = 0; + private y: number = NaN; - private borderRadius: number = 0; + private currentPoint: number = 0; - private points: Point[] = []; + private isHorizontal: boolean = false; + + private gap: number = 0; constructor( context: CanvasRenderingContext2D, - { - isHorizontal, - gap, - borderRadius, - }: { isHorizontal: boolean; gap?: number; borderRadius?: number }, + { isHorizontal, gap }: { isHorizontal: boolean; gap?: number }, ) { this.context = context; this.isHorizontal = isHorizontal; this.gap = (gap ?? 0) / 2; - this.borderRadius = borderRadius ?? 0; } areaStart(): void {} @@ -44,26 +39,44 @@ export class Bump implements CurveGenerator { lineEnd(): void {} - point(xIn: number, yIn: number): void { - this.points.push({ x: xIn, y: yIn }); - if (this.points.length < 4) { + point(x: number, y: number): void { + x = +x; + y = +y; + + // 0 is the top-left corner. + if (this.isHorizontal) { + if (this.currentPoint === 0) { + this.context.moveTo(x + this.gap, y); + this.context.lineTo(x + this.gap, y); + } else if (this.currentPoint === 1) { + this.context.bezierCurveTo((this.x + x) / 2, this.y, (this.x + x) / 2, y, x - this.gap, y); + } else if (this.currentPoint === 2) { + this.context.lineTo(x - this.gap, y); + } else { + this.context.bezierCurveTo((this.x + x) / 2, this.y, (this.x + x) / 2, y, x + this.gap, y); + } + + this.currentPoint += 1; + this.x = x; + this.y = y; return; } - // Add gaps where they are needed. - this.points = this.points.map((point, index) => { - if (this.isHorizontal) { - return { - x: point.x + (index === 0 || index === 3 ? this.gap : -this.gap), - y: point.y, - }; - } - return { - x: point.x, - y: point.y + (index === 0 || index === 3 ? this.gap : -this.gap), - }; - }); + // 0 is the top-right corner. + if (this.currentPoint === 0) { + // X from Y + this.context.moveTo(x, y + this.gap); + this.context.lineTo(x, y + this.gap); + } else if (this.currentPoint === 1) { + this.context.bezierCurveTo(this.x, (this.y + y) / 2, x, (this.y + y) / 2, x, y - this.gap); + } else if (this.currentPoint === 2) { + this.context.lineTo(x, y - this.gap); + } else { + this.context.bezierCurveTo(this.x, (this.y + y) / 2, x, (this.y + y) / 2, x, y + this.gap); + } - borderRadiusBumpPolygon(this.context, this.points, this.borderRadius); + this.currentPoint += 1; + this.x = x; + this.y = y; } } diff --git a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts index 6fbc31ff5b22e..d023c313efdde 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts @@ -123,6 +123,19 @@ export class Linear implements CurveGenerator { return 0; }; - borderRadiusPolygon(this.context, this.points, getBorderRadius()); + if (this.borderRadius > 0) { + borderRadiusPolygon(this.context, this.points, getBorderRadius()); + } else { + this.context.moveTo(this.points[0].x, this.points[0].y); + this.points.forEach((point, index) => { + if (index === 0) { + this.context.moveTo(point.x, point.y); + } + this.context.lineTo(point.x, point.y); + if (index === this.points.length - 1) { + this.context.closePath(); + } + }); + } } } diff --git a/packages/x-charts-pro/src/FunnelChart/curves/step.ts b/packages/x-charts-pro/src/FunnelChart/curves/step.ts index 932c54ffc4b1e..7d3c459eb1f36 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/step.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/step.ts @@ -95,12 +95,25 @@ export class Step implements CurveGenerator { }; }); - borderRadiusPolygon( - this.context, - this.points, - this.gap > 0 || this.position === 0 - ? this.borderRadius - : [this.borderRadius, this.borderRadius], - ); + if (this.borderRadius > 0) { + borderRadiusPolygon( + this.context, + this.points, + this.gap > 0 || this.position === 0 + ? this.borderRadius + : [this.borderRadius, this.borderRadius], + ); + } else { + this.context.moveTo(this.points[0].x, this.points[0].y); + this.points.forEach((point, index) => { + if (index === 0) { + this.context.moveTo(point.x, point.y); + } + this.context.lineTo(point.x, point.y); + if (index === this.points.length - 1) { + this.context.closePath(); + } + }); + } } } From aea349b37402a456d3938faacfe1eb8b88344b16 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 18:33:08 +0200 Subject: [PATCH 09/20] simplify --- .../src/FunnelChart/curves/linear.ts | 15 +---------- .../src/FunnelChart/curves/step.ts | 27 +++++-------------- 2 files changed, 8 insertions(+), 34 deletions(-) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts index d023c313efdde..6fbc31ff5b22e 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts @@ -123,19 +123,6 @@ export class Linear implements CurveGenerator { return 0; }; - if (this.borderRadius > 0) { - borderRadiusPolygon(this.context, this.points, getBorderRadius()); - } else { - this.context.moveTo(this.points[0].x, this.points[0].y); - this.points.forEach((point, index) => { - if (index === 0) { - this.context.moveTo(point.x, point.y); - } - this.context.lineTo(point.x, point.y); - if (index === this.points.length - 1) { - this.context.closePath(); - } - }); - } + borderRadiusPolygon(this.context, this.points, getBorderRadius()); } } diff --git a/packages/x-charts-pro/src/FunnelChart/curves/step.ts b/packages/x-charts-pro/src/FunnelChart/curves/step.ts index 7d3c459eb1f36..932c54ffc4b1e 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/step.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/step.ts @@ -95,25 +95,12 @@ export class Step implements CurveGenerator { }; }); - if (this.borderRadius > 0) { - borderRadiusPolygon( - this.context, - this.points, - this.gap > 0 || this.position === 0 - ? this.borderRadius - : [this.borderRadius, this.borderRadius], - ); - } else { - this.context.moveTo(this.points[0].x, this.points[0].y); - this.points.forEach((point, index) => { - if (index === 0) { - this.context.moveTo(point.x, point.y); - } - this.context.lineTo(point.x, point.y); - if (index === this.points.length - 1) { - this.context.closePath(); - } - }); - } + borderRadiusPolygon( + this.context, + this.points, + this.gap > 0 || this.position === 0 + ? this.borderRadius + : [this.borderRadius, this.borderRadius], + ); } } From abfe1e0241dfacdc7c7f81d09aac0b03a2e2ea27 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 18:48:41 +0200 Subject: [PATCH 10/20] docs and scripts --- docs/data/charts/funnel/FunnelBorderRadius.js | 29 +++++++++++++++++++ .../data/charts/funnel/FunnelBorderRadius.tsx | 29 +++++++++++++++++++ docs/data/charts/funnel/FunnelCurves.js | 9 ++++++ docs/data/charts/funnel/FunnelCurves.tsx | 2 +- docs/data/charts/funnel/funnel.md | 15 ++++++++++ docs/pages/x/api/charts/funnel-chart.json | 1 + docs/pages/x/api/charts/funnel-plot.json | 1 + .../charts/funnel-chart/funnel-chart.json | 3 ++ .../charts/funnel-plot/funnel-plot.json | 3 ++ .../src/FunnelChart/FunnelChart.tsx | 5 ++++ .../src/FunnelChart/FunnelPlot.tsx | 7 ++++- 11 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 docs/data/charts/funnel/FunnelBorderRadius.js create mode 100644 docs/data/charts/funnel/FunnelBorderRadius.tsx diff --git a/docs/data/charts/funnel/FunnelBorderRadius.js b/docs/data/charts/funnel/FunnelBorderRadius.js new file mode 100644 index 0000000000000..422d7dd4141e7 --- /dev/null +++ b/docs/data/charts/funnel/FunnelBorderRadius.js @@ -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 ( + + + + + ); +} diff --git a/docs/data/charts/funnel/FunnelBorderRadius.tsx b/docs/data/charts/funnel/FunnelBorderRadius.tsx new file mode 100644 index 0000000000000..422d7dd4141e7 --- /dev/null +++ b/docs/data/charts/funnel/FunnelBorderRadius.tsx @@ -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 ( + + + + + ); +} diff --git a/docs/data/charts/funnel/FunnelCurves.js b/docs/data/charts/funnel/FunnelCurves.js index 4e67aafae13a1..e1d38397ea61b 100644 --- a/docs/data/charts/funnel/FunnelCurves.js +++ b/docs/data/charts/funnel/FunnelCurves.js @@ -22,6 +22,12 @@ export default function FunnelCurves() { min: 0, max: 20, }, + borderRadius: { + knob: 'slider', + defaultValue: 0, + min: 0, + max: 20, + }, }} renderDemo={(props) => ( @@ -34,6 +40,7 @@ export default function FunnelCurves() { }, ]} gap={props.gap} + borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> @@ -46,6 +53,7 @@ export default function FunnelCurves() { }, ]} gap={props.gap} + borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> @@ -57,6 +65,7 @@ export default function FunnelCurves() { `; }} diff --git a/docs/data/charts/funnel/FunnelCurves.tsx b/docs/data/charts/funnel/FunnelCurves.tsx index 107f69a6aeab0..7b0c46d96e280 100644 --- a/docs/data/charts/funnel/FunnelCurves.tsx +++ b/docs/data/charts/funnel/FunnelCurves.tsx @@ -67,7 +67,7 @@ export default function FunnelCurves() { `; }} diff --git a/docs/data/charts/funnel/funnel.md b/docs/data/charts/funnel/funnel.md index 74c2879a15f9d..d3bedee34c715 100644 --- a/docs/data/charts/funnel/funnel.md +++ b/docs/data/charts/funnel/funnel.md @@ -67,6 +67,21 @@ It accepts a number that represents the gap in pixels. {{"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. +- The `linear` curve will respect the border radius to some extent due to the angle of the sections. +- The `step` curve will respect the border radius. + +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. + +{{"demo": "FunnelBorderRadius.js"}} + ### Colors The funnel colors can be customized in two ways. diff --git a/docs/pages/x/api/charts/funnel-chart.json b/docs/pages/x/api/charts/funnel-chart.json index a743f00c98845..6fc217fd7bc31 100644 --- a/docs/pages/x/api/charts/funnel-chart.json +++ b/docs/pages/x/api/charts/funnel-chart.json @@ -14,6 +14,7 @@ "text": "highlighting docs" } }, + "borderRadius": { "type": { "name": "number" }, "default": "0" }, "categoryAxis": { "type": { "name": "union", diff --git a/docs/pages/x/api/charts/funnel-plot.json b/docs/pages/x/api/charts/funnel-plot.json index fc1a9df651640..2eef8187115e2 100644 --- a/docs/pages/x/api/charts/funnel-plot.json +++ b/docs/pages/x/api/charts/funnel-plot.json @@ -1,5 +1,6 @@ { "props": { + "borderRadius": { "type": { "name": "number" }, "default": "0" }, "gap": { "type": { "name": "number" }, "default": "0" }, "onItemClick": { "type": { "name": "func" }, diff --git a/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json b/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json index 7400c0bb002ba..6f50ca0ad83fb 100644 --- a/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json +++ b/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json @@ -5,6 +5,9 @@ "description": "The configuration of axes highlight. Default is set to 'band' in the bar direction. Depends on layout prop.", "seeMoreText": "See {{link}} for more details." }, + "borderRadius": { + "description": "The radius, in pixels, of the corners of the funnel sections." + }, "categoryAxis": { "description": "The configuration of the category axis." }, "colors": { "description": "Color palette used to colorize multiple series." }, "disableAxisListener": { diff --git a/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json b/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json index b2ef54118410c..dd6a672de41c8 100644 --- a/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json +++ b/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json @@ -1,6 +1,9 @@ { "componentDescription": "", "propDescriptions": { + "borderRadius": { + "description": "The radius, in pixels, of the corners of the funnel sections." + }, "gap": { "description": "The gap, in pixels, between funnel sections." }, "onItemClick": { "description": "Callback fired when a funnel item is clicked.", diff --git a/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx b/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx index 13aeafda539b3..39f88bde3d8a5 100644 --- a/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx +++ b/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx @@ -133,6 +133,11 @@ FunnelChart.propTypes = { x: PropTypes.oneOf(['band', 'line', 'none']), y: PropTypes.oneOf(['band', 'line', 'none']), }), + /** + * The radius, in pixels, of the corners of the funnel sections. + * @default 0 + */ + borderRadius: PropTypes.number, /** * The configuration of the category axis. * diff --git a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx index 53d00cba00d92..0db3fb198b7b4 100644 --- a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx +++ b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx @@ -21,7 +21,7 @@ export interface FunnelPlotProps extends FunnelPlotSlotExtension { */ gap?: number; /** - * The radius of the corners of the funnel sections. + * The radius, in pixels, of the corners of the funnel sections. * @default 0 */ borderRadius?: number; @@ -220,6 +220,11 @@ FunnelPlot.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- + /** + * The radius, in pixels, of the corners of the funnel sections. + * @default 0 + */ + borderRadius: PropTypes.number, /** * The gap, in pixels, between funnel sections. * @default 0 From bb32b289b1dea95874529ff5ad8ba7ed1cca95a3 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Fri, 2 May 2025 20:00:37 +0200 Subject: [PATCH 11/20] close the bump path --- packages/x-charts-pro/src/FunnelChart/curves/bump.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts index 8052662fadb94..4cdd2db0e6b32 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/bump.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/bump.ts @@ -56,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; @@ -75,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; From 5e824f0fae1786f2640371a040cc2f12c5d10fba Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Sat, 3 May 2025 15:54:34 +0200 Subject: [PATCH 12/20] Move border radius to series --- docs/data/charts/funnel/FunnelBorderRadius.tsx | 4 ++-- docs/data/charts/funnel/FunnelCurves.tsx | 10 ++++++---- .../x-charts-pro/src/FunnelChart/FunnelPlot.tsx | 15 +++++---------- .../x-charts-pro/src/FunnelChart/funnel.types.ts | 5 +++++ .../src/FunnelChart/useFunnelChartProps.ts | 2 -- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/data/charts/funnel/FunnelBorderRadius.tsx b/docs/data/charts/funnel/FunnelBorderRadius.tsx index 422d7dd4141e7..fc51bc47653a2 100644 --- a/docs/data/charts/funnel/FunnelBorderRadius.tsx +++ b/docs/data/charts/funnel/FunnelBorderRadius.tsx @@ -9,20 +9,20 @@ export default function FunnelBorderRadius() { series={[ { data: [{ value: 200 }, { value: 180 }, { value: 90 }, { value: 50 }], + borderRadius: 10, }, ]} height={300} - borderRadius={10} /> ); diff --git a/docs/data/charts/funnel/FunnelCurves.tsx b/docs/data/charts/funnel/FunnelCurves.tsx index 7b0c46d96e280..cc63c398762a0 100644 --- a/docs/data/charts/funnel/FunnelCurves.tsx +++ b/docs/data/charts/funnel/FunnelCurves.tsx @@ -37,12 +37,12 @@ export default function FunnelCurves() { series={[ { curve: props.curveType, + borderRadius: props.borderRadius, layout: 'vertical', ...populationByEducationLevelPercentageSeries, }, ]} gap={props.gap} - borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> @@ -50,12 +50,12 @@ export default function FunnelCurves() { series={[ { curve: props.curveType, + borderRadius: props.borderRadius, layout: 'horizontal', ...populationByEducationLevelPercentageSeries, }, ]} gap={props.gap} - borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> @@ -65,9 +65,11 @@ export default function FunnelCurves() { return `import { FunnelChart } from '@mui/x-charts-pro/FunnelChart'; `; }} diff --git a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx index 0db3fb198b7b4..4ddf4923c6e23 100644 --- a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx +++ b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx @@ -20,11 +20,6 @@ export interface FunnelPlotProps extends FunnelPlotSlotExtension { * @default 0 */ gap?: number; - /** - * The radius, in pixels, of the corners of the funnel sections. - * @default 0 - */ - borderRadius?: number; /** * Callback fired when a funnel item is clicked. * @param {React.MouseEvent} event The event source of the callback. @@ -36,7 +31,7 @@ export interface FunnelPlotProps extends FunnelPlotSlotExtension { ) => void; } -const useAggregatedData = (gap: number | undefined, borderRadius: number | undefined) => { +const useAggregatedData = (gap: number | undefined) => { const seriesData = useFunnelSeriesContext(); const { xAxis, xAxisIds } = useXAxes(); const { yAxis, yAxisIds } = useYAxes(); @@ -116,7 +111,7 @@ const useAggregatedData = (gap: number | undefined, borderRadius: number | undef gap, dataIndex, currentSeries.dataPoints.length, - borderRadius, + currentSeries.borderRadius, ); const line = d3Line() @@ -154,16 +149,16 @@ const useAggregatedData = (gap: number | undefined, borderRadius: number | undef }); return result.flat(); - }, [seriesData, xAxis, xAxisIds, yAxis, yAxisIds, gap, borderRadius]); + }, [seriesData, xAxis, xAxisIds, yAxis, yAxisIds, gap]); return allData; }; function FunnelPlot(props: FunnelPlotProps) { - const { onItemClick, gap, borderRadius, ...other } = props; + const { onItemClick, gap, ...other } = props; const theme = useTheme(); - const data = useAggregatedData(gap, borderRadius); + const data = useAggregatedData(gap); return ( diff --git a/packages/x-charts-pro/src/FunnelChart/funnel.types.ts b/packages/x-charts-pro/src/FunnelChart/funnel.types.ts index f4d91a61c59ca..4136e113ba6b1 100644 --- a/packages/x-charts-pro/src/FunnelChart/funnel.types.ts +++ b/packages/x-charts-pro/src/FunnelChart/funnel.types.ts @@ -58,6 +58,11 @@ export interface FunnelSeriesType * @default 'linear' */ curve?: FunnelCurveType; + /** + * The radius, in pixels, of the corners of the funnel sections. + * @default 0 + */ + borderRadius?: number; /** * The label configuration for the funnel plot. * Allows to customize the position and margin of the label that is displayed on the funnel sections. diff --git a/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts b/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts index 534778d986d80..66db4c7ce4e25 100644 --- a/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts +++ b/packages/x-charts-pro/src/FunnelChart/useFunnelChartProps.ts @@ -127,7 +127,6 @@ export const useFunnelChartProps = (props: FunnelChartProps) => { axisHighlight, apiRef, gap, - borderRadius, ...rest } = props; const margin = defaultizeMargin(marginProps, DEFAULT_MARGINS); @@ -173,7 +172,6 @@ export const useFunnelChartProps = (props: FunnelChartProps) => { const funnelPlotProps: FunnelPlotProps = { gap, - borderRadius, onItemClick, slots, slotProps, From fb25b9ce4c74c786e646619e0313dffb36820600 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Mon, 5 May 2025 10:43:17 +0200 Subject: [PATCH 13/20] scripts --- docs/data/charts/funnel/FunnelBorderRadius.js | 4 ++-- docs/data/charts/funnel/FunnelCurves.js | 4 ++-- docs/pages/x/api/charts/funnel-chart.json | 1 - docs/pages/x/api/charts/funnel-plot.json | 1 - .../api-docs/charts/funnel-chart/funnel-chart.json | 3 --- .../api-docs/charts/funnel-plot/funnel-plot.json | 3 --- packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx | 5 ----- packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx | 5 ----- 8 files changed, 4 insertions(+), 22 deletions(-) diff --git a/docs/data/charts/funnel/FunnelBorderRadius.js b/docs/data/charts/funnel/FunnelBorderRadius.js index 422d7dd4141e7..fc51bc47653a2 100644 --- a/docs/data/charts/funnel/FunnelBorderRadius.js +++ b/docs/data/charts/funnel/FunnelBorderRadius.js @@ -9,20 +9,20 @@ export default function FunnelBorderRadius() { series={[ { data: [{ value: 200 }, { value: 180 }, { value: 90 }, { value: 50 }], + borderRadius: 10, }, ]} height={300} - borderRadius={10} /> ); diff --git a/docs/data/charts/funnel/FunnelCurves.js b/docs/data/charts/funnel/FunnelCurves.js index e1d38397ea61b..75141f6c3f7be 100644 --- a/docs/data/charts/funnel/FunnelCurves.js +++ b/docs/data/charts/funnel/FunnelCurves.js @@ -36,11 +36,11 @@ export default function FunnelCurves() { { curve: props.curveType, layout: 'vertical', + borderRadius: props.borderRadius, ...populationByEducationLevelPercentageSeries, }, ]} gap={props.gap} - borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> @@ -49,11 +49,11 @@ export default function FunnelCurves() { { curve: props.curveType, layout: 'horizontal', + borderRadius: props.borderRadius, ...populationByEducationLevelPercentageSeries, }, ]} gap={props.gap} - borderRadius={props.borderRadius} height={300} slotProps={{ legend: { direction: 'vertical' } }} /> diff --git a/docs/pages/x/api/charts/funnel-chart.json b/docs/pages/x/api/charts/funnel-chart.json index 6fc217fd7bc31..a743f00c98845 100644 --- a/docs/pages/x/api/charts/funnel-chart.json +++ b/docs/pages/x/api/charts/funnel-chart.json @@ -14,7 +14,6 @@ "text": "highlighting docs" } }, - "borderRadius": { "type": { "name": "number" }, "default": "0" }, "categoryAxis": { "type": { "name": "union", diff --git a/docs/pages/x/api/charts/funnel-plot.json b/docs/pages/x/api/charts/funnel-plot.json index 2eef8187115e2..fc1a9df651640 100644 --- a/docs/pages/x/api/charts/funnel-plot.json +++ b/docs/pages/x/api/charts/funnel-plot.json @@ -1,6 +1,5 @@ { "props": { - "borderRadius": { "type": { "name": "number" }, "default": "0" }, "gap": { "type": { "name": "number" }, "default": "0" }, "onItemClick": { "type": { "name": "func" }, diff --git a/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json b/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json index 6f50ca0ad83fb..7400c0bb002ba 100644 --- a/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json +++ b/docs/translations/api-docs/charts/funnel-chart/funnel-chart.json @@ -5,9 +5,6 @@ "description": "The configuration of axes highlight. Default is set to 'band' in the bar direction. Depends on layout prop.", "seeMoreText": "See {{link}} for more details." }, - "borderRadius": { - "description": "The radius, in pixels, of the corners of the funnel sections." - }, "categoryAxis": { "description": "The configuration of the category axis." }, "colors": { "description": "Color palette used to colorize multiple series." }, "disableAxisListener": { diff --git a/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json b/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json index dd6a672de41c8..b2ef54118410c 100644 --- a/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json +++ b/docs/translations/api-docs/charts/funnel-plot/funnel-plot.json @@ -1,9 +1,6 @@ { "componentDescription": "", "propDescriptions": { - "borderRadius": { - "description": "The radius, in pixels, of the corners of the funnel sections." - }, "gap": { "description": "The gap, in pixels, between funnel sections." }, "onItemClick": { "description": "Callback fired when a funnel item is clicked.", diff --git a/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx b/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx index 39f88bde3d8a5..13aeafda539b3 100644 --- a/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx +++ b/packages/x-charts-pro/src/FunnelChart/FunnelChart.tsx @@ -133,11 +133,6 @@ FunnelChart.propTypes = { x: PropTypes.oneOf(['band', 'line', 'none']), y: PropTypes.oneOf(['band', 'line', 'none']), }), - /** - * The radius, in pixels, of the corners of the funnel sections. - * @default 0 - */ - borderRadius: PropTypes.number, /** * The configuration of the category axis. * diff --git a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx index 4ddf4923c6e23..0d61e68c5aa37 100644 --- a/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx +++ b/packages/x-charts-pro/src/FunnelChart/FunnelPlot.tsx @@ -215,11 +215,6 @@ FunnelPlot.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "pnpm proptypes" | // ---------------------------------------------------------------------- - /** - * The radius, in pixels, of the corners of the funnel sections. - * @default 0 - */ - borderRadius: PropTypes.number, /** * The gap, in pixels, between funnel sections. * @default 0 From d849488116ac21efe34682b659a55eba7709f143 Mon Sep 17 00:00:00 2001 From: Jose C Quintas Jr Date: Mon, 5 May 2025 13:04:24 +0200 Subject: [PATCH 14/20] Update docs/data/charts/funnel/funnel.md Co-authored-by: Bernardo Belchior Signed-off-by: Jose C Quintas Jr --- docs/data/charts/funnel/funnel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/charts/funnel/funnel.md b/docs/data/charts/funnel/funnel.md index d3bedee34c715..b740613ef19e1 100644 --- a/docs/data/charts/funnel/funnel.md +++ b/docs/data/charts/funnel/funnel.md @@ -73,7 +73,7 @@ The border radius of the sections can be customized by the `borderRadius` proper It accepts a number that represents the radius in pixels. - The `bump` curve interpolation will not respect the border radius. -- The `linear` curve will respect the border radius to some extent due to the angle of the sections. +- The `linear` curve respects the border radius to some extent due to the angle of the sections. - The `step` curve will respect the border radius. You can play with this in the [curve interpolation example](/x/react-charts/funnel/#curve-interpolation) above. From a0c75277eac6cde9c2f4dac8ae309d6087b2fe5a Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Mon, 5 May 2025 13:07:27 +0200 Subject: [PATCH 15/20] docs suggestions --- docs/data/charts/funnel/funnel.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/data/charts/funnel/funnel.md b/docs/data/charts/funnel/funnel.md index b740613ef19e1..5dd46bcb7e06d 100644 --- a/docs/data/charts/funnel/funnel.md +++ b/docs/data/charts/funnel/funnel.md @@ -76,10 +76,13 @@ It accepts a number that represents the radius in pixels. - The `linear` curve respects the border radius to some extent due to the angle of the sections. - The `step` curve will respect the border radius. -You can play with this in the [curve interpolation example](/x/react-charts/funnel/#curve-interpolation) above. +To understand how the border radius interacts with the `curve` prop, see 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. +- If the `gap` is 0, the border radius will be applied to the corners of the sections that are not connected to another section. +- If the `gap` is bigger than 0, the border radius will be applied to all the corners of the sections. + {{"demo": "FunnelBorderRadius.js"}} ### Colors From 80dfa127406368a0d5ef621a6425bf8986dfe13f Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Mon, 5 May 2025 13:23:32 +0200 Subject: [PATCH 16/20] scripts --- docs/data/charts/funnel/FunnelCurves.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/data/charts/funnel/FunnelCurves.js b/docs/data/charts/funnel/FunnelCurves.js index 75141f6c3f7be..141b4deb6089b 100644 --- a/docs/data/charts/funnel/FunnelCurves.js +++ b/docs/data/charts/funnel/FunnelCurves.js @@ -35,8 +35,8 @@ export default function FunnelCurves() { series={[ { curve: props.curveType, - layout: 'vertical', borderRadius: props.borderRadius, + layout: 'vertical', ...populationByEducationLevelPercentageSeries, }, ]} @@ -48,8 +48,8 @@ export default function FunnelCurves() { series={[ { curve: props.curveType, - layout: 'horizontal', borderRadius: props.borderRadius, + layout: 'horizontal', ...populationByEducationLevelPercentageSeries, }, ]} @@ -63,9 +63,11 @@ export default function FunnelCurves() { return `import { FunnelChart } from '@mui/x-charts-pro/FunnelChart'; `; }} From 91d126c18c569cb15894c51d343e4d2974594d5d Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Mon, 5 May 2025 15:31:40 +0200 Subject: [PATCH 17/20] linear curve alignment --- .../src/FunnelChart/curves/linear.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts index 6fbc31ff5b22e..7275253658599 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts @@ -90,9 +90,14 @@ export class Linear implements CurveGenerator { // Add gaps where they are needed. this.points = this.points.map((point, index) => { - const slopeStart = this.points.at(index <= 1 ? 0 : 2)!; - const slopeEnd = this.points.at(index <= 1 ? 1 : 3)!; - const yGetter = yFromX(slopeStart.x, slopeStart.y, slopeEnd.x, slopeEnd.y); + const slopeStart = this.points.at(index <= 1 ? 0 : 3)!; + const slopeEnd = this.points.at(index <= 1 ? 1 : 2)!; + const yGetter = yFromX( + slopeStart.x - this.gap, + slopeStart.y, + slopeEnd.x - this.gap, + slopeEnd.y, + ); if (this.isHorizontal) { const xGap = point.x + (index === 0 || index === 3 ? this.gap : -this.gap); @@ -102,7 +107,12 @@ export class Linear implements CurveGenerator { }; } - const xGetter = xFromY(slopeStart.x, slopeStart.y, slopeEnd.x, slopeEnd.y); + const xGetter = xFromY( + slopeStart.x, + slopeStart.y - this.gap, + slopeEnd.x, + slopeEnd.y - this.gap, + ); const yGap = point.y + (index === 0 || index === 3 ? this.gap : -this.gap); return { x: xGetter(yGap), From fe4ffb2775cb15acf30de6a0471e509f1d3dd39a Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Mon, 5 May 2025 17:10:01 +0200 Subject: [PATCH 18/20] invert second slope --- packages/x-charts-pro/src/FunnelChart/curves/linear.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts index 7275253658599..016d92921944e 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/linear.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/linear.ts @@ -90,8 +90,8 @@ export class Linear implements CurveGenerator { // Add gaps where they are needed. this.points = this.points.map((point, index) => { - const slopeStart = this.points.at(index <= 1 ? 0 : 3)!; - const slopeEnd = this.points.at(index <= 1 ? 1 : 2)!; + const slopeStart = this.points.at(index <= 1 ? 0 : 2)!; + const slopeEnd = this.points.at(index <= 1 ? 1 : 3)!; const yGetter = yFromX( slopeStart.x - this.gap, slopeStart.y, From 35fd0335d2923831dd90829c47a19ec793c020c2 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Tue, 6 May 2025 11:43:44 +0200 Subject: [PATCH 19/20] default to 8 px border radius --- packages/x-charts-pro/src/FunnelChart/funnel.types.ts | 2 +- .../src/FunnelChart/seriesConfig/getSeriesWithDefaultValues.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/x-charts-pro/src/FunnelChart/funnel.types.ts b/packages/x-charts-pro/src/FunnelChart/funnel.types.ts index 4136e113ba6b1..3a5bb138f66f2 100644 --- a/packages/x-charts-pro/src/FunnelChart/funnel.types.ts +++ b/packages/x-charts-pro/src/FunnelChart/funnel.types.ts @@ -60,7 +60,7 @@ export interface FunnelSeriesType curve?: FunnelCurveType; /** * The radius, in pixels, of the corners of the funnel sections. - * @default 0 + * @default 8 */ borderRadius?: number; /** diff --git a/packages/x-charts-pro/src/FunnelChart/seriesConfig/getSeriesWithDefaultValues.ts b/packages/x-charts-pro/src/FunnelChart/seriesConfig/getSeriesWithDefaultValues.ts index 14cbadb5b9fc1..19c0764c80038 100644 --- a/packages/x-charts-pro/src/FunnelChart/seriesConfig/getSeriesWithDefaultValues.ts +++ b/packages/x-charts-pro/src/FunnelChart/seriesConfig/getSeriesWithDefaultValues.ts @@ -8,6 +8,7 @@ const getSeriesWithDefaultValues: GetSeriesWithDefaultValues<'funnel'> = ( return { id: seriesData.id ?? `auto-generated-id-${seriesIndex}`, ...seriesData, + borderRadius: seriesData.borderRadius ?? 8, data: seriesData.data.map((d, index) => ({ color: colors[index % colors.length], ...d, From bfab8a621a51dde3526363489e637d27a531e3f0 Mon Sep 17 00:00:00 2001 From: Jose Quintas Date: Tue, 6 May 2025 13:13:26 +0200 Subject: [PATCH 20/20] suggestions --- docs/data/charts/funnel/funnel.md | 4 ++-- .../src/FunnelChart/curves/borderRadiusPolygon.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data/charts/funnel/funnel.md b/docs/data/charts/funnel/funnel.md index 5dd46bcb7e06d..ba94ff1a48c2b 100644 --- a/docs/data/charts/funnel/funnel.md +++ b/docs/data/charts/funnel/funnel.md @@ -78,10 +78,10 @@ It accepts a number that represents the radius in pixels. To understand how the border radius interacts with the `curve` prop, see 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. +The `borderRadius` property will also behave differently depending on whether the `gap` property is greater than 0. - If the `gap` is 0, the border radius will be applied to the corners of the sections that are not connected to another section. -- If the `gap` is bigger than 0, the border radius will be applied to all the corners of the sections. +- If the `gap` is greater than 0, the border radius will be applied to all the corners of the sections. {{"demo": "FunnelBorderRadius.js"}} diff --git a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts index 0217feb7e51ad..ac42441538b6e 100644 --- a/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts +++ b/packages/x-charts-pro/src/FunnelChart/curves/borderRadiusPolygon.ts @@ -11,7 +11,7 @@ const lerp2D = (p1: Point, p2: Point, t: number) => ({ * 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 + * @param {number} radius how much to round the corners */ export function borderRadiusPolygon( ctx: CanvasRenderingContext2D,