@antv/infographic 0.2.6 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/infographic.min.js +45 -45
- package/dist/infographic.min.js.map +1 -1
- package/esm/designs/items/HorizontalIconArrow.js +2 -2
- package/esm/designs/structures/index.d.ts +1 -0
- package/esm/designs/structures/index.js +1 -0
- package/esm/designs/structures/sequence-funnel.d.ts +10 -0
- package/esm/designs/structures/sequence-funnel.js +110 -0
- package/esm/options/types.d.ts +2 -0
- package/esm/renderer/composites/background.d.ts +2 -1
- package/esm/renderer/composites/background.js +6 -4
- package/esm/renderer/fonts/loader.js +2 -2
- package/esm/renderer/renderer.js +18 -9
- package/esm/resource/loaders/svg.js +6 -4
- package/esm/templates/built-in.js +10 -0
- package/esm/utils/padding.js +19 -14
- package/lib/designs/items/HorizontalIconArrow.js +1 -1
- package/lib/designs/structures/index.d.ts +1 -0
- package/lib/designs/structures/index.js +1 -0
- package/lib/designs/structures/sequence-funnel.d.ts +10 -0
- package/lib/designs/structures/sequence-funnel.js +150 -0
- package/lib/options/types.d.ts +2 -0
- package/lib/renderer/composites/background.d.ts +2 -1
- package/lib/renderer/composites/background.js +6 -4
- package/lib/renderer/fonts/loader.js +2 -2
- package/lib/renderer/renderer.js +18 -9
- package/lib/resource/loaders/svg.js +6 -4
- package/lib/templates/built-in.js +10 -0
- package/lib/utils/padding.js +19 -14
- package/package.json +2 -2
- package/src/designs/items/HorizontalIconArrow.tsx +10 -5
- package/src/designs/structures/index.ts +1 -0
- package/src/designs/structures/sequence-funnel.tsx +260 -0
- package/src/options/types.ts +2 -0
- package/src/renderer/composites/background.ts +8 -5
- package/src/renderer/fonts/loader.ts +2 -2
- package/src/renderer/renderer.ts +18 -9
- package/src/resource/loaders/svg.ts +8 -4
- package/src/templates/built-in.ts +10 -0
- package/src/utils/padding.ts +18 -14
package/lib/renderer/renderer.js
CHANGED
|
@@ -30,24 +30,34 @@ class Renderer {
|
|
|
30
30
|
return svg;
|
|
31
31
|
renderTemplate(svg, this.options);
|
|
32
32
|
svg.style.visibility = 'hidden';
|
|
33
|
+
const postRender = () => {
|
|
34
|
+
setView(this.template, this.options);
|
|
35
|
+
(0, fonts_1.loadFonts)(this.template);
|
|
36
|
+
svg.style.visibility = '';
|
|
37
|
+
};
|
|
33
38
|
const observer = new MutationObserver((mutations) => {
|
|
34
39
|
mutations.forEach((mutation) => {
|
|
35
40
|
mutation.addedNodes.forEach((node) => {
|
|
36
41
|
if (node === svg || node.contains(svg)) {
|
|
37
42
|
// post render
|
|
38
|
-
|
|
39
|
-
(0, fonts_1.loadFonts)(this.template);
|
|
43
|
+
postRender();
|
|
40
44
|
// disconnect observer
|
|
41
45
|
observer.disconnect();
|
|
42
|
-
svg.style.visibility = '';
|
|
43
46
|
}
|
|
44
47
|
});
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
try {
|
|
51
|
+
observer.observe(document, {
|
|
52
|
+
childList: true,
|
|
53
|
+
subtree: true,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
// Fallback for micro-app environments that proxy document.
|
|
58
|
+
postRender();
|
|
59
|
+
console.error(error);
|
|
60
|
+
}
|
|
51
61
|
this.rendered = true;
|
|
52
62
|
return svg;
|
|
53
63
|
}
|
|
@@ -56,8 +66,7 @@ exports.Renderer = Renderer;
|
|
|
56
66
|
function renderTemplate(svg, options) {
|
|
57
67
|
fill(svg, options);
|
|
58
68
|
setSVG(svg, options);
|
|
59
|
-
|
|
60
|
-
(0, composites_1.renderBackground)(svg, themeConfig?.colorBg);
|
|
69
|
+
(0, composites_1.renderBackground)(svg, options);
|
|
61
70
|
}
|
|
62
71
|
function fill(svg, options) {
|
|
63
72
|
const { themeConfig, data } = options;
|
|
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.loadSVGResource = loadSVGResource;
|
|
4
4
|
const utils_1 = require("../../utils");
|
|
5
5
|
function isSVGResource(resource) {
|
|
6
|
-
|
|
6
|
+
const trimmedResource = resource.trim();
|
|
7
|
+
return (/^(?:<\?xml[^>]*>\s*)?<svg[\s>]/i.test(trimmedResource) ||
|
|
8
|
+
trimmedResource.startsWith('<symbol'));
|
|
7
9
|
}
|
|
8
10
|
function loadSVGResource(data) {
|
|
9
11
|
if (!data || !isSVGResource(data))
|
|
10
12
|
return null;
|
|
11
|
-
const str = data
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const str = data
|
|
14
|
+
.replace(/<svg(?=[\s/>])/i, '<symbol')
|
|
15
|
+
.replace(/<\/svg>/i, '</symbol>');
|
|
14
16
|
return (0, utils_1.parseSVG)(str);
|
|
15
17
|
}
|
|
@@ -310,6 +310,16 @@ const BUILT_IN_TEMPLATES = {
|
|
|
310
310
|
colorPrimary: '#1677ff',
|
|
311
311
|
},
|
|
312
312
|
},
|
|
313
|
+
'sequence-funnel-simple': {
|
|
314
|
+
design: {
|
|
315
|
+
title: 'default',
|
|
316
|
+
structure: { type: 'sequence-funnel' },
|
|
317
|
+
items: [{ type: 'simple', showIcon: false, usePaletteColor: true }],
|
|
318
|
+
},
|
|
319
|
+
themeConfig: {
|
|
320
|
+
palette: '#1677ff',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
313
323
|
'list-row-horizontal-icon-line': {
|
|
314
324
|
design: {
|
|
315
325
|
title: 'default',
|
package/lib/utils/padding.js
CHANGED
|
@@ -34,22 +34,27 @@ function setSVGPadding(svg, padding) {
|
|
|
34
34
|
setSVGPaddingInBrowser(svg, padding);
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
try {
|
|
38
|
+
const observer = new MutationObserver((mutations) => {
|
|
39
|
+
mutations.forEach((mutation) => {
|
|
40
|
+
mutation.addedNodes.forEach((node) => {
|
|
41
|
+
if (node === svg || node.contains(svg)) {
|
|
42
|
+
waitForLayout(svg, () => {
|
|
43
|
+
setSVGPaddingInBrowser(svg, padding);
|
|
44
|
+
});
|
|
45
|
+
observer.disconnect();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
46
48
|
});
|
|
47
49
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
50
|
+
observer.observe(document, {
|
|
51
|
+
childList: true,
|
|
52
|
+
subtree: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
setSVGPaddingInNode(svg, padding);
|
|
57
|
+
}
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antv/infographic",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "An Infographic Generation and Rendering Framework, bring words to life!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antv",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"css": "^3.0.0",
|
|
92
92
|
"culori": "^4.0.2",
|
|
93
93
|
"d3": "^7.9.0",
|
|
94
|
+
"eventemitter3": "^5.0.1",
|
|
94
95
|
"flru": "^1.0.2",
|
|
95
96
|
"lodash-es": "^4.17.21",
|
|
96
97
|
"measury": "^0.1.3",
|
|
@@ -116,7 +117,6 @@
|
|
|
116
117
|
"any-skills": "^0.1.1",
|
|
117
118
|
"csstype": "^3.2.2",
|
|
118
119
|
"eslint": "^9.35.0",
|
|
119
|
-
"eventemitter3": "^5.0.1",
|
|
120
120
|
"globals": "^16.4.0",
|
|
121
121
|
"gzip-size": "^7.0.0",
|
|
122
122
|
"husky": "^9.1.7",
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
ItemLabel,
|
|
16
16
|
ShapesGroup,
|
|
17
17
|
} from '../components';
|
|
18
|
-
import { FlexLayout } from '../layouts';
|
|
18
|
+
import { AlignLayout, FlexLayout } from '../layouts';
|
|
19
19
|
import { getItemProps } from '../utils';
|
|
20
20
|
import { registerItem } from './registry';
|
|
21
21
|
import type { BaseItemProps } from './types';
|
|
@@ -114,15 +114,20 @@ export const HorizontalIconArrow: ComponentType<HorizontalIconArrowProps> = (
|
|
|
114
114
|
<Gap height={iconGap} />
|
|
115
115
|
</>
|
|
116
116
|
)}
|
|
117
|
-
<
|
|
117
|
+
<AlignLayout
|
|
118
|
+
horizontal="center"
|
|
119
|
+
vertical="middle"
|
|
120
|
+
width={width}
|
|
121
|
+
height={arrowHeight}
|
|
122
|
+
>
|
|
118
123
|
<HorizontalArrow
|
|
119
124
|
width={width}
|
|
120
125
|
height={arrowHeight}
|
|
121
126
|
fill={themeColors.colorPrimary}
|
|
122
127
|
/>
|
|
123
128
|
<Text
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
width={width}
|
|
130
|
+
height={arrowHeight}
|
|
126
131
|
alignHorizontal="center"
|
|
127
132
|
alignVertical="middle"
|
|
128
133
|
fill={themeColors.colorWhite}
|
|
@@ -135,7 +140,7 @@ export const HorizontalIconArrow: ComponentType<HorizontalIconArrowProps> = (
|
|
|
135
140
|
.padStart(2, '0')
|
|
136
141
|
.slice(-2)}
|
|
137
142
|
</Text>
|
|
138
|
-
</
|
|
143
|
+
</AlignLayout>
|
|
139
144
|
{!isVNormal ? (
|
|
140
145
|
<>
|
|
141
146
|
{dotLine}
|
|
@@ -28,6 +28,7 @@ export * from './sequence-circular';
|
|
|
28
28
|
export * from './sequence-color-snake-steps';
|
|
29
29
|
export * from './sequence-cylinders-3d';
|
|
30
30
|
export * from './sequence-filter-mesh';
|
|
31
|
+
export * from './sequence-funnel';
|
|
31
32
|
export * from './sequence-horizontal-zigzag';
|
|
32
33
|
export * from './sequence-mountain';
|
|
33
34
|
export * from './sequence-pyramid';
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 序列漏斗结构(SequenceFunnel)
|
|
3
|
+
* 用途:
|
|
4
|
+
* - 在左侧渲染分层漏斗形状(倒置梯形堆叠),右侧渲染对应的 item 卡片与图标
|
|
5
|
+
* - 形状上宽下窄,底部平滑(梯形),卡片背景插入漏斗下方
|
|
6
|
+
*/
|
|
7
|
+
import roundPolygon, { getSegments } from 'round-polygon';
|
|
8
|
+
import tinycolor from 'tinycolor2';
|
|
9
|
+
import type { ComponentType } from '../../jsx';
|
|
10
|
+
import { Defs, Group, Point, Polygon, Rect } from '../../jsx';
|
|
11
|
+
import {
|
|
12
|
+
BtnAdd,
|
|
13
|
+
BtnRemove,
|
|
14
|
+
BtnsGroup,
|
|
15
|
+
ItemIcon,
|
|
16
|
+
ItemsGroup,
|
|
17
|
+
} from '../components';
|
|
18
|
+
import { FlexLayout } from '../layouts';
|
|
19
|
+
import { getPaletteColor, getThemeColors } from '../utils';
|
|
20
|
+
import { registerStructure } from './registry';
|
|
21
|
+
import type { BaseStructureProps } from './types';
|
|
22
|
+
|
|
23
|
+
// Constants
|
|
24
|
+
const FUNNEL_CORNER_RADIUS = 6;
|
|
25
|
+
const ICON_SIZE = 32;
|
|
26
|
+
const FUNNEL_LAYER_HEIGHT_RATIO = 1.25;
|
|
27
|
+
const OVERLAP_DIST = 25;
|
|
28
|
+
const TEXT_GAP = 15;
|
|
29
|
+
|
|
30
|
+
// SequenceFunnel 的可配置属性
|
|
31
|
+
export interface SequenceFunnelProps extends BaseStructureProps {
|
|
32
|
+
gap?: number;
|
|
33
|
+
width?: number;
|
|
34
|
+
funnelWidth?: number;
|
|
35
|
+
itemHeight?: number;
|
|
36
|
+
// 新增:底部宽度比例(0~1),控制漏斗底部的收窄程度,避免变成尖角
|
|
37
|
+
// 默认为 0.25 (即底部宽度是顶部的 25%)
|
|
38
|
+
minBottomRatio?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const SequenceFunnel: ComponentType<SequenceFunnelProps> = (props) => {
|
|
42
|
+
const {
|
|
43
|
+
Title,
|
|
44
|
+
Item,
|
|
45
|
+
data,
|
|
46
|
+
gap = 10,
|
|
47
|
+
width = 700,
|
|
48
|
+
funnelWidth,
|
|
49
|
+
itemHeight = 60,
|
|
50
|
+
minBottomRatio = 0.25, // 默认底部保留 25% 的宽度,形成梯形
|
|
51
|
+
options,
|
|
52
|
+
} = props;
|
|
53
|
+
|
|
54
|
+
const { title, desc, items = [] } = data;
|
|
55
|
+
|
|
56
|
+
const titleContent = Title ? <Title title={title} desc={desc} /> : null;
|
|
57
|
+
|
|
58
|
+
if (items.length === 0) {
|
|
59
|
+
return (
|
|
60
|
+
<FlexLayout
|
|
61
|
+
id="infographic-container"
|
|
62
|
+
flexDirection="column"
|
|
63
|
+
justifyContent="center"
|
|
64
|
+
alignItems="center"
|
|
65
|
+
>
|
|
66
|
+
{titleContent}
|
|
67
|
+
</FlexLayout>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const themeColors = getThemeColors(options.themeConfig);
|
|
72
|
+
|
|
73
|
+
// 计算各区域尺寸
|
|
74
|
+
const actualFunnelWidth = funnelWidth ?? width * 0.55; // 稍微调窄一点漏斗,给右侧留更多空间
|
|
75
|
+
const itemAreaWidth = width - actualFunnelWidth;
|
|
76
|
+
|
|
77
|
+
// 漏斗层高度
|
|
78
|
+
const funnelLayerHeight = itemHeight * FUNNEL_LAYER_HEIGHT_RATIO;
|
|
79
|
+
const totalHeight =
|
|
80
|
+
items.length * funnelLayerHeight + (items.length - 1) * gap;
|
|
81
|
+
|
|
82
|
+
// 计算底部的最小像素宽度
|
|
83
|
+
const minFunnelPixelWidth = actualFunnelWidth * minBottomRatio;
|
|
84
|
+
|
|
85
|
+
const elements = items.map((item, index) => {
|
|
86
|
+
const indexes = [index];
|
|
87
|
+
// 获取颜色
|
|
88
|
+
const color = getPaletteColor(options, [index]) || themeColors.colorPrimary;
|
|
89
|
+
|
|
90
|
+
// 1. 计算当前层的梯形形状
|
|
91
|
+
// 使用线性插值,从 actualFunnelWidth 收缩到 minFunnelPixelWidth
|
|
92
|
+
const { points, topWidth } = calculateTrapezoidSegment(
|
|
93
|
+
actualFunnelWidth,
|
|
94
|
+
minFunnelPixelWidth,
|
|
95
|
+
funnelLayerHeight,
|
|
96
|
+
gap,
|
|
97
|
+
totalHeight,
|
|
98
|
+
index,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// 圆角处理
|
|
102
|
+
const rounded = roundPolygon(points, FUNNEL_CORNER_RADIUS);
|
|
103
|
+
const segments = getSegments(rounded, 'AMOUNT', 10);
|
|
104
|
+
|
|
105
|
+
// 坐标计算
|
|
106
|
+
const funnelCenterX = actualFunnelWidth / 2;
|
|
107
|
+
const funnelY = index * (funnelLayerHeight + gap);
|
|
108
|
+
|
|
109
|
+
// 2. 背景与 Item 的位置计算
|
|
110
|
+
// 在漏斗(倒梯形)中,顶边(topWidth)总是比底边(bottomWidth)宽
|
|
111
|
+
// 所以右侧边缘的最外点是 topWidth 的一半
|
|
112
|
+
const rightTopX = funnelCenterX + topWidth / 2;
|
|
113
|
+
|
|
114
|
+
// 背景卡片:
|
|
115
|
+
// X 轴起点:从漏斗最宽处向左回缩 overlapDist,形成“插入”效果
|
|
116
|
+
const backgroundX = rightTopX - OVERLAP_DIST;
|
|
117
|
+
// 宽度:填满剩余空间,但要补上左侧回缩的距离
|
|
118
|
+
const backgroundWidth = itemAreaWidth + OVERLAP_DIST - 10; // -10 用于右侧留白
|
|
119
|
+
const backgroundYOffset = (funnelLayerHeight - itemHeight) / 2;
|
|
120
|
+
const backgroundY = funnelY + backgroundYOffset;
|
|
121
|
+
|
|
122
|
+
// 文本内容 (Item):
|
|
123
|
+
// X 轴起点:不应该跟着背景向左缩,而应该在漏斗边缘右侧,避免被漏斗遮挡
|
|
124
|
+
const itemX = rightTopX + TEXT_GAP;
|
|
125
|
+
const itemWidth = backgroundWidth - OVERLAP_DIST - TEXT_GAP;
|
|
126
|
+
const itemY = backgroundY;
|
|
127
|
+
|
|
128
|
+
// 图标位置
|
|
129
|
+
const iconX = funnelCenterX - ICON_SIZE / 2;
|
|
130
|
+
const iconY = funnelY + funnelLayerHeight / 2 - ICON_SIZE / 2;
|
|
131
|
+
|
|
132
|
+
const funnelColorId = `${color.replace('#', '')}-funnel-${index}`;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
background: (
|
|
136
|
+
<Rect
|
|
137
|
+
x={backgroundX}
|
|
138
|
+
y={backgroundY}
|
|
139
|
+
width={backgroundWidth}
|
|
140
|
+
height={itemHeight}
|
|
141
|
+
ry="8" // 背景圆角稍微大一点,显得柔和
|
|
142
|
+
fill={tinycolor(color).setAlpha(0.1).toRgbString()} // 使用当前主题色的浅色背景
|
|
143
|
+
data-element-type="shape"
|
|
144
|
+
/>
|
|
145
|
+
),
|
|
146
|
+
funnel: [
|
|
147
|
+
<Defs>
|
|
148
|
+
<linearGradient id={funnelColorId} x1="0%" y1="0%" x2="100%" y2="0%">
|
|
149
|
+
<stop
|
|
150
|
+
offset="0%"
|
|
151
|
+
stopColor={tinycolor(color).lighten(10).toString()}
|
|
152
|
+
/>
|
|
153
|
+
<stop offset="100%" stopColor={color} />
|
|
154
|
+
</linearGradient>
|
|
155
|
+
</Defs>,
|
|
156
|
+
<Polygon
|
|
157
|
+
points={segments}
|
|
158
|
+
fill={`url(#${funnelColorId})`}
|
|
159
|
+
y={funnelY}
|
|
160
|
+
data-element-type="shape"
|
|
161
|
+
// 添加轻微阴影效果增加层次感(可选,依赖环境支持 filter)
|
|
162
|
+
style={{ filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.15))' }}
|
|
163
|
+
/>,
|
|
164
|
+
],
|
|
165
|
+
icon: (
|
|
166
|
+
<ItemIcon
|
|
167
|
+
indexes={indexes}
|
|
168
|
+
x={iconX}
|
|
169
|
+
y={iconY}
|
|
170
|
+
size={ICON_SIZE}
|
|
171
|
+
fill="#fff"
|
|
172
|
+
/>
|
|
173
|
+
),
|
|
174
|
+
item: (
|
|
175
|
+
<Item
|
|
176
|
+
indexes={indexes}
|
|
177
|
+
datum={item}
|
|
178
|
+
data={data}
|
|
179
|
+
x={itemX}
|
|
180
|
+
y={itemY}
|
|
181
|
+
width={itemWidth}
|
|
182
|
+
height={itemHeight}
|
|
183
|
+
positionV="middle"
|
|
184
|
+
/>
|
|
185
|
+
),
|
|
186
|
+
btnRemove: (
|
|
187
|
+
<BtnRemove
|
|
188
|
+
indexes={indexes}
|
|
189
|
+
x={backgroundX + backgroundWidth}
|
|
190
|
+
y={backgroundY}
|
|
191
|
+
/>
|
|
192
|
+
),
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const btnAdd = (
|
|
197
|
+
<BtnAdd indexes={[items.length]} x={width / 2} y={totalHeight + 10} />
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<FlexLayout
|
|
202
|
+
id="infographic-container"
|
|
203
|
+
flexDirection="column"
|
|
204
|
+
justifyContent="center"
|
|
205
|
+
alignItems="center"
|
|
206
|
+
>
|
|
207
|
+
{titleContent}
|
|
208
|
+
<Group width={width} height={totalHeight + 40}>
|
|
209
|
+
{/* 背景最先渲染,位于底部 */}
|
|
210
|
+
<Group>{elements.map((element) => element.background)}</Group>
|
|
211
|
+
{/* 漏斗覆盖在背景之上 */}
|
|
212
|
+
<Group>{elements.flatMap((element) => element.funnel)}</Group>
|
|
213
|
+
{/* 图标和文字在最上层 */}
|
|
214
|
+
<Group>{elements.map((element) => element.icon)}</Group>
|
|
215
|
+
<ItemsGroup>{elements.map((element) => element.item)}</ItemsGroup>
|
|
216
|
+
<BtnsGroup>
|
|
217
|
+
{elements.map((element) => element.btnRemove)}
|
|
218
|
+
{btnAdd}
|
|
219
|
+
</BtnsGroup>
|
|
220
|
+
</Group>
|
|
221
|
+
</FlexLayout>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// 计算梯形分段逻辑
|
|
226
|
+
function calculateTrapezoidSegment(
|
|
227
|
+
maxWidth: number,
|
|
228
|
+
minWidth: number,
|
|
229
|
+
layerHeight: number,
|
|
230
|
+
gap: number,
|
|
231
|
+
totalHeight: number,
|
|
232
|
+
index: number,
|
|
233
|
+
) {
|
|
234
|
+
const centerX = maxWidth / 2;
|
|
235
|
+
|
|
236
|
+
// 当前层顶部和底部的 Y 坐标(相对于总高度)
|
|
237
|
+
const currentTopY = index * (layerHeight + gap);
|
|
238
|
+
const currentBottomY = currentTopY + layerHeight;
|
|
239
|
+
|
|
240
|
+
// 线性插值计算宽度
|
|
241
|
+
// Width = MaxWidth - (MaxWidth - MinWidth) * (Y / TotalHeight)
|
|
242
|
+
const widthDiff = maxWidth - minWidth;
|
|
243
|
+
|
|
244
|
+
const topWidth = maxWidth - widthDiff * (currentTopY / totalHeight);
|
|
245
|
+
const bottomWidth = maxWidth - widthDiff * (currentBottomY / totalHeight);
|
|
246
|
+
|
|
247
|
+
// 生成四个顶点 (梯形)
|
|
248
|
+
const p1: Point = { x: centerX - topWidth / 2, y: 0 }; // 左上
|
|
249
|
+
const p2: Point = { x: centerX + topWidth / 2, y: 0 }; // 右上
|
|
250
|
+
const p3: Point = { x: centerX + bottomWidth / 2, y: layerHeight }; // 右下
|
|
251
|
+
const p4: Point = { x: centerX - bottomWidth / 2, y: layerHeight }; // 左下
|
|
252
|
+
|
|
253
|
+
return { points: [p1, p2, p3, p4], topWidth, bottomWidth };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 注册
|
|
257
|
+
registerStructure('sequence-funnel', {
|
|
258
|
+
component: SequenceFunnel,
|
|
259
|
+
composites: ['title', 'item'],
|
|
260
|
+
});
|
package/src/options/types.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
+
import type { ParsedInfographicOptions } from '../../options';
|
|
1
2
|
import { createElement, getElementByRole } from '../../utils';
|
|
2
3
|
import { ElementTypeEnum } from '../constants';
|
|
3
4
|
|
|
4
5
|
export function renderBackground(
|
|
5
6
|
svg: SVGSVGElement,
|
|
6
|
-
|
|
7
|
+
options: ParsedInfographicOptions,
|
|
7
8
|
): void {
|
|
9
|
+
if (options.svg?.background === false) return;
|
|
10
|
+
const {
|
|
11
|
+
themeConfig: { colorBg: background },
|
|
12
|
+
} = options;
|
|
13
|
+
if (!background) return;
|
|
8
14
|
const container = svg.parentElement;
|
|
15
|
+
|
|
9
16
|
if (container) container.style.backgroundColor = background || 'none';
|
|
10
17
|
const element = getElementByRole(svg, ElementTypeEnum.Background);
|
|
11
18
|
|
|
12
|
-
if (!background) {
|
|
13
|
-
return element?.remove();
|
|
14
|
-
}
|
|
15
|
-
|
|
16
19
|
svg.style.backgroundColor = background;
|
|
17
20
|
|
|
18
21
|
if (element) {
|
|
@@ -69,7 +69,7 @@ function trackFontPromise(
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function isLinkLoaded(link: HTMLLinkElement): boolean {
|
|
72
|
-
if (link.
|
|
72
|
+
if (link.getAttribute('data-infographic-font-loaded') === 'true') return true;
|
|
73
73
|
try {
|
|
74
74
|
return !!link.sheet;
|
|
75
75
|
} catch {
|
|
@@ -91,7 +91,7 @@ function getFontLoadPromise(
|
|
|
91
91
|
|
|
92
92
|
const promise = new Promise<void>((resolve) => {
|
|
93
93
|
const done = () => {
|
|
94
|
-
link.
|
|
94
|
+
link.setAttribute('data-infographic-font-loaded', 'true');
|
|
95
95
|
resolve();
|
|
96
96
|
};
|
|
97
97
|
link.addEventListener('load', done, { once: true });
|
package/src/renderer/renderer.ts
CHANGED
|
@@ -64,26 +64,36 @@ export class Renderer implements IRenderer {
|
|
|
64
64
|
|
|
65
65
|
renderTemplate(svg, this.options);
|
|
66
66
|
svg.style.visibility = 'hidden';
|
|
67
|
+
const postRender = () => {
|
|
68
|
+
setView(this.template, this.options);
|
|
69
|
+
loadFonts(this.template);
|
|
70
|
+
svg.style.visibility = '';
|
|
71
|
+
};
|
|
72
|
+
|
|
67
73
|
const observer = new MutationObserver((mutations) => {
|
|
68
74
|
mutations.forEach((mutation) => {
|
|
69
75
|
mutation.addedNodes.forEach((node) => {
|
|
70
76
|
if (node === svg || node.contains(svg)) {
|
|
71
77
|
// post render
|
|
72
|
-
|
|
73
|
-
loadFonts(this.template);
|
|
78
|
+
postRender();
|
|
74
79
|
|
|
75
80
|
// disconnect observer
|
|
76
81
|
observer.disconnect();
|
|
77
|
-
svg.style.visibility = '';
|
|
78
82
|
}
|
|
79
83
|
});
|
|
80
84
|
});
|
|
81
85
|
});
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
try {
|
|
88
|
+
observer.observe(document, {
|
|
89
|
+
childList: true,
|
|
90
|
+
subtree: true,
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
// Fallback for micro-app environments that proxy document.
|
|
94
|
+
postRender();
|
|
95
|
+
console.error(error);
|
|
96
|
+
}
|
|
87
97
|
|
|
88
98
|
this.rendered = true;
|
|
89
99
|
return svg;
|
|
@@ -95,8 +105,7 @@ function renderTemplate(svg: SVGSVGElement, options: ParsedInfographicOptions) {
|
|
|
95
105
|
|
|
96
106
|
setSVG(svg, options);
|
|
97
107
|
|
|
98
|
-
|
|
99
|
-
renderBackground(svg, themeConfig?.colorBg);
|
|
108
|
+
renderBackground(svg, options);
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
function fill(svg: SVGSVGElement, options: ParsedInfographicOptions) {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { parseSVG } from '../../utils';
|
|
2
2
|
|
|
3
3
|
function isSVGResource(resource: string): boolean {
|
|
4
|
-
|
|
4
|
+
const trimmedResource = resource.trim();
|
|
5
|
+
return (
|
|
6
|
+
/^(?:<\?xml[^>]*>\s*)?<svg[\s>]/i.test(trimmedResource) ||
|
|
7
|
+
trimmedResource.startsWith('<symbol')
|
|
8
|
+
);
|
|
5
9
|
}
|
|
6
10
|
|
|
7
11
|
export function loadSVGResource(data: string) {
|
|
8
12
|
if (!data || !isSVGResource(data)) return null;
|
|
9
|
-
const str = data
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const str = data
|
|
14
|
+
.replace(/<svg(?=[\s/>])/i, '<symbol')
|
|
15
|
+
.replace(/<\/svg>/i, '</symbol>');
|
|
12
16
|
return parseSVG(str);
|
|
13
17
|
}
|
|
@@ -310,6 +310,16 @@ const BUILT_IN_TEMPLATES: Record<string, TemplateOptions> = {
|
|
|
310
310
|
colorPrimary: '#1677ff',
|
|
311
311
|
},
|
|
312
312
|
},
|
|
313
|
+
'sequence-funnel-simple': {
|
|
314
|
+
design: {
|
|
315
|
+
title: 'default',
|
|
316
|
+
structure: { type: 'sequence-funnel'},
|
|
317
|
+
items: [{ type: 'simple', showIcon: false, usePaletteColor: true}],
|
|
318
|
+
},
|
|
319
|
+
themeConfig: {
|
|
320
|
+
palette: '#1677ff',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
313
323
|
'list-row-horizontal-icon-line': {
|
|
314
324
|
design: {
|
|
315
325
|
title: 'default',
|
package/src/utils/padding.ts
CHANGED
|
@@ -30,24 +30,28 @@ export function setSVGPadding(svg: SVGSVGElement, padding: ParsedPadding) {
|
|
|
30
30
|
if (document.contains(svg)) {
|
|
31
31
|
setSVGPaddingInBrowser(svg, padding);
|
|
32
32
|
} else {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
try {
|
|
34
|
+
const observer = new MutationObserver((mutations) => {
|
|
35
|
+
mutations.forEach((mutation) => {
|
|
36
|
+
mutation.addedNodes.forEach((node) => {
|
|
37
|
+
if (node === svg || node.contains(svg)) {
|
|
38
|
+
waitForLayout(svg, () => {
|
|
39
|
+
setSVGPaddingInBrowser(svg, padding);
|
|
40
|
+
});
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
observer.disconnect();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
43
45
|
});
|
|
44
46
|
});
|
|
45
|
-
});
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
observer.observe(document, {
|
|
49
|
+
childList: true,
|
|
50
|
+
subtree: true,
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
setSVGPaddingInNode(svg, padding);
|
|
54
|
+
}
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
}
|