@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.
Files changed (78) hide show
  1. package/README.md +27 -0
  2. package/README.zh-CN.md +27 -0
  3. package/dist/infographic.min.js +130 -129
  4. package/dist/infographic.min.js.map +1 -1
  5. package/esm/constants/service.d.ts +1 -1
  6. package/esm/constants/service.js +1 -1
  7. package/esm/designs/structures/chart-line.js +2 -1
  8. package/esm/designs/structures/sequence-interaction.js +36 -15
  9. package/esm/designs/structures/sequence-timeline.d.ts +1 -0
  10. package/esm/designs/structures/sequence-timeline.js +4 -2
  11. package/esm/exporter/png.js +2 -2
  12. package/esm/exporter/svg.js +176 -2
  13. package/esm/exporter/types.d.ts +10 -0
  14. package/esm/options/parser.js +8 -6
  15. package/esm/options/types.d.ts +3 -3
  16. package/esm/renderer/renderer.js +1 -1
  17. package/esm/resource/loaders/search.js +2 -3
  18. package/esm/runtime/options.js +1 -1
  19. package/esm/syntax/index.js +56 -10
  20. package/esm/syntax/mapper.js +20 -6
  21. package/esm/syntax/parser.js +89 -3
  22. package/esm/syntax/types.d.ts +1 -1
  23. package/esm/templates/built-in.js +2 -2
  24. package/esm/templates/registry.d.ts +1 -0
  25. package/esm/templates/registry.js +6 -0
  26. package/esm/templates/utils.d.ts +1 -0
  27. package/esm/templates/utils.js +63 -0
  28. package/esm/themes/built-in.js +3 -0
  29. package/esm/version.d.ts +1 -1
  30. package/esm/version.js +1 -1
  31. package/lib/constants/service.d.ts +1 -1
  32. package/lib/constants/service.js +1 -1
  33. package/lib/designs/structures/chart-line.js +2 -1
  34. package/lib/designs/structures/sequence-interaction.js +36 -15
  35. package/lib/designs/structures/sequence-timeline.d.ts +1 -0
  36. package/lib/designs/structures/sequence-timeline.js +4 -2
  37. package/lib/exporter/png.js +2 -2
  38. package/lib/exporter/svg.js +176 -2
  39. package/lib/exporter/types.d.ts +10 -0
  40. package/lib/options/parser.js +7 -5
  41. package/lib/options/types.d.ts +3 -3
  42. package/lib/renderer/renderer.js +1 -1
  43. package/lib/resource/loaders/search.js +2 -3
  44. package/lib/runtime/options.js +1 -1
  45. package/lib/syntax/index.js +56 -10
  46. package/lib/syntax/mapper.js +20 -6
  47. package/lib/syntax/parser.js +89 -3
  48. package/lib/syntax/types.d.ts +1 -1
  49. package/lib/templates/built-in.js +2 -2
  50. package/lib/templates/registry.d.ts +1 -0
  51. package/lib/templates/registry.js +7 -0
  52. package/lib/templates/utils.d.ts +1 -0
  53. package/lib/templates/utils.js +66 -0
  54. package/lib/themes/built-in.js +3 -0
  55. package/lib/version.d.ts +1 -1
  56. package/lib/version.js +1 -1
  57. package/package.json +1 -1
  58. package/src/constants/service.ts +1 -1
  59. package/src/designs/structures/chart-line.tsx +3 -1
  60. package/src/designs/structures/sequence-interaction.tsx +92 -46
  61. package/src/designs/structures/sequence-timeline.tsx +18 -15
  62. package/src/exporter/png.ts +3 -2
  63. package/src/exporter/svg.ts +209 -2
  64. package/src/exporter/types.ts +10 -0
  65. package/src/options/parser.ts +7 -6
  66. package/src/options/types.ts +3 -3
  67. package/src/renderer/renderer.ts +1 -1
  68. package/src/resource/loaders/search.ts +2 -2
  69. package/src/runtime/options.ts +1 -1
  70. package/src/syntax/index.ts +71 -10
  71. package/src/syntax/mapper.ts +20 -6
  72. package/src/syntax/parser.ts +111 -3
  73. package/src/syntax/types.ts +1 -0
  74. package/src/templates/built-in.ts +2 -2
  75. package/src/templates/registry.ts +6 -0
  76. package/src/templates/utils.ts +87 -0
  77. package/src/themes/built-in.ts +4 -0
  78. package/src/version.ts +1 -1
@@ -1 +1 @@
1
- export declare const ICON_SERVICE_URL = "https://www.weavefox.cn/api/open/v1/icon";
1
+ export declare const ICON_SERVICE_URL = "https://lab.weavefox.cn/api/v1/infographic/icon";
@@ -1 +1 @@
1
- export const ICON_SERVICE_URL = 'https://www.weavefox.cn/api/open/v1/icon';
1
+ export const ICON_SERVICE_URL = 'https://lab.weavefox.cn/api/v1/infographic/icon';
@@ -67,10 +67,11 @@ export const ChartLine = (props) => {
67
67
  const titleElements = [];
68
68
  const tickElements = [];
69
69
  const ticksY = scaleY.ticks(6);
70
+ const formatTickY = scaleY.tickFormat(6);
70
71
  ticksY.forEach((tick) => {
71
72
  const yPos = chartOriginY + scaleY(tick);
72
73
  gridElements.push(_jsx(Path, { d: `M ${chartOriginX} ${yPos} L ${chartOriginX + derivedChartWidth} ${yPos}`, width: derivedChartWidth, height: 1, stroke: axisColor, strokeWidth: 1, "data-element-type": "shape", opacity: 0.08 }));
73
- tickElements.push(_jsx(Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children: Number.isInteger(tick) ? tick.toString() : tick.toFixed(1) }));
74
+ tickElements.push(_jsx(Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children: formatTickY(tick) }));
74
75
  });
75
76
  const xLabels = [];
76
77
  const pointPositions = [];
@@ -15,6 +15,7 @@ const DEFAULT_ITEM_HEIGHT = 50;
15
15
  const FONT_SIZE = 14;
16
16
  const ARROW_SIZE = 14;
17
17
  const CORNER_RADIUS_NODE = 6;
18
+ const LIFELINE_MASK_GAP = 2;
18
19
  const LANE_PADDING = 60;
19
20
  const BTN_HALF_SIZE = 12;
20
21
  const BTN_MARGIN = 10;
@@ -229,17 +230,6 @@ export const SequenceInteractionFlow = (props) => {
229
230
  const decorElements = [];
230
231
  const defsElements = [];
231
232
  const btnElements = [];
232
- // 绘制生命线
233
- if (showLifeline) {
234
- lanes.forEach((_lane, laneIndex) => {
235
- const centerX = getLaneCenterX(laneIndex);
236
- const startY = padding + headerOffset;
237
- const endY = totalHeight - padding;
238
- decorElements.push(_jsx(Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape" }));
239
- // 绘制生命线末端箭头(实心)
240
- decorElements.push(...createArrowElements(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
241
- });
242
- }
243
233
  // 绘制泳道标题
244
234
  if (showLaneHeader) {
245
235
  lanes.forEach((lane, laneIndex) => {
@@ -282,10 +272,6 @@ export const SequenceInteractionFlow = (props) => {
282
272
  });
283
273
  const nodeColor = getPaletteColor(options, [laneIndex]);
284
274
  const nodeThemeColors = getThemeColors({ colorPrimary: nodeColor }, options);
285
- // 添加节点背景遮挡层,防止生命线虚线透过半透明节点显示
286
- // 只在节点中心放置窄条遮挡生命线,避免圆角处露出白色背景
287
- const maskStripWidth = lifelineWidth + 6;
288
- decorElements.push(_jsx(Rect, { x: centerX - maskStripWidth / 2, y: y, width: maskStripWidth, height: itemHeight, fill: colorBg }));
289
275
  // 构造类似 hierarchy-tree 的 _originalIndex
290
276
  const originalIndex = [laneIndex, rowIndex];
291
277
  // 附加到数据上,确保 Item 组件能正确识别
@@ -322,6 +308,41 @@ export const SequenceInteractionFlow = (props) => {
322
308
  const centerX = getLaneCenterX(laneIndex);
323
309
  btnElements.push(_jsx(BtnAdd, { indexes: [laneIndex, childCount], x: centerX - BTN_HALF_SIZE, y: addNodeY }));
324
310
  });
311
+ // 绘制生命线(使用 mask 挖空节点区域,避免虚线穿透半透明节点)
312
+ if (showLifeline) {
313
+ // 预先按泳道分组节点,避免每条泳道都遍历全部节点
314
+ const nodeRectsByLane = new Map();
315
+ nodeLayoutById.forEach((layout) => {
316
+ let list = nodeRectsByLane.get(layout.laneIndex);
317
+ if (!list) {
318
+ list = [];
319
+ nodeRectsByLane.set(layout.laneIndex, list);
320
+ }
321
+ list.push({
322
+ x: layout.x,
323
+ y: layout.y,
324
+ width: layout.width,
325
+ height: layout.height,
326
+ });
327
+ });
328
+ lanes.forEach((_lane, laneIndex) => {
329
+ var _a;
330
+ const centerX = getLaneCenterX(laneIndex);
331
+ const startY = padding + headerOffset;
332
+ const endY = totalHeight - padding;
333
+ const laneNodeRects = (_a = nodeRectsByLane.get(laneIndex)) !== null && _a !== void 0 ? _a : [];
334
+ // 如果该泳道有节点,创建 mask 来挖空节点区域
335
+ let lifelineMaskAttr;
336
+ if (laneNodeRects.length > 0) {
337
+ const maskId = `lifeline-mask-${instanceId}-${laneIndex}`;
338
+ defsElements.push(_jsxs("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: 0, y: 0, width: totalWidth, height: totalHeight, children: [_jsx(Rect, { x: 0, y: 0, width: totalWidth, height: totalHeight, fill: "white" }), laneNodeRects.map((rect) => (_jsx(Rect, { x: rect.x, y: rect.y - LIFELINE_MASK_GAP, width: rect.width, height: rect.height + LIFELINE_MASK_GAP * 2, fill: "black" })))] }));
339
+ lifelineMaskAttr = `url(#${maskId})`;
340
+ }
341
+ decorElements.push(_jsx(Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape", mask: lifelineMaskAttr }));
342
+ // 绘制生命线末端箭头(实心)
343
+ decorElements.push(...createArrowElements(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
344
+ });
345
+ }
325
346
  // 添加新泳道按钮 (最右侧)
326
347
  const lastLaneRightX = getLaneCenterX(lanes.length - 1) + laneWidth / 2;
327
348
  const newLaneX = lanes.length > 0 ? lastLaneRightX + BTN_LANE_ADD_Gap : padding;
@@ -3,5 +3,6 @@ import type { BaseStructureProps } from './types';
3
3
  export interface SequenceTimelineProps extends BaseStructureProps {
4
4
  gap?: number;
5
5
  lineOffset?: number;
6
+ showStepLabels?: boolean;
6
7
  }
7
8
  export declare const SequenceTimeline: ComponentType<SequenceTimelineProps>;
@@ -5,7 +5,7 @@ import { FlexLayout } from '../layouts/index.js';
5
5
  import { getColorPrimary, getPaletteColor, getPaletteColors } from '../utils/index.js';
6
6
  import { registerStructure } from './registry.js';
7
7
  export const SequenceTimeline = (props) => {
8
- const { Title, Item, data, gap = 10, options } = props;
8
+ const { Title, Item, data, gap = 10, showStepLabels = true, options } = props;
9
9
  const { title, desc, items = [] } = data;
10
10
  const titleContent = Title ? _jsx(Title, { title: title, desc: desc }) : null;
11
11
  const colorPrimary = getColorPrimary(options);
@@ -41,7 +41,9 @@ export const SequenceTimeline = (props) => {
41
41
  const itemY = index * (itemBounds.height + gap);
42
42
  const nodeY = itemY + itemBounds.height / 2;
43
43
  const indexes = [index];
44
- decorElements.push(_jsx(Text, { x: stepLabelX, y: nodeY, width: 70, fontSize: 18, fontWeight: "bold", alignHorizontal: "left", alignVertical: "middle", fill: palette[index % palette.length], children: `STEP ${index + 1}` }));
44
+ if (showStepLabels) {
45
+ decorElements.push(_jsx(Text, { x: stepLabelX, y: nodeY, width: 70, fontSize: 18, fontWeight: "bold", alignHorizontal: "left", alignVertical: "middle", fill: palette[index % palette.length], children: `STEP ${index + 1}` }));
46
+ }
45
47
  itemElements.push(_jsx(Item, { indexes: indexes, datum: item, data: data, x: itemX, y: itemY, positionH: "normal" }));
46
48
  decorElements.push(_jsx(Ellipse, { x: timelineX - nodeRadius, y: nodeY - nodeRadius, width: nodeRadius * 2, height: nodeRadius * 2, fill: palette[index % palette.length] }));
47
49
  btnElements.push(_jsx(BtnRemove, { indexes: indexes, x: itemX - btnBounds.width - 10, y: itemY + (itemBounds.height - btnBounds.height) / 2 }));
@@ -12,8 +12,8 @@ import { exportToSVG } from './svg.js';
12
12
  export function exportToPNGString(svg_1) {
13
13
  return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
14
14
  var _a;
15
- const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2 } = options;
16
- const node = yield exportToSVG(svg);
15
+ const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2, removeBackground = false } = options;
16
+ const node = yield exportToSVG(svg, { removeBackground });
17
17
  const { width, height } = getViewBox(node);
18
18
  return new Promise((resolve, reject) => {
19
19
  try {
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { createElement, getElementByRole, getViewBox, setAttributes, setElementRole, traverse, } from '../utils/index.js';
11
11
  import { embedFonts } from './font.js';
12
+ const VIEWBOX_CHANGE_TOLERANCE = 0.5;
12
13
  export function exportToSVGString(svg_1) {
13
14
  return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
14
15
  const node = yield exportToSVG(svg, options);
@@ -16,11 +17,176 @@ export function exportToSVGString(svg_1) {
16
17
  return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(str);
17
18
  });
18
19
  }
20
+ function getExportViewBox(svg) {
21
+ if (svg.hasAttribute('viewBox'))
22
+ return getViewBox(svg);
23
+ const width = parseAbsoluteLength(svg.getAttribute('width'));
24
+ const height = parseAbsoluteLength(svg.getAttribute('height'));
25
+ if (width > 0 && height > 0) {
26
+ return { x: 0, y: 0, width, height };
27
+ }
28
+ const rect = svg.getBoundingClientRect();
29
+ if (rect.width > 0 && rect.height > 0) {
30
+ return { x: 0, y: 0, width: rect.width, height: rect.height };
31
+ }
32
+ return null;
33
+ }
34
+ function parseAbsoluteLength(value) {
35
+ if (!value)
36
+ return Number.NaN;
37
+ const trimmed = value.trim();
38
+ if (!trimmed)
39
+ return Number.NaN;
40
+ if (!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:px)?$/.test(trimmed))
41
+ return Number.NaN;
42
+ return Number.parseFloat(trimmed);
43
+ }
44
+ function measureSpanContentHeight(span) {
45
+ const prevHeight = span.style.height;
46
+ const prevOverflow = span.style.overflow;
47
+ try {
48
+ span.style.height = 'max-content';
49
+ span.style.overflow = 'hidden';
50
+ void span.offsetHeight; // force reflow
51
+ return span.scrollHeight;
52
+ }
53
+ finally {
54
+ span.style.height = prevHeight;
55
+ span.style.overflow = prevOverflow;
56
+ }
57
+ }
58
+ function measureSpanContentWidth(span) {
59
+ const prevWidth = span.style.width;
60
+ const prevOverflow = span.style.overflow;
61
+ try {
62
+ span.style.width = 'max-content';
63
+ span.style.overflow = 'hidden';
64
+ void span.offsetWidth; // force reflow
65
+ return span.scrollWidth;
66
+ }
67
+ finally {
68
+ span.style.width = prevWidth;
69
+ span.style.overflow = prevOverflow;
70
+ }
71
+ }
72
+ // Returns [left, top, right, bottom] in SVG coordinates for a foreignObject,
73
+ // accounting for flex alignment: bottom/center-aligned content can overflow,
74
+ // and horizontally aligned content can overflow as well.
75
+ function getFOContentBoundsInSVG(fo, content, toSVGCoord) {
76
+ const foRect = fo.getBoundingClientRect();
77
+ const foTopLeft = toSVGCoord(foRect.left, foRect.top);
78
+ const foBottomRight = toSVGCoord(foRect.right, foRect.bottom);
79
+ const foLeftSVG = foTopLeft.x;
80
+ const foTopSVG = foTopLeft.y;
81
+ const foRightSVG = foBottomRight.x;
82
+ const foBottomSVG = foBottomRight.y;
83
+ const foWidthSVG = foRightSVG - foLeftSVG;
84
+ const foHeightSVG = foBottomSVG - foTopSVG;
85
+ const svgUnitsPerClientPxY = foRect.height > 0 ? foHeightSVG / foRect.height : 1;
86
+ const svgUnitsPerClientPxX = foRect.width > 0 ? foWidthSVG / foRect.width : 1;
87
+ // Measure actual content dimensions
88
+ const realScrollHeight = measureSpanContentHeight(content);
89
+ const contentHeightSVG = realScrollHeight > 0
90
+ ? realScrollHeight * svgUnitsPerClientPxY
91
+ : foHeightSVG;
92
+ const realScrollWidth = measureSpanContentWidth(content);
93
+ const contentWidthSVG = realScrollWidth > 0 ? realScrollWidth * svgUnitsPerClientPxX : foWidthSVG;
94
+ const computedStyle = window.getComputedStyle(content);
95
+ const alignItems = computedStyle.alignItems;
96
+ const justifyContent = computedStyle.justifyContent;
97
+ // Calculate vertical bounds
98
+ let top, bottom;
99
+ if (alignItems === 'flex-end' || alignItems === 'end') {
100
+ top = foBottomSVG - contentHeightSVG;
101
+ bottom = foBottomSVG;
102
+ }
103
+ else if (alignItems === 'center') {
104
+ const overflowY = contentHeightSVG - foHeightSVG;
105
+ top = foTopSVG - overflowY / 2;
106
+ bottom = foBottomSVG + overflowY / 2;
107
+ }
108
+ else {
109
+ top = foTopSVG;
110
+ bottom = foTopSVG + contentHeightSVG;
111
+ }
112
+ // Calculate horizontal bounds
113
+ let left, right;
114
+ if (justifyContent === 'flex-end' ||
115
+ justifyContent === 'end' ||
116
+ justifyContent === 'right') {
117
+ left = foRightSVG - contentWidthSVG;
118
+ right = foRightSVG;
119
+ }
120
+ else if (justifyContent === 'center') {
121
+ const overflowX = contentWidthSVG - foWidthSVG;
122
+ left = foLeftSVG - overflowX / 2;
123
+ right = foRightSVG + overflowX / 2;
124
+ }
125
+ else {
126
+ left = foLeftSVG;
127
+ right = foLeftSVG + contentWidthSVG;
128
+ }
129
+ return [left, top, right, bottom];
130
+ }
131
+ /**
132
+ * Computes a viewBox that fully covers all foreignObject text content,
133
+ * accounting for overflow caused by flex alignment (bottom/center align
134
+ * can push content outside the foreignObject bounds).
135
+ */
136
+ function computeFullViewBox(svg) {
137
+ const viewBox = getExportViewBox(svg);
138
+ if (!viewBox)
139
+ return null;
140
+ if (typeof svg.getScreenCTM !== 'function')
141
+ return null;
142
+ const screenCTM = svg.getScreenCTM();
143
+ if (!screenCTM)
144
+ return null;
145
+ const inverseCTM = screenCTM.inverse();
146
+ const toSVGCoord = (clientX, clientY) => {
147
+ const pt = svg.createSVGPoint();
148
+ pt.x = clientX;
149
+ pt.y = clientY;
150
+ return pt.matrixTransform(inverseCTM);
151
+ };
152
+ let minX = viewBox.x;
153
+ let minY = viewBox.y;
154
+ let maxX = viewBox.x + viewBox.width;
155
+ let maxY = viewBox.y + viewBox.height;
156
+ svg
157
+ .querySelectorAll('foreignObject')
158
+ .forEach((fo) => {
159
+ const content = fo.firstElementChild;
160
+ if (!content)
161
+ return;
162
+ const [left, top, right, bottom] = getFOContentBoundsInSVG(fo, content, toSVGCoord);
163
+ minX = Math.min(minX, left);
164
+ minY = Math.min(minY, top);
165
+ maxX = Math.max(maxX, right);
166
+ maxY = Math.max(maxY, bottom);
167
+ });
168
+ const newX = minX;
169
+ const newY = minY;
170
+ const newWidth = maxX - newX;
171
+ const newHeight = maxY - newY;
172
+ if (newWidth <= viewBox.width + VIEWBOX_CHANGE_TOLERANCE &&
173
+ newHeight <= viewBox.height + VIEWBOX_CHANGE_TOLERANCE &&
174
+ newX >= viewBox.x - VIEWBOX_CHANGE_TOLERANCE &&
175
+ newY >= viewBox.y - VIEWBOX_CHANGE_TOLERANCE)
176
+ return null;
177
+ return `${newX} ${newY} ${newWidth} ${newHeight}`;
178
+ }
19
179
  export function exportToSVG(svg_1) {
20
180
  return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
21
- const { embedResources = true, removeIds = false } = options;
181
+ const { removeBackground = false, embedResources = true, removeIds = false, } = options;
22
182
  const clonedSVG = svg.cloneNode(true);
23
- const { width, height } = getViewBox(svg);
183
+ if (typeof document !== 'undefined') {
184
+ const fullViewBox = computeFullViewBox(svg);
185
+ if (fullViewBox) {
186
+ clonedSVG.setAttribute('viewBox', fullViewBox);
187
+ }
188
+ }
189
+ const { width, height } = getViewBox(clonedSVG);
24
190
  setAttributes(clonedSVG, { width, height });
25
191
  if (removeIds) {
26
192
  inlineUseElements(clonedSVG);
@@ -30,6 +196,9 @@ export function exportToSVG(svg_1) {
30
196
  yield embedIcons(clonedSVG);
31
197
  }
32
198
  yield embedFonts(clonedSVG, embedResources);
199
+ if (removeBackground) {
200
+ removeSVGBackground(clonedSVG);
201
+ }
33
202
  cleanSVG(clonedSVG);
34
203
  return clonedSVG;
35
204
  });
@@ -284,6 +453,11 @@ function cleanSVG(svg) {
284
453
  removeUselessAttrs(svg);
285
454
  clearDataset(svg);
286
455
  }
456
+ function removeSVGBackground(svg) {
457
+ svg.style.removeProperty('background-color');
458
+ const background = getElementByRole(svg, "background" /* ElementTypeEnum.Background */);
459
+ background === null || background === void 0 ? void 0 : background.remove();
460
+ }
287
461
  function removeBtnGroup(svg) {
288
462
  const btnGroup = getElementByRole(svg, "btns-group" /* ElementTypeEnum.BtnsGroup */);
289
463
  btnGroup === null || btnGroup === void 0 ? void 0 : 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
@@ -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
@@ -10,17 +10,19 @@ var __rest = (this && this.__rest) || function (s, e) {
10
10
  return t;
11
11
  };
12
12
  import { merge } from 'lodash-es';
13
- import { getItem, getStructure, getTemplate, Title, } from '../designs/index.js';
13
+ import { getItem, getStructure, Title, } from '../designs/index.js';
14
14
  import { getPaletteColor } from '../renderer/index.js';
15
+ import { getTemplate, resolveTemplateKey } from '../templates/registry.js';
15
16
  import { generateThemeColors, getTheme } from '../themes/index.js';
16
17
  import { isDarkColor, isNonNullableParsedDesignsOptions, parsePadding, } from '../utils/index.js';
17
18
  export function parseOptions(options) {
18
19
  const { container = '#container', padding = 0, template, design, theme, themeConfig, data } = options, restOptions = __rest(options, ["container", "padding", "template", "design", "theme", "themeConfig", "data"]);
20
+ const resolvedTemplate = template ? resolveTemplateKey(template) : undefined;
19
21
  const parsedContainer = typeof container === 'string'
20
22
  ? document.querySelector(container) || document.createElement('div')
21
23
  : container;
22
- const templateOptions = template
23
- ? getTemplate(template)
24
+ const templateOptions = resolvedTemplate
25
+ ? getTemplate(resolvedTemplate)
24
26
  : undefined;
25
27
  const mergedThemeConfig = merge({}, templateOptions === null || templateOptions === void 0 ? void 0 : templateOptions.themeConfig, themeConfig);
26
28
  const resolvedThemeConfig = theme || themeConfig || (templateOptions === null || templateOptions === void 0 ? void 0 : templateOptions.themeConfig)
@@ -35,11 +37,11 @@ export function parseOptions(options) {
35
37
  Object.assign(parsed, restTemplateOptions);
36
38
  }
37
39
  Object.assign(parsed, restOptions);
38
- const parsedData = parseData(data, options.template);
40
+ const parsedData = parseData(data, resolvedTemplate);
39
41
  if (parsedData)
40
42
  parsed.data = parsedData;
41
- if (template)
42
- parsed.template = template;
43
+ if (resolvedTemplate)
44
+ parsed.template = resolvedTemplate;
43
45
  if ((templateOptions === null || templateOptions === void 0 ? void 0 : templateOptions.design) || design) {
44
46
  const designOptions = Object.assign(Object.assign({}, (resolvedThemeConfig
45
47
  ? Object.assign(Object.assign({}, options), { themeConfig: resolvedThemeConfig }) : options)), { data: parsedData || data });
@@ -4,8 +4,8 @@ import type { ThemeConfig } from '../themes';
4
4
  import type { Data, Padding, ParsedData } from '../types';
5
5
  import type { Path } from '../utils';
6
6
  export interface InfographicOptions {
7
- /** 容器,可以是选择器或者 HTMLElement */
8
- container?: string | HTMLElement;
7
+ /** 容器,可以是选择器、Element ShadowRoot */
8
+ container?: string | Element | ShadowRoot;
9
9
  /** 宽度 */
10
10
  width?: number | string;
11
11
  /** 高度 */
@@ -34,7 +34,7 @@ export interface InfographicOptions {
34
34
  elements?: ElementProps[];
35
35
  }
36
36
  export interface ParsedInfographicOptions {
37
- container: HTMLElement;
37
+ container: Element | ShadowRoot;
38
38
  width?: number | string;
39
39
  height?: number | string;
40
40
  padding?: Padding;
@@ -47,7 +47,7 @@ export class Renderer {
47
47
  });
48
48
  });
49
49
  try {
50
- observer.observe(document, {
50
+ observer.observe(this.options.container, {
51
51
  childList: true,
52
52
  subtree: true,
53
53
  });
@@ -13,7 +13,6 @@ import { loadImageBase64Resource } from './image.js';
13
13
  import { loadRemoteResource } from './remote.js';
14
14
  import { loadSVGResource } from './svg.js';
15
15
  const queryIcon = (query) => __awaiter(void 0, void 0, void 0, function* () {
16
- var _a;
17
16
  try {
18
17
  const params = new URLSearchParams({ text: query, topK: '1' });
19
18
  const url = `${ICON_SERVICE_URL}?${params.toString()}`;
@@ -21,9 +20,9 @@ const queryIcon = (query) => __awaiter(void 0, void 0, void 0, function* () {
21
20
  if (!response.ok)
22
21
  return null;
23
22
  const result = yield response.json();
24
- if (!(result === null || result === void 0 ? void 0 : result.status) || !Array.isArray((_a = result.data) === null || _a === void 0 ? void 0 : _a.data))
23
+ if (!(result === null || result === void 0 ? void 0 : result.success) || !Array.isArray(result.data))
25
24
  return null;
26
- return result.data.data[0] || null;
25
+ return result.data[0] || null;
27
26
  }
28
27
  catch (error) {
29
28
  console.error(`Failed to query icon for "${query}":`, error);
@@ -16,7 +16,7 @@ const createDefaultInteractions = () => [
16
16
  ];
17
17
  export const DEFAULT_OPTIONS = {
18
18
  padding: 20,
19
- theme: 'light',
19
+ theme: 'default',
20
20
  themeConfig: {
21
21
  palette: 'antv',
22
22
  },
@@ -13,6 +13,15 @@ import { mapWithSchema } from './mapper.js';
13
13
  import { parseSyntaxToAst } from './parser.js';
14
14
  import { parseRelationsNode } from './relations.js';
15
15
  import { DataSchema, DesignSchema, RootSchema, TemplateSchema, ThemeSchema, } from './schema.js';
16
+ const ALLOWED_ROOT_KEYS = new Set([
17
+ 'infographic',
18
+ 'template',
19
+ 'design',
20
+ 'data',
21
+ 'theme',
22
+ 'width',
23
+ 'height',
24
+ ]);
16
25
  function normalizeItems(items) {
17
26
  var _a;
18
27
  const seen = new Set();
@@ -33,6 +42,13 @@ function normalizeItems(items) {
33
42
  }
34
43
  return normalized.reverse();
35
44
  }
45
+ function assignMissingNodeIds(items) {
46
+ items.forEach((item) => {
47
+ if (!item.id && item.label) {
48
+ item.id = item.label;
49
+ }
50
+ });
51
+ }
36
52
  function resolveTemplate(node, errors) {
37
53
  if (!node)
38
54
  return undefined;
@@ -46,12 +62,42 @@ function resolveTemplate(node, errors) {
46
62
  }
47
63
  return undefined;
48
64
  }
65
+ function inferTemplateFromBareFirstLine(entries, warnings) {
66
+ if ('infographic' in entries || 'template' in entries) {
67
+ return undefined;
68
+ }
69
+ const [firstEntry] = Object.entries(entries);
70
+ if (!firstEntry)
71
+ return undefined;
72
+ const [key, node] = firstEntry;
73
+ if (ALLOWED_ROOT_KEYS.has(key) || node.kind !== 'object') {
74
+ return undefined;
75
+ }
76
+ if (node.value !== undefined || Object.keys(node.entries).length > 0) {
77
+ return undefined;
78
+ }
79
+ warnings.push({
80
+ path: 'template',
81
+ line: node.line,
82
+ code: 'implicit_template',
83
+ message: 'Inferred template from a bare first line. Prefix it with "infographic" or "template" to make the syntax explicit.',
84
+ raw: key,
85
+ });
86
+ return {
87
+ template: key,
88
+ inferredKey: key,
89
+ };
90
+ }
49
91
  export function parseSyntax(input) {
50
92
  var _a;
51
93
  const { ast, errors } = parseSyntaxToAst(input);
52
94
  const warnings = [];
53
95
  const options = {};
54
96
  const mergedEntries = Object.assign({}, ast.entries);
97
+ const inferredTemplate = inferTemplateFromBareFirstLine(ast.entries, warnings);
98
+ if (inferredTemplate) {
99
+ delete mergedEntries[inferredTemplate.inferredKey];
100
+ }
55
101
  const infographicNode = ast.entries.infographic;
56
102
  let templateFromInfographic;
57
103
  if (infographicNode && infographicNode.kind === 'object') {
@@ -62,17 +108,8 @@ export function parseSyntax(input) {
62
108
  mergedEntries[key] = value;
63
109
  });
64
110
  }
65
- const allowedRootKeys = new Set([
66
- 'infographic',
67
- 'template',
68
- 'design',
69
- 'data',
70
- 'theme',
71
- 'width',
72
- 'height',
73
- ]);
74
111
  Object.keys(mergedEntries).forEach((key) => {
75
- if (!allowedRootKeys.has(key)) {
112
+ if (!ALLOWED_ROOT_KEYS.has(key)) {
76
113
  errors.push({
77
114
  path: key,
78
115
  line: mergedEntries[key].line,
@@ -89,6 +126,9 @@ export function parseSyntax(input) {
89
126
  if (!options.template && templateFromInfographic) {
90
127
  options.template = templateFromInfographic;
91
128
  }
129
+ if (!options.template && inferredTemplate) {
130
+ options.template = inferredTemplate.template;
131
+ }
92
132
  const designNode = mergedEntries.design;
93
133
  if (designNode) {
94
134
  const design = mapWithSchema(designNode, DesignSchema, 'design', errors);
@@ -111,6 +151,12 @@ export function parseSyntax(input) {
111
151
  const parsed = parseRelationsNode(relationsNode, errors, 'data.relations');
112
152
  if (parsed.relations.length > 0 || parsed.items.length > 0) {
113
153
  const current = ((_a = options.data) !== null && _a !== void 0 ? _a : {});
154
+ if (Array.isArray(current.items)) {
155
+ assignMissingNodeIds(current.items);
156
+ }
157
+ if (Array.isArray(current.nodes)) {
158
+ assignMissingNodeIds(current.nodes);
159
+ }
114
160
  // 优先使用已存在的数据列表 (sequences, lists, etc.)
115
161
  const dataKeys = Object.keys(DataSchema.fields).filter((key) => key !== 'items' && key !== 'relations');
116
162
  let hasStructuredData = false;
@@ -5,11 +5,20 @@ function createValueNode(value, line) {
5
5
  }
6
6
  const HEX_COLOR_PATTERN = /^(#[0-9a-f]{8}|#[0-9a-f]{6}|#[0-9a-f]{4}|#[0-9a-f]{3})/i;
7
7
  const FUNCTION_COLOR_PATTERN = /^((?:rgb|rgba|hsl|hsla)\([^)]*\))/i;
8
+ function normalizeBooleanLiteral(value) {
9
+ const trimmed = value.trim();
10
+ if (/^true$/i.test(trimmed))
11
+ return 'true';
12
+ if (/^false$/i.test(trimmed))
13
+ return 'false';
14
+ return undefined;
15
+ }
8
16
  function parseScalar(value) {
9
17
  const trimmed = value.trim();
10
- if (trimmed === 'true')
18
+ const normalizedBoolean = normalizeBooleanLiteral(trimmed);
19
+ if (normalizedBoolean === 'true')
11
20
  return true;
12
- if (trimmed === 'false')
21
+ if (normalizedBoolean === 'false')
13
22
  return false;
14
23
  if (/^-?\d+(\.\d+)?$/.test(trimmed))
15
24
  return parseFloat(trimmed);
@@ -175,11 +184,12 @@ export function mapWithSchema(node, schema, path, errors) {
175
184
  addError(errors, node, path, 'schema_mismatch', 'Expected boolean value.');
176
185
  return undefined;
177
186
  }
178
- if (value !== 'true' && value !== 'false') {
187
+ const normalizedBoolean = normalizeBooleanLiteral(value);
188
+ if (!normalizedBoolean) {
179
189
  addError(errors, node, path, 'invalid_value', 'Invalid boolean value.', value);
180
190
  return undefined;
181
191
  }
182
- return value === 'true';
192
+ return normalizedBoolean === 'true';
183
193
  }
184
194
  case 'enum': {
185
195
  const value = readScalar(node);
@@ -187,11 +197,15 @@ export function mapWithSchema(node, schema, path, errors) {
187
197
  addError(errors, node, path, 'schema_mismatch', 'Expected enum value.');
188
198
  return undefined;
189
199
  }
190
- if (!schema.values.includes(value)) {
200
+ const normalizedBoolean = normalizeBooleanLiteral(value);
201
+ const enumValue = normalizedBoolean && schema.values.includes(normalizedBoolean)
202
+ ? normalizedBoolean
203
+ : value;
204
+ if (!schema.values.includes(enumValue)) {
191
205
  addError(errors, node, path, 'invalid_value', 'Invalid enum value.', value);
192
206
  return undefined;
193
207
  }
194
- return value;
208
+ return enumValue;
195
209
  }
196
210
  case 'array': {
197
211
  if (node.kind === 'array') {