Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new promise-based toImage and downloadImage to Plotly #446

Merged
merged 7 commits into from
May 16, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add new promise-based toImage and downloadImage to Plotly
  • Loading branch information
timelyportfolio committed May 11, 2016
commit 642ddbaee517bed535e69208cc8097233f5a6e82
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"no-floating-decimal": [2],
"space-infix-ops": [0, {"int32Hint": false}],
"quotes": [2, "single"],
"dot-notation": [2, {"allowKeywords": false}],
"dot-notation": [2],
"operator-linebreak": [2, "after"],
"eqeqeq": [2],
"new-cap": [0],
Expand Down
50 changes: 8 additions & 42 deletions src/components/modebar/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
var Plotly = require('../../plotly');
var Lib = require('../../lib');
var setCursor = require('../../lib/setcursor');
var Snapshot = require('../../snapshot');
var downloadImage = require('../../snapshot/download');
var Icons = require('../../../build/ploticon');


Expand Down Expand Up @@ -49,49 +49,15 @@ modeBarButtons.toImage = {
title: 'Download plot as a png',
icon: Icons.camera,
click: function(gd) {
var format = 'png';

if(Lib.isIE()) {
Lib.notifier('Snapshotting is unavailable in Internet Explorer. ' +
'Consider exporting your images using the Plotly Cloud', 'long');
return;
}

if(gd._snapshotInProgress) {
Lib.notifier('Snapshotting is still in progress - please hold', 'long');
return;
}

gd._snapshotInProgress = true;
Lib.notifier('Taking snapshot - this may take a few seconds', 'long');

var ev = Snapshot.toImage(gd, {format: format});

var filename = gd.fn || 'newplot';
filename += '.' + format;

ev.once('success', function(result) {
gd._snapshotInProgress = false;

var downloadLink = document.createElement('a');
downloadLink.href = result;
downloadLink.download = filename; // only supported by FF and Chrome

document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);

ev.clean();
});

ev.once('error', function(err) {
gd._snapshotInProgress = false;

Lib.notifier('Sorry there was a problem downloading your ' + format, 'long');
console.error(err);

ev.clean();
});
downloadImage(gd)
.then(function(filename) {
Lib.notifier('Snapshot succeeded - ' + filename, 'long');
})
.catch(function() {
Lib.notifier('Sorry there was a problem downloading your snapshot', 'long');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we're in here, a little grammar fix:

Sorry, there was a problem downloading your snapshot!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wish they were all this easy

});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timelyportfolio why did you review this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

Expand Down
2 changes: 2 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ exports.moveTraces = Plotly.moveTraces;
exports.purge = Plotly.purge;
exports.setPlotConfig = require('./plot_api/set_plot_config');
exports.register = Plotly.register;
exports.toImage = require('./plot_api/to_image');
exports.downloadImage = require('./snapshot/download');

// plot icons
exports.Icons = require('../build/ploticon');
Expand Down
112 changes: 112 additions & 0 deletions src/plot_api/to_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Plotly = require('../plotly');

var isNumeric = require('fast-isnumeric');

/**
* @param {object} gd figure Object
* @param {object} opts option object
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
* @param opts.width width of snapshot in px
* @param opts.height height of snapshot in px
*/
function toImage(gd, opts) {
var Snapshot = require('../snapshot');

var promise = new Promise(function(resolve, reject) {
// check for undefined opts
opts = opts || {};
// default to png
opts.format = opts.format || 'png';

var isSizeGood = function(size) {
Copy link
Contributor

@mdtusz mdtusz May 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to explicitly return false on this one when size not good (just to be safe!)

Edit: Nvm I see it's only used right below. Up to you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added to be safe.

// undefined and null are valid options
if(size === undefined || size === null) {
return true;
}

if(isNumeric(size) && size > 1) {
return true;
}
};

if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) {
reject(new Error('Height and width should be pixel values.'));
}

// first clone the GD so we can operate in a clean environment
var clone = Snapshot.clone(gd, {format: 'png', height: opts.height, width: opts.width});
var clonedGd = clone.td;

// put the cloned div somewhere off screen before attaching to DOM
clonedGd.style.position = 'absolute';
clonedGd.style.left = '-5000px';
document.body.appendChild(clonedGd);

function wait() {
var delay = Snapshot.getDelay(clonedGd._fullLayout);

return new Promise(function(resolve, reject) {
setTimeout(function() {
var svg = Snapshot.toSVG(clonedGd);

var canvasContainer = window.document.createElement('div');
var canvas = window.document.createElement('canvas');

// window.document.body.appendChild(canvasContainer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ the commented out code please.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

canvasContainer.appendChild(canvas);

canvasContainer.id = Plotly.Lib.randstr();
canvas.id = Plotly.Lib.randstr();

Snapshot.svgToImg({
format: opts.format,
width: clonedGd._fullLayout.width,
height: clonedGd._fullLayout.height,
canvas: canvas,
svg: svg,
// ask svgToImg to return a Promise
// rather than EventEmitter
// leave EventEmitter for backward
// compatibility
promise: true
}).then(function(url) {
if(clonedGd) clonedGd.remove();
resolve(url);
}).catch(function(err) {
reject(err);
});
}, delay);
});
}

var redrawFunc = Snapshot.getRedrawFunc(clonedGd);

Plotly.plot(clonedGd, clone.data, clone.layout, clone.config)
// TODO: the following is Plotly.Plots.redrawText but without the waiting.
// we shouldn't need to do this, but in *occasional* cases we do. Figure
// out why and take it out.

// not sure the above TODO makes sense anymore since
// we have converted to promises
.then(redrawFunc)
.then(wait)
.then(function(url) { resolve(url); })
.catch(function(err) {
reject(err);
});
});

return promise;
}

module.exports = toImage;
64 changes: 64 additions & 0 deletions src/snapshot/download.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/


'use strict';

var toImage = require('../plot_api/to_image');
var Lib = require('../lib'); // for isIE
var fileSaver = require('./filesaver');

/**
* @param {object} gd figure Object
* @param {object} opts option object
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
* @param opts.width width of snapshot in px
* @param opts.height height of snapshot in px
* @param opts.filename name of file excluding extension
*/
function downloadImage(gd, opts) {

// check for undefined opts
opts = opts || {};

// default to png
opts.format = opts.format || 'png';

return new Promise(function(resolve,reject) {
if(gd._snapshotInProgress) {
reject(new Error('Snapshotting already in progress.'));
}

// see comments within svgtoimg for additional
// discussion of problems with IE
// can now draw to canvas, but CORS tainted canvas
// does not allow toDataURL
// svg format will work though
if(Lib.isIE() && opts.format !== 'svg') {
reject(new Error('Sorry IE does not support downloading from canvas.'));
}

gd._snapshotInProgress = true;
var promise = toImage(gd, opts);

var filename = opts.filename || gd.fn || 'newplot';
filename += '.' + opts.format;

promise.then(function(result) {
gd._snapshotInProgress = false;
return fileSaver(result,filename);
}).then(function(name) {
resolve(name);
}).catch(function(err) {
gd._snapshotInProgress = false;
reject(err);
});
});
}

module.exports = downloadImage;
66 changes: 66 additions & 0 deletions src/snapshot/filesaver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/*
* substantial portions of this code from FileSaver.js
* https://github.com/eligrey/FileSaver.js
* License: https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
* FileSaver.js
* A saveAs() FileSaver implementation.
* 1.1.20160328
*
* By Eli Grey, http://eligrey.com
* License: MIT
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
*/

'use strict';

var fileSaver = function(url, name) {
var saveLink = document.createElement('a');
var canUseSaveLink = 'download' in saveLink;
var isSafari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent);
var promise = new Promise(function(resolve, reject) {
// IE <10 is explicitly unsupported
if(typeof navigator !== 'undefined' && /MSIE [1-9]\./.test(navigator.userAgent)) {
reject(new Error('IE < 10 unsupported'));
}

// First try a.download, then web filesystem, then object URLs
if(isSafari) {
// Safari doesn't allow downloading of blob urls
document.location.href = 'data:attachment/file' + url.slice(url.search(/[,;]/));
resolve(name);
}

if(!name) {
name = 'download';
}

if(canUseSaveLink) {
saveLink.href = url;
saveLink.download = name;
document.body.appendChild(saveLink);
saveLink.click();
document.body.removeChild(saveLink);
resolve(name);
}

// IE 10+ (native saveAs)
if(typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(url, name);
resolve(name);
}

reject(new Error('download error'));
});

return promise;
};

module.exports = fileSaver;
3 changes: 2 additions & 1 deletion src/snapshot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ var Snapshot = {
clone: require('./cloneplot'),
toSVG: require('./tosvg'),
svgToImg: require('./svgtoimg'),
toImage: require('./toimage')
toImage: require('./toimage'),
downloadImage: require('./download')
};

module.exports = Snapshot;
Loading