Skip to content

Commit 7113539

Browse files
committed
Merge branch 'master' into jun_candidate
2 parents 0488aec + 75614a5 commit 7113539

27 files changed

+11426
-197
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
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],

devtools/test_dashboard/devtools.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ var Tabs = {
5555
});
5656
},
5757

58+
getMock: function(mockName, callback) {
59+
var mockURL = '/test/image/mocks/' + mockName + '.json';
60+
61+
d3.json(mockURL, function(err, fig) {
62+
if(typeof callback !== 'function') {
63+
window.mock = fig;
64+
} else {
65+
callback(err, fig);
66+
}
67+
});
68+
},
69+
5870
// Save a png snapshot and display it below the plot
5971
snapshot: function(id) {
6072
var gd = Tabs.getGraph(id);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
"delaunay-triangulate": "^1.1.6",
5555
"es6-promise": "^3.0.2",
5656
"fast-isnumeric": "^1.1.1",
57-
"gl-contour2d": "^1.0.1",
57+
"gl-contour2d": "^1.1.2",
5858
"gl-error2d": "^1.0.0",
5959
"gl-error3d": "^1.0.0",
6060
"gl-heatmap2d": "^1.0.2",

src/components/modebar/buttons.js

Lines changed: 12 additions & 39 deletions
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

Lines changed: 2 additions & 0 deletions
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/plot_api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) {
15751575
'mode','visible','type','orientation','fill',
15761576
'histfunc','histnorm','text',
15771577
'x', 'y', 'z',
1578+
'a', 'b', 'c',
15781579
'xtype','x0','dx','ytype','y0','dy','xaxis','yaxis',
15791580
'line.width',
15801581
'connectgaps', 'transpose', 'zsmooth',

src/plot_api/to_image.js

Lines changed: 113 additions & 0 deletions
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/plots/cartesian/graph_interact.js

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ fx.getClosest = function(cd, distfn, pointData) {
628628
};
629629

630630
function cleanPoint(d, hovermode) {
631-
d.posref = hovermode==='y' ? (d.x0+d.x1)/2 : (d.y0+d.y1)/2;
631+
d.posref = hovermode === 'y' ? (d.x0 + d.x1) / 2 : (d.y0 + d.y1) / 2;
632632

633633
// then constrain all the positions to be on the plot
634634
d.x0 = Lib.constrain(d.x0, 0, d.xa._length);
@@ -639,36 +639,36 @@ function cleanPoint(d, hovermode) {
639639
// and convert the x and y label values into objects
640640
// formatted as text, with font info
641641
var logOffScale;
642-
if(d.xLabelVal!==undefined) {
643-
logOffScale = (d.xa.type==='log' && d.xLabelVal<=0);
642+
if(d.xLabelVal !== undefined) {
643+
logOffScale = (d.xa.type === 'log' && d.xLabelVal <= 0);
644644
var xLabelObj = Axes.tickText(d.xa,
645645
d.xa.c2l(logOffScale ? -d.xLabelVal : d.xLabelVal), 'hover');
646646
if(logOffScale) {
647-
if(d.xLabelVal===0) d.xLabel = '0';
647+
if(d.xLabelVal === 0) d.xLabel = '0';
648648
else d.xLabel = '-' + xLabelObj.text;
649649
}
650650
else d.xLabel = xLabelObj.text;
651651
d.xVal = d.xa.c2d(d.xLabelVal);
652652
}
653653

654-
if(d.yLabelVal!==undefined) {
655-
logOffScale = (d.ya.type==='log' && d.yLabelVal<=0);
654+
if(d.yLabelVal !== undefined) {
655+
logOffScale = (d.ya.type === 'log' && d.yLabelVal <= 0);
656656
var yLabelObj = Axes.tickText(d.ya,
657657
d.ya.c2l(logOffScale ? -d.yLabelVal : d.yLabelVal), 'hover');
658658
if(logOffScale) {
659-
if(d.yLabelVal===0) d.yLabel = '0';
659+
if(d.yLabelVal === 0) d.yLabel = '0';
660660
else d.yLabel = '-' + yLabelObj.text;
661661
}
662662
else d.yLabel = yLabelObj.text;
663663
d.yVal = d.ya.c2d(d.yLabelVal);
664664
}
665665

666-
if(d.zLabelVal!==undefined) d.zLabel = String(d.zLabelVal);
666+
if(d.zLabelVal !== undefined) d.zLabel = String(d.zLabelVal);
667667

668668
// for box means and error bars, add the range to the label
669-
if(d.xerr!==undefined) {
669+
if(!isNaN(d.xerr) && !(d.xa.type === 'log' && d.xerr <= 0)) {
670670
var xeText = Axes.tickText(d.xa, d.xa.c2l(d.xerr), 'hover').text;
671-
if(d.xerrneg!==undefined) {
671+
if(d.xerrneg !== undefined) {
672672
d.xLabel += ' +' + xeText + ' / -' +
673673
Axes.tickText(d.xa, d.xa.c2l(d.xerrneg), 'hover').text;
674674
}
@@ -677,27 +677,27 @@ function cleanPoint(d, hovermode) {
677677
// small distance penalty for error bars, so that if there are
678678
// traces with errors and some without, the error bar label will
679679
// hoist up to the point
680-
if(hovermode==='x') d.distance += 1;
680+
if(hovermode === 'x') d.distance += 1;
681681
}
682-
if(d.yerr!==undefined) {
682+
if(!isNaN(d.yerr) && !(d.ya.type === 'log' && d.yerr <= 0)) {
683683
var yeText = Axes.tickText(d.ya, d.ya.c2l(d.yerr), 'hover').text;
684-
if(d.yerrneg!==undefined) {
684+
if(d.yerrneg !== undefined) {
685685
d.yLabel += ' +' + yeText + ' / -' +
686686
Axes.tickText(d.ya, d.ya.c2l(d.yerrneg), 'hover').text;
687687
}
688688
else d.yLabel += ' &plusmn; ' + yeText;
689689

690-
if(hovermode==='y') d.distance += 1;
690+
if(hovermode === 'y') d.distance += 1;
691691
}
692692

693693
var infomode = d.trace.hoverinfo;
694-
if(infomode!=='all') {
694+
if(infomode !== 'all') {
695695
infomode = infomode.split('+');
696-
if(infomode.indexOf('x')===-1) d.xLabel = undefined;
697-
if(infomode.indexOf('y')===-1) d.yLabel = undefined;
698-
if(infomode.indexOf('z')===-1) d.zLabel = undefined;
699-
if(infomode.indexOf('text')===-1) d.text = undefined;
700-
if(infomode.indexOf('name')===-1) d.name = undefined;
696+
if(infomode.indexOf('x') === -1) d.xLabel = undefined;
697+
if(infomode.indexOf('y') === -1) d.yLabel = undefined;
698+
if(infomode.indexOf('z') === -1) d.zLabel = undefined;
699+
if(infomode.indexOf('text') === -1) d.text = undefined;
700+
if(infomode.indexOf('name') === -1) d.name = undefined;
701701
}
702702
return d;
703703
}

src/snapshot/download.js

Lines changed: 64 additions & 0 deletions
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;

0 commit comments

Comments
 (0)