Skip to content

Commit 0be79e2

Browse files
committed
Merge pull request #446 from timelyportfolio/feature/snapshot-pull
add new promise-based toImage and downloadImage to Plotly
2 parents 5601e69 + 05a2448 commit 0be79e2

File tree

11 files changed

+569
-91
lines changed

11 files changed

+569
-91
lines changed

.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"no-floating-decimal": [2],
3939
"space-infix-ops": [0, {"int32Hint": false}],
4040
"quotes": [2, "single"],
41-
"dot-notation": [2, {"allowKeywords": false}],
41+
"dot-notation": [2],
4242
"operator-linebreak": [2, "after"],
4343
"eqeqeq": [2],
4444
"new-cap": [0],

src/components/modebar/buttons.js

+12-39
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

@@ -51,47 +51,20 @@ modeBarButtons.toImage = {
5151
click: function(gd) {
5252
var format = 'png';
5353

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;
6654
Lib.notifier('Taking snapshot - this may take a few seconds', 'long');
6755

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);
56+
if(Lib.isIE()) {
57+
Lib.notifier('IE only supports svg. Changing format to svg.', 'long');
58+
format = 'svg';
59+
}
9260

93-
ev.clean();
94-
});
61+
downloadImage(gd, {'format': format})
62+
.then(function(filename) {
63+
Lib.notifier('Snapshot succeeded - ' + filename, 'long');
64+
})
65+
.catch(function() {
66+
Lib.notifier('Sorry there was a problem downloading your snapshot!', 'long');
67+
});
9568
}
9669
};
9770

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

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
return false;
42+
};
43+
44+
if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) {
45+
reject(new Error('Height and width should be pixel values.'));
46+
}
47+
48+
// first clone the GD so we can operate in a clean environment
49+
var clone = Snapshot.clone(gd, {format: 'png', height: opts.height, width: opts.width});
50+
var clonedGd = clone.td;
51+
52+
// put the cloned div somewhere off screen before attaching to DOM
53+
clonedGd.style.position = 'absolute';
54+
clonedGd.style.left = '-5000px';
55+
document.body.appendChild(clonedGd);
56+
57+
function wait() {
58+
var delay = Snapshot.getDelay(clonedGd._fullLayout);
59+
60+
return new Promise(function(resolve, reject) {
61+
setTimeout(function() {
62+
var svg = Snapshot.toSVG(clonedGd);
63+
64+
var canvasContainer = window.document.createElement('div');
65+
var canvas = window.document.createElement('canvas');
66+
67+
canvasContainer.appendChild(canvas);
68+
69+
canvasContainer.id = Plotly.Lib.randstr();
70+
canvas.id = Plotly.Lib.randstr();
71+
72+
Snapshot.svgToImg({
73+
format: opts.format,
74+
width: clonedGd._fullLayout.width,
75+
height: clonedGd._fullLayout.height,
76+
canvas: canvas,
77+
svg: svg,
78+
// ask svgToImg to return a Promise
79+
// rather than EventEmitter
80+
// leave EventEmitter for backward
81+
// compatibility
82+
promise: true
83+
}).then(function(url) {
84+
if(clonedGd) document.body.removeChild(clonedGd);
85+
resolve(url);
86+
}).catch(function(err) {
87+
reject(err);
88+
});
89+
}, delay);
90+
});
91+
}
92+
93+
var redrawFunc = Snapshot.getRedrawFunc(clonedGd);
94+
95+
Plotly.plot(clonedGd, clone.data, clone.layout, clone.config)
96+
// TODO: the following is Plotly.Plots.redrawText but without the waiting.
97+
// we shouldn't need to do this, but in *occasional* cases we do. Figure
98+
// out why and take it out.
99+
100+
// not sure the above TODO makes sense anymore since
101+
// we have converted to promises
102+
.then(redrawFunc)
103+
.then(wait)
104+
.then(function(url) { resolve(url); })
105+
.catch(function(err) {
106+
reject(err);
107+
});
108+
});
109+
110+
return promise;
111+
}
112+
113+
module.exports = toImage;

src/snapshot/download.js

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 Lib = require('../lib'); // for isIE
14+
var fileSaver = require('./filesaver');
15+
16+
/**
17+
* @param {object} gd figure Object
18+
* @param {object} opts option object
19+
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
20+
* @param opts.width width of snapshot in px
21+
* @param opts.height height of snapshot in px
22+
* @param opts.filename name of file excluding extension
23+
*/
24+
function downloadImage(gd, opts) {
25+
26+
// check for undefined opts
27+
opts = opts || {};
28+
29+
// default to png
30+
opts.format = opts.format || 'png';
31+
32+
return new Promise(function(resolve,reject) {
33+
if(gd._snapshotInProgress) {
34+
reject(new Error('Snapshotting already in progress.'));
35+
}
36+
37+
// see comments within svgtoimg for additional
38+
// discussion of problems with IE
39+
// can now draw to canvas, but CORS tainted canvas
40+
// does not allow toDataURL
41+
// svg format will work though
42+
if(Lib.isIE() && opts.format !== 'svg') {
43+
reject(new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.'));
44+
}
45+
46+
gd._snapshotInProgress = true;
47+
var promise = toImage(gd, opts);
48+
49+
var filename = opts.filename || gd.fn || 'newplot';
50+
filename += '.' + opts.format;
51+
52+
promise.then(function(result) {
53+
gd._snapshotInProgress = false;
54+
return fileSaver(result,filename);
55+
}).then(function(name) {
56+
resolve(name);
57+
}).catch(function(err) {
58+
gd._snapshotInProgress = false;
59+
reject(err);
60+
});
61+
});
62+
}
63+
64+
module.exports = downloadImage;

src/snapshot/filesaver.js

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

src/snapshot/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ var Snapshot = {
3636
clone: require('./cloneplot'),
3737
toSVG: require('./tosvg'),
3838
svgToImg: require('./svgtoimg'),
39-
toImage: require('./toimage')
39+
toImage: require('./toimage'),
40+
downloadImage: require('./download')
4041
};
4142

4243
module.exports = Snapshot;

0 commit comments

Comments
 (0)