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 CHANGED
@@ -11,6 +11,10 @@ These websites and web-applications use `svelte-plots-basic` library:
11
11
 
12
12
  ## News
13
13
 
14
+ ### 2.4.0
15
+
16
+ * added possibility to copy a plot to clipboard.
17
+
14
18
 
15
19
  ### 2.3.0
16
20
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-plots-basic",
3
3
  "description": "Svelte components for creating simple plots",
4
- "version": "2.3.6",
4
+ "version": "2.4.0",
5
5
  "author": "Sergey Kucheryavskiy",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -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 = 'hover'; // how to show download links panel ('none', 'hover', 'fixed')
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 #00000040;
446
- margin: 0.5em;
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
- /* Functions needed to adjust DPI of resulting PNG image */
248
- /* taken from: https://github.com/shutterstock/changeDPI */
249
- /* and modified */
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
- * 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);
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
- return c ^ -1;
262
+ crcTable[n] = c;
278
263
  }
264
+ return crcTable;
265
+ }
279
266
 
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;
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
- * 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);
350
- }
351
- const finalArray = changeDpiOnArray(dataArray, dpi, 'image/png');
352
- const base64Header = btoa(String.fromCharCode(...finalArray));
353
- return [format, ',', base64Header, restOfData].join('');
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
+ }