@antv/infographic 0.1.0 → 0.1.1

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 (107) hide show
  1. package/README.md +8 -12
  2. package/README.zh-CN.md +8 -9
  3. package/dist/infographic.min.js +45 -34
  4. package/dist/infographic.min.js.map +1 -1
  5. package/esm/designs/structures/hierarchy-tree.js +13 -6
  6. package/esm/exporter/font.d.ts +18 -0
  7. package/esm/exporter/font.js +202 -0
  8. package/esm/exporter/index.d.ts +3 -0
  9. package/esm/exporter/index.js +2 -0
  10. package/esm/exporter/png.d.ts +2 -0
  11. package/esm/exporter/png.js +42 -0
  12. package/esm/exporter/svg.d.ts +3 -0
  13. package/esm/exporter/svg.js +72 -0
  14. package/esm/exporter/types.d.ts +17 -0
  15. package/esm/index.d.ts +2 -2
  16. package/esm/renderer/composites/text.js +1 -3
  17. package/esm/renderer/fonts/built-in.d.ts +1 -1
  18. package/esm/renderer/fonts/index.d.ts +0 -2
  19. package/esm/renderer/fonts/index.js +0 -1
  20. package/esm/renderer/fonts/loader.js +1 -2
  21. package/esm/renderer/fonts/registry.d.ts +1 -1
  22. package/esm/renderer/fonts/registry.js +1 -1
  23. package/esm/renderer/renderer.js +1 -50
  24. package/esm/resource/utils/ref.js +1 -1
  25. package/esm/runtime/Infographic.d.ts +10 -0
  26. package/esm/runtime/Infographic.js +23 -2
  27. package/esm/types/font.js +1 -0
  28. package/esm/types/index.d.ts +1 -0
  29. package/{lib/renderer/fonts/utils.d.ts → esm/utils/font.d.ts} +1 -1
  30. package/esm/utils/index.d.ts +2 -0
  31. package/esm/utils/index.js +2 -0
  32. package/esm/utils/is-node.d.ts +4 -0
  33. package/esm/utils/is-node.js +6 -0
  34. package/esm/utils/padding.d.ts +1 -0
  35. package/esm/utils/padding.js +70 -0
  36. package/esm/utils/svg.d.ts +0 -1
  37. package/esm/utils/svg.js +3 -18
  38. package/esm/utils/text.js +1 -1
  39. package/esm/utils/viewbox.d.ts +6 -0
  40. package/esm/utils/viewbox.js +12 -0
  41. package/lib/designs/structures/hierarchy-tree.js +31 -24
  42. package/lib/exporter/font.d.ts +18 -0
  43. package/lib/exporter/font.js +210 -0
  44. package/lib/exporter/index.d.ts +3 -0
  45. package/lib/exporter/index.js +7 -0
  46. package/lib/exporter/png.d.ts +2 -0
  47. package/lib/exporter/png.js +45 -0
  48. package/lib/exporter/svg.d.ts +3 -0
  49. package/lib/exporter/svg.js +76 -0
  50. package/lib/exporter/types.d.ts +17 -0
  51. package/lib/index.d.ts +2 -2
  52. package/lib/renderer/composites/text.js +1 -3
  53. package/lib/renderer/fonts/built-in.d.ts +1 -1
  54. package/lib/renderer/fonts/index.d.ts +0 -2
  55. package/lib/renderer/fonts/index.js +1 -4
  56. package/lib/renderer/fonts/loader.js +1 -2
  57. package/lib/renderer/fonts/registry.d.ts +1 -1
  58. package/lib/renderer/fonts/registry.js +1 -1
  59. package/lib/renderer/renderer.js +1 -50
  60. package/lib/resource/utils/ref.js +1 -1
  61. package/lib/runtime/Infographic.d.ts +10 -0
  62. package/lib/runtime/Infographic.js +23 -2
  63. package/lib/types/font.js +2 -0
  64. package/lib/types/index.d.ts +1 -0
  65. package/{esm/renderer/fonts/utils.d.ts → lib/utils/font.d.ts} +1 -1
  66. package/lib/utils/index.d.ts +2 -0
  67. package/lib/utils/index.js +2 -0
  68. package/lib/utils/is-node.d.ts +4 -0
  69. package/lib/utils/is-node.js +9 -0
  70. package/lib/utils/padding.d.ts +1 -0
  71. package/lib/utils/padding.js +71 -0
  72. package/lib/utils/svg.d.ts +0 -1
  73. package/lib/utils/svg.js +3 -19
  74. package/lib/utils/text.js +2 -2
  75. package/lib/utils/viewbox.d.ts +6 -0
  76. package/lib/utils/viewbox.js +15 -0
  77. package/package.json +5 -2
  78. package/src/designs/structures/hierarchy-tree.tsx +14 -8
  79. package/src/exporter/font.ts +273 -0
  80. package/src/exporter/index.ts +7 -0
  81. package/src/exporter/png.ts +58 -0
  82. package/src/exporter/svg.ts +94 -0
  83. package/src/exporter/types.ts +19 -0
  84. package/src/index.ts +7 -3
  85. package/src/renderer/composites/text.ts +1 -2
  86. package/src/renderer/fonts/built-in.ts +1 -1
  87. package/src/renderer/fonts/index.ts +1 -3
  88. package/src/renderer/fonts/loader.ts +1 -2
  89. package/src/renderer/fonts/registry.ts +2 -2
  90. package/src/renderer/renderer.ts +1 -69
  91. package/src/resource/utils/ref.ts +1 -1
  92. package/src/runtime/Infographic.tsx +30 -2
  93. package/src/types/index.ts +1 -0
  94. package/src/{renderer/fonts/utils.ts → utils/font.ts} +1 -1
  95. package/src/utils/index.ts +2 -0
  96. package/src/utils/is-node.ts +8 -0
  97. package/src/utils/padding.ts +79 -0
  98. package/src/utils/svg.ts +5 -19
  99. package/src/utils/text.ts +2 -2
  100. package/src/utils/viewbox.ts +12 -0
  101. /package/esm/{renderer/fonts → exporter}/types.js +0 -0
  102. /package/esm/{renderer/fonts/types.d.ts → types/font.d.ts} +0 -0
  103. /package/esm/{renderer/fonts/utils.js → utils/font.js} +0 -0
  104. /package/lib/{renderer/fonts → exporter}/types.js +0 -0
  105. /package/lib/{renderer/fonts/types.d.ts → types/font.d.ts} +0 -0
  106. /package/lib/{renderer/fonts/utils.js → utils/font.js} +0 -0
  107. /package/src/{renderer/fonts/types.ts → types/font.ts} +0 -0
@@ -1,7 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@antv/infographic/jsx-runtime";
2
2
  import * as d3 from 'd3';
3
3
  import { Defs, Ellipse, getElementBounds, Group, Path, Polygon, } from '../../jsx';
4
- import { getDatumByIndexes } from '../../utils';
5
4
  import { BtnAdd, BtnRemove, BtnsGroup, ItemsGroup, ShapesGroup, } from '../components';
6
5
  import { FlexLayout } from '../layouts';
7
6
  import { getColorPrimary, getHierarchyColorIndexes, getItemComponent, getPaletteColor, getThemeColors, } from '../utils';
@@ -72,13 +71,21 @@ export const HierarchyTree = (props) => {
72
71
  };
73
72
  };
74
73
  // 内置工具方法:计算各层节点边界
75
- const computeLevelBounds = (maxLevels) => {
74
+ const computeLevelBounds = (rootNode) => {
76
75
  let maxWidth = 0, maxHeight = 0;
77
76
  const levelBounds = new Map();
78
- for (let level = 0; level < maxLevels; level++) {
77
+ const sampleDatumByLevel = new Map();
78
+ // 记录每个深度遇到的首个节点,用于计算该层的尺寸
79
+ rootNode.each((node) => {
80
+ if (!sampleDatumByLevel.has(node.depth)) {
81
+ sampleDatumByLevel.set(node.depth, node.data);
82
+ }
83
+ });
84
+ for (let level = 0; level < rootNode.height + 1; level++) {
79
85
  const ItemComponent = getItemComponent(Items, level);
80
- const indexes = Array(level + 1).fill(0);
81
- const bounds = getElementBounds(_jsx(ItemComponent, { indexes: indexes, data: data, datum: getDatumByIndexes(items, indexes), positionH: "center" }));
86
+ const sampleDatum = sampleDatumByLevel.get(level) ?? {};
87
+ const indexes = sampleDatum._originalIndex ?? Array(level + 1).fill(0);
88
+ const bounds = getElementBounds(_jsx(ItemComponent, { indexes: indexes, data: data, datum: sampleDatum, positionH: "center" }));
82
89
  levelBounds.set(level, bounds);
83
90
  maxWidth = Math.max(maxWidth, bounds.width);
84
91
  maxHeight = Math.max(maxHeight, bounds.height);
@@ -258,7 +265,7 @@ export const HierarchyTree = (props) => {
258
265
  // 构建和布局
259
266
  const hierarchyData = buildHierarchyData(items);
260
267
  const root = d3.hierarchy(hierarchyData);
261
- const { levelBounds, maxWidth, maxHeight } = computeLevelBounds(root.height + 1);
268
+ const { levelBounds, maxWidth, maxHeight } = computeLevelBounds(root);
262
269
  const treeLayout = d3
263
270
  .tree()
264
271
  .nodeSize([maxWidth + nodeGap, maxHeight + levelGap])
@@ -0,0 +1,18 @@
1
+ interface FontFaceAttributes {
2
+ 'font-family': string;
3
+ src: string;
4
+ 'font-style': string;
5
+ 'font-display': string;
6
+ 'font-weight': string;
7
+ 'unicode-range': string;
8
+ }
9
+ export declare function embedFonts(svg: SVGSVGElement, embedResources?: boolean): Promise<void>;
10
+ /**
11
+ * 解析给定 font-family 对应的 CSS @font-face
12
+ */
13
+ export declare function parseFontFamily(fontFamily: string): Promise<Partial<FontFaceAttributes>[]>;
14
+ /**
15
+ * 从 document.fonts 中获取给定 family 且已加载的 FontFace
16
+ */
17
+ export declare function getActualLoadedFontFace(fontFamily: string): FontFace[];
18
+ export {};
@@ -0,0 +1,202 @@
1
+ // @ts-expect-error ignore
2
+ import parse from 'css/lib/parse';
3
+ import { getFontURLs, getWoff2BaseURL } from '../renderer';
4
+ import { createElement, decodeFontFamily, join, normalizeFontWeightName, } from '../utils';
5
+ const fontDataUrlCache = new Map();
6
+ export async function embedFonts(svg, embedResources = true) {
7
+ // 1. 收集使用到的 font-family
8
+ const usedFonts = collectUsedFonts(svg);
9
+ if (usedFonts.size === 0)
10
+ return;
11
+ const parsedFontsFaces = [];
12
+ // 2. 对每个使用到的字体,解析 CSS + 结合 document.fonts 的实际加载子集
13
+ await Promise.all(Array.from(usedFonts).map(async (fontFamily) => {
14
+ const loadedFonts = getActualLoadedFontFace(fontFamily);
15
+ if (!loadedFonts.length)
16
+ return;
17
+ const cssFontFaces = await parseFontFamily(fontFamily);
18
+ if (!cssFontFaces.length)
19
+ return;
20
+ const processed = await Promise.all(cssFontFaces.map(async (rawFace) => {
21
+ const fontFace = normalizeFontFace(rawFace);
22
+ const unicodeRange = fontFace['unicode-range'].replace(/\s/g, '');
23
+ const subset = loadedFonts.find((font) => font.unicodeRange &&
24
+ font.unicodeRange.replace(/\s/g, '') === unicodeRange);
25
+ // 如果找不到对应子集,就不处理这个 font-face
26
+ if (!subset)
27
+ return null;
28
+ const baseURL = getWoff2BaseURL(fontFace['font-family'], normalizeFontWeightName(fontFace['font-weight']));
29
+ if (!baseURL)
30
+ return null;
31
+ // 更宽松地从 src 中提取 .woff2 URL 片段
32
+ const urlMatch = fontFace.src.match(/url\(["']?(.*?\.woff2)[^"']*["']?\)/);
33
+ if (!urlMatch?.[1])
34
+ return null;
35
+ const woff2URL = join(baseURL, urlMatch[1]);
36
+ if (embedResources) {
37
+ const woff2DataUrl = await loadWoff2(woff2URL);
38
+ fontFace.src = `url(${woff2DataUrl}) format('woff2')`;
39
+ }
40
+ else {
41
+ fontFace.src = `url(${woff2URL}) format('woff2')`;
42
+ }
43
+ return fontFace;
44
+ }));
45
+ parsedFontsFaces.push(...(processed.filter(Boolean) || []));
46
+ }));
47
+ // 3. 创建 <style>@font-face...</style> 并插入 SVG
48
+ if (parsedFontsFaces.length > 0) {
49
+ insertFontStyle(svg, parsedFontsFaces);
50
+ }
51
+ }
52
+ /**
53
+ * 收集 SVG 中用到的 font-family
54
+ */
55
+ function collectUsedFonts(svg) {
56
+ const usedFonts = new Set();
57
+ const svgFontFamily = svg.getAttribute('font-family');
58
+ if (svgFontFamily) {
59
+ const decodedFontFamily = decodeFontFamily(svgFontFamily);
60
+ if (decodedFontFamily) {
61
+ usedFonts.add(decodedFontFamily);
62
+ }
63
+ }
64
+ const textElements = svg.querySelectorAll('foreignObject span');
65
+ for (const span of textElements) {
66
+ const fontFamily = decodeFontFamily(span.style.fontFamily);
67
+ if (fontFamily)
68
+ usedFonts.add(fontFamily);
69
+ }
70
+ return usedFonts;
71
+ }
72
+ /**
73
+ * 解析给定 font-family 对应的 CSS @font-face
74
+ */
75
+ export async function parseFontFamily(fontFamily) {
76
+ const urls = getFontURLs(fontFamily);
77
+ const fontFaces = [];
78
+ await Promise.allSettled(urls.map(async (url) => {
79
+ const css = await fetch(url)
80
+ .then((res) => res.text())
81
+ .then((text) => parse(text))
82
+ .catch(() => {
83
+ console.error(`Failed to fetch or parse font CSS: ${url}`);
84
+ return null;
85
+ });
86
+ css?.stylesheet?.rules.forEach((rule) => {
87
+ if (rule.type === 'font-face') {
88
+ const fontFace = parseFontFace(rule);
89
+ fontFaces.push(fontFace);
90
+ }
91
+ });
92
+ }));
93
+ return fontFaces;
94
+ }
95
+ /**
96
+ * 从 document.fonts 中获取给定 family 且已加载的 FontFace
97
+ */
98
+ export function getActualLoadedFontFace(fontFamily) {
99
+ const fonts = [];
100
+ document.fonts.forEach((font) => {
101
+ if (decodeFontFamily(font.family) === decodeFontFamily(fontFamily) &&
102
+ font.status === 'loaded') {
103
+ fonts.push(font);
104
+ }
105
+ });
106
+ return fonts;
107
+ }
108
+ /**
109
+ * 从 css 的 FontFace 规则中提取声明
110
+ */
111
+ function parseFontFace(rule) {
112
+ const declarations = (rule.declarations || []);
113
+ const attrs = {};
114
+ declarations.forEach((declaration) => {
115
+ const { property, value } = declaration;
116
+ if (property && value) {
117
+ attrs[property] = value;
118
+ }
119
+ });
120
+ // 这里返回 Partial,后面统一 normalize
121
+ return attrs;
122
+ }
123
+ /**
124
+ * 将不完整的 FontFaceAttributes 补全为完整结构,给后续逻辑使用
125
+ */
126
+ function normalizeFontFace(face) {
127
+ return {
128
+ 'font-family': face['font-family'] ?? '',
129
+ src: face.src ?? '',
130
+ 'font-style': face['font-style'] ?? 'normal',
131
+ 'font-display': face['font-display'] ?? 'swap',
132
+ 'font-weight': face['font-weight'] ?? '400',
133
+ 'unicode-range': face['unicode-range'] ?? 'U+0-FFFF',
134
+ };
135
+ }
136
+ /**
137
+ * 将 @font-face 写入 <style>,插入到 SVG 中合适的位置
138
+ */
139
+ function insertFontStyle(svg, fontFaces) {
140
+ // 简单去重:相同 family + weight + style + unicode-range + src 只保留一条
141
+ const unique = [];
142
+ const seen = new Set();
143
+ for (const f of fontFaces) {
144
+ const key = [
145
+ f['font-family'],
146
+ f['font-weight'],
147
+ f['font-style'],
148
+ f['unicode-range'],
149
+ f.src,
150
+ ].join('|');
151
+ if (!seen.has(key)) {
152
+ seen.add(key);
153
+ unique.push(f);
154
+ }
155
+ }
156
+ if (unique.length === 0)
157
+ return;
158
+ const style = createElement('style', { type: 'text/css' });
159
+ style.innerHTML = unique
160
+ .map((f) => `
161
+ @font-face {
162
+ font-family: ${f['font-family']};
163
+ src: ${f.src};
164
+ font-style: ${f['font-style']};
165
+ font-weight: ${f['font-weight']};
166
+ font-display: ${f['font-display']};
167
+ unicode-range: ${f['unicode-range']};
168
+ }
169
+ `.trim())
170
+ .join('\n');
171
+ // 尽量插在 <defs> 后面,否则插在第一个子节点前
172
+ const defs = svg.querySelector('defs');
173
+ if (defs && defs.parentNode) {
174
+ defs.parentNode.insertBefore(style, defs.nextSibling);
175
+ }
176
+ else {
177
+ svg.insertBefore(style, svg.firstChild);
178
+ }
179
+ }
180
+ /**
181
+ * 加载 woff2 并转为 DataURL,带缓存
182
+ */
183
+ async function loadWoff2(url) {
184
+ const cached = fontDataUrlCache.get(url);
185
+ if (cached)
186
+ return cached;
187
+ const response = await fetch(url);
188
+ if (!response.ok) {
189
+ throw new Error(`Failed to load font: ${url}`);
190
+ }
191
+ const blob = await response.blob();
192
+ const dataUrl = await new Promise((resolve, reject) => {
193
+ const reader = new FileReader();
194
+ reader.onloadend = () => {
195
+ resolve(reader.result);
196
+ };
197
+ reader.onerror = reject;
198
+ reader.readAsDataURL(blob);
199
+ });
200
+ fontDataUrlCache.set(url, dataUrl);
201
+ return dataUrl;
202
+ }
@@ -0,0 +1,3 @@
1
+ export { exportToPNGString } from './png';
2
+ export { exportToSVGString } from './svg';
3
+ export type { ExportOptions, PNGExportOptions, SVGExportOptions, } from './types';
@@ -0,0 +1,2 @@
1
+ export { exportToPNGString } from './png';
2
+ export { exportToSVGString } from './svg';
@@ -0,0 +1,2 @@
1
+ import { PNGExportOptions } from './types';
2
+ export declare function exportToPNGString(svg: SVGSVGElement, options?: Omit<PNGExportOptions, 'type'>): Promise<string>;
@@ -0,0 +1,42 @@
1
+ import { getViewBox } from '../utils';
2
+ import { exportToSVG } from './svg';
3
+ export async function exportToPNGString(svg, options = {}) {
4
+ const { dpr = globalThis.devicePixelRatio ?? 2 } = options;
5
+ const node = await exportToSVG(svg);
6
+ const { width, height } = getViewBox(node);
7
+ return new Promise((resolve, reject) => {
8
+ try {
9
+ const canvas = document.createElement('canvas');
10
+ canvas.width = width * dpr;
11
+ canvas.height = height * dpr;
12
+ const ctx = canvas.getContext('2d');
13
+ if (!ctx) {
14
+ reject(new Error('Failed to get canvas context'));
15
+ return;
16
+ }
17
+ // 应用 DPR 缩放
18
+ ctx.scale(dpr, dpr);
19
+ ctx.imageSmoothingEnabled = true;
20
+ ctx.imageSmoothingQuality = 'high';
21
+ node.setAttribute('width', String(width));
22
+ node.setAttribute('height', String(height));
23
+ const updatedSvgData = new XMLSerializer().serializeToString(node);
24
+ const svgURL = 'data:image/svg+xml;charset=utf-8,' +
25
+ encodeURIComponent(updatedSvgData);
26
+ const img = new Image();
27
+ img.onload = function () {
28
+ ctx.clearRect(0, 0, width, height);
29
+ ctx.drawImage(img, 0, 0, width, height);
30
+ const pngURL = canvas.toDataURL('image/png');
31
+ resolve(pngURL);
32
+ };
33
+ img.onerror = function (error) {
34
+ reject(new Error('Image load failed: ' + error));
35
+ };
36
+ img.src = svgURL;
37
+ }
38
+ catch (error) {
39
+ reject(error);
40
+ }
41
+ });
42
+ }
@@ -0,0 +1,3 @@
1
+ import type { SVGExportOptions } from './types';
2
+ export declare function exportToSVGString(svg: SVGSVGElement, options?: Omit<SVGExportOptions, 'type'>): Promise<string>;
3
+ export declare function exportToSVG(svg: SVGSVGElement, options?: Omit<SVGExportOptions, 'type'>): Promise<SVGSVGElement>;
@@ -0,0 +1,72 @@
1
+ import { createElement, getViewBox, setAttributes, traverse } from '../utils';
2
+ import { embedFonts } from './font';
3
+ export async function exportToSVGString(svg, options = {}) {
4
+ const node = await exportToSVG(svg, options);
5
+ const str = new XMLSerializer().serializeToString(node);
6
+ return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(str);
7
+ }
8
+ export async function exportToSVG(svg, options = {}) {
9
+ const { embedResources = true } = options;
10
+ const clonedSVG = svg.cloneNode(true);
11
+ const { width, height } = getViewBox(svg);
12
+ setAttributes(clonedSVG, { width, height });
13
+ await embedIcons(clonedSVG);
14
+ await embedFonts(clonedSVG, embedResources);
15
+ cleanSVG(clonedSVG);
16
+ return clonedSVG;
17
+ }
18
+ async function embedIcons(svg) {
19
+ const icons = svg.querySelectorAll('use');
20
+ const defs = getDefs(svg);
21
+ icons.forEach((icon) => {
22
+ const href = icon.getAttribute('href');
23
+ if (!href)
24
+ return;
25
+ const existsSymbol = svg.querySelector(href);
26
+ if (!existsSymbol) {
27
+ const symbolElement = document.querySelector(href);
28
+ if (symbolElement)
29
+ defs.appendChild(symbolElement.cloneNode(true));
30
+ }
31
+ });
32
+ }
33
+ function getDefs(svg) {
34
+ const defs = svg.querySelector('defs[id="icon-defs"]');
35
+ if (defs)
36
+ return defs;
37
+ const _defs = createElement('defs', { id: 'icon-defs' });
38
+ svg.prepend(_defs);
39
+ return _defs;
40
+ }
41
+ function cleanSVG(svg) {
42
+ removeBtnGroup(svg);
43
+ removeTransientContainer(svg);
44
+ removeUselessAttrs(svg);
45
+ clearDataset(svg);
46
+ }
47
+ function removeBtnGroup(svg) {
48
+ const btnGroup = svg.querySelector('[data-element-type=btns-group]');
49
+ btnGroup?.remove();
50
+ const btnIconDefs = svg.querySelector('#btn-icon-defs');
51
+ btnIconDefs?.remove();
52
+ }
53
+ function removeTransientContainer(svg) {
54
+ const transientContainer = svg.querySelector('[data-element-type=transient-container]');
55
+ transientContainer?.remove();
56
+ }
57
+ function removeUselessAttrs(svg) {
58
+ const groups = svg.querySelectorAll('g');
59
+ groups.forEach((group) => {
60
+ group.removeAttribute('x');
61
+ group.removeAttribute('y');
62
+ group.removeAttribute('width');
63
+ group.removeAttribute('height');
64
+ });
65
+ }
66
+ function clearDataset(svg) {
67
+ traverse(svg, (node) => {
68
+ for (const key in node.dataset) {
69
+ delete node.dataset[key];
70
+ }
71
+ });
72
+ }
@@ -0,0 +1,17 @@
1
+ export interface SVGExportOptions {
2
+ type: 'svg';
3
+ /**
4
+ * 是否将远程资源嵌入到 SVG 中
5
+ * @default true
6
+ */
7
+ embedResources?: boolean;
8
+ }
9
+ export interface PNGExportOptions {
10
+ type: 'png';
11
+ /**
12
+ * 设备像素比,默认为浏览器的 devicePixelRatio
13
+ * @default globalThis.devicePixelRatio || 2
14
+ */
15
+ dpr?: number;
16
+ }
17
+ export type ExportOptions = SVGExportOptions | PNGExportOptions;
package/esm/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export { getTheme, getThemes, registerTheme } from './themes';
10
10
  export { parseSVG } from './utils';
11
11
  export type { Bounds, ComponentType, DefsProps, EllipseProps, FragmentProps, GroupProps, JSXElement, JSXElementConstructor, JSXNode, PathProps, Point, PolygonProps, RectProps, RenderableNode, SVGAttributes, SVGProps, TextProps, WithChildren, } from './jsx';
12
12
  export type { InfographicOptions, ParsedInfographicOptions } from './options';
13
- export type { Font, FontWeightName, GradientConfig, IRenderer, LinearGradient, Palette, PatternConfig, PatternGenerator, PatternStyle, RadialGradient, RoughConfig, StylizeConfig, TextAlignment, TextHorizontalAlign, TextVerticalAlign, } from './renderer';
13
+ export type { GradientConfig, IRenderer, LinearGradient, Palette, PatternConfig, PatternGenerator, PatternStyle, RadialGradient, RoughConfig, StylizeConfig, TextAlignment, TextHorizontalAlign, TextVerticalAlign, } from './renderer';
14
14
  export type { ParsedTemplateOptions, TemplateOptions } from './templates';
15
15
  export type { ThemeColors, ThemeConfig, ThemeSeed } from './themes';
16
- export type { Data, ImageResource, ItemDatum } from './types';
16
+ export type { Data, Font, FontWeightName, ImageResource, ItemDatum, } from './types';
@@ -1,6 +1,5 @@
1
1
  import { get, kebabCase } from 'lodash-es';
2
- import { createTextElement, getAttributes, getDatumByIndexes, getItemIndexes, setAttributes, } from '../../utils';
3
- import { encodeFontFamily } from '../fonts';
2
+ import { createTextElement, encodeFontFamily, getAttributes, getDatumByIndexes, getItemIndexes, setAttributes, } from '../../utils';
4
3
  import { parseDynamicAttributes } from '../utils';
5
4
  export function renderText(node, text, attrs = {}) {
6
5
  if (!text)
@@ -12,7 +11,6 @@ export function renderText(node, text, attrs = {}) {
12
11
  for (const key in textElement.dataset) {
13
12
  renderedText.setAttribute(`data-${kebabCase(key)}`, textElement.dataset[key]);
14
13
  }
15
- renderedText.setAttribute('id', node.getAttribute('id'));
16
14
  return renderedText;
17
15
  }
18
16
  export function renderItemText(type, node, options) {
@@ -1,2 +1,2 @@
1
- import { Font } from './types';
1
+ import type { Font } from '../../types';
2
2
  export declare const BUILT_IN_FONTS: Font[];
@@ -1,4 +1,2 @@
1
1
  export { getFontURLs, getWoff2BaseURL, loadFont, loadFonts } from './loader';
2
2
  export { DEFAULT_FONT, getFont, getFonts, registerFont, setDefaultFont, } from './registry';
3
- export type * from './types';
4
- export { decodeFontFamily, encodeFontFamily } from './utils';
@@ -1,6 +1,5 @@
1
1
  export { getFontURLs, getWoff2BaseURL, loadFont, loadFonts } from './loader';
2
2
  export { DEFAULT_FONT, getFont, getFonts, registerFont, setDefaultFont, } from './registry';
3
- export { decodeFontFamily, encodeFontFamily } from './utils';
4
3
  import { BUILT_IN_FONTS } from './built-in';
5
4
  import { registerFont } from './registry';
6
5
  BUILT_IN_FONTS.forEach(registerFont);
@@ -1,6 +1,5 @@
1
- import { join } from '../../utils';
1
+ import { join, normalizeFontWeightName } from '../../utils';
2
2
  import { getFont, getFonts } from './registry';
3
- import { normalizeFontWeightName } from './utils';
4
3
  export function getFontURLs(font) {
5
4
  const config = getFont(font);
6
5
  if (!config)
@@ -1,4 +1,4 @@
1
- import type { Font } from './types';
1
+ import type { Font } from '../../types';
2
2
  export declare let DEFAULT_FONT: string;
3
3
  export declare function getFont(font: string): Font | null;
4
4
  export declare function getFonts(): Font[];
@@ -1,4 +1,4 @@
1
- import { decodeFontFamily, encodeFontFamily } from './utils';
1
+ import { decodeFontFamily, encodeFontFamily } from '../../utils';
2
2
  const FONT_REGISTRY = new Map();
3
3
  export let DEFAULT_FONT = 'Alibaba PuHuiTi';
4
4
  export function getFont(font) {
@@ -1,4 +1,4 @@
1
- import { getDatumByIndexes, getItemIndexes, getSizeBaseVal, isBtnsGroup, isDesc, isGroup, isIllus, isItemDesc, isItemIcon, isItemIllus, isItemLabel, isItemValue, isShape, isShapesGroup, isText, isTitle, parsePadding, setAttributes, } from '../utils';
1
+ import { getDatumByIndexes, getItemIndexes, isBtnsGroup, isDesc, isGroup, isIllus, isItemDesc, isItemIcon, isItemIllus, isItemLabel, isItemValue, isShape, isShapesGroup, isText, isTitle, parsePadding, setAttributes, setSVGPadding, } from '../utils';
2
2
  import { renderBackground, renderBaseElement, renderButtonsGroup, renderIllus, renderItemIcon, renderItemText, renderShape, renderStaticShape, renderStaticText, renderText, } from './composites';
3
3
  import { loadFonts } from './fonts';
4
4
  const upsert = (original, modified) => {
@@ -134,52 +134,3 @@ function setView(svg, options) {
134
134
  setSVGPadding(svg, parsePadding(padding));
135
135
  }
136
136
  }
137
- function setSVGPadding(svg, padding, options = {}) {
138
- const { preserveAspectRatio = false } = options;
139
- if (!svg.isConnected)
140
- return false;
141
- try {
142
- const bbox = svg.getBBox();
143
- // 检查包围盒是否有效
144
- if (bbox.width === 0 || bbox.height === 0) {
145
- return false;
146
- }
147
- const [widthBaseVal, heightBaseVal] = getSizeBaseVal(svg);
148
- const svgWidth = widthBaseVal || svg.clientWidth || 0;
149
- const svgHeight = heightBaseVal || svg.clientHeight || 0;
150
- const parentElement = svg.parentElement;
151
- const effectiveWidth = svgWidth || (parentElement ? parentElement.clientWidth : 300);
152
- const effectiveHeight = svgHeight || (parentElement ? parentElement.clientHeight : 150);
153
- let viewBoxPadding;
154
- if (effectiveWidth > 0 && effectiveHeight > 0) {
155
- const scaleX = bbox.width / effectiveWidth;
156
- const scaleY = bbox.height / effectiveHeight;
157
- if (preserveAspectRatio) {
158
- const scale = Math.max(scaleX, scaleY);
159
- viewBoxPadding = padding.map((p) => p * scale);
160
- }
161
- else {
162
- viewBoxPadding = [
163
- padding[0] * scaleY,
164
- padding[1] * scaleX,
165
- padding[2] * scaleY,
166
- padding[3] * scaleX,
167
- ];
168
- }
169
- }
170
- else {
171
- viewBoxPadding = [...padding];
172
- }
173
- const newViewBox = [
174
- bbox.x - viewBoxPadding[3],
175
- bbox.y - viewBoxPadding[0],
176
- bbox.width + viewBoxPadding[1] + viewBoxPadding[3],
177
- bbox.height + viewBoxPadding[0] + viewBoxPadding[2],
178
- ].join(' ');
179
- svg.setAttribute('viewBox', newViewBox);
180
- return true;
181
- }
182
- catch {
183
- return false;
184
- }
185
- }
@@ -4,7 +4,7 @@ export function getResourceId(config) {
4
4
  const cfg = typeof config === 'string' ? parseDataURI(config) : config;
5
5
  if (!cfg)
6
6
  return null;
7
- return getSimpleHash(JSON.stringify(cfg));
7
+ return 'rsc-' + getSimpleHash(JSON.stringify(cfg));
8
8
  }
9
9
  export function getResourceHref(config) {
10
10
  const id = getResourceId(config);
@@ -1,6 +1,8 @@
1
+ import { ExportOptions } from '../exporter';
1
2
  import { InfographicOptions } from '../options';
2
3
  export declare class Infographic {
3
4
  private options;
5
+ private node;
4
6
  private parsedOptions;
5
7
  constructor(options: InfographicOptions);
6
8
  /**
@@ -12,4 +14,12 @@ export declare class Infographic {
12
14
  */
13
15
  compose(): SVGSVGElement;
14
16
  getTypes(): string;
17
+ /**
18
+ * Export the infographic to data URL
19
+ * @param options Export option
20
+ * @returns Data URL string of the exported infographic
21
+ * @description This method need to be called after `render()` and in a browser environment.
22
+ */
23
+ toDataURL(options?: ExportOptions): Promise<string>;
24
+ destroy(): void;
15
25
  }
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "@antv/infographic/jsx-runtime";
2
+ import { exportToPNGString, exportToSVGString, } from '../exporter';
2
3
  import { renderSVG } from '../jsx';
3
4
  import { parseOptions, } from '../options';
4
5
  import { Renderer } from '../renderer';
@@ -6,6 +7,7 @@ import { getTypes, parseSVG } from '../utils';
6
7
  export class Infographic {
7
8
  constructor(options) {
8
9
  this.options = options;
10
+ this.node = null;
9
11
  this.parsedOptions = parseOptions(this.options);
10
12
  }
11
13
  /**
@@ -15,8 +17,8 @@ export class Infographic {
15
17
  const { container } = this.parsedOptions;
16
18
  const template = this.compose();
17
19
  const renderer = new Renderer(this.parsedOptions, template);
18
- const infographic = renderer.render();
19
- container.replaceChildren(infographic);
20
+ this.node = renderer.render();
21
+ container.replaceChildren(this.node);
20
22
  }
21
23
  /**
22
24
  * Compose the SVG template
@@ -41,4 +43,23 @@ export class Infographic {
41
43
  const items = design.items.map((it) => it.composites || []);
42
44
  return getTypes({ structure, items });
43
45
  }
46
+ /**
47
+ * Export the infographic to data URL
48
+ * @param options Export option
49
+ * @returns Data URL string of the exported infographic
50
+ * @description This method need to be called after `render()` and in a browser environment.
51
+ */
52
+ async toDataURL(options) {
53
+ if (!this.node) {
54
+ throw new Error('Infographic is not rendered yet.');
55
+ }
56
+ if (options?.type === 'svg') {
57
+ return await exportToSVGString(this.node, options);
58
+ }
59
+ return await exportToPNGString(this.node, options);
60
+ }
61
+ destroy() {
62
+ this.node?.remove();
63
+ this.node = null;
64
+ }
44
65
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  export type * from './attrs';
2
2
  export type * from './data';
3
3
  export type * from './element';
4
+ export type * from './font';
4
5
  export type * from './padding';
5
6
  export type * from './resource';
@@ -1,4 +1,4 @@
1
- import type { FontWeightName } from './types';
1
+ import type { FontWeightName } from '../types';
2
2
  export declare function decodeFontFamily(font: string): string;
3
3
  export declare function encodeFontFamily(font: string): string;
4
4
  export declare function normalizeFontWeightName(fontWeight: string | number): FontWeightName;
@@ -1,5 +1,6 @@
1
1
  export * from './color';
2
2
  export * from './data';
3
+ export * from './font';
3
4
  export * from './get-types';
4
5
  export * from './icon';
5
6
  export * from './item';
@@ -9,3 +10,4 @@ export * from './recognizer';
9
10
  export * from './svg';
10
11
  export * from './text';
11
12
  export * from './uuid';
13
+ export * from './viewbox';