@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.
Files changed (39) hide show
  1. package/dist/infographic.min.js +45 -45
  2. package/dist/infographic.min.js.map +1 -1
  3. package/esm/designs/items/HorizontalIconArrow.js +2 -2
  4. package/esm/designs/structures/index.d.ts +1 -0
  5. package/esm/designs/structures/index.js +1 -0
  6. package/esm/designs/structures/sequence-funnel.d.ts +10 -0
  7. package/esm/designs/structures/sequence-funnel.js +110 -0
  8. package/esm/options/types.d.ts +2 -0
  9. package/esm/renderer/composites/background.d.ts +2 -1
  10. package/esm/renderer/composites/background.js +6 -4
  11. package/esm/renderer/fonts/loader.js +2 -2
  12. package/esm/renderer/renderer.js +18 -9
  13. package/esm/resource/loaders/svg.js +6 -4
  14. package/esm/templates/built-in.js +10 -0
  15. package/esm/utils/padding.js +19 -14
  16. package/lib/designs/items/HorizontalIconArrow.js +1 -1
  17. package/lib/designs/structures/index.d.ts +1 -0
  18. package/lib/designs/structures/index.js +1 -0
  19. package/lib/designs/structures/sequence-funnel.d.ts +10 -0
  20. package/lib/designs/structures/sequence-funnel.js +150 -0
  21. package/lib/options/types.d.ts +2 -0
  22. package/lib/renderer/composites/background.d.ts +2 -1
  23. package/lib/renderer/composites/background.js +6 -4
  24. package/lib/renderer/fonts/loader.js +2 -2
  25. package/lib/renderer/renderer.js +18 -9
  26. package/lib/resource/loaders/svg.js +6 -4
  27. package/lib/templates/built-in.js +10 -0
  28. package/lib/utils/padding.js +19 -14
  29. package/package.json +2 -2
  30. package/src/designs/items/HorizontalIconArrow.tsx +10 -5
  31. package/src/designs/structures/index.ts +1 -0
  32. package/src/designs/structures/sequence-funnel.tsx +260 -0
  33. package/src/options/types.ts +2 -0
  34. package/src/renderer/composites/background.ts +8 -5
  35. package/src/renderer/fonts/loader.ts +2 -2
  36. package/src/renderer/renderer.ts +18 -9
  37. package/src/resource/loaders/svg.ts +8 -4
  38. package/src/templates/built-in.ts +10 -0
  39. package/src/utils/padding.ts +18 -14
@@ -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
- setView(this.template, this.options);
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
- observer.observe(document, {
48
- childList: true,
49
- subtree: true,
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
- const { themeConfig } = options;
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
- return resource.startsWith('<svg') || resource.startsWith('<symbol');
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.startsWith('<svg')
12
- ? data.replace(/<svg/, '<symbol').replace(/svg>/, 'symbol>')
13
- : data;
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',
@@ -34,22 +34,27 @@ function setSVGPadding(svg, padding) {
34
34
  setSVGPaddingInBrowser(svg, padding);
35
35
  }
36
36
  else {
37
- const observer = new MutationObserver((mutations) => {
38
- mutations.forEach((mutation) => {
39
- mutation.addedNodes.forEach((node) => {
40
- if (node === svg || node.contains(svg)) {
41
- waitForLayout(svg, () => {
42
- setSVGPaddingInBrowser(svg, padding);
43
- });
44
- observer.disconnect();
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
- observer.observe(document, {
50
- childList: true,
51
- subtree: true,
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.6",
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
- <Group>
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
- x={width / 2}
125
- y={arrowHeight / 2}
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
- </Group>
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
+ });
@@ -59,4 +59,6 @@ interface SVGOptions {
59
59
  attributes?: Record<string, string | number | boolean>;
60
60
  id?: string;
61
61
  className?: string;
62
+ /** 是否启用背景 */
63
+ background?: boolean;
62
64
  }
@@ -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
- background?: string,
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.dataset.infographicFontLoaded === 'true') return true;
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.dataset.infographicFontLoaded = 'true';
94
+ link.setAttribute('data-infographic-font-loaded', 'true');
95
95
  resolve();
96
96
  };
97
97
  link.addEventListener('load', done, { once: true });
@@ -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
- setView(this.template, this.options);
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
- observer.observe(document, {
84
- childList: true,
85
- subtree: true,
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
- const { themeConfig } = options;
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
- return resource.startsWith('<svg') || resource.startsWith('<symbol');
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.startsWith('<svg')
10
- ? data.replace(/<svg/, '<symbol').replace(/svg>/, 'symbol>')
11
- : data;
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',
@@ -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
- const observer = new MutationObserver((mutations) => {
34
- mutations.forEach((mutation) => {
35
- mutation.addedNodes.forEach((node) => {
36
- if (node === svg || node.contains(svg)) {
37
- waitForLayout(svg, () => {
38
- setSVGPaddingInBrowser(svg, padding);
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
- observer.disconnect();
42
- }
42
+ observer.disconnect();
43
+ }
44
+ });
43
45
  });
44
46
  });
45
- });
46
47
 
47
- observer.observe(document, {
48
- childList: true,
49
- subtree: true,
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
  }