@antv/infographic 0.2.5 → 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 +79 -79
- 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/exporter/index.d.ts +1 -1
- package/esm/exporter/index.js +1 -1
- package/esm/exporter/svg.js +223 -2
- package/esm/exporter/types.d.ts +5 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.js +1 -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 +63 -8
- package/esm/renderer/renderer.js +18 -9
- package/esm/renderer/stylize/gradient.js +1 -1
- package/esm/resource/index.d.ts +1 -1
- package/esm/resource/index.js +1 -1
- package/esm/resource/load-tracker.d.ts +6 -0
- package/esm/resource/load-tracker.js +36 -0
- package/esm/resource/loader.d.ts +1 -0
- package/esm/resource/loader.js +27 -14
- package/esm/resource/loaders/svg.js +6 -4
- package/esm/runtime/Infographic.js +13 -0
- 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/exporter/index.d.ts +1 -1
- package/lib/exporter/index.js +2 -1
- package/lib/exporter/svg.js +223 -2
- package/lib/exporter/types.d.ts +5 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +4 -2
- 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 +63 -8
- package/lib/renderer/renderer.js +18 -9
- package/lib/renderer/stylize/gradient.js +1 -1
- package/lib/resource/index.d.ts +1 -1
- package/lib/resource/index.js +3 -1
- package/lib/resource/load-tracker.d.ts +6 -0
- package/lib/resource/load-tracker.js +42 -0
- package/lib/resource/loader.d.ts +1 -0
- package/lib/resource/loader.js +30 -14
- package/lib/resource/loaders/svg.js +6 -4
- package/lib/runtime/Infographic.js +13 -0
- 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/exporter/index.ts +1 -1
- package/src/exporter/svg.ts +254 -2
- package/src/exporter/types.ts +5 -0
- package/src/index.ts +6 -0
- package/src/options/types.ts +2 -0
- package/src/renderer/composites/background.ts +8 -5
- package/src/renderer/fonts/loader.ts +82 -9
- package/src/renderer/renderer.ts +18 -9
- package/src/renderer/stylize/gradient.ts +1 -1
- package/src/resource/index.ts +1 -1
- package/src/resource/load-tracker.ts +51 -0
- package/src/resource/loader.ts +27 -12
- package/src/resource/loaders/svg.ts +8 -4
- package/src/runtime/Infographic.tsx +12 -0
- package/src/templates/built-in.ts +10 -0
- package/src/utils/padding.ts +18 -14
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const SVG_LOAD_PROMISE_MAP = new WeakMap();
|
|
2
|
+
export function getSvgLoadPromises(svg) {
|
|
3
|
+
const map = SVG_LOAD_PROMISE_MAP.get(svg);
|
|
4
|
+
return map ? Array.from(map.values()) : [];
|
|
5
|
+
}
|
|
6
|
+
export function getSvgLoadPromise(svg, key) {
|
|
7
|
+
return SVG_LOAD_PROMISE_MAP.get(svg)?.get(key);
|
|
8
|
+
}
|
|
9
|
+
export function trackSvgLoadPromise(svg, key, promise) {
|
|
10
|
+
let map = SVG_LOAD_PROMISE_MAP.get(svg);
|
|
11
|
+
if (!map) {
|
|
12
|
+
map = new Map();
|
|
13
|
+
SVG_LOAD_PROMISE_MAP.set(svg, map);
|
|
14
|
+
}
|
|
15
|
+
map.set(key, promise);
|
|
16
|
+
promise.finally(() => {
|
|
17
|
+
const map = SVG_LOAD_PROMISE_MAP.get(svg);
|
|
18
|
+
if (!map)
|
|
19
|
+
return;
|
|
20
|
+
if (map.get(key) === promise)
|
|
21
|
+
map.delete(key);
|
|
22
|
+
if (map.size === 0)
|
|
23
|
+
SVG_LOAD_PROMISE_MAP.delete(svg);
|
|
24
|
+
});
|
|
25
|
+
return promise;
|
|
26
|
+
}
|
|
27
|
+
export async function waitForSvgLoads(svg) {
|
|
28
|
+
await Promise.resolve();
|
|
29
|
+
while (true) {
|
|
30
|
+
const promises = getSvgLoadPromises(svg);
|
|
31
|
+
if (!promises.length)
|
|
32
|
+
break;
|
|
33
|
+
await Promise.allSettled(promises);
|
|
34
|
+
await Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
}
|
package/esm/resource/loader.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ import type { ResourceConfig, ResourceScene } from './types';
|
|
|
5
5
|
* @returns resource ref id
|
|
6
6
|
*/
|
|
7
7
|
export declare function loadResource(svg: SVGSVGElement | null, scene: ResourceScene, config: string | ResourceConfig, datum?: ItemDatum): Promise<string | null>;
|
|
8
|
+
export { getSvgLoadPromises, waitForSvgLoads } from './load-tracker';
|
package/esm/resource/loader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getOrCreateDefs } from '../utils';
|
|
2
|
+
import { getSvgLoadPromise, trackSvgLoadPromise } from './load-tracker';
|
|
2
3
|
import { loadImageBase64Resource, loadRemoteResource, loadSearchResource, loadSVGResource, } from './loaders';
|
|
3
4
|
import { getCustomResourceLoader } from './registry';
|
|
4
5
|
import { getResourceId, parseResourceConfig } from './utils';
|
|
@@ -57,22 +58,34 @@ export async function loadResource(svg, scene, config, datum) {
|
|
|
57
58
|
if (!cfg)
|
|
58
59
|
return null;
|
|
59
60
|
const id = getResourceId(cfg);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
61
|
+
const promiseKey = `resource:${id}`;
|
|
62
|
+
const loadedMap = RESOURCE_LOAD_MAP.get(svg);
|
|
63
|
+
if (loadedMap?.has(id))
|
|
64
|
+
return id;
|
|
65
|
+
const existingPromise = getSvgLoadPromise(svg, promiseKey);
|
|
66
|
+
if (existingPromise)
|
|
67
|
+
return await existingPromise;
|
|
68
|
+
const loadPromise = (async () => {
|
|
69
|
+
const resource = RESOURCE_MAP.has(id)
|
|
70
|
+
? RESOURCE_MAP.get(id) || null
|
|
71
|
+
: await getResource(scene, cfg, datum);
|
|
72
|
+
if (!resource)
|
|
73
|
+
return null;
|
|
74
|
+
if (!RESOURCE_LOAD_MAP.has(svg))
|
|
75
|
+
RESOURCE_LOAD_MAP.set(svg, new Map());
|
|
76
|
+
const map = RESOURCE_LOAD_MAP.get(svg);
|
|
77
|
+
if (map.has(id))
|
|
78
|
+
return id;
|
|
79
|
+
const defs = getOrCreateDefs(svg);
|
|
80
|
+
resource.id = id;
|
|
81
|
+
defs.appendChild(resource);
|
|
82
|
+
map.set(id, resource);
|
|
69
83
|
return id;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
map.set(id, resource);
|
|
74
|
-
return id;
|
|
84
|
+
})();
|
|
85
|
+
trackSvgLoadPromise(svg, promiseKey, loadPromise);
|
|
86
|
+
return await loadPromise;
|
|
75
87
|
}
|
|
88
|
+
export { getSvgLoadPromises, waitForSvgLoads } from './load-tracker';
|
|
76
89
|
function getFallbackQuery(cfg, scene, datum) {
|
|
77
90
|
const defaultQuery = scene === 'illus' ? 'illustration' : 'icon';
|
|
78
91
|
const datumQuery = normalizeQuery(datum?.label) || normalizeQuery(datum?.desc);
|
|
@@ -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
|
}
|
|
@@ -5,6 +5,7 @@ import { exportToPNGString, exportToSVGString, } from '../exporter';
|
|
|
5
5
|
import { renderSVG } from '../jsx';
|
|
6
6
|
import { parseOptions, } from '../options';
|
|
7
7
|
import { Renderer } from '../renderer';
|
|
8
|
+
import { waitForSvgLoads } from '../resource';
|
|
8
9
|
import { parseSyntax } from '../syntax';
|
|
9
10
|
import { getTypes, parseSVG } from '../utils';
|
|
10
11
|
import { DEFAULT_OPTIONS } from './options';
|
|
@@ -71,6 +72,18 @@ export class Infographic {
|
|
|
71
72
|
}
|
|
72
73
|
this.rendered = true;
|
|
73
74
|
this.emitter.emit('rendered', { node: this.node, options: this.options });
|
|
75
|
+
const currentNode = this.node;
|
|
76
|
+
if (currentNode) {
|
|
77
|
+
void waitForSvgLoads(currentNode).then(() => {
|
|
78
|
+
if (this.node !== currentNode)
|
|
79
|
+
return;
|
|
80
|
+
this.emitter.emit('loaded', {
|
|
81
|
+
node: currentNode,
|
|
82
|
+
options: this.options,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
74
87
|
}
|
|
75
88
|
/**
|
|
76
89
|
* Compose the SVG template
|
|
@@ -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/exporter/index.d.ts
CHANGED
package/lib/exporter/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.exportToSVGString = exports.exportToPNGString = void 0;
|
|
3
|
+
exports.exportToSVGString = exports.exportToSVG = exports.exportToPNGString = void 0;
|
|
4
4
|
var png_1 = require("./png");
|
|
5
5
|
Object.defineProperty(exports, "exportToPNGString", { enumerable: true, get: function () { return png_1.exportToPNGString; } });
|
|
6
6
|
var svg_1 = require("./svg");
|
|
7
|
+
Object.defineProperty(exports, "exportToSVG", { enumerable: true, get: function () { return svg_1.exportToSVG; } });
|
|
7
8
|
Object.defineProperty(exports, "exportToSVGString", { enumerable: true, get: function () { return svg_1.exportToSVGString; } });
|
package/lib/exporter/svg.js
CHANGED
|
@@ -10,11 +10,17 @@ async function exportToSVGString(svg, options = {}) {
|
|
|
10
10
|
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(str);
|
|
11
11
|
}
|
|
12
12
|
async function exportToSVG(svg, options = {}) {
|
|
13
|
-
const { embedResources = true } = options;
|
|
13
|
+
const { embedResources = true, removeIds = false } = options;
|
|
14
14
|
const clonedSVG = svg.cloneNode(true);
|
|
15
15
|
const { width, height } = (0, utils_1.getViewBox)(svg);
|
|
16
16
|
(0, utils_1.setAttributes)(clonedSVG, { width, height });
|
|
17
|
-
|
|
17
|
+
if (removeIds) {
|
|
18
|
+
inlineUseElements(clonedSVG);
|
|
19
|
+
inlineDefsReferences(clonedSVG);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
await embedIcons(clonedSVG);
|
|
23
|
+
}
|
|
18
24
|
await (0, font_1.embedFonts)(clonedSVG, embedResources);
|
|
19
25
|
cleanSVG(clonedSVG);
|
|
20
26
|
return clonedSVG;
|
|
@@ -44,6 +50,221 @@ function getDefs(svg) {
|
|
|
44
50
|
svg.prepend(_defs);
|
|
45
51
|
return _defs;
|
|
46
52
|
}
|
|
53
|
+
function inlineUseElements(svg) {
|
|
54
|
+
const uses = Array.from(svg.querySelectorAll('use'));
|
|
55
|
+
if (!uses.length)
|
|
56
|
+
return;
|
|
57
|
+
uses.forEach((use) => {
|
|
58
|
+
const href = getUseHref(use);
|
|
59
|
+
if (!href || !href.startsWith('#'))
|
|
60
|
+
return;
|
|
61
|
+
const target = resolveUseTarget(svg, href);
|
|
62
|
+
if (!target || target === use)
|
|
63
|
+
return;
|
|
64
|
+
const replacement = createInlineElement(use, target);
|
|
65
|
+
if (!replacement)
|
|
66
|
+
return;
|
|
67
|
+
use.replaceWith(replacement);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function getUseHref(use) {
|
|
71
|
+
return use.getAttribute('href') ?? use.getAttribute('xlink:href');
|
|
72
|
+
}
|
|
73
|
+
function resolveUseTarget(svg, href) {
|
|
74
|
+
const localTarget = svg.querySelector(href);
|
|
75
|
+
if (localTarget)
|
|
76
|
+
return localTarget;
|
|
77
|
+
const docTarget = document.querySelector(href);
|
|
78
|
+
return docTarget;
|
|
79
|
+
}
|
|
80
|
+
function createInlineElement(use, target) {
|
|
81
|
+
const tag = target.tagName.toLowerCase();
|
|
82
|
+
if (tag === 'symbol') {
|
|
83
|
+
return materializeSymbol(use, target);
|
|
84
|
+
}
|
|
85
|
+
if (tag === 'svg') {
|
|
86
|
+
return materializeSVG(use, target);
|
|
87
|
+
}
|
|
88
|
+
return materializeElement(use, target);
|
|
89
|
+
}
|
|
90
|
+
function materializeSymbol(use, symbol) {
|
|
91
|
+
const symbolClone = symbol.cloneNode(true);
|
|
92
|
+
const svg = (0, utils_1.createElement)('svg');
|
|
93
|
+
applyAttributes(svg, symbolClone, new Set(['id']));
|
|
94
|
+
applyAttributes(svg, use, new Set(['href', 'xlink:href']));
|
|
95
|
+
while (symbolClone.firstChild) {
|
|
96
|
+
svg.appendChild(symbolClone.firstChild);
|
|
97
|
+
}
|
|
98
|
+
return svg;
|
|
99
|
+
}
|
|
100
|
+
function materializeSVG(use, source) {
|
|
101
|
+
const clone = source.cloneNode(true);
|
|
102
|
+
clone.removeAttribute('id');
|
|
103
|
+
applyAttributes(clone, use, new Set(['href', 'xlink:href']));
|
|
104
|
+
return clone;
|
|
105
|
+
}
|
|
106
|
+
function materializeElement(use, source) {
|
|
107
|
+
const clone = source.cloneNode(true);
|
|
108
|
+
clone.removeAttribute('id');
|
|
109
|
+
const wrapper = (0, utils_1.createElement)('g');
|
|
110
|
+
applyAttributes(wrapper, use, new Set(['href', 'xlink:href', 'x', 'y', 'width', 'height', 'transform']));
|
|
111
|
+
const transform = buildUseTransform(use);
|
|
112
|
+
if (transform) {
|
|
113
|
+
wrapper.setAttribute('transform', transform);
|
|
114
|
+
}
|
|
115
|
+
wrapper.appendChild(clone);
|
|
116
|
+
return wrapper;
|
|
117
|
+
}
|
|
118
|
+
function buildUseTransform(use) {
|
|
119
|
+
const x = use.getAttribute('x');
|
|
120
|
+
const y = use.getAttribute('y');
|
|
121
|
+
const translate = x || y ? `translate(${x ?? 0} ${y ?? 0})` : '';
|
|
122
|
+
const transform = use.getAttribute('transform') ?? '';
|
|
123
|
+
if (translate && transform)
|
|
124
|
+
return `${translate} ${transform}`;
|
|
125
|
+
return translate || transform || null;
|
|
126
|
+
}
|
|
127
|
+
function applyAttributes(target, source, exclude = new Set()) {
|
|
128
|
+
Array.from(source.attributes).forEach((attr) => {
|
|
129
|
+
if (exclude.has(attr.name))
|
|
130
|
+
return;
|
|
131
|
+
if (attr.name === 'style') {
|
|
132
|
+
mergeStyleAttribute(target, attr.value);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (attr.name === 'class') {
|
|
136
|
+
mergeClassAttribute(target, attr.value);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
target.setAttribute(attr.name, attr.value);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function mergeStyleAttribute(target, value) {
|
|
143
|
+
const current = target.getAttribute('style');
|
|
144
|
+
if (!current) {
|
|
145
|
+
target.setAttribute('style', value);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const separator = current.trim().endsWith(';') ? '' : ';';
|
|
149
|
+
target.setAttribute('style', `${current}${separator}${value}`);
|
|
150
|
+
}
|
|
151
|
+
function mergeClassAttribute(target, value) {
|
|
152
|
+
const current = target.getAttribute('class');
|
|
153
|
+
if (!current) {
|
|
154
|
+
target.setAttribute('class', value);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
target.setAttribute('class', `${current} ${value}`.trim());
|
|
158
|
+
}
|
|
159
|
+
const urlRefRegex = /url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/g;
|
|
160
|
+
function inlineDefsReferences(svg) {
|
|
161
|
+
const referencedIds = collectReferencedIds(svg);
|
|
162
|
+
if (referencedIds.size === 0) {
|
|
163
|
+
removeDefs(svg);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const defsDataUrl = createDefsDataUrl(svg, referencedIds);
|
|
167
|
+
if (!defsDataUrl)
|
|
168
|
+
return;
|
|
169
|
+
(0, utils_1.traverse)(svg, (node) => {
|
|
170
|
+
if (node.tagName.toLowerCase() === 'defs')
|
|
171
|
+
return false;
|
|
172
|
+
const attrs = Array.from(node.attributes);
|
|
173
|
+
attrs.forEach((attr) => {
|
|
174
|
+
const value = attr.value;
|
|
175
|
+
if (!value.includes('url('))
|
|
176
|
+
return;
|
|
177
|
+
const updated = value.replace(urlRefRegex, (_match, id) => {
|
|
178
|
+
const encodedId = encodeURIComponent(id);
|
|
179
|
+
return `url("${defsDataUrl}#${encodedId}")`;
|
|
180
|
+
});
|
|
181
|
+
if (updated !== value)
|
|
182
|
+
node.setAttribute(attr.name, updated);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
removeDefs(svg);
|
|
186
|
+
}
|
|
187
|
+
function collectReferencedIds(svg) {
|
|
188
|
+
const ids = new Set();
|
|
189
|
+
(0, utils_1.traverse)(svg, (node) => {
|
|
190
|
+
if (node.tagName.toLowerCase() === 'defs')
|
|
191
|
+
return false;
|
|
192
|
+
collectIdsFromAttributes(node, (id) => ids.add(id));
|
|
193
|
+
});
|
|
194
|
+
return ids;
|
|
195
|
+
}
|
|
196
|
+
function collectIdsFromAttributes(node, addId) {
|
|
197
|
+
for (const attr of Array.from(node.attributes)) {
|
|
198
|
+
const value = attr.value;
|
|
199
|
+
if (value.includes('url(')) {
|
|
200
|
+
for (const match of value.matchAll(urlRefRegex)) {
|
|
201
|
+
if (match[1])
|
|
202
|
+
addId(match[1]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if ((attr.name === 'href' || attr.name === 'xlink:href') &&
|
|
206
|
+
value[0] === '#') {
|
|
207
|
+
addId(value.slice(1));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function createDefsDataUrl(svg, ids) {
|
|
212
|
+
if (ids.size === 0)
|
|
213
|
+
return null;
|
|
214
|
+
const collected = collectDefElements(svg, ids);
|
|
215
|
+
if (collected.size === 0)
|
|
216
|
+
return null;
|
|
217
|
+
const defsSvg = (0, utils_1.createElement)('svg', {
|
|
218
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
219
|
+
'xmlns:xlink': 'http://www.w3.org/1999/xlink',
|
|
220
|
+
});
|
|
221
|
+
const defs = (0, utils_1.createElement)('defs');
|
|
222
|
+
collected.forEach((node) => {
|
|
223
|
+
defs.appendChild(node.cloneNode(true));
|
|
224
|
+
});
|
|
225
|
+
if (!defs.children.length)
|
|
226
|
+
return null;
|
|
227
|
+
defsSvg.appendChild(defs);
|
|
228
|
+
const serialized = new XMLSerializer().serializeToString(defsSvg);
|
|
229
|
+
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(serialized);
|
|
230
|
+
}
|
|
231
|
+
function collectDefElements(svg, ids) {
|
|
232
|
+
const collected = new Map();
|
|
233
|
+
const queue = Array.from(ids);
|
|
234
|
+
const queued = new Set(queue);
|
|
235
|
+
const visited = new Set();
|
|
236
|
+
const enqueue = (id) => {
|
|
237
|
+
if (visited.has(id) || queued.has(id))
|
|
238
|
+
return;
|
|
239
|
+
queue.push(id);
|
|
240
|
+
queued.add(id);
|
|
241
|
+
};
|
|
242
|
+
while (queue.length) {
|
|
243
|
+
const id = queue.shift();
|
|
244
|
+
if (visited.has(id))
|
|
245
|
+
continue;
|
|
246
|
+
visited.add(id);
|
|
247
|
+
const selector = `#${escapeCssId(id)}`;
|
|
248
|
+
const target = svg.querySelector(selector);
|
|
249
|
+
if (!target)
|
|
250
|
+
continue;
|
|
251
|
+
collected.set(id, target);
|
|
252
|
+
(0, utils_1.traverse)(target, (node) => {
|
|
253
|
+
collectIdsFromAttributes(node, enqueue);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return collected;
|
|
257
|
+
}
|
|
258
|
+
function escapeCssId(id) {
|
|
259
|
+
if (globalThis.CSS && typeof globalThis.CSS.escape === 'function') {
|
|
260
|
+
return globalThis.CSS.escape(id);
|
|
261
|
+
}
|
|
262
|
+
return id.replace(/([!"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g, '\\$1');
|
|
263
|
+
}
|
|
264
|
+
function removeDefs(svg) {
|
|
265
|
+
const defsList = Array.from(svg.querySelectorAll('defs'));
|
|
266
|
+
defsList.forEach((defs) => defs.remove());
|
|
267
|
+
}
|
|
47
268
|
function cleanSVG(svg) {
|
|
48
269
|
removeBtnGroup(svg);
|
|
49
270
|
removeTransientContainer(svg);
|