Skip to content

Commit c0c1f27

Browse files
add new promise-based toImage and downloadImage to Plotly
1 parent 894bbad commit c0c1f27

File tree

8 files changed

+448
-86
lines changed

8 files changed

+448
-86
lines changed

src/components/modebar/buttons.js

+5-31
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,48 +49,22 @@ modeBarButtons.toImage = {
4949
title: 'Download plot as a png',
5050
icon: Icons.camera,
5151
click: function(gd) {
52-
var format = 'png';
53-
5452
if(Lib.isIE()) {
5553
Lib.notifier('Snapshotting is unavailable in Internet Explorer. ' +
5654
'Consider exporting your images using the Plotly Cloud', 'long');
5755
return;
5856
}
59-
57+
6058
if(gd._snapshotInProgress) {
6159
Lib.notifier('Snapshotting is still in progress - please hold', 'long');
6260
return;
6361
}
64-
62+
6563
gd._snapshotInProgress = true;
6664
Lib.notifier('Taking snapshot - this may take a few seconds', 'long');
6765

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();
66+
downloadImage(gd).catch(function(err){
67+
Lib.notifier('Sorry there was a problem downloading your snapshot', 'long');
9468
});
9569
}
9670
};

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

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

src/snapshot/download.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
14+
/**
15+
* @param {object} gd figure Object
16+
* @param {object} opts option object
17+
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
18+
* @param opts.width width of snapshot in px
19+
* @param opts.height height of snapshot in px
20+
* @param opts.filename name of file excluding extension
21+
*/
22+
function downloadImage(gd, opts) {
23+
24+
// check for undefined opts
25+
opts = opts || {};
26+
27+
// default to png
28+
opts.format = opts.format || 'png';
29+
30+
return new Promise(function(resolve,reject) {
31+
if(gd._snapshotInProgress) {
32+
reject(new Error('Snapshotting is unavailable in Internet Explorer. ' +
33+
'Consider exporting your images using the Plotly Cloud'));
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+
45+
var downloadLink = document.createElement('a');
46+
downloadLink.href = result;
47+
downloadLink.download = filename; // only supported by FF and Chrome
48+
49+
document.body.appendChild(downloadLink);
50+
downloadLink.click();
51+
document.body.removeChild(downloadLink);
52+
resolve(filename);
53+
}).catch(function(err) {
54+
gd._snapshotInProgress = false;
55+
console.error(err);
56+
reject(err);
57+
});
58+
});
59+
}
60+
61+
module.exports = downloadImage;

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;

src/snapshot/svgtoimg.js

+77-54
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,87 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9+
var EventEmitter = require('events').EventEmitter;
910

1011
'use strict';
1112

12-
var EventEmitter = require('events').EventEmitter;
13-
1413
function svgToImg(opts) {
15-
16-
var ev = opts.emitter ? opts.emitter : new EventEmitter();
17-
18-
var Image = window.Image;
19-
var Blob = window.Blob;
20-
21-
var svg = opts.svg;
22-
var format = opts.format || 'png';
23-
var canvas = opts.canvas;
24-
25-
var ctx = canvas.getContext('2d');
26-
var img = new Image();
27-
var DOMURL = window.URL || window.webkitURL;
28-
var svgBlob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'});
29-
var url = DOMURL.createObjectURL(svgBlob);
30-
31-
canvas.height = opts.height || 150;
32-
canvas.width = opts.width || 300;
33-
34-
img.onload = function() {
35-
var imgData;
36-
37-
DOMURL.revokeObjectURL(url);
38-
ctx.drawImage(img, 0, 0);
39-
40-
switch(format) {
41-
case 'jpeg':
42-
imgData = canvas.toDataURL('image/jpeg');
43-
break;
44-
case 'png':
45-
imgData = canvas.toDataURL('image/png');
46-
break;
47-
case 'webp':
48-
imgData = canvas.toDataURL('image/webp');
49-
break;
50-
case 'svg':
51-
imgData = svg;
52-
break;
53-
default:
54-
return ev.emit('error', 'Image format is not jpeg, png or svg');
55-
}
56-
57-
ev.emit('success', imgData);
58-
};
59-
60-
img.onerror = function(err) {
61-
DOMURL.revokeObjectURL(url);
62-
return ev.emit('error', err);
63-
};
64-
65-
img.src = url;
66-
14+
15+
var ev = opts.emitter || new EventEmitter();
16+
17+
var promise = new Promise(function(resolve, reject) {
18+
19+
var Image = window.Image;
20+
var Blob = window.Blob;
21+
22+
var svg = opts.svg;
23+
var format = opts.format || 'png';
24+
var canvas = opts.canvas;
25+
26+
var ctx = canvas.getContext('2d');
27+
var img = new Image();
28+
var DOMURL = window.URL || window.webkitURL;
29+
var svgBlob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'});
30+
var url = DOMURL.createObjectURL(svgBlob);
31+
32+
canvas.height = opts.height || 150;
33+
canvas.width = opts.width || 300;
34+
35+
img.onload = function() {
36+
var imgData;
37+
38+
DOMURL.revokeObjectURL(url);
39+
ctx.drawImage(img, 0, 0);
40+
41+
switch(format) {
42+
case 'jpeg':
43+
imgData = canvas.toDataURL('image/jpeg');
44+
break;
45+
case 'png':
46+
imgData = canvas.toDataURL('image/png');
47+
break;
48+
case 'webp':
49+
imgData = canvas.toDataURL('image/webp');
50+
break;
51+
case 'svg':
52+
imgData = svg;
53+
break;
54+
default:
55+
reject(new Error('Image format is not jpeg, png or svg'));
56+
// eventually remove the ev
57+
// in favor of promises
58+
if(!opts.promise){
59+
return ev.emit('error', 'Image format is not jpeg, png or svg');
60+
}
61+
}
62+
resolve(imgData);
63+
// eventually remove the ev
64+
// in favor of promises
65+
if(!opts.promise){
66+
ev.emit('success', imgData);
67+
}
68+
};
69+
70+
img.onerror = function(err) {
71+
DOMURL.revokeObjectURL(url);
72+
reject(err);
73+
// eventually remove the ev
74+
// in favor of promises
75+
if(!opts.promise){
76+
return ev.emit('error', err);
77+
}
78+
};
79+
80+
img.src = url;
81+
});
82+
83+
// temporary for backward compatibility
84+
// move to only Promise in 2.0.0
85+
// and eliminate the EventEmitter
86+
if(opts.promise) {
87+
return promise;
88+
}
89+
6790
return ev;
6891
}
6992

0 commit comments

Comments
 (0)