diff --git a/src/lib/prepare_regl.js b/src/lib/prepare_regl.js index 228d134208a..13931e4eaa4 100644 --- a/src/lib/prepare_regl.js +++ b/src/lib/prepare_regl.js @@ -8,6 +8,8 @@ 'use strict'; +var showNoWebGlMsg = require('./show_no_webgl_msg'); + // Note that this module should be ONLY required into // files corresponding to regl trace modules // so that bundles with non-regl only don't include @@ -21,23 +23,35 @@ var createRegl = require('regl'); * * @param {DOM node or object} gd : graph div object * @param {array} extensions : list of extension to pass to createRegl + * + * @return {boolean} true if all createRegl calls succeeded, false otherwise */ module.exports = function prepareRegl(gd, extensions) { var fullLayout = gd._fullLayout; + var success = true; fullLayout._glcanvas.each(function(d) { if(d.regl) return; // only parcoords needs pick layer if(d.pick && !fullLayout._has('parcoords')) return; - d.regl = createRegl({ - canvas: this, - attributes: { - antialias: !d.pick, - preserveDrawingBuffer: true - }, - pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio, - extensions: extensions || [] - }); + try { + d.regl = createRegl({ + canvas: this, + attributes: { + antialias: !d.pick, + preserveDrawingBuffer: true + }, + pixelRatio: gd._context.plotGlPixelRatio || global.devicePixelRatio, + extensions: extensions || [] + }); + } catch(e) { + success = false; + } }); + + if(!success) { + showNoWebGlMsg({container: fullLayout._glcontainer.node()}); + } + return success; }; diff --git a/src/lib/show_no_webgl_msg.js b/src/lib/show_no_webgl_msg.js index 34545151b1a..65349670b76 100644 --- a/src/lib/show_no_webgl_msg.js +++ b/src/lib/show_no_webgl_msg.js @@ -21,7 +21,7 @@ var noop = function() {}; * Expects 'scene' to have property 'container' * */ -module.exports = function showWebGlMsg(scene) { +module.exports = function showNoWebGlMsg(scene) { for(var prop in scene) { if(typeof scene[prop] === 'function') scene[prop] = noop; } @@ -31,11 +31,26 @@ module.exports = function showWebGlMsg(scene) { }; var div = document.createElement('div'); - div.textContent = 'Webgl is not supported by your browser - visit https://get.webgl.org for more info'; + div.className = 'no-webgl'; div.style.cursor = 'pointer'; div.style.fontSize = '24px'; div.style.color = Color.defaults[0]; - + div.style.position = 'absolute'; + div.style.left = div.style.top = '0px'; + div.style.width = div.style.height = '100%'; + div.style['background-color'] = Color.lightLine; + div.style['z-index'] = 30; + + var p = document.createElement('p'); + p.textContent = 'WebGL is not supported by your browser - visit https://get.webgl.org for more info'; + p.style.position = 'relative'; + p.style.top = '50%'; + p.style.left = '50%'; + p.style.height = '30%'; + p.style.width = '50%'; + p.style.margin = '-15% 0 0 -25%'; + + div.appendChild(p); scene.container.appendChild(div); scene.container.style.background = '#FFFFFF'; scene.container.onclick = function() { diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index 1ab225ded52..9bc5db60cf2 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -45,6 +45,7 @@ function Scene2D(options, fullLayout) { this.updateRefs(fullLayout); this.makeFramework(); + if(this.stopped) return; // update options this.glplotOptions = createOptions(this); @@ -121,7 +122,11 @@ proto.makeFramework = function() { premultipliedAlpha: true }); - if(!gl) showNoWebGlMsg(this); + if(!gl) { + showNoWebGlMsg(this); + this.stopped = true; + return; + } this.canvas = liveCanvas; this.gl = gl; diff --git a/src/plots/gl3d/scene.js b/src/plots/gl3d/scene.js index af02713a09c..f85c2077dcc 100644 --- a/src/plots/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -204,7 +204,7 @@ function initializeGLPlot(scene, fullLayout, canvas, gl) { * The destroy method - which will remove the container from the DOM * is overridden with a function that removes the container only. */ - showNoWebGlMsg(scene); + return showNoWebGlMsg(scene); } var relayoutCallback = function(scene) { diff --git a/src/plots/plots.js b/src/plots/plots.js index c49f5cb3ae7..497a506c97a 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -682,6 +682,7 @@ plots.cleanPlot = function(newFullData, newFullLayout, oldFullData, oldFullLayou if(hadGl && !hasGl) { if(oldFullLayout._glcontainer !== undefined) { oldFullLayout._glcontainer.selectAll('.gl-canvas').remove(); + oldFullLayout._glcontainer.selectAll('.no-webgl').remove(); oldFullLayout._glcanvas = null; } } diff --git a/src/traces/parcoords/plot.js b/src/traces/parcoords/plot.js index f8a0a485376..aef8c9581c4 100644 --- a/src/traces/parcoords/plot.js +++ b/src/traces/parcoords/plot.js @@ -17,7 +17,8 @@ module.exports = function plot(gd, cdparcoords) { var root = fullLayout._paperdiv; var container = fullLayout._glcontainer; - prepareRegl(gd); + var success = prepareRegl(gd); + if(!success) return; var gdDimensions = {}; var gdDimensionsOriginalOrder = {}; diff --git a/src/traces/scattergl/index.js b/src/traces/scattergl/index.js index 17b2096e37c..81bda45806f 100644 --- a/src/traces/scattergl/index.js +++ b/src/traces/scattergl/index.js @@ -166,7 +166,7 @@ function sceneUpdate(gd, subplot) { var scene = subplot._scene; var fullLayout = gd._fullLayout; - var reset = { + var resetOpts = { // number of traces in subplot, since scene:subplot → 1:1 count: 0, // whether scene requires init hook in plot call (dirty plot call) @@ -181,7 +181,7 @@ function sceneUpdate(gd, subplot) { errorYOptions: [] }; - var first = { + var initOpts = { selectBatch: null, unselectBatch: null, // regl- component stubs, initialized in dirty plot call @@ -193,7 +193,13 @@ function sceneUpdate(gd, subplot) { }; if(!subplot._scene) { - scene = subplot._scene = Lib.extendFlat({}, reset, first); + scene = subplot._scene = {}; + + scene.init = function init() { + Lib.extendFlat(scene, initOpts, resetOpts); + }; + + scene.init(); // apply new option to all regl components (used on drag) scene.update = function update(opt) { @@ -306,7 +312,7 @@ function sceneUpdate(gd, subplot) { // In case if we have scene from the last calc - reset data if(!scene.dirty) { - Lib.extendFlat(scene, reset); + Lib.extendFlat(scene, resetOpts); } return scene; @@ -326,7 +332,12 @@ function plot(gd, subplot, cdata) { var width = fullLayout.width; var height = fullLayout.height; - prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); + var success = prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); + if(!success) { + scene.init(); + return; + } + var regl = fullLayout._glcanvas.data()[0].regl; // that is needed for fills diff --git a/src/traces/splom/base_plot.js b/src/traces/splom/base_plot.js index 10b062e2c38..80bef941fb1 100644 --- a/src/traces/splom/base_plot.js +++ b/src/traces/splom/base_plot.js @@ -24,7 +24,8 @@ function plot(gd) { var _module = Registry.getModule(SPLOM); var splomCalcData = getModuleCalcData(gd.calcdata, _module)[0]; - prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); + var success = prepareRegl(gd, ['ANGLE_instanced_arrays', 'OES_element_index_uint']); + if(!success) return; if(fullLayout._hasOnlyLargeSploms) { drawGrid(gd); @@ -209,7 +210,10 @@ function clean(newFullData, newFullLayout, oldFullData, oldFullLayout, oldCalcda var trace = cd0.trace; var scene = cd0.t._scene; - if(trace.type === 'splom' && scene && scene.matrix) { + if( + trace.type === 'splom' && + scene && scene.matrix && scene.matrix.destroy + ) { scene.matrix.destroy(); cd0.t._scene = null; } diff --git a/test/jasmine/bundle_tests/no_webgl_test.js b/test/jasmine/bundle_tests/no_webgl_test.js new file mode 100644 index 00000000000..29489ba7958 --- /dev/null +++ b/test/jasmine/bundle_tests/no_webgl_test.js @@ -0,0 +1,122 @@ +var Plotly = require('@lib'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); + +describe('Plotly w/o WebGL support:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + function checkNoWebGLMsg(visible) { + var msg = gd.querySelector('div.no-webgl > p'); + if(visible) { + expect(msg.innerHTML.substr(0, 22)).toBe('WebGL is not supported'); + } else { + expect(msg).toBe(null); + } + } + + it('gl3d subplots', function(done) { + Plotly.react(gd, require('@mocks/gl3d_autocolorscale.json')) + .then(function() { + checkNoWebGLMsg(true); + return Plotly.react(gd, require('@mocks/10.json')); + }) + .then(function() { + checkNoWebGLMsg(false); + }) + .catch(failTest) + .then(done); + }); + + it('gl2d subplots', function(done) { + Plotly.react(gd, require('@mocks/gl2d_pointcloud-basic.json')) + .then(function() { + checkNoWebGLMsg(true); + return Plotly.react(gd, require('@mocks/10.json')); + }) + .then(function() { + checkNoWebGLMsg(false); + }) + .catch(failTest) + .then(done); + }); + + it('scattergl subplots', function(done) { + Plotly.react(gd, require('@mocks/gl2d_12.json')) + .then(function() { + checkNoWebGLMsg(true); + return Plotly.react(gd, require('@mocks/10.json')); + }) + .then(function() { + checkNoWebGLMsg(false); + + // one with all regl2d modules + return Plotly.react(gd, [{ + type: 'scattergl', + mode: 'lines+markers', + fill: 'tozerox', + y: [1, 2, 1], + error_x: { value: 10 }, + error_y: { value: 10 } + }]); + }) + .then(function() { + checkNoWebGLMsg(true); + return Plotly.react(gd, require('@mocks/10.json')); + }) + .then(function() { + checkNoWebGLMsg(false); + }) + .catch(failTest) + .then(done); + }); + + it('scatterpolargl subplots', function(done) { + Plotly.react(gd, require('@mocks/glpolar_scatter.json')) + .then(function() { + checkNoWebGLMsg(true); + return Plotly.react(gd, require('@mocks/10.json')); + }) + .then(function() { + checkNoWebGLMsg(false); + }) + .catch(failTest) + .then(done); + }); + + it('splom subplots', function(done) { + Plotly.react(gd, require('@mocks/splom_0.json')) + .then(function() { + checkNoWebGLMsg(true); + return Plotly.react(gd, require('@mocks/10.json')); + }) + .then(function() { + checkNoWebGLMsg(false); + }) + .catch(failTest) + .then(done); + }); + + it('parcoords subplots', function(done) { + Plotly.react(gd, require('@mocks/gl2d_parcoords_2.json')) + .then(function() { + checkNoWebGLMsg(true); + return Plotly.react(gd, require('@mocks/10.json')); + }) + .then(function() { + checkNoWebGLMsg(false); + }) + .catch(failTest) + .then(done); + }); +}); diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index a427ea4a2ef..468eb11b498 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -188,7 +188,8 @@ func.defaultConfig = { flags: [ '--touch-events', '--window-size=' + argv.width + ',' + argv.height, - isCI ? '--ignore-gpu-blacklist' : '' + isCI ? '--ignore-gpu-blacklist' : '', + (isBundleTest && basename(testFileGlob) === 'no_webgl') ? '--disable-webgl' : '' ] }, _Firefox: {