svelte-plots-basic 2.3.6 → 2.4.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 +4 -0
- package/package.json +1 -1
- package/src/2d/Axes.svelte +56 -10
- package/src/Utils.js +174 -102
package/README.md
CHANGED
package/package.json
CHANGED
package/src/2d/Axes.svelte
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { setContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
|
|
9
9
|
import { writable } from 'svelte/store';
|
|
10
10
|
import { isvector, vector, Vector } from 'mdatools/arrays';
|
|
11
|
-
import { roundCoords, getScale, downloadSVG, downloadPNG } from '../Utils.js';
|
|
11
|
+
import { roundCoords, getScale, downloadSVG, downloadPNG, createPngBlob } from '../Utils.js';
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
/*****************************************/
|
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
export let margins = [1.0, 0.75, 0.5, 0.5]; // margins [bottom, left, top, right] )
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
/* Input parameters for downloading
|
|
28
|
-
|
|
29
|
-
export let downloadLinks = '
|
|
26
|
+
/***********************************************/
|
|
27
|
+
/* Input parameters for downloading and copy */
|
|
28
|
+
/***********************************************/
|
|
29
|
+
export let downloadLinks = 'none'; // how to show download links panel ('none', 'hover', 'fixed')
|
|
30
30
|
export let fileName = 'plot'; // file name for download (without extension)
|
|
31
31
|
export let pngWidth = 8; // width of PNG image in cm
|
|
32
32
|
export let pngHeight = 8; // height of PNG image in cm
|
|
33
33
|
export let pngRes = 300; // resolution of PNG image (pixels per inch)
|
|
34
|
+
export let clipboardWidth = 1200; // width of PNG image to copy to clipboard in pixels
|
|
35
|
+
export let clipboardHeight = 800; // height of PNG image to copy to clipboard in pixels
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
/*****************************************/
|
|
@@ -129,7 +131,7 @@
|
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
/**
|
|
132
|
-
* Handler (router) for click events.
|
|
134
|
+
* Handler (router) for axes or series click events.
|
|
133
135
|
*
|
|
134
136
|
* @param {event} e - event object.
|
|
135
137
|
*
|
|
@@ -155,12 +157,52 @@
|
|
|
155
157
|
dispatch('axesclick');
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handler (router) for click event on copy plot button.
|
|
163
|
+
*
|
|
164
|
+
* @description
|
|
165
|
+
* Copy plot to clipboard as PNG image.
|
|
166
|
+
*
|
|
167
|
+
*/
|
|
168
|
+
async function handleClickCopy(e) {
|
|
169
|
+
navigator.clipboard.write([
|
|
170
|
+
new ClipboardItem({ 'image/png': createPngBlob(plotElement, clipboardWidth, clipboardHeight) })
|
|
171
|
+
]).catch(function (error) { console.error(error); });
|
|
172
|
+
|
|
173
|
+
const content = e.target.textContent;
|
|
174
|
+
e.target.classList.add('copied');
|
|
175
|
+
e.target.textContent = '✓ done';
|
|
176
|
+
|
|
177
|
+
// Remove the class after 1 second and revert the text
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
e.target.classList.remove('copied');
|
|
180
|
+
e.target.textContent = content;
|
|
181
|
+
}, 500);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Handler (router) for click event to download SVG image.
|
|
187
|
+
*
|
|
188
|
+
* @description
|
|
189
|
+
* Copy plot to clipboard as SVG image.
|
|
190
|
+
*
|
|
191
|
+
*/
|
|
158
192
|
function handleClickSVG() {
|
|
159
|
-
downloadSVG(plotElement, fileName)
|
|
193
|
+
downloadSVG(plotElement, fileName);
|
|
160
194
|
}
|
|
161
195
|
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handler (router) for click event to download PNG image.
|
|
199
|
+
*
|
|
200
|
+
* @description
|
|
201
|
+
* Copy plot to clipboard as PNG image.
|
|
202
|
+
*
|
|
203
|
+
*/
|
|
162
204
|
function handleClickPNG() {
|
|
163
|
-
downloadPNG(plotElement, fileName, pngWidth, pngHeight, pngRes)
|
|
205
|
+
downloadPNG(plotElement, fileName, pngWidth, pngHeight, pngRes);
|
|
164
206
|
}
|
|
165
207
|
|
|
166
208
|
/**
|
|
@@ -413,6 +455,7 @@
|
|
|
413
455
|
<div class="download-links">
|
|
414
456
|
<button on:click={handleClickSVG} on:keydown={handleClickSVG}>⇩ svg</button>
|
|
415
457
|
<button on:click={handleClickPNG} on:keydown={handleClickPNG}>⇩ png</button>
|
|
458
|
+
<button on:click={handleClickCopy} on:keydown={handleClickCopy}>⧉ copy</button>
|
|
416
459
|
</div>
|
|
417
460
|
{/if}
|
|
418
461
|
|
|
@@ -432,6 +475,7 @@
|
|
|
432
475
|
}
|
|
433
476
|
|
|
434
477
|
.download-links {
|
|
478
|
+
box-sizing: border-box;
|
|
435
479
|
bottom: right 0.5s ease;
|
|
436
480
|
position: absolute;
|
|
437
481
|
font-size: 0.8em;
|
|
@@ -442,8 +486,8 @@
|
|
|
442
486
|
background: #fefefe;
|
|
443
487
|
border-radius: 0.5em;
|
|
444
488
|
transition: bottom 0.35s ease;
|
|
445
|
-
box-shadow: 0 0 1em #
|
|
446
|
-
margin: 0.
|
|
489
|
+
box-shadow: 0 0 1em #00000060;
|
|
490
|
+
margin: 0.6em;
|
|
447
491
|
box-sizing: border-box;
|
|
448
492
|
|
|
449
493
|
display: flex;
|
|
@@ -452,6 +496,7 @@
|
|
|
452
496
|
}
|
|
453
497
|
|
|
454
498
|
.download-links > button{
|
|
499
|
+
box-sizing: border-box;
|
|
455
500
|
color: #606060;
|
|
456
501
|
border: none;
|
|
457
502
|
box-shadow: none;
|
|
@@ -465,6 +510,7 @@
|
|
|
465
510
|
color: #fafafa;
|
|
466
511
|
}
|
|
467
512
|
|
|
513
|
+
|
|
468
514
|
/* Plot container */
|
|
469
515
|
.plot-container {
|
|
470
516
|
font-family: Arial, Helvetica, sans-serif;
|
package/src/Utils.js
CHANGED
|
@@ -243,115 +243,115 @@ export function getScale(width, height) {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Create table for PNG CRC calculation
|
|
254
|
-
*/
|
|
255
|
-
function createPngDataTable() {
|
|
256
|
-
const crcTable = new Int32Array(256);
|
|
257
|
-
for (let n = 0; n < 256; n++) {
|
|
258
|
-
let c = n;
|
|
259
|
-
for (let k = 0; k < 8; k++) {
|
|
260
|
-
c = (c & 1) ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
261
|
-
}
|
|
262
|
-
crcTable[n] = c;
|
|
263
|
-
}
|
|
264
|
-
return crcTable;
|
|
265
|
-
}
|
|
246
|
+
/*************************************************************/
|
|
247
|
+
/* Functions needed to adjust DPI of resulting PNG image */
|
|
248
|
+
/* taken from: https://github.com/shutterstock/changeDPI */
|
|
249
|
+
/* and modified */
|
|
250
|
+
/*************************************************************/
|
|
266
251
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let
|
|
274
|
-
for (let
|
|
275
|
-
c =
|
|
252
|
+
/**
|
|
253
|
+
* Create table for PNG CRC calculation
|
|
254
|
+
*/
|
|
255
|
+
function createPngDataTable() {
|
|
256
|
+
const crcTable = new Int32Array(256);
|
|
257
|
+
for (let n = 0; n < 256; n++) {
|
|
258
|
+
let c = n;
|
|
259
|
+
for (let k = 0; k < 8; k++) {
|
|
260
|
+
c = (c & 1) ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
276
261
|
}
|
|
277
|
-
|
|
262
|
+
crcTable[n] = c;
|
|
278
263
|
}
|
|
264
|
+
return crcTable;
|
|
265
|
+
}
|
|
279
266
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// 9 bytes of data
|
|
290
|
-
// 4 bytes of crc
|
|
291
|
-
// this multiplication is because the standard is dpi per meter.
|
|
292
|
-
dpi *= 39.3701;
|
|
293
|
-
physChunk[0] = 'p'.charCodeAt(0);
|
|
294
|
-
physChunk[1] = 'H'.charCodeAt(0);
|
|
295
|
-
physChunk[2] = 'Y'.charCodeAt(0);
|
|
296
|
-
physChunk[3] = 's'.charCodeAt(0);
|
|
297
|
-
physChunk[4] = dpi >>> 24; // dpiX byte 3
|
|
298
|
-
physChunk[5] = dpi >>> 16; // dpiX byte 2
|
|
299
|
-
physChunk[6] = dpi >>> 8; // dpiX byte 1
|
|
300
|
-
physChunk[7] = dpi & 0xff; // dpiX byte 0
|
|
301
|
-
physChunk[8] = physChunk[4]; // dpiY byte 3
|
|
302
|
-
physChunk[9] = physChunk[5]; // dpiY byte 2
|
|
303
|
-
physChunk[10] = physChunk[6]; // dpiY byte 1
|
|
304
|
-
physChunk[11] = physChunk[7]; // dpiY byte 0
|
|
305
|
-
physChunk[12] = 1; // dot per meter....
|
|
306
|
-
|
|
307
|
-
// compute and add CRC for physChunk
|
|
308
|
-
const crc = calcCrc(physChunk);
|
|
309
|
-
const crcChunk = new Uint8Array(4);
|
|
310
|
-
crcChunk[0] = crc >>> 24;
|
|
311
|
-
crcChunk[1] = crc >>> 16;
|
|
312
|
-
crcChunk[2] = crc >>> 8;
|
|
313
|
-
crcChunk[3] = crc & 0xff;
|
|
314
|
-
|
|
315
|
-
// chunk structur 4 bytes for length is 9
|
|
316
|
-
const chunkLength = new Uint8Array(4);
|
|
317
|
-
chunkLength[0] = 0;
|
|
318
|
-
chunkLength[1] = 0;
|
|
319
|
-
chunkLength[2] = 0;
|
|
320
|
-
chunkLength[3] = 9;
|
|
321
|
-
|
|
322
|
-
const finalHeader = new Uint8Array(54);
|
|
323
|
-
finalHeader.set(dataArray, 0);
|
|
324
|
-
finalHeader.set(chunkLength, 33);
|
|
325
|
-
finalHeader.set(physChunk, 37);
|
|
326
|
-
finalHeader.set(crcChunk, 50);
|
|
327
|
-
return finalHeader;
|
|
267
|
+
/**
|
|
268
|
+
* Calculate CRC for PNG image
|
|
269
|
+
* @param buf image data
|
|
270
|
+
*/
|
|
271
|
+
function calcCrc(buf) {
|
|
272
|
+
let c = -1;
|
|
273
|
+
let pngDataTable = createPngDataTable();
|
|
274
|
+
for (let n = 0; n < buf.length; n++) {
|
|
275
|
+
c = pngDataTable[(c ^ buf[n]) & 0xFF] ^ (c >>> 8);
|
|
328
276
|
}
|
|
277
|
+
return c ^ -1;
|
|
278
|
+
}
|
|
329
279
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Change DPI of PNG image
|
|
282
|
+
* @param dataArray image data
|
|
283
|
+
* @param dpi new DPI
|
|
284
|
+
*/
|
|
285
|
+
function changeDpiOnArray(dataArray, dpi) {
|
|
286
|
+
|
|
287
|
+
const physChunk = new Uint8Array(13);
|
|
288
|
+
// chunk header pHYs
|
|
289
|
+
// 9 bytes of data
|
|
290
|
+
// 4 bytes of crc
|
|
291
|
+
// this multiplication is because the standard is dpi per meter.
|
|
292
|
+
dpi *= 39.3701;
|
|
293
|
+
physChunk[0] = 'p'.charCodeAt(0);
|
|
294
|
+
physChunk[1] = 'H'.charCodeAt(0);
|
|
295
|
+
physChunk[2] = 'Y'.charCodeAt(0);
|
|
296
|
+
physChunk[3] = 's'.charCodeAt(0);
|
|
297
|
+
physChunk[4] = dpi >>> 24; // dpiX byte 3
|
|
298
|
+
physChunk[5] = dpi >>> 16; // dpiX byte 2
|
|
299
|
+
physChunk[6] = dpi >>> 8; // dpiX byte 1
|
|
300
|
+
physChunk[7] = dpi & 0xff; // dpiX byte 0
|
|
301
|
+
physChunk[8] = physChunk[4]; // dpiY byte 3
|
|
302
|
+
physChunk[9] = physChunk[5]; // dpiY byte 2
|
|
303
|
+
physChunk[10] = physChunk[6]; // dpiY byte 1
|
|
304
|
+
physChunk[11] = physChunk[7]; // dpiY byte 0
|
|
305
|
+
physChunk[12] = 1; // dot per meter....
|
|
306
|
+
|
|
307
|
+
// compute and add CRC for physChunk
|
|
308
|
+
const crc = calcCrc(physChunk);
|
|
309
|
+
const crcChunk = new Uint8Array(4);
|
|
310
|
+
crcChunk[0] = crc >>> 24;
|
|
311
|
+
crcChunk[1] = crc >>> 16;
|
|
312
|
+
crcChunk[2] = crc >>> 8;
|
|
313
|
+
crcChunk[3] = crc & 0xff;
|
|
314
|
+
|
|
315
|
+
// chunk structur 4 bytes for length is 9
|
|
316
|
+
const chunkLength = new Uint8Array(4);
|
|
317
|
+
chunkLength[0] = 0;
|
|
318
|
+
chunkLength[1] = 0;
|
|
319
|
+
chunkLength[2] = 0;
|
|
320
|
+
chunkLength[3] = 9;
|
|
321
|
+
|
|
322
|
+
const finalHeader = new Uint8Array(54);
|
|
323
|
+
finalHeader.set(dataArray, 0);
|
|
324
|
+
finalHeader.set(chunkLength, 33);
|
|
325
|
+
finalHeader.set(physChunk, 37);
|
|
326
|
+
finalHeader.set(crcChunk, 50);
|
|
327
|
+
return finalHeader;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Change DPI of PNG image
|
|
332
|
+
* @param base64Image image data
|
|
333
|
+
* @param dpi new DPI
|
|
334
|
+
*/
|
|
335
|
+
function changeDPI(base64Image, dpi) {
|
|
336
|
+
|
|
337
|
+
const dataSplitted = base64Image.split(',');
|
|
338
|
+
const format = dataSplitted[0];
|
|
339
|
+
const body = dataSplitted[1];
|
|
340
|
+
|
|
341
|
+
// here we assume there is pHYS chunk in the header
|
|
342
|
+
const headerLength = 33 / 3 * 4;
|
|
343
|
+
|
|
344
|
+
const stringHeader = body.substring(0, headerLength);
|
|
345
|
+
const restOfData = body.substring(headerLength);
|
|
346
|
+
const headerBytes = atob(stringHeader);
|
|
347
|
+
const dataArray = new Uint8Array(headerBytes.length);
|
|
348
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
349
|
+
dataArray[i] = headerBytes.charCodeAt(i);
|
|
354
350
|
}
|
|
351
|
+
const finalArray = changeDpiOnArray(dataArray, dpi, 'image/png');
|
|
352
|
+
const base64Header = btoa(String.fromCharCode(...finalArray));
|
|
353
|
+
return [format, ',', base64Header, restOfData].join('');
|
|
354
|
+
}
|
|
355
355
|
|
|
356
356
|
|
|
357
357
|
/**
|
|
@@ -417,6 +417,7 @@ export function downloadPNG (svg, fileName, width, height, res) {
|
|
|
417
417
|
canvas.width = svgWidth * scaleFactor;
|
|
418
418
|
canvas.height = svgHeight * scaleFactor;
|
|
419
419
|
|
|
420
|
+
// set corresponding attributes for SVG element
|
|
420
421
|
svg.setAttribute('width', canvas.width);
|
|
421
422
|
svg.setAttribute('height', canvas.height);
|
|
422
423
|
|
|
@@ -484,3 +485,74 @@ export function downloadSVG(svg, fileName) {
|
|
|
484
485
|
a.click()
|
|
485
486
|
}
|
|
486
487
|
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Returns plot BLOB to copy it to clipboard.
|
|
491
|
+
*
|
|
492
|
+
* @param {SVGElement} svg - root SVG element of a plot
|
|
493
|
+
* @param {number} width - desired image width in pixels
|
|
494
|
+
* @param {number} height - desired image height in pixels
|
|
495
|
+
*
|
|
496
|
+
*/
|
|
497
|
+
export async function createPngBlob(svg, width, height) {
|
|
498
|
+
|
|
499
|
+
// compute canvas size based on SVG size and scaling factor
|
|
500
|
+
// based on desired size in pixels
|
|
501
|
+
const svgHeight = svg.clientHeight;
|
|
502
|
+
const svgWidth = svg.clientWidth;
|
|
503
|
+
const scaleFactor = Math.max(width / svgWidth, height / svgHeight);
|
|
504
|
+
|
|
505
|
+
// create a new canvas element and 2D context for it
|
|
506
|
+
const canvas = document.createElement('canvas');
|
|
507
|
+
const context = canvas.getContext('2d');
|
|
508
|
+
|
|
509
|
+
// set the canvas size to match the size of SVG element
|
|
510
|
+
canvas.width = svgWidth * scaleFactor;
|
|
511
|
+
canvas.height = svgHeight * scaleFactor;
|
|
512
|
+
|
|
513
|
+
// scale the context
|
|
514
|
+
context.scale(scaleFactor, scaleFactor)
|
|
515
|
+
|
|
516
|
+
// set corresponding attributes for SVG element
|
|
517
|
+
svg.setAttribute('width', canvas.width);
|
|
518
|
+
svg.setAttribute('height', canvas.height);
|
|
519
|
+
|
|
520
|
+
// create promise
|
|
521
|
+
const promise = new Promise((resolve, reject) => {
|
|
522
|
+
|
|
523
|
+
// serialzie SVG, create blob and URL for it
|
|
524
|
+
const svgData = new XMLSerializer().serializeToString(svg);
|
|
525
|
+
const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
|
|
526
|
+
const DOMURL = window.URL || window.webkitURL || window;
|
|
527
|
+
const url = DOMURL.createObjectURL(svgBlob);
|
|
528
|
+
|
|
529
|
+
// create image an image and add callback
|
|
530
|
+
const img = new Image();
|
|
531
|
+
img.onload = function () {
|
|
532
|
+
|
|
533
|
+
// draw the image
|
|
534
|
+
context.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
535
|
+
DOMURL.revokeObjectURL(url);
|
|
536
|
+
|
|
537
|
+
// convert canvas to blob
|
|
538
|
+
canvas.toBlob(function (blob) {
|
|
539
|
+
if (blob) {
|
|
540
|
+
resolve(blob);
|
|
541
|
+
} else {
|
|
542
|
+
reject(new Error('Failed to create blob'));
|
|
543
|
+
}
|
|
544
|
+
}, 'image/png');
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// add error callback
|
|
548
|
+
img.onerror = function (err) {
|
|
549
|
+
reject(err);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// assign value to run the onload callback
|
|
553
|
+
img.src = url;
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// resolve promise and return the result
|
|
557
|
+
return (await promise);
|
|
558
|
+
}
|