svelte-plots-basic 2.5.7 → 3.0.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 +530 -445
- package/package.json +5 -2
- package/src/2d/Area.svelte +56 -48
- package/src/2d/Axes.svelte +409 -373
- package/src/2d/AxisLines.svelte +37 -36
- package/src/2d/AxisTickLabels.svelte +41 -49
- package/src/2d/Bars.svelte +102 -47
- package/src/2d/Box.svelte +21 -41
- package/src/2d/ColormapLegend.svelte +47 -111
- package/src/2d/Heatmap.svelte +107 -85
- package/src/2d/Legend.svelte +115 -126
- package/src/2d/Lines.svelte +54 -40
- package/src/2d/Multilines.svelte +73 -46
- package/src/2d/Points.svelte +76 -58
- package/src/2d/Rectangles.svelte +67 -58
- package/src/2d/Segments.svelte +59 -46
- package/src/2d/TextLabels.svelte +102 -72
- package/src/2d/TextLegend.svelte +17 -39
- package/src/2d/XAxis.svelte +49 -98
- package/src/2d/YAxis.svelte +47 -93
- package/src/2d/index.js +2 -1
- package/src/3d/Axes.svelte +306 -348
- package/src/3d/AxisLines.svelte +19 -37
- package/src/3d/AxisTickLabels.svelte +15 -32
- package/src/3d/Lines.svelte +59 -32
- package/src/3d/Mesh.svelte +106 -128
- package/src/3d/Points.svelte +56 -36
- package/src/3d/Segments.svelte +68 -52
- package/src/3d/TextLabels.svelte +63 -49
- package/src/3d/XAxis.svelte +36 -113
- package/src/3d/YAxis.svelte +36 -113
- package/src/3d/ZAxis.svelte +37 -114
- package/src/3d/index.js +2 -1
- package/src/constants.js +103 -0
- package/src/methods.js +1484 -0
- package/src/2d/Axis.svelte +0 -60
- package/src/3d/Axis.svelte +0 -64
- package/src/Colors.js +0 -51
- package/src/Utils.js +0 -626
package/src/Utils.js
DELETED
|
@@ -1,626 +0,0 @@
|
|
|
1
|
-
import { cbind, vector, isvector, Vector } from 'mdatools/arrays';
|
|
2
|
-
import { min, max, diff } from 'mdatools/stat';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates label for a tick factor.
|
|
7
|
-
*
|
|
8
|
-
* @param {Number} tf - tick factor value (negative or positive)
|
|
9
|
-
* @returns
|
|
10
|
-
*/
|
|
11
|
-
export function getTickFactorLabel(tf) {
|
|
12
|
-
if (tf === 0) return '';
|
|
13
|
-
return `<tspan>×10</tspan><tspan baseline-shift="super">${(tf).toString()}</tspan>`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Trim number leaving only significant decimals.
|
|
19
|
-
* @param {number} num
|
|
20
|
-
* @returns trimmed number
|
|
21
|
-
*/
|
|
22
|
-
export function trimNum(num) {
|
|
23
|
-
|
|
24
|
-
// check if number is whole — return as is
|
|
25
|
-
if (num === Math.round(num)) return num;
|
|
26
|
-
|
|
27
|
-
// Convert to string with precision to maintain significant digits
|
|
28
|
-
let str = num.toPrecision(12);
|
|
29
|
-
|
|
30
|
-
// Remove trailing zeros
|
|
31
|
-
str = str.replace(/\.?0+$/, '');
|
|
32
|
-
|
|
33
|
-
// Convert back to number
|
|
34
|
-
return parseFloat(str);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Generate vector with axis tick labels and tick factor based on numeric tick values.
|
|
39
|
-
*
|
|
40
|
-
* @param {Vector} ticks - vector with numeric tick values.
|
|
41
|
-
*
|
|
42
|
-
* @returns {Array} an array with tick factor and array with tick labels.
|
|
43
|
-
*/
|
|
44
|
-
export function getTickLabels(ticks) {
|
|
45
|
-
|
|
46
|
-
if (ticks === undefined) return undefined;
|
|
47
|
-
if (ticks.length === 1) return [0, [ticks[0].toString()]];
|
|
48
|
-
|
|
49
|
-
// compute and correct step between ticks
|
|
50
|
-
let step = trimNum(Math.abs(min(diff(ticks))));
|
|
51
|
-
let tickFactor = 0;
|
|
52
|
-
|
|
53
|
-
// step is large (over 1)
|
|
54
|
-
if (step >= 1 && ticks.length > 1) {
|
|
55
|
-
if (step < 100) return [0, Array.from(ticks).map(v => v.toFixed(0))];
|
|
56
|
-
let digNum = Math.ceil(trimNum(Math.log10(step)));
|
|
57
|
-
tickFactor = digNum - 2;
|
|
58
|
-
return [tickFactor, Array.from(ticks).map(v => (v / Math.pow(10, tickFactor)).toFixed(0))];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let decNum = Math.ceil(-trimNum(Math.log10(step)));
|
|
62
|
-
if (decNum <= 2) {
|
|
63
|
-
return [0, Array.from(ticks).map(v => v.toFixed(decNum))];
|
|
64
|
-
}
|
|
65
|
-
tickFactor = decNum - 2;
|
|
66
|
-
decNum = 2;
|
|
67
|
-
|
|
68
|
-
if (trimNum(Math.abs(max(ticks))) <= 10) {
|
|
69
|
-
return [-tickFactor, Array.from(ticks).map(v => (v * Math.pow(10, tickFactor)).toFixed(decNum))];
|
|
70
|
-
} else {
|
|
71
|
-
return [-tickFactor, Array.from(ticks).map(v => (v / Math.pow(10, tickFactor)).toString())];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Check coordinates and convert them to vector if necessary.
|
|
78
|
-
*
|
|
79
|
-
* @param {Array|Vector} x - vector or array with coordinates.
|
|
80
|
-
* @param {string} source - name of component which called the method (needed for error message).
|
|
81
|
-
*
|
|
82
|
-
* @returns {Vector}
|
|
83
|
-
*
|
|
84
|
-
*/
|
|
85
|
-
export function checkCoords(x, source) {
|
|
86
|
-
|
|
87
|
-
if (Array.isArray(x)) {
|
|
88
|
-
x = vector(x);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!isvector(x)) {
|
|
92
|
-
throw Error(source + ": coordinates must be provided as array or instance of Vector class.");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (x.length < 1) {
|
|
96
|
-
throw Error(source + ": vector with coordinates is empty.");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return x;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Create string with coordinates of SVG polygon in 3D.
|
|
105
|
-
*
|
|
106
|
-
* @param {Array|Vector} x - vector with x-coordinates of polygon points.
|
|
107
|
-
* @param {Array|Vector} y - vector with y-coordinates of polygon points.
|
|
108
|
-
* @param {Array|Vector} z - vector with y-coordinates of polygon points.
|
|
109
|
-
* @param {Array} tM - transformation matrix for 3D->2D (from 'Axes').
|
|
110
|
-
* @param {Object} axes - JSON with Axes context.
|
|
111
|
-
*
|
|
112
|
-
* @returns {string} string with coordinates.
|
|
113
|
-
*
|
|
114
|
-
*/
|
|
115
|
-
export function val2p3d(x, y, z, tM, axes) {
|
|
116
|
-
|
|
117
|
-
if (x === undefined || y === undefined) return undefined;
|
|
118
|
-
|
|
119
|
-
const [px, py] = axes.transform(cbind(checkCoords(x), checkCoords(y), checkCoords(z)), tM);
|
|
120
|
-
|
|
121
|
-
if (px.length !== py.length) {
|
|
122
|
-
throw Error('PlotSeries: parameters "xValues", "yValues" and "zValues" must be numeric vectors of the same length.');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let p = "";
|
|
126
|
-
for (let i = 0; i < px.length; i++) {
|
|
127
|
-
p += px[i] + "," + py[i] + " ";
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return p;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Create string with coordinates of SVG polygon.
|
|
136
|
-
*
|
|
137
|
-
* @param {Array|Vector} x - vector with x-coordinates of polygon points.
|
|
138
|
-
* @param {Array|Vector} y - vector with y-coordinates of polygon points.
|
|
139
|
-
* @param {Array} tX - transformation parameters for x-coordinates (from 'Axes').
|
|
140
|
-
* @param {Array} tY - transformation parameters for y-coordinates (from 'Axes').
|
|
141
|
-
* @param {Object} axes - JSON with Axes context.
|
|
142
|
-
*
|
|
143
|
-
* @returns {string} string with coordinates.
|
|
144
|
-
*
|
|
145
|
-
*/
|
|
146
|
-
export function val2p(x, y, tX, tY, axes) {
|
|
147
|
-
|
|
148
|
-
if (x === undefined || y === undefined) return undefined;
|
|
149
|
-
|
|
150
|
-
const px = axes.transform(checkCoords(x, 'val2p'), tX.coords);
|
|
151
|
-
const py = axes.transform(checkCoords(y, 'val2p'), tY.coords);
|
|
152
|
-
|
|
153
|
-
if (px.length != py.length) {
|
|
154
|
-
throw Error('PlotSeries: parameters "xValues" and "yValues" must be numeric vectors of the same length.');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let p = "";
|
|
158
|
-
for (let i = 0; i < px.length; i++) {
|
|
159
|
-
p += px[i] + "," + py[i] + " ";
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return p;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Computes nice tick values for axis.
|
|
167
|
-
*
|
|
168
|
-
* @param {Array} ticks - vector with ticks if alredy available (if not, new will be computed).
|
|
169
|
-
* @param {Array} lim - vector with axis limits tickets must be computed for.
|
|
170
|
-
* @param {number} maxTickNum - maximum number of ticks to compute.
|
|
171
|
-
* @param {boolean} round - round or not the fractions when computing nice numbers for the ticks.
|
|
172
|
-
* @param {boolean} whole - should the ticks be a whole number.
|
|
173
|
-
* @param {Number} deltaFactor - percent of range to use as internal margins
|
|
174
|
-
*
|
|
175
|
-
* @returns {Array} an array with computed tick positions.
|
|
176
|
-
*
|
|
177
|
-
*/
|
|
178
|
-
export function getAxisTicks(ticks, lim, maxTickNum, round, whole, deltaFactor) {
|
|
179
|
-
|
|
180
|
-
if (round === undefined || round === null) {
|
|
181
|
-
round = true;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (whole === undefined || whole === null) {
|
|
185
|
-
whole = false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (deltaFactor === undefined) {
|
|
189
|
-
deltaFactor = 0.001;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// if ticks are already provided do not recompute them
|
|
193
|
-
if (ticks !== undefined) {
|
|
194
|
-
if (Array.isArray(ticks)) {
|
|
195
|
-
ticks = vector(ticks);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (!isvector(ticks)) {
|
|
199
|
-
throw Error('getAxisTicks: axis ticks must be provided as an array or as a vector.');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return ticks.filter(x => x >= lim[0] & x <= lim[1]);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// check if limits are ok
|
|
206
|
-
if (typeof(lim) !== "object" || lim[0] === undefined || lim[1] === undefined) return undefined;
|
|
207
|
-
|
|
208
|
-
// get range as a nice number and compute min, max and steps for the tick sequence
|
|
209
|
-
const delta = (lim[1] - lim[0]) * deltaFactor;
|
|
210
|
-
const localRange = lim[1] - lim[0] - delta;
|
|
211
|
-
const exponent = Math.floor(Math.log10(localRange));
|
|
212
|
-
const fraction = 1 / Math.pow(10, exponent - 1);
|
|
213
|
-
const range = Math.round(localRange * fraction) / fraction;
|
|
214
|
-
|
|
215
|
-
let tickSpacing = niceNum(range / (maxTickNum - 1), round);
|
|
216
|
-
if (whole) {
|
|
217
|
-
if (tickSpacing < 1) tickSpacing = 1;
|
|
218
|
-
tickSpacing = Math.round(tickSpacing);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// compute smallest and largest tick rounded to tickSpacing
|
|
222
|
-
let tickMin = Math.ceil((lim[0] + delta ) / tickSpacing) * tickSpacing;
|
|
223
|
-
let tickMax = Math.floor((lim[1] - delta) / tickSpacing) * tickSpacing;
|
|
224
|
-
|
|
225
|
-
// adjust largest tick if it is too close to axis limit and tick spacing
|
|
226
|
-
// is small (so tick factor may appear)
|
|
227
|
-
if (tickSpacing < 0.1 && (lim[1] - tickMax) < (tickSpacing / 2)) tickMax = tickMax - tickSpacing;
|
|
228
|
-
|
|
229
|
-
// recompute maxTickNum
|
|
230
|
-
maxTickNum = (Math.ceil(trimNum((tickMax - tickMin) / tickSpacing)) + 1);
|
|
231
|
-
|
|
232
|
-
// create a sequence of ticks
|
|
233
|
-
ticks = Vector.zeros(maxTickNum);
|
|
234
|
-
for (let i = 0; i < maxTickNum; i++) {
|
|
235
|
-
ticks.v[i] = tickMin + tickSpacing * i;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// if step is smaller than 1 round values to remove small decimals accidentially added by JS
|
|
239
|
-
if (Math.abs(tickSpacing) < 1) {
|
|
240
|
-
const decNum = Math.round(-Math.log10(tickSpacing));
|
|
241
|
-
const r = Math.pow(10, 1 + Math.round(-Math.log10(tickSpacing)));
|
|
242
|
-
ticks = ticks.apply(v => (Math.round((v + Number.EPSILON) * r) / r));
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// make sure the ticks are not aligned with axes limits
|
|
246
|
-
return ticks.filter(x => x >= lim[0] & x <= lim[1]);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Computes a nice spacing value for a given range.
|
|
252
|
-
*
|
|
253
|
-
* @param {Number} localRange - a range (max - min).
|
|
254
|
-
* @param {boolean} round - round or not the fractions when computing the number.
|
|
255
|
-
*
|
|
256
|
-
* @returns {Number} the computed spacing value.
|
|
257
|
-
*
|
|
258
|
-
*/
|
|
259
|
-
export function niceNum(localRange, round) {
|
|
260
|
-
|
|
261
|
-
const exponent = Math.floor(Math.log10(localRange));
|
|
262
|
-
const fraction = localRange / Math.pow(10, exponent);
|
|
263
|
-
let niceFraction;
|
|
264
|
-
|
|
265
|
-
if (round) {
|
|
266
|
-
if (fraction < 1.5)
|
|
267
|
-
niceFraction = 1;
|
|
268
|
-
else if (fraction < 3)
|
|
269
|
-
niceFraction = 2;
|
|
270
|
-
else if (fraction < 7)
|
|
271
|
-
niceFraction = 5;
|
|
272
|
-
else
|
|
273
|
-
niceFraction = 10;
|
|
274
|
-
} else {
|
|
275
|
-
if (fraction <= 1)
|
|
276
|
-
niceFraction = 1;
|
|
277
|
-
else if (fraction <= 2)
|
|
278
|
-
niceFraction = 2;
|
|
279
|
-
else if (fraction <= 5)
|
|
280
|
-
niceFraction = 5;
|
|
281
|
-
else
|
|
282
|
-
niceFraction = 10;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return niceFraction * Math.pow(10, exponent);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Rounds coordinate values to single decimal
|
|
291
|
-
*/
|
|
292
|
-
export function roundCoords(x) {
|
|
293
|
-
return Math.round(x * 10) / 10
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Computes a scale level.
|
|
299
|
-
*
|
|
300
|
-
* @param {numeric} width - width of plotting area in pixels.
|
|
301
|
-
* @param {numeric} height - height of plotting area in pixels.
|
|
302
|
-
*
|
|
303
|
-
* @returns {text} the scale level ("small", "medium" or "large").
|
|
304
|
-
*
|
|
305
|
-
*/
|
|
306
|
-
export function getScale(width, height) {
|
|
307
|
-
if (height < 300.2 || width < 400.2) return "small";
|
|
308
|
-
if (height < 400.2 || width < 700.2) return "medium";
|
|
309
|
-
if (height < 650.2 || width < 850.2) return "large";
|
|
310
|
-
return "xlarge";
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
/*************************************************************/
|
|
315
|
-
/* Functions needed to adjust DPI of resulting PNG image */
|
|
316
|
-
/* taken from: https://github.com/shutterstock/changeDPI */
|
|
317
|
-
/* and modified */
|
|
318
|
-
/*************************************************************/
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Create table for PNG CRC calculation
|
|
322
|
-
*/
|
|
323
|
-
function createPngDataTable() {
|
|
324
|
-
const crcTable = new Int32Array(256);
|
|
325
|
-
for (let n = 0; n < 256; n++) {
|
|
326
|
-
let c = n;
|
|
327
|
-
for (let k = 0; k < 8; k++) {
|
|
328
|
-
c = (c & 1) ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
329
|
-
}
|
|
330
|
-
crcTable[n] = c;
|
|
331
|
-
}
|
|
332
|
-
return crcTable;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Calculate CRC for PNG image
|
|
337
|
-
* @param buf image data
|
|
338
|
-
*/
|
|
339
|
-
function calcCrc(buf) {
|
|
340
|
-
let c = -1;
|
|
341
|
-
let pngDataTable = createPngDataTable();
|
|
342
|
-
for (let n = 0; n < buf.length; n++) {
|
|
343
|
-
c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ (c >>> 8);
|
|
344
|
-
}
|
|
345
|
-
return c ^ -1;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Change DPI of PNG image
|
|
350
|
-
* @param dataArray image data
|
|
351
|
-
* @param dpi new DPI
|
|
352
|
-
*/
|
|
353
|
-
function changeDpiOnArray(dataArray, dpi) {
|
|
354
|
-
|
|
355
|
-
const physChunk = new Uint8Array(13);
|
|
356
|
-
// chunk header pHYs
|
|
357
|
-
// 9 bytes of data
|
|
358
|
-
// 4 bytes of crc
|
|
359
|
-
// this multiplication is because the standard is dpi per meter.
|
|
360
|
-
dpi *= 39.3701;
|
|
361
|
-
physChunk[0] = 'p'.charCodeAt(0);
|
|
362
|
-
physChunk[1] = 'H'.charCodeAt(0);
|
|
363
|
-
physChunk[2] = 'Y'.charCodeAt(0);
|
|
364
|
-
physChunk[3] = 's'.charCodeAt(0);
|
|
365
|
-
physChunk[4] = dpi >>> 24; // dpiX byte 3
|
|
366
|
-
physChunk[5] = dpi >>> 16; // dpiX byte 2
|
|
367
|
-
physChunk[6] = dpi >>> 8; // dpiX byte 1
|
|
368
|
-
physChunk[7] = dpi & 0xff; // dpiX byte 0
|
|
369
|
-
physChunk[8] = physChunk[4]; // dpiY byte 3
|
|
370
|
-
physChunk[9] = physChunk[5]; // dpiY byte 2
|
|
371
|
-
physChunk[10] = physChunk[6]; // dpiY byte 1
|
|
372
|
-
physChunk[11] = physChunk[7]; // dpiY byte 0
|
|
373
|
-
physChunk[12] = 1; // dot per meter....
|
|
374
|
-
|
|
375
|
-
// compute and add CRC for physChunk
|
|
376
|
-
const crc = calcCrc(physChunk);
|
|
377
|
-
const crcChunk = new Uint8Array(4);
|
|
378
|
-
crcChunk[0] = crc >>> 24;
|
|
379
|
-
crcChunk[1] = crc >>> 16;
|
|
380
|
-
crcChunk[2] = crc >>> 8;
|
|
381
|
-
crcChunk[3] = crc & 0xff;
|
|
382
|
-
|
|
383
|
-
// chunk structur 4 bytes for length is 9
|
|
384
|
-
const chunkLength = new Uint8Array(4);
|
|
385
|
-
chunkLength[0] = 0;
|
|
386
|
-
chunkLength[1] = 0;
|
|
387
|
-
chunkLength[2] = 0;
|
|
388
|
-
chunkLength[3] = 9;
|
|
389
|
-
|
|
390
|
-
const finalHeader = new Uint8Array(54);
|
|
391
|
-
finalHeader.set(dataArray, 0);
|
|
392
|
-
finalHeader.set(chunkLength, 33);
|
|
393
|
-
finalHeader.set(physChunk, 37);
|
|
394
|
-
finalHeader.set(crcChunk, 50);
|
|
395
|
-
return finalHeader;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Change DPI of PNG image
|
|
400
|
-
* @param base64Image image data
|
|
401
|
-
* @param dpi new DPI
|
|
402
|
-
*/
|
|
403
|
-
function changeDPI(base64Image, dpi) {
|
|
404
|
-
|
|
405
|
-
const dataSplitted = base64Image.split(',');
|
|
406
|
-
const format = dataSplitted[0];
|
|
407
|
-
const body = dataSplitted[1];
|
|
408
|
-
|
|
409
|
-
// here we assume there is pHYS chunk in the header
|
|
410
|
-
const headerLength = 33 / 3 * 4;
|
|
411
|
-
|
|
412
|
-
const stringHeader = body.substring(0, headerLength);
|
|
413
|
-
const restOfData = body.substring(headerLength);
|
|
414
|
-
const headerBytes = atob(stringHeader);
|
|
415
|
-
const dataArray = new Uint8Array(headerBytes.length);
|
|
416
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
417
|
-
dataArray[i] = headerBytes.charCodeAt(i);
|
|
418
|
-
}
|
|
419
|
-
const finalArray = changeDpiOnArray(dataArray, dpi, 'image/png');
|
|
420
|
-
const base64Header = btoa(String.fromCharCode(...finalArray));
|
|
421
|
-
return [format, ',', base64Header, restOfData].join('');
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Downloads plot (or any other SVG image) as a PNG file.
|
|
427
|
-
*
|
|
428
|
-
* @param {SVGElement} svg - root SVG element of a plot
|
|
429
|
-
* @param {string} fileName - filename to save with (without extension)
|
|
430
|
-
* @param {number} width - desired image width in cm
|
|
431
|
-
* @param {number} height - desired image height in cm
|
|
432
|
-
* @param {number} res - resolution (pixels per inch)
|
|
433
|
-
*
|
|
434
|
-
*/
|
|
435
|
-
export function downloadPNG (svg, fileName, width, height, res) {
|
|
436
|
-
|
|
437
|
-
if (!width) {
|
|
438
|
-
width = 10;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (!height) {
|
|
442
|
-
height = 10;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (!res) {
|
|
446
|
-
res = 300;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (width < 1 || width > 30) {
|
|
450
|
-
throw Error('Parameter "width" must be between 1 and 30 (cm).');
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (height < 1 || height > 30) {
|
|
454
|
-
throw Error('Parameter "height" must be between 1 and 30 (cm).');
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (res < 50 || res > 1200) {
|
|
458
|
-
throw Error('Parameter "res" must be between 50 and 1200 (ppi).');
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (!svg || !(svg instanceof SVGElement)) {
|
|
462
|
-
throw Error('Parameter "svg" is not an instance of SVGElement.');
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (!fileName || fileName.trim() === '') {
|
|
466
|
-
fileName = 'plot';
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// recalculate width and height to pixels
|
|
470
|
-
width = width / 2.54 * res;
|
|
471
|
-
height = height / 2.54 * res;
|
|
472
|
-
|
|
473
|
-
setTimeout(() => {
|
|
474
|
-
|
|
475
|
-
// compute canvas size based on SVG size and desired PNG WIDTH in pixels
|
|
476
|
-
const svgHeight = svg.clientHeight;
|
|
477
|
-
const svgWidth = svg.clientWidth;
|
|
478
|
-
const scaleFactor = Math.max(width / svgWidth, height / svgHeight);
|
|
479
|
-
|
|
480
|
-
// create a new canvas element
|
|
481
|
-
const canvas = document.createElement('canvas');
|
|
482
|
-
const context = canvas.getContext('2d');
|
|
483
|
-
|
|
484
|
-
// set the canvas size to match the SVG element
|
|
485
|
-
canvas.width = svgWidth * scaleFactor;
|
|
486
|
-
canvas.height = svgHeight * scaleFactor;
|
|
487
|
-
|
|
488
|
-
// set corresponding attributes for SVG element
|
|
489
|
-
svg.setAttribute('width', canvas.width);
|
|
490
|
-
svg.setAttribute('height', canvas.height);
|
|
491
|
-
|
|
492
|
-
// draw a white background
|
|
493
|
-
context.fillStyle = 'white';
|
|
494
|
-
context.scale(scaleFactor, scaleFactor)
|
|
495
|
-
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
496
|
-
|
|
497
|
-
// draw the SVG onto the canvas
|
|
498
|
-
const svgXml = new XMLSerializer().serializeToString(svg);
|
|
499
|
-
const blob = new Blob([svgXml], { type: 'image/svg+xml;charset=utf-8' });
|
|
500
|
-
const url = URL.createObjectURL(blob);
|
|
501
|
-
|
|
502
|
-
const img = new Image();
|
|
503
|
-
|
|
504
|
-
img.onload = function () {
|
|
505
|
-
|
|
506
|
-
// draw the image
|
|
507
|
-
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
508
|
-
|
|
509
|
-
// create a data URL for the canvas and adjust its DPI
|
|
510
|
-
const dataURL = canvas.toDataURL('image/png');
|
|
511
|
-
const dataURL2 = changeDPI(dataURL, res);
|
|
512
|
-
|
|
513
|
-
// create a temporary link and trigger the download
|
|
514
|
-
const downloadLink = document.createElement('a');
|
|
515
|
-
downloadLink.href = dataURL2;
|
|
516
|
-
downloadLink.download = `${fileName}.png`;
|
|
517
|
-
downloadLink.click();
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
// set the image source
|
|
521
|
-
img.src = url;
|
|
522
|
-
}, 20);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Downloads plot (or any other SVG image) as an SVG file.
|
|
528
|
-
*
|
|
529
|
-
* @param {SVGElement} svg - root SVG element of a plot
|
|
530
|
-
* @param {string} fileName - filename to save with (without extension)
|
|
531
|
-
*/
|
|
532
|
-
export function downloadSVG(svg, fileName) {
|
|
533
|
-
|
|
534
|
-
if (!svg || !(svg instanceof SVGElement)) {
|
|
535
|
-
throw Error('Variable "svg" is not an instance of SVGElement.');
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (!fileName || fileName.trim() === "") {
|
|
539
|
-
fileName = "plot";
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const svgHeight = svg.clientHeight;
|
|
543
|
-
const svgWidth = svg.clientWidth;
|
|
544
|
-
svg.setAttribute('width', svgWidth + 'px');
|
|
545
|
-
svg.setAttribute('height', svgHeight + 'px');
|
|
546
|
-
svg.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);
|
|
547
|
-
|
|
548
|
-
const blob = new Blob([svg.outerHTML], { type: 'image/svg+xml' });
|
|
549
|
-
const url = window.URL.createObjectURL(blob)
|
|
550
|
-
const a = document.createElement('a')
|
|
551
|
-
a.setAttribute('href', url)
|
|
552
|
-
a.setAttribute('download', `${fileName}.svg`);
|
|
553
|
-
a.click()
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Returns plot BLOB to copy it to clipboard.
|
|
559
|
-
*
|
|
560
|
-
* @param {SVGElement} svg - root SVG element of a plot
|
|
561
|
-
* @param {number} width - desired image width in pixels
|
|
562
|
-
* @param {number} height - desired image height in pixels
|
|
563
|
-
*
|
|
564
|
-
*/
|
|
565
|
-
export async function createPngBlob(svg, width, height) {
|
|
566
|
-
|
|
567
|
-
// compute canvas size based on SVG size and scaling factor
|
|
568
|
-
// based on desired size in pixels
|
|
569
|
-
const svgHeight = svg.clientHeight;
|
|
570
|
-
const svgWidth = svg.clientWidth;
|
|
571
|
-
const scaleFactor = Math.max(width / svgWidth, height / svgHeight);
|
|
572
|
-
|
|
573
|
-
// create a new canvas element and 2D context for it
|
|
574
|
-
const canvas = document.createElement('canvas');
|
|
575
|
-
const context = canvas.getContext('2d');
|
|
576
|
-
|
|
577
|
-
// set the canvas size to match the size of SVG element
|
|
578
|
-
canvas.width = svgWidth * scaleFactor;
|
|
579
|
-
canvas.height = svgHeight * scaleFactor;
|
|
580
|
-
|
|
581
|
-
// scale the context
|
|
582
|
-
context.scale(scaleFactor, scaleFactor)
|
|
583
|
-
|
|
584
|
-
// set corresponding attributes for SVG element
|
|
585
|
-
svg.setAttribute('width', canvas.width);
|
|
586
|
-
svg.setAttribute('height', canvas.height);
|
|
587
|
-
|
|
588
|
-
// create promise
|
|
589
|
-
const promise = new Promise((resolve, reject) => {
|
|
590
|
-
|
|
591
|
-
// serialzie SVG, create blob and URL for it
|
|
592
|
-
const svgData = new XMLSerializer().serializeToString(svg);
|
|
593
|
-
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
|
594
|
-
const DOMURL = window.URL || window.webkitURL || window;
|
|
595
|
-
const url = DOMURL.createObjectURL(svgBlob);
|
|
596
|
-
|
|
597
|
-
// create image an image and add callback
|
|
598
|
-
const img = new Image();
|
|
599
|
-
img.onload = function () {
|
|
600
|
-
|
|
601
|
-
// draw the image
|
|
602
|
-
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
603
|
-
DOMURL.revokeObjectURL(url);
|
|
604
|
-
|
|
605
|
-
// convert canvas to blob
|
|
606
|
-
canvas.toBlob(function (blob) {
|
|
607
|
-
if (blob) {
|
|
608
|
-
resolve(blob);
|
|
609
|
-
} else {
|
|
610
|
-
reject(new Error('Failed to create blob'));
|
|
611
|
-
}
|
|
612
|
-
}, 'image/png');
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
// add error callback
|
|
616
|
-
img.onerror = function (err) {
|
|
617
|
-
reject(err);
|
|
618
|
-
};
|
|
619
|
-
|
|
620
|
-
// assign value to run the onload callback
|
|
621
|
-
img.src = url;
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
// resolve promise and return the result
|
|
625
|
-
return (await promise);
|
|
626
|
-
}
|