@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.
- package/README.md +54 -40
- package/README.zh-CN.md +52 -36
- package/dist/infographic.min.js +110 -105
- package/dist/infographic.min.js.map +1 -1
- package/esm/constants/element.d.ts +1 -1
- package/esm/constants/index.d.ts +1 -0
- package/esm/constants/index.js +1 -0
- package/esm/constants/service.d.ts +1 -0
- package/esm/constants/service.js +1 -0
- package/esm/designs/components/Illus.js +1 -1
- package/esm/designs/structures/chart-wordcloud.d.ts +11 -0
- package/esm/designs/structures/chart-wordcloud.js +156 -0
- package/esm/designs/structures/hierarchy-tree.d.ts +2 -0
- package/esm/designs/structures/hierarchy-tree.js +179 -50
- package/esm/designs/structures/index.d.ts +2 -0
- package/esm/designs/structures/index.js +2 -0
- package/esm/designs/structures/sequence-stairs-front.d.ts +8 -0
- package/esm/designs/structures/sequence-stairs-front.js +116 -0
- package/esm/designs/types.d.ts +8 -0
- package/esm/editor/managers/state.js +1 -1
- package/esm/exporter/font.js +4 -9
- package/esm/index.d.ts +2 -0
- package/esm/index.js +1 -0
- package/esm/options/parser.d.ts +1 -1
- package/esm/options/parser.js +33 -15
- package/esm/renderer/composites/icon.js +1 -1
- package/esm/renderer/composites/illus.js +1 -1
- package/esm/resource/loader.d.ts +2 -2
- package/esm/resource/loader.js +22 -11
- package/esm/resource/loaders/index.d.ts +1 -0
- package/esm/resource/loaders/index.js +1 -0
- package/esm/resource/loaders/remote.d.ts +1 -1
- package/esm/resource/loaders/remote.js +12 -3
- package/esm/resource/loaders/search.d.ts +1 -0
- package/esm/resource/loaders/search.js +52 -0
- package/esm/resource/types/index.d.ts +1 -0
- package/esm/resource/types/resource.d.ts +8 -1
- package/esm/resource/types/scene.d.ts +1 -0
- package/esm/resource/utils/data-uri.js +20 -11
- package/esm/resource/utils/parser.js +92 -1
- package/esm/resource/utils/ref.js +2 -2
- package/esm/runtime/Infographic.d.ts +10 -6
- package/esm/runtime/Infographic.js +66 -17
- package/esm/runtime/utils.d.ts +4 -2
- package/esm/runtime/utils.js +33 -13
- package/esm/syntax/index.d.ts +3 -0
- package/esm/syntax/index.js +101 -0
- package/esm/syntax/mapper.d.ts +3 -0
- package/esm/syntax/mapper.js +334 -0
- package/esm/syntax/parser.d.ts +14 -0
- package/esm/syntax/parser.js +142 -0
- package/esm/syntax/schema.d.ts +6 -0
- package/esm/syntax/schema.js +86 -0
- package/esm/syntax/types.d.ts +68 -0
- package/esm/syntax/types.js +1 -0
- package/esm/templates/built-in.js +4 -0
- package/esm/templates/hierarchy-tree.js +25 -11
- package/esm/templates/sequence-stairs.d.ts +2 -0
- package/esm/templates/sequence-stairs.js +42 -0
- package/esm/templates/word-cloud.d.ts +2 -0
- package/esm/templates/word-cloud.js +19 -0
- package/esm/themes/types.d.ts +1 -1
- package/esm/utils/design.d.ts +2 -0
- package/esm/utils/design.js +10 -0
- package/esm/utils/fetch.d.ts +1 -0
- package/esm/utils/fetch.js +61 -0
- package/esm/utils/font.js +11 -1
- package/esm/utils/index.d.ts +2 -0
- package/esm/utils/index.js +2 -0
- package/lib/constants/element.d.ts +1 -1
- package/lib/constants/index.d.ts +1 -0
- package/lib/constants/index.js +1 -0
- package/lib/constants/service.d.ts +1 -0
- package/lib/constants/service.js +4 -0
- package/lib/designs/components/Illus.js +1 -1
- package/lib/designs/structures/chart-wordcloud.d.ts +11 -0
- package/lib/designs/structures/chart-wordcloud.js +160 -0
- package/lib/designs/structures/hierarchy-tree.d.ts +2 -0
- package/lib/designs/structures/hierarchy-tree.js +179 -50
- package/lib/designs/structures/index.d.ts +2 -0
- package/lib/designs/structures/index.js +2 -0
- package/lib/designs/structures/sequence-stairs-front.d.ts +8 -0
- package/lib/designs/structures/sequence-stairs-front.js +120 -0
- package/lib/designs/types.d.ts +8 -0
- package/lib/editor/managers/state.js +1 -1
- package/lib/exporter/font.js +3 -8
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -1
- package/lib/options/parser.d.ts +1 -1
- package/lib/options/parser.js +32 -14
- package/lib/renderer/composites/icon.js +1 -1
- package/lib/renderer/composites/illus.js +1 -1
- package/lib/resource/loader.d.ts +2 -2
- package/lib/resource/loader.js +21 -10
- package/lib/resource/loaders/index.d.ts +1 -0
- package/lib/resource/loaders/index.js +1 -0
- package/lib/resource/loaders/remote.d.ts +1 -1
- package/lib/resource/loaders/remote.js +12 -3
- package/lib/resource/loaders/search.d.ts +1 -0
- package/lib/resource/loaders/search.js +55 -0
- package/lib/resource/types/index.d.ts +1 -0
- package/lib/resource/types/resource.d.ts +8 -1
- package/lib/resource/types/scene.d.ts +1 -0
- package/lib/resource/utils/data-uri.js +20 -11
- package/lib/resource/utils/parser.js +92 -1
- package/lib/resource/utils/ref.js +2 -2
- package/lib/runtime/Infographic.d.ts +10 -6
- package/lib/runtime/Infographic.js +65 -16
- package/lib/runtime/utils.d.ts +4 -2
- package/lib/runtime/utils.js +35 -13
- package/lib/syntax/index.d.ts +3 -0
- package/lib/syntax/index.js +104 -0
- package/lib/syntax/mapper.d.ts +3 -0
- package/lib/syntax/mapper.js +341 -0
- package/lib/syntax/parser.d.ts +14 -0
- package/lib/syntax/parser.js +146 -0
- package/lib/syntax/schema.d.ts +6 -0
- package/lib/syntax/schema.js +89 -0
- package/lib/syntax/types.d.ts +68 -0
- package/lib/syntax/types.js +2 -0
- package/lib/templates/built-in.js +4 -0
- package/lib/templates/hierarchy-tree.js +25 -11
- package/lib/templates/sequence-stairs.d.ts +2 -0
- package/lib/templates/sequence-stairs.js +45 -0
- package/lib/templates/word-cloud.d.ts +2 -0
- package/lib/templates/word-cloud.js +22 -0
- package/lib/themes/types.d.ts +1 -1
- package/lib/utils/design.d.ts +2 -0
- package/lib/utils/design.js +13 -0
- package/lib/utils/fetch.d.ts +1 -0
- package/lib/utils/fetch.js +67 -0
- package/lib/utils/font.js +11 -1
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/index.js +2 -0
- package/package.json +14 -2
- package/src/constants/element.ts +1 -1
- package/src/constants/index.ts +1 -0
- package/src/constants/service.ts +1 -0
- package/src/designs/components/Illus.tsx +1 -1
- package/src/designs/structures/chart-wordcloud.tsx +278 -0
- package/src/designs/structures/hierarchy-tree.tsx +212 -59
- package/src/designs/structures/index.ts +2 -0
- package/src/designs/structures/sequence-stairs-front.tsx +291 -0
- package/src/designs/types.ts +9 -0
- package/src/editor/managers/state.ts +1 -1
- package/src/exporter/font.ts +4 -9
- package/src/index.ts +2 -0
- package/src/options/parser.ts +57 -28
- package/src/renderer/composites/icon.ts +1 -1
- package/src/renderer/composites/illus.ts +1 -1
- package/src/resource/loader.ts +22 -8
- package/src/resource/loaders/index.ts +1 -0
- package/src/resource/loaders/remote.ts +11 -3
- package/src/resource/loaders/search.ts +53 -0
- package/src/resource/types/index.ts +2 -1
- package/src/resource/types/resource.ts +12 -1
- package/src/resource/types/scene.ts +1 -0
- package/src/resource/utils/data-uri.ts +20 -11
- package/src/resource/utils/parser.ts +103 -2
- package/src/resource/utils/ref.ts +2 -2
- package/src/runtime/Infographic.tsx +103 -22
- package/src/runtime/utils.ts +38 -16
- package/src/syntax/index.ts +124 -0
- package/src/syntax/mapper.ts +496 -0
- package/src/syntax/parser.ts +171 -0
- package/src/syntax/schema.ts +112 -0
- package/src/syntax/types.ts +100 -0
- package/src/templates/built-in.ts +4 -0
- package/src/templates/hierarchy-tree.ts +34 -11
- package/src/templates/sequence-stairs.ts +44 -0
- package/src/templates/word-cloud.ts +21 -0
- package/src/themes/types.ts +1 -1
- package/src/utils/design.ts +14 -0
- package/src/utils/fetch.ts +90 -0
- package/src/utils/font.ts +11 -1
- package/src/utils/index.ts +2 -0
- package/esm/resource/types/font.d.ts +0 -12
- package/lib/resource/types/font.d.ts +0 -12
- package/src/resource/types/font.ts +0 -23
- /package/esm/resource/types/{font.js → scene.js} +0 -0
- /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
|
|
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(
|
|
127
|
-
Math.abs(
|
|
145
|
+
Math.abs(deltaMain) / 2,
|
|
146
|
+
Math.abs(deltaCross) / 2,
|
|
128
147
|
);
|
|
129
148
|
|
|
130
149
|
if (effectiveRadius === 0) {
|
|
131
|
-
return
|
|
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 ${
|
|
136
|
-
Q ${
|
|
137
|
-
L ${
|
|
138
|
-
Q ${
|
|
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 {
|
|
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
|
|
225
|
-
const
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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 =
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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={
|
|
547
|
+
x={
|
|
548
|
+
(isHorizontal
|
|
549
|
+
? mainSign > 0
|
|
550
|
+
? parentRect.right + edgeOffset
|
|
551
|
+
: parentRect.left - edgeOffset
|
|
552
|
+
: parentX) - markerSize
|
|
553
|
+
}
|
|
433
554
|
y={
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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={
|
|
454
|
-
y={
|
|
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
|
|
488
|
-
|
|
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
|
|
492
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
x
|
|
500
|
-
|
|
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(
|
|
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
|
|
550
|
-
const
|
|
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) => {
|