diff --git a/src/assets/geo_assets.js b/src/assets/geo_assets.js index 2356fb4c4db..32a15723b2b 100644 --- a/src/assets/geo_assets.js +++ b/src/assets/geo_assets.js @@ -11,7 +11,6 @@ var saneTopojson = require('sane-topojson'); -// package version injected by `npm run preprocess` -exports.version = '1.52.3'; +exports.version = require('../version').version; exports.topojson = saneTopojson; diff --git a/src/core.js b/src/core.js index 6327d455450..eb45036dbd0 100644 --- a/src/core.js +++ b/src/core.js @@ -8,8 +8,7 @@ 'use strict'; -// package version injected by `npm run preprocess` -exports.version = '1.52.3'; +exports.version = require('./version').version; // inject promise polyfill require('es6-promise').polyfill(); diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 55639a1c0e3..11491416f01 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -11,16 +11,18 @@ 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'], + values: ['png', 'jpeg', 'webp', 'svg', 'full-json'], dflt: 'png', description: 'Sets the format of exported image.' }, @@ -170,8 +172,24 @@ function toImage(gd, opts) { var width = clonedGd._fullLayout.width; var height = clonedGd._fullLayout.height; - plotApi.purge(clonedGd); - document.body.removeChild(clonedGd); + 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) { diff --git a/src/plots/plots.js b/src/plots/plots.js index 55fd49714ca..85d32db040a 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -2037,9 +2037,10 @@ plots.didMarginChange = function(margin0, margin1) { * keepall: keep data and src * @param {String} output If you specify 'object', the result will not be stringified * @param {Boolean} useDefaults If truthy, use _fullLayout and _fullData + * @param {Boolean} includeConfig If truthy, include _context * @returns {Object|String} */ -plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { +plots.graphJson = function(gd, dataonly, mode, output, useDefaults, includeConfig) { // if the defaults aren't supplied yet, we need to do that... if((useDefaults && dataonly && !gd._fullData) || (useDefaults && !dataonly && !gd._fullLayout)) { @@ -2050,26 +2051,29 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { var layout = (useDefaults) ? gd._fullLayout : gd.layout; var frames = (gd._transitionData || {})._frames; - function stripObj(d) { + function stripObj(d, keepFunction) { if(typeof d === 'function') { - return null; + return keepFunction ? '_function_' : null; } if(Lib.isPlainObject(d)) { var o = {}; - var v, src; - for(v in d) { + var src; + Object.keys(d).sort().forEach(function(v) { // remove private elements and functions // _ is for private, [ is a mistake ie [object Object] - if(typeof d[v] === 'function' || - ['_', '['].indexOf(v.charAt(0)) !== -1) { - continue; + if(['_', '['].indexOf(v.charAt(0)) !== -1) return; + + // if a function, add if necessary then move on + if(typeof d[v] === 'function') { + if(keepFunction) o[v] = '_function'; + return; } // look for src/data matches and remove the appropriate one if(mode === 'keepdata') { // keepdata: remove all ...src tags if(v.substr(v.length - 3) === 'src') { - continue; + return; } } else if(mode === 'keepstream') { // keep sourced data if it's being streamed. @@ -2078,7 +2082,7 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { src = d[v + 'src']; if(typeof src === 'string' && src.indexOf(':') > 0) { if(!Lib.isPlainObject(d.stream)) { - continue; + return; } } } else if(mode !== 'keepall') { @@ -2086,18 +2090,18 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { // if the source tag is well-formed src = d[v + 'src']; if(typeof src === 'string' && src.indexOf(':') > 0) { - continue; + return; } } // OK, we're including this... recurse into it - o[v] = stripObj(d[v]); - } + o[v] = stripObj(d[v], keepFunction); + }); return o; } if(Array.isArray(d)) { - return d.map(stripObj); + return d.map(function(x) {return stripObj(x, keepFunction);}); } if(Lib.isTypedArray(d)) { @@ -2126,6 +2130,8 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { if(frames) obj.frames = stripObj(frames); + if(includeConfig) obj.config = stripObj(gd._context, true); + return (output === 'object') ? obj : JSON.stringify(obj); }; diff --git a/src/snapshot/download.js b/src/snapshot/download.js index 286de0c8a0d..82e2c589c07 100644 --- a/src/snapshot/download.js +++ b/src/snapshot/download.js @@ -51,7 +51,7 @@ function downloadImage(gd, opts) { var promise = toImage(gd, opts); var filename = opts.filename || gd.fn || 'newplot'; - filename += '.' + opts.format; + filename += '.' + opts.format.replace('-', '.'); promise.then(function(result) { if(_gd) _gd._snapshotInProgress = false; diff --git a/src/snapshot/helpers.js b/src/snapshot/helpers.js index 6ab83e0b073..9a968ba90a7 100644 --- a/src/snapshot/helpers.js +++ b/src/snapshot/helpers.js @@ -36,6 +36,10 @@ exports.encodeSVG = function(svg) { return 'data:image/svg+xml,' + encodeURIComponent(svg); }; +exports.encodeJSON = function(json) { + return 'data:application/json,' + encodeURIComponent(json); +}; + var DOM_URL = window.URL || window.webkitURL; exports.createObjectURL = function(blob) { @@ -49,6 +53,8 @@ exports.revokeObjectURL = function(url) { exports.createBlob = function(url, format) { if(format === 'svg') { return new window.Blob([url], {type: 'image/svg+xml;charset=utf-8'}); + } else if(format === 'full-json') { + return new window.Blob([url], {type: 'application/json;charset=utf-8'}); } else { var binary = fixBinary(window.atob(url)); return new window.Blob([binary], {type: 'image/' + format}); diff --git a/src/version.js b/src/version.js new file mode 100644 index 00000000000..8cd3f7238a6 --- /dev/null +++ b/src/version.js @@ -0,0 +1,12 @@ +/** +* Copyright 2012-2020, 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'; + +// package version injected by `npm run preprocess` +exports.version = '1.52.3'; diff --git a/tasks/preprocess.js b/tasks/preprocess.js index 287af6913bb..f88d9f31cb8 100644 --- a/tasks/preprocess.js +++ b/tasks/preprocess.js @@ -9,8 +9,7 @@ var updateVersion = require('./util/update_version'); // main makeBuildCSS(); copyTopojsonFiles(); -updateVersion(constants.pathToPlotlyCore); -updateVersion(constants.pathToPlotlyGeoAssetsSrc); +updateVersion(constants.pathToPlotlyVersion); // convert scss to css to js function makeBuildCSS() { diff --git a/tasks/util/constants.js b/tasks/util/constants.js index 3fb01da2428..ea9cc95213c 100644 --- a/tasks/util/constants.js +++ b/tasks/util/constants.js @@ -47,6 +47,7 @@ module.exports = { pathToPlotlyIndex: path.join(pathToLib, 'index.js'), pathToPlotlyCore: path.join(pathToSrc, 'core.js'), + pathToPlotlyVersion: path.join(pathToSrc, 'version.js'), pathToPlotlyBuild: path.join(pathToBuild, 'plotly.js'), pathToPlotlyDist: path.join(pathToDist, 'plotly.js'), pathToPlotlyDistMin: path.join(pathToDist, 'plotly.min.js'), diff --git a/test/jasmine/tests/download_test.js b/test/jasmine/tests/download_test.js index adee0c911ef..6b7473419df 100644 --- a/test/jasmine/tests/download_test.js +++ b/test/jasmine/tests/download_test.js @@ -52,6 +52,12 @@ describe('Plotly.downloadImage', function() { .then(done); }, LONG_TIMEOUT_INTERVAL); + it('should create link, remove link, accept options', function(done) { + downloadTest(gd, 'full-json') + .catch(failTest) + .then(done); + }, LONG_TIMEOUT_INTERVAL); + it('should create link, remove link, accept options', function(done) { checkWebp(function(supported) { if(supported) { @@ -203,7 +209,7 @@ function downloadTest(gd, format) { var linkdeleted = domchanges[domchanges.length - 1].removedNodes[0]; expect(linkadded.getAttribute('href').split(':')[0]).toBe('blob'); - expect(filename).toBe('plotly_download.' + format); + expect(filename).toBe('plotly_download.' + format.replace('-', '.')); expect(linkadded).toBe(linkdeleted); }); } diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index 45252af0aa8..be4bc062ca8 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -263,4 +263,43 @@ describe('Plotly.toImage', function() { done(); }); }); + + describe('with format `full-json`', function() { + var imgOpts = {format: 'full-json', imageDataOnly: true}; + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + afterEach(destroyGraphDiv); + + it('export a graph div', function(done) { + Plotly.plot(gd, [{y: [1, 2, 3]}]) + .then(function(gd) { return Plotly.toImage(gd, imgOpts);}) + .then(function(fig) { + fig = JSON.parse(fig); + ['data', 'layout', 'config'].forEach(function(key) { + expect(fig.hasOwnProperty(key)).toBeTruthy('is missing key: ' + key); + }); + expect(fig.data[0].mode).toBe('lines+markers', 'contain default mode'); + expect(fig.version).toBe(Plotly.version, 'contains Plotly version'); + }) + .catch(failTest) + .then(done); + }); + + it('export an object with data/layout/config', function(done) { + Plotly.toImage({data: [{y: [1, 2, 3]}]}, imgOpts) + .then(function(fig) { + fig = JSON.parse(fig); + ['data', 'layout', 'config'].forEach(function(key) { + expect(fig.hasOwnProperty(key)).toBeTruthy('is missing key: ' + key); + }); + expect(fig.data[0].mode).toBe('lines+markers', 'contain default mode'); + expect(fig.version).toBe(Plotly.version, 'contains Plotly version'); + }) + .catch(failTest) + .then(done); + }); + }); });