@antv/infographic 0.2.6 → 0.2.8

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.
Files changed (164) hide show
  1. package/dist/infographic.min.js +191 -191
  2. package/dist/infographic.min.js.map +1 -1
  3. package/esm/designs/items/BadgeCard.js +6 -1
  4. package/esm/designs/items/HorizontalIconArrow.js +2 -2
  5. package/esm/designs/items/SimpleCircleNode.d.ts +8 -0
  6. package/esm/designs/items/SimpleCircleNode.js +14 -0
  7. package/esm/designs/items/index.d.ts +1 -0
  8. package/esm/designs/items/index.js +1 -0
  9. package/esm/designs/structures/hierarchy-mindmap.js +19 -5
  10. package/esm/designs/structures/hierarchy-tree.d.ts +2 -1
  11. package/esm/designs/structures/hierarchy-tree.js +23 -20
  12. package/esm/designs/structures/index.d.ts +2 -0
  13. package/esm/designs/structures/index.js +2 -0
  14. package/esm/designs/structures/relation-dagre-flow.d.ts +21 -0
  15. package/esm/designs/structures/relation-dagre-flow.js +497 -0
  16. package/esm/designs/structures/sequence-funnel.d.ts +10 -0
  17. package/esm/designs/structures/sequence-funnel.js +110 -0
  18. package/esm/designs/utils/hierarchy-color.d.ts +1 -1
  19. package/esm/editor/plugins/edit-bar/edit-bar.js +27 -9
  20. package/esm/index.js +1 -1
  21. package/esm/jsx/global.d.ts +1 -0
  22. package/esm/jsx/types/element.d.ts +5 -1
  23. package/esm/jsx/utils/svg.js +2 -0
  24. package/esm/options/types.d.ts +2 -0
  25. package/esm/renderer/composites/background.d.ts +2 -1
  26. package/esm/renderer/composites/background.js +6 -4
  27. package/esm/renderer/composites/icon.js +2 -0
  28. package/esm/renderer/composites/illus.d.ts +1 -1
  29. package/esm/renderer/composites/illus.js +9 -4
  30. package/esm/renderer/composites/text.js +4 -2
  31. package/esm/renderer/fonts/loader.js +5 -3
  32. package/esm/renderer/fonts/registry.js +1 -1
  33. package/esm/renderer/renderer.js +34 -22
  34. package/esm/resource/loader.js +3 -1
  35. package/esm/resource/loaders/svg.js +6 -4
  36. package/esm/runtime/Infographic.js +1 -1
  37. package/esm/ssr/dom-shim.d.ts +4 -0
  38. package/esm/ssr/dom-shim.js +107 -0
  39. package/esm/ssr/index.d.ts +1 -0
  40. package/esm/ssr/index.js +1 -0
  41. package/esm/ssr/renderer.d.ts +2 -0
  42. package/esm/ssr/renderer.js +60 -0
  43. package/esm/syntax/index.js +57 -1
  44. package/esm/syntax/parser.js +44 -0
  45. package/esm/syntax/relations.d.ts +6 -0
  46. package/esm/syntax/relations.js +251 -0
  47. package/esm/syntax/schema.d.ts +1 -0
  48. package/esm/syntax/schema.js +12 -0
  49. package/esm/templates/built-in.js +12 -0
  50. package/esm/templates/relation-dagre-flow.d.ts +2 -0
  51. package/esm/templates/relation-dagre-flow.js +68 -0
  52. package/esm/types/data.d.ts +24 -3
  53. package/esm/utils/data.js +1 -1
  54. package/esm/utils/index.d.ts +1 -0
  55. package/esm/utils/index.js +1 -0
  56. package/esm/utils/is-browser.js +5 -9
  57. package/esm/utils/measure-text.d.ts +2 -2
  58. package/esm/utils/measure-text.js +4 -4
  59. package/esm/utils/padding.js +19 -14
  60. package/esm/utils/recognizer.js +8 -5
  61. package/esm/utils/text.js +27 -19
  62. package/lib/designs/items/BadgeCard.js +6 -1
  63. package/lib/designs/items/HorizontalIconArrow.js +1 -1
  64. package/lib/designs/items/SimpleCircleNode.d.ts +8 -0
  65. package/lib/designs/items/SimpleCircleNode.js +18 -0
  66. package/lib/designs/items/index.d.ts +1 -0
  67. package/lib/designs/items/index.js +1 -0
  68. package/lib/designs/structures/hierarchy-mindmap.js +19 -5
  69. package/lib/designs/structures/hierarchy-tree.d.ts +2 -1
  70. package/lib/designs/structures/hierarchy-tree.js +23 -20
  71. package/lib/designs/structures/index.d.ts +2 -0
  72. package/lib/designs/structures/index.js +2 -0
  73. package/lib/designs/structures/relation-dagre-flow.d.ts +21 -0
  74. package/lib/designs/structures/relation-dagre-flow.js +501 -0
  75. package/lib/designs/structures/sequence-funnel.d.ts +10 -0
  76. package/lib/designs/structures/sequence-funnel.js +150 -0
  77. package/lib/designs/utils/hierarchy-color.d.ts +1 -1
  78. package/lib/editor/plugins/edit-bar/edit-bar.js +27 -9
  79. package/lib/jsx/global.d.ts +1 -0
  80. package/lib/jsx/types/element.d.ts +5 -1
  81. package/lib/jsx/utils/svg.js +2 -0
  82. package/lib/options/types.d.ts +2 -0
  83. package/lib/renderer/composites/background.d.ts +2 -1
  84. package/lib/renderer/composites/background.js +6 -4
  85. package/lib/renderer/composites/icon.js +2 -0
  86. package/lib/renderer/composites/illus.d.ts +1 -1
  87. package/lib/renderer/composites/illus.js +8 -3
  88. package/lib/renderer/composites/text.js +4 -2
  89. package/lib/renderer/fonts/loader.js +4 -2
  90. package/lib/renderer/fonts/registry.js +6 -6
  91. package/lib/renderer/renderer.js +33 -21
  92. package/lib/resource/loader.js +3 -1
  93. package/lib/resource/loaders/svg.js +6 -4
  94. package/lib/runtime/Infographic.js +1 -1
  95. package/lib/ssr/dom-shim.d.ts +4 -0
  96. package/lib/ssr/dom-shim.js +110 -0
  97. package/lib/ssr/index.d.ts +1 -0
  98. package/lib/ssr/index.js +5 -0
  99. package/lib/ssr/renderer.d.ts +2 -0
  100. package/lib/ssr/renderer.js +63 -0
  101. package/lib/syntax/index.js +57 -1
  102. package/lib/syntax/parser.js +44 -0
  103. package/lib/syntax/relations.d.ts +6 -0
  104. package/lib/syntax/relations.js +254 -0
  105. package/lib/syntax/schema.d.ts +1 -0
  106. package/lib/syntax/schema.js +13 -1
  107. package/lib/templates/built-in.js +12 -0
  108. package/lib/templates/relation-dagre-flow.d.ts +2 -0
  109. package/lib/templates/relation-dagre-flow.js +71 -0
  110. package/lib/types/data.d.ts +24 -3
  111. package/lib/utils/data.js +2 -5
  112. package/lib/utils/index.d.ts +1 -0
  113. package/lib/utils/index.js +1 -0
  114. package/lib/utils/is-browser.js +5 -9
  115. package/lib/utils/measure-text.d.ts +2 -2
  116. package/lib/utils/measure-text.js +4 -4
  117. package/lib/utils/padding.js +19 -14
  118. package/lib/utils/recognizer.js +8 -5
  119. package/lib/utils/text.js +28 -23
  120. package/package.json +20 -8
  121. package/src/designs/items/BadgeCard.tsx +9 -2
  122. package/src/designs/items/HorizontalIconArrow.tsx +10 -5
  123. package/src/designs/items/SimpleCircleNode.tsx +46 -0
  124. package/src/designs/items/index.ts +1 -0
  125. package/src/designs/structures/hierarchy-mindmap.tsx +15 -2
  126. package/src/designs/structures/hierarchy-tree.tsx +33 -31
  127. package/src/designs/structures/index.ts +2 -0
  128. package/src/designs/structures/relation-dagre-flow.tsx +782 -0
  129. package/src/designs/structures/sequence-funnel.tsx +260 -0
  130. package/src/designs/utils/hierarchy-color.ts +6 -1
  131. package/src/editor/plugins/edit-bar/edit-bar.ts +41 -17
  132. package/src/index.ts +1 -1
  133. package/src/jsx/global.ts +1 -0
  134. package/src/jsx/types/element.ts +15 -6
  135. package/src/jsx/utils/svg.ts +2 -0
  136. package/src/options/types.ts +2 -0
  137. package/src/renderer/composites/background.ts +8 -5
  138. package/src/renderer/composites/icon.ts +2 -0
  139. package/src/renderer/composites/illus.ts +16 -3
  140. package/src/renderer/composites/text.ts +7 -2
  141. package/src/renderer/fonts/loader.ts +9 -3
  142. package/src/renderer/fonts/registry.ts +1 -1
  143. package/src/renderer/renderer.ts +49 -22
  144. package/src/resource/loader.ts +3 -1
  145. package/src/resource/loaders/svg.ts +8 -4
  146. package/src/runtime/Infographic.tsx +1 -1
  147. package/src/ssr/dom-shim.ts +120 -0
  148. package/src/ssr/index.ts +1 -0
  149. package/src/ssr/renderer.ts +72 -0
  150. package/src/syntax/index.ts +58 -1
  151. package/src/syntax/parser.ts +49 -0
  152. package/src/syntax/relations.ts +291 -0
  153. package/src/syntax/schema.ts +16 -0
  154. package/src/templates/built-in.ts +12 -0
  155. package/src/templates/relation-dagre-flow.ts +73 -0
  156. package/src/types/data.ts +26 -3
  157. package/src/utils/data.ts +1 -1
  158. package/src/utils/index.ts +1 -0
  159. package/src/utils/is-browser.ts +3 -9
  160. package/src/utils/measure-text.ts +6 -7
  161. package/src/utils/padding.ts +18 -14
  162. package/src/utils/recognizer.ts +9 -5
  163. package/src/utils/svg.ts +0 -1
  164. package/src/utils/text.ts +25 -19
@@ -0,0 +1,497 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@antv/infographic/jsx-runtime";
2
+ import { DagreLayout } from '@antv/layout';
3
+ import { Defs, getElementBounds, Group, Path, Polygon, Text } from '../../jsx';
4
+ import { BtnAdd, BtnsGroup, ItemsGroup } from '../components';
5
+ import { FlexLayout } from '../layouts';
6
+ import { getColorPrimary, getPaletteColor, getThemeColors } from '../utils';
7
+ import { registerStructure } from './registry';
8
+ const DEFAULT_NODE_SEP = 50;
9
+ const DEFAULT_RANK_SEP = 70;
10
+ const DEFAULT_EDGE_SEP = 10;
11
+ const DEFAULT_EDGE_WIDTH = 2;
12
+ const DEFAULT_PADDING = 30;
13
+ const checkUndirectedCycle = (nodeIds, edges) => {
14
+ const adj = new Map();
15
+ nodeIds.forEach((id) => adj.set(id, []));
16
+ for (const edge of edges) {
17
+ if (edge.source === edge.target)
18
+ return true;
19
+ adj.get(edge.source)?.push({ target: edge.target, edgeId: edge.id });
20
+ adj.get(edge.target)?.push({ target: edge.source, edgeId: edge.id });
21
+ }
22
+ const visited = new Set();
23
+ const dfs = (u, parentEdgeId) => {
24
+ visited.add(u);
25
+ const neighbors = adj.get(u) || [];
26
+ for (const { target: v, edgeId } of neighbors) {
27
+ if (edgeId === parentEdgeId)
28
+ continue;
29
+ if (visited.has(v))
30
+ return true;
31
+ if (dfs(v, edgeId))
32
+ return true;
33
+ }
34
+ return false;
35
+ };
36
+ for (const node of nodeIds) {
37
+ if (!visited.has(node)) {
38
+ if (dfs(node, null))
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ };
44
+ export const RelationDagreFlow = (props) => {
45
+ const { Title, Item, data, rankdir = 'TB', nodesep = DEFAULT_NODE_SEP, ranksep = DEFAULT_RANK_SEP, edgesep = DEFAULT_EDGE_SEP, edgeWidth = DEFAULT_EDGE_WIDTH, showConnections = true, edgeColorMode = 'gradient', edgeStyle = 'solid', edgeDashPattern = '5,5', edgeCornerRadius = 12, edgeRouting = 'orth', showArrow = true, arrowType = 'triangle', padding = DEFAULT_PADDING, edgeAnimation = 'none', edgeAnimationSpeed = 1, options, } = props;
46
+ const { title, desc, items = [] } = data;
47
+ const titleContent = Title ? _jsx(Title, { title: title, desc: desc }) : null;
48
+ if (!Item || items.length === 0) {
49
+ return (_jsxs(FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: [titleContent, _jsx(Group, { children: _jsx(BtnAdd, { indexes: [0], x: 0, y: 0 }) })] }));
50
+ }
51
+ const nodeMetaMap = new Map();
52
+ const nodeSizeMap = new Map();
53
+ const nodeColorMap = new Map();
54
+ const nodeIdsByIndex = new Map();
55
+ const nodeIdSet = new Set();
56
+ const colorGroupIndexMap = new Map();
57
+ let nextColorGroupIndex = 0;
58
+ const nodes = items.map((item, index) => {
59
+ const datum = item;
60
+ const id = String(datum.id ?? index);
61
+ const indexes = [index];
62
+ let primary;
63
+ const groupKey = String(datum.group ?? '');
64
+ if (groupKey) {
65
+ let groupIndex = colorGroupIndexMap.get(groupKey);
66
+ if (groupIndex == null) {
67
+ groupIndex = nextColorGroupIndex;
68
+ colorGroupIndexMap.set(groupKey, groupIndex);
69
+ nextColorGroupIndex += 1;
70
+ }
71
+ primary = getPaletteColor(options, [groupIndex]);
72
+ }
73
+ else {
74
+ primary = getPaletteColor(options, indexes);
75
+ }
76
+ const themeColors = primary
77
+ ? getThemeColors({ colorPrimary: primary }, options)
78
+ : undefined;
79
+ if (primary) {
80
+ nodeColorMap.set(id, primary);
81
+ }
82
+ const bounds = getElementBounds(_jsx(Item, { indexes: indexes, data: data, datum: datum, positionH: "center", positionV: "middle", themeColors: themeColors }));
83
+ nodeSizeMap.set(id, bounds);
84
+ nodeMetaMap.set(id, { id, indexes, datum, themeColors });
85
+ nodeIdsByIndex.set(index, id);
86
+ nodeIdSet.add(id);
87
+ return { id, parentId: datum.parentId };
88
+ });
89
+ const relations = data.relations ?? [];
90
+ const resolveNodeId = (value) => {
91
+ if (value == null)
92
+ return null;
93
+ const direct = String(value);
94
+ if (nodeIdSet.has(direct))
95
+ return direct;
96
+ const asIndex = Number(value);
97
+ if (!Number.isNaN(asIndex)) {
98
+ const mapped = nodeIdsByIndex.get(asIndex);
99
+ if (mapped)
100
+ return mapped;
101
+ }
102
+ return null;
103
+ };
104
+ const edges = relations
105
+ .map((relation, index) => {
106
+ const source = resolveNodeId(relation.from);
107
+ const target = resolveNodeId(relation.to);
108
+ if (!source || !target)
109
+ return null;
110
+ return {
111
+ id: relation.id ? String(relation.id) : `edge-${index}`,
112
+ source,
113
+ target,
114
+ relation,
115
+ };
116
+ })
117
+ .filter(Boolean);
118
+ const hasCycle = checkUndirectedCycle(Array.from(nodeIdSet), edges);
119
+ const finalEdgeRouting = hasCycle ? 'dagre' : edgeRouting;
120
+ const layout = new DagreLayout({
121
+ rankdir,
122
+ nodesep,
123
+ ranksep,
124
+ edgesep,
125
+ controlPoints: true,
126
+ nodeSize: (node) => {
127
+ const id = String(node.id ?? '');
128
+ const bounds = nodeSizeMap.get(id);
129
+ return bounds ? [bounds.width, bounds.height] : [0, 0];
130
+ },
131
+ });
132
+ layout.execute({ nodes, edges });
133
+ const nodeLayouts = [];
134
+ layout.forEachNode((node) => {
135
+ const id = String(node.id);
136
+ const meta = nodeMetaMap.get(id);
137
+ if (!meta)
138
+ return;
139
+ const bounds = nodeSizeMap.get(id);
140
+ const width = bounds?.width ?? 0;
141
+ const height = bounds?.height ?? 0;
142
+ const x = (node.x ?? 0) - width / 2;
143
+ const y = (node.y ?? 0) - height / 2;
144
+ nodeLayouts.push({
145
+ ...meta,
146
+ x,
147
+ y,
148
+ width,
149
+ height,
150
+ centerX: x + width / 2,
151
+ centerY: y + height / 2,
152
+ });
153
+ });
154
+ if (nodeLayouts.length === 0) {
155
+ return (_jsxs(FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: [titleContent, _jsx(Group, { children: _jsx(BtnAdd, { indexes: [0], x: 0, y: 0 }) })] }));
156
+ }
157
+ const minX = Math.min(...nodeLayouts.map((node) => node.x));
158
+ const minY = Math.min(...nodeLayouts.map((node) => node.y));
159
+ const offsetX = padding - minX;
160
+ const offsetY = padding - minY;
161
+ const nodeLayoutById = new Map();
162
+ const itemElements = [];
163
+ nodeLayouts.forEach((node) => {
164
+ const displayX = node.x + offsetX;
165
+ const displayY = node.y + offsetY;
166
+ const positionH = rankdir === 'LR' ? 'normal' : rankdir === 'RL' ? 'flipped' : 'center';
167
+ const positionV = rankdir === 'TB' ? 'normal' : rankdir === 'BT' ? 'flipped' : 'middle';
168
+ itemElements.push(_jsx(Item, { indexes: node.indexes, datum: node.datum, data: data, x: displayX, y: displayY, positionH: positionH, positionV: positionV, themeColors: node.themeColors }));
169
+ nodeLayoutById.set(node.id, {
170
+ ...node,
171
+ x: displayX,
172
+ y: displayY,
173
+ centerX: displayX + node.width / 2,
174
+ centerY: displayY + node.height / 2,
175
+ });
176
+ });
177
+ const defsElements = [];
178
+ const decorElements = [];
179
+ if (showConnections) {
180
+ const defaultStroke = getColorPrimary(options);
181
+ const themeColors = getThemeColors(options.themeConfig, options);
182
+ const labelBackground = themeColors?.colorBg ?? '#ffffff';
183
+ const labelTextColor = themeColors?.colorText ?? defaultStroke;
184
+ const arrowSize = Math.max(10, edgeWidth * 4);
185
+ const isVertical = rankdir === 'TB' || rankdir === 'BT';
186
+ const enableAnimation = edgeAnimation === 'ant-line';
187
+ const animationDashArray = enableAnimation ? edgeDashPattern : '';
188
+ const staticDashArray = !enableAnimation && edgeStyle === 'dashed' ? edgeDashPattern : '';
189
+ const actualDashArray = enableAnimation
190
+ ? animationDashArray
191
+ : staticDashArray;
192
+ const dashPatternLength = enableAnimation
193
+ ? animationDashArray
194
+ .split(',')
195
+ .reduce((sum, val) => sum + parseFloat(val.trim() || '0'), 0)
196
+ : 0;
197
+ const animationDuration = enableAnimation && dashPatternLength > 0
198
+ ? `${dashPatternLength / (edgeAnimationSpeed * 10)}s`
199
+ : '1s';
200
+ const straightCornerRadius = edgeCornerRadius;
201
+ const createStraightPath = (points, dx, dy) => points
202
+ .map(([x, y], index) => {
203
+ const prefix = index === 0 ? 'M' : 'L';
204
+ return `${prefix} ${x + dx} ${y + dy}`;
205
+ })
206
+ .join(' ');
207
+ const createRoundedPath = (points, radius, dx, dy) => {
208
+ if (points.length < 2)
209
+ return '';
210
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
211
+ const toPoint = ([x, y]) => ({
212
+ x: x + dx,
213
+ y: y + dy,
214
+ });
215
+ const output = [];
216
+ const first = toPoint(points[0]);
217
+ output.push(`M ${first.x} ${first.y}`);
218
+ if (points.length === 2) {
219
+ const last = toPoint(points[1]);
220
+ output.push(`L ${last.x} ${last.y}`);
221
+ return output.join(' ');
222
+ }
223
+ for (let i = 1; i < points.length - 1; i += 1) {
224
+ const prev = points[i - 1];
225
+ const curr = points[i];
226
+ const next = points[i + 1];
227
+ const v0x = curr[0] - prev[0];
228
+ const v0y = curr[1] - prev[1];
229
+ const v1x = next[0] - curr[0];
230
+ const v1y = next[1] - curr[1];
231
+ const d0 = Math.hypot(v0x, v0y);
232
+ const d1 = Math.hypot(v1x, v1y);
233
+ if (d0 === 0 || d1 === 0) {
234
+ const currPoint = toPoint(curr);
235
+ output.push(`L ${currPoint.x} ${currPoint.y}`);
236
+ continue;
237
+ }
238
+ const r = clamp(radius, 0, Math.min(d0, d1) / 2);
239
+ if (r === 0) {
240
+ const currPoint = toPoint(curr);
241
+ output.push(`L ${currPoint.x} ${currPoint.y}`);
242
+ continue;
243
+ }
244
+ const u0x = v0x / d0;
245
+ const u0y = v0y / d0;
246
+ const u1x = v1x / d1;
247
+ const u1y = v1y / d1;
248
+ const start = toPoint([curr[0] - u0x * r, curr[1] - u0y * r]);
249
+ const end = toPoint([curr[0] + u1x * r, curr[1] + u1y * r]);
250
+ output.push(`L ${start.x} ${start.y}`);
251
+ const currPoint = toPoint(curr);
252
+ output.push(`Q ${currPoint.x} ${currPoint.y} ${end.x} ${end.y}`);
253
+ }
254
+ const last = toPoint(points[points.length - 1]);
255
+ output.push(`L ${last.x} ${last.y}`);
256
+ return output.join(' ');
257
+ };
258
+ const createArrowElements = (x, y, angle, type, fillColor) => {
259
+ const ux = Math.cos(angle);
260
+ const uy = Math.sin(angle);
261
+ const px = -uy;
262
+ const py = ux;
263
+ const length = arrowSize;
264
+ const halfWidth = arrowSize * 0.55;
265
+ if (type === 'arrow') {
266
+ const leftX = x - ux * length + px * halfWidth;
267
+ const leftY = y - uy * length + py * halfWidth;
268
+ const rightX = x - ux * length - px * halfWidth;
269
+ const rightY = y - uy * length - py * halfWidth;
270
+ return [
271
+ _jsx(Path, { d: `M ${leftX} ${leftY} L ${x} ${y} L ${rightX} ${rightY}`, stroke: fillColor, strokeWidth: Math.max(1.5, edgeWidth), strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }),
272
+ ];
273
+ }
274
+ if (type === 'diamond') {
275
+ const diamondLength = length * 1.25;
276
+ const diamondWidth = halfWidth * 0.75;
277
+ const midX = x - ux * diamondLength * 0.5;
278
+ const midY = y - uy * diamondLength * 0.5;
279
+ const diamondPoints = [
280
+ { x, y },
281
+ { x: midX + px * diamondWidth, y: midY + py * diamondWidth },
282
+ { x: x - ux * diamondLength, y: y - uy * diamondLength },
283
+ { x: midX - px * diamondWidth, y: midY - py * diamondWidth },
284
+ ];
285
+ return [
286
+ _jsx(Polygon, { points: diamondPoints, fill: fillColor, stroke: fillColor, strokeWidth: Math.max(1, edgeWidth * 0.8) }),
287
+ ];
288
+ }
289
+ const trianglePoints = [
290
+ { x, y },
291
+ {
292
+ x: x - ux * length + px * halfWidth,
293
+ y: y - uy * length + py * halfWidth,
294
+ },
295
+ {
296
+ x: x - ux * length - px * halfWidth,
297
+ y: y - uy * length - py * halfWidth,
298
+ },
299
+ ];
300
+ return [
301
+ _jsx(Polygon, { points: trianglePoints, fill: fillColor, stroke: fillColor, strokeWidth: Math.max(1, edgeWidth * 0.8) }),
302
+ ];
303
+ };
304
+ const getMidPoint = (points) => {
305
+ if (points.length === 0)
306
+ return null;
307
+ if (points.length === 1)
308
+ return points[0];
309
+ let total = 0;
310
+ const segments = [];
311
+ for (let i = 0; i < points.length - 1; i += 1) {
312
+ const start = points[i];
313
+ const end = points[i + 1];
314
+ const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
315
+ segments.push({ length, start, end });
316
+ total += length;
317
+ }
318
+ if (total === 0)
319
+ return points[0];
320
+ let target = total / 2;
321
+ for (let i = 0; i < segments.length; i += 1) {
322
+ const segment = segments[i];
323
+ if (target <= segment.length || i === segments.length - 1) {
324
+ const ratio = segment.length === 0
325
+ ? 0
326
+ : Math.max(0, Math.min(1, target / segment.length));
327
+ return [
328
+ segment.start[0] + (segment.end[0] - segment.start[0]) * ratio,
329
+ segment.start[1] + (segment.end[1] - segment.start[1]) * ratio,
330
+ ];
331
+ }
332
+ target -= segment.length;
333
+ }
334
+ return points[Math.floor(points.length / 2)];
335
+ };
336
+ const getOrthEdgeEndpoints = (sourceId, targetId) => {
337
+ const source = nodeLayoutById.get(sourceId);
338
+ const target = nodeLayoutById.get(targetId);
339
+ if (!source || !target)
340
+ return null;
341
+ if (rankdir === 'TB') {
342
+ return {
343
+ start: [source.centerX, source.y + source.height],
344
+ end: [target.centerX, target.y],
345
+ };
346
+ }
347
+ if (rankdir === 'BT') {
348
+ return {
349
+ start: [source.centerX, source.y],
350
+ end: [target.centerX, target.y + target.height],
351
+ };
352
+ }
353
+ if (rankdir === 'LR') {
354
+ return {
355
+ start: [source.x + source.width, source.centerY],
356
+ end: [target.x, target.centerY],
357
+ };
358
+ }
359
+ return {
360
+ start: [source.x, source.centerY],
361
+ end: [target.x + target.width, target.centerY],
362
+ };
363
+ };
364
+ const getOrthEdgePoints = (sourceId, targetId) => {
365
+ const endpoints = getOrthEdgeEndpoints(sourceId, targetId);
366
+ if (!endpoints)
367
+ return null;
368
+ const { start, end } = endpoints;
369
+ if (isVertical) {
370
+ const midY = start[1] + (end[1] - start[1]) / 2;
371
+ return {
372
+ start,
373
+ end,
374
+ points: [start, [start[0], midY], [end[0], midY], end],
375
+ };
376
+ }
377
+ const midX = start[0] + (end[0] - start[0]) / 2;
378
+ return {
379
+ start,
380
+ end,
381
+ points: [start, [midX, start[1]], [midX, end[1]], end],
382
+ };
383
+ };
384
+ layout.forEachEdge((edge) => {
385
+ const normalizePoints = (rawPoints) => {
386
+ if (!Array.isArray(rawPoints))
387
+ return [];
388
+ return rawPoints
389
+ .map((point) => {
390
+ if (!point)
391
+ return null;
392
+ if (Array.isArray(point) && point.length >= 2) {
393
+ return [Number(point[0]), Number(point[1])];
394
+ }
395
+ return null;
396
+ })
397
+ .filter((point) => !!point && Number.isFinite(point[0]) && Number.isFinite(point[1]));
398
+ };
399
+ const fallbackPoints = () => {
400
+ const source = nodeLayoutById.get(String(edge.source));
401
+ const target = nodeLayoutById.get(String(edge.target));
402
+ if (!source || !target)
403
+ return [];
404
+ return [
405
+ [source.centerX - offsetX, source.centerY - offsetY],
406
+ [target.centerX - offsetX, target.centerY - offsetY],
407
+ ];
408
+ };
409
+ const useOrthRouting = finalEdgeRouting === 'orth';
410
+ const orthEdge = useOrthRouting
411
+ ? getOrthEdgePoints(String(edge.source), String(edge.target))
412
+ : null;
413
+ const normalized = useOrthRouting ? [] : normalizePoints(edge.points);
414
+ const points = useOrthRouting
415
+ ? (orthEdge?.points ?? [])
416
+ : normalized.length
417
+ ? normalized
418
+ : fallbackPoints();
419
+ if (!points.length)
420
+ return;
421
+ const pointsOffsetX = useOrthRouting ? 0 : offsetX;
422
+ const pointsOffsetY = useOrthRouting ? 0 : offsetY;
423
+ const startPoint = useOrthRouting
424
+ ? (orthEdge?.start ?? points[0])
425
+ : points[0];
426
+ const endPoint = useOrthRouting
427
+ ? (orthEdge?.end ?? points[points.length - 1])
428
+ : points[points.length - 1];
429
+ const relation = edge
430
+ ._original?.relation;
431
+ const sourceColor = nodeColorMap.get(String(edge.source)) ?? defaultStroke;
432
+ const targetColor = nodeColorMap.get(String(edge.target)) ?? defaultStroke;
433
+ const gradientKey = `edge-gradient-${String(sourceColor)}-${String(targetColor)}`.replace(/[^a-zA-Z0-9_-]/g, '');
434
+ const edgeStroke = edgeColorMode === 'gradient' ? `url(#${gradientKey})` : defaultStroke;
435
+ let pathD = '';
436
+ if (straightCornerRadius > 0) {
437
+ pathD = createRoundedPath(points, straightCornerRadius, pointsOffsetX, pointsOffsetY);
438
+ }
439
+ else {
440
+ pathD = createStraightPath(points, pointsOffsetX, pointsOffsetY);
441
+ }
442
+ if (!pathD)
443
+ return;
444
+ const pathElement = (_jsx(Path, { d: pathD, stroke: edgeStroke, strokeWidth: edgeWidth, strokeDasharray: actualDashArray, fill: "none", "data-element-type": "shape", children: enableAnimation && (_jsx("animate", { attributeName: "stroke-dashoffset", from: String(dashPatternLength), to: "0", dur: animationDuration, repeatCount: "indefinite" })) }));
445
+ decorElements.push(pathElement);
446
+ if (edgeColorMode === 'gradient') {
447
+ const start = startPoint;
448
+ const end = endPoint;
449
+ defsElements.push(_jsxs("linearGradient", { id: gradientKey, gradientUnits: "userSpaceOnUse", x1: start[0] + pointsOffsetX, y1: start[1] + pointsOffsetY, x2: end[0] + pointsOffsetX, y2: end[1] + pointsOffsetY, children: [_jsx("stop", { offset: "0%", stopColor: sourceColor }), _jsx("stop", { offset: "100%", stopColor: targetColor })] }));
450
+ }
451
+ if (relation?.label) {
452
+ let labelPoint = null;
453
+ const midPoint = getMidPoint(points);
454
+ if (midPoint) {
455
+ labelPoint = [
456
+ midPoint[0] + pointsOffsetX,
457
+ midPoint[1] + pointsOffsetY,
458
+ ];
459
+ }
460
+ if (labelPoint) {
461
+ const labelText = String(relation.label);
462
+ const labelBounds = getElementBounds(_jsx(Text, { fontSize: 14, fontWeight: "normal", children: labelText }));
463
+ const labelX = labelPoint[0] - labelBounds.width / 2;
464
+ const labelY = labelPoint[1] - labelBounds.height / 2;
465
+ decorElements.push(_jsx(Text, { x: labelX, y: labelY, width: labelBounds.width, height: labelBounds.height, fontSize: 14, fontWeight: "normal", alignHorizontal: "center", alignVertical: "middle", fill: labelTextColor, backgroundColor: labelBackground, children: labelText }));
466
+ }
467
+ }
468
+ const effectiveShowArrow = relation?.showArrow ?? showArrow;
469
+ const direction = relation?.direction ?? 'forward';
470
+ const edgeArrowType = relation?.arrowType ?? arrowType;
471
+ const lastIndex = points.length - 1;
472
+ if (effectiveShowArrow && points.length > 1) {
473
+ if (direction === 'forward' || direction === 'both') {
474
+ const head = points[lastIndex];
475
+ const tail = points[lastIndex - 1];
476
+ const angle = Math.atan2(head[1] - tail[1], head[0] - tail[0]);
477
+ const arrowFill = edgeColorMode === 'gradient' ? targetColor : defaultStroke;
478
+ const arrowElements = createArrowElements(head[0] + pointsOffsetX, head[1] + pointsOffsetY, angle, edgeArrowType, arrowFill);
479
+ decorElements.push(...arrowElements);
480
+ }
481
+ if (direction === 'both') {
482
+ const head = points[0];
483
+ const tail = points[1];
484
+ const angle = Math.atan2(head[1] - tail[1], head[0] - tail[0]);
485
+ const arrowFill = edgeColorMode === 'gradient' ? sourceColor : defaultStroke;
486
+ const arrowElements = createArrowElements(head[0] + pointsOffsetX, head[1] + pointsOffsetY, angle, edgeArrowType, arrowFill);
487
+ decorElements.push(...arrowElements);
488
+ }
489
+ }
490
+ });
491
+ }
492
+ return (_jsxs(FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: [titleContent, _jsxs(Group, { children: [_jsx(Defs, { children: defsElements }), _jsx(Group, { width: 0, height: 0, children: decorElements }), _jsx(ItemsGroup, { children: itemElements }), _jsx(BtnsGroup, {})] })] }));
493
+ };
494
+ registerStructure('relation-dagre-flow', {
495
+ component: RelationDagreFlow,
496
+ composites: ['title', 'item'],
497
+ });
@@ -0,0 +1,10 @@
1
+ import type { ComponentType } from '../../jsx';
2
+ import type { BaseStructureProps } from './types';
3
+ export interface SequenceFunnelProps extends BaseStructureProps {
4
+ gap?: number;
5
+ width?: number;
6
+ funnelWidth?: number;
7
+ itemHeight?: number;
8
+ minBottomRatio?: number;
9
+ }
10
+ export declare const SequenceFunnel: ComponentType<SequenceFunnelProps>;
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@antv/infographic/jsx-runtime";
2
+ /**
3
+ * 序列漏斗结构(SequenceFunnel)
4
+ * 用途:
5
+ * - 在左侧渲染分层漏斗形状(倒置梯形堆叠),右侧渲染对应的 item 卡片与图标
6
+ * - 形状上宽下窄,底部平滑(梯形),卡片背景插入漏斗下方
7
+ */
8
+ import roundPolygon, { getSegments } from 'round-polygon';
9
+ import tinycolor from 'tinycolor2';
10
+ import { Defs, Group, Polygon, Rect } from '../../jsx';
11
+ import { BtnAdd, BtnRemove, BtnsGroup, ItemIcon, ItemsGroup, } from '../components';
12
+ import { FlexLayout } from '../layouts';
13
+ import { getPaletteColor, getThemeColors } from '../utils';
14
+ import { registerStructure } from './registry';
15
+ // Constants
16
+ const FUNNEL_CORNER_RADIUS = 6;
17
+ const ICON_SIZE = 32;
18
+ const FUNNEL_LAYER_HEIGHT_RATIO = 1.25;
19
+ const OVERLAP_DIST = 25;
20
+ const TEXT_GAP = 15;
21
+ export const SequenceFunnel = (props) => {
22
+ const { Title, Item, data, gap = 10, width = 700, funnelWidth, itemHeight = 60, minBottomRatio = 0.25, // 默认底部保留 25% 的宽度,形成梯形
23
+ options, } = props;
24
+ const { title, desc, items = [] } = data;
25
+ const titleContent = Title ? _jsx(Title, { title: title, desc: desc }) : null;
26
+ if (items.length === 0) {
27
+ return (_jsx(FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: titleContent }));
28
+ }
29
+ const themeColors = getThemeColors(options.themeConfig);
30
+ // 计算各区域尺寸
31
+ const actualFunnelWidth = funnelWidth ?? width * 0.55; // 稍微调窄一点漏斗,给右侧留更多空间
32
+ const itemAreaWidth = width - actualFunnelWidth;
33
+ // 漏斗层高度
34
+ const funnelLayerHeight = itemHeight * FUNNEL_LAYER_HEIGHT_RATIO;
35
+ const totalHeight = items.length * funnelLayerHeight + (items.length - 1) * gap;
36
+ // 计算底部的最小像素宽度
37
+ const minFunnelPixelWidth = actualFunnelWidth * minBottomRatio;
38
+ const elements = items.map((item, index) => {
39
+ const indexes = [index];
40
+ // 获取颜色
41
+ const color = getPaletteColor(options, [index]) || themeColors.colorPrimary;
42
+ // 1. 计算当前层的梯形形状
43
+ // 使用线性插值,从 actualFunnelWidth 收缩到 minFunnelPixelWidth
44
+ const { points, topWidth } = calculateTrapezoidSegment(actualFunnelWidth, minFunnelPixelWidth, funnelLayerHeight, gap, totalHeight, index);
45
+ // 圆角处理
46
+ const rounded = roundPolygon(points, FUNNEL_CORNER_RADIUS);
47
+ const segments = getSegments(rounded, 'AMOUNT', 10);
48
+ // 坐标计算
49
+ const funnelCenterX = actualFunnelWidth / 2;
50
+ const funnelY = index * (funnelLayerHeight + gap);
51
+ // 2. 背景与 Item 的位置计算
52
+ // 在漏斗(倒梯形)中,顶边(topWidth)总是比底边(bottomWidth)宽
53
+ // 所以右侧边缘的最外点是 topWidth 的一半
54
+ const rightTopX = funnelCenterX + topWidth / 2;
55
+ // 背景卡片:
56
+ // X 轴起点:从漏斗最宽处向左回缩 overlapDist,形成“插入”效果
57
+ const backgroundX = rightTopX - OVERLAP_DIST;
58
+ // 宽度:填满剩余空间,但要补上左侧回缩的距离
59
+ const backgroundWidth = itemAreaWidth + OVERLAP_DIST - 10; // -10 用于右侧留白
60
+ const backgroundYOffset = (funnelLayerHeight - itemHeight) / 2;
61
+ const backgroundY = funnelY + backgroundYOffset;
62
+ // 文本内容 (Item):
63
+ // X 轴起点:不应该跟着背景向左缩,而应该在漏斗边缘右侧,避免被漏斗遮挡
64
+ const itemX = rightTopX + TEXT_GAP;
65
+ const itemWidth = backgroundWidth - OVERLAP_DIST - TEXT_GAP;
66
+ const itemY = backgroundY;
67
+ // 图标位置
68
+ const iconX = funnelCenterX - ICON_SIZE / 2;
69
+ const iconY = funnelY + funnelLayerHeight / 2 - ICON_SIZE / 2;
70
+ const funnelColorId = `${color.replace('#', '')}-funnel-${index}`;
71
+ return {
72
+ background: (_jsx(Rect, { x: backgroundX, y: backgroundY, width: backgroundWidth, height: itemHeight, ry: "8" // 背景圆角稍微大一点,显得柔和
73
+ , fill: tinycolor(color).setAlpha(0.1).toRgbString(), "data-element-type": "shape" })),
74
+ funnel: [
75
+ _jsx(Defs, { children: _jsxs("linearGradient", { id: funnelColorId, x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [_jsx("stop", { offset: "0%", stopColor: tinycolor(color).lighten(10).toString() }), _jsx("stop", { offset: "100%", stopColor: color })] }) }),
76
+ _jsx(Polygon, { points: segments, fill: `url(#${funnelColorId})`, y: funnelY, "data-element-type": "shape",
77
+ // 添加轻微阴影效果增加层次感(可选,依赖环境支持 filter)
78
+ style: { filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.15))' } }),
79
+ ],
80
+ icon: (_jsx(ItemIcon, { indexes: indexes, x: iconX, y: iconY, size: ICON_SIZE, fill: "#fff" })),
81
+ item: (_jsx(Item, { indexes: indexes, datum: item, data: data, x: itemX, y: itemY, width: itemWidth, height: itemHeight, positionV: "middle" })),
82
+ btnRemove: (_jsx(BtnRemove, { indexes: indexes, x: backgroundX + backgroundWidth, y: backgroundY })),
83
+ };
84
+ });
85
+ const btnAdd = (_jsx(BtnAdd, { indexes: [items.length], x: width / 2, y: totalHeight + 10 }));
86
+ return (_jsxs(FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: [titleContent, _jsxs(Group, { width: width, height: totalHeight + 40, children: [_jsx(Group, { children: elements.map((element) => element.background) }), _jsx(Group, { children: elements.flatMap((element) => element.funnel) }), _jsx(Group, { children: elements.map((element) => element.icon) }), _jsx(ItemsGroup, { children: elements.map((element) => element.item) }), _jsxs(BtnsGroup, { children: [elements.map((element) => element.btnRemove), btnAdd] })] })] }));
87
+ };
88
+ // 计算梯形分段逻辑
89
+ function calculateTrapezoidSegment(maxWidth, minWidth, layerHeight, gap, totalHeight, index) {
90
+ const centerX = maxWidth / 2;
91
+ // 当前层顶部和底部的 Y 坐标(相对于总高度)
92
+ const currentTopY = index * (layerHeight + gap);
93
+ const currentBottomY = currentTopY + layerHeight;
94
+ // 线性插值计算宽度
95
+ // Width = MaxWidth - (MaxWidth - MinWidth) * (Y / TotalHeight)
96
+ const widthDiff = maxWidth - minWidth;
97
+ const topWidth = maxWidth - widthDiff * (currentTopY / totalHeight);
98
+ const bottomWidth = maxWidth - widthDiff * (currentBottomY / totalHeight);
99
+ // 生成四个顶点 (梯形)
100
+ const p1 = { x: centerX - topWidth / 2, y: 0 }; // 左上
101
+ const p2 = { x: centerX + topWidth / 2, y: 0 }; // 右上
102
+ const p3 = { x: centerX + bottomWidth / 2, y: layerHeight }; // 右下
103
+ const p4 = { x: centerX - bottomWidth / 2, y: layerHeight }; // 左下
104
+ return { points: [p1, p2, p3, p4], topWidth, bottomWidth };
105
+ }
106
+ // 注册
107
+ registerStructure('sequence-funnel', {
108
+ component: SequenceFunnel,
109
+ composites: ['title', 'item'],
110
+ });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 层级结构着色模式类型
3
3
  */
4
- export type HierarchyColorMode = 'level' | 'branch' | 'node' | 'node-flat';
4
+ export type HierarchyColorMode = 'level' | 'branch' | 'node' | 'node-flat' | 'group';
5
5
  /**
6
6
  * 层级节点信息接口
7
7
  */