'use strict';
var isNumeric = require('fast-isnumeric');
var plotApi = require('./plot_api');
var plots = require('../plots/plots');
var Lib = require('../lib');
var helpers = require('../snapshot/helpers');
var toSVG = require('../snapshot/tosvg');
var svgToImg = require('../snapshot/svgtoimg');
var version = require('../version').version;
var attrs = {
format: {
valType: 'enumerated',
values: ['png', 'jpeg', 'webp', 'svg', 'full-json'],
dflt: 'png',
description: 'Sets the format of exported image.'
},
width: {
valType: 'number',
min: 1,
description: [
'Sets the exported image width.',
'Defaults to the value found in `layout.width`',
'If set to *null*, the exported image width will match the current graph width.'
].join(' ')
},
height: {
valType: 'number',
min: 1,
description: [
'Sets the exported image height.',
'Defaults to the value found in `layout.height`',
'If set to *null*, the exported image height will match the current graph height.'
].join(' ')
},
scale: {
valType: 'number',
min: 0,
dflt: 1,
description: [
'Sets a scaling for the generated image.',
'If set, all features of a graphs (e.g. text, line width)',
'are scaled, unlike simply setting',
'a bigger *width* and *height*.'
].join(' ')
},
setBackground: {
valType: 'any',
dflt: false,
description: [
'Sets the image background mode.',
'By default, the image background is determined by `layout.paper_bgcolor`,',
'the *transparent* mode.',
'One might consider setting `setBackground` to *opaque*',
'when exporting a *jpeg* image as JPEGs do not support opacity.'
].join(' ')
},
imageDataOnly: {
valType: 'boolean',
dflt: false,
description: [
'Determines whether or not the return value is prefixed by',
'the image format\'s corresponding \'data:image;\' spec.'
].join(' ')
}
};
/** Plotly.toImage
*
* @param {object | string | HTML div} gd
* can either be a data/layout/config object
* or an existing graph
* or an id to an existing graph
* @param {object} opts (see above)
* @return {promise}
*/
function toImage(gd, opts) {
opts = opts || {};
var data;
var layout;
var config;
var fullLayout;
if(Lib.isPlainObject(gd)) {
data = gd.data || [];
layout = gd.layout || {};
config = gd.config || {};
fullLayout = {};
} else {
gd = Lib.getGraphDiv(gd);
data = Lib.extendDeep([], gd.data);
layout = Lib.extendDeep({}, gd.layout);
config = gd._context;
fullLayout = gd._fullLayout || {};
}
function isImpliedOrValid(attr) {
return !(attr in opts) || Lib.validate(opts[attr], attrs[attr]);
}
if((!isImpliedOrValid('width') && opts.width !== null) ||
(!isImpliedOrValid('height') && opts.height !== null)) {
throw new Error('Height and width should be pixel values.');
}
if(!isImpliedOrValid('format')) {
throw new Error('Export format is not ' + Lib.join2(attrs.format.values, ', ', ' or ') + '.');
}
var fullOpts = {};
function coerce(attr, dflt) {
return Lib.coerce(opts, fullOpts, attrs, attr, dflt);
}
var format = coerce('format');
var width = coerce('width');
var height = coerce('height');
var scale = coerce('scale');
var setBackground = coerce('setBackground');
var imageDataOnly = coerce('imageDataOnly');
// put the cloned div somewhere off screen before attaching to DOM
var clonedGd = document.createElement('div');
clonedGd.style.position = 'absolute';
clonedGd.style.left = '-5000px';
document.body.appendChild(clonedGd);
// extend layout with image options
var layoutImage = Lib.extendFlat({}, layout);
if(width) {
layoutImage.width = width;
} else if(opts.width === null && isNumeric(fullLayout.width)) {
layoutImage.width = fullLayout.width;
}
if(height) {
layoutImage.height = height;
} else if(opts.height === null && isNumeric(fullLayout.height)) {
layoutImage.height = fullLayout.height;
}
// extend config for static plot
var configImage = Lib.extendFlat({}, config, {
_exportedPlot: true,
staticPlot: true,
setBackground: setBackground
});
var redrawFunc = helpers.getRedrawFunc(clonedGd);
function wait() {
return new Promise(function(resolve) {
setTimeout(resolve, helpers.getDelay(clonedGd._fullLayout));
});
}
function convert() {
return new Promise(function(resolve, reject) {
var svg = toSVG(clonedGd, format, scale);
var width = clonedGd._fullLayout.width;
var height = clonedGd._fullLayout.height;
function cleanup() {
plotApi.purge(clonedGd);
document.body.removeChild(clonedGd);
}
if(format === 'full-json') {
var json = plots.graphJson(clonedGd, false, 'keepdata', 'object', true, true);
json.version = version;
json = JSON.stringify(json);
cleanup();
if(imageDataOnly) {
return resolve(json);
} else {
return resolve(helpers.encodeJSON(json));
}
}
cleanup();
if(format === 'svg') {
if(imageDataOnly) {
return resolve(svg);
} else {
return resolve(helpers.encodeSVG(svg));
}
}
var canvas = document.createElement('canvas');
canvas.id = Lib.randstr();
svgToImg({
format: format,
width: width,
height: height,
scale: scale,
canvas: canvas,
svg: svg,
// ask svgToImg to return a Promise
// rather than EventEmitter
// leave EventEmitter for backward
// compatibility
promise: true
})
.then(resolve)
.catch(reject);
});
}
function urlToImageData(url) {
if(imageDataOnly) {
return url.replace(helpers.IMAGE_URL_PREFIX, '');
} else {
return url;
}
}
return new Promise(function(resolve, reject) {
plotApi.newPlot(clonedGd, data, layoutImage, configImage)
.then(redrawFunc)
.then(wait)
.then(convert)
.then(function(url) { resolve(urlToImageData(url)); })
.catch(function(err) { reject(err); });
});
}
module.exports = toImage;