From 2ca0ae30018e274442f3a51cb98b1ea612fd3b9e Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 19 Feb 2020 13:00:05 -0500 Subject: [PATCH 1/6] add format "full-json" to Plotly.toImage and Plotly.dowloadImage --- src/plot_api/to_image.js | 12 +++++++++++- src/snapshot/helpers.js | 6 ++++++ test/jasmine/tests/download_test.js | 6 ++++++ test/jasmine/tests/toimage_test.js | 24 ++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 55639a1c0e3..1db9ba31ecb 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -11,6 +11,7 @@ var isNumeric = require('fast-isnumeric'); var plotApi = require('./plot_api'); +var plots = require('../plots/plots'); var Lib = require('../lib'); var helpers = require('../snapshot/helpers'); @@ -20,7 +21,7 @@ var svgToImg = require('../snapshot/svgtoimg'); 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,6 +171,15 @@ function toImage(gd, opts) { var width = clonedGd._fullLayout.width; var height = clonedGd._fullLayout.height; + if(format === 'full-json') { + var json = plots.graphJson(clonedGd, false, 'keepdata', false, true); + if(imageDataOnly) { + return resolve(json); + } else { + return resolve(helpers.encodeJSON(json)); + } + } + plotApi.purge(clonedGd); document.body.removeChild(clonedGd); 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/test/jasmine/tests/download_test.js b/test/jasmine/tests/download_test.js index adee0c911ef..310a242ccd7 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) { diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index 45252af0aa8..36e3daee666 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -263,4 +263,28 @@ describe('Plotly.toImage', function() { done(); }); }); + + describe('with format `full-json`', function() { + var imgOpts = {format: 'full-json', imageDataOnly: true}; + it('export a graph div', function(done) { + Plotly.plot(gd, [{y: [1, 2, 3]}]) + .then(function() { return Plotly.toImage('graph', imgOpts);}) + .then(function(fig) { + fig = JSON.parse(fig); + expect(fig.data[0].mode).toBe('lines+markers', 'contain default mode'); + }) + .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); + expect(fig.data[0].mode).toBe('lines+markers', 'contain default mode'); + }) + .catch(failTest) + .then(done); + }); + }); }); From b2eb326d8c6b44ac168edf6c2e59f6b153d228b8 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 19 Feb 2020 18:03:34 -0500 Subject: [PATCH 2/6] Plotly.toImage: format "full-json" now contains config and version key --- src/plot_api/to_image.js | 14 +++++++++++--- src/plots/plots.js | 20 +++++++++++++------- test/jasmine/tests/toimage_test.js | 17 ++++++++++++++++- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 1db9ba31ecb..22dc849abfd 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -17,6 +17,7 @@ var Lib = require('../lib'); var helpers = require('../snapshot/helpers'); var toSVG = require('../snapshot/tosvg'); var svgToImg = require('../snapshot/svgtoimg'); +var version = require('../core').version; var attrs = { format: { @@ -171,8 +172,16 @@ function toImage(gd, opts) { 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', false, true); + var json = plots.graphJson(clonedGd, false, 'keepdata', 'object', true, true); + json.version = version; + json = JSON.stringify(json); + cleanup(); if(imageDataOnly) { return resolve(json); } else { @@ -180,8 +189,7 @@ function toImage(gd, opts) { } } - plotApi.purge(clonedGd); - document.body.removeChild(clonedGd); + cleanup(); if(format === 'svg') { if(imageDataOnly) { diff --git a/src/plots/plots.js b/src/plots/plots.js index 55fd49714ca..151bfb05e3d 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,9 +2051,9 @@ 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 = {}; @@ -2060,8 +2061,11 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { for(v in d) { // remove private elements and functions // _ is for private, [ is a mistake ie [object Object] - if(typeof d[v] === 'function' || - ['_', '['].indexOf(v.charAt(0)) !== -1) { + if(['_', '['].indexOf(v.charAt(0)) !== -1) continue; + + // if a function, add if necessary then move on + if(typeof d[v] === 'function') { + if(keepFunction) o[v] = '_function'; continue; } @@ -2091,13 +2095,13 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) { } // 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/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index 36e3daee666..be4bc062ca8 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -266,12 +266,23 @@ describe('Plotly.toImage', function() { 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() { return Plotly.toImage('graph', imgOpts);}) + .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); @@ -281,7 +292,11 @@ describe('Plotly.toImage', function() { 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); From 6a8ac6528599f31f1dbb8917c12fb76221dd866c Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 19 Feb 2020 22:33:25 -0500 Subject: [PATCH 3/6] create version.js to easily get version number without circularity --- src/assets/geo_assets.js | 3 +-- src/core.js | 3 +-- src/plot_api/to_image.js | 2 +- src/version.js | 12 ++++++++++++ tasks/preprocess.js | 3 +-- tasks/util/constants.js | 1 + 6 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 src/version.js diff --git a/src/assets/geo_assets.js b/src/assets/geo_assets.js index 223b0b654fa..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.2'; +exports.version = require('../version').version; exports.topojson = saneTopojson; diff --git a/src/core.js b/src/core.js index c7c2c9891d6..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.2'; +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 22dc849abfd..11491416f01 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -17,7 +17,7 @@ var Lib = require('../lib'); var helpers = require('../snapshot/helpers'); var toSVG = require('../snapshot/tosvg'); var svgToImg = require('../snapshot/svgtoimg'); -var version = require('../core').version; +var version = require('../version').version; var attrs = { format: { diff --git a/src/version.js b/src/version.js new file mode 100644 index 00000000000..9dd2c9702e5 --- /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.2'; 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'), From e7131d5af756db694181d8c28165ced464c26625 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 11 Mar 2020 00:38:58 -0400 Subject: [PATCH 4/6] downloads in "full-json" produce filename with .json extension --- src/snapshot/download.js | 2 +- test/jasmine/tests/download_test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/test/jasmine/tests/download_test.js b/test/jasmine/tests/download_test.js index 310a242ccd7..6b7473419df 100644 --- a/test/jasmine/tests/download_test.js +++ b/test/jasmine/tests/download_test.js @@ -209,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); }); } From 08fd4992989bf2218a6bc448a88cf41816a9c3fb Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 11 Mar 2020 00:43:01 -0400 Subject: [PATCH 5/6] bump src/version.js from 1.52.2 to 1.52.3 --- src/version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.js b/src/version.js index 9dd2c9702e5..8cd3f7238a6 100644 --- a/src/version.js +++ b/src/version.js @@ -9,4 +9,4 @@ 'use strict'; // package version injected by `npm run preprocess` -exports.version = '1.52.2'; +exports.version = '1.52.3'; From c60990c82d50fc23496aca686938b696c2c776d3 Mon Sep 17 00:00:00 2001 From: Antoine Roy-Gobeil Date: Wed, 11 Mar 2020 15:25:42 -0400 Subject: [PATCH 6/6] plots.graphJson: sort object's keys --- src/plots/plots.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plots/plots.js b/src/plots/plots.js index 151bfb05e3d..85d32db040a 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -2057,23 +2057,23 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults, includeConfi } 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(['_', '['].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'; - continue; + 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. @@ -2082,7 +2082,7 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults, includeConfi src = d[v + 'src']; if(typeof src === 'string' && src.indexOf(':') > 0) { if(!Lib.isPlainObject(d.stream)) { - continue; + return; } } } else if(mode !== 'keepall') { @@ -2090,13 +2090,13 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults, includeConfi // 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], keepFunction); - } + }); return o; }