@antv/infographic 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/infographic.min.js +68 -68
- package/dist/infographic.min.js.map +1 -1
- package/esm/exporter/svg.js +129 -49
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/lib/exporter/svg.js +129 -49
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/exporter/svg.ts +187 -59
- package/src/version.ts +1 -1
package/src/exporter/svg.ts
CHANGED
|
@@ -11,6 +11,17 @@ import { embedFonts } from './font';
|
|
|
11
11
|
import type { SVGExportOptions } from './types';
|
|
12
12
|
|
|
13
13
|
const VIEWBOX_CHANGE_TOLERANCE = 0.5;
|
|
14
|
+
type BoundsTuple = [number, number, number, number];
|
|
15
|
+
|
|
16
|
+
interface ForeignObjectExportAdjustment {
|
|
17
|
+
rootBounds: BoundsTuple;
|
|
18
|
+
exportBounds: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
14
25
|
|
|
15
26
|
export async function exportToSVGString(
|
|
16
27
|
svg: SVGSVGElement,
|
|
@@ -51,41 +62,98 @@ function parseAbsoluteLength(value: string | null): number {
|
|
|
51
62
|
return Number.parseFloat(trimmed);
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
function
|
|
65
|
+
function parseCoordinate(value: string | null): number {
|
|
66
|
+
const parsed = parseAbsoluteLength(value);
|
|
67
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface MeasuredSpanContentDimensions {
|
|
71
|
+
height: number;
|
|
72
|
+
width: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function measureSpanContentDimensions(
|
|
76
|
+
span: HTMLElement,
|
|
77
|
+
measureWidth: boolean,
|
|
78
|
+
): MeasuredSpanContentDimensions {
|
|
55
79
|
const prevHeight = span.style.height;
|
|
80
|
+
const prevWidth = span.style.width;
|
|
56
81
|
const prevOverflow = span.style.overflow;
|
|
57
82
|
try {
|
|
58
83
|
span.style.height = 'max-content';
|
|
59
84
|
span.style.overflow = 'hidden';
|
|
60
85
|
void span.offsetHeight; // force reflow
|
|
61
|
-
|
|
86
|
+
const scrollHeight = span.scrollHeight;
|
|
87
|
+
const rectHeight = span.getBoundingClientRect().height;
|
|
88
|
+
let width = span.scrollWidth;
|
|
89
|
+
|
|
90
|
+
if (measureWidth) {
|
|
91
|
+
span.style.width = 'max-content';
|
|
92
|
+
void span.offsetWidth; // force reflow
|
|
93
|
+
width = span.scrollWidth;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
height: Math.max(scrollHeight, rectHeight),
|
|
98
|
+
width,
|
|
99
|
+
};
|
|
62
100
|
} finally {
|
|
63
101
|
span.style.height = prevHeight;
|
|
102
|
+
span.style.width = prevWidth;
|
|
64
103
|
span.style.overflow = prevOverflow;
|
|
65
104
|
}
|
|
66
105
|
}
|
|
67
106
|
|
|
68
|
-
function
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
107
|
+
function shouldKeepForeignObjectWidth(style: CSSStyleDeclaration): boolean {
|
|
108
|
+
const whiteSpace = style.whiteSpace;
|
|
109
|
+
const flexWrap = style.flexWrap;
|
|
110
|
+
const wordBreak = style.wordBreak;
|
|
111
|
+
const overflowWrap = style.overflowWrap;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
flexWrap === 'wrap' ||
|
|
115
|
+
flexWrap === 'wrap-reverse' ||
|
|
116
|
+
whiteSpace === 'pre-wrap' ||
|
|
117
|
+
whiteSpace === 'pre-line' ||
|
|
118
|
+
whiteSpace === 'normal' ||
|
|
119
|
+
overflowWrap === 'break-word' ||
|
|
120
|
+
wordBreak === 'break-word' ||
|
|
121
|
+
wordBreak === 'break-all'
|
|
122
|
+
);
|
|
80
123
|
}
|
|
81
124
|
|
|
82
|
-
|
|
125
|
+
function createCoordConverter(
|
|
126
|
+
svg: SVGSVGElement,
|
|
127
|
+
element: SVGGraphicsElement,
|
|
128
|
+
): ((x: number, y: number) => SVGPoint) | null {
|
|
129
|
+
if (typeof element.getScreenCTM !== 'function') return null;
|
|
130
|
+
const screenCTM = element.getScreenCTM();
|
|
131
|
+
if (!screenCTM) return null;
|
|
132
|
+
const inverseCTM = screenCTM.inverse();
|
|
133
|
+
|
|
134
|
+
return (clientX: number, clientY: number) => {
|
|
135
|
+
const pt = svg.createSVGPoint();
|
|
136
|
+
pt.x = clientX;
|
|
137
|
+
pt.y = clientY;
|
|
138
|
+
return pt.matrixTransform(inverseCTM);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Returns [left, top, right, bottom] in target coordinates for a foreignObject,
|
|
83
143
|
// accounting for flex alignment: bottom/center-aligned content can overflow,
|
|
84
144
|
// and horizontally aligned content can overflow as well.
|
|
85
145
|
function getFOContentBoundsInSVG(
|
|
86
146
|
fo: SVGForeignObjectElement,
|
|
87
|
-
content: HTMLElement,
|
|
88
147
|
toSVGCoord: (x: number, y: number) => SVGPoint,
|
|
148
|
+
{
|
|
149
|
+
contentHeight,
|
|
150
|
+
contentWidth,
|
|
151
|
+
keepForeignObjectWidth,
|
|
152
|
+
}: {
|
|
153
|
+
contentHeight: number;
|
|
154
|
+
contentWidth: number;
|
|
155
|
+
keepForeignObjectWidth: boolean;
|
|
156
|
+
},
|
|
89
157
|
): [number, number, number, number] {
|
|
90
158
|
const foRect = fo.getBoundingClientRect();
|
|
91
159
|
const foTopLeft = toSVGCoord(foRect.left, foRect.top);
|
|
@@ -103,20 +171,17 @@ function getFOContentBoundsInSVG(
|
|
|
103
171
|
foRect.height > 0 ? foHeightSVG / foRect.height : 1;
|
|
104
172
|
const svgUnitsPerClientPxX = foRect.width > 0 ? foWidthSVG / foRect.width : 1;
|
|
105
173
|
|
|
106
|
-
// Measure actual content dimensions
|
|
107
|
-
const realScrollHeight = measureSpanContentHeight(content);
|
|
108
174
|
const contentHeightSVG =
|
|
109
|
-
|
|
110
|
-
?
|
|
175
|
+
contentHeight > 0
|
|
176
|
+
? contentHeight * svgUnitsPerClientPxY
|
|
111
177
|
: foHeightSVG;
|
|
112
178
|
|
|
113
|
-
const
|
|
114
|
-
const contentWidthSVG =
|
|
115
|
-
realScrollWidth > 0 ? realScrollWidth * svgUnitsPerClientPxX : foWidthSVG;
|
|
116
|
-
|
|
117
|
-
const computedStyle = window.getComputedStyle(content);
|
|
179
|
+
const computedStyle = window.getComputedStyle(fo.firstElementChild as Element);
|
|
118
180
|
const alignItems = computedStyle.alignItems;
|
|
119
181
|
const justifyContent = computedStyle.justifyContent;
|
|
182
|
+
const contentWidthSVG = keepForeignObjectWidth
|
|
183
|
+
? foWidthSVG
|
|
184
|
+
: Math.max(foWidthSVG, contentWidth * svgUnitsPerClientPxX);
|
|
120
185
|
|
|
121
186
|
// Calculate vertical bounds
|
|
122
187
|
let top: number, bottom: number;
|
|
@@ -153,47 +218,91 @@ function getFOContentBoundsInSVG(
|
|
|
153
218
|
return [left, top, right, bottom];
|
|
154
219
|
}
|
|
155
220
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
221
|
+
function collectForeignObjectExportAdjustments(svg: SVGSVGElement) {
|
|
222
|
+
const toSVGCoord = createCoordConverter(svg, svg);
|
|
223
|
+
if (!toSVGCoord) return [];
|
|
224
|
+
|
|
225
|
+
return Array.from(
|
|
226
|
+
svg.querySelectorAll<SVGForeignObjectElement>('foreignObject'),
|
|
227
|
+
).map((fo) => {
|
|
228
|
+
const content = fo.firstElementChild as HTMLElement | null;
|
|
229
|
+
if (!content) return null;
|
|
230
|
+
const computedStyle = window.getComputedStyle(content);
|
|
231
|
+
const keepForeignObjectWidth = shouldKeepForeignObjectWidth(computedStyle);
|
|
232
|
+
const measuredContent = measureSpanContentDimensions(
|
|
233
|
+
content,
|
|
234
|
+
!keepForeignObjectWidth,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const parent =
|
|
238
|
+
fo.parentElement instanceof SVGGraphicsElement ? fo.parentElement : svg;
|
|
239
|
+
const toParentCoord = createCoordConverter(svg, parent);
|
|
240
|
+
const toLocalCoord = createCoordConverter(svg, fo);
|
|
241
|
+
if (!toParentCoord) return null;
|
|
242
|
+
|
|
243
|
+
const parentBounds = getFOContentBoundsInSVG(fo, toParentCoord, {
|
|
244
|
+
contentHeight: measuredContent.height,
|
|
245
|
+
contentWidth: measuredContent.width,
|
|
246
|
+
keepForeignObjectWidth,
|
|
247
|
+
});
|
|
248
|
+
const originalX = parseCoordinate(fo.getAttribute('x'));
|
|
249
|
+
const originalY = parseCoordinate(fo.getAttribute('y'));
|
|
250
|
+
const localBounds = toLocalCoord
|
|
251
|
+
? getFOContentBoundsInSVG(fo, toLocalCoord, {
|
|
252
|
+
contentHeight: measuredContent.height,
|
|
253
|
+
contentWidth: measuredContent.width,
|
|
254
|
+
keepForeignObjectWidth,
|
|
255
|
+
})
|
|
256
|
+
: null;
|
|
257
|
+
const hasTransform = fo.hasAttribute('transform');
|
|
258
|
+
if (hasTransform && !localBounds) return null;
|
|
259
|
+
|
|
260
|
+
const exportBounds = localBounds
|
|
261
|
+
? {
|
|
262
|
+
x: originalX + localBounds[0],
|
|
263
|
+
y: originalY + localBounds[1],
|
|
264
|
+
width: localBounds[2] - localBounds[0],
|
|
265
|
+
height: localBounds[3] - localBounds[1],
|
|
266
|
+
}
|
|
267
|
+
: {
|
|
268
|
+
x: parentBounds[0],
|
|
269
|
+
y: parentBounds[1],
|
|
270
|
+
width: parentBounds[2] - parentBounds[0],
|
|
271
|
+
height: parentBounds[3] - parentBounds[1],
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
rootBounds: getFOContentBoundsInSVG(fo, toSVGCoord, {
|
|
276
|
+
contentHeight: measuredContent.height,
|
|
277
|
+
contentWidth: measuredContent.width,
|
|
278
|
+
keepForeignObjectWidth,
|
|
279
|
+
}),
|
|
280
|
+
exportBounds,
|
|
281
|
+
} satisfies ForeignObjectExportAdjustment;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function computeFullViewBox(
|
|
286
|
+
svg: SVGSVGElement,
|
|
287
|
+
adjustments: Array<ForeignObjectExportAdjustment | null>,
|
|
288
|
+
): string | null {
|
|
162
289
|
const viewBox = getExportViewBox(svg);
|
|
163
290
|
if (!viewBox) return null;
|
|
164
291
|
|
|
165
|
-
if (typeof svg.getScreenCTM !== 'function') return null;
|
|
166
|
-
const screenCTM = svg.getScreenCTM();
|
|
167
|
-
if (!screenCTM) return null;
|
|
168
|
-
const inverseCTM = screenCTM.inverse();
|
|
169
|
-
|
|
170
|
-
const toSVGCoord = (clientX: number, clientY: number) => {
|
|
171
|
-
const pt = svg.createSVGPoint();
|
|
172
|
-
pt.x = clientX;
|
|
173
|
-
pt.y = clientY;
|
|
174
|
-
return pt.matrixTransform(inverseCTM);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
292
|
let minX = viewBox.x;
|
|
178
293
|
let minY = viewBox.y;
|
|
179
294
|
let maxX = viewBox.x + viewBox.width;
|
|
180
295
|
let maxY = viewBox.y + viewBox.height;
|
|
181
296
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
);
|
|
192
|
-
minX = Math.min(minX, left);
|
|
193
|
-
minY = Math.min(minY, top);
|
|
194
|
-
maxX = Math.max(maxX, right);
|
|
195
|
-
maxY = Math.max(maxY, bottom);
|
|
196
|
-
});
|
|
297
|
+
adjustments.forEach((adjustment) => {
|
|
298
|
+
if (!adjustment) return;
|
|
299
|
+
const { rootBounds } = adjustment;
|
|
300
|
+
const [left, top, right, bottom] = rootBounds;
|
|
301
|
+
minX = Math.min(minX, left);
|
|
302
|
+
minY = Math.min(minY, top);
|
|
303
|
+
maxX = Math.max(maxX, right);
|
|
304
|
+
maxY = Math.max(maxY, bottom);
|
|
305
|
+
});
|
|
197
306
|
|
|
198
307
|
const newX = minX;
|
|
199
308
|
const newY = minY;
|
|
@@ -210,6 +319,23 @@ function computeFullViewBox(svg: SVGSVGElement): string | null {
|
|
|
210
319
|
return `${newX} ${newY} ${newWidth} ${newHeight}`;
|
|
211
320
|
}
|
|
212
321
|
|
|
322
|
+
function applyForeignObjectExportAdjustments(
|
|
323
|
+
svg: SVGSVGElement,
|
|
324
|
+
adjustments: Array<ForeignObjectExportAdjustment | null>,
|
|
325
|
+
) {
|
|
326
|
+
const clonedForeignObjects = Array.from(
|
|
327
|
+
svg.querySelectorAll<SVGForeignObjectElement>('foreignObject'),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
adjustments.forEach((adjustment, index) => {
|
|
331
|
+
if (!adjustment) return;
|
|
332
|
+
const clonedForeignObject = clonedForeignObjects[index];
|
|
333
|
+
if (!clonedForeignObject) return;
|
|
334
|
+
|
|
335
|
+
setAttributes(clonedForeignObject, adjustment.exportBounds);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
213
339
|
export async function exportToSVG(
|
|
214
340
|
svg: SVGSVGElement,
|
|
215
341
|
options: Omit<SVGExportOptions, 'type'> = {},
|
|
@@ -222,7 +348,9 @@ export async function exportToSVG(
|
|
|
222
348
|
const clonedSVG = svg.cloneNode(true) as SVGSVGElement;
|
|
223
349
|
|
|
224
350
|
if (typeof document !== 'undefined') {
|
|
225
|
-
const
|
|
351
|
+
const adjustments = collectForeignObjectExportAdjustments(svg);
|
|
352
|
+
applyForeignObjectExportAdjustments(clonedSVG, adjustments);
|
|
353
|
+
const fullViewBox = computeFullViewBox(svg, adjustments);
|
|
226
354
|
if (fullViewBox) {
|
|
227
355
|
clonedSVG.setAttribute('viewBox', fullViewBox);
|
|
228
356
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.19';
|