Skip to content

Feature/snapshot #1

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

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
547f2b5
begin thinking through the snapshot issue
timelyportfolio Apr 6, 2016
89e05ce
little more
timelyportfolio Apr 6, 2016
fb52fe7
add some starter code to a `_toImage` attached to the `div` leveragin…
timelyportfolio Apr 7, 2016
4436f3c
add bullets and codepen example
timelyportfolio Apr 7, 2016
760a93b
change `toImage` to use height and width if specified in opts
timelyportfolio Apr 7, 2016
cff741a
copy snapshot click handler to `toSnapshot` for generic use
timelyportfolio Apr 7, 2016
937340c
convert `svgtoimg` to `Promise` from `EventEmitter`
timelyportfolio Apr 8, 2016
ac86931
convert `toimage` to Promises from EventEmitter
timelyportfolio Apr 8, 2016
ef09066
change snapshot modebar to work with promises instead of eventemitter
timelyportfolio Apr 8, 2016
099acf3
changes to snapshot thoughts; move to discuss on pull
timelyportfolio Apr 11, 2016
a223546
let `svgToImg` support both `EventEmitter` and `Promise`; temporary f…
timelyportfolio Apr 14, 2016
1f09a8e
start working toward a `downloadImage` method
timelyportfolio Apr 14, 2016
00623e2
return the Snapshot `toimage` to its original state for backward comp…
timelyportfolio Apr 14, 2016
1f7c8e3
attach new promise-based `toImage` to `Plotly`
timelyportfolio Apr 14, 2016
4b618db
add `downloadImage` to `Plotly.Snapshot` and use that for click handl…
timelyportfolio Apr 14, 2016
e1b031e
lint buttons.js
timelyportfolio Apr 15, 2016
dc33644
remove unnecessary `toImage` from plotly.js
timelyportfolio Apr 15, 2016
5116a84
move snapshot notifier to modebar handler and out of `downloadImage`;…
timelyportfolio Apr 15, 2016
8fbf716
doc height and width in `toImage`
timelyportfolio Apr 15, 2016
e7da65d
add `require` for `toImage` in core.js
timelyportfolio Apr 18, 2016
74e9bf0
begin tests for `toImage`
timelyportfolio Apr 18, 2016
9a8559e
add error on download if snapshot already in progress
timelyportfolio Apr 18, 2016
166724b
do some error checking on height and width of `toImage`
timelyportfolio Apr 18, 2016
9dc9423
test errors, file formats, and sizes of `toImage`
timelyportfolio Apr 18, 2016
13eef9f
lint `toImage` and `toImage` tests
timelyportfolio Apr 18, 2016
f124a03
address line notes
timelyportfolio Apr 18, 2016
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
48 changes: 48 additions & 0 deletions snapshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Plotly Snapshots

## Purpose
The purpose of this markdown document is to document exploration of how to best attach the `Plotly.Snapshot.toImage` function to the plot/`div` itself most fully discussed in [issue 83](https://github.com/plotly/plotly.js/issues/83). Another very nice ability would be to offer resize options for the snapshot.



## Questions
Where do we attach toImage on the graph div?
Is it _toImage?
Do we just require /snapshot and bind to `this`?

Will any of the chart types require special snapshot abilities or features?

What is the expected use case of our new ability?

How do we piggyback on the snapshot button in the toolbar?

How do we ask for new size?

Are there reference points from other libraries that we could mimic or learn from?


## Thoughts

- `Plotly.Snapshot.clone` could be used to resize by adding this to `options` when/if we use `Plotly.plot` with our cloned `div`. We could also dynamically show a resulting view in a modal or something similar and adjust with `Plotly.relayout`.

- `Plotly.Snapshot.clone` by default sets `staticPlot:true` in `config`.

- A very basic way to attach this assuming there is a modebar would be to do something like this. See [codepen](http://codepen.io/timelyportfolio/pen/ZWvyYM).
```
gd._toImage = function(){
this._fullLayout._modeBar.buttons.filter(
function(btn){return btn[0].name==="toImage"
})[0][0].click(this)
}
```

- `Plotly.Snapshot.clone` already has thumbnail ability by specifying [options tileClass:"thumbnail"](https://github.com/plotly/plotly.js/blob/master/src/snapshot/cloneplot.js#L76) for the specific thumbnail use case.



- Quick code to experiment from R
```
library(plotly)

ggplotly(ggplot(cars,aes(speed,dist))+geom_point())
```
36 changes: 5 additions & 31 deletions src/components/modebar/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

var Plotly = require('../../plotly');
var Lib = require('../../lib');
var Snapshot = require('../../snapshot');
var downloadImage = require('../../snapshot/download');
var Icons = require('../../../build/ploticon');


Expand Down Expand Up @@ -48,48 +48,22 @@ 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).catch(function(err){
Lib.notifier('Sorry there was a problem downloading your snapshot', 'long');
});
}
};
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');

Choose a reason for hiding this comment

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

nice 👍

Choose a reason for hiding this comment

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

@timelyportfolio any objections to add downloadImage to Plotly in this PR?

Choose a reason for hiding this comment

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

the way you wrote snapshot/download.js, I think it would as easy as adding:

exports.downloadImage = require('./snapshot/download');

below this line.

Copy link
Owner Author

Choose a reason for hiding this comment

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

makes sense, and I am sure it would get used. I'll just need to add some tests to feel good about it if it is an "official" function.

exports.downloadImage = require('./snapshot/download');

// plot icons
exports.Icons = require('../build/ploticon');
Expand Down
106 changes: 106 additions & 0 deletions src/plot_api/to_image.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* 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.
*/

/*eslint dot-notation: [2, {"allowPattern": "^catch$"}]*/

'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'

Choose a reason for hiding this comment

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

add info about opts.width and opts.height

* @param opts.width width of snapshot in px
* @param opts.height height of snapshot in px
*/
function toImage(gd, opts) {
var Snapshot = require('../snapshot');

Choose a reason for hiding this comment

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

bring var Snapshot = require('../snapshot'); just below var Plotly = require('../plotly');

Copy link
Owner Author

Choose a reason for hiding this comment

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

embarrassed to say, but when I do this Snapshot is empty, and I don't understand why. What I understand about scoping says that this makes no sense. Defining as before and where you suggest should make it available within, but for some reason it is an empty {} when I put it where it belongs.

Choose a reason for hiding this comment

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

Oh. That mean there's a circular dependency somewhere between src/plot_api/ and src/snapshot.

Removing that circular dependency might be tricky. I think it would require splitting up src/plot_api/plot_api.js into smaller files. Something nobody really wants to do.

You should be able to get away with using Plotly.Snapshot in src/plot_api/to_image.js without any issues.

Copy link
Owner Author

Choose a reason for hiding this comment

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

ok


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

if(
(opts.width && isNumeric(opts.width) && opts.width < 1) ||
(opts.height && isNumeric(opts.height) && opts.height < 1)
) {
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);
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

Choose a reason for hiding this comment

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

nicely done.

}).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;
62 changes: 62 additions & 0 deletions src/snapshot/download.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* 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');

/**
* @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 is unavailable in Internet Explorer. ' +
'Consider exporting your images using the Plotly Cloud'));
}

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;

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);
resolve();
})
.catch(function(err) {
gd._snapshotInProgress = false;
console.error(err);
reject(err);
});
});
};

module.exports = downloadImage;
3 changes: 2 additions & 1 deletion src/snapshot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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