@antv/infographic 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/README.zh-CN.md +27 -0
- package/dist/infographic.min.js +130 -129
- package/dist/infographic.min.js.map +1 -1
- package/esm/constants/service.d.ts +1 -1
- package/esm/constants/service.js +1 -1
- package/esm/designs/structures/chart-line.js +2 -1
- package/esm/designs/structures/sequence-interaction.js +36 -15
- package/esm/designs/structures/sequence-timeline.d.ts +1 -0
- package/esm/designs/structures/sequence-timeline.js +4 -2
- package/esm/exporter/png.js +2 -2
- package/esm/exporter/svg.js +176 -2
- package/esm/exporter/types.d.ts +10 -0
- package/esm/options/parser.js +8 -6
- package/esm/options/types.d.ts +3 -3
- package/esm/renderer/renderer.js +1 -1
- package/esm/resource/loaders/search.js +2 -3
- package/esm/runtime/options.js +1 -1
- package/esm/syntax/index.js +56 -10
- package/esm/syntax/mapper.js +20 -6
- package/esm/syntax/parser.js +89 -3
- package/esm/syntax/types.d.ts +1 -1
- package/esm/templates/built-in.js +2 -2
- package/esm/templates/registry.d.ts +1 -0
- package/esm/templates/registry.js +6 -0
- package/esm/templates/utils.d.ts +1 -0
- package/esm/templates/utils.js +63 -0
- package/esm/themes/built-in.js +3 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/lib/constants/service.d.ts +1 -1
- package/lib/constants/service.js +1 -1
- package/lib/designs/structures/chart-line.js +2 -1
- package/lib/designs/structures/sequence-interaction.js +36 -15
- package/lib/designs/structures/sequence-timeline.d.ts +1 -0
- package/lib/designs/structures/sequence-timeline.js +4 -2
- package/lib/exporter/png.js +2 -2
- package/lib/exporter/svg.js +176 -2
- package/lib/exporter/types.d.ts +10 -0
- package/lib/options/parser.js +7 -5
- package/lib/options/types.d.ts +3 -3
- package/lib/renderer/renderer.js +1 -1
- package/lib/resource/loaders/search.js +2 -3
- package/lib/runtime/options.js +1 -1
- package/lib/syntax/index.js +56 -10
- package/lib/syntax/mapper.js +20 -6
- package/lib/syntax/parser.js +89 -3
- package/lib/syntax/types.d.ts +1 -1
- package/lib/templates/built-in.js +2 -2
- package/lib/templates/registry.d.ts +1 -0
- package/lib/templates/registry.js +7 -0
- package/lib/templates/utils.d.ts +1 -0
- package/lib/templates/utils.js +66 -0
- package/lib/themes/built-in.js +3 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/constants/service.ts +1 -1
- package/src/designs/structures/chart-line.tsx +3 -1
- package/src/designs/structures/sequence-interaction.tsx +92 -46
- package/src/designs/structures/sequence-timeline.tsx +18 -15
- package/src/exporter/png.ts +3 -2
- package/src/exporter/svg.ts +209 -2
- package/src/exporter/types.ts +10 -0
- package/src/options/parser.ts +7 -6
- package/src/options/types.ts +3 -3
- package/src/renderer/renderer.ts +1 -1
- package/src/resource/loaders/search.ts +2 -2
- package/src/runtime/options.ts +1 -1
- package/src/syntax/index.ts +71 -10
- package/src/syntax/mapper.ts +20 -6
- package/src/syntax/parser.ts +111 -3
- package/src/syntax/types.ts +1 -0
- package/src/templates/built-in.ts +2 -2
- package/src/templates/registry.ts +6 -0
- package/src/templates/utils.ts +87 -0
- package/src/themes/built-in.ts +4 -0
- package/src/version.ts +1 -1
package/esm/syntax/parser.js
CHANGED
|
@@ -23,6 +23,13 @@ function getIndentInfo(line) {
|
|
|
23
23
|
function stripComments(content) {
|
|
24
24
|
return content.trimEnd();
|
|
25
25
|
}
|
|
26
|
+
function isCommentLine(content) {
|
|
27
|
+
const trimmed = content.trimStart();
|
|
28
|
+
return trimmed.startsWith('#') || trimmed.startsWith('//');
|
|
29
|
+
}
|
|
30
|
+
function isCodeFenceLine(content) {
|
|
31
|
+
return /^```[\w-]*\s*$/.test(content.trim());
|
|
32
|
+
}
|
|
26
33
|
function looksLikeRelationExpression(text) {
|
|
27
34
|
return /[<>=o.x-]{2,}/.test(text);
|
|
28
35
|
}
|
|
@@ -40,6 +47,81 @@ function parseKeyValue(raw) {
|
|
|
40
47
|
}
|
|
41
48
|
return { key: text, value: undefined };
|
|
42
49
|
}
|
|
50
|
+
function isUnsafeObjectKey(key) {
|
|
51
|
+
return key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
52
|
+
}
|
|
53
|
+
function assignObjectEntry(parent, rawKey, node, line, errors) {
|
|
54
|
+
if (!rawKey.includes('.')) {
|
|
55
|
+
if (isUnsafeObjectKey(rawKey)) {
|
|
56
|
+
errors.push({
|
|
57
|
+
path: rawKey,
|
|
58
|
+
line,
|
|
59
|
+
code: 'bad_syntax',
|
|
60
|
+
message: `Invalid key part: ${rawKey}`,
|
|
61
|
+
raw: rawKey,
|
|
62
|
+
});
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
parent.entries[rawKey] = node;
|
|
66
|
+
return { parent, key: rawKey };
|
|
67
|
+
}
|
|
68
|
+
const parts = rawKey.split('.');
|
|
69
|
+
if (parts.some((part) => !part)) {
|
|
70
|
+
errors.push({
|
|
71
|
+
path: rawKey,
|
|
72
|
+
line,
|
|
73
|
+
code: 'bad_syntax',
|
|
74
|
+
message: 'Invalid dotted key path.',
|
|
75
|
+
raw: rawKey,
|
|
76
|
+
});
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
let current = parent;
|
|
80
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
81
|
+
const part = parts[index];
|
|
82
|
+
if (isUnsafeObjectKey(part)) {
|
|
83
|
+
errors.push({
|
|
84
|
+
path: rawKey,
|
|
85
|
+
line,
|
|
86
|
+
code: 'bad_syntax',
|
|
87
|
+
message: `Invalid key part in dotted path: ${part}`,
|
|
88
|
+
raw: rawKey,
|
|
89
|
+
});
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const existing = current.entries[part];
|
|
93
|
+
if (!existing) {
|
|
94
|
+
const container = createObjectNode(line);
|
|
95
|
+
current.entries[part] = container;
|
|
96
|
+
current = container;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (existing.kind !== 'object') {
|
|
100
|
+
errors.push({
|
|
101
|
+
path: parts.slice(0, index + 1).join('.'),
|
|
102
|
+
line,
|
|
103
|
+
code: 'bad_syntax',
|
|
104
|
+
message: 'Cannot assign dotted key under a list value.',
|
|
105
|
+
raw: rawKey,
|
|
106
|
+
});
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
current = existing;
|
|
110
|
+
}
|
|
111
|
+
const finalKey = parts[parts.length - 1];
|
|
112
|
+
if (isUnsafeObjectKey(finalKey)) {
|
|
113
|
+
errors.push({
|
|
114
|
+
path: rawKey,
|
|
115
|
+
line,
|
|
116
|
+
code: 'bad_syntax',
|
|
117
|
+
message: `Invalid key part in dotted path: ${finalKey}`,
|
|
118
|
+
raw: rawKey,
|
|
119
|
+
});
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
current.entries[finalKey] = node;
|
|
123
|
+
return { parent: current, key: finalKey };
|
|
124
|
+
}
|
|
43
125
|
function createObjectNode(line, value) {
|
|
44
126
|
return { kind: 'object', line, value, entries: {} };
|
|
45
127
|
}
|
|
@@ -58,6 +140,8 @@ export function parseSyntaxToAst(input) {
|
|
|
58
140
|
if (!line.trim())
|
|
59
141
|
return;
|
|
60
142
|
const { indent, content } = getIndentInfo(line);
|
|
143
|
+
if (isCommentLine(content) || isCodeFenceLine(content))
|
|
144
|
+
return;
|
|
61
145
|
const stripped = stripComments(content);
|
|
62
146
|
if (!stripped.trim())
|
|
63
147
|
return;
|
|
@@ -171,12 +255,14 @@ export function parseSyntaxToAst(input) {
|
|
|
171
255
|
return;
|
|
172
256
|
}
|
|
173
257
|
const node = createObjectNode(lineNumber, parsed.value);
|
|
174
|
-
parentNode
|
|
258
|
+
const assigned = assignObjectEntry(parentNode, parsed.key, node, lineNumber, errors);
|
|
259
|
+
if (!assigned)
|
|
260
|
+
return;
|
|
175
261
|
stack.push({
|
|
176
262
|
indent,
|
|
177
263
|
node,
|
|
178
|
-
parent:
|
|
179
|
-
key:
|
|
264
|
+
parent: assigned.parent,
|
|
265
|
+
key: assigned.key,
|
|
180
266
|
});
|
|
181
267
|
});
|
|
182
268
|
return { ast: root, errors };
|
package/esm/syntax/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface ArrayNode {
|
|
|
16
16
|
line: number;
|
|
17
17
|
items: SyntaxNode[];
|
|
18
18
|
}
|
|
19
|
-
export type SyntaxErrorCode = 'unknown_key' | 'schema_mismatch' | 'invalid_value' | 'bad_indent' | 'bad_list' | 'bad_syntax';
|
|
19
|
+
export type SyntaxErrorCode = 'implicit_template' | 'unknown_key' | 'schema_mismatch' | 'invalid_value' | 'bad_indent' | 'bad_list' | 'bad_syntax';
|
|
20
20
|
export interface SyntaxError {
|
|
21
21
|
path: string;
|
|
22
22
|
line: number;
|
|
@@ -181,7 +181,7 @@ const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assi
|
|
|
181
181
|
}, 'sequence-timeline-plain-text': {
|
|
182
182
|
design: {
|
|
183
183
|
title: 'default',
|
|
184
|
-
structure: { type: 'sequence-timeline' },
|
|
184
|
+
structure: { type: 'sequence-timeline', showStepLabels: false },
|
|
185
185
|
items: [{ type: 'plain-text' }],
|
|
186
186
|
},
|
|
187
187
|
}, 'sequence-timeline-rounded-rect-node': {
|
|
@@ -199,7 +199,7 @@ const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assi
|
|
|
199
199
|
}, 'sequence-timeline-simple': {
|
|
200
200
|
design: {
|
|
201
201
|
title: 'default',
|
|
202
|
-
structure: { type: 'sequence-timeline', gap: 20 },
|
|
202
|
+
structure: { type: 'sequence-timeline', gap: 20, showStepLabels: false },
|
|
203
203
|
items: [{ type: 'simple', positionV: 'middle' }],
|
|
204
204
|
},
|
|
205
205
|
}, 'sequence-cylinders-3d-simple': {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TemplateOptions } from './types';
|
|
2
2
|
export declare function registerTemplate(type: string, template: TemplateOptions): void;
|
|
3
|
+
export declare function resolveTemplateKey(type: string): string | undefined;
|
|
3
4
|
export declare function getTemplate(type: string): TemplateOptions | undefined;
|
|
4
5
|
export declare function getTemplates(): string[];
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { findClosestTemplateKey } from './utils.js';
|
|
1
2
|
const TEMPLATE_REGISTRY = new Map();
|
|
2
3
|
export function registerTemplate(type, template) {
|
|
3
4
|
TEMPLATE_REGISTRY.set(type, template);
|
|
4
5
|
}
|
|
6
|
+
export function resolveTemplateKey(type) {
|
|
7
|
+
if (TEMPLATE_REGISTRY.has(type))
|
|
8
|
+
return type;
|
|
9
|
+
return findClosestTemplateKey(type, TEMPLATE_REGISTRY.keys());
|
|
10
|
+
}
|
|
5
11
|
export function getTemplate(type) {
|
|
6
12
|
return TEMPLATE_REGISTRY.get(type);
|
|
7
13
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findClosestTemplateKey(type: string, keys: Iterable<string>): string | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function normalizeTemplateKey(type) {
|
|
2
|
+
return type
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[_\s]+/g, '-')
|
|
6
|
+
.replace(/-+/g, '-');
|
|
7
|
+
}
|
|
8
|
+
function getLevenshteinDistance(source, target) {
|
|
9
|
+
if (source === target)
|
|
10
|
+
return 0;
|
|
11
|
+
if (!source.length)
|
|
12
|
+
return target.length;
|
|
13
|
+
if (!target.length)
|
|
14
|
+
return source.length;
|
|
15
|
+
const previous = Array.from({ length: target.length + 1 }, (_, index) => index);
|
|
16
|
+
const current = new Array(target.length + 1);
|
|
17
|
+
for (let sourceIndex = 1; sourceIndex <= source.length; sourceIndex += 1) {
|
|
18
|
+
current[0] = sourceIndex;
|
|
19
|
+
const sourceCode = source.charCodeAt(sourceIndex - 1);
|
|
20
|
+
for (let targetIndex = 1; targetIndex <= target.length; targetIndex += 1) {
|
|
21
|
+
const replaceCost = sourceCode === target.charCodeAt(targetIndex - 1) ? 0 : 1;
|
|
22
|
+
current[targetIndex] = Math.min(previous[targetIndex] + 1, current[targetIndex - 1] + 1, previous[targetIndex - 1] + replaceCost);
|
|
23
|
+
}
|
|
24
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
25
|
+
previous[index] = current[index];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return previous[target.length];
|
|
29
|
+
}
|
|
30
|
+
function getCommonPrefixLength(source, target) {
|
|
31
|
+
const limit = Math.min(source.length, target.length);
|
|
32
|
+
let index = 0;
|
|
33
|
+
while (index < limit && source[index] === target[index]) {
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
return index;
|
|
37
|
+
}
|
|
38
|
+
export function findClosestTemplateKey(type, keys) {
|
|
39
|
+
const normalizedType = normalizeTemplateKey(type);
|
|
40
|
+
if (!normalizedType)
|
|
41
|
+
return undefined;
|
|
42
|
+
let bestMatch;
|
|
43
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
44
|
+
let bestPrefixLength = -1;
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
const normalizedKey = normalizeTemplateKey(key);
|
|
47
|
+
if (normalizedKey === normalizedType) {
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
const distance = getLevenshteinDistance(normalizedType, normalizedKey);
|
|
51
|
+
const prefixLength = getCommonPrefixLength(normalizedType, normalizedKey);
|
|
52
|
+
if (distance < bestDistance ||
|
|
53
|
+
(distance === bestDistance && prefixLength > bestPrefixLength) ||
|
|
54
|
+
(distance === bestDistance &&
|
|
55
|
+
prefixLength === bestPrefixLength &&
|
|
56
|
+
(!bestMatch || key < bestMatch))) {
|
|
57
|
+
bestMatch = key;
|
|
58
|
+
bestDistance = distance;
|
|
59
|
+
bestPrefixLength = prefixLength;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return bestMatch;
|
|
63
|
+
}
|
package/esm/themes/built-in.js
CHANGED
package/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.17";
|
package/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.17';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ICON_SERVICE_URL = "https://
|
|
1
|
+
export declare const ICON_SERVICE_URL = "https://lab.weavefox.cn/api/v1/infographic/icon";
|
package/lib/constants/service.js
CHANGED
|
@@ -70,10 +70,11 @@ const ChartLine = (props) => {
|
|
|
70
70
|
const titleElements = [];
|
|
71
71
|
const tickElements = [];
|
|
72
72
|
const ticksY = scaleY.ticks(6);
|
|
73
|
+
const formatTickY = scaleY.tickFormat(6);
|
|
73
74
|
ticksY.forEach((tick) => {
|
|
74
75
|
const yPos = chartOriginY + scaleY(tick);
|
|
75
76
|
gridElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M ${chartOriginX} ${yPos} L ${chartOriginX + derivedChartWidth} ${yPos}`, width: derivedChartWidth, height: 1, stroke: axisColor, strokeWidth: 1, "data-element-type": "shape", opacity: 0.08 }));
|
|
76
|
-
tickElements.push((0, jsx_runtime_1.jsx)(jsx_1.Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children:
|
|
77
|
+
tickElements.push((0, jsx_runtime_1.jsx)(jsx_1.Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children: formatTickY(tick) }));
|
|
77
78
|
});
|
|
78
79
|
const xLabels = [];
|
|
79
80
|
const pointPositions = [];
|
|
@@ -18,6 +18,7 @@ const DEFAULT_ITEM_HEIGHT = 50;
|
|
|
18
18
|
const FONT_SIZE = 14;
|
|
19
19
|
const ARROW_SIZE = 14;
|
|
20
20
|
const CORNER_RADIUS_NODE = 6;
|
|
21
|
+
const LIFELINE_MASK_GAP = 2;
|
|
21
22
|
const LANE_PADDING = 60;
|
|
22
23
|
const BTN_HALF_SIZE = 12;
|
|
23
24
|
const BTN_MARGIN = 10;
|
|
@@ -232,17 +233,6 @@ const SequenceInteractionFlow = (props) => {
|
|
|
232
233
|
const decorElements = [];
|
|
233
234
|
const defsElements = [];
|
|
234
235
|
const btnElements = [];
|
|
235
|
-
// 绘制生命线
|
|
236
|
-
if (showLifeline) {
|
|
237
|
-
lanes.forEach((_lane, laneIndex) => {
|
|
238
|
-
const centerX = getLaneCenterX(laneIndex);
|
|
239
|
-
const startY = padding + headerOffset;
|
|
240
|
-
const endY = totalHeight - padding;
|
|
241
|
-
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape" }));
|
|
242
|
-
// 绘制生命线末端箭头(实心)
|
|
243
|
-
decorElements.push(...(0, utils_1.createArrowElements)(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
236
|
// 绘制泳道标题
|
|
247
237
|
if (showLaneHeader) {
|
|
248
238
|
lanes.forEach((lane, laneIndex) => {
|
|
@@ -285,10 +275,6 @@ const SequenceInteractionFlow = (props) => {
|
|
|
285
275
|
});
|
|
286
276
|
const nodeColor = (0, utils_1.getPaletteColor)(options, [laneIndex]);
|
|
287
277
|
const nodeThemeColors = (0, utils_1.getThemeColors)({ colorPrimary: nodeColor }, options);
|
|
288
|
-
// 添加节点背景遮挡层,防止生命线虚线透过半透明节点显示
|
|
289
|
-
// 只在节点中心放置窄条遮挡生命线,避免圆角处露出白色背景
|
|
290
|
-
const maskStripWidth = lifelineWidth + 6;
|
|
291
|
-
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Rect, { x: centerX - maskStripWidth / 2, y: y, width: maskStripWidth, height: itemHeight, fill: colorBg }));
|
|
292
278
|
// 构造类似 hierarchy-tree 的 _originalIndex
|
|
293
279
|
const originalIndex = [laneIndex, rowIndex];
|
|
294
280
|
// 附加到数据上,确保 Item 组件能正确识别
|
|
@@ -325,6 +311,41 @@ const SequenceInteractionFlow = (props) => {
|
|
|
325
311
|
const centerX = getLaneCenterX(laneIndex);
|
|
326
312
|
btnElements.push((0, jsx_runtime_1.jsx)(components_1.BtnAdd, { indexes: [laneIndex, childCount], x: centerX - BTN_HALF_SIZE, y: addNodeY }));
|
|
327
313
|
});
|
|
314
|
+
// 绘制生命线(使用 mask 挖空节点区域,避免虚线穿透半透明节点)
|
|
315
|
+
if (showLifeline) {
|
|
316
|
+
// 预先按泳道分组节点,避免每条泳道都遍历全部节点
|
|
317
|
+
const nodeRectsByLane = new Map();
|
|
318
|
+
nodeLayoutById.forEach((layout) => {
|
|
319
|
+
let list = nodeRectsByLane.get(layout.laneIndex);
|
|
320
|
+
if (!list) {
|
|
321
|
+
list = [];
|
|
322
|
+
nodeRectsByLane.set(layout.laneIndex, list);
|
|
323
|
+
}
|
|
324
|
+
list.push({
|
|
325
|
+
x: layout.x,
|
|
326
|
+
y: layout.y,
|
|
327
|
+
width: layout.width,
|
|
328
|
+
height: layout.height,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
lanes.forEach((_lane, laneIndex) => {
|
|
332
|
+
var _a;
|
|
333
|
+
const centerX = getLaneCenterX(laneIndex);
|
|
334
|
+
const startY = padding + headerOffset;
|
|
335
|
+
const endY = totalHeight - padding;
|
|
336
|
+
const laneNodeRects = (_a = nodeRectsByLane.get(laneIndex)) !== null && _a !== void 0 ? _a : [];
|
|
337
|
+
// 如果该泳道有节点,创建 mask 来挖空节点区域
|
|
338
|
+
let lifelineMaskAttr;
|
|
339
|
+
if (laneNodeRects.length > 0) {
|
|
340
|
+
const maskId = `lifeline-mask-${instanceId}-${laneIndex}`;
|
|
341
|
+
defsElements.push((0, jsx_runtime_1.jsxs)("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: 0, y: 0, width: totalWidth, height: totalHeight, children: [(0, jsx_runtime_1.jsx)(jsx_1.Rect, { x: 0, y: 0, width: totalWidth, height: totalHeight, fill: "white" }), laneNodeRects.map((rect) => ((0, jsx_runtime_1.jsx)(jsx_1.Rect, { x: rect.x, y: rect.y - LIFELINE_MASK_GAP, width: rect.width, height: rect.height + LIFELINE_MASK_GAP * 2, fill: "black" })))] }));
|
|
342
|
+
lifelineMaskAttr = `url(#${maskId})`;
|
|
343
|
+
}
|
|
344
|
+
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape", mask: lifelineMaskAttr }));
|
|
345
|
+
// 绘制生命线末端箭头(实心)
|
|
346
|
+
decorElements.push(...(0, utils_1.createArrowElements)(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
328
349
|
// 添加新泳道按钮 (最右侧)
|
|
329
350
|
const lastLaneRightX = getLaneCenterX(lanes.length - 1) + laneWidth / 2;
|
|
330
351
|
const newLaneX = lanes.length > 0 ? lastLaneRightX + BTN_LANE_ADD_Gap : padding;
|
|
@@ -8,7 +8,7 @@ const layouts_1 = require("../layouts");
|
|
|
8
8
|
const utils_1 = require("../utils");
|
|
9
9
|
const registry_1 = require("./registry");
|
|
10
10
|
const SequenceTimeline = (props) => {
|
|
11
|
-
const { Title, Item, data, gap = 10, options } = props;
|
|
11
|
+
const { Title, Item, data, gap = 10, showStepLabels = true, options } = props;
|
|
12
12
|
const { title, desc, items = [] } = data;
|
|
13
13
|
const titleContent = Title ? (0, jsx_runtime_1.jsx)(Title, { title: title, desc: desc }) : null;
|
|
14
14
|
const colorPrimary = (0, utils_1.getColorPrimary)(options);
|
|
@@ -44,7 +44,9 @@ const SequenceTimeline = (props) => {
|
|
|
44
44
|
const itemY = index * (itemBounds.height + gap);
|
|
45
45
|
const nodeY = itemY + itemBounds.height / 2;
|
|
46
46
|
const indexes = [index];
|
|
47
|
-
|
|
47
|
+
if (showStepLabels) {
|
|
48
|
+
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Text, { x: stepLabelX, y: nodeY, width: 70, fontSize: 18, fontWeight: "bold", alignHorizontal: "left", alignVertical: "middle", fill: palette[index % palette.length], children: `STEP ${index + 1}` }));
|
|
49
|
+
}
|
|
48
50
|
itemElements.push((0, jsx_runtime_1.jsx)(Item, { indexes: indexes, datum: item, data: data, x: itemX, y: itemY, positionH: "normal" }));
|
|
49
51
|
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Ellipse, { x: timelineX - nodeRadius, y: nodeY - nodeRadius, width: nodeRadius * 2, height: nodeRadius * 2, fill: palette[index % palette.length] }));
|
|
50
52
|
btnElements.push((0, jsx_runtime_1.jsx)(components_1.BtnRemove, { indexes: indexes, x: itemX - btnBounds.width - 10, y: itemY + (itemBounds.height - btnBounds.height) / 2 }));
|
package/lib/exporter/png.js
CHANGED
|
@@ -15,8 +15,8 @@ const svg_1 = require("./svg");
|
|
|
15
15
|
function exportToPNGString(svg_2) {
|
|
16
16
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
17
17
|
var _a;
|
|
18
|
-
const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2 } = options;
|
|
19
|
-
const node = yield (0, svg_1.exportToSVG)(svg);
|
|
18
|
+
const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2, removeBackground = false } = options;
|
|
19
|
+
const node = yield (0, svg_1.exportToSVG)(svg, { removeBackground });
|
|
20
20
|
const { width, height } = (0, utils_1.getViewBox)(node);
|
|
21
21
|
return new Promise((resolve, reject) => {
|
|
22
22
|
try {
|
package/lib/exporter/svg.js
CHANGED
|
@@ -13,6 +13,7 @@ exports.exportToSVGString = exportToSVGString;
|
|
|
13
13
|
exports.exportToSVG = exportToSVG;
|
|
14
14
|
const utils_1 = require("../utils");
|
|
15
15
|
const font_1 = require("./font");
|
|
16
|
+
const VIEWBOX_CHANGE_TOLERANCE = 0.5;
|
|
16
17
|
function exportToSVGString(svg_1) {
|
|
17
18
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
18
19
|
const node = yield exportToSVG(svg, options);
|
|
@@ -20,11 +21,176 @@ function exportToSVGString(svg_1) {
|
|
|
20
21
|
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(str);
|
|
21
22
|
});
|
|
22
23
|
}
|
|
24
|
+
function getExportViewBox(svg) {
|
|
25
|
+
if (svg.hasAttribute('viewBox'))
|
|
26
|
+
return (0, utils_1.getViewBox)(svg);
|
|
27
|
+
const width = parseAbsoluteLength(svg.getAttribute('width'));
|
|
28
|
+
const height = parseAbsoluteLength(svg.getAttribute('height'));
|
|
29
|
+
if (width > 0 && height > 0) {
|
|
30
|
+
return { x: 0, y: 0, width, height };
|
|
31
|
+
}
|
|
32
|
+
const rect = svg.getBoundingClientRect();
|
|
33
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
34
|
+
return { x: 0, y: 0, width: rect.width, height: rect.height };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function parseAbsoluteLength(value) {
|
|
39
|
+
if (!value)
|
|
40
|
+
return Number.NaN;
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (!trimmed)
|
|
43
|
+
return Number.NaN;
|
|
44
|
+
if (!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:px)?$/.test(trimmed))
|
|
45
|
+
return Number.NaN;
|
|
46
|
+
return Number.parseFloat(trimmed);
|
|
47
|
+
}
|
|
48
|
+
function measureSpanContentHeight(span) {
|
|
49
|
+
const prevHeight = span.style.height;
|
|
50
|
+
const prevOverflow = span.style.overflow;
|
|
51
|
+
try {
|
|
52
|
+
span.style.height = 'max-content';
|
|
53
|
+
span.style.overflow = 'hidden';
|
|
54
|
+
void span.offsetHeight; // force reflow
|
|
55
|
+
return span.scrollHeight;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
span.style.height = prevHeight;
|
|
59
|
+
span.style.overflow = prevOverflow;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function measureSpanContentWidth(span) {
|
|
63
|
+
const prevWidth = span.style.width;
|
|
64
|
+
const prevOverflow = span.style.overflow;
|
|
65
|
+
try {
|
|
66
|
+
span.style.width = 'max-content';
|
|
67
|
+
span.style.overflow = 'hidden';
|
|
68
|
+
void span.offsetWidth; // force reflow
|
|
69
|
+
return span.scrollWidth;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
span.style.width = prevWidth;
|
|
73
|
+
span.style.overflow = prevOverflow;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Returns [left, top, right, bottom] in SVG coordinates for a foreignObject,
|
|
77
|
+
// accounting for flex alignment: bottom/center-aligned content can overflow,
|
|
78
|
+
// and horizontally aligned content can overflow as well.
|
|
79
|
+
function getFOContentBoundsInSVG(fo, content, toSVGCoord) {
|
|
80
|
+
const foRect = fo.getBoundingClientRect();
|
|
81
|
+
const foTopLeft = toSVGCoord(foRect.left, foRect.top);
|
|
82
|
+
const foBottomRight = toSVGCoord(foRect.right, foRect.bottom);
|
|
83
|
+
const foLeftSVG = foTopLeft.x;
|
|
84
|
+
const foTopSVG = foTopLeft.y;
|
|
85
|
+
const foRightSVG = foBottomRight.x;
|
|
86
|
+
const foBottomSVG = foBottomRight.y;
|
|
87
|
+
const foWidthSVG = foRightSVG - foLeftSVG;
|
|
88
|
+
const foHeightSVG = foBottomSVG - foTopSVG;
|
|
89
|
+
const svgUnitsPerClientPxY = foRect.height > 0 ? foHeightSVG / foRect.height : 1;
|
|
90
|
+
const svgUnitsPerClientPxX = foRect.width > 0 ? foWidthSVG / foRect.width : 1;
|
|
91
|
+
// Measure actual content dimensions
|
|
92
|
+
const realScrollHeight = measureSpanContentHeight(content);
|
|
93
|
+
const contentHeightSVG = realScrollHeight > 0
|
|
94
|
+
? realScrollHeight * svgUnitsPerClientPxY
|
|
95
|
+
: foHeightSVG;
|
|
96
|
+
const realScrollWidth = measureSpanContentWidth(content);
|
|
97
|
+
const contentWidthSVG = realScrollWidth > 0 ? realScrollWidth * svgUnitsPerClientPxX : foWidthSVG;
|
|
98
|
+
const computedStyle = window.getComputedStyle(content);
|
|
99
|
+
const alignItems = computedStyle.alignItems;
|
|
100
|
+
const justifyContent = computedStyle.justifyContent;
|
|
101
|
+
// Calculate vertical bounds
|
|
102
|
+
let top, bottom;
|
|
103
|
+
if (alignItems === 'flex-end' || alignItems === 'end') {
|
|
104
|
+
top = foBottomSVG - contentHeightSVG;
|
|
105
|
+
bottom = foBottomSVG;
|
|
106
|
+
}
|
|
107
|
+
else if (alignItems === 'center') {
|
|
108
|
+
const overflowY = contentHeightSVG - foHeightSVG;
|
|
109
|
+
top = foTopSVG - overflowY / 2;
|
|
110
|
+
bottom = foBottomSVG + overflowY / 2;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
top = foTopSVG;
|
|
114
|
+
bottom = foTopSVG + contentHeightSVG;
|
|
115
|
+
}
|
|
116
|
+
// Calculate horizontal bounds
|
|
117
|
+
let left, right;
|
|
118
|
+
if (justifyContent === 'flex-end' ||
|
|
119
|
+
justifyContent === 'end' ||
|
|
120
|
+
justifyContent === 'right') {
|
|
121
|
+
left = foRightSVG - contentWidthSVG;
|
|
122
|
+
right = foRightSVG;
|
|
123
|
+
}
|
|
124
|
+
else if (justifyContent === 'center') {
|
|
125
|
+
const overflowX = contentWidthSVG - foWidthSVG;
|
|
126
|
+
left = foLeftSVG - overflowX / 2;
|
|
127
|
+
right = foRightSVG + overflowX / 2;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
left = foLeftSVG;
|
|
131
|
+
right = foLeftSVG + contentWidthSVG;
|
|
132
|
+
}
|
|
133
|
+
return [left, top, right, bottom];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Computes a viewBox that fully covers all foreignObject text content,
|
|
137
|
+
* accounting for overflow caused by flex alignment (bottom/center align
|
|
138
|
+
* can push content outside the foreignObject bounds).
|
|
139
|
+
*/
|
|
140
|
+
function computeFullViewBox(svg) {
|
|
141
|
+
const viewBox = getExportViewBox(svg);
|
|
142
|
+
if (!viewBox)
|
|
143
|
+
return null;
|
|
144
|
+
if (typeof svg.getScreenCTM !== 'function')
|
|
145
|
+
return null;
|
|
146
|
+
const screenCTM = svg.getScreenCTM();
|
|
147
|
+
if (!screenCTM)
|
|
148
|
+
return null;
|
|
149
|
+
const inverseCTM = screenCTM.inverse();
|
|
150
|
+
const toSVGCoord = (clientX, clientY) => {
|
|
151
|
+
const pt = svg.createSVGPoint();
|
|
152
|
+
pt.x = clientX;
|
|
153
|
+
pt.y = clientY;
|
|
154
|
+
return pt.matrixTransform(inverseCTM);
|
|
155
|
+
};
|
|
156
|
+
let minX = viewBox.x;
|
|
157
|
+
let minY = viewBox.y;
|
|
158
|
+
let maxX = viewBox.x + viewBox.width;
|
|
159
|
+
let maxY = viewBox.y + viewBox.height;
|
|
160
|
+
svg
|
|
161
|
+
.querySelectorAll('foreignObject')
|
|
162
|
+
.forEach((fo) => {
|
|
163
|
+
const content = fo.firstElementChild;
|
|
164
|
+
if (!content)
|
|
165
|
+
return;
|
|
166
|
+
const [left, top, right, bottom] = getFOContentBoundsInSVG(fo, content, toSVGCoord);
|
|
167
|
+
minX = Math.min(minX, left);
|
|
168
|
+
minY = Math.min(minY, top);
|
|
169
|
+
maxX = Math.max(maxX, right);
|
|
170
|
+
maxY = Math.max(maxY, bottom);
|
|
171
|
+
});
|
|
172
|
+
const newX = minX;
|
|
173
|
+
const newY = minY;
|
|
174
|
+
const newWidth = maxX - newX;
|
|
175
|
+
const newHeight = maxY - newY;
|
|
176
|
+
if (newWidth <= viewBox.width + VIEWBOX_CHANGE_TOLERANCE &&
|
|
177
|
+
newHeight <= viewBox.height + VIEWBOX_CHANGE_TOLERANCE &&
|
|
178
|
+
newX >= viewBox.x - VIEWBOX_CHANGE_TOLERANCE &&
|
|
179
|
+
newY >= viewBox.y - VIEWBOX_CHANGE_TOLERANCE)
|
|
180
|
+
return null;
|
|
181
|
+
return `${newX} ${newY} ${newWidth} ${newHeight}`;
|
|
182
|
+
}
|
|
23
183
|
function exportToSVG(svg_1) {
|
|
24
184
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
25
|
-
const { embedResources = true, removeIds = false } = options;
|
|
185
|
+
const { removeBackground = false, embedResources = true, removeIds = false, } = options;
|
|
26
186
|
const clonedSVG = svg.cloneNode(true);
|
|
27
|
-
|
|
187
|
+
if (typeof document !== 'undefined') {
|
|
188
|
+
const fullViewBox = computeFullViewBox(svg);
|
|
189
|
+
if (fullViewBox) {
|
|
190
|
+
clonedSVG.setAttribute('viewBox', fullViewBox);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const { width, height } = (0, utils_1.getViewBox)(clonedSVG);
|
|
28
194
|
(0, utils_1.setAttributes)(clonedSVG, { width, height });
|
|
29
195
|
if (removeIds) {
|
|
30
196
|
inlineUseElements(clonedSVG);
|
|
@@ -34,6 +200,9 @@ function exportToSVG(svg_1) {
|
|
|
34
200
|
yield embedIcons(clonedSVG);
|
|
35
201
|
}
|
|
36
202
|
yield (0, font_1.embedFonts)(clonedSVG, embedResources);
|
|
203
|
+
if (removeBackground) {
|
|
204
|
+
removeSVGBackground(clonedSVG);
|
|
205
|
+
}
|
|
37
206
|
cleanSVG(clonedSVG);
|
|
38
207
|
return clonedSVG;
|
|
39
208
|
});
|
|
@@ -288,6 +457,11 @@ function cleanSVG(svg) {
|
|
|
288
457
|
removeUselessAttrs(svg);
|
|
289
458
|
clearDataset(svg);
|
|
290
459
|
}
|
|
460
|
+
function removeSVGBackground(svg) {
|
|
461
|
+
svg.style.removeProperty('background-color');
|
|
462
|
+
const background = (0, utils_1.getElementByRole)(svg, "background" /* ElementTypeEnum.Background */);
|
|
463
|
+
background === null || background === void 0 ? void 0 : background.remove();
|
|
464
|
+
}
|
|
291
465
|
function removeBtnGroup(svg) {
|
|
292
466
|
const btnGroup = (0, utils_1.getElementByRole)(svg, "btns-group" /* ElementTypeEnum.BtnsGroup */);
|
|
293
467
|
btnGroup === null || btnGroup === void 0 ? void 0 : btnGroup.remove();
|
package/lib/exporter/types.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export interface SVGExportOptions {
|
|
2
2
|
type: 'svg';
|
|
3
|
+
/**
|
|
4
|
+
* 是否移除背景(SVG 背景样式 + 背景矩形)
|
|
5
|
+
* @default false
|
|
6
|
+
*/
|
|
7
|
+
removeBackground?: boolean;
|
|
3
8
|
/**
|
|
4
9
|
* 是否将远程资源嵌入到 SVG 中
|
|
5
10
|
* @default true
|
|
@@ -13,6 +18,11 @@ export interface SVGExportOptions {
|
|
|
13
18
|
}
|
|
14
19
|
export interface PNGExportOptions {
|
|
15
20
|
type: 'png';
|
|
21
|
+
/**
|
|
22
|
+
* 是否移除背景(SVG 背景样式 + 背景矩形)
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
removeBackground?: boolean;
|
|
16
26
|
/**
|
|
17
27
|
* 设备像素比,默认为浏览器的 devicePixelRatio
|
|
18
28
|
* @default globalThis.devicePixelRatio || 2
|