@antv/infographic 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/infographic.min.js +98 -94
- package/dist/infographic.min.js.map +1 -1
- package/esm/designs/structures/chart-pie.d.ts +25 -0
- package/esm/designs/structures/chart-pie.js +182 -23
- package/esm/designs/utils/index.d.ts +1 -0
- package/esm/designs/utils/index.js +1 -0
- package/esm/designs/utils/normalize-percent.d.ts +19 -0
- package/esm/designs/utils/normalize-percent.js +32 -0
- package/esm/editor/interactions/zoom-wheel.d.ts +3 -0
- package/esm/editor/interactions/zoom-wheel.js +46 -23
- package/esm/editor/managers/state.js +8 -2
- package/esm/templates/built-in.js +3 -77
- package/esm/templates/chart-pie.d.ts +2 -0
- package/esm/templates/chart-pie.js +87 -0
- package/esm/utils/viewbox.d.ts +20 -0
- package/esm/utils/viewbox.js +10 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/lib/designs/structures/chart-pie.d.ts +25 -0
- package/lib/designs/structures/chart-pie.js +182 -22
- package/lib/designs/utils/index.d.ts +1 -0
- package/lib/designs/utils/index.js +1 -0
- package/lib/designs/utils/normalize-percent.d.ts +19 -0
- package/lib/designs/utils/normalize-percent.js +35 -0
- package/lib/editor/interactions/zoom-wheel.d.ts +3 -0
- package/lib/editor/interactions/zoom-wheel.js +45 -22
- package/lib/editor/managers/state.js +8 -2
- package/lib/templates/built-in.js +3 -77
- package/lib/templates/chart-pie.d.ts +2 -0
- package/lib/templates/chart-pie.js +90 -0
- package/lib/utils/viewbox.d.ts +20 -0
- package/lib/utils/viewbox.js +12 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -2
- package/src/designs/structures/chart-pie.tsx +259 -26
- package/src/designs/utils/index.ts +1 -0
- package/src/designs/utils/normalize-percent.ts +33 -0
- package/src/editor/interactions/zoom-wheel.ts +64 -22
- package/src/editor/managers/state.ts +10 -5
- package/src/templates/built-in.ts +2 -81
- package/src/templates/chart-pie.ts +89 -0
- package/src/utils/viewbox.ts +23 -0
- package/src/version.ts +1 -1
package/esm/utils/viewbox.d.ts
CHANGED
|
@@ -4,3 +4,23 @@ export declare function getViewBox(svg: SVGSVGElement): {
|
|
|
4
4
|
width: number;
|
|
5
5
|
height: number;
|
|
6
6
|
};
|
|
7
|
+
export declare function calculateZoomedViewBox(current: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
}, factor: number, pivot: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
}): {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
};
|
|
21
|
+
export declare function viewBoxToString(box: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}): string;
|
package/esm/utils/viewbox.js
CHANGED
|
@@ -10,3 +10,13 @@ export function getViewBox(svg) {
|
|
|
10
10
|
const height = Number(heightStr) || 0;
|
|
11
11
|
return { x: 0, y: 0, width, height };
|
|
12
12
|
}
|
|
13
|
+
export function calculateZoomedViewBox(current, factor, pivot) {
|
|
14
|
+
const newWidth = current.width * factor;
|
|
15
|
+
const newHeight = current.height * factor;
|
|
16
|
+
const newX = pivot.x - (pivot.x - current.x) * factor;
|
|
17
|
+
const newY = pivot.y - (pivot.y - current.y) * factor;
|
|
18
|
+
return { x: newX, y: newY, width: newWidth, height: newHeight };
|
|
19
|
+
}
|
|
20
|
+
export function viewBoxToString(box) {
|
|
21
|
+
return `${box.x} ${box.y} ${box.width} ${box.height}`;
|
|
22
|
+
}
|
package/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.13";
|
package/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.13';
|
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
import { type PieArcDatum } from 'd3';
|
|
1
2
|
import type { ComponentType } from '../../jsx';
|
|
3
|
+
import { ItemDatum } from '../../types';
|
|
2
4
|
import type { BaseStructureProps } from './types';
|
|
3
5
|
export interface ChartPieProps extends BaseStructureProps {
|
|
4
6
|
radius?: number;
|
|
5
7
|
innerRadius?: number;
|
|
6
8
|
padding?: number;
|
|
7
9
|
showPercentage?: boolean;
|
|
10
|
+
avoidLabelOverlap?: boolean;
|
|
11
|
+
minShowLabelPercent?: number | string;
|
|
8
12
|
}
|
|
9
13
|
export declare const ChartPie: ComponentType<ChartPieProps>;
|
|
14
|
+
export interface LabelItem {
|
|
15
|
+
arcDatum: PieArcDatum<ItemDatum>;
|
|
16
|
+
originalIndex: number;
|
|
17
|
+
/** 标签中心点 Y 坐标 */
|
|
18
|
+
y: number;
|
|
19
|
+
x: number;
|
|
20
|
+
height: number;
|
|
21
|
+
isRight: boolean;
|
|
22
|
+
color: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 核心避障逻辑:蜘蛛腿算法 (Spider Leg Layout)
|
|
26
|
+
*
|
|
27
|
+
* 注意:y 坐标表示标签的中心点坐标
|
|
28
|
+
*
|
|
29
|
+
* @param items 待处理的标签数组(y 为中心点坐标)
|
|
30
|
+
* @param spacing 垂直最小间距(标签边缘之间的间距)
|
|
31
|
+
* @param minY 标签中心点的上边界
|
|
32
|
+
* @param maxY 标签中心点的下边界
|
|
33
|
+
*/
|
|
34
|
+
export declare function distributeLabels(items: LabelItem[], spacing: number, minY: number, maxY: number): LabelItem[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ChartPie = void 0;
|
|
4
|
+
exports.distributeLabels = distributeLabels;
|
|
4
5
|
const jsx_runtime_1 = require("@antv/infographic/jsx-runtime");
|
|
5
6
|
const d3_1 = require("d3");
|
|
6
7
|
const jsx_1 = require("../../jsx");
|
|
@@ -8,9 +9,34 @@ const components_1 = require("../components");
|
|
|
8
9
|
const layouts_1 = require("../layouts");
|
|
9
10
|
const utils_1 = require("../utils");
|
|
10
11
|
const registry_1 = require("./registry");
|
|
12
|
+
// === 连线布局常量 ===
|
|
13
|
+
/** 连线水平拉伸系数:控制拐点相对于外半径的延伸比例 */
|
|
14
|
+
const EXTENSION_FACTOR = 1.35;
|
|
15
|
+
/** 文本与连线终点之间的水平间距 */
|
|
16
|
+
const TEXT_GAP = 8;
|
|
17
|
+
/** 平滑系数:控制 Y 轴偏移对 X 轴补偿的影响幅度 */
|
|
18
|
+
const SMOOTH_FACTOR = 0.3;
|
|
19
|
+
/** 最大预期偏移系数:相对于外半径的比例 */
|
|
20
|
+
const MAX_EXPECTED_SHIFT_FACTOR = 0.2;
|
|
21
|
+
/** 文本锚点与拐点之间的固定间距 */
|
|
22
|
+
const FIXED_TEXT_RADIUS_GAP = 20;
|
|
23
|
+
/** 连线拐点半径系数:相对于外半径的比例 */
|
|
24
|
+
const ELBOW_RADIUS_FACTOR = 1.15;
|
|
25
|
+
/** 百分比文本位置系数:从内半径到外半径的比例 (0.5 = 中间) */
|
|
26
|
+
const PERCENT_TEXT_POSITION = 0.5;
|
|
27
|
+
/** 删除按钮半径系数:相对于外半径的比例 */
|
|
28
|
+
const DELETE_BUTTON_RADIUS_FACTOR = 0.85;
|
|
29
|
+
/** 添加按钮半径系数:相对于外半径的比例 */
|
|
30
|
+
const ADD_BUTTON_RADIUS_FACTOR = 1.0;
|
|
31
|
+
/** 连线透明度 */
|
|
32
|
+
const CONNECTOR_STROKE_OPACITY = 0.45;
|
|
33
|
+
/** 连线宽度 */
|
|
34
|
+
const CONNECTOR_STROKE_WIDTH = 2;
|
|
11
35
|
const ChartPie = (props) => {
|
|
12
36
|
var _a;
|
|
13
|
-
const { Title, Item, data, radius = 140, innerRadius = 0, padding = 30, showPercentage = true, options, } = props;
|
|
37
|
+
const { Title, Item, data, radius = 140, innerRadius = 0, padding = 30, showPercentage = true, avoidLabelOverlap = false, minShowLabelPercent: rawMinShowLabelPercent = 0, options, } = props;
|
|
38
|
+
// 规范化百分比阈值
|
|
39
|
+
const minShowLabelPercent = (0, utils_1.normalizePercent)(rawMinShowLabelPercent);
|
|
14
40
|
const { title, desc, items = [] } = data;
|
|
15
41
|
const titleContent = Title ? (0, jsx_runtime_1.jsx)(Title, { title: title, desc: desc }) : null;
|
|
16
42
|
const btnBounds = (0, jsx_1.getElementBounds)((0, jsx_runtime_1.jsx)(components_1.BtnAdd, { indexes: [0] }));
|
|
@@ -21,12 +47,9 @@ const ChartPie = (props) => {
|
|
|
21
47
|
const labelHeight = itemBounds.height || 32;
|
|
22
48
|
// 基础半径设置
|
|
23
49
|
const outerRadius = Math.max(radius, 60);
|
|
24
|
-
// 连线水平拉伸的系数
|
|
25
|
-
const extensionFactor = 1.35;
|
|
26
|
-
const textGap = 8;
|
|
27
50
|
// 计算画布中心和总尺寸
|
|
28
51
|
// 水平方向:半径 * 系数 + 间距 + 标签宽度 + 边缘padding
|
|
29
|
-
const maxHorizontalDistance = outerRadius *
|
|
52
|
+
const maxHorizontalDistance = outerRadius * EXTENSION_FACTOR + TEXT_GAP + labelWidth;
|
|
30
53
|
const maxVerticalDistance = outerRadius;
|
|
31
54
|
const centerX = padding + maxHorizontalDistance;
|
|
32
55
|
const centerY = padding + maxVerticalDistance;
|
|
@@ -57,24 +80,24 @@ const ChartPie = (props) => {
|
|
|
57
80
|
.outerRadius(outerRadius);
|
|
58
81
|
// 连线拐点
|
|
59
82
|
const outerArc = (0, d3_1.arc)()
|
|
60
|
-
.innerRadius(outerRadius *
|
|
61
|
-
.outerRadius(outerRadius *
|
|
62
|
-
const percentTextRadius = innerRadius + (outerRadius - innerRadius) *
|
|
83
|
+
.innerRadius(outerRadius * ELBOW_RADIUS_FACTOR)
|
|
84
|
+
.outerRadius(outerRadius * ELBOW_RADIUS_FACTOR);
|
|
85
|
+
const percentTextRadius = innerRadius + (outerRadius - innerRadius) * PERCENT_TEXT_POSITION;
|
|
63
86
|
const percentageArc = (0, d3_1.arc)()
|
|
64
87
|
.innerRadius(percentTextRadius)
|
|
65
88
|
.outerRadius(percentTextRadius);
|
|
66
89
|
// 删除按钮位置
|
|
67
90
|
const deleteButtonArc = (0, d3_1.arc)()
|
|
68
|
-
.innerRadius(outerRadius *
|
|
69
|
-
.outerRadius(outerRadius *
|
|
91
|
+
.innerRadius(outerRadius * DELETE_BUTTON_RADIUS_FACTOR)
|
|
92
|
+
.outerRadius(outerRadius * DELETE_BUTTON_RADIUS_FACTOR);
|
|
70
93
|
const sliceElements = [];
|
|
71
94
|
const percentElements = [];
|
|
72
95
|
const connectorElements = [];
|
|
73
96
|
const itemElements = [];
|
|
74
97
|
const btnElements = [];
|
|
98
|
+
const labelItems = [];
|
|
75
99
|
// 3. 遍历生成图形
|
|
76
100
|
arcData.forEach((arcDatum) => {
|
|
77
|
-
const currentItem = arcDatum.data;
|
|
78
101
|
const originalIndex = arcDatum.index;
|
|
79
102
|
const color = (0, utils_1.getPaletteColor)(options, [originalIndex]) ||
|
|
80
103
|
themeColors.colorPrimary ||
|
|
@@ -84,20 +107,77 @@ const ChartPie = (props) => {
|
|
|
84
107
|
sliceElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: pathD, fill: color, stroke: themeColors.colorBg, strokeWidth: 1, "data-element-type": "shape", width: outerRadius * 2, height: outerRadius * 2 }));
|
|
85
108
|
// --- 计算关键点 ---
|
|
86
109
|
const midAngle = arcDatum.startAngle + (arcDatum.endAngle - arcDatum.startAngle) / 2;
|
|
87
|
-
const
|
|
88
|
-
|
|
110
|
+
const normalizedAngle = midAngle < 0 ? midAngle + Math.PI * 2 : midAngle;
|
|
111
|
+
const isRight = normalizedAngle < Math.PI;
|
|
112
|
+
// 计算扇形占比,如果小于 minShowLabelPercent 则跳过生成连线和标签
|
|
113
|
+
const slicePercent = totalValue > 0 ? (arcDatum.value / totalValue) * 100 : 0;
|
|
114
|
+
if (slicePercent < minShowLabelPercent) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const centroid = outerArc.centroid(arcDatum);
|
|
118
|
+
labelItems.push({
|
|
119
|
+
arcDatum,
|
|
120
|
+
originalIndex,
|
|
121
|
+
x: centroid[0],
|
|
122
|
+
y: centroid[1], // 中心点 Y 坐标
|
|
123
|
+
height: labelHeight,
|
|
124
|
+
isRight,
|
|
125
|
+
color,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
let finalLabels = labelItems;
|
|
129
|
+
if (avoidLabelOverlap) {
|
|
130
|
+
// 标签中心点的上下边界(中心点不能超出此范围)
|
|
131
|
+
const labelMinY = -maxVerticalDistance * EXTENSION_FACTOR;
|
|
132
|
+
const labelMaxY = maxVerticalDistance * EXTENSION_FACTOR;
|
|
133
|
+
const leftItems = labelItems.filter((item) => !item.isRight);
|
|
134
|
+
const rightItems = labelItems.filter((item) => item.isRight);
|
|
135
|
+
const labelSpacing = labelHeight;
|
|
136
|
+
const adjustedRight = distributeLabels(rightItems, labelSpacing, labelMinY, labelMaxY);
|
|
137
|
+
const adjustedLeft = distributeLabels(leftItems, labelSpacing, labelMinY, labelMaxY);
|
|
138
|
+
finalLabels = [...adjustedLeft, ...adjustedRight];
|
|
139
|
+
}
|
|
140
|
+
finalLabels.forEach((item) => {
|
|
141
|
+
const { arcDatum, originalIndex, isRight, color, y: adjustedY } = item;
|
|
142
|
+
// 1. P0: 连线起点 (内部扇形质心)
|
|
89
143
|
const p0 = innerArc.centroid(arcDatum);
|
|
90
|
-
// 2.
|
|
144
|
+
// 2. P1: 第一拐点 (外部扇形质心)
|
|
91
145
|
const p1 = outerArc.centroid(arcDatum);
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const
|
|
146
|
+
// 计算因避障算法导致的 Y 轴偏移量
|
|
147
|
+
// adjustedY 已经是中心点坐标
|
|
148
|
+
const labelCenterY = adjustedY;
|
|
149
|
+
const deltaY = Math.abs(labelCenterY - p1[1]);
|
|
150
|
+
// --- 动态补偿策略 (Dynamic Compensation) ---
|
|
151
|
+
// 根据 Y 轴的偏移量动态向外推移 X 轴,以缓解连线折角过于陡峭的问题
|
|
152
|
+
const dynamicShift = deltaY * SMOOTH_FACTOR;
|
|
153
|
+
// 计算基础拐点半径
|
|
154
|
+
const baseElbowRadius = outerRadius * EXTENSION_FACTOR;
|
|
155
|
+
// 计算实际拐点半径 (基础半径 + 动态补偿)
|
|
156
|
+
const currentElbowRadius = baseElbowRadius + dynamicShift;
|
|
157
|
+
// 设定文本锚点的固定半径,确保所有标签在垂直方向上对齐
|
|
158
|
+
const maxExpectedShift = outerRadius * MAX_EXPECTED_SHIFT_FACTOR;
|
|
159
|
+
const fixedTextRadius = baseElbowRadius + maxExpectedShift + FIXED_TEXT_RADIUS_GAP;
|
|
160
|
+
// 计算 P2 X 坐标 (注意方向性)
|
|
161
|
+
// 限制 Elbow X 不超过文本锚点半径,防止连线出现回折
|
|
162
|
+
const elbowRadiusClamped = Math.min(currentElbowRadius, fixedTextRadius);
|
|
163
|
+
const elbowX = elbowRadiusClamped * (isRight ? 1 : -1);
|
|
164
|
+
// 计算文本锚点 X 坐标 (始终对齐)
|
|
165
|
+
const textX = fixedTextRadius * (isRight ? 1 : -1);
|
|
166
|
+
// 3. P2: 第二拐点 (动态调整后的 Elbow 位置)
|
|
167
|
+
// 连线终点对齐标签垂直中心
|
|
168
|
+
const p2 = [elbowX, labelCenterY];
|
|
169
|
+
// 4. P3: 终点 (文本锚点)
|
|
170
|
+
const p3 = [textX, labelCenterY];
|
|
95
171
|
// --- 绘制连线 ---
|
|
96
|
-
connectorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M${centerX + p0[0]} ${centerY + p0[1]}
|
|
172
|
+
connectorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M${centerX + p0[0]} ${centerY + p0[1]}
|
|
173
|
+
L${centerX + p1[0]} ${centerY + p1[1]}
|
|
174
|
+
L${centerX + p2[0]} ${centerY + p2[1]}
|
|
175
|
+
L${centerX + p3[0]} ${centerY + p3[1]}
|
|
176
|
+
`, stroke: color, strokeOpacity: CONNECTOR_STROKE_OPACITY, strokeWidth: CONNECTOR_STROKE_WIDTH, fill: "none", "data-element-type": "shape" }));
|
|
97
177
|
// --- 绘制 Item ---
|
|
98
|
-
const itemX = centerX +
|
|
99
|
-
const itemY = centerY +
|
|
100
|
-
itemElements.push((0, jsx_runtime_1.jsx)(Item, { indexes: [originalIndex], datum:
|
|
178
|
+
const itemX = centerX + p3[0] + (isRight ? TEXT_GAP : -TEXT_GAP - labelWidth);
|
|
179
|
+
const itemY = centerY + adjustedY - labelHeight / 2; // 转换为顶部坐标用于渲染
|
|
180
|
+
itemElements.push((0, jsx_runtime_1.jsx)(Item, { indexes: [originalIndex], datum: arcDatum.data, data: data, x: itemX, y: itemY, width: labelWidth, height: labelHeight, positionH: isRight ? 'normal' : 'flipped', positionV: "middle", themeColors: (0, utils_1.getThemeColors)({ colorPrimary: color }, options) }));
|
|
101
181
|
// --- 绘制百分比 ---
|
|
102
182
|
if (showPercentage && totalValue > 0) {
|
|
103
183
|
const percentPos = percentageArc.centroid(arcDatum);
|
|
@@ -118,7 +198,7 @@ const ChartPie = (props) => {
|
|
|
118
198
|
const currentEnd = arcDatum.endAngle;
|
|
119
199
|
const nextStart = arcData[nextIndex].startAngle + (nextIndex === 0 ? Math.PI * 2 : 0);
|
|
120
200
|
const midAngle = (currentEnd + nextStart) / 2;
|
|
121
|
-
const btnR = outerRadius *
|
|
201
|
+
const btnR = outerRadius * ADD_BUTTON_RADIUS_FACTOR;
|
|
122
202
|
const btnX = Math.sin(midAngle) * btnR;
|
|
123
203
|
const btnY = -Math.cos(midAngle) * btnR;
|
|
124
204
|
btnElements.push((0, jsx_runtime_1.jsx)(components_1.BtnAdd, { indexes: [index + 1], x: centerX + btnX - btnBounds.width / 2, y: centerY + btnY - btnBounds.height / 2 }));
|
|
@@ -130,3 +210,83 @@ exports.ChartPie = ChartPie;
|
|
|
130
210
|
component: exports.ChartPie,
|
|
131
211
|
composites: ['title', 'item'],
|
|
132
212
|
});
|
|
213
|
+
/**
|
|
214
|
+
* 核心避障逻辑:蜘蛛腿算法 (Spider Leg Layout)
|
|
215
|
+
*
|
|
216
|
+
* 注意:y 坐标表示标签的中心点坐标
|
|
217
|
+
*
|
|
218
|
+
* @param items 待处理的标签数组(y 为中心点坐标)
|
|
219
|
+
* @param spacing 垂直最小间距(标签边缘之间的间距)
|
|
220
|
+
* @param minY 标签中心点的上边界
|
|
221
|
+
* @param maxY 标签中心点的下边界
|
|
222
|
+
*/
|
|
223
|
+
function distributeLabels(items, spacing, minY, maxY) {
|
|
224
|
+
// 避免除零风险
|
|
225
|
+
if (items.length <= 1)
|
|
226
|
+
return items.map((item) => (Object.assign({}, item)));
|
|
227
|
+
// 按照 Y 坐标排序 (从上到下)
|
|
228
|
+
const sorted = items.map((item) => (Object.assign({}, item))).sort((a, b) => a.y - b.y);
|
|
229
|
+
// === 预检测:是否需要退避 ===
|
|
230
|
+
// 检查是否有任何标签重叠或超出边界
|
|
231
|
+
const hasOverlap = sorted.some((item, i) => {
|
|
232
|
+
if (i === 0)
|
|
233
|
+
return false;
|
|
234
|
+
const prev = sorted[i - 1];
|
|
235
|
+
// 中心点间距 < 两个半高度之和 → 有重叠
|
|
236
|
+
return item.y - prev.y < (prev.height + item.height) / 2;
|
|
237
|
+
});
|
|
238
|
+
const firstItem = sorted[0];
|
|
239
|
+
const lastItem = sorted[sorted.length - 1];
|
|
240
|
+
const isOutOfBounds = firstItem.y - firstItem.height / 2 < minY ||
|
|
241
|
+
lastItem.y + lastItem.height / 2 > maxY;
|
|
242
|
+
// 如果没有重叠且都在边界内,直接返回原位置
|
|
243
|
+
if (!hasOverlap && !isOutOfBounds) {
|
|
244
|
+
return sorted;
|
|
245
|
+
}
|
|
246
|
+
// === 第一步:计算总高度需求,动态调整间距 ===
|
|
247
|
+
const totalLabelsHeight = sorted.reduce((sum, item) => sum + item.height, 0);
|
|
248
|
+
const availableSpace = maxY - minY;
|
|
249
|
+
const requiredSpaceWithIdealSpacing = totalLabelsHeight + spacing * (sorted.length - 1);
|
|
250
|
+
// 如果理想间距放不下,动态压缩间距(最小为0)
|
|
251
|
+
let actualSpacing = spacing;
|
|
252
|
+
if (requiredSpaceWithIdealSpacing > availableSpace) {
|
|
253
|
+
const excessSpace = availableSpace - totalLabelsHeight;
|
|
254
|
+
actualSpacing = Math.max(0, excessSpace / (sorted.length - 1));
|
|
255
|
+
}
|
|
256
|
+
// === 第二步:向下挤压 (Downwards push) ===
|
|
257
|
+
// y 为中心点坐标
|
|
258
|
+
// 当前标签中心 必须 >= 前一标签中心 + 两个半高度之和 + 间距
|
|
259
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
260
|
+
const prev = sorted[i - 1];
|
|
261
|
+
const curr = sorted[i];
|
|
262
|
+
const minAllowedY = prev.y + (prev.height + curr.height) / 2 + actualSpacing;
|
|
263
|
+
if (curr.y < minAllowedY) {
|
|
264
|
+
curr.y = minAllowedY;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// === 第三步:边界钳制 + 向上回推 (Upwards push) ===
|
|
268
|
+
// 如果最后一个标签超出下边界,从下往上回推
|
|
269
|
+
const lastIdx = sorted.length - 1;
|
|
270
|
+
const last = sorted[lastIdx];
|
|
271
|
+
if (last.y + last.height / 2 > maxY) {
|
|
272
|
+
// 先把最后一个钳制到边界内(中心点 = 下边界 - 半高度)
|
|
273
|
+
last.y = maxY - last.height / 2;
|
|
274
|
+
// 然后从下往上检查,如果上一个标签被挤到了,就往上推
|
|
275
|
+
for (let i = lastIdx - 1; i >= 0; i--) {
|
|
276
|
+
const next = sorted[i + 1];
|
|
277
|
+
const curr = sorted[i];
|
|
278
|
+
const maxAllowedY = next.y - (next.height + curr.height) / 2 - actualSpacing;
|
|
279
|
+
if (curr.y > maxAllowedY) {
|
|
280
|
+
curr.y = maxAllowedY;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// === 第四步:上边界钳制 ===
|
|
285
|
+
// 如果向上回推后,第一个标签超出上边界,整体往下移
|
|
286
|
+
const first = sorted[0];
|
|
287
|
+
if (first.y - first.height / 2 < minY) {
|
|
288
|
+
const shift = minY - (first.y - first.height / 2);
|
|
289
|
+
sorted.forEach((item) => (item.y += shift));
|
|
290
|
+
}
|
|
291
|
+
return sorted;
|
|
292
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 规范化百分比输入
|
|
3
|
+
*
|
|
4
|
+
* 支持以下格式:
|
|
5
|
+
* - "2%" 或 "2.5%" → 2 或 2.5
|
|
6
|
+
* - 2 或 2.5 (数字直接作为百分比) → 2 或 2.5
|
|
7
|
+
*
|
|
8
|
+
* @param value - 百分比值,可以是数字或带 "%" 的字符串
|
|
9
|
+
* @returns 规范化后的百分比数值
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* normalizePercent("2%"); // 返回 2
|
|
14
|
+
* normalizePercent("2.5%"); // 返回 2.5
|
|
15
|
+
* normalizePercent(2); // 返回 2
|
|
16
|
+
* normalizePercent(2.5); // 返回 2.5
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizePercent(value: number | string | undefined): number;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizePercent = normalizePercent;
|
|
4
|
+
/**
|
|
5
|
+
* 规范化百分比输入
|
|
6
|
+
*
|
|
7
|
+
* 支持以下格式:
|
|
8
|
+
* - "2%" 或 "2.5%" → 2 或 2.5
|
|
9
|
+
* - 2 或 2.5 (数字直接作为百分比) → 2 或 2.5
|
|
10
|
+
*
|
|
11
|
+
* @param value - 百分比值,可以是数字或带 "%" 的字符串
|
|
12
|
+
* @returns 规范化后的百分比数值
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* normalizePercent("2%"); // 返回 2
|
|
17
|
+
* normalizePercent("2.5%"); // 返回 2.5
|
|
18
|
+
* normalizePercent(2); // 返回 2
|
|
19
|
+
* normalizePercent(2.5); // 返回 2.5
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function normalizePercent(value) {
|
|
23
|
+
if (value === undefined || value === null)
|
|
24
|
+
return 0;
|
|
25
|
+
// 处理字符串格式 (如 "2%" 或 "2.5%")
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
// 移除可能的 '%' 后缀,然后解析
|
|
29
|
+
const numStr = trimmed.endsWith('%') ? trimmed.slice(0, -1) : trimmed;
|
|
30
|
+
const num = parseFloat(numStr);
|
|
31
|
+
return isNaN(num) ? 0 : num;
|
|
32
|
+
}
|
|
33
|
+
// 数字直接作为百分比使用
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
@@ -3,6 +3,9 @@ import { Interaction } from './base';
|
|
|
3
3
|
export declare class ZoomWheel extends Interaction implements IInteraction {
|
|
4
4
|
name: string;
|
|
5
5
|
private wheelListener;
|
|
6
|
+
private getMousePoint;
|
|
7
|
+
private getCenterPoint;
|
|
8
|
+
private shouldZoom;
|
|
6
9
|
init(options: InteractionInitOptions): void;
|
|
7
10
|
destroy(): void;
|
|
8
11
|
}
|
|
@@ -2,43 +2,66 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ZoomWheel = void 0;
|
|
4
4
|
const lodash_es_1 = require("lodash-es");
|
|
5
|
-
const
|
|
5
|
+
const viewbox_1 = require("../../utils/viewbox");
|
|
6
6
|
const commands_1 = require("../commands");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
7
8
|
const base_1 = require("./base");
|
|
8
|
-
const MIN_VIEWBOX_SIZE =
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const MIN_VIEWBOX_SIZE = 20;
|
|
10
|
+
const MAX_VIEWBOX_SIZE = 2000;
|
|
11
|
+
const ZOOM_FACTOR = 1.1;
|
|
11
12
|
class ZoomWheel extends base_1.Interaction {
|
|
12
13
|
constructor() {
|
|
13
14
|
super(...arguments);
|
|
14
15
|
this.name = 'zoom-wheel';
|
|
15
16
|
this.wheelListener = (event) => {
|
|
16
|
-
|
|
17
|
-
if (!this.interaction.isActive())
|
|
18
|
-
return;
|
|
19
|
-
if (!event.ctrlKey && !event.metaKey)
|
|
17
|
+
if (!this.shouldZoom(event))
|
|
20
18
|
return;
|
|
21
19
|
event.preventDefault();
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const parsed = (0, utils_1.parsePadding)(currentPadding);
|
|
20
|
+
// Standard Zoom: Scroll Up (deltaY < 0) = Zoom In
|
|
21
|
+
const isZoomIn = event.deltaY < 0;
|
|
22
|
+
const factor = isZoomIn ? 1 / ZOOM_FACTOR : ZOOM_FACTOR;
|
|
26
23
|
const svg = this.editor.getDocument();
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
24
|
+
const viewBox = (0, viewbox_1.getViewBox)(svg);
|
|
25
|
+
const { width, height } = viewBox;
|
|
26
|
+
const newWidth = width * factor;
|
|
27
|
+
const newHeight = height * factor;
|
|
28
|
+
if (!(0, lodash_es_1.inRange)(newWidth, MIN_VIEWBOX_SIZE, MAX_VIEWBOX_SIZE) ||
|
|
29
|
+
!(0, lodash_es_1.inRange)(newHeight, MIN_VIEWBOX_SIZE, MAX_VIEWBOX_SIZE))
|
|
30
|
+
return;
|
|
31
|
+
// TODO: Remove after implementing the reset UI plugin
|
|
32
|
+
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
|
|
33
|
+
const command = new commands_1.UpdateOptionsCommand({
|
|
34
|
+
viewBox: undefined,
|
|
35
|
+
});
|
|
36
|
+
void this.commander.execute(command);
|
|
36
37
|
return;
|
|
38
|
+
}
|
|
39
|
+
const pivot = (event.ctrlKey || event.metaKey) && !event.shiftKey
|
|
40
|
+
? this.getMousePoint(svg, event)
|
|
41
|
+
: this.getCenterPoint(viewBox);
|
|
42
|
+
const newViewBox = (0, viewbox_1.calculateZoomedViewBox)(viewBox, factor, pivot);
|
|
37
43
|
const command = new commands_1.UpdateOptionsCommand({
|
|
38
|
-
|
|
44
|
+
viewBox: (0, viewbox_1.viewBoxToString)(newViewBox),
|
|
39
45
|
});
|
|
40
46
|
void this.commander.execute(command);
|
|
41
47
|
};
|
|
48
|
+
this.getMousePoint = (svg, event) => {
|
|
49
|
+
return (0, utils_1.clientToViewport)(svg, event.clientX, event.clientY);
|
|
50
|
+
};
|
|
51
|
+
this.getCenterPoint = (viewBox) => {
|
|
52
|
+
const centerX = viewBox.x + viewBox.width / 2;
|
|
53
|
+
const centerY = viewBox.y + viewBox.height / 2;
|
|
54
|
+
return { x: centerX, y: centerY };
|
|
55
|
+
};
|
|
56
|
+
this.shouldZoom = (event) => {
|
|
57
|
+
if (!this.interaction.isActive())
|
|
58
|
+
return false;
|
|
59
|
+
if (event.deltaY === 0)
|
|
60
|
+
return false;
|
|
61
|
+
const isMouseZoom = event.ctrlKey || event.metaKey;
|
|
62
|
+
const isCenterZoom = event.shiftKey;
|
|
63
|
+
return isMouseZoom || isCenterZoom;
|
|
64
|
+
};
|
|
42
65
|
}
|
|
43
66
|
init(options) {
|
|
44
67
|
super.init(options);
|
|
@@ -79,8 +79,14 @@ class StateManager {
|
|
|
79
79
|
}
|
|
80
80
|
updateOptions(options) {
|
|
81
81
|
this.options = Object.assign(Object.assign({}, this.options), options);
|
|
82
|
-
if (this.options.
|
|
83
|
-
|
|
82
|
+
if (this.options.viewBox) {
|
|
83
|
+
this.editor.getDocument().setAttribute('viewBox', this.options.viewBox);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
this.editor.getDocument().removeAttribute('viewBox');
|
|
87
|
+
if (this.options.padding !== undefined) {
|
|
88
|
+
(0, utils_1.setSVGPadding)(this.editor.getDocument(), (0, utils_1.parsePadding)(this.options.padding));
|
|
89
|
+
}
|
|
84
90
|
}
|
|
85
91
|
this.emitter.emit('options:change', {
|
|
86
92
|
type: 'options:change',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chart_pie_1 = require("./chart-pie");
|
|
3
4
|
const compare_quadrant_1 = require("./compare-quadrant");
|
|
4
5
|
const hierarchy_mindmap_1 = require("./hierarchy-mindmap");
|
|
5
6
|
const hierarchy_structure_1 = require("./hierarchy-structure");
|
|
@@ -9,7 +10,7 @@ const registry_1 = require("./registry");
|
|
|
9
10
|
const relation_dagre_flow_1 = require("./relation-dagre-flow");
|
|
10
11
|
const sequence_stairs_1 = require("./sequence-stairs");
|
|
11
12
|
const word_cloud_1 = require("./word-cloud");
|
|
12
|
-
const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ 'compare-hierarchy-left-right-circle-node-pill-badge': {
|
|
13
|
+
const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ 'compare-hierarchy-left-right-circle-node-pill-badge': {
|
|
13
14
|
design: {
|
|
14
15
|
structure: {
|
|
15
16
|
type: 'compare-hierarchy-left-right',
|
|
@@ -625,82 +626,7 @@ const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assi
|
|
|
625
626
|
},
|
|
626
627
|
],
|
|
627
628
|
},
|
|
628
|
-
},
|
|
629
|
-
design: {
|
|
630
|
-
title: 'default',
|
|
631
|
-
structure: {
|
|
632
|
-
type: 'chart-pie',
|
|
633
|
-
},
|
|
634
|
-
items: [
|
|
635
|
-
{
|
|
636
|
-
type: 'plain-text',
|
|
637
|
-
},
|
|
638
|
-
],
|
|
639
|
-
},
|
|
640
|
-
}, 'chart-pie-compact-card': {
|
|
641
|
-
design: {
|
|
642
|
-
title: 'default',
|
|
643
|
-
structure: {
|
|
644
|
-
type: 'chart-pie',
|
|
645
|
-
},
|
|
646
|
-
items: [
|
|
647
|
-
{
|
|
648
|
-
type: 'compact-card',
|
|
649
|
-
},
|
|
650
|
-
],
|
|
651
|
-
},
|
|
652
|
-
}, 'chart-pie-pill-badge': {
|
|
653
|
-
design: {
|
|
654
|
-
title: 'default',
|
|
655
|
-
structure: {
|
|
656
|
-
type: 'chart-pie',
|
|
657
|
-
},
|
|
658
|
-
items: [
|
|
659
|
-
{
|
|
660
|
-
type: 'pill-badge',
|
|
661
|
-
},
|
|
662
|
-
],
|
|
663
|
-
},
|
|
664
|
-
}, 'chart-pie-donut-plain-text': {
|
|
665
|
-
design: {
|
|
666
|
-
title: 'default',
|
|
667
|
-
structure: {
|
|
668
|
-
type: 'chart-pie',
|
|
669
|
-
innerRadius: 90,
|
|
670
|
-
},
|
|
671
|
-
items: [
|
|
672
|
-
{
|
|
673
|
-
type: 'plain-text',
|
|
674
|
-
},
|
|
675
|
-
],
|
|
676
|
-
},
|
|
677
|
-
}, 'chart-pie-donut-compact-card': {
|
|
678
|
-
design: {
|
|
679
|
-
title: 'default',
|
|
680
|
-
structure: {
|
|
681
|
-
type: 'chart-pie',
|
|
682
|
-
innerRadius: 90,
|
|
683
|
-
},
|
|
684
|
-
items: [
|
|
685
|
-
{
|
|
686
|
-
type: 'compact-card',
|
|
687
|
-
},
|
|
688
|
-
],
|
|
689
|
-
},
|
|
690
|
-
}, 'chart-pie-donut-pill-badge': {
|
|
691
|
-
design: {
|
|
692
|
-
title: 'default',
|
|
693
|
-
structure: {
|
|
694
|
-
type: 'chart-pie',
|
|
695
|
-
innerRadius: 90,
|
|
696
|
-
},
|
|
697
|
-
items: [
|
|
698
|
-
{
|
|
699
|
-
type: 'pill-badge',
|
|
700
|
-
},
|
|
701
|
-
],
|
|
702
|
-
},
|
|
703
|
-
} }, compare_quadrant_1.compareQuadrantTemplates), hierarchy_tree_1.hierarchyTreeTemplates), hierarchy_mindmap_1.hierarchyMindmapTemplates), sequence_stairs_1.sequenceStairsTemplates), word_cloud_1.wordCloudTemplate), list_zigzag_1.listZigzagTemplates), relation_dagre_flow_1.relationDagreFlowTemplates), hierarchy_structure_1.hierarchyStructureTemplates);
|
|
629
|
+
} }, chart_pie_1.chartPieTemplates), compare_quadrant_1.compareQuadrantTemplates), hierarchy_tree_1.hierarchyTreeTemplates), hierarchy_mindmap_1.hierarchyMindmapTemplates), sequence_stairs_1.sequenceStairsTemplates), word_cloud_1.wordCloudTemplate), list_zigzag_1.listZigzagTemplates), relation_dagre_flow_1.relationDagreFlowTemplates), hierarchy_structure_1.hierarchyStructureTemplates);
|
|
704
630
|
Object.entries(BUILT_IN_TEMPLATES).forEach(([name, options]) => {
|
|
705
631
|
(0, registry_1.registerTemplate)(name, options);
|
|
706
632
|
});
|