Skip to content

Commit cfc720b

Browse files
authored
Merge pull request #2974 from plotly/2969-responsive-charts
add support for responsive charts
2 parents 70fcaad + 1262172 commit cfc720b

File tree

10 files changed

+190
-1
lines changed

10 files changed

+190
-1
lines changed

package-lock.json

+38
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"karma-jasmine-spec-tags": "^1.0.1",
143143
"karma-spec-reporter": "0.0.32",
144144
"karma-verbose-reporter": "0.0.6",
145+
"karma-viewport": "^1.0.2",
145146
"madge": "^3.2.0",
146147
"minify-stream": "^1.2.0",
147148
"minimist": "^1.2.0",

src/lib/clear_responsive.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright 2012-2018, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
/**
12+
* Clear responsive handlers (if any).
13+
*
14+
* @param {DOM node or object} gd : graph div object
15+
*/
16+
module.exports = function clearResponsive(gd) {
17+
if(gd._responsiveChartHandler) {
18+
window.removeEventListener('resize', gd._responsiveChartHandler);
19+
delete gd._responsiveChartHandler;
20+
}
21+
};

src/lib/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ lib.clearThrottle = throttleModule.clear;
128128

129129
lib.getGraphDiv = require('./get_graph_div');
130130

131+
lib.clearResponsive = require('./clear_responsive');
132+
131133
lib.makeTraceGroups = require('./make_trace_groups');
132134

133135
lib._ = require('./localize');

src/plot_api/plot_api.js

+13
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,19 @@ exports.plot = function(gd, data, layout, config) {
191191
gd.calcdata[i][0].trace = gd._fullData[i];
192192
}
193193

194+
// make the figure responsive
195+
if(gd._context.responsive) {
196+
if(!gd._responsiveChartHandler) {
197+
// Keep a reference to the resize handler to purge it down the road
198+
gd._responsiveChartHandler = function() {Plots.resize(gd);};
199+
200+
// Listen to window resize
201+
window.addEventListener('resize', gd._responsiveChartHandler);
202+
}
203+
} else {
204+
Lib.clearResponsive(gd);
205+
}
206+
194207
/*
195208
* start async-friendly code - now we're actually drawing things
196209
*/

src/plot_api/plot_config.js

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ module.exports = {
5757
*/
5858
autosizable: false,
5959

60+
// responsive: determines whether to change the layout size when window is resized
61+
responsive: false,
62+
6063
// set the length of the undo/redo queue
6164
queueLength: 0,
6265

src/plots/plots.js

+3
Original file line numberDiff line numberDiff line change
@@ -1542,6 +1542,9 @@ plots.purge = function(gd) {
15421542
// remove any planned throttles
15431543
Lib.clearThrottle();
15441544

1545+
// remove responsive handler
1546+
Lib.clearResponsive(gd);
1547+
15451548
// data and layout
15461549
delete gd.data;
15471550
delete gd.layout;

test/jasmine/.eslintrc

+3
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
"env": {
44
"browser": true,
55
"jasmine": true
6+
},
7+
"globals": {
8+
"viewport": true
69
}
710
}

test/jasmine/karma.conf.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ func.defaultConfig = {
137137

138138
// frameworks to use
139139
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
140-
frameworks: ['jasmine', 'jasmine-spec-tags', 'browserify'],
140+
frameworks: ['jasmine', 'jasmine-spec-tags', 'browserify', 'viewport'],
141141

142142
// list of files / patterns to load in the browser
143143
//

test/jasmine/tests/config_test.js

+105
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var destroyGraphDiv = require('../assets/destroy_graph_div');
66
var click = require('../assets/click');
77
var mouseEvent = require('../assets/mouse_event');
88
var failTest = require('../assets/fail_test');
9+
var delay = require('../assets/delay');
910

1011
describe('config argument', function() {
1112

@@ -529,4 +530,108 @@ describe('config argument', function() {
529530
});
530531
});
531532
});
533+
534+
describe('responsive figure', function() {
535+
var gd;
536+
var startWidth = 960, startHeight = 400;
537+
var newWidth = 400, newHeight = 700;
538+
var data = [{x: [1, 2, 3, 4], y: [5, 10, 2, 8]}];
539+
540+
beforeEach(function() {
541+
viewport.set(startWidth, startHeight);
542+
gd = createGraphDiv();
543+
544+
// Make the graph fill the parent
545+
gd.style.width = '100%';
546+
gd.style.height = '100%';
547+
});
548+
549+
afterEach(function() {
550+
Plotly.purge(gd); // Needed to remove all event listeners
551+
destroyGraphDiv();
552+
viewport.reset();
553+
});
554+
555+
function checkLayoutSize(width, height) {
556+
expect(gd._fullLayout.width).toBe(width);
557+
expect(gd._fullLayout.height).toBe(height);
558+
559+
var svg = document.getElementsByClassName('main-svg')[0];
560+
expect(+svg.getAttribute('width')).toBe(width);
561+
expect(+svg.getAttribute('height')).toBe(height);
562+
}
563+
564+
function testResponsive() {
565+
checkLayoutSize(startWidth, startHeight);
566+
viewport.set(newWidth, newHeight);
567+
568+
return Promise.resolve()
569+
.then(delay(200))
570+
.then(function() {
571+
checkLayoutSize(newWidth, newHeight);
572+
})
573+
.catch(failTest);
574+
}
575+
576+
it('should resize when the viewport width/height changes', function(done) {
577+
Plotly.plot(gd, data, {}, {responsive: true})
578+
.then(testResponsive)
579+
.then(done);
580+
});
581+
582+
it('should still be responsive if the plot is edited', function(done) {
583+
Plotly.plot(gd, data, {}, {responsive: true})
584+
.then(function() {return Plotly.restyle(gd, 'y[0]', data[0].y[0] + 2);})
585+
.then(testResponsive)
586+
.then(done);
587+
});
588+
589+
it('should still be responsive if the plot is purged and replotted', function(done) {
590+
Plotly.plot(gd, data, {}, {responsive: true})
591+
.then(function() {return Plotly.newPlot(gd, data, {}, {responsive: true});})
592+
.then(testResponsive)
593+
.then(done);
594+
});
595+
596+
it('should only have one resize handler when plotted more than once', function(done) {
597+
var cntWindowResize = 0;
598+
window.addEventListener('resize', function() {cntWindowResize++;});
599+
spyOn(Plotly.Plots, 'resize').and.callThrough();
600+
601+
Plotly.plot(gd, data, {}, {responsive: true})
602+
.then(function() {return Plotly.restyle(gd, 'y[0]', data[0].y[0] + 2);})
603+
.then(function() {viewport.set(newWidth, newHeight);})
604+
.then(delay(200))
605+
// .then(function() {viewport.set(newWidth, 2 * newHeight);}).then(delay(200))
606+
.then(function() {
607+
expect(cntWindowResize).toBe(1);
608+
expect(Plotly.Plots.resize.calls.count()).toBe(1);
609+
})
610+
.catch(failTest)
611+
.then(done);
612+
});
613+
614+
it('should become responsive if configured as such via Plotly.react', function(done) {
615+
Plotly.plot(gd, data, {}, {responsive: false})
616+
.then(function() {return Plotly.react(gd, data, {}, {responsive: true});})
617+
.then(testResponsive)
618+
.then(done);
619+
});
620+
621+
it('should stop being responsive if configured as such via Plotly.react', function(done) {
622+
Plotly.plot(gd, data, {}, {responsive: true})
623+
// Check initial size
624+
.then(function() {checkLayoutSize(startWidth, startHeight);})
625+
// Turn off responsiveness
626+
.then(function() {return Plotly.react(gd, data, {}, {responsive: false});})
627+
// Resize viewport
628+
.then(function() {viewport.set(newWidth, newHeight);})
629+
// Wait for resize to happen (Plotly.resize has an internal timeout)
630+
.then(delay(200))
631+
// Check that final figure's size hasn't changed
632+
.then(function() {checkLayoutSize(startWidth, startHeight);})
633+
.catch(failTest)
634+
.then(done);
635+
});
636+
});
532637
});

0 commit comments

Comments
 (0)