Skip to content

Commit bc96343

Browse files
add new promise-based toImage and downloadImage to Plotly
1 parent 1f406e5 commit bc96343

File tree

9 files changed

+512
-94
lines changed

9 files changed

+512
-94
lines changed

src/components/modebar/buttons.js

+8-42
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
var Plotly = require('../../plotly');
1313
var Lib = require('../../lib');
1414
var setCursor = require('../../lib/setcursor');
15-
var Snapshot = require('../../snapshot');
15+
var downloadImage = require('../../snapshot/download');
1616
var Icons = require('../../../build/ploticon');
1717

1818

@@ -49,49 +49,15 @@ modeBarButtons.toImage = {
4949
title: 'Download plot as a png',
5050
icon: Icons.camera,
5151
click: function(gd) {
52-
var format = 'png';
53-
54-
if(Lib.isIE()) {
55-
Lib.notifier('Snapshotting is unavailable in Internet Explorer. ' +
56-
'Consider exporting your images using the Plotly Cloud', 'long');
57-
return;
58-
}
59-
60-
if(gd._snapshotInProgress) {
61-
Lib.notifier('Snapshotting is still in progress - please hold', 'long');
62-
return;
63-
}
64-
65-
gd._snapshotInProgress = true;
6652
Lib.notifier('Taking snapshot - this may take a few seconds', 'long');
6753

68-
var ev = Snapshot.toImage(gd, {format: format});
69-
70-
var filename = gd.fn || 'newplot';
71-
filename += '.' + format;
72-
73-
ev.once('success', function(result) {
74-
gd._snapshotInProgress = false;
75-
76-
var downloadLink = document.createElement('a');
77-
downloadLink.href = result;
78-
downloadLink.download = filename; // only supported by FF and Chrome
79-
80-
document.body.appendChild(downloadLink);
81-
downloadLink.click();
82-
document.body.removeChild(downloadLink);
83-
84-
ev.clean();
85-
});
86-
87-
ev.once('error', function(err) {
88-
gd._snapshotInProgress = false;
89-
90-
Lib.notifier('Sorry there was a problem downloading your ' + format, 'long');
91-
console.error(err);
92-
93-
ev.clean();
94-
});
54+
downloadImage(gd)
55+
.then(function(filename) {
56+
Lib.notifier('Snapshot succeeded - ' + filename, 'long');
57+
})
58+
.catch(function() {
59+
Lib.notifier('Sorry there was a problem downloading your snapshot', 'long');
60+
});
9561
}
9662
};
9763

src/core.js

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ exports.moveTraces = Plotly.moveTraces;
3131
exports.purge = Plotly.purge;
3232
exports.setPlotConfig = require('./plot_api/set_plot_config');
3333
exports.register = Plotly.register;
34+
exports.toImage = require('./plot_api/to_image');
35+
exports.downloadImage = require('./snapshot/download');
3436

3537
// plot icons
3638
exports.Icons = require('../build/ploticon');

src/plot_api/to_image.js

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Plotly = require('../plotly');
12+
13+
var isNumeric = require('fast-isnumeric');
14+
15+
/**
16+
* @param {object} gd figure Object
17+
* @param {object} opts option object
18+
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
19+
* @param opts.width width of snapshot in px
20+
* @param opts.height height of snapshot in px
21+
*/
22+
function toImage(gd, opts) {
23+
var Snapshot = require('../snapshot');
24+
25+
var promise = new Promise(function(resolve, reject) {
26+
// check for undefined opts
27+
opts = opts || {};
28+
// default to png
29+
opts.format = opts.format || 'png';
30+
31+
var isSizeGood = function(size) {
32+
// undefined and null are valid options
33+
if(size === undefined || size === null) {
34+
return true;
35+
}
36+
37+
if(isNumeric(size) && size > 1) {
38+
return true;
39+
}
40+
};
41+
42+
if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) {
43+
reject(new Error('Height and width should be pixel values.'));
44+
}
45+
46+
// first clone the GD so we can operate in a clean environment
47+
var clone = Snapshot.clone(gd, {format: 'png', height: opts.height, width: opts.width});
48+
var clonedGd = clone.td;
49+
50+
// put the cloned div somewhere off screen before attaching to DOM
51+
clonedGd.style.position = 'absolute';
52+
clonedGd.style.left = '-5000px';
53+
document.body.appendChild(clonedGd);
54+
55+
function wait() {
56+
var delay = Snapshot.getDelay(clonedGd._fullLayout);
57+
58+
return new Promise(function(resolve, reject) {
59+
setTimeout(function() {
60+
var svg = Snapshot.toSVG(clonedGd);
61+
62+
var canvasContainer = window.document.createElement('div');
63+
var canvas = window.document.createElement('canvas');
64+
65+
// window.document.body.appendChild(canvasContainer);
66+
canvasContainer.appendChild(canvas);
67+
68+
canvasContainer.id = Plotly.Lib.randstr();
69+
canvas.id = Plotly.Lib.randstr();
70+
71+
Snapshot.svgToImg({
72+
format: opts.format,
73+
width: clonedGd._fullLayout.width,
74+
height: clonedGd._fullLayout.height,
75+
canvas: canvas,
76+
svg: svg,
77+
// ask svgToImg to return a Promise
78+
// rather than EventEmitter
79+
// leave EventEmitter for backward
80+
// compatibility
81+
promise: true
82+
}).then(function(url) {
83+
if(clonedGd) clonedGd.remove();
84+
resolve(url);
85+
}).catch(function(err) {
86+
reject(err);
87+
});
88+
}, delay);
89+
});
90+
}
91+
92+
var redrawFunc = Snapshot.getRedrawFunc(clonedGd);
93+
94+
Plotly.plot(clonedGd, clone.data, clone.layout, clone.config)
95+
// TODO: the following is Plotly.Plots.redrawText but without the waiting.
96+
// we shouldn't need to do this, but in *occasional* cases we do. Figure
97+
// out why and take it out.
98+
99+
// not sure the above TODO makes sense anymore since
100+
// we have converted to promises
101+
.then(redrawFunc)
102+
.then(wait)
103+
.then(function(url) { resolve(url); })
104+
.catch(function(err) {
105+
reject(err);
106+
});
107+
});
108+
109+
return promise;
110+
}
111+
112+
module.exports = toImage;

src/snapshot/download.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright 2012-2016, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
10+
'use strict';
11+
12+
var toImage = require('../plot_api/to_image');
13+
var fileSaver = require('./filesaver');
14+
15+
/**
16+
* @param {object} gd figure Object
17+
* @param {object} opts option object
18+
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
19+
* @param opts.width width of snapshot in px
20+
* @param opts.height height of snapshot in px
21+
* @param opts.filename name of file excluding extension
22+
*/
23+
function downloadImage(gd, opts) {
24+
25+
// check for undefined opts
26+
opts = opts || {};
27+
28+
// default to png
29+
opts.format = opts.format || 'png';
30+
31+
return new Promise(function(resolve,reject) {
32+
if(gd._snapshotInProgress) {
33+
reject(new Error('Snapshotting already in progress.'));
34+
}
35+
36+
gd._snapshotInProgress = true;
37+
var promise = toImage(gd, opts);
38+
39+
var filename = opts.filename || gd.fn || 'newplot';
40+
filename += '.' + opts.format;
41+
42+
promise.then(function(result) {
43+
gd._snapshotInProgress = false;
44+
return fileSaver(result,filename);
45+
}).then(function(name) {
46+
resolve(name);
47+
}).catch(function(err) {
48+
gd._snapshotInProgress = false;
49+
reject(err);
50+
});
51+
});
52+
}
53+
54+
module.exports = downloadImage;

src/snapshot/filesaver.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
substantial portions of this code from FileSaver.js
3+
https://github.com/eligrey/FileSaver.js
4+
License: https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
5+
*/
6+
7+
/* FileSaver.js
8+
* A saveAs() FileSaver implementation.
9+
* 1.1.20160328
10+
*
11+
* By Eli Grey, http://eligrey.com
12+
* License: MIT
13+
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
14+
*/
15+
16+
17+
'use strict';
18+
19+
var fileSaver = function(url, name) {
20+
var saveLink = document.createElement('a');
21+
var canUseSaveLink = 'download' in saveLink;
22+
var isSafari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent);
23+
var promise = new Promise(function(resolve, reject) {
24+
// IE <10 is explicitly unsupported
25+
if(typeof navigator !== 'undefined' && /MSIE [1-9]\./.test(navigator.userAgent)) {
26+
reject(new Error('IE < 10 unsupported'));
27+
}
28+
29+
// First try a.download, then web filesystem, then object URLs
30+
if(isSafari) {
31+
// Safari doesn't allow downloading of blob urls
32+
document.location.href = 'data:attachment/file' + url.slice(url.search(/[,;]/));
33+
resolve(name);
34+
}
35+
36+
if(!name) {
37+
name = 'download';
38+
}
39+
40+
if(canUseSaveLink) {
41+
saveLink.href = url;
42+
saveLink.download = name;
43+
document.body.appendChild(saveLink);
44+
saveLink.click();
45+
document.body.removeChild(saveLink);
46+
resolve(name);
47+
}
48+
49+
// IE 10+ (native saveAs)
50+
if(typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) {
51+
navigator.msSaveOrOpenBlob(url, name);
52+
resolve(name);
53+
}
54+
55+
reject(new Error('download error'));
56+
});
57+
58+
return promise;
59+
};
60+
61+
module.exports = fileSaver;

src/snapshot/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ var Snapshot = {
3434
clone: require('./cloneplot'),
3535
toSVG: require('./tosvg'),
3636
svgToImg: require('./svgtoimg'),
37-
toImage: require('./toimage')
37+
toImage: require('./toimage'),
38+
downloadImage: require('./download')
3839
};
3940

4041
module.exports = Snapshot;

0 commit comments

Comments
 (0)