Skip to content

Commit 8866525

Browse files
committed
improve Plotly.toImage
- accept data/layout/config figure (plain) object as input, along side existing graph div or (string) id of existing graph div - use Lib.validate & Lib.coerce to sanatize options - bypass Snapshot.cloneplot (Lib.extendDeep is really all we need) - handle 'setBackground` (same as config option) - add 'imageDataOnly' option to strip 'data:image/' prefix
1 parent 5d2b635 commit 8866525

File tree

2 files changed

+266
-98
lines changed

2 files changed

+266
-98
lines changed

src/plot_api/to_image.js

Lines changed: 165 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -8,101 +8,191 @@
88

99
'use strict';
1010

11-
var isNumeric = require('fast-isnumeric');
12-
1311
var Plotly = require('../plotly');
1412
var Lib = require('../lib');
1513

1614
var helpers = require('../snapshot/helpers');
17-
var clonePlot = require('../snapshot/cloneplot');
1815
var toSVG = require('../snapshot/tosvg');
1916
var svgToImg = require('../snapshot/svgtoimg');
2017

21-
/**
22-
* @param {object} gd figure Object
23-
* @param {object} opts option object
24-
* @param opts.format 'jpeg' | 'png' | 'webp' | 'svg'
25-
* @param opts.width width of snapshot in px
26-
* @param opts.height height of snapshot in px
18+
var getGraphDiv = require('./helpers').getGraphDiv;
19+
20+
var attrs = {
21+
format: {
22+
valType: 'enumerated',
23+
values: ['png', 'jpeg', 'webp', 'svg'],
24+
dflt: 'png',
25+
description: 'Sets the format of exported image.'
26+
},
27+
width: {
28+
valType: 'number',
29+
min: 1,
30+
description: [
31+
'Sets the exported image width.',
32+
'Defaults to the value found in `layout.width`'
33+
].join(' ')
34+
},
35+
height: {
36+
valType: 'number',
37+
min: 1,
38+
description: [
39+
'Sets the exported image height.',
40+
'Defaults to the value found in `layout.height`'
41+
].join(' ')
42+
},
43+
setBackground: {
44+
valType: 'any',
45+
dflt: false,
46+
description: [
47+
'Sets the image background mode.',
48+
'By default, the image background is determined by `layout.paper_bgcolor`,',
49+
'the *transparent* mode.',
50+
'One might consider setting `setBackground` to *opaque* or *blend*',
51+
'when exporting a *jpeg* image as JPEGs do not support opacity.'
52+
].join(' ')
53+
},
54+
imageDataOnly: {
55+
valType: 'boolean',
56+
dflt: false,
57+
description: [
58+
'Determines whether or not the return value is prefixed by',
59+
'the image format\'s corresponding \'data:image;\' spec.'
60+
].join(' ')
61+
}
62+
};
63+
64+
var IMAGE_URL_PREFIX = /^data:image\/\w+;base64,/;
65+
66+
/** Plotly.toImage
67+
*
68+
* @param {object | string | HTML div} gd
69+
* can either be a data/layout/config object
70+
* or an existing graph <div>
71+
* or an id to an existing graph <div>
72+
* @param {object} opts (see above)
73+
* @return {promise}
2774
*/
2875
function toImage(gd, opts) {
76+
opts = opts || {};
77+
78+
var data;
79+
var layout;
80+
var config;
81+
82+
if(Lib.isPlainObject(gd)) {
83+
data = gd.data || [];
84+
layout = gd.layout || {};
85+
config = gd.config || {};
86+
} else {
87+
gd = getGraphDiv(gd);
88+
data = Lib.extendDeep([], gd.data);
89+
layout = Lib.extendDeep({}, gd.layout);
90+
config = gd._context;
91+
}
92+
93+
function isBadlySet(attr) {
94+
return !(attr in opts) || Lib.validate(opts[attr], attrs[attr]);
95+
}
96+
97+
if(!isBadlySet('width') || !isBadlySet('height')) {
98+
throw new Error('Height and width should be pixel values.');
99+
}
100+
101+
if(!isBadlySet('format')) {
102+
throw new Error('Image format is not jpeg, png, svg or webp.');
103+
}
104+
105+
var fullOpts = {};
106+
107+
function coerce(attr, dflt) {
108+
return Lib.coerce(opts, fullOpts, attrs, attr, dflt);
109+
}
110+
111+
var format = coerce('format');
112+
var width = coerce('width');
113+
var height = coerce('height');
114+
var setBackground = coerce('setBackground');
115+
var imageDataOnly = coerce('imageDataOnly');
116+
117+
// put the cloned div somewhere off screen before attaching to DOM
118+
var clonedGd = document.createElement('div');
119+
clonedGd.style.position = 'absolute';
120+
clonedGd.style.left = '-5000px';
121+
document.body.appendChild(clonedGd);
122+
123+
// extend layout with image options
124+
var layoutImage = Lib.extendFlat({}, layout);
125+
if(width) layoutImage.width = width;
126+
if(height) layoutImage.height = height;
127+
128+
// extend config for static plot
129+
var configImage = Lib.extendFlat({}, config, {
130+
staticPlot: true,
131+
plotGlPixelRatio: 2,
132+
displaylogo: false,
133+
showLink: false,
134+
showTips: false,
135+
setBackground: setBackground
136+
});
29137

30-
var promise = new Promise(function(resolve, reject) {
31-
// check for undefined opts
32-
opts = opts || {};
33-
// default to png
34-
opts.format = opts.format || 'png';
35-
36-
var isSizeGood = function(size) {
37-
// undefined and null are valid options
38-
if(size === undefined || size === null) {
39-
return true;
40-
}
138+
var redrawFunc = helpers.getRedrawFunc(clonedGd);
41139

42-
if(isNumeric(size) && size > 1) {
43-
return true;
44-
}
140+
function wait() {
141+
return new Promise(function(resolve) {
142+
setTimeout(resolve, helpers.getDelay(clonedGd._fullLayout));
143+
});
144+
}
45145

46-
return false;
47-
};
146+
function convert() {
147+
return new Promise(function(resolve, reject) {
148+
var svg = toSVG(clonedGd);
48149

49-
if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) {
50-
reject(new Error('Height and width should be pixel values.'));
51-
}
150+
if(format === 'svg' && imageDataOnly) {
151+
return resolve(svg);
152+
}
52153

53-
// first clone the GD so we can operate in a clean environment
54-
var clone = clonePlot(gd, {format: 'png', height: opts.height, width: opts.width});
55-
var clonedGd = clone.gd;
56-
57-
// put the cloned div somewhere off screen before attaching to DOM
58-
clonedGd.style.position = 'absolute';
59-
clonedGd.style.left = '-5000px';
60-
document.body.appendChild(clonedGd);
61-
62-
function wait() {
63-
var delay = helpers.getDelay(clonedGd._fullLayout);
64-
65-
return new Promise(function(resolve, reject) {
66-
setTimeout(function() {
67-
var svg = toSVG(clonedGd);
68-
69-
var canvas = document.createElement('canvas');
70-
canvas.id = Lib.randstr();
71-
72-
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-
90-
}, delay);
154+
var canvas = document.createElement('canvas');
155+
canvas.id = Lib.randstr();
156+
157+
svgToImg({
158+
format: format,
159+
width: clonedGd._fullLayout.width,
160+
height: clonedGd._fullLayout.height,
161+
canvas: canvas,
162+
svg: svg,
163+
// ask svgToImg to return a Promise
164+
// rather than EventEmitter
165+
// leave EventEmitter for backward
166+
// compatibility
167+
promise: true
168+
})
169+
.then(function(url) {
170+
Plotly.purge(clonedGd);
171+
document.body.removeChild(clonedGd);
172+
resolve(url);
173+
})
174+
.catch(function(err) {
175+
reject(err);
91176
});
177+
});
178+
}
179+
180+
function urlToImageData(url) {
181+
if(imageDataOnly) {
182+
return url.replace(IMAGE_URL_PREFIX, '');
183+
} else {
184+
return url;
92185
}
186+
}
93187

94-
var redrawFunc = helpers.getRedrawFunc(clonedGd);
95-
96-
Plotly.plot(clonedGd, clone.data, clone.layout, clone.config)
188+
return new Promise(function(resolve, reject) {
189+
Plotly.plot(clonedGd, data, layoutImage, configImage)
97190
.then(redrawFunc)
98191
.then(wait)
99-
.then(function(url) { resolve(url); })
100-
.catch(function(err) {
101-
reject(err);
102-
});
192+
.then(convert)
193+
.then(function(url) { resolve(urlToImageData(url)); })
194+
.catch(function(err) { reject(err); });
103195
});
104-
105-
return promise;
106196
}
107197

108198
module.exports = toImage;

0 commit comments

Comments
 (0)