From 642ddbaee517bed535e69208cc8097233f5a6e82 Mon Sep 17 00:00:00 2001 From: timelyportfolio Date: Wed, 11 May 2016 13:33:40 -0500 Subject: [PATCH 1/7] add new promise-based toImage and downloadImage to Plotly --- .eslintrc | 2 +- src/components/modebar/buttons.js | 50 ++-------- src/core.js | 2 + src/plot_api/to_image.js | 112 ++++++++++++++++++++++ src/snapshot/download.js | 64 +++++++++++++ src/snapshot/filesaver.js | 66 +++++++++++++ src/snapshot/index.js | 3 +- src/snapshot/svgtoimg.js | 140 ++++++++++++++++++---------- test/jasmine/tests/download_test.js | 88 +++++++++++++++++ test/jasmine/tests/toimage_test.js | 112 ++++++++++++++++++++++ 10 files changed, 546 insertions(+), 93 deletions(-) create mode 100644 src/plot_api/to_image.js create mode 100644 src/snapshot/download.js create mode 100644 src/snapshot/filesaver.js create mode 100644 test/jasmine/tests/download_test.js create mode 100644 test/jasmine/tests/toimage_test.js diff --git a/.eslintrc b/.eslintrc index 5f0ec7491e4..330bf09f50f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -38,7 +38,7 @@ "no-floating-decimal": [2], "space-infix-ops": [0, {"int32Hint": false}], "quotes": [2, "single"], - "dot-notation": [2, {"allowKeywords": false}], + "dot-notation": [2], "operator-linebreak": [2, "after"], "eqeqeq": [2], "new-cap": [0], diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index ec8db20a371..7ec0277d822 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -12,7 +12,7 @@ var Plotly = require('../../plotly'); var Lib = require('../../lib'); var setCursor = require('../../lib/setcursor'); -var Snapshot = require('../../snapshot'); +var downloadImage = require('../../snapshot/download'); var Icons = require('../../../build/ploticon'); @@ -49,49 +49,15 @@ modeBarButtons.toImage = { title: 'Download plot as a png', icon: Icons.camera, click: function(gd) { - var format = 'png'; - - if(Lib.isIE()) { - Lib.notifier('Snapshotting is unavailable in Internet Explorer. ' + - 'Consider exporting your images using the Plotly Cloud', 'long'); - return; - } - - if(gd._snapshotInProgress) { - Lib.notifier('Snapshotting is still in progress - please hold', 'long'); - return; - } - - gd._snapshotInProgress = true; Lib.notifier('Taking snapshot - this may take a few seconds', 'long'); - var ev = Snapshot.toImage(gd, {format: format}); - - var filename = gd.fn || 'newplot'; - filename += '.' + format; - - ev.once('success', function(result) { - gd._snapshotInProgress = false; - - var downloadLink = document.createElement('a'); - downloadLink.href = result; - downloadLink.download = filename; // only supported by FF and Chrome - - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); - - ev.clean(); - }); - - ev.once('error', function(err) { - gd._snapshotInProgress = false; - - Lib.notifier('Sorry there was a problem downloading your ' + format, 'long'); - console.error(err); - - ev.clean(); - }); + downloadImage(gd) + .then(function(filename) { + Lib.notifier('Snapshot succeeded - ' + filename, 'long'); + }) + .catch(function() { + Lib.notifier('Sorry there was a problem downloading your snapshot', 'long'); + }); } }; diff --git a/src/core.js b/src/core.js index 9f6efc44329..120dc0ded8d 100644 --- a/src/core.js +++ b/src/core.js @@ -31,6 +31,8 @@ exports.moveTraces = Plotly.moveTraces; exports.purge = Plotly.purge; exports.setPlotConfig = require('./plot_api/set_plot_config'); exports.register = Plotly.register; +exports.toImage = require('./plot_api/to_image'); +exports.downloadImage = require('./snapshot/download'); // plot icons exports.Icons = require('../build/ploticon'); diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js new file mode 100644 index 00000000000..4dec3442089 --- /dev/null +++ b/src/plot_api/to_image.js @@ -0,0 +1,112 @@ +/** +* Copyright 2012-2016, 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'; + +var Plotly = require('../plotly'); + +var isNumeric = require('fast-isnumeric'); + +/** + * @param {object} gd figure Object + * @param {object} opts option object + * @param opts.format 'jpeg' | 'png' | 'webp' | 'svg' + * @param opts.width width of snapshot in px + * @param opts.height height of snapshot in px + */ +function toImage(gd, opts) { + var Snapshot = require('../snapshot'); + + var promise = new Promise(function(resolve, reject) { + // check for undefined opts + opts = opts || {}; + // default to png + opts.format = opts.format || 'png'; + + var isSizeGood = function(size) { + // undefined and null are valid options + if(size === undefined || size === null) { + return true; + } + + if(isNumeric(size) && size > 1) { + return true; + } + }; + + if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) { + reject(new Error('Height and width should be pixel values.')); + } + + // first clone the GD so we can operate in a clean environment + var clone = Snapshot.clone(gd, {format: 'png', height: opts.height, width: opts.width}); + var clonedGd = clone.td; + + // put the cloned div somewhere off screen before attaching to DOM + clonedGd.style.position = 'absolute'; + clonedGd.style.left = '-5000px'; + document.body.appendChild(clonedGd); + + function wait() { + var delay = Snapshot.getDelay(clonedGd._fullLayout); + + return new Promise(function(resolve, reject) { + setTimeout(function() { + var svg = Snapshot.toSVG(clonedGd); + + var canvasContainer = window.document.createElement('div'); + var canvas = window.document.createElement('canvas'); + + // window.document.body.appendChild(canvasContainer); + canvasContainer.appendChild(canvas); + + canvasContainer.id = Plotly.Lib.randstr(); + canvas.id = Plotly.Lib.randstr(); + + Snapshot.svgToImg({ + format: opts.format, + width: clonedGd._fullLayout.width, + height: clonedGd._fullLayout.height, + canvas: canvas, + svg: svg, + // ask svgToImg to return a Promise + // rather than EventEmitter + // leave EventEmitter for backward + // compatibility + promise: true + }).then(function(url) { + if(clonedGd) clonedGd.remove(); + resolve(url); + }).catch(function(err) { + reject(err); + }); + }, delay); + }); + } + + var redrawFunc = Snapshot.getRedrawFunc(clonedGd); + + Plotly.plot(clonedGd, clone.data, clone.layout, clone.config) + // TODO: the following is Plotly.Plots.redrawText but without the waiting. + // we shouldn't need to do this, but in *occasional* cases we do. Figure + // out why and take it out. + + // not sure the above TODO makes sense anymore since + // we have converted to promises + .then(redrawFunc) + .then(wait) + .then(function(url) { resolve(url); }) + .catch(function(err) { + reject(err); + }); + }); + + return promise; +} + +module.exports = toImage; diff --git a/src/snapshot/download.js b/src/snapshot/download.js new file mode 100644 index 00000000000..cc508cedffe --- /dev/null +++ b/src/snapshot/download.js @@ -0,0 +1,64 @@ +/** +* Copyright 2012-2016, 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'; + +var toImage = require('../plot_api/to_image'); +var Lib = require('../lib'); // for isIE +var fileSaver = require('./filesaver'); + +/** + * @param {object} gd figure Object + * @param {object} opts option object + * @param opts.format 'jpeg' | 'png' | 'webp' | 'svg' + * @param opts.width width of snapshot in px + * @param opts.height height of snapshot in px + * @param opts.filename name of file excluding extension + */ +function downloadImage(gd, opts) { + + // check for undefined opts + opts = opts || {}; + + // default to png + opts.format = opts.format || 'png'; + + return new Promise(function(resolve,reject) { + if(gd._snapshotInProgress) { + reject(new Error('Snapshotting already in progress.')); + } + + // see comments within svgtoimg for additional + // discussion of problems with IE + // can now draw to canvas, but CORS tainted canvas + // does not allow toDataURL + // svg format will work though + if(Lib.isIE() && opts.format !== 'svg') { + reject(new Error('Sorry IE does not support downloading from canvas.')); + } + + gd._snapshotInProgress = true; + var promise = toImage(gd, opts); + + var filename = opts.filename || gd.fn || 'newplot'; + filename += '.' + opts.format; + + promise.then(function(result) { + gd._snapshotInProgress = false; + return fileSaver(result,filename); + }).then(function(name) { + resolve(name); + }).catch(function(err) { + gd._snapshotInProgress = false; + reject(err); + }); + }); +} + +module.exports = downloadImage; diff --git a/src/snapshot/filesaver.js b/src/snapshot/filesaver.js new file mode 100644 index 00000000000..61bac46aff0 --- /dev/null +++ b/src/snapshot/filesaver.js @@ -0,0 +1,66 @@ +/** +* Copyright 2012-2016, 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. +*/ + +/* +* substantial portions of this code from FileSaver.js +* https://github.com/eligrey/FileSaver.js +* License: https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md +* FileSaver.js +* A saveAs() FileSaver implementation. +* 1.1.20160328 +* +* By Eli Grey, http://eligrey.com +* License: MIT +* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md +*/ + +'use strict'; + +var fileSaver = function(url, name) { + var saveLink = document.createElement('a'); + var canUseSaveLink = 'download' in saveLink; + var isSafari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent); + var promise = new Promise(function(resolve, reject) { + // IE <10 is explicitly unsupported + if(typeof navigator !== 'undefined' && /MSIE [1-9]\./.test(navigator.userAgent)) { + reject(new Error('IE < 10 unsupported')); + } + + // First try a.download, then web filesystem, then object URLs + if(isSafari) { + // Safari doesn't allow downloading of blob urls + document.location.href = 'data:attachment/file' + url.slice(url.search(/[,;]/)); + resolve(name); + } + + if(!name) { + name = 'download'; + } + + if(canUseSaveLink) { + saveLink.href = url; + saveLink.download = name; + document.body.appendChild(saveLink); + saveLink.click(); + document.body.removeChild(saveLink); + resolve(name); + } + + // IE 10+ (native saveAs) + if(typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) { + navigator.msSaveOrOpenBlob(url, name); + resolve(name); + } + + reject(new Error('download error')); + }); + + return promise; +}; + +module.exports = fileSaver; diff --git a/src/snapshot/index.js b/src/snapshot/index.js index 86d3dd104ca..15473249544 100644 --- a/src/snapshot/index.js +++ b/src/snapshot/index.js @@ -36,7 +36,8 @@ var Snapshot = { clone: require('./cloneplot'), toSVG: require('./tosvg'), svgToImg: require('./svgtoimg'), - toImage: require('./toimage') + toImage: require('./toimage'), + downloadImage: require('./download') }; module.exports = Snapshot; diff --git a/src/snapshot/svgtoimg.js b/src/snapshot/svgtoimg.js index 8dc868cb937..2ee21309943 100644 --- a/src/snapshot/svgtoimg.js +++ b/src/snapshot/svgtoimg.js @@ -6,63 +6,105 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; +var Lib = require('../lib'); var EventEmitter = require('events').EventEmitter; function svgToImg(opts) { - var ev = opts.emitter ? opts.emitter : new EventEmitter(); - - var Image = window.Image; - var Blob = window.Blob; - - var svg = opts.svg; - var format = opts.format || 'png'; - var canvas = opts.canvas; - - var ctx = canvas.getContext('2d'); - var img = new Image(); - var DOMURL = window.URL || window.webkitURL; - var svgBlob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'}); - var url = DOMURL.createObjectURL(svgBlob); - - canvas.height = opts.height || 150; - canvas.width = opts.width || 300; - - img.onload = function() { - var imgData; - - DOMURL.revokeObjectURL(url); - ctx.drawImage(img, 0, 0); - - switch(format) { - case 'jpeg': - imgData = canvas.toDataURL('image/jpeg'); - break; - case 'png': - imgData = canvas.toDataURL('image/png'); - break; - case 'webp': - imgData = canvas.toDataURL('image/webp'); - break; - case 'svg': - imgData = svg; - break; - default: - return ev.emit('error', 'Image format is not jpeg, png or svg'); + var ev = opts.emitter || new EventEmitter(); + + var promise = new Promise(function(resolve, reject) { + + var Image = window.Image; + + var svg = opts.svg; + var format = opts.format || 'png'; + var canvas = opts.canvas; + + var ctx = canvas.getContext('2d'); + var img = new Image(); + + // IE is very strict, so we will need to clean + // svg with the following regex + // yes this is messy, but do not know a better way + // Even with this IE will not work due to tainted canvas + // see https://github.com/kangax/fabric.js/issues/1957 + // http://stackoverflow.com/questions/18112047/canvas-todataurl-working-in-all-browsers-except-ie10 + // Leave here just in case the CORS/tainted IE issue gets resolved + if(Lib.isIE()) { + // replace double quote with single quote + svg = svg.replace(/"/gi,"'"); + // url in svg are single quoted + // since we changed double to single + // we'll need to change these to double-quoted + svg = svg.replace(/(\('#)(.*)('\))/gi,'(\"$2\")'); + // font names with spaces will be escaped single-quoted + // we'll need to change these to double-quoted + svg = svg.replace(/(\\')/gi,'\"'); } - ev.emit('success', imgData); - }; - - img.onerror = function(err) { - DOMURL.revokeObjectURL(url); - return ev.emit('error', err); - }; - - img.src = url; + // for Safari support, eliminate createObjectURL + // this decision could cause problems if content + // is not restricted to svg + var url = 'data:image/svg+xml,' + encodeURIComponent(svg); + + canvas.height = opts.height || 150; + canvas.width = opts.width || 300; + + img.onload = function() { + var imgData; + + ctx.drawImage(img, 0, 0); + + switch(format) { + case 'jpeg': + imgData = canvas.toDataURL('image/jpeg'); + break; + case 'png': + imgData = canvas.toDataURL('image/png'); + break; + case 'webp': + imgData = canvas.toDataURL('image/webp'); + break; + case 'svg': + imgData = svg; + break; + default: + reject(new Error('Image format is not jpeg, png or svg')); + // eventually remove the ev + // in favor of promises + if(!opts.promise) { + return ev.emit('error', 'Image format is not jpeg, png or svg'); + } + } + resolve(imgData); + // eventually remove the ev + // in favor of promises + if(!opts.promise) { + ev.emit('success', imgData); + } + }; + + img.onerror = function(err) { + reject(err); + // eventually remove the ev + // in favor of promises + if(!opts.promise) { + return ev.emit('error', err); + } + }; + + img.src = url; + }); + + // temporary for backward compatibility + // move to only Promise in 2.0.0 + // and eliminate the EventEmitter + if(opts.promise) { + return promise; + } return ev; } diff --git a/test/jasmine/tests/download_test.js b/test/jasmine/tests/download_test.js new file mode 100644 index 00000000000..f072e8918c6 --- /dev/null +++ b/test/jasmine/tests/download_test.js @@ -0,0 +1,88 @@ +var Plotly = require('@lib/index'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var textchartMock = require('@mocks/text_chart_arrays.json'); + +describe('Plotly.downloadImage', function() { + 'use strict'; + var gd; + var originalTimeout; + + // override click handler on createElement + // so these tests will not actually + // download an image each time they are run + // full credit goes to @etpinard; thanks + var createElement = document.createElement; + beforeAll(function() { + document.createElement = function(args) { + var el = createElement.call(document, args); + el.click = function() {}; + return el; + }; + }); + + afterAll(function() { + document.createElement = createElement; + }); + + beforeEach(function() { + gd = createGraphDiv(); + + // downloadImage can take a little longer + // so give it a little more time to finish + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; + }); + + afterEach(function() { + destroyGraphDiv(); + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + }); + + it('should be attached to Plotly', function() { + expect(Plotly.downloadImage).toBeDefined(); + }); + + it('should create link, remove link, accept options', function(done) { + //use MutationObserver to monitor the DOM + //for changes + //code modeled after + //https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver + // select the target node + var target = document.body; + var domchanges = []; + + // create an observer instance + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + domchanges.push(mutation); + }); + }); + + Plotly.plot(gd, textchartMock.data, textchartMock.layout).then(function(gd) { + // start observing dom + // configuration of the observer: + var config = { childList: true }; + + // pass in the target node and observer options + observer.observe(target, config); + + return Plotly.downloadImage(gd, {format: 'jpeg', height: 300, width: 300, filename: 'plotly_download'}); + }).then(function(filename) { + // stop observing + observer.disconnect(); + // look for an added and removed link + var linkadded = domchanges[domchanges.length-2].addedNodes[0].outerHTML; + var linkdeleted = domchanges[domchanges.length-1].removedNodes[0].outerHTML; + + // check for a Date: Wed, 11 May 2016 16:42:05 -0500 Subject: [PATCH 3/7] painfully work through more IE bugs with snapshot --- src/components/modebar/buttons.js | 6 +++--- src/plot_api/to_image.js | 4 +++- src/snapshot/filesaver.js | 4 ++-- src/snapshot/svgtoimg.js | 6 +++++- src/snapshot/toimage.js | 2 +- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 5f3d106c2ce..b3805db0780 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -53,17 +53,17 @@ modeBarButtons.toImage = { Lib.notifier('Taking snapshot - this may take a few seconds', 'long'); - if(Lib.isIE()){ + if(Lib.isIE()) { Lib.notifier('IE only supports svg. Changing format to svg.', 'long'); format = 'svg'; } - downloadImage(gd, {'format':format}) + downloadImage(gd, {'format': format}) .then(function(filename) { Lib.notifier('Snapshot succeeded - ' + filename, 'long'); }) .catch(function() { - Lib.notifier('Sorry there was a problem downloading your snapshot', 'long'); + Lib.notifier('Sorry there was a problem downloading your snapshot!', 'long'); }); } }; diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 4dec3442089..3cca59059ea 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -37,6 +37,8 @@ function toImage(gd, opts) { if(isNumeric(size) && size > 1) { return true; } + + return false; }; if(!isSizeGood(opts.width) || !isSizeGood(opts.height)) { @@ -80,7 +82,7 @@ function toImage(gd, opts) { // compatibility promise: true }).then(function(url) { - if(clonedGd) clonedGd.remove(); + if(clonedGd) document.body.removeChild(clonedGd); resolve(url); }).catch(function(err) { reject(err); diff --git a/src/snapshot/filesaver.js b/src/snapshot/filesaver.js index 61bac46aff0..e1f44f5c80f 100644 --- a/src/snapshot/filesaver.js +++ b/src/snapshot/filesaver.js @@ -52,8 +52,8 @@ var fileSaver = function(url, name) { } // IE 10+ (native saveAs) - if(typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob) { - navigator.msSaveOrOpenBlob(url, name); + if(typeof navigator !== 'undefined' && navigator.msSaveBlob) { + navigator.msSaveBlob(new Blob([url]), name); resolve(name); } diff --git a/src/snapshot/svgtoimg.js b/src/snapshot/svgtoimg.js index 109125a5128..b0e40b240cb 100644 --- a/src/snapshot/svgtoimg.js +++ b/src/snapshot/svgtoimg.js @@ -56,7 +56,11 @@ function svgToImg(opts) { img.onload = function() { var imgData; - ctx.drawImage(img, 0, 0); + // don't need to draw to canvas if svg + // save some time and also avoid failure on IE + if(format !== 'svg') { + ctx.drawImage(img, 0, 0); + } switch(format) { case 'jpeg': diff --git a/src/snapshot/toimage.js b/src/snapshot/toimage.js index 5eac155d5af..d4aef39dfc7 100644 --- a/src/snapshot/toimage.js +++ b/src/snapshot/toimage.js @@ -57,7 +57,7 @@ function toImage(gd, opts) { }); ev.clean = function() { - if(clonedGd) clonedGd.remove(); + if(clonedGd) document.body.removeChild(clonedGd); }; }, delay); From 649e067a5f04cd09cd1764eec12292466886d798 Mon Sep 17 00:00:00 2001 From: timelyportfolio Date: Thu, 12 May 2016 15:36:53 -0500 Subject: [PATCH 4/7] reject with error in `svgtoimg` in IE if not `format:'svg'` --- src/snapshot/download.js | 2 +- src/snapshot/svgtoimg.js | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/snapshot/download.js b/src/snapshot/download.js index 405f78608fd..f7a72fad9fd 100644 --- a/src/snapshot/download.js +++ b/src/snapshot/download.js @@ -40,7 +40,7 @@ function downloadImage(gd, opts) { // does not allow toDataURL // svg format will work though if(Lib.isIE() && opts.format !== 'svg') { - reject(new Error('Sorry IE does not support downloading from canvas. Try {format:"svg"} instead.')); + reject(new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.')); } gd._snapshotInProgress = true; diff --git a/src/snapshot/svgtoimg.js b/src/snapshot/svgtoimg.js index b0e40b240cb..146259e633e 100644 --- a/src/snapshot/svgtoimg.js +++ b/src/snapshot/svgtoimg.js @@ -21,10 +21,6 @@ function svgToImg(opts) { var svg = opts.svg; var format = opts.format || 'png'; - var canvas = opts.canvas; - - var ctx = canvas.getContext('2d'); - var img = new Image(); // IE is very strict, so we will need to clean // svg with the following regex @@ -43,8 +39,25 @@ function svgToImg(opts) { // font names with spaces will be escaped single-quoted // we'll need to change these to double-quoted svg = svg.replace(/(\\')/gi,'\"'); + // IE only support svg + if(format!=='svg') { + var ieSvgError = new Error('Sorry IE does not support downloading from canvas. Try {format:\'svg\'} instead.'); + reject(ieSvgError); + // eventually remove the ev + // in favor of promises + if(!opts.promise) { + return ev.emit('error', ieSvgError); + } else { + return promise; + } + } } + var canvas = opts.canvas; + + var ctx = canvas.getContext('2d'); + var img = new Image(); + // for Safari support, eliminate createObjectURL // this decision could cause problems if content // is not restricted to svg From 0f5022e34e8e024b9f3eeb2b70e5e15ab0839808 Mon Sep 17 00:00:00 2001 From: timelyportfolio Date: Sat, 14 May 2016 08:12:25 -0500 Subject: [PATCH 5/7] fix promise check in toimage_test --- test/jasmine/tests/toimage_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/toimage_test.js b/test/jasmine/tests/toimage_test.js index 79b9d57060c..6f39302d644 100644 --- a/test/jasmine/tests/toimage_test.js +++ b/test/jasmine/tests/toimage_test.js @@ -23,7 +23,7 @@ describe('Plotly.toImage', function() { it('should return a promise', function(done) { function isPromise(x) { - return !!x.then || typeof x.then === 'function'; + return !!x.then && typeof x.then === 'function'; } var returnValue = Plotly.plot(gd, subplotMock.data, subplotMock.layout) From 5f8877feed276531a664d1f41dcc1ca2022ca411 Mon Sep 17 00:00:00 2001 From: timelyportfolio Date: Mon, 16 May 2016 11:23:29 -0500 Subject: [PATCH 6/7] change safari download to `application/octet-stream' --- src/snapshot/filesaver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snapshot/filesaver.js b/src/snapshot/filesaver.js index e1f44f5c80f..fb45882ac3d 100644 --- a/src/snapshot/filesaver.js +++ b/src/snapshot/filesaver.js @@ -34,7 +34,7 @@ var fileSaver = function(url, name) { // First try a.download, then web filesystem, then object URLs if(isSafari) { // Safari doesn't allow downloading of blob urls - document.location.href = 'data:attachment/file' + url.slice(url.search(/[,;]/)); + document.location.href = 'data:application/octet-stream' + url.slice(url.search(/[,;]/)); resolve(name); } From 05a24486f3576fc2662fb9d81c5d3e7a1144b824 Mon Sep 17 00:00:00 2001 From: timelyportfolio Date: Mon, 16 May 2016 13:51:00 -0500 Subject: [PATCH 7/7] remove unnecessary comment --- src/plot_api/to_image.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index 3cca59059ea..049ea559e2b 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -64,7 +64,6 @@ function toImage(gd, opts) { var canvasContainer = window.document.createElement('div'); var canvas = window.document.createElement('canvas'); - // window.document.body.appendChild(canvasContainer); canvasContainer.appendChild(canvas); canvasContainer.id = Plotly.Lib.randstr();