@antv/infographic 0.2.15 → 0.2.16

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.
@@ -100,6 +100,7 @@ const DEFAULT_ITEM_HEIGHT = 50;
100
100
  const FONT_SIZE = 14;
101
101
  const ARROW_SIZE = 14;
102
102
  const CORNER_RADIUS_NODE = 6;
103
+ const LIFELINE_MASK_GAP = 2;
103
104
  const LANE_PADDING = 60;
104
105
 
105
106
  const BTN_HALF_SIZE = 12;
@@ -419,39 +420,6 @@ export const SequenceInteractionFlow: ComponentType<
419
420
  const defsElements: JSXElement[] = [];
420
421
  const btnElements: JSXElement[] = [];
421
422
 
422
- // 绘制生命线
423
- if (showLifeline) {
424
- lanes.forEach((_lane, laneIndex) => {
425
- const centerX = getLaneCenterX(laneIndex);
426
- const startY = padding + headerOffset;
427
- const endY = totalHeight - padding;
428
-
429
- decorElements.push(
430
- <Path
431
- d={`M ${centerX} ${startY} L ${centerX} ${endY}`}
432
- stroke={colorBorder}
433
- strokeWidth={lifelineWidth}
434
- strokeDasharray="5,5"
435
- fill="none"
436
- data-element-type="shape"
437
- />,
438
- );
439
-
440
- // 绘制生命线末端箭头(实心)
441
- decorElements.push(
442
- ...createArrowElements(
443
- centerX,
444
- endY,
445
- Math.PI / 2,
446
- 'triangle',
447
- colorBorder,
448
- 1,
449
- 10,
450
- ),
451
- );
452
- });
453
- }
454
-
455
423
  // 绘制泳道标题
456
424
  if (showLaneHeader) {
457
425
  lanes.forEach((lane, laneIndex) => {
@@ -523,19 +491,6 @@ export const SequenceInteractionFlow: ComponentType<
523
491
  options,
524
492
  );
525
493
 
526
- // 添加节点背景遮挡层,防止生命线虚线透过半透明节点显示
527
- // 只在节点中心放置窄条遮挡生命线,避免圆角处露出白色背景
528
- const maskStripWidth = lifelineWidth + 6;
529
- decorElements.push(
530
- <Rect
531
- x={centerX - maskStripWidth / 2}
532
- y={y}
533
- width={maskStripWidth}
534
- height={itemHeight}
535
- fill={colorBg}
536
- />,
537
- );
538
-
539
494
  // 构造类似 hierarchy-tree 的 _originalIndex
540
495
  const originalIndex = [laneIndex, rowIndex];
541
496
  // 附加到数据上,确保 Item 组件能正确识别
@@ -631,6 +586,97 @@ export const SequenceInteractionFlow: ComponentType<
631
586
  );
632
587
  });
633
588
 
589
+ // 绘制生命线(使用 mask 挖空节点区域,避免虚线穿透半透明节点)
590
+ if (showLifeline) {
591
+ // 预先按泳道分组节点,避免每条泳道都遍历全部节点
592
+ const nodeRectsByLane = new Map<
593
+ number,
594
+ { x: number; y: number; width: number; height: number }[]
595
+ >();
596
+ nodeLayoutById.forEach((layout) => {
597
+ let list = nodeRectsByLane.get(layout.laneIndex);
598
+ if (!list) {
599
+ list = [];
600
+ nodeRectsByLane.set(layout.laneIndex, list);
601
+ }
602
+ list.push({
603
+ x: layout.x,
604
+ y: layout.y,
605
+ width: layout.width,
606
+ height: layout.height,
607
+ });
608
+ });
609
+
610
+ lanes.forEach((_lane, laneIndex) => {
611
+ const centerX = getLaneCenterX(laneIndex);
612
+ const startY = padding + headerOffset;
613
+ const endY = totalHeight - padding;
614
+
615
+ const laneNodeRects = nodeRectsByLane.get(laneIndex) ?? [];
616
+
617
+ // 如果该泳道有节点,创建 mask 来挖空节点区域
618
+ let lifelineMaskAttr: string | undefined;
619
+ if (laneNodeRects.length > 0) {
620
+ const maskId = `lifeline-mask-${instanceId}-${laneIndex}`;
621
+ defsElements.push(
622
+ <mask
623
+ id={maskId}
624
+ maskUnits="userSpaceOnUse"
625
+ x={0}
626
+ y={0}
627
+ width={totalWidth}
628
+ height={totalHeight}
629
+ >
630
+ {/* 白色底:显示所有线条 */}
631
+ <Rect
632
+ x={0}
633
+ y={0}
634
+ width={totalWidth}
635
+ height={totalHeight}
636
+ fill="white"
637
+ />
638
+ {/* 黑色块:在节点位置挖空生命线,上下各留 LIFELINE_MASK_GAP 间距 */}
639
+ {laneNodeRects.map((rect) => (
640
+ <Rect
641
+ x={rect.x}
642
+ y={rect.y - LIFELINE_MASK_GAP}
643
+ width={rect.width}
644
+ height={rect.height + LIFELINE_MASK_GAP * 2}
645
+ fill="black"
646
+ />
647
+ ))}
648
+ </mask>,
649
+ );
650
+ lifelineMaskAttr = `url(#${maskId})`;
651
+ }
652
+
653
+ decorElements.push(
654
+ <Path
655
+ d={`M ${centerX} ${startY} L ${centerX} ${endY}`}
656
+ stroke={colorBorder}
657
+ strokeWidth={lifelineWidth}
658
+ strokeDasharray="5,5"
659
+ fill="none"
660
+ data-element-type="shape"
661
+ mask={lifelineMaskAttr}
662
+ />,
663
+ );
664
+
665
+ // 绘制生命线末端箭头(实心)
666
+ decorElements.push(
667
+ ...createArrowElements(
668
+ centerX,
669
+ endY,
670
+ Math.PI / 2,
671
+ 'triangle',
672
+ colorBorder,
673
+ 1,
674
+ 10,
675
+ ),
676
+ );
677
+ });
678
+ }
679
+
634
680
  // 添加新泳道按钮 (最右侧)
635
681
  const lastLaneRightX = getLaneCenterX(lanes.length - 1) + laneWidth / 2;
636
682
  const newLaneX =
@@ -15,12 +15,13 @@ import type { BaseStructureProps } from './types';
15
15
  export interface SequenceTimelineProps extends BaseStructureProps {
16
16
  gap?: number;
17
17
  lineOffset?: number;
18
+ showStepLabels?: boolean;
18
19
  }
19
20
 
20
21
  export const SequenceTimeline: ComponentType<SequenceTimelineProps> = (
21
22
  props,
22
23
  ) => {
23
- const { Title, Item, data, gap = 10, options } = props;
24
+ const { Title, Item, data, gap = 10, showStepLabels = true, options } = props;
24
25
  const { title, desc, items = [] } = data;
25
26
 
26
27
  const titleContent = Title ? <Title title={title} desc={desc} /> : null;
@@ -90,20 +91,22 @@ export const SequenceTimeline: ComponentType<SequenceTimelineProps> = (
90
91
  const nodeY = itemY + itemBounds.height / 2;
91
92
  const indexes = [index];
92
93
 
93
- decorElements.push(
94
- <Text
95
- x={stepLabelX}
96
- y={nodeY}
97
- width={70}
98
- fontSize={18}
99
- fontWeight="bold"
100
- alignHorizontal="left"
101
- alignVertical="middle"
102
- fill={palette[index % palette.length]}
103
- >
104
- {`STEP ${index + 1}`}
105
- </Text>,
106
- );
94
+ if (showStepLabels) {
95
+ decorElements.push(
96
+ <Text
97
+ x={stepLabelX}
98
+ y={nodeY}
99
+ width={70}
100
+ fontSize={18}
101
+ fontWeight="bold"
102
+ alignHorizontal="left"
103
+ alignVertical="middle"
104
+ fill={palette[index % palette.length]}
105
+ >
106
+ {`STEP ${index + 1}`}
107
+ </Text>,
108
+ );
109
+ }
107
110
 
108
111
  itemElements.push(
109
112
  <Item
@@ -6,8 +6,9 @@ export async function exportToPNGString(
6
6
  svg: SVGSVGElement,
7
7
  options: Omit<PNGExportOptions, 'type'> = {},
8
8
  ): Promise<string> {
9
- const { dpr = globalThis.devicePixelRatio ?? 2 } = options;
10
- const node = await exportToSVG(svg);
9
+ const { dpr = globalThis.devicePixelRatio ?? 2, removeBackground = false } =
10
+ options;
11
+ const node = await exportToSVG(svg, { removeBackground });
11
12
 
12
13
  const { width, height } = getViewBox(node);
13
14
 
@@ -23,7 +23,11 @@ export async function exportToSVG(
23
23
  svg: SVGSVGElement,
24
24
  options: Omit<SVGExportOptions, 'type'> = {},
25
25
  ) {
26
- const { embedResources = true, removeIds = false } = options;
26
+ const {
27
+ removeBackground = false,
28
+ embedResources = true,
29
+ removeIds = false,
30
+ } = options;
27
31
  const clonedSVG = svg.cloneNode(true) as SVGSVGElement;
28
32
  const { width, height } = getViewBox(svg);
29
33
  setAttributes(clonedSVG, { width, height });
@@ -36,6 +40,9 @@ export async function exportToSVG(
36
40
  }
37
41
  await embedFonts(clonedSVG, embedResources);
38
42
 
43
+ if (removeBackground) {
44
+ removeSVGBackground(clonedSVG);
45
+ }
39
46
  cleanSVG(clonedSVG);
40
47
 
41
48
  return clonedSVG;
@@ -322,6 +329,12 @@ function cleanSVG(svg: SVGSVGElement) {
322
329
  clearDataset(svg);
323
330
  }
324
331
 
332
+ function removeSVGBackground(svg: SVGSVGElement) {
333
+ svg.style.removeProperty('background-color');
334
+ const background = getElementByRole(svg, ElementTypeEnum.Background);
335
+ background?.remove();
336
+ }
337
+
325
338
  function removeBtnGroup(svg: SVGSVGElement) {
326
339
  const btnGroup = getElementByRole(svg, ElementTypeEnum.BtnsGroup);
327
340
  btnGroup?.remove();
@@ -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
@@ -14,6 +19,11 @@ export interface SVGExportOptions {
14
19
 
15
20
  export interface PNGExportOptions {
16
21
  type: 'png';
22
+ /**
23
+ * 是否移除背景(SVG 背景样式 + 背景矩形)
24
+ * @default false
25
+ */
26
+ removeBackground?: boolean;
17
27
  /**
18
28
  * 设备像素比,默认为浏览器的 devicePixelRatio
19
29
  * @default globalThis.devicePixelRatio || 2
@@ -58,6 +58,97 @@ function parseKeyValue(raw: string) {
58
58
  return { key: text, value: undefined };
59
59
  }
60
60
 
61
+ interface AssignEntryResult {
62
+ parent: ObjectNode;
63
+ key: string;
64
+ }
65
+
66
+ function isUnsafeObjectKey(key: string) {
67
+ return key === '__proto__' || key === 'constructor' || key === 'prototype';
68
+ }
69
+
70
+ function assignObjectEntry(
71
+ parent: ObjectNode,
72
+ rawKey: string,
73
+ node: ObjectNode,
74
+ line: number,
75
+ errors: SyntaxError[],
76
+ ): AssignEntryResult | null {
77
+ if (!rawKey.includes('.')) {
78
+ if (isUnsafeObjectKey(rawKey)) {
79
+ errors.push({
80
+ path: rawKey,
81
+ line,
82
+ code: 'bad_syntax',
83
+ message: `Invalid key part: ${rawKey}`,
84
+ raw: rawKey,
85
+ });
86
+ return null;
87
+ }
88
+ parent.entries[rawKey] = node;
89
+ return { parent, key: rawKey };
90
+ }
91
+
92
+ const parts = rawKey.split('.');
93
+ if (parts.some((part) => !part)) {
94
+ errors.push({
95
+ path: rawKey,
96
+ line,
97
+ code: 'bad_syntax',
98
+ message: 'Invalid dotted key path.',
99
+ raw: rawKey,
100
+ });
101
+ return null;
102
+ }
103
+
104
+ let current = parent;
105
+ for (let index = 0; index < parts.length - 1; index += 1) {
106
+ const part = parts[index];
107
+ if (isUnsafeObjectKey(part)) {
108
+ errors.push({
109
+ path: rawKey,
110
+ line,
111
+ code: 'bad_syntax',
112
+ message: `Invalid key part in dotted path: ${part}`,
113
+ raw: rawKey,
114
+ });
115
+ return null;
116
+ }
117
+ const existing = current.entries[part];
118
+ if (!existing) {
119
+ const container = createObjectNode(line);
120
+ current.entries[part] = container;
121
+ current = container;
122
+ continue;
123
+ }
124
+ if (existing.kind !== 'object') {
125
+ errors.push({
126
+ path: parts.slice(0, index + 1).join('.'),
127
+ line,
128
+ code: 'bad_syntax',
129
+ message: 'Cannot assign dotted key under a list value.',
130
+ raw: rawKey,
131
+ });
132
+ return null;
133
+ }
134
+ current = existing;
135
+ }
136
+
137
+ const finalKey = parts[parts.length - 1];
138
+ if (isUnsafeObjectKey(finalKey)) {
139
+ errors.push({
140
+ path: rawKey,
141
+ line,
142
+ code: 'bad_syntax',
143
+ message: `Invalid key part in dotted path: ${finalKey}`,
144
+ raw: rawKey,
145
+ });
146
+ return null;
147
+ }
148
+ current.entries[finalKey] = node;
149
+ return { parent: current, key: finalKey };
150
+ }
151
+
61
152
  function createObjectNode(line: number, value?: string): ObjectNode {
62
153
  return { kind: 'object', line, value, entries: {} };
63
154
  }
@@ -203,12 +294,19 @@ export function parseSyntaxToAst(input: string): ParseResult {
203
294
  }
204
295
 
205
296
  const node = createObjectNode(lineNumber, parsed.value);
206
- parentNode.entries[parsed.key] = node;
297
+ const assigned = assignObjectEntry(
298
+ parentNode,
299
+ parsed.key,
300
+ node,
301
+ lineNumber,
302
+ errors,
303
+ );
304
+ if (!assigned) return;
207
305
  stack.push({
208
306
  indent,
209
307
  node,
210
- parent: parentNode,
211
- key: parsed.key,
308
+ parent: assigned.parent,
309
+ key: assigned.key,
212
310
  });
213
311
  });
214
312
 
@@ -210,7 +210,7 @@ const BUILT_IN_TEMPLATES: Record<string, TemplateOptions> = {
210
210
  'sequence-timeline-plain-text': {
211
211
  design: {
212
212
  title: 'default',
213
- structure: { type: 'sequence-timeline' },
213
+ structure: { type: 'sequence-timeline', showStepLabels: false },
214
214
  items: [{ type: 'plain-text' }],
215
215
  },
216
216
  },
@@ -231,7 +231,7 @@ const BUILT_IN_TEMPLATES: Record<string, TemplateOptions> = {
231
231
  'sequence-timeline-simple': {
232
232
  design: {
233
233
  title: 'default',
234
- structure: { type: 'sequence-timeline', gap: 20 },
234
+ structure: { type: 'sequence-timeline', gap: 20, showStepLabels: false },
235
235
  items: [{ type: 'simple', positionV: 'middle' }],
236
236
  },
237
237
  },
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.15';
1
+ export const VERSION = '0.2.16';