@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@antv/infographic/jsx-runtime";
|
|
2
2
|
import { Ellipse, getElementBounds, Group, Path, Polygon, Text, } from '../../jsx';
|
|
3
3
|
import { Gap, ItemDesc, ItemIconCircle, ItemLabel, ShapesGroup, } from '../components';
|
|
4
|
-
import { FlexLayout } from '../layouts';
|
|
4
|
+
import { AlignLayout, FlexLayout } from '../layouts';
|
|
5
5
|
import { getItemProps } from '../utils';
|
|
6
6
|
import { registerItem } from './registry';
|
|
7
7
|
export const HorizontalIconArrow = (props) => {
|
|
@@ -32,7 +32,7 @@ export const HorizontalIconArrow = (props) => {
|
|
|
32
32
|
dotLineGap +
|
|
33
33
|
labelBounds.height +
|
|
34
34
|
descBounds.height;
|
|
35
|
-
return (_jsx(Group, { width: width, height: totalHeight, ...restProps, children: _jsxs(FlexLayout, { flexDirection: "column", alignItems: "center", children: [isVNormal ? (_jsxs(_Fragment, { children: [desc, label, _jsx(Gap, { height: dotLineGap }), dotLine] })) : (_jsxs(_Fragment, { children: [_jsx(Gap, { height: fixedGap }), icon, _jsx(Gap, { height: iconGap })] })), _jsxs(
|
|
35
|
+
return (_jsx(Group, { width: width, height: totalHeight, ...restProps, children: _jsxs(FlexLayout, { flexDirection: "column", alignItems: "center", children: [isVNormal ? (_jsxs(_Fragment, { children: [desc, label, _jsx(Gap, { height: dotLineGap }), dotLine] })) : (_jsxs(_Fragment, { children: [_jsx(Gap, { height: fixedGap }), icon, _jsx(Gap, { height: iconGap })] })), _jsxs(AlignLayout, { horizontal: "center", vertical: "middle", width: width, height: arrowHeight, children: [_jsx(HorizontalArrow, { width: width, height: arrowHeight, fill: themeColors.colorPrimary }), _jsx(Text, { width: width, height: arrowHeight, alignHorizontal: "center", alignVertical: "middle", fill: themeColors.colorWhite, fontWeight: "bold", fontSize: 16, children: datum.time
|
|
36
36
|
? datum.time
|
|
37
37
|
: String(indexes[0] + 1)
|
|
38
38
|
.padStart(2, '0')
|
|
@@ -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';
|
|
@@ -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,10 @@
|
|
|
1
|
+
import type { ComponentType } from '../../jsx';
|
|
2
|
+
import type { BaseStructureProps } from './types';
|
|
3
|
+
export interface SequenceFunnelProps extends BaseStructureProps {
|
|
4
|
+
gap?: number;
|
|
5
|
+
width?: number;
|
|
6
|
+
funnelWidth?: number;
|
|
7
|
+
itemHeight?: number;
|
|
8
|
+
minBottomRatio?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const SequenceFunnel: ComponentType<SequenceFunnelProps>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@antv/infographic/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* 序列漏斗结构(SequenceFunnel)
|
|
4
|
+
* 用途:
|
|
5
|
+
* - 在左侧渲染分层漏斗形状(倒置梯形堆叠),右侧渲染对应的 item 卡片与图标
|
|
6
|
+
* - 形状上宽下窄,底部平滑(梯形),卡片背景插入漏斗下方
|
|
7
|
+
*/
|
|
8
|
+
import roundPolygon, { getSegments } from 'round-polygon';
|
|
9
|
+
import tinycolor from 'tinycolor2';
|
|
10
|
+
import { Defs, Group, Polygon, Rect } from '../../jsx';
|
|
11
|
+
import { BtnAdd, BtnRemove, BtnsGroup, ItemIcon, ItemsGroup, } from '../components';
|
|
12
|
+
import { FlexLayout } from '../layouts';
|
|
13
|
+
import { getPaletteColor, getThemeColors } from '../utils';
|
|
14
|
+
import { registerStructure } from './registry';
|
|
15
|
+
// Constants
|
|
16
|
+
const FUNNEL_CORNER_RADIUS = 6;
|
|
17
|
+
const ICON_SIZE = 32;
|
|
18
|
+
const FUNNEL_LAYER_HEIGHT_RATIO = 1.25;
|
|
19
|
+
const OVERLAP_DIST = 25;
|
|
20
|
+
const TEXT_GAP = 15;
|
|
21
|
+
export const SequenceFunnel = (props) => {
|
|
22
|
+
const { Title, Item, data, gap = 10, width = 700, funnelWidth, itemHeight = 60, minBottomRatio = 0.25, // 默认底部保留 25% 的宽度,形成梯形
|
|
23
|
+
options, } = props;
|
|
24
|
+
const { title, desc, items = [] } = data;
|
|
25
|
+
const titleContent = Title ? _jsx(Title, { title: title, desc: desc }) : null;
|
|
26
|
+
if (items.length === 0) {
|
|
27
|
+
return (_jsx(FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: titleContent }));
|
|
28
|
+
}
|
|
29
|
+
const themeColors = getThemeColors(options.themeConfig);
|
|
30
|
+
// 计算各区域尺寸
|
|
31
|
+
const actualFunnelWidth = funnelWidth ?? width * 0.55; // 稍微调窄一点漏斗,给右侧留更多空间
|
|
32
|
+
const itemAreaWidth = width - actualFunnelWidth;
|
|
33
|
+
// 漏斗层高度
|
|
34
|
+
const funnelLayerHeight = itemHeight * FUNNEL_LAYER_HEIGHT_RATIO;
|
|
35
|
+
const totalHeight = items.length * funnelLayerHeight + (items.length - 1) * gap;
|
|
36
|
+
// 计算底部的最小像素宽度
|
|
37
|
+
const minFunnelPixelWidth = actualFunnelWidth * minBottomRatio;
|
|
38
|
+
const elements = items.map((item, index) => {
|
|
39
|
+
const indexes = [index];
|
|
40
|
+
// 获取颜色
|
|
41
|
+
const color = getPaletteColor(options, [index]) || themeColors.colorPrimary;
|
|
42
|
+
// 1. 计算当前层的梯形形状
|
|
43
|
+
// 使用线性插值,从 actualFunnelWidth 收缩到 minFunnelPixelWidth
|
|
44
|
+
const { points, topWidth } = calculateTrapezoidSegment(actualFunnelWidth, minFunnelPixelWidth, funnelLayerHeight, gap, totalHeight, index);
|
|
45
|
+
// 圆角处理
|
|
46
|
+
const rounded = roundPolygon(points, FUNNEL_CORNER_RADIUS);
|
|
47
|
+
const segments = getSegments(rounded, 'AMOUNT', 10);
|
|
48
|
+
// 坐标计算
|
|
49
|
+
const funnelCenterX = actualFunnelWidth / 2;
|
|
50
|
+
const funnelY = index * (funnelLayerHeight + gap);
|
|
51
|
+
// 2. 背景与 Item 的位置计算
|
|
52
|
+
// 在漏斗(倒梯形)中,顶边(topWidth)总是比底边(bottomWidth)宽
|
|
53
|
+
// 所以右侧边缘的最外点是 topWidth 的一半
|
|
54
|
+
const rightTopX = funnelCenterX + topWidth / 2;
|
|
55
|
+
// 背景卡片:
|
|
56
|
+
// X 轴起点:从漏斗最宽处向左回缩 overlapDist,形成“插入”效果
|
|
57
|
+
const backgroundX = rightTopX - OVERLAP_DIST;
|
|
58
|
+
// 宽度:填满剩余空间,但要补上左侧回缩的距离
|
|
59
|
+
const backgroundWidth = itemAreaWidth + OVERLAP_DIST - 10; // -10 用于右侧留白
|
|
60
|
+
const backgroundYOffset = (funnelLayerHeight - itemHeight) / 2;
|
|
61
|
+
const backgroundY = funnelY + backgroundYOffset;
|
|
62
|
+
// 文本内容 (Item):
|
|
63
|
+
// X 轴起点:不应该跟着背景向左缩,而应该在漏斗边缘右侧,避免被漏斗遮挡
|
|
64
|
+
const itemX = rightTopX + TEXT_GAP;
|
|
65
|
+
const itemWidth = backgroundWidth - OVERLAP_DIST - TEXT_GAP;
|
|
66
|
+
const itemY = backgroundY;
|
|
67
|
+
// 图标位置
|
|
68
|
+
const iconX = funnelCenterX - ICON_SIZE / 2;
|
|
69
|
+
const iconY = funnelY + funnelLayerHeight / 2 - ICON_SIZE / 2;
|
|
70
|
+
const funnelColorId = `${color.replace('#', '')}-funnel-${index}`;
|
|
71
|
+
return {
|
|
72
|
+
background: (_jsx(Rect, { x: backgroundX, y: backgroundY, width: backgroundWidth, height: itemHeight, ry: "8" // 背景圆角稍微大一点,显得柔和
|
|
73
|
+
, fill: tinycolor(color).setAlpha(0.1).toRgbString(), "data-element-type": "shape" })),
|
|
74
|
+
funnel: [
|
|
75
|
+
_jsx(Defs, { children: _jsxs("linearGradient", { id: funnelColorId, x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [_jsx("stop", { offset: "0%", stopColor: tinycolor(color).lighten(10).toString() }), _jsx("stop", { offset: "100%", stopColor: color })] }) }),
|
|
76
|
+
_jsx(Polygon, { points: segments, fill: `url(#${funnelColorId})`, y: funnelY, "data-element-type": "shape",
|
|
77
|
+
// 添加轻微阴影效果增加层次感(可选,依赖环境支持 filter)
|
|
78
|
+
style: { filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.15))' } }),
|
|
79
|
+
],
|
|
80
|
+
icon: (_jsx(ItemIcon, { indexes: indexes, x: iconX, y: iconY, size: ICON_SIZE, fill: "#fff" })),
|
|
81
|
+
item: (_jsx(Item, { indexes: indexes, datum: item, data: data, x: itemX, y: itemY, width: itemWidth, height: itemHeight, positionV: "middle" })),
|
|
82
|
+
btnRemove: (_jsx(BtnRemove, { indexes: indexes, x: backgroundX + backgroundWidth, y: backgroundY })),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
const btnAdd = (_jsx(BtnAdd, { indexes: [items.length], x: width / 2, y: totalHeight + 10 }));
|
|
86
|
+
return (_jsxs(FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: [titleContent, _jsxs(Group, { width: width, height: totalHeight + 40, children: [_jsx(Group, { children: elements.map((element) => element.background) }), _jsx(Group, { children: elements.flatMap((element) => element.funnel) }), _jsx(Group, { children: elements.map((element) => element.icon) }), _jsx(ItemsGroup, { children: elements.map((element) => element.item) }), _jsxs(BtnsGroup, { children: [elements.map((element) => element.btnRemove), btnAdd] })] })] }));
|
|
87
|
+
};
|
|
88
|
+
// 计算梯形分段逻辑
|
|
89
|
+
function calculateTrapezoidSegment(maxWidth, minWidth, layerHeight, gap, totalHeight, index) {
|
|
90
|
+
const centerX = maxWidth / 2;
|
|
91
|
+
// 当前层顶部和底部的 Y 坐标(相对于总高度)
|
|
92
|
+
const currentTopY = index * (layerHeight + gap);
|
|
93
|
+
const currentBottomY = currentTopY + layerHeight;
|
|
94
|
+
// 线性插值计算宽度
|
|
95
|
+
// Width = MaxWidth - (MaxWidth - MinWidth) * (Y / TotalHeight)
|
|
96
|
+
const widthDiff = maxWidth - minWidth;
|
|
97
|
+
const topWidth = maxWidth - widthDiff * (currentTopY / totalHeight);
|
|
98
|
+
const bottomWidth = maxWidth - widthDiff * (currentBottomY / totalHeight);
|
|
99
|
+
// 生成四个顶点 (梯形)
|
|
100
|
+
const p1 = { x: centerX - topWidth / 2, y: 0 }; // 左上
|
|
101
|
+
const p2 = { x: centerX + topWidth / 2, y: 0 }; // 右上
|
|
102
|
+
const p3 = { x: centerX + bottomWidth / 2, y: layerHeight }; // 右下
|
|
103
|
+
const p4 = { x: centerX - bottomWidth / 2, y: layerHeight }; // 左下
|
|
104
|
+
return { points: [p1, p2, p3, p4], topWidth, bottomWidth };
|
|
105
|
+
}
|
|
106
|
+
// 注册
|
|
107
|
+
registerStructure('sequence-funnel', {
|
|
108
|
+
component: SequenceFunnel,
|
|
109
|
+
composites: ['title', 'item'],
|
|
110
|
+
});
|
package/esm/options/types.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ParsedInfographicOptions } from '../../options';
|
|
2
|
+
export declare function renderBackground(svg: SVGSVGElement, options: ParsedInfographicOptions): void;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createElement, getElementByRole } from '../../utils';
|
|
2
|
-
export function renderBackground(svg,
|
|
2
|
+
export function renderBackground(svg, options) {
|
|
3
|
+
if (options.svg?.background === false)
|
|
4
|
+
return;
|
|
5
|
+
const { themeConfig: { colorBg: background }, } = options;
|
|
6
|
+
if (!background)
|
|
7
|
+
return;
|
|
3
8
|
const container = svg.parentElement;
|
|
4
9
|
if (container)
|
|
5
10
|
container.style.backgroundColor = background || 'none';
|
|
6
11
|
const element = getElementByRole(svg, "background" /* ElementTypeEnum.Background */);
|
|
7
|
-
if (!background) {
|
|
8
|
-
return element?.remove();
|
|
9
|
-
}
|
|
10
12
|
svg.style.backgroundColor = background;
|
|
11
13
|
if (element) {
|
|
12
14
|
element.setAttribute('fill', background);
|
|
@@ -50,7 +50,7 @@ function trackFontPromise(target, id, promise) {
|
|
|
50
50
|
return promise;
|
|
51
51
|
}
|
|
52
52
|
function isLinkLoaded(link) {
|
|
53
|
-
if (link.
|
|
53
|
+
if (link.getAttribute('data-infographic-font-loaded') === 'true')
|
|
54
54
|
return true;
|
|
55
55
|
try {
|
|
56
56
|
return !!link.sheet;
|
|
@@ -68,7 +68,7 @@ function getFontLoadPromise(target, id, link) {
|
|
|
68
68
|
}
|
|
69
69
|
const promise = new Promise((resolve) => {
|
|
70
70
|
const done = () => {
|
|
71
|
-
link.
|
|
71
|
+
link.setAttribute('data-infographic-font-loaded', 'true');
|
|
72
72
|
resolve();
|
|
73
73
|
};
|
|
74
74
|
link.addEventListener('load', done, { once: true });
|
package/esm/renderer/renderer.js
CHANGED
|
@@ -27,24 +27,34 @@ export class Renderer {
|
|
|
27
27
|
return svg;
|
|
28
28
|
renderTemplate(svg, this.options);
|
|
29
29
|
svg.style.visibility = 'hidden';
|
|
30
|
+
const postRender = () => {
|
|
31
|
+
setView(this.template, this.options);
|
|
32
|
+
loadFonts(this.template);
|
|
33
|
+
svg.style.visibility = '';
|
|
34
|
+
};
|
|
30
35
|
const observer = new MutationObserver((mutations) => {
|
|
31
36
|
mutations.forEach((mutation) => {
|
|
32
37
|
mutation.addedNodes.forEach((node) => {
|
|
33
38
|
if (node === svg || node.contains(svg)) {
|
|
34
39
|
// post render
|
|
35
|
-
|
|
36
|
-
loadFonts(this.template);
|
|
40
|
+
postRender();
|
|
37
41
|
// disconnect observer
|
|
38
42
|
observer.disconnect();
|
|
39
|
-
svg.style.visibility = '';
|
|
40
43
|
}
|
|
41
44
|
});
|
|
42
45
|
});
|
|
43
46
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
try {
|
|
48
|
+
observer.observe(document, {
|
|
49
|
+
childList: true,
|
|
50
|
+
subtree: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// Fallback for micro-app environments that proxy document.
|
|
55
|
+
postRender();
|
|
56
|
+
console.error(error);
|
|
57
|
+
}
|
|
48
58
|
this.rendered = true;
|
|
49
59
|
return svg;
|
|
50
60
|
}
|
|
@@ -52,8 +62,7 @@ export class Renderer {
|
|
|
52
62
|
function renderTemplate(svg, options) {
|
|
53
63
|
fill(svg, options);
|
|
54
64
|
setSVG(svg, options);
|
|
55
|
-
|
|
56
|
-
renderBackground(svg, themeConfig?.colorBg);
|
|
65
|
+
renderBackground(svg, options);
|
|
57
66
|
}
|
|
58
67
|
function fill(svg, options) {
|
|
59
68
|
const { themeConfig, data } = options;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { parseSVG } from '../../utils';
|
|
2
2
|
function isSVGResource(resource) {
|
|
3
|
-
|
|
3
|
+
const trimmedResource = resource.trim();
|
|
4
|
+
return (/^(?:<\?xml[^>]*>\s*)?<svg[\s>]/i.test(trimmedResource) ||
|
|
5
|
+
trimmedResource.startsWith('<symbol'));
|
|
4
6
|
}
|
|
5
7
|
export function loadSVGResource(data) {
|
|
6
8
|
if (!data || !isSVGResource(data))
|
|
7
9
|
return null;
|
|
8
|
-
const str = data
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const str = data
|
|
11
|
+
.replace(/<svg(?=[\s/>])/i, '<symbol')
|
|
12
|
+
.replace(/<\/svg>/i, '</symbol>');
|
|
11
13
|
return parseSVG(str);
|
|
12
14
|
}
|
|
@@ -308,6 +308,16 @@ const BUILT_IN_TEMPLATES = {
|
|
|
308
308
|
colorPrimary: '#1677ff',
|
|
309
309
|
},
|
|
310
310
|
},
|
|
311
|
+
'sequence-funnel-simple': {
|
|
312
|
+
design: {
|
|
313
|
+
title: 'default',
|
|
314
|
+
structure: { type: 'sequence-funnel' },
|
|
315
|
+
items: [{ type: 'simple', showIcon: false, usePaletteColor: true }],
|
|
316
|
+
},
|
|
317
|
+
themeConfig: {
|
|
318
|
+
palette: '#1677ff',
|
|
319
|
+
},
|
|
320
|
+
},
|
|
311
321
|
'list-row-horizontal-icon-line': {
|
|
312
322
|
design: {
|
|
313
323
|
title: 'default',
|
package/esm/utils/padding.js
CHANGED
|
@@ -30,22 +30,27 @@ export function setSVGPadding(svg, padding) {
|
|
|
30
30
|
setSVGPaddingInBrowser(svg, padding);
|
|
31
31
|
}
|
|
32
32
|
else {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
});
|
|
41
|
+
observer.disconnect();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
42
44
|
});
|
|
43
45
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
46
|
+
observer.observe(document, {
|
|
47
|
+
childList: true,
|
|
48
|
+
subtree: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
setSVGPaddingInNode(svg, padding);
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
}
|
|
@@ -35,7 +35,7 @@ const HorizontalIconArrow = (props) => {
|
|
|
35
35
|
dotLineGap +
|
|
36
36
|
labelBounds.height +
|
|
37
37
|
descBounds.height;
|
|
38
|
-
return ((0, jsx_runtime_1.jsx)(jsx_1.Group, { width: width, height: totalHeight, ...restProps, children: (0, jsx_runtime_1.jsxs)(layouts_1.FlexLayout, { flexDirection: "column", alignItems: "center", children: [isVNormal ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [desc, label, (0, jsx_runtime_1.jsx)(components_1.Gap, { height: dotLineGap }), dotLine] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(components_1.Gap, { height: fixedGap }), icon, (0, jsx_runtime_1.jsx)(components_1.Gap, { height: iconGap })] })), (0, jsx_runtime_1.jsxs)(
|
|
38
|
+
return ((0, jsx_runtime_1.jsx)(jsx_1.Group, { width: width, height: totalHeight, ...restProps, children: (0, jsx_runtime_1.jsxs)(layouts_1.FlexLayout, { flexDirection: "column", alignItems: "center", children: [isVNormal ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [desc, label, (0, jsx_runtime_1.jsx)(components_1.Gap, { height: dotLineGap }), dotLine] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(components_1.Gap, { height: fixedGap }), icon, (0, jsx_runtime_1.jsx)(components_1.Gap, { height: iconGap })] })), (0, jsx_runtime_1.jsxs)(layouts_1.AlignLayout, { horizontal: "center", vertical: "middle", width: width, height: arrowHeight, children: [(0, jsx_runtime_1.jsx)(HorizontalArrow, { width: width, height: arrowHeight, fill: themeColors.colorPrimary }), (0, jsx_runtime_1.jsx)(jsx_1.Text, { width: width, height: arrowHeight, alignHorizontal: "center", alignVertical: "middle", fill: themeColors.colorWhite, fontWeight: "bold", fontSize: 16, children: datum.time
|
|
39
39
|
? datum.time
|
|
40
40
|
: String(indexes[0] + 1)
|
|
41
41
|
.padStart(2, '0')
|
|
@@ -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';
|
|
@@ -48,6 +48,7 @@ __exportStar(require("./sequence-circular"), exports);
|
|
|
48
48
|
__exportStar(require("./sequence-color-snake-steps"), exports);
|
|
49
49
|
__exportStar(require("./sequence-cylinders-3d"), exports);
|
|
50
50
|
__exportStar(require("./sequence-filter-mesh"), exports);
|
|
51
|
+
__exportStar(require("./sequence-funnel"), exports);
|
|
51
52
|
__exportStar(require("./sequence-horizontal-zigzag"), exports);
|
|
52
53
|
__exportStar(require("./sequence-mountain"), exports);
|
|
53
54
|
__exportStar(require("./sequence-pyramid"), exports);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ComponentType } from '../../jsx';
|
|
2
|
+
import type { BaseStructureProps } from './types';
|
|
3
|
+
export interface SequenceFunnelProps extends BaseStructureProps {
|
|
4
|
+
gap?: number;
|
|
5
|
+
width?: number;
|
|
6
|
+
funnelWidth?: number;
|
|
7
|
+
itemHeight?: number;
|
|
8
|
+
minBottomRatio?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const SequenceFunnel: ComponentType<SequenceFunnelProps>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.SequenceFunnel = void 0;
|
|
40
|
+
const jsx_runtime_1 = require("@antv/infographic/jsx-runtime");
|
|
41
|
+
/**
|
|
42
|
+
* 序列漏斗结构(SequenceFunnel)
|
|
43
|
+
* 用途:
|
|
44
|
+
* - 在左侧渲染分层漏斗形状(倒置梯形堆叠),右侧渲染对应的 item 卡片与图标
|
|
45
|
+
* - 形状上宽下窄,底部平滑(梯形),卡片背景插入漏斗下方
|
|
46
|
+
*/
|
|
47
|
+
const round_polygon_1 = __importStar(require("round-polygon"));
|
|
48
|
+
const tinycolor2_1 = __importDefault(require("tinycolor2"));
|
|
49
|
+
const jsx_1 = require("../../jsx");
|
|
50
|
+
const components_1 = require("../components");
|
|
51
|
+
const layouts_1 = require("../layouts");
|
|
52
|
+
const utils_1 = require("../utils");
|
|
53
|
+
const registry_1 = require("./registry");
|
|
54
|
+
// Constants
|
|
55
|
+
const FUNNEL_CORNER_RADIUS = 6;
|
|
56
|
+
const ICON_SIZE = 32;
|
|
57
|
+
const FUNNEL_LAYER_HEIGHT_RATIO = 1.25;
|
|
58
|
+
const OVERLAP_DIST = 25;
|
|
59
|
+
const TEXT_GAP = 15;
|
|
60
|
+
const SequenceFunnel = (props) => {
|
|
61
|
+
const { Title, Item, data, gap = 10, width = 700, funnelWidth, itemHeight = 60, minBottomRatio = 0.25, // 默认底部保留 25% 的宽度,形成梯形
|
|
62
|
+
options, } = props;
|
|
63
|
+
const { title, desc, items = [] } = data;
|
|
64
|
+
const titleContent = Title ? (0, jsx_runtime_1.jsx)(Title, { title: title, desc: desc }) : null;
|
|
65
|
+
if (items.length === 0) {
|
|
66
|
+
return ((0, jsx_runtime_1.jsx)(layouts_1.FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: titleContent }));
|
|
67
|
+
}
|
|
68
|
+
const themeColors = (0, utils_1.getThemeColors)(options.themeConfig);
|
|
69
|
+
// 计算各区域尺寸
|
|
70
|
+
const actualFunnelWidth = funnelWidth ?? width * 0.55; // 稍微调窄一点漏斗,给右侧留更多空间
|
|
71
|
+
const itemAreaWidth = width - actualFunnelWidth;
|
|
72
|
+
// 漏斗层高度
|
|
73
|
+
const funnelLayerHeight = itemHeight * FUNNEL_LAYER_HEIGHT_RATIO;
|
|
74
|
+
const totalHeight = items.length * funnelLayerHeight + (items.length - 1) * gap;
|
|
75
|
+
// 计算底部的最小像素宽度
|
|
76
|
+
const minFunnelPixelWidth = actualFunnelWidth * minBottomRatio;
|
|
77
|
+
const elements = items.map((item, index) => {
|
|
78
|
+
const indexes = [index];
|
|
79
|
+
// 获取颜色
|
|
80
|
+
const color = (0, utils_1.getPaletteColor)(options, [index]) || themeColors.colorPrimary;
|
|
81
|
+
// 1. 计算当前层的梯形形状
|
|
82
|
+
// 使用线性插值,从 actualFunnelWidth 收缩到 minFunnelPixelWidth
|
|
83
|
+
const { points, topWidth } = calculateTrapezoidSegment(actualFunnelWidth, minFunnelPixelWidth, funnelLayerHeight, gap, totalHeight, index);
|
|
84
|
+
// 圆角处理
|
|
85
|
+
const rounded = (0, round_polygon_1.default)(points, FUNNEL_CORNER_RADIUS);
|
|
86
|
+
const segments = (0, round_polygon_1.getSegments)(rounded, 'AMOUNT', 10);
|
|
87
|
+
// 坐标计算
|
|
88
|
+
const funnelCenterX = actualFunnelWidth / 2;
|
|
89
|
+
const funnelY = index * (funnelLayerHeight + gap);
|
|
90
|
+
// 2. 背景与 Item 的位置计算
|
|
91
|
+
// 在漏斗(倒梯形)中,顶边(topWidth)总是比底边(bottomWidth)宽
|
|
92
|
+
// 所以右侧边缘的最外点是 topWidth 的一半
|
|
93
|
+
const rightTopX = funnelCenterX + topWidth / 2;
|
|
94
|
+
// 背景卡片:
|
|
95
|
+
// X 轴起点:从漏斗最宽处向左回缩 overlapDist,形成“插入”效果
|
|
96
|
+
const backgroundX = rightTopX - OVERLAP_DIST;
|
|
97
|
+
// 宽度:填满剩余空间,但要补上左侧回缩的距离
|
|
98
|
+
const backgroundWidth = itemAreaWidth + OVERLAP_DIST - 10; // -10 用于右侧留白
|
|
99
|
+
const backgroundYOffset = (funnelLayerHeight - itemHeight) / 2;
|
|
100
|
+
const backgroundY = funnelY + backgroundYOffset;
|
|
101
|
+
// 文本内容 (Item):
|
|
102
|
+
// X 轴起点:不应该跟着背景向左缩,而应该在漏斗边缘右侧,避免被漏斗遮挡
|
|
103
|
+
const itemX = rightTopX + TEXT_GAP;
|
|
104
|
+
const itemWidth = backgroundWidth - OVERLAP_DIST - TEXT_GAP;
|
|
105
|
+
const itemY = backgroundY;
|
|
106
|
+
// 图标位置
|
|
107
|
+
const iconX = funnelCenterX - ICON_SIZE / 2;
|
|
108
|
+
const iconY = funnelY + funnelLayerHeight / 2 - ICON_SIZE / 2;
|
|
109
|
+
const funnelColorId = `${color.replace('#', '')}-funnel-${index}`;
|
|
110
|
+
return {
|
|
111
|
+
background: ((0, jsx_runtime_1.jsx)(jsx_1.Rect, { x: backgroundX, y: backgroundY, width: backgroundWidth, height: itemHeight, ry: "8" // 背景圆角稍微大一点,显得柔和
|
|
112
|
+
, fill: (0, tinycolor2_1.default)(color).setAlpha(0.1).toRgbString(), "data-element-type": "shape" })),
|
|
113
|
+
funnel: [
|
|
114
|
+
(0, jsx_runtime_1.jsx)(jsx_1.Defs, { children: (0, jsx_runtime_1.jsxs)("linearGradient", { id: funnelColorId, x1: "0%", y1: "0%", x2: "100%", y2: "0%", children: [(0, jsx_runtime_1.jsx)("stop", { offset: "0%", stopColor: (0, tinycolor2_1.default)(color).lighten(10).toString() }), (0, jsx_runtime_1.jsx)("stop", { offset: "100%", stopColor: color })] }) }),
|
|
115
|
+
(0, jsx_runtime_1.jsx)(jsx_1.Polygon, { points: segments, fill: `url(#${funnelColorId})`, y: funnelY, "data-element-type": "shape",
|
|
116
|
+
// 添加轻微阴影效果增加层次感(可选,依赖环境支持 filter)
|
|
117
|
+
style: { filter: 'drop-shadow(0px 2px 3px rgba(0,0,0,0.15))' } }),
|
|
118
|
+
],
|
|
119
|
+
icon: ((0, jsx_runtime_1.jsx)(components_1.ItemIcon, { indexes: indexes, x: iconX, y: iconY, size: ICON_SIZE, fill: "#fff" })),
|
|
120
|
+
item: ((0, jsx_runtime_1.jsx)(Item, { indexes: indexes, datum: item, data: data, x: itemX, y: itemY, width: itemWidth, height: itemHeight, positionV: "middle" })),
|
|
121
|
+
btnRemove: ((0, jsx_runtime_1.jsx)(components_1.BtnRemove, { indexes: indexes, x: backgroundX + backgroundWidth, y: backgroundY })),
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
const btnAdd = ((0, jsx_runtime_1.jsx)(components_1.BtnAdd, { indexes: [items.length], x: width / 2, y: totalHeight + 10 }));
|
|
125
|
+
return ((0, jsx_runtime_1.jsxs)(layouts_1.FlexLayout, { id: "infographic-container", flexDirection: "column", justifyContent: "center", alignItems: "center", children: [titleContent, (0, jsx_runtime_1.jsxs)(jsx_1.Group, { width: width, height: totalHeight + 40, children: [(0, jsx_runtime_1.jsx)(jsx_1.Group, { children: elements.map((element) => element.background) }), (0, jsx_runtime_1.jsx)(jsx_1.Group, { children: elements.flatMap((element) => element.funnel) }), (0, jsx_runtime_1.jsx)(jsx_1.Group, { children: elements.map((element) => element.icon) }), (0, jsx_runtime_1.jsx)(components_1.ItemsGroup, { children: elements.map((element) => element.item) }), (0, jsx_runtime_1.jsxs)(components_1.BtnsGroup, { children: [elements.map((element) => element.btnRemove), btnAdd] })] })] }));
|
|
126
|
+
};
|
|
127
|
+
exports.SequenceFunnel = SequenceFunnel;
|
|
128
|
+
// 计算梯形分段逻辑
|
|
129
|
+
function calculateTrapezoidSegment(maxWidth, minWidth, layerHeight, gap, totalHeight, index) {
|
|
130
|
+
const centerX = maxWidth / 2;
|
|
131
|
+
// 当前层顶部和底部的 Y 坐标(相对于总高度)
|
|
132
|
+
const currentTopY = index * (layerHeight + gap);
|
|
133
|
+
const currentBottomY = currentTopY + layerHeight;
|
|
134
|
+
// 线性插值计算宽度
|
|
135
|
+
// Width = MaxWidth - (MaxWidth - MinWidth) * (Y / TotalHeight)
|
|
136
|
+
const widthDiff = maxWidth - minWidth;
|
|
137
|
+
const topWidth = maxWidth - widthDiff * (currentTopY / totalHeight);
|
|
138
|
+
const bottomWidth = maxWidth - widthDiff * (currentBottomY / totalHeight);
|
|
139
|
+
// 生成四个顶点 (梯形)
|
|
140
|
+
const p1 = { x: centerX - topWidth / 2, y: 0 }; // 左上
|
|
141
|
+
const p2 = { x: centerX + topWidth / 2, y: 0 }; // 右上
|
|
142
|
+
const p3 = { x: centerX + bottomWidth / 2, y: layerHeight }; // 右下
|
|
143
|
+
const p4 = { x: centerX - bottomWidth / 2, y: layerHeight }; // 左下
|
|
144
|
+
return { points: [p1, p2, p3, p4], topWidth, bottomWidth };
|
|
145
|
+
}
|
|
146
|
+
// 注册
|
|
147
|
+
(0, registry_1.registerStructure)('sequence-funnel', {
|
|
148
|
+
component: exports.SequenceFunnel,
|
|
149
|
+
composites: ['title', 'item'],
|
|
150
|
+
});
|
package/lib/options/types.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ParsedInfographicOptions } from '../../options';
|
|
2
|
+
export declare function renderBackground(svg: SVGSVGElement, options: ParsedInfographicOptions): void;
|
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.renderBackground = renderBackground;
|
|
4
4
|
const utils_1 = require("../../utils");
|
|
5
|
-
function renderBackground(svg,
|
|
5
|
+
function renderBackground(svg, options) {
|
|
6
|
+
if (options.svg?.background === false)
|
|
7
|
+
return;
|
|
8
|
+
const { themeConfig: { colorBg: background }, } = options;
|
|
9
|
+
if (!background)
|
|
10
|
+
return;
|
|
6
11
|
const container = svg.parentElement;
|
|
7
12
|
if (container)
|
|
8
13
|
container.style.backgroundColor = background || 'none';
|
|
9
14
|
const element = (0, utils_1.getElementByRole)(svg, "background" /* ElementTypeEnum.Background */);
|
|
10
|
-
if (!background) {
|
|
11
|
-
return element?.remove();
|
|
12
|
-
}
|
|
13
15
|
svg.style.backgroundColor = background;
|
|
14
16
|
if (element) {
|
|
15
17
|
element.setAttribute('fill', background);
|
|
@@ -56,7 +56,7 @@ function trackFontPromise(target, id, promise) {
|
|
|
56
56
|
return promise;
|
|
57
57
|
}
|
|
58
58
|
function isLinkLoaded(link) {
|
|
59
|
-
if (link.
|
|
59
|
+
if (link.getAttribute('data-infographic-font-loaded') === 'true')
|
|
60
60
|
return true;
|
|
61
61
|
try {
|
|
62
62
|
return !!link.sheet;
|
|
@@ -74,7 +74,7 @@ function getFontLoadPromise(target, id, link) {
|
|
|
74
74
|
}
|
|
75
75
|
const promise = new Promise((resolve) => {
|
|
76
76
|
const done = () => {
|
|
77
|
-
link.
|
|
77
|
+
link.setAttribute('data-infographic-font-loaded', 'true');
|
|
78
78
|
resolve();
|
|
79
79
|
};
|
|
80
80
|
link.addEventListener('load', done, { once: true });
|