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 10 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())
```
12 changes: 4 additions & 8 deletions src/components/modebar/buttons.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ modeBarButtons.toImage = {
gd._snapshotInProgress = true;
Lib.notifier('Taking snapshot - this may take a few seconds', 'long');

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

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

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

var downloadLink = document.createElement('a');
Expand All @@ -80,16 +80,12 @@ modeBarButtons.toImage = {
downloadLink.click();
document.body.removeChild(downloadLink);

ev.clean();
});

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

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

ev.clean();
});
}
};
Expand Down
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'),
toSnapshot: require('./tosnapshot')

Choose a reason for hiding this comment

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

Let's put toSnapshot in a new file called src/plot_api/to_image.js.

Copy link
Owner Author

Choose a reason for hiding this comment

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

actually, tosnapshot remains from my first misguided attempt. I can eliminate, since I'm not sure it has much value any more with our improved promise-based toimage.

Choose a reason for hiding this comment

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

Oops. Scratch ⏫ , toSnapshot is essentially what the modebar uses. Putting that in a separate file in src/snapshot/ makes sense.

Maybe you could rename tosnapshot.js -> download.js instead?

};

module.exports = Snapshot;
107 changes: 53 additions & 54 deletions src/snapshot/svgtoimg.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,61 @@

'use strict';

var EventEmitter = require('events').EventEmitter;

function svgToImg(opts) {

Choose a reason for hiding this comment

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

I think the solution would be to make svgToImg return an event object by default and optionally an promise (e.g. in the case of Plotly.Snapshot).

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, I'll get that working. Maybe in 2.0.0 we can move to all promises. Are you thinking in opts something like {promise:true}?

Choose a reason for hiding this comment

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

Maybe in 2.0.0 we can move to all promises.

That's the goal. We un-exposed Plotly.Snapshot in v2.0.0.

Are you thinking in opts something like {promise:true}

Exactly. So that Plotly.toImage will call svgToImg with { promise: true } under the covers.


var ev = opts.emitter ? opts.emitter : new EventEmitter();

var Image = window.Image;
var Blob = window.Blob;

var svg = opts.svg;
var format = opts.format || 'png';
var canvas = opts.canvas;

var ctx = canvas.getContext('2d');
var img = new Image();
var DOMURL = window.URL || window.webkitURL;
var svgBlob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svgBlob);

canvas.height = opts.height || 150;
canvas.width = opts.width || 300;

img.onload = function() {
var imgData;

DOMURL.revokeObjectURL(url);
ctx.drawImage(img, 0, 0);

switch(format) {
case 'jpeg':
imgData = canvas.toDataURL('image/jpeg');
break;
case 'png':
imgData = canvas.toDataURL('image/png');
break;
case 'webp':
imgData = canvas.toDataURL('image/webp');
break;
case 'svg':
imgData = svg;
break;
default:
return ev.emit('error', 'Image format is not jpeg, png or svg');
}

ev.emit('success', imgData);
};

img.onerror = function(err) {
DOMURL.revokeObjectURL(url);
return ev.emit('error', err);
};

img.src = url;

return ev;
var promise = new Promise(function(resolve, reject) {

var Image = window.Image;
var Blob = window.Blob;

var svg = opts.svg;
var format = opts.format || 'png';
var canvas = opts.canvas;

var ctx = canvas.getContext('2d');
var img = new Image();
var DOMURL = window.URL || window.webkitURL;
var svgBlob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svgBlob);

canvas.height = opts.height || 150;
canvas.width = opts.width || 300;

img.onload = function() {
var imgData;

DOMURL.revokeObjectURL(url);
ctx.drawImage(img, 0, 0);

switch(format) {
case 'jpeg':
imgData = canvas.toDataURL('image/jpeg');
break;
case 'png':
imgData = canvas.toDataURL('image/png');
break;
case 'webp':
imgData = canvas.toDataURL('image/webp');
break;
case 'svg':
imgData = svg;
break;
default:
reject(new Error('Image format is not jpeg, png or svg'));
}

resolve(imgData);
};

img.onerror = function(err) {
DOMURL.revokeObjectURL(url);
reject(err);
};

img.src = url;
});

return promise;
}

Choose a reason for hiding this comment

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

Unfortunately, making svgtoimg.js return a promise instead of an event object would be a breaking change as some adventurous users might have used Plotly.Snapshot.svgToImg in their app.

Choose a reason for hiding this comment

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

@timelyportfolio I should have mentioned ⏫ to you before, my mistake.

Copy link
Owner Author

Choose a reason for hiding this comment

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

no problem, I should have asked.


module.exports = svgToImg;
122 changes: 65 additions & 57 deletions src/snapshot/toimage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

'use strict';

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

/**
Expand All @@ -19,64 +18,73 @@ var Plotly = require('../plotly');
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
*/
function toImage(gd, opts) {

// first clone the GD so we can operate in a clean environment
var Snapshot = Plotly.Snapshot;
var ev = new EventEmitter();

var clone = Snapshot.clone(gd, {format: 'png'});
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);

setTimeout(function() {
var svg = Plotly.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();

ev = Plotly.Snapshot.svgToImg({
format: opts.format,
width: clonedGd._fullLayout.width,
height: clonedGd._fullLayout.height,
canvas: canvas,
emitter: ev,
svg: svg
var promise = new Promise(function(resolve, reject) {
// check for undefined opts
opts = (opts) ? opts : {};
// default to png
opts.format = (opts.format) ? opts.format : 'png';

// first clone the GD so we can operate in a clean environment
var Snapshot = Plotly.Snapshot;

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 = Plotly.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();

Plotly.Snapshot.svgToImg({
format: opts.format,
width: clonedGd._fullLayout.width,
height: clonedGd._fullLayout.height,
canvas: canvas,
svg: svg
}).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);
});
});

ev.clean = function() {
if(clonedGd) clonedGd.remove();
};

}, 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.
.then(redrawFunc)
.then(wait)
.catch(function(err) {
ev.emit('error', err);
});


return ev;
return promise;
}

module.exports = toImage;
Loading