@antv/infographic 0.1.3 → 0.2.0

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 (181) hide show
  1. package/README.md +54 -40
  2. package/README.zh-CN.md +52 -36
  3. package/dist/infographic.min.js +110 -105
  4. package/dist/infographic.min.js.map +1 -1
  5. package/esm/constants/element.d.ts +1 -1
  6. package/esm/constants/index.d.ts +1 -0
  7. package/esm/constants/index.js +1 -0
  8. package/esm/constants/service.d.ts +1 -0
  9. package/esm/constants/service.js +1 -0
  10. package/esm/designs/components/Illus.js +1 -1
  11. package/esm/designs/structures/chart-wordcloud.d.ts +11 -0
  12. package/esm/designs/structures/chart-wordcloud.js +156 -0
  13. package/esm/designs/structures/hierarchy-tree.d.ts +2 -0
  14. package/esm/designs/structures/hierarchy-tree.js +179 -50
  15. package/esm/designs/structures/index.d.ts +2 -0
  16. package/esm/designs/structures/index.js +2 -0
  17. package/esm/designs/structures/sequence-stairs-front.d.ts +8 -0
  18. package/esm/designs/structures/sequence-stairs-front.js +116 -0
  19. package/esm/designs/types.d.ts +8 -0
  20. package/esm/editor/managers/state.js +1 -1
  21. package/esm/exporter/font.js +4 -9
  22. package/esm/index.d.ts +2 -0
  23. package/esm/index.js +1 -0
  24. package/esm/options/parser.d.ts +1 -1
  25. package/esm/options/parser.js +33 -15
  26. package/esm/renderer/composites/icon.js +1 -1
  27. package/esm/renderer/composites/illus.js +1 -1
  28. package/esm/resource/loader.d.ts +2 -2
  29. package/esm/resource/loader.js +22 -11
  30. package/esm/resource/loaders/index.d.ts +1 -0
  31. package/esm/resource/loaders/index.js +1 -0
  32. package/esm/resource/loaders/remote.d.ts +1 -1
  33. package/esm/resource/loaders/remote.js +12 -3
  34. package/esm/resource/loaders/search.d.ts +1 -0
  35. package/esm/resource/loaders/search.js +52 -0
  36. package/esm/resource/types/index.d.ts +1 -0
  37. package/esm/resource/types/resource.d.ts +8 -1
  38. package/esm/resource/types/scene.d.ts +1 -0
  39. package/esm/resource/utils/data-uri.js +20 -11
  40. package/esm/resource/utils/parser.js +92 -1
  41. package/esm/resource/utils/ref.js +2 -2
  42. package/esm/runtime/Infographic.d.ts +10 -6
  43. package/esm/runtime/Infographic.js +66 -17
  44. package/esm/runtime/utils.d.ts +4 -2
  45. package/esm/runtime/utils.js +33 -13
  46. package/esm/syntax/index.d.ts +3 -0
  47. package/esm/syntax/index.js +101 -0
  48. package/esm/syntax/mapper.d.ts +3 -0
  49. package/esm/syntax/mapper.js +334 -0
  50. package/esm/syntax/parser.d.ts +14 -0
  51. package/esm/syntax/parser.js +142 -0
  52. package/esm/syntax/schema.d.ts +6 -0
  53. package/esm/syntax/schema.js +86 -0
  54. package/esm/syntax/types.d.ts +68 -0
  55. package/esm/syntax/types.js +1 -0
  56. package/esm/templates/built-in.js +4 -0
  57. package/esm/templates/hierarchy-tree.js +25 -11
  58. package/esm/templates/sequence-stairs.d.ts +2 -0
  59. package/esm/templates/sequence-stairs.js +42 -0
  60. package/esm/templates/word-cloud.d.ts +2 -0
  61. package/esm/templates/word-cloud.js +19 -0
  62. package/esm/themes/types.d.ts +1 -1
  63. package/esm/utils/design.d.ts +2 -0
  64. package/esm/utils/design.js +10 -0
  65. package/esm/utils/fetch.d.ts +1 -0
  66. package/esm/utils/fetch.js +61 -0
  67. package/esm/utils/font.js +11 -1
  68. package/esm/utils/index.d.ts +2 -0
  69. package/esm/utils/index.js +2 -0
  70. package/lib/constants/element.d.ts +1 -1
  71. package/lib/constants/index.d.ts +1 -0
  72. package/lib/constants/index.js +1 -0
  73. package/lib/constants/service.d.ts +1 -0
  74. package/lib/constants/service.js +4 -0
  75. package/lib/designs/components/Illus.js +1 -1
  76. package/lib/designs/structures/chart-wordcloud.d.ts +11 -0
  77. package/lib/designs/structures/chart-wordcloud.js +160 -0
  78. package/lib/designs/structures/hierarchy-tree.d.ts +2 -0
  79. package/lib/designs/structures/hierarchy-tree.js +179 -50
  80. package/lib/designs/structures/index.d.ts +2 -0
  81. package/lib/designs/structures/index.js +2 -0
  82. package/lib/designs/structures/sequence-stairs-front.d.ts +8 -0
  83. package/lib/designs/structures/sequence-stairs-front.js +120 -0
  84. package/lib/designs/types.d.ts +8 -0
  85. package/lib/editor/managers/state.js +1 -1
  86. package/lib/exporter/font.js +3 -8
  87. package/lib/index.d.ts +2 -0
  88. package/lib/index.js +4 -1
  89. package/lib/options/parser.d.ts +1 -1
  90. package/lib/options/parser.js +32 -14
  91. package/lib/renderer/composites/icon.js +1 -1
  92. package/lib/renderer/composites/illus.js +1 -1
  93. package/lib/resource/loader.d.ts +2 -2
  94. package/lib/resource/loader.js +21 -10
  95. package/lib/resource/loaders/index.d.ts +1 -0
  96. package/lib/resource/loaders/index.js +1 -0
  97. package/lib/resource/loaders/remote.d.ts +1 -1
  98. package/lib/resource/loaders/remote.js +12 -3
  99. package/lib/resource/loaders/search.d.ts +1 -0
  100. package/lib/resource/loaders/search.js +55 -0
  101. package/lib/resource/types/index.d.ts +1 -0
  102. package/lib/resource/types/resource.d.ts +8 -1
  103. package/lib/resource/types/scene.d.ts +1 -0
  104. package/lib/resource/utils/data-uri.js +20 -11
  105. package/lib/resource/utils/parser.js +92 -1
  106. package/lib/resource/utils/ref.js +2 -2
  107. package/lib/runtime/Infographic.d.ts +10 -6
  108. package/lib/runtime/Infographic.js +65 -16
  109. package/lib/runtime/utils.d.ts +4 -2
  110. package/lib/runtime/utils.js +35 -13
  111. package/lib/syntax/index.d.ts +3 -0
  112. package/lib/syntax/index.js +104 -0
  113. package/lib/syntax/mapper.d.ts +3 -0
  114. package/lib/syntax/mapper.js +341 -0
  115. package/lib/syntax/parser.d.ts +14 -0
  116. package/lib/syntax/parser.js +146 -0
  117. package/lib/syntax/schema.d.ts +6 -0
  118. package/lib/syntax/schema.js +89 -0
  119. package/lib/syntax/types.d.ts +68 -0
  120. package/lib/syntax/types.js +2 -0
  121. package/lib/templates/built-in.js +4 -0
  122. package/lib/templates/hierarchy-tree.js +25 -11
  123. package/lib/templates/sequence-stairs.d.ts +2 -0
  124. package/lib/templates/sequence-stairs.js +45 -0
  125. package/lib/templates/word-cloud.d.ts +2 -0
  126. package/lib/templates/word-cloud.js +22 -0
  127. package/lib/themes/types.d.ts +1 -1
  128. package/lib/utils/design.d.ts +2 -0
  129. package/lib/utils/design.js +13 -0
  130. package/lib/utils/fetch.d.ts +1 -0
  131. package/lib/utils/fetch.js +67 -0
  132. package/lib/utils/font.js +11 -1
  133. package/lib/utils/index.d.ts +2 -0
  134. package/lib/utils/index.js +2 -0
  135. package/package.json +14 -2
  136. package/src/constants/element.ts +1 -1
  137. package/src/constants/index.ts +1 -0
  138. package/src/constants/service.ts +1 -0
  139. package/src/designs/components/Illus.tsx +1 -1
  140. package/src/designs/structures/chart-wordcloud.tsx +278 -0
  141. package/src/designs/structures/hierarchy-tree.tsx +212 -59
  142. package/src/designs/structures/index.ts +2 -0
  143. package/src/designs/structures/sequence-stairs-front.tsx +291 -0
  144. package/src/designs/types.ts +9 -0
  145. package/src/editor/managers/state.ts +1 -1
  146. package/src/exporter/font.ts +4 -9
  147. package/src/index.ts +2 -0
  148. package/src/options/parser.ts +57 -28
  149. package/src/renderer/composites/icon.ts +1 -1
  150. package/src/renderer/composites/illus.ts +1 -1
  151. package/src/resource/loader.ts +22 -8
  152. package/src/resource/loaders/index.ts +1 -0
  153. package/src/resource/loaders/remote.ts +11 -3
  154. package/src/resource/loaders/search.ts +53 -0
  155. package/src/resource/types/index.ts +2 -1
  156. package/src/resource/types/resource.ts +12 -1
  157. package/src/resource/types/scene.ts +1 -0
  158. package/src/resource/utils/data-uri.ts +20 -11
  159. package/src/resource/utils/parser.ts +103 -2
  160. package/src/resource/utils/ref.ts +2 -2
  161. package/src/runtime/Infographic.tsx +103 -22
  162. package/src/runtime/utils.ts +38 -16
  163. package/src/syntax/index.ts +124 -0
  164. package/src/syntax/mapper.ts +496 -0
  165. package/src/syntax/parser.ts +171 -0
  166. package/src/syntax/schema.ts +112 -0
  167. package/src/syntax/types.ts +100 -0
  168. package/src/templates/built-in.ts +4 -0
  169. package/src/templates/hierarchy-tree.ts +34 -11
  170. package/src/templates/sequence-stairs.ts +44 -0
  171. package/src/templates/word-cloud.ts +21 -0
  172. package/src/themes/types.ts +1 -1
  173. package/src/utils/design.ts +14 -0
  174. package/src/utils/fetch.ts +90 -0
  175. package/src/utils/font.ts +11 -1
  176. package/src/utils/index.ts +2 -0
  177. package/esm/resource/types/font.d.ts +0 -12
  178. package/lib/resource/types/font.d.ts +0 -12
  179. package/src/resource/types/font.ts +0 -23
  180. /package/esm/resource/types/{font.js → scene.js} +0 -0
  181. /package/lib/resource/types/{font.js → scene.js} +0 -0
@@ -0,0 +1,278 @@
1
+ import { ElementTypeEnum } from '../../constants';
2
+ import type { ComponentType, JSXElement } from '../../jsx';
3
+ import { getElementBounds, Group, Text } from '../../jsx';
4
+ import { ItemsGroup } from '../components';
5
+ import { FlexLayout } from '../layouts';
6
+ import { getColorPrimary, getPaletteColor } from '../utils';
7
+ import { registerStructure } from './registry';
8
+ import type { BaseStructureProps } from './types';
9
+
10
+ interface WordCandidate {
11
+ label: string;
12
+ value?: number;
13
+ color: string;
14
+ fontSize: number;
15
+ width: number;
16
+ height: number;
17
+ }
18
+
19
+ interface PlacedWord extends WordCandidate {
20
+ angle: number;
21
+ centerX: number;
22
+ centerY: number;
23
+ box: { x: number; y: number; width: number; height: number };
24
+ }
25
+
26
+ const DEFAULT_ROTATE_ANGLES = [0, 30, -30, 60, -60];
27
+ const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
28
+
29
+ function getRotatedSize(width: number, height: number, angle: number) {
30
+ const rad = (Math.PI / 180) * angle;
31
+ const cos = Math.cos(rad);
32
+ const sin = Math.sin(rad);
33
+ return {
34
+ width: Math.abs(width * cos) + Math.abs(height * sin),
35
+ height: Math.abs(width * sin) + Math.abs(height * cos),
36
+ };
37
+ }
38
+
39
+ function hasCollision(
40
+ x: number,
41
+ y: number,
42
+ width: number,
43
+ height: number,
44
+ placed: PlacedWord[],
45
+ padding: number,
46
+ ) {
47
+ const left = x - padding;
48
+ const right = x + width + padding;
49
+ const top = y - padding;
50
+ const bottom = y + height + padding;
51
+
52
+ return placed.some((word) => {
53
+ const wLeft = word.box.x - padding;
54
+ const wRight = word.box.x + word.box.width + padding;
55
+ const wTop = word.box.y - padding;
56
+ const wBottom = word.box.y + word.box.height + padding;
57
+ return !(
58
+ right <= wLeft ||
59
+ left >= wRight ||
60
+ bottom <= wTop ||
61
+ top >= wBottom
62
+ );
63
+ });
64
+ }
65
+
66
+ function placeWords(
67
+ words: WordCandidate[],
68
+ enableRotate: boolean,
69
+ padding: number,
70
+ spiralStep: number,
71
+ radiusStep: number,
72
+ ): PlacedWord[] {
73
+ const placed: PlacedWord[] = [];
74
+ const rotationAngles = enableRotate ? DEFAULT_ROTATE_ANGLES : [0];
75
+ const maxAttempts = Math.max(1600, words.length * 28);
76
+
77
+ words.forEach((word, wordIndex) => {
78
+ const sizeBias = Math.max(word.width, word.height);
79
+ const angleOffset = wordIndex * GOLDEN_ANGLE;
80
+ let extraRadius = 0;
81
+ let placedWord: PlacedWord | null = null;
82
+
83
+ for (let attempt = 0; attempt < maxAttempts && !placedWord; attempt++) {
84
+ if (attempt === Math.floor(maxAttempts * 0.6)) {
85
+ // Gradually expand the search radius for dense layouts
86
+ extraRadius = sizeBias;
87
+ }
88
+
89
+ const theta = angleOffset + attempt * spiralStep;
90
+ const radius =
91
+ radiusStep * Math.sqrt(attempt + 1) + extraRadius + sizeBias * 0.25;
92
+ const centerX = radius * Math.cos(theta);
93
+ const centerY = radius * Math.sin(theta);
94
+
95
+ for (const angle of rotationAngles) {
96
+ const rotated = getRotatedSize(word.width, word.height, angle);
97
+ const x = centerX - rotated.width / 2;
98
+ const y = centerY - rotated.height / 2;
99
+ if (
100
+ !hasCollision(x, y, rotated.width, rotated.height, placed, padding)
101
+ ) {
102
+ placedWord = {
103
+ ...word,
104
+ angle,
105
+ centerX,
106
+ centerY,
107
+ box: { x, y, width: rotated.width, height: rotated.height },
108
+ };
109
+ break;
110
+ }
111
+ }
112
+ }
113
+
114
+ if (!placedWord) {
115
+ const fallbackAngle = rotationAngles[wordIndex % rotationAngles.length];
116
+ const farRadius = radiusStep * Math.sqrt(maxAttempts + 1) + sizeBias;
117
+ const theta = angleOffset;
118
+ const centerX = farRadius * Math.cos(theta);
119
+ const centerY = farRadius * Math.sin(theta);
120
+ const rotated = getRotatedSize(word.width, word.height, fallbackAngle);
121
+ placedWord = {
122
+ ...word,
123
+ angle: fallbackAngle,
124
+ centerX,
125
+ centerY,
126
+ box: {
127
+ x: centerX - rotated.width / 2,
128
+ y: centerY - rotated.height / 2,
129
+ width: rotated.width,
130
+ height: rotated.height,
131
+ },
132
+ };
133
+ }
134
+
135
+ placed.push(placedWord);
136
+ });
137
+
138
+ return placed;
139
+ }
140
+
141
+ export interface ChartWordCloudProps extends BaseStructureProps {
142
+ minFontSize?: number;
143
+ maxFontSize?: number;
144
+ enableRotate?: boolean;
145
+ padding?: number;
146
+ spiralStep?: number;
147
+ radiusStep?: number;
148
+ }
149
+
150
+ export const ChartWordCloud: ComponentType<ChartWordCloudProps> = (props) => {
151
+ const {
152
+ data,
153
+ options,
154
+ minFontSize = 16,
155
+ maxFontSize = 48,
156
+ enableRotate = true,
157
+ padding = 6,
158
+ spiralStep = 0.45,
159
+ radiusStep = 10,
160
+ } = props;
161
+ const { items = [] } = data;
162
+
163
+ const validItems = items
164
+ .map((datum, index) => ({ datum, index }))
165
+ .filter(({ datum }) => datum.label);
166
+
167
+ if (validItems.length === 0) {
168
+ return (
169
+ <FlexLayout
170
+ id="infographic-container"
171
+ flexDirection="column"
172
+ justifyContent="center"
173
+ alignItems="center"
174
+ >
175
+ <Group>
176
+ <ItemsGroup />
177
+ </Group>
178
+ </FlexLayout>
179
+ );
180
+ }
181
+
182
+ const values = validItems
183
+ .map(({ datum }) => datum.value)
184
+ .filter((v): v is number => typeof v === 'number');
185
+ const hasValues = values.length > 0;
186
+ const minValue = hasValues ? Math.min(...values) : 0;
187
+ const maxValue = hasValues ? Math.max(...values) : 0;
188
+ const sameValue = hasValues && minValue === maxValue;
189
+ const uniformSize = (minFontSize + maxFontSize) / 2;
190
+
191
+ const mapFontSize = (value?: number) => {
192
+ if (!hasValues || sameValue) return uniformSize;
193
+ if (value == null) return minFontSize;
194
+ const ratio = (value - minValue) / (maxValue - minValue || 1);
195
+ return minFontSize + ratio * (maxFontSize - minFontSize);
196
+ };
197
+
198
+ const words: WordCandidate[] = validItems
199
+ .map(({ datum, index }) => {
200
+ const fontSize = mapFontSize(datum.value);
201
+ const measured = getElementBounds(
202
+ <Text fontSize={fontSize} fontWeight="bold">
203
+ {datum.label}
204
+ </Text>,
205
+ );
206
+ const color =
207
+ getPaletteColor(options, [index]) ||
208
+ getColorPrimary(options) ||
209
+ '#333333';
210
+ return {
211
+ label: datum.label as string,
212
+ value: datum.value,
213
+ color,
214
+ fontSize,
215
+ width: measured.width * 1.05,
216
+ height: measured.height,
217
+ };
218
+ })
219
+ .sort((a, b) => b.fontSize - a.fontSize);
220
+
221
+ const placedWords = placeWords(
222
+ words,
223
+ enableRotate,
224
+ padding,
225
+ spiralStep,
226
+ radiusStep,
227
+ );
228
+
229
+ const minX = Math.min(...placedWords.map((w) => w.box.x));
230
+ const minY = Math.min(...placedWords.map((w) => w.box.y));
231
+ const maxX = Math.max(...placedWords.map((w) => w.box.x + w.box.width));
232
+ const maxY = Math.max(...placedWords.map((w) => w.box.y + w.box.height));
233
+ const offsetX = -minX + padding;
234
+ const offsetY = -minY + padding;
235
+ const containerWidth = maxX - minX + padding * 2;
236
+ const containerHeight = maxY - minY + padding * 2;
237
+
238
+ const wordElements: JSXElement[] = placedWords.map((word, index) => {
239
+ const translateX = word.centerX - word.width / 2 + offsetX;
240
+ const translateY = word.centerY - word.height / 2 + offsetY;
241
+ const rotationOriginX = word.width / 2;
242
+ const rotationOriginY = word.height / 2;
243
+ const transform = `translate(${translateX}, ${translateY}) rotate(${word.angle}, ${rotationOriginX}, ${rotationOriginY})`;
244
+
245
+ return (
246
+ <Group transform={transform}>
247
+ <Text
248
+ width={word.width}
249
+ height={word.height}
250
+ fontSize={word.fontSize}
251
+ fontWeight="bold"
252
+ alignHorizontal="center"
253
+ alignVertical="middle"
254
+ fill={word.color}
255
+ data-indexes={index}
256
+ data-element-type={ElementTypeEnum.ItemLabel}
257
+ >
258
+ {word.label}
259
+ </Text>
260
+ </Group>
261
+ );
262
+ });
263
+
264
+ return (
265
+ <ItemsGroup
266
+ id="infographic-container"
267
+ width={containerWidth}
268
+ height={containerHeight}
269
+ >
270
+ {wordElements}
271
+ </ItemsGroup>
272
+ );
273
+ };
274
+
275
+ registerStructure('chart-wordcloud', {
276
+ component: ChartWordCloud,
277
+ composites: ['item'],
278
+ });
@@ -33,6 +33,8 @@ export interface HierarchyTreeProps extends BaseStructureProps {
33
33
  levelGap?: number;
34
34
  /** 节点间距:同级节点之间的水平距离,默认 60 */
35
35
  nodeGap?: number;
36
+ /** 布局方向:'top-bottom' 自上而下 | 'bottom-top' 自下而上 | 'left-right' 自左向右 | 'right-left' 自右向左,默认 'top-bottom' */
37
+ orientation?: 'top-bottom' | 'bottom-top' | 'left-right' | 'right-left';
36
38
 
37
39
  // ========== 连接线样式配置 ==========
38
40
  /** 连接线类型:'straight' 直线 | 'curved' 曲线,默认 'curved' */
@@ -73,6 +75,11 @@ export interface HierarchyTreeProps extends BaseStructureProps {
73
75
  colorMode?: HierarchyColorMode;
74
76
  }
75
77
 
78
+ const distributedPadding = (rawPadding: number, size: number): number => {
79
+ const maxPadding = Math.max(0, size / 2 - 1);
80
+ return Math.min(rawPadding, maxPadding);
81
+ };
82
+
76
83
  export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
77
84
  const {
78
85
  Title,
@@ -97,8 +104,14 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
97
104
  markerSize = 12,
98
105
  // 着色模式配置
99
106
  colorMode = 'branch',
107
+ // 布局方向
108
+ orientation = 'top-bottom',
100
109
  options,
101
110
  } = props;
111
+ const isHorizontal =
112
+ orientation === 'left-right' || orientation === 'right-left';
113
+ const mainSign =
114
+ orientation === 'bottom-top' || orientation === 'right-left' ? -1 : 1;
102
115
  const { title, desc } = data;
103
116
  const colorPrimary = getColorPrimary(options);
104
117
 
@@ -119,25 +132,64 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
119
132
  x2: number,
120
133
  y2: number,
121
134
  radius: number,
135
+ direction: 'vertical' | 'horizontal' = 'vertical',
122
136
  ): string => {
123
- const midY = y1 + (y2 - y1) / 2;
137
+ const isVertical = direction === 'vertical';
138
+ const deltaMain = isVertical ? y2 - y1 : x2 - x1;
139
+ const deltaCross = isVertical ? x2 - x1 : y2 - y1;
140
+ const signMain = deltaMain === 0 ? 1 : Math.sign(deltaMain);
141
+ const signCross = deltaCross === 0 ? 1 : Math.sign(deltaCross);
142
+ const midMain = isVertical ? y1 + deltaMain / 2 : x1 + deltaMain / 2;
124
143
  const effectiveRadius = Math.min(
125
144
  radius,
126
- Math.abs(y2 - y1) / 2,
127
- Math.abs(x2 - x1) / 2,
145
+ Math.abs(deltaMain) / 2,
146
+ Math.abs(deltaCross) / 2,
128
147
  );
129
148
 
130
149
  if (effectiveRadius === 0) {
131
- return `M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`;
150
+ return isVertical
151
+ ? `M ${x1} ${y1} L ${x1} ${midMain} L ${x2} ${midMain} L ${x2} ${y2}`
152
+ : `M ${x1} ${y1} L ${midMain} ${y1} L ${midMain} ${y2} L ${x2} ${y2}`;
153
+ }
154
+
155
+ if (isVertical) {
156
+ return `M ${x1} ${y1}
157
+ L ${x1} ${midMain - signMain * effectiveRadius}
158
+ Q ${x1} ${midMain} ${x1 + signCross * effectiveRadius} ${midMain}
159
+ L ${x2 - signCross * effectiveRadius} ${midMain}
160
+ Q ${x2} ${midMain} ${x2} ${midMain + signMain * effectiveRadius}
161
+ L ${x2} ${y2}`;
132
162
  }
133
163
 
134
164
  return `M ${x1} ${y1}
135
- L ${x1} ${midY - effectiveRadius}
136
- Q ${x1} ${midY} ${x1 + (x2 > x1 ? effectiveRadius : -effectiveRadius)} ${midY}
137
- L ${x2 - (x2 > x1 ? effectiveRadius : -effectiveRadius)} ${midY}
138
- Q ${x2} ${midY} ${x2} ${midY + effectiveRadius}
165
+ L ${midMain - signMain * effectiveRadius} ${y1}
166
+ Q ${midMain} ${y1} ${midMain} ${y1 + signCross * effectiveRadius}
167
+ L ${midMain} ${y2 - signCross * effectiveRadius}
168
+ Q ${midMain} ${y2} ${midMain + signMain * effectiveRadius} ${y2}
139
169
  L ${x2} ${y2}`;
140
170
  };
171
+ const getLayoutPoint = (node: any) => {
172
+ const { x, y } = node;
173
+ return isHorizontal ? { x: y * mainSign, y: x } : { x, y: y * mainSign };
174
+ };
175
+ const getNodeRect = (
176
+ node: any,
177
+ bounds: any,
178
+ offsets: { x: number; y: number },
179
+ ) => {
180
+ const { x, y } = getLayoutPoint(node);
181
+ const centerX = x + offsets.x;
182
+ const top = y + offsets.y;
183
+ const centerY = top + bounds.height / 2;
184
+ return {
185
+ centerX,
186
+ centerY,
187
+ left: centerX - bounds.width / 2,
188
+ right: centerX + bounds.width / 2,
189
+ top,
190
+ bottom: top + bounds.height,
191
+ };
192
+ };
141
193
 
142
194
  // 内置工具方法:构建层级数据
143
195
  const buildHierarchyData = (list: any[]): any => {
@@ -217,12 +269,13 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
217
269
  gradientDefs: JSXElement[],
218
270
  allNodes: any[],
219
271
  ) => {
220
- const { x, y, depth, data: nodeData, parent } = node;
272
+ const { depth, data: nodeData, parent } = node;
221
273
  const indexes = nodeData._originalIndex;
222
274
  const NodeComponent = getItemComponent(Items, depth);
223
275
  const bounds = levelBounds.get(depth)!;
224
- const nodeX = x + offsets.x - bounds.width / 2;
225
- const nodeY = y + offsets.y;
276
+ const nodeRect = getNodeRect(node, bounds, offsets);
277
+ const nodeX = nodeRect.left;
278
+ const nodeY = nodeRect.top;
226
279
 
227
280
  const elements = {
228
281
  items: [] as JSXElement[],
@@ -277,6 +330,7 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
277
330
  // 父子连线
278
331
  if (parent) {
279
332
  const parentBounds = levelBounds.get(parent.depth)!;
333
+ const parentRect = getNodeRect(parent, parentBounds, offsets);
280
334
 
281
335
  // 计算父节点的子节点数量和当前节点在兄弟中的索引
282
336
  const siblings = allNodes.filter((n) => n.parent === parent);
@@ -285,34 +339,77 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
285
339
 
286
340
  // 计算连接线起点
287
341
  let parentX: number;
342
+ let parentY: number;
288
343
  if (edgeOrigin === 'distributed' && siblingCount > 1) {
289
344
  // 分布式起点:根据子节点数量分配起点位置
290
- const startX =
291
- parent.x + offsets.x - parentBounds.width / 2 + edgeOriginPadding;
292
- const endX =
293
- parent.x + offsets.x + parentBounds.width / 2 - edgeOriginPadding;
294
- const segmentWidth = (endX - startX) / siblingCount;
295
- parentX = startX + segmentWidth * siblingIndex + segmentWidth / 2;
345
+ if (isHorizontal) {
346
+ const padding = distributedPadding(
347
+ edgeOriginPadding,
348
+ parentBounds.height,
349
+ );
350
+ const startY = parentRect.top + padding;
351
+ const endY = parentRect.bottom - padding;
352
+ const segmentHeight = (endY - startY) / siblingCount;
353
+ parentY = startY + segmentHeight * siblingIndex + segmentHeight / 2;
354
+ parentX =
355
+ (mainSign > 0 ? parentRect.right : parentRect.left) +
356
+ edgeOffset * mainSign;
357
+ } else {
358
+ const padding = distributedPadding(
359
+ edgeOriginPadding,
360
+ parentBounds.width,
361
+ );
362
+ const startX = parentRect.left + padding;
363
+ const endX = parentRect.right - padding;
364
+ const segmentWidth = (endX - startX) / siblingCount;
365
+ parentX = startX + segmentWidth * siblingIndex + segmentWidth / 2;
366
+ parentY =
367
+ (mainSign > 0 ? parentRect.bottom : parentRect.top) +
368
+ edgeOffset * mainSign;
369
+ }
296
370
  } else {
297
371
  // 中心起点:所有线从节点中心出发
298
- parentX = parent.x + offsets.x;
372
+ parentX = isHorizontal
373
+ ? (mainSign > 0 ? parentRect.right : parentRect.left) +
374
+ edgeOffset * mainSign
375
+ : parentRect.centerX;
376
+ parentY = isHorizontal
377
+ ? parentRect.centerY
378
+ : (mainSign > 0 ? parentRect.bottom : parentRect.top) +
379
+ edgeOffset * mainSign;
299
380
  }
300
381
 
301
- const parentY = parent.y + offsets.y + parentBounds.height + edgeOffset;
302
- const childX = x + offsets.x;
303
- let childY = y + offsets.y - edgeOffset;
382
+ const baseChildX = isHorizontal
383
+ ? (mainSign > 0 ? nodeRect.left : nodeRect.right) -
384
+ edgeOffset * mainSign
385
+ : nodeRect.centerX;
386
+ const baseChildY = isHorizontal
387
+ ? nodeRect.centerY
388
+ : (mainSign > 0 ? nodeRect.top : nodeRect.bottom) -
389
+ edgeOffset * mainSign;
390
+ let childX = baseChildX;
391
+ let childY = baseChildY;
304
392
 
305
393
  // 调整终点位置(为箭头留出空间)
306
394
  if (edgeMarker === 'arrow') {
307
- childY -= markerSize;
395
+ if (isHorizontal) {
396
+ childX -= markerSize * mainSign;
397
+ } else {
398
+ childY -= markerSize * mainSign;
399
+ }
308
400
  }
309
401
 
310
402
  // 生成路径
311
403
  let pathD: string;
312
404
  if (edgeType === 'curved') {
313
405
  // 使用贝塞尔曲线绘制曲线连接
314
- const midY = parentY + (childY - parentY) / 2;
315
- pathD = `M ${parentX} ${parentY} C ${parentX} ${midY}, ${childX} ${midY}, ${childX} ${childY}`;
406
+ if (isHorizontal) {
407
+ const midX = parentX + (childX - parentX) / 2;
408
+ pathD = `M ${parentX} ${parentY} C ${midX} ${parentY}, ${midX} ${childY}, ${childX} ${childY}`;
409
+ } else {
410
+ const midY = parentY + (childY - parentY) / 2;
411
+ pathD = `M ${parentX} ${parentY} C ${parentX} ${midY}, ${childX} ${midY}, ${childX} ${childY}`;
412
+ }
316
413
  } else if (edgeCornerRadius > 0) {
317
414
  // 使用圆角路径
318
415
  pathD = createRoundedPath(
@@ -321,11 +418,17 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
321
418
  childX,
322
419
  childY,
323
420
  edgeCornerRadius,
421
+ isHorizontal ? 'horizontal' : 'vertical',
324
422
  );
325
423
  } else {
326
424
  // 使用直角折线
327
- const midY = parentY + (childY - parentY) / 2;
328
- pathD = `M ${parentX} ${parentY} L ${parentX} ${midY} L ${childX} ${midY} L ${childX} ${childY}`;
425
+ if (isHorizontal) {
426
+ const midX = parentX + (childX - parentX) / 2;
427
+ pathD = `M ${parentX} ${parentY} L ${midX} ${parentY} L ${midX} ${childY} L ${childX} ${childY}`;
428
+ } else {
429
+ const midY = parentY + (childY - parentY) / 2;
430
+ pathD = `M ${parentX} ${parentY} L ${parentX} ${midY} L ${childX} ${midY} L ${childX} ${childY}`;
431
+ }
329
432
  }
330
433
 
331
434
  // 确定连接线颜色
@@ -389,17 +492,29 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
389
492
  : getColorPrimary(options);
390
493
 
391
494
  // 三角形箭头
392
- const arrowPoints = [
393
- { x: childX, y: y + offsets.y - edgeOffset },
394
- {
395
- x: childX - markerSize / 2,
396
- y: y + offsets.y - edgeOffset - markerSize,
397
- },
398
- {
399
- x: childX + markerSize / 2,
400
- y: y + offsets.y - edgeOffset - markerSize,
401
- },
402
- ];
495
+ const arrowPoints = isHorizontal
496
+ ? [
497
+ { x: baseChildX, y: baseChildY },
498
+ {
499
+ x: baseChildX - markerSize * mainSign,
500
+ y: baseChildY - markerSize / 2,
501
+ },
502
+ {
503
+ x: baseChildX - markerSize * mainSign,
504
+ y: baseChildY + markerSize / 2,
505
+ },
506
+ ]
507
+ : [
508
+ { x: baseChildX, y: baseChildY },
509
+ {
510
+ x: baseChildX - markerSize / 2,
511
+ y: baseChildY - markerSize * mainSign,
512
+ },
513
+ {
514
+ x: baseChildX + markerSize / 2,
515
+ y: baseChildY - markerSize * mainSign,
516
+ },
517
+ ];
403
518
 
404
519
  elements.deco.push(
405
520
  <Polygon
@@ -429,13 +544,19 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
429
544
  // 父节点连接点
430
545
  elements.deco.push(
431
546
  <Ellipse
432
- x={parentX - markerSize}
547
+ x={
548
+ (isHorizontal
549
+ ? mainSign > 0
550
+ ? parentRect.right + edgeOffset
551
+ : parentRect.left - edgeOffset
552
+ : parentX) - markerSize
553
+ }
433
554
  y={
434
- parent.y +
435
- offsets.y +
436
- parentBounds.height +
437
- edgeOffset -
438
- markerSize
555
+ (isHorizontal
556
+ ? parentY
557
+ : mainSign > 0
558
+ ? parentRect.bottom + edgeOffset
559
+ : parentRect.top - edgeOffset) - markerSize
439
560
  }
440
561
  width={markerSize * 2}
441
562
  height={markerSize * 2}
@@ -450,8 +571,8 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
450
571
 
451
572
  elements.deco.push(
452
573
  <Ellipse
453
- x={childX - markerSize}
454
- y={y + offsets.y - edgeOffset - markerSize}
574
+ x={baseChildX - markerSize}
575
+ y={baseChildY - markerSize}
455
576
  width={markerSize * 2}
456
577
  height={markerSize * 2}
457
578
  fill={childDotColor}
@@ -484,22 +605,47 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
484
605
  nodesByParent.forEach((siblings) => {
485
606
  if (siblings.length <= 1) return;
486
607
 
487
- const sorted = siblings.slice().sort((a, b) => a.x - b.x);
488
- const siblingY = sorted[0].y + offsets.y - btnBounds.height - 5;
608
+ const sorted = siblings
609
+ .slice()
610
+ .sort((a, b) =>
611
+ isHorizontal
612
+ ? getLayoutPoint(a).y - getLayoutPoint(b).y
613
+ : getLayoutPoint(a).x - getLayoutPoint(b).x,
614
+ );
615
+ if (sorted.length === 0) return;
489
616
 
490
617
  for (let i = 0; i < sorted.length - 1; i++) {
491
- const btnX =
492
- (sorted[i].x + sorted[i + 1].x) / 2 + offsets.x - btnBounds.width / 2;
618
+ const current = getLayoutPoint(sorted[i]);
619
+ const next = getLayoutPoint(sorted[i + 1]);
493
620
  const parentIndexes = sorted[i].data._originalIndex.slice(0, -1);
494
621
  const insertIndex = sorted[i].data._originalIndex.at(-1)! + 1;
495
622
 
496
- btns.push(
497
- <BtnAdd
498
- indexes={[...parentIndexes, insertIndex]}
499
- x={btnX}
500
- y={siblingY}
501
- />,
502
- );
623
+ if (isHorizontal) {
624
+ const btnX =
625
+ current.x +
626
+ offsets.x +
627
+ (mainSign > 0 ? -btnBounds.width - 5 : btnBounds.width + 5);
628
+ const btnY =
629
+ (current.y + next.y) / 2 + offsets.y - btnBounds.height / 2;
630
+ btns.push(
631
+ <BtnAdd
632
+ indexes={[...parentIndexes, insertIndex]}
633
+ x={btnX}
634
+ y={btnY}
635
+ />,
636
+ );
637
+ } else {
638
+ const siblingY = current.y + offsets.y - btnBounds.height - 5;
639
+ const btnX =
640
+ (current.x + next.x) / 2 + offsets.x - btnBounds.width / 2;
641
+ btns.push(
642
+ <BtnAdd
643
+ indexes={[...parentIndexes, insertIndex]}
644
+ x={btnX}
645
+ y={siblingY}
646
+ />,
647
+ );
648
+ }
503
649
  }
504
650
  });
505
651
 
@@ -541,13 +687,18 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
541
687
 
542
688
  const treeLayout = d3
543
689
  .tree<any>()
544
- .nodeSize([maxWidth + nodeGap, maxHeight + levelGap])
690
+ .nodeSize(
691
+ isHorizontal
692
+ ? [maxHeight + nodeGap, maxWidth + levelGap]
693
+ : [maxWidth + nodeGap, maxHeight + levelGap],
694
+ )
545
695
  .separation(() => 1);
546
696
  const nodes = treeLayout(root).descendants();
547
697
 
548
698
  // 计算偏移量
549
- const minX = Math.min(...nodes.map((d) => d.x));
550
- const minY = Math.min(...nodes.map((d) => d.y));
699
+ const layoutPoints = nodes.map((d) => getLayoutPoint(d));
700
+ const minX = Math.min(...layoutPoints.map((d) => d.x));
701
+ const minY = Math.min(...layoutPoints.map((d) => d.y));
551
702
  const offsets = {
552
703
  x: Math.max(0, -minX + maxWidth / 2),
553
704
  y: Math.max(0, -minY + btnBounds.height + 10),
@@ -563,6 +714,8 @@ export const HierarchyTree: ComponentType<HierarchyTreeProps> = (props) => {
563
714
  nodes.forEach((node, index) => {
564
715
  // 将扁平索引附加到节点数据上,用于 node-flat 模式
565
716
  node.data._flatIndex = index;
717
+ const { x, y } = getLayoutPoint(node);
718
+ (node as any).__layout = { x, y };
566
719
  });
567
720
 
568
721
  nodes.forEach((node) => {