-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: increase the fillet configuration of funnel charts #7080
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v5
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -238,3 +238,252 @@ export function getOrigin(points: (Vector2 | Vector3)[]) { | |||||||||||||||||||||||||
| const [[x0, y0, z0 = 0], [x2, y2, z2 = 0]] = points; | ||||||||||||||||||||||||||
| return [(x0 + x2) / 2, (y0 + y2) / 2, (z0 + z2) / 2]; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 表示一条边 | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| interface Edge { | ||||||||||||||||||||||||||
| start: Vector2; | ||||||||||||||||||||||||||
| end: Vector2; | ||||||||||||||||||||||||||
| direction: Vector2; // 单位方向向量 | ||||||||||||||||||||||||||
| length: number; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 根据坐标自动识别四边形的顶点位置,无需考虑坐标的顺序 | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export function identifyVertices(points: Vector2[]) { | ||||||||||||||||||||||||||
| const xs = points.map((p) => p[0]); | ||||||||||||||||||||||||||
| const ys = points.map((p) => p[1]); | ||||||||||||||||||||||||||
| const minX = Math.min(...xs); | ||||||||||||||||||||||||||
| const maxX = Math.max(...xs); | ||||||||||||||||||||||||||
| const minY = Math.min(...ys); | ||||||||||||||||||||||||||
| const maxY = Math.max(...ys); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const identifiedPoints = points.map((point, index) => { | ||||||||||||||||||||||||||
| const [x, y] = point; | ||||||||||||||||||||||||||
| const distToTopLeft = Math.sqrt((x - minX) ** 2 + (y - minY) ** 2); | ||||||||||||||||||||||||||
| const distToTopRight = Math.sqrt((x - maxX) ** 2 + (y - minY) ** 2); | ||||||||||||||||||||||||||
| const distToBottomRight = Math.sqrt((x - maxX) ** 2 + (y - maxY) ** 2); | ||||||||||||||||||||||||||
| const distToBottomLeft = Math.sqrt((x - minX) ** 2 + (y - maxY) ** 2); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const distances = { | ||||||||||||||||||||||||||
| topLeft: distToTopLeft, | ||||||||||||||||||||||||||
| topRight: distToTopRight, | ||||||||||||||||||||||||||
| bottomRight: distToBottomRight, | ||||||||||||||||||||||||||
| bottomLeft: distToBottomLeft, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const closestCorner = Object.keys(distances).reduce((a, b) => | ||||||||||||||||||||||||||
| distances[a as keyof typeof distances] < | ||||||||||||||||||||||||||
| distances[b as keyof typeof distances] | ||||||||||||||||||||||||||
| ? a | ||||||||||||||||||||||||||
| : b, | ||||||||||||||||||||||||||
| ) as keyof typeof distances; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| point, | ||||||||||||||||||||||||||
| originalIndex: index, | ||||||||||||||||||||||||||
| position: closestCorner, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const sortedPoints = { | ||||||||||||||||||||||||||
| topLeft: | ||||||||||||||||||||||||||
| identifiedPoints.find((p) => p.position === 'topLeft')?.point || | ||||||||||||||||||||||||||
| points[0], | ||||||||||||||||||||||||||
| topRight: | ||||||||||||||||||||||||||
| identifiedPoints.find((p) => p.position === 'topRight')?.point || | ||||||||||||||||||||||||||
| points[1], | ||||||||||||||||||||||||||
| bottomRight: | ||||||||||||||||||||||||||
| identifiedPoints.find((p) => p.position === 'bottomRight')?.point || | ||||||||||||||||||||||||||
| points[2], | ||||||||||||||||||||||||||
| bottomLeft: | ||||||||||||||||||||||||||
| identifiedPoints.find((p) => p.position === 'bottomLeft')?.point || | ||||||||||||||||||||||||||
| points[3], | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| topLeft: sortedPoints.topLeft, | ||||||||||||||||||||||||||
| topRight: sortedPoints.topRight, | ||||||||||||||||||||||||||
| bottomRight: sortedPoints.bottomRight, | ||||||||||||||||||||||||||
| bottomLeft: sortedPoints.bottomLeft, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 创建边对象 | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| function createEdge(start: Vector2, end: Vector2): Edge { | ||||||||||||||||||||||||||
| const dx = end[0] - start[0]; | ||||||||||||||||||||||||||
| const dy = end[1] - start[1]; | ||||||||||||||||||||||||||
| const length = Math.sqrt(dx * dx + dy * dy); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| start, | ||||||||||||||||||||||||||
| end, | ||||||||||||||||||||||||||
| direction: length > 0 ? [dx / length, dy / length] : [0, 0], | ||||||||||||||||||||||||||
| length, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 计算边上的圆角信息 | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| function calculateEdgeCorner( | ||||||||||||||||||||||||||
| edge: Edge, | ||||||||||||||||||||||||||
| radius: number, | ||||||||||||||||||||||||||
| isStartCorner: boolean, // true表示在边的起点,false表示在边的终点 | ||||||||||||||||||||||||||
| ): { | ||||||||||||||||||||||||||
| cornerPoint: Vector2; // 圆角在边上的位置 | ||||||||||||||||||||||||||
| hasRadius: boolean; | ||||||||||||||||||||||||||
| actualRadius: number; | ||||||||||||||||||||||||||
| } { | ||||||||||||||||||||||||||
| if (radius <= 0) { | ||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| cornerPoint: isStartCorner ? edge.start : edge.end, | ||||||||||||||||||||||||||
| hasRadius: false, | ||||||||||||||||||||||||||
| actualRadius: 0, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 限制圆角半径不超过边长的一半 | ||||||||||||||||||||||||||
| const maxRadius = edge.length / 2; | ||||||||||||||||||||||||||
| const actualRadius = Math.min(radius, maxRadius); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (actualRadius <= 0) { | ||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| cornerPoint: isStartCorner ? edge.start : edge.end, | ||||||||||||||||||||||||||
| hasRadius: false, | ||||||||||||||||||||||||||
| actualRadius: 0, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 计算圆角在边上的位置 | ||||||||||||||||||||||||||
| let cornerPoint: Vector2; | ||||||||||||||||||||||||||
| if (isStartCorner) { | ||||||||||||||||||||||||||
| // 从起点沿边方向移动radius距离 | ||||||||||||||||||||||||||
| cornerPoint = [ | ||||||||||||||||||||||||||
| edge.start[0] + edge.direction[0] * actualRadius, | ||||||||||||||||||||||||||
| edge.start[1] + edge.direction[1] * actualRadius, | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| // 从终点沿边反方向移动radius距离 | ||||||||||||||||||||||||||
| cornerPoint = [ | ||||||||||||||||||||||||||
| edge.end[0] - edge.direction[0] * actualRadius, | ||||||||||||||||||||||||||
| edge.end[1] - edge.direction[1] * actualRadius, | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| cornerPoint, | ||||||||||||||||||||||||||
| hasRadius: true, | ||||||||||||||||||||||||||
| actualRadius, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * 生成基于边的圆角路径 | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| export function createEdgeBasedRoundedPath( | ||||||||||||||||||||||||||
| points: Vector2[], | ||||||||||||||||||||||||||
| borderRadius: | ||||||||||||||||||||||||||
| | number | ||||||||||||||||||||||||||
| | { | ||||||||||||||||||||||||||
| topLeft?: number; | ||||||||||||||||||||||||||
| topRight?: number; | ||||||||||||||||||||||||||
| bottomLeft?: number; | ||||||||||||||||||||||||||
| bottomRight?: number; | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| ): string { | ||||||||||||||||||||||||||
| // 1. 识别顶点位置 | ||||||||||||||||||||||||||
| const vertices = identifyVertices(points); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 2. 获取圆角配置 | ||||||||||||||||||||||||||
| const getRadius = (corner: string) => { | ||||||||||||||||||||||||||
| if (typeof borderRadius === 'number') { | ||||||||||||||||||||||||||
| return borderRadius; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| return (borderRadius as any)?.[corner] || 0; | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
Comment on lines
+402
to
+410
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const radii = { | ||||||||||||||||||||||||||
| topLeft: getRadius('topLeft'), | ||||||||||||||||||||||||||
| topRight: getRadius('topRight'), | ||||||||||||||||||||||||||
| bottomRight: getRadius('bottomRight'), | ||||||||||||||||||||||||||
| bottomLeft: getRadius('bottomLeft'), | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 3. 如果所有圆角都为0,返回简单路径 | ||||||||||||||||||||||||||
| if (Object.values(radii).every((r) => r === 0)) { | ||||||||||||||||||||||||||
| const { topLeft, topRight, bottomRight, bottomLeft } = vertices; | ||||||||||||||||||||||||||
| return `M ${topLeft[0]} ${topLeft[1]} L ${topRight[0]} ${topRight[1]} L ${bottomRight[0]} ${bottomRight[1]} L ${bottomLeft[0]} ${bottomLeft[1]} Z`; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 4. 创建四条边 | ||||||||||||||||||||||||||
| const edges = [ | ||||||||||||||||||||||||||
| createEdge(vertices.topLeft, vertices.topRight), // 上边 | ||||||||||||||||||||||||||
| createEdge(vertices.topRight, vertices.bottomRight), // 右边 | ||||||||||||||||||||||||||
| createEdge(vertices.bottomRight, vertices.bottomLeft), // 下边 | ||||||||||||||||||||||||||
| createEdge(vertices.bottomLeft, vertices.topLeft), // 左边 | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const edgeNames = ['top', 'right', 'bottom', 'left']; | ||||||||||||||||||||||||||
| const cornerNames = ['topLeft', 'topRight', 'bottomRight', 'bottomLeft']; | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The type casts
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 5. 计算每条边上的圆角点 | ||||||||||||||||||||||||||
| const edgeCorners = edges.map((edge, edgeIndex) => { | ||||||||||||||||||||||||||
| const startCornerName = cornerNames[edgeIndex]; // 边起点对应的角 | ||||||||||||||||||||||||||
| const endCornerName = cornerNames[(edgeIndex + 1) % 4]; // 边终点对应的角 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const startRadius = radii[startCornerName as keyof typeof radii]; | ||||||||||||||||||||||||||
| const endRadius = radii[endCornerName as keyof typeof radii]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| edge, | ||||||||||||||||||||||||||
| edgeName: edgeNames[edgeIndex], | ||||||||||||||||||||||||||
| startCorner: calculateEdgeCorner(edge, startRadius, true), // 边起点的圆角 | ||||||||||||||||||||||||||
| endCorner: calculateEdgeCorner(edge, endRadius, false), // 边终点的圆角 | ||||||||||||||||||||||||||
| startCornerName, | ||||||||||||||||||||||||||
| endCornerName, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 6. 生成SVG路径 | ||||||||||||||||||||||||||
| const pathCommands: string[] = []; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 从第一条边的起点圆角开始 | ||||||||||||||||||||||||||
| const firstEdge = edgeCorners[0]; | ||||||||||||||||||||||||||
| pathCommands.push( | ||||||||||||||||||||||||||
| `M ${firstEdge.startCorner.cornerPoint[0]} ${firstEdge.startCorner.cornerPoint[1]}`, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for (let i = 0; i < 4; i++) { | ||||||||||||||||||||||||||
| const currentEdge = edgeCorners[i]; | ||||||||||||||||||||||||||
| const nextEdge = edgeCorners[(i + 1) % 4]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 沿着当前边绘制到终点圆角 | ||||||||||||||||||||||||||
| pathCommands.push( | ||||||||||||||||||||||||||
| `L ${currentEdge.endCorner.cornerPoint[0]} ${currentEdge.endCorner.cornerPoint[1]}`, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 在角点处绘制圆角弧线 | ||||||||||||||||||||||||||
| const cornerVertex = edges[i].end; // 当前边的终点就是角点 | ||||||||||||||||||||||||||
| const hasCornerRadius = currentEdge.endCorner.hasRadius; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (hasCornerRadius) { | ||||||||||||||||||||||||||
| // 绘制圆角弧线:从当前边的终点圆角到下一条边的起点圆角 | ||||||||||||||||||||||||||
| const controlPoint = cornerVertex; // 使用角点作为控制点 | ||||||||||||||||||||||||||
| pathCommands.push( | ||||||||||||||||||||||||||
| `Q ${controlPoint[0]} ${controlPoint[1]} ${nextEdge.startCorner.cornerPoint[0]} ${nextEdge.startCorner.cornerPoint[1]}`, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| // 没有圆角,直接连到下一条边的起点 | ||||||||||||||||||||||||||
| pathCommands.push( | ||||||||||||||||||||||||||
| `L ${nextEdge.startCorner.cornerPoint[0]} ${nextEdge.startCorner.cornerPoint[1]}`, | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| pathCommands.push('Z'); | ||||||||||||||||||||||||||
| const finalPath = pathCommands.join(' '); | ||||||||||||||||||||||||||
| return finalPath; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+242
to
+497
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new functions and interfaces in this file have comments written in Chinese. For consistency with the rest of the codebase, which is in English, and to improve maintainability for all contributors, please translate these comments to English. Examples:
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback logic for
sortedPointsis unsafe and can lead to bugs. It might assign the same point to multiple corners, which would result in a malformed or degenerate shape. For example, if no point is identified astopLeft, the logic falls back to usingpoints[0]. However,points[0]might have already been identified and assigned to another corner (e.g.,topRight), causing two corners of the shape to share the same coordinate.