@peter.prib/renderengine 0.10.0 → 0.13.0
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 +10 -4
- package/dist/renderengine-workspace.umd.js +5779 -3731
- package/dist/renderengine.umd.js +5757 -3725
- package/documentation/REFERENCE.md +3291 -3077
- package/package.json +2 -2
- package/src/charting/Axis.js +47 -5
- package/src/charting/Chart.js +25 -0
- package/src/charting/ChartChooser.js +5 -4
- package/src/charting/DiagramEditor.js +239 -34
- package/src/charting/Legend.js +16 -0
- package/src/charting/ModelFileFormat.js +47 -0
- package/src/charting/archimate/ArchimateCanvasEditor.js +1108 -1012
- package/src/charting/archimate/deleteFromModel.js +57 -0
- package/src/charting/archimate/elementMenuItems.js +3 -1
- package/src/charting/archimate/heatMaps.js +169 -0
- package/src/charting/archimate/index.js +1 -0
- package/src/charting/charts/BarChart.js +5 -5
- package/src/charting/charts/BarChartBase.js +21 -3
- package/src/charting/charts/GroupedBarChart.js +42 -22
- package/src/charting/charts/LineChart.js +9 -8
- package/src/charting/charts/PieChart.js +2 -2
- package/src/charting/charts/TableChart.js +34 -5
- package/src/charting/diagrams/ArchimateModel.js +33 -0
- package/src/charting/diagrams/DeleteFromModelCommand.js +81 -0
- package/src/charting/diagrams/EdgeGeometry.js +16 -0
- package/src/charting/edgeStyle/EdgeStyleSchema.js +6 -2
- package/src/charting/pivot/PivotChart.js +50 -0
- package/src/charting/pivot/PivotGridChart.js +191 -0
- package/src/charting/pivot/PivotView.js +740 -0
- package/src/charting/table/TableView.js +84 -15
- package/src/charting/table/TableViewChart.js +5 -3
- package/src/charting/templates/Template.js +10 -0
- package/src/exportImage.js +45 -18
- package/src/index.js +2899 -2807
- package/src/workspace/parts/ArchimateCanvasPart.js +3 -0
- package/src/workspace/parts/DiagramEditorPart.js +2 -0
- package/src/workspace/parts/ModelHosted.js +15 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChartChooser } from '../ChartChooser.js';
|
|
2
|
+
import { TableChart } from '../charts/TableChart.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* TableView — the state of an interactive table over result rows, kept apart from drawing so all of
|
|
@@ -52,8 +53,10 @@ export class TableView {
|
|
|
52
53
|
* @param {function} [options.copy] writes text to the clipboard (navigator.clipboard by default)
|
|
53
54
|
* @param {ChartChooser} [options.chooser]
|
|
54
55
|
* @param {function} [options.highlight] row → true for a row drawn highlighted (a host's selection)
|
|
56
|
+
* @param {function|Intl.NumberFormat|object} [options.format] how numbers show, for every column or per
|
|
57
|
+
* column (`TableChart.formatFor`). A copy keeps the raw numbers.
|
|
55
58
|
*/
|
|
56
|
-
constructor(rows, { columns = null, complete = true, pageSize = 50, onChange = null, menus = {}, copy = null, chooser = null, highlight = null } = {}) {
|
|
59
|
+
constructor(rows, { columns = null, complete = true, pageSize = 50, onChange = null, menus = {}, copy = null, chooser = null, highlight = null, format = null } = {}) {
|
|
57
60
|
this.rows = rows;
|
|
58
61
|
this.columns = ChartChooser.describe(rows, columns);
|
|
59
62
|
this.complete = complete;
|
|
@@ -63,12 +66,19 @@ export class TableView {
|
|
|
63
66
|
this.copy = copy ?? TableView.clipboard;
|
|
64
67
|
this.chooser = chooser ?? new ChartChooser();
|
|
65
68
|
this.highlight = highlight;
|
|
66
|
-
this.
|
|
69
|
+
this.format = format;
|
|
70
|
+
/** The sort keys, most significant first: `{ column, direction: 'asc' | 'desc' }`. */
|
|
71
|
+
this.sorts = [];
|
|
67
72
|
this.filters = []; // { column, op, value }
|
|
68
73
|
this.hidden = new Set();
|
|
69
74
|
this.page = 0;
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
/** A column's value as the table shows it, in the column's `format` when there is one. */
|
|
78
|
+
valueText(column, value, nullText = '—') {
|
|
79
|
+
return TableChart.cellText(value, nullText, TableChart.formatFor(this.format, column));
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
static clipboard(text) {
|
|
73
83
|
return globalThis.navigator?.clipboard?.writeText(text);
|
|
74
84
|
}
|
|
@@ -85,14 +95,36 @@ export class TableView {
|
|
|
85
95
|
: this.rows.filter(r => this.filters.every(f => FILTER_OPS[f.op](r[f.column], f.value)));
|
|
86
96
|
}
|
|
87
97
|
|
|
88
|
-
/** The
|
|
98
|
+
/** The first sort key, or null — the one a single-column sort has. */
|
|
99
|
+
get sort() { return this.sorts[0] ?? null; }
|
|
100
|
+
|
|
101
|
+
/** Where `column` is among the sort keys: `{ direction, rank }` (rank from 1), or null. */
|
|
102
|
+
sortOf(column) {
|
|
103
|
+
const at = this.sorts.findIndex(s => s.column === column);
|
|
104
|
+
return at < 0 ? null : { direction: this.sorts[at].direction, rank: at + 1 };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The filtered rows in sort order (a copy): by the first key, then the next among equals, and so
|
|
109
|
+
* on; rows equal on every key keep their order.
|
|
110
|
+
*/
|
|
89
111
|
ordered() {
|
|
90
112
|
const rows = this.filtered();
|
|
91
|
-
if (this.
|
|
92
|
-
const { column, direction }
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
if (this.sorts.length === 0) return rows;
|
|
114
|
+
const keys = this.sorts.map(({ column, direction }) => ({ column, sign: direction === 'desc' ? -1 : 1 }));
|
|
115
|
+
return [...rows].sort((a, b) => {
|
|
116
|
+
for (const { column, sign } of keys) {
|
|
117
|
+
// Nulls stay last whichever way: a reader looks for values, not for blanks.
|
|
118
|
+
const c = a[column] == null || b[column] == null ? compareValues(a[column], b[column]) : sign * compareValues(a[column], b[column]);
|
|
119
|
+
if (c !== 0) return c;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** "Region ▲, then Revenue ▼", or '' when unsorted. */
|
|
126
|
+
describeSort() {
|
|
127
|
+
return this.sorts.map(s => `${s.column} ${s.direction === 'asc' ? '▲' : '▼'}`).join(', then ');
|
|
96
128
|
}
|
|
97
129
|
|
|
98
130
|
pageCount() {
|
|
@@ -119,14 +151,41 @@ export class TableView {
|
|
|
119
151
|
this.onChange?.(this);
|
|
120
152
|
}
|
|
121
153
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
154
|
+
/**
|
|
155
|
+
* A header click: unsorted → ascending → descending → unsorted, by this column alone. With
|
|
156
|
+
* `{ then: true }` (a shift-click) the column is a further key instead: added after the others,
|
|
157
|
+
* or, if it is one already, cycled in its place — the other keys stay.
|
|
158
|
+
*/
|
|
159
|
+
toggleSort(column, { then = false } = {}) {
|
|
160
|
+
const next = (d) => (d === null ? 'asc' : d === 'asc' ? 'desc' : null);
|
|
161
|
+
if (then) {
|
|
162
|
+
this.thenSortBy(column, next(this.sortOf(column)?.direction ?? null));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const alone = this.sorts.length === 1 && this.sorts[0].column === column ? this.sorts[0].direction : null;
|
|
166
|
+
this.sortBy(column, next(alone));
|
|
126
167
|
}
|
|
127
168
|
|
|
169
|
+
/** Sorts by `column` alone (null: unsorted). */
|
|
128
170
|
sortBy(column, direction) {
|
|
129
|
-
this.
|
|
171
|
+
this.sorts = direction === null ? [] : [{ column, direction }];
|
|
172
|
+
this.page = 0;
|
|
173
|
+
this.changed();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** `column` as a further sort key: its direction changed in place, appended if new, removed with null. */
|
|
177
|
+
thenSortBy(column, direction) {
|
|
178
|
+
const at = this.sorts.findIndex(s => s.column === column);
|
|
179
|
+
if (direction === null) this.sorts = this.sorts.filter(s => s.column !== column);
|
|
180
|
+
else if (at >= 0) this.sorts = this.sorts.map(s => (s.column === column ? { column, direction } : s));
|
|
181
|
+
else this.sorts = [...this.sorts, { column, direction }];
|
|
182
|
+
this.page = 0;
|
|
183
|
+
this.changed();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Unsorted: rows in the order given. */
|
|
187
|
+
clearSort() {
|
|
188
|
+
this.sorts = [];
|
|
130
189
|
this.page = 0;
|
|
131
190
|
this.changed();
|
|
132
191
|
}
|
|
@@ -206,13 +265,23 @@ export class TableView {
|
|
|
206
265
|
}
|
|
207
266
|
|
|
208
267
|
columnMenu(column) {
|
|
209
|
-
const
|
|
268
|
+
const key = this.sortOf(column);
|
|
269
|
+
const alone = this.sorts.length === 1 && key !== null;
|
|
270
|
+
const sorted = alone ? key.direction : null;
|
|
271
|
+
// A further key: offered once something else is sorted (so this column is never the only key
|
|
272
|
+
// here); ticked where this column already is one.
|
|
273
|
+
const further = this.sorts.some(s => s.column !== column) ? [
|
|
274
|
+
{ label: 'Then sort ascending', checked: key?.direction === 'asc', action: () => this.thenSortBy(column, 'asc') },
|
|
275
|
+
{ label: 'Then sort descending', checked: key?.direction === 'desc', action: () => this.thenSortBy(column, 'desc') },
|
|
276
|
+
...(key !== null ? [{ label: 'Stop sorting by this column', action: () => this.thenSortBy(column, null) }] : []),
|
|
277
|
+
] : [];
|
|
210
278
|
const values = [...new Set(this.filtered().map(r => r[column]))].sort(compareValues);
|
|
211
279
|
const listed = values.slice(0, TableView.MAX_FILTER_VALUES);
|
|
212
280
|
return [
|
|
213
281
|
{ label: 'Sort ascending', checked: sorted === 'asc', action: () => this.sortBy(column, 'asc') },
|
|
214
282
|
{ label: 'Sort descending', checked: sorted === 'desc', action: () => this.sortBy(column, 'desc') },
|
|
215
|
-
...
|
|
283
|
+
...further,
|
|
284
|
+
...(this.sorts.length > 0 ? [{ label: 'Clear sort', action: () => this.clearSort() }] : []),
|
|
216
285
|
{ separator: true },
|
|
217
286
|
{
|
|
218
287
|
label: 'Filter to',
|
|
@@ -58,13 +58,15 @@ export class TableViewChart extends TableChart {
|
|
|
58
58
|
|
|
59
59
|
columns.forEach((c, i) => {
|
|
60
60
|
const left = x0 + i * colW;
|
|
61
|
-
|
|
61
|
+
// ▲ / ▼, and with several keys their order: "Region ▲1", "Revenue ▼2".
|
|
62
|
+
const key = view.sortOf(c.key);
|
|
63
|
+
const sorted = key === null ? '' : ` ${key.direction === 'asc' ? '▲' : '▼'}${view.sorts.length > 1 ? key.rank : ''}`;
|
|
62
64
|
// In words: an arrow-like mark here read as a second sort arrow.
|
|
63
65
|
const filtered = view.filters.some(f => f.column === c.key) ? ' (filtered)' : '';
|
|
64
66
|
const head = this.renderer.rect(left, top, colW, ROW_H, TableChart.HEADER_FILL, null);
|
|
65
67
|
// The label leaves room at the right for the ▾.
|
|
66
68
|
const label = this._cell(`${c.label}${sorted}${filtered}`, c, left, top, colW - 16, { fontWeight: 'bold', fill: '#333' });
|
|
67
|
-
this._on(interactive, [head, label], () => view.toggleSort(c.key), 'Sort');
|
|
69
|
+
this._on(interactive, [head, label], (e) => view.toggleSort(c.key, { then: e?.shiftKey === true }), 'Sort (shift-click: then sort by)');
|
|
68
70
|
const glyph = this._text(TableViewChart.MENU_GLYPH, left + colW - 8, top, { textAnchor: 'middle', fill: '#777' });
|
|
69
71
|
this._on(interactive, [glyph], (e) => this.showMenu(e.clientX, e.clientY, view.columnMenu(c.key)), `${c.label} menu`);
|
|
70
72
|
});
|
|
@@ -80,7 +82,7 @@ export class TableViewChart extends TableChart {
|
|
|
80
82
|
columns.forEach((c, i) => {
|
|
81
83
|
const left = x0 + i * colW;
|
|
82
84
|
const hit = this.renderer.rect(left, top, colW, ROW_H, 'transparent', null);
|
|
83
|
-
const text = this._cell(
|
|
85
|
+
const text = this._cell(view.valueText(c.key, row[c.key], nullText), c, left, top, colW, { fill: row[c.key] == null ? '#999' : '#222' });
|
|
84
86
|
this._on(interactive, [hit, text], (e) => this.showMenu(e.clientX, e.clientY, view.cellMenu(row, c.key)), null);
|
|
85
87
|
});
|
|
86
88
|
top += ROW_H;
|
|
@@ -102,6 +102,16 @@ export class Template {
|
|
|
102
102
|
return { x: this.x + ux * t, y: this.y + uy * t };
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Where a line from (px, py), a point inside the shape, heading in
|
|
107
|
+
* direction (dx, dy) leaves its outline (the shape unrotated).
|
|
108
|
+
* @returns {{x: number, y: number}}
|
|
109
|
+
*/
|
|
110
|
+
exitPoint(px, py, dx, dy) {
|
|
111
|
+
const t = rayExitDistance(this.outline(), px, py, dx, dy);
|
|
112
|
+
return { x: px + dx * t, y: py + dy * t };
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
/** Centre of the name text; the z-index line sits just below it. */
|
|
106
116
|
get labelAnchor() { return { x: this.x, y: this.y - 8 }; }
|
|
107
117
|
|
package/src/exportImage.js
CHANGED
|
@@ -31,10 +31,10 @@ export const MAX_IMAGE_PIXELS = 32_000_000;
|
|
|
31
31
|
/**
|
|
32
32
|
* Work out the bitmap size to rasterize at.
|
|
33
33
|
*
|
|
34
|
-
* `scale` is what the caller asked for (2 = retina-sharp
|
|
35
|
-
* the diagram's own dimensions, when the result
|
|
36
|
-
* scale of a huge diagram still shows all of
|
|
37
|
-
* the diagram loses content or legibility.
|
|
34
|
+
* `scale` is what the caller asked for (1 = 100%, 2 = retina-sharp, 0.5 = half
|
|
35
|
+
* size). It is reduced, never the diagram's own dimensions, when the result
|
|
36
|
+
* would be too large: halving the scale of a huge diagram still shows all of
|
|
37
|
+
* it, whereas cropping or shrinking the diagram loses content or legibility.
|
|
38
38
|
*
|
|
39
39
|
* @returns {{width:number, height:number, scale:number, clamped:boolean}}
|
|
40
40
|
*/
|
|
@@ -49,10 +49,6 @@ export function imageSize({ width, height, scale = 2, maxPixels = MAX_IMAGE_PIXE
|
|
|
49
49
|
s = Math.sqrt(maxPixels / (w * h));
|
|
50
50
|
clamped = true;
|
|
51
51
|
}
|
|
52
|
-
// A scale below 1:1 would be a downscale the caller never asked for, but a
|
|
53
|
-
// diagram that is itself over the budget leaves no choice.
|
|
54
|
-
if (s < 1 && w * h <= maxPixels) s = 1;
|
|
55
|
-
|
|
56
52
|
return { width: Math.max(1, Math.round(w * s)), height: Math.max(1, Math.round(h * s)), scale: s, clamped };
|
|
57
53
|
}
|
|
58
54
|
|
|
@@ -67,8 +63,10 @@ export function imageSize({ width, height, scale = 2, maxPixels = MAX_IMAGE_PIXE
|
|
|
67
63
|
* @param {number} [options.width] pixels; omitted leaves what is there
|
|
68
64
|
* @param {number} [options.height]
|
|
69
65
|
* @param {string|null} [options.background='#ffffff'] null for transparent
|
|
66
|
+
* @param {string} [options.viewBox] the drawing's own units, when it has none
|
|
67
|
+
* (so a picture sized other than as drawn scales rather than crops)
|
|
70
68
|
*/
|
|
71
|
-
export function prepareSvgMarkup(markup, { width, height, background = '#ffffff' } = {}) {
|
|
69
|
+
export function prepareSvgMarkup(markup, { width, height, background = '#ffffff', viewBox } = {}) {
|
|
72
70
|
let out = String(markup ?? '').trim();
|
|
73
71
|
const open = out.match(/^<svg\b[^>]*>/i);
|
|
74
72
|
if (!open) return out; // not an <svg> — hand it back untouched
|
|
@@ -87,13 +85,23 @@ export function prepareSvgMarkup(markup, { width, height, background = '#ffffff'
|
|
|
87
85
|
};
|
|
88
86
|
if (Number.isFinite(width) && width > 0) setAttr('width', Math.round(width));
|
|
89
87
|
if (Number.isFinite(height) && height > 0) setAttr('height', Math.round(height));
|
|
88
|
+
if (viewBox && !/\sviewBox\s*=/i.test(tag)) setAttr('viewBox', viewBox);
|
|
90
89
|
|
|
91
90
|
out = tag + out.slice(open[0].length);
|
|
92
91
|
|
|
93
92
|
if (background) {
|
|
94
|
-
// First child, so it sits behind everything.
|
|
95
|
-
//
|
|
96
|
-
|
|
93
|
+
// First child, so it sits behind everything. With a viewBox it reaches
|
|
94
|
+
// a whole viewBox past each side (the picture clips it): a percentage
|
|
95
|
+
// counts from (0,0), so a viewBox starting elsewhere (a cropped
|
|
96
|
+
// drawing) would leave a strip bare, and the viewBox alone would leave
|
|
97
|
+
// the bands a picture of another shape letterboxes it with.
|
|
98
|
+
// Without one, percentages, so it covers whatever size it is shown at.
|
|
99
|
+
const box = tag.match(/\sviewBox\s*=\s*["']\s*([-\d.e+]+)[\s,]+([-\d.e+]+)[\s,]+([-\d.e+]+)[\s,]+([-\d.e+]+)\s*["']/i);
|
|
100
|
+
const [x, y, w, h] = box ? box.slice(1).map(Number) : [];
|
|
101
|
+
const rect = box
|
|
102
|
+
? `<rect x="${x - w}" y="${y - h}" width="${3 * w}" height="${3 * h}" fill="${background}"/>`
|
|
103
|
+
: `<rect x="0" y="0" width="100%" height="100%" fill="${background}"/>`;
|
|
104
|
+
out = out.replace(tag, `${tag}${rect}`);
|
|
97
105
|
}
|
|
98
106
|
return out;
|
|
99
107
|
}
|
|
@@ -126,12 +134,12 @@ export function findDrawable(host) {
|
|
|
126
134
|
* Size is taken from the element's laid-out box rather than its attributes,
|
|
127
135
|
* because that is what the user is actually looking at.
|
|
128
136
|
*/
|
|
129
|
-
export function serializeSvg(svgEl, { background = '#ffffff', width, height } = {}) {
|
|
137
|
+
export function serializeSvg(svgEl, { background = '#ffffff', width, height, scale = 1 } = {}) {
|
|
130
138
|
const box = svgEl.getBoundingClientRect?.() ?? {};
|
|
131
139
|
const w = width ?? svgEl.width?.baseVal?.value ?? box.width;
|
|
132
140
|
const h = height ?? svgEl.height?.baseVal?.value ?? box.height;
|
|
133
141
|
const markup = new XMLSerializer().serializeToString(svgEl);
|
|
134
|
-
return prepareSvgMarkup(markup, { width: w, height: h, background });
|
|
142
|
+
return prepareSvgMarkup(markup, { width: w * scale, height: h * scale, background, viewBox: `0 0 ${w} ${h}` });
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
/**
|
|
@@ -150,9 +158,16 @@ export async function toPngBlob(host, { scale = 2, background = '#ffffff', maxPi
|
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
const box = drawable.getBoundingClientRect();
|
|
153
|
-
const size = imageSize({ width: box.width, height: box.height, scale, maxPixels });
|
|
154
161
|
const markup = serializeSvg(drawable, { background, width: box.width, height: box.height });
|
|
162
|
+
return svgMarkupToPngBlob(markup, { width: box.width, height: box.height, scale, background, maxPixels });
|
|
163
|
+
}
|
|
155
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Rasterize standalone SVG markup (prepareSvgMarkup's) drawn `width` × `height`
|
|
167
|
+
* to a PNG Blob at `scale`. DOM only.
|
|
168
|
+
*/
|
|
169
|
+
export async function svgMarkupToPngBlob(markup, { width, height, scale = 1, background = '#ffffff', maxPixels = MAX_IMAGE_PIXELS } = {}) {
|
|
170
|
+
const size = imageSize({ width, height, scale, maxPixels });
|
|
156
171
|
// A data: URL rather than a blob: URL — a blob: URL taints nothing either,
|
|
157
172
|
// but Safari has historically refused to draw SVG from one.
|
|
158
173
|
const url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(markup)}`;
|
|
@@ -200,7 +215,15 @@ export function downloadBlob(blob, filename) {
|
|
|
200
215
|
* @returns {Promise<{via:'clipboard'|'download', filename?:string}>}
|
|
201
216
|
*/
|
|
202
217
|
export async function copyImage(host, { scale = 2, background = '#ffffff', name = 'diagram' } = {}) {
|
|
203
|
-
|
|
218
|
+
return copyPngBlob(await toPngBlob(host, { scale, background }), { name });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Put a PNG Blob on the clipboard, or download it where the browser cannot
|
|
223
|
+
* (as copyImage). DOM only.
|
|
224
|
+
* @returns {Promise<{via:'clipboard'|'download', filename?:string}>}
|
|
225
|
+
*/
|
|
226
|
+
export async function copyPngBlob(blob, { name = 'diagram' } = {}) {
|
|
204
227
|
const canWrite = typeof ClipboardItem !== 'undefined'
|
|
205
228
|
&& typeof navigator !== 'undefined'
|
|
206
229
|
&& navigator.clipboard?.write;
|
|
@@ -220,10 +243,14 @@ export async function copyImage(host, { scale = 2, background = '#ffffff', name
|
|
|
220
243
|
}
|
|
221
244
|
|
|
222
245
|
/** Save a diagram as an SVG file. DOM only. */
|
|
223
|
-
export function downloadSvg(host, { background = '#ffffff', name = 'diagram' } = {}) {
|
|
246
|
+
export function downloadSvg(host, { background = '#ffffff', name = 'diagram', scale = 1 } = {}) {
|
|
224
247
|
const drawable = findDrawable(host);
|
|
225
248
|
if (!drawable || drawable.tagName.toLowerCase() !== 'svg') throw new Error('No <svg> to export');
|
|
226
|
-
|
|
249
|
+
downloadSvgMarkup(serializeSvg(drawable, { background, scale }), { name });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Save standalone SVG markup as a file. DOM only. */
|
|
253
|
+
export function downloadSvgMarkup(markup, { name = 'diagram' } = {}) {
|
|
227
254
|
downloadBlob(new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }), imageFileName(name, 'svg'));
|
|
228
255
|
}
|
|
229
256
|
|