diff --git a/package-lock.json b/package-lock.json index 2ec7d9ec2bf..8b17ba63fc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,6 +124,28 @@ "d3-interpolate": "1" } }, + "@types/bluebird": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.24.tgz", + "integrity": "sha512-YeQoDpq4Lm8ppSBqAnAeF/xy1cYp/dMTif2JFcvmAbETMRlvKHT2iLcWu+WyYiJO3b3Ivokwo7EQca/xfLVJmg==", + "dev": true + }, + "@types/karma": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@types/karma/-/karma-1.7.6.tgz", + "integrity": "sha512-VLyBOU0SmMjGTUpuZvPOzoR0GIKMGcYueGz803V55lbkI4oGsLG03rYP43kJowh9vNNkVERrDYYoFoTbMftzFw==", + "dev": true, + "requires": { + "@types/bluebird": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", + "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==", + "dev": true + }, "JSONStream": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz", @@ -6187,6 +6209,12 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "jsonschema": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", + "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==", + "dev": true + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6308,6 +6336,16 @@ "colors": ">=1.0" } }, + "karma-viewport": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/karma-viewport/-/karma-viewport-1.0.2.tgz", + "integrity": "sha512-rvzY9UTVXHPt9QRwawyh1D50qzqtxRsoPe5svgOY5kvV7eigv8e5dcW3RSmPZ6m/3Hx+QwJEMY+kLhCmamKJ6A==", + "dev": true, + "requires": { + "@types/karma": "^1.7.3", + "jsonschema": "^1.1.1" + } + }, "kdbush": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-1.0.1.tgz", diff --git a/package.json b/package.json index 4a966f70a1a..e0f42fb32e0 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "karma-jasmine-spec-tags": "^1.0.1", "karma-spec-reporter": "0.0.32", "karma-verbose-reporter": "0.0.6", + "karma-viewport": "^1.0.2", "madge": "^3.2.0", "minify-stream": "^1.2.0", "minimist": "^1.2.0", diff --git a/src/lib/clear_responsive.js b/src/lib/clear_responsive.js new file mode 100644 index 00000000000..1fbfb7848f1 --- /dev/null +++ b/src/lib/clear_responsive.js @@ -0,0 +1,21 @@ +/** +* Copyright 2012-2018, 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'; + +/** + * Clear responsive handlers (if any). + * + * @param {DOM node or object} gd : graph div object + */ +module.exports = function clearResponsive(gd) { + if(gd._responsiveChartHandler) { + window.removeEventListener('resize', gd._responsiveChartHandler); + delete gd._responsiveChartHandler; + } +}; diff --git a/src/lib/index.js b/src/lib/index.js index cd9f7e2ad6b..10839689e54 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -118,6 +118,8 @@ lib.clearThrottle = throttleModule.clear; lib.getGraphDiv = require('./get_graph_div'); +lib.clearResponsive = require('./clear_responsive'); + lib.makeTraceGroups = require('./make_trace_groups'); lib._ = require('./localize'); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 9804f2d50ff..cd899de782c 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -191,6 +191,19 @@ exports.plot = function(gd, data, layout, config) { gd.calcdata[i][0].trace = gd._fullData[i]; } + // make the figure responsive + if(gd._context.responsive) { + if(!gd._responsiveChartHandler) { + // Keep a reference to the resize handler to purge it down the road + gd._responsiveChartHandler = function() {Plots.resize(gd);}; + + // Listen to window resize + window.addEventListener('resize', gd._responsiveChartHandler); + } + } else { + Lib.clearResponsive(gd); + } + /* * start async-friendly code - now we're actually drawing things */ diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index 596f0bf43c3..574b618e0a7 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -57,6 +57,9 @@ module.exports = { */ autosizable: false, + // responsive: determines whether to change the layout size when window is resized + responsive: false, + // set the length of the undo/redo queue queueLength: 0, diff --git a/src/plots/plots.js b/src/plots/plots.js index 908289fa72c..2ec8811a917 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1536,6 +1536,9 @@ plots.purge = function(gd) { // remove any planned throttles Lib.clearThrottle(); + // remove responsive handler + Lib.clearResponsive(gd); + // data and layout delete gd.data; delete gd.layout; diff --git a/test/jasmine/.eslintrc b/test/jasmine/.eslintrc index d177999d069..d57cd856c99 100644 --- a/test/jasmine/.eslintrc +++ b/test/jasmine/.eslintrc @@ -3,5 +3,8 @@ "env": { "browser": true, "jasmine": true + }, + "globals": { + "viewport": true } } diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js index c46ac22b472..66bce39e73c 100644 --- a/test/jasmine/karma.conf.js +++ b/test/jasmine/karma.conf.js @@ -137,7 +137,7 @@ func.defaultConfig = { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ['jasmine', 'jasmine-spec-tags', 'browserify'], + frameworks: ['jasmine', 'jasmine-spec-tags', 'browserify', 'viewport'], // list of files / patterns to load in the browser // diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index 041aa96ae82..3b360d93dc5 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -6,6 +6,7 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var click = require('../assets/click'); var mouseEvent = require('../assets/mouse_event'); var failTest = require('../assets/fail_test'); +var delay = require('../assets/delay'); describe('config argument', function() { @@ -529,4 +530,108 @@ describe('config argument', function() { }); }); }); + + describe('responsive figure', function() { + var gd; + var startWidth = 960, startHeight = 400; + var newWidth = 400, newHeight = 700; + var data = [{x: [1, 2, 3, 4], y: [5, 10, 2, 8]}]; + + beforeEach(function() { + viewport.set(startWidth, startHeight); + gd = createGraphDiv(); + + // Make the graph fill the parent + gd.style.width = '100%'; + gd.style.height = '100%'; + }); + + afterEach(function() { + Plotly.purge(gd); // Needed to remove all event listeners + destroyGraphDiv(); + viewport.reset(); + }); + + function checkLayoutSize(width, height) { + expect(gd._fullLayout.width).toBe(width); + expect(gd._fullLayout.height).toBe(height); + + var svg = document.getElementsByClassName('main-svg')[0]; + expect(+svg.getAttribute('width')).toBe(width); + expect(+svg.getAttribute('height')).toBe(height); + } + + function testResponsive() { + checkLayoutSize(startWidth, startHeight); + viewport.set(newWidth, newHeight); + + return Promise.resolve() + .then(delay(200)) + .then(function() { + checkLayoutSize(newWidth, newHeight); + }) + .catch(failTest); + } + + it('should resize when the viewport width/height changes', function(done) { + Plotly.plot(gd, data, {}, {responsive: true}) + .then(testResponsive) + .then(done); + }); + + it('should still be responsive if the plot is edited', function(done) { + Plotly.plot(gd, data, {}, {responsive: true}) + .then(function() {return Plotly.restyle(gd, 'y[0]', data[0].y[0] + 2);}) + .then(testResponsive) + .then(done); + }); + + it('should still be responsive if the plot is purged and replotted', function(done) { + Plotly.plot(gd, data, {}, {responsive: true}) + .then(function() {return Plotly.newPlot(gd, data, {}, {responsive: true});}) + .then(testResponsive) + .then(done); + }); + + it('should only have one resize handler when plotted more than once', function(done) { + var cntWindowResize = 0; + window.addEventListener('resize', function() {cntWindowResize++;}); + spyOn(Plotly.Plots, 'resize').and.callThrough(); + + Plotly.plot(gd, data, {}, {responsive: true}) + .then(function() {return Plotly.restyle(gd, 'y[0]', data[0].y[0] + 2);}) + .then(function() {viewport.set(newWidth, newHeight);}) + .then(delay(200)) + // .then(function() {viewport.set(newWidth, 2 * newHeight);}).then(delay(200)) + .then(function() { + expect(cntWindowResize).toBe(1); + expect(Plotly.Plots.resize.calls.count()).toBe(1); + }) + .catch(failTest) + .then(done); + }); + + it('should become responsive if configured as such via Plotly.react', function(done) { + Plotly.plot(gd, data, {}, {responsive: false}) + .then(function() {return Plotly.react(gd, data, {}, {responsive: true});}) + .then(testResponsive) + .then(done); + }); + + it('should stop being responsive if configured as such via Plotly.react', function(done) { + Plotly.plot(gd, data, {}, {responsive: true}) + // Check initial size + .then(function() {checkLayoutSize(startWidth, startHeight);}) + // Turn off responsiveness + .then(function() {return Plotly.react(gd, data, {}, {responsive: false});}) + // Resize viewport + .then(function() {viewport.set(newWidth, newHeight);}) + // Wait for resize to happen (Plotly.resize has an internal timeout) + .then(delay(200)) + // Check that final figure's size hasn't changed + .then(function() {checkLayoutSize(startWidth, startHeight);}) + .catch(failTest) + .then(done); + }); + }); });