diff --git a/build/plotcss.js b/build/plotcss.js index f6e9e453cdd..d7697da8f75 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -35,12 +35,12 @@ var rules = { "X .ease-bg": "-webkit-transition:background-color 0.3s ease 0s;-moz-transition:background-color 0.3s ease 0s;-ms-transition:background-color 0.3s ease 0s;-o-transition:background-color 0.3s ease 0s;transition:background-color 0.3s ease 0s;", "X .modebar--hover>:not(.watermark)": "opacity:0;-webkit-transition:opacity 0.3s ease 0s;-moz-transition:opacity 0.3s ease 0s;-ms-transition:opacity 0.3s ease 0s;-o-transition:opacity 0.3s ease 0s;transition:opacity 0.3s ease 0s;", "X:hover .modebar--hover .modebar-group": "opacity:1;", - "X .modebar-group": "float:left;display:inline-block;box-sizing:border-box;margin-left:8px;position:relative;vertical-align:middle;white-space:nowrap;", + "X .modebar-group": "float:left;display:inline-block;box-sizing:border-box;padding-left:8px;position:relative;vertical-align:middle;white-space:nowrap;", "X .modebar-btn": "position:relative;font-size:16px;padding:3px 4px;height:22px;cursor:pointer;line-height:normal;box-sizing:border-box;", "X .modebar-btn svg": "position:relative;top:2px;", "X .modebar.vertical": "display:flex;flex-direction:column;flex-wrap:wrap;align-content:flex-end;max-height:100%;", "X .modebar.vertical svg": "top:-1px;", - "X .modebar.vertical .modebar-group": "display:block;float:none;margin-left:0px;margin-bottom:8px;", + "X .modebar.vertical .modebar-group": "display:block;float:none;padding-left:0px;padding-bottom:8px;", "X .modebar.vertical .modebar-group .modebar-btn": "display:block;text-align:center;", "X [data-title]:before,X [data-title]:after": "position:absolute;-webkit-transform:translate3d(0, 0, 0);-moz-transform:translate3d(0, 0, 0);-ms-transform:translate3d(0, 0, 0);-o-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);display:none;opacity:0;z-index:1001;pointer-events:none;top:110%;right:50%;", "X [data-title]:hover:before,X [data-title]:hover:after": "display:block;opacity:1;", diff --git a/src/components/modebar/modebar.js b/src/components/modebar/modebar.js index 78d5591575b..682780a79b9 100644 --- a/src/components/modebar/modebar.js +++ b/src/components/modebar/modebar.js @@ -64,7 +64,7 @@ proto.update = function(graphInfo, buttons) { var bgSelector = context.displayModeBar === 'hover' ? '.js-plotly-plot .plotly:hover ' : ''; Lib.deleteRelatedStyleRule(modeBarId); - Lib.addRelatedStyleRule(modeBarId, bgSelector + '#' + modeBarId, 'background-color: ' + style.bgcolor); + Lib.addRelatedStyleRule(modeBarId, bgSelector + '#' + modeBarId + ' .modebar-group', 'background-color: ' + style.bgcolor); Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn .icon path', 'fill: ' + style.color); Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn:hover .icon path', 'fill: ' + style.activecolor); Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn.active .icon path', 'fill: ' + style.activecolor); @@ -333,7 +333,7 @@ function createModeBar(gd, buttons) { var modeBar = new ModeBar({ graphInfo: gd, - container: fullLayout._paperdiv.node(), + container: fullLayout._modebardiv.node(), buttons: buttons }); diff --git a/src/css/_modebar.scss b/src/css/_modebar.scss index f648113e6ed..78af40d29c5 100644 --- a/src/css/_modebar.scss +++ b/src/css/_modebar.scss @@ -22,7 +22,7 @@ float: left; display: inline-block; box-sizing: border-box; - margin-left: 8px; + padding-left: 8px; position: relative; vertical-align: middle; white-space: nowrap; @@ -60,8 +60,8 @@ .modebar-group { display: block; float: none; - margin-left: 0px; - margin-bottom: 8px; + padding-left: 0px; + padding-bottom: 8px; .modebar-btn { display: block; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index e950d614426..8db0e3b340a 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -244,8 +244,6 @@ exports.plot = function(gd, data, layout, config) { 'position': 'absolute', 'top': 0, 'left': 0, - 'width': '100%', - 'height': '100%', 'overflow': 'visible', 'pointer-events': 'none' }); @@ -279,6 +277,16 @@ exports.plot = function(gd, data, layout, config) { } } + if(fullLayout.modebar.orientation === 'h') { + fullLayout._modebardiv + .style('height', null) + .style('width', '100%'); + } else { + fullLayout._modebardiv + .style('width', null) + .style('height', fullLayout.height + 'px'); + } + return Plots.previousPromises(gd); } @@ -3845,5 +3853,13 @@ function makePlotFramework(gd) { fullLayout._zoomlayer = fullLayout._toppaper.append('g').classed('zoomlayer', true); fullLayout._hoverlayer = fullLayout._toppaper.append('g').classed('hoverlayer', true); + // Make the modebar container + fullLayout._modebardiv = fullLayout._paperdiv.selectAll('.modebar-container').data([0]); + fullLayout._modebardiv.enter().append('div') + .classed('modebar-container', true) + .style('position', 'absolute') + .style('top', '0px') + .style('right', '0px'); + gd.emit('plotly_framework'); } diff --git a/test/jasmine/tests/config_test.js b/test/jasmine/tests/config_test.js index d414bd33593..cdfaf2d5d7c 100644 --- a/test/jasmine/tests/config_test.js +++ b/test/jasmine/tests/config_test.js @@ -551,230 +551,232 @@ describe('config argument', function() { }); }); - describe('responsive figure', function() { - var gd; - var data = [{type: 'scatter', x: [1, 2, 3, 4], y: [5, 10, 2, 8]}]; - var width = 960; - var height = 800; - - var parent, elWidth, elHeight; - - beforeEach(function() { - viewport.set(width, height); - - // Prepare a parent container that fills the viewport - parent = document.createElement('div'); - parent.style.width = '100vw'; - parent.style.height = '100vh'; - parent.style.position = 'fixed'; - parent.style.top = '0'; - parent.style.left = '0'; - }); - - afterEach(function() { - Plotly.purge(gd); // Needed to remove all event listeners - document.body.removeChild(parent); - viewport.reset(); - }); + ['scatter', 'scattergl'].forEach(function(traceType) { + describe('responsive ' + traceType + ' trace', function() { + var gd; + var data = [{type: traceType, x: [1, 2, 3, 4], y: [5, 10, 2, 8]}]; + var width = 960; + var height = 800; + + var parent, elWidth, elHeight; + + beforeEach(function() { + viewport.set(width, height); + + // Prepare a parent container that fills the viewport + parent = document.createElement('div'); + parent.style.width = '100vw'; + parent.style.height = '100vh'; + parent.style.position = 'fixed'; + parent.style.top = '0'; + parent.style.left = '0'; + }); - function checkLayoutSize(width, height) { - expect(gd._fullLayout.width).toBe(width); - expect(gd._fullLayout.height).toBe(height); - } + afterEach(function() { + Plotly.purge(gd); // Needed to remove all event listeners + document.body.removeChild(parent); + viewport.reset(); + }); - function checkElementsSize(nodeList, width, height) { - var i; - for(i = 0; i < nodeList.length; i++) { - var domRect = nodeList[i].getBoundingClientRect(); - expect(domRect.width).toBe(width); - expect(domRect.height).toBe(height); - expect(+nodeList[i].getAttribute('width')).toBe(width); - expect(+nodeList[i].getAttribute('height')).toBe(height); + function checkLayoutSize(width, height) { + expect(gd._fullLayout.width).toBe(width); + expect(gd._fullLayout.height).toBe(height); } - } - - function testResponsive() { - checkLayoutSize(elWidth, elHeight); - viewport.set(width / 2, height / 2); - return Promise.resolve() - .then(delay(RESIZE_DELAY)) - .then(function() { - checkLayoutSize(elWidth / 2, elHeight / 2); + function checkElementsSize(nodeList, width, height) { + var i; + for(i = 0; i < nodeList.length; i++) { + var domRect = nodeList[i].getBoundingClientRect(); + expect(domRect.width).toBe(width); + expect(domRect.height).toBe(height); + expect(+nodeList[i].getAttribute('width')).toBe(width); + expect(+nodeList[i].getAttribute('height')).toBe(height); + } + } - var mainSvgs = document.getElementsByClassName('main-svg'); - checkElementsSize(mainSvgs, elWidth / 2, elHeight / 2); + function testResponsive() { + checkLayoutSize(elWidth, elHeight); + viewport.set(width / 2, height / 2); - var canvases = document.getElementsByTagName('canvas'); - checkElementsSize(canvases, elWidth / 2, elHeight / 2); + return Promise.resolve() + .then(delay(RESIZE_DELAY)) + .then(function() { + checkLayoutSize(elWidth / 2, elHeight / 2); - }) - .catch(failTest); - } + var mainSvgs = document.getElementsByClassName('main-svg'); + checkElementsSize(mainSvgs, elWidth / 2, elHeight / 2); - function fillParent(numRows, numCols, cb) { - elWidth = width / numCols, elHeight = height / numRows; + var canvases = document.getElementsByTagName('canvas'); + checkElementsSize(canvases, elWidth / 2, elHeight / 2); - // Fill parent - for(var i = 0; i < (numCols * numRows); i++) { - var col = document.createElement('div'); - col.style.height = '100%'; - col.style.width = '100%'; - if(typeof(cb) === typeof(Function)) cb.call(col, i); - parent.appendChild(col); + }) + .catch(failTest); } - document.body.appendChild(parent); - gd = parent.childNodes[0]; - } - it('@flaky should resize when the viewport width/height changes', function(done) { - fillParent(1, 1); - Plotly.plot(gd, data, {}, {responsive: true}) - .then(testResponsive) - .then(done); - }); - - it('@flaky should still be responsive if the plot is edited', function(done) { - fillParent(1, 1); - Plotly.plot(gd, data, {}, {responsive: true}) - .then(function() {return Plotly.restyle(gd, 'y[0]', data[0].y[0] + 2);}) - .then(testResponsive) - .then(done); - }); + function fillParent(numRows, numCols, cb) { + elWidth = width / numCols, elHeight = height / numRows; + + // Fill parent + for(var i = 0; i < (numCols * numRows); i++) { + var col = document.createElement('div'); + col.style.height = '100%'; + col.style.width = '100%'; + if(typeof(cb) === typeof(Function)) cb.call(col, i); + parent.appendChild(col); + } + document.body.appendChild(parent); + gd = parent.childNodes[0]; + } - it('@flaky should still be responsive if the plot is purged and replotted', function(done) { - fillParent(1, 1); - Plotly.plot(gd, data, {}, {responsive: true}) - .then(function() {return Plotly.newPlot(gd, data, {}, {responsive: true});}) - .then(testResponsive) - .then(done); - }); + it('@flaky should resize when the viewport width/height changes', function(done) { + fillParent(1, 1); + Plotly.plot(gd, data, {}, {responsive: true}) + .then(testResponsive) + .then(done); + }); - it('@flaky should only have one resize handler when plotted more than once', function(done) { - fillParent(1, 1); - var cntWindowResize = 0; - window.addEventListener('resize', function() {cntWindowResize++;}); - spyOn(Plotly.Plots, 'resize').and.callThrough(); + it('@flaky should still be responsive if the plot is edited', function(done) { + fillParent(1, 1); + Plotly.plot(gd, data, {}, {responsive: true}) + .then(function() {return Plotly.restyle(gd, 'y[0]', data[0].y[0] + 2);}) + .then(testResponsive) + .then(done); + }); - Plotly.plot(gd, data, {}, {responsive: true}) - .then(function() {return Plotly.restyle(gd, 'y[0]', data[0].y[0] + 2);}) - .then(function() {viewport.set(width / 2, width / 2);}) - .then(delay(RESIZE_DELAY)) - // .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('@flaky should still be responsive if the plot is purged and replotted', function(done) { + fillParent(1, 1); + Plotly.plot(gd, data, {}, {responsive: true}) + .then(function() {return Plotly.newPlot(gd, data, {}, {responsive: true});}) + .then(testResponsive) + .then(done); + }); - it('@flaky should become responsive if configured as such via Plotly.react', function(done) { - fillParent(1, 1); - Plotly.plot(gd, data, {}, {responsive: false}) - .then(function() {return Plotly.react(gd, data, {}, {responsive: true});}) - .then(testResponsive) - .then(done); - }); + it('@flaky should only have one resize handler when plotted more than once', function(done) { + fillParent(1, 1); + 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(width / 2, width / 2);}) + .then(delay(RESIZE_DELAY)) + // .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('@flaky should stop being responsive if configured as such via Plotly.react', function(done) { - fillParent(1, 1); - Plotly.plot(gd, data, {}, {responsive: true}) - // Check initial size - .then(function() {checkLayoutSize(width, height);}) - // Turn off responsiveness - .then(function() {return Plotly.react(gd, data, {}, {responsive: false});}) - // Resize viewport - .then(function() {viewport.set(width / 2, height / 2);}) - // Wait for resize to happen (Plotly.resize has an internal timeout) - .then(delay(RESIZE_DELAY)) - // Check that final figure's size hasn't changed - .then(function() {checkLayoutSize(width, height);}) - .catch(failTest) - .then(done); - }); + it('@flaky should become responsive if configured as such via Plotly.react', function(done) { + fillParent(1, 1); + Plotly.plot(gd, data, {}, {responsive: false}) + .then(function() {return Plotly.react(gd, data, {}, {responsive: true});}) + .then(testResponsive) + .then(done); + }); - // Testing fancier CSS layouts - it('@flaky should resize horizontally in a flexbox when responsive: true', function(done) { - parent.style.display = 'flex'; - parent.style.flexDirection = 'row'; - fillParent(1, 2, function() { - this.style.flexGrow = '1'; + it('@flaky should stop being responsive if configured as such via Plotly.react', function(done) { + fillParent(1, 1); + Plotly.plot(gd, data, {}, {responsive: true}) + // Check initial size + .then(function() {checkLayoutSize(width, height);}) + // Turn off responsiveness + .then(function() {return Plotly.react(gd, data, {}, {responsive: false});}) + // Resize viewport + .then(function() {viewport.set(width / 2, height / 2);}) + // Wait for resize to happen (Plotly.resize has an internal timeout) + .then(delay(RESIZE_DELAY)) + // Check that final figure's size hasn't changed + .then(function() {checkLayoutSize(width, height);}) + .catch(failTest) + .then(done); }); - Plotly.plot(gd, data, {}, { responsive: true }) - .then(testResponsive) - .then(done); - }); + // Testing fancier CSS layouts + it('@flaky should resize horizontally in a flexbox when responsive: true', function(done) { + parent.style.display = 'flex'; + parent.style.flexDirection = 'row'; + fillParent(1, 2, function() { + this.style.flexGrow = '1'; + }); - it('@flaky should resize vertically in a flexbox when responsive: true', function(done) { - parent.style.display = 'flex'; - parent.style.flexDirection = 'column'; - fillParent(2, 1, function() { - this.style.flexGrow = '1'; + Plotly.plot(gd, data, {}, { responsive: true }) + .then(testResponsive) + .then(done); }); - Plotly.plot(gd, data, {}, { responsive: true }) - .then(testResponsive) - .then(done); - }); - - it('@flaky should resize in both direction in a grid when responsive: true', function(done) { - var numCols = 2; - var numRows = 2; - parent.style.display = 'grid'; - parent.style.gridTemplateColumns = 'repeat(' + numCols + ', 1fr)'; - parent.style.gridTemplateRows = 'repeat(' + numRows + ', 1fr)'; - fillParent(numRows, numCols); + it('@flaky should resize vertically in a flexbox when responsive: true', function(done) { + parent.style.display = 'flex'; + parent.style.flexDirection = 'column'; + fillParent(2, 1, function() { + this.style.flexGrow = '1'; + }); - Plotly.plot(gd, data, {}, { responsive: true }) - .then(testResponsive) - .then(done); - }); + Plotly.plot(gd, data, {}, { responsive: true }) + .then(testResponsive) + .then(done); + }); - it('@flaky should provide a fixed non-zero width/height when autosize/responsive: true and container\' size is zero', function(done) { - fillParent(1, 1, function() { - this.style.display = 'inline-block'; - this.style.width = null; - this.style.height = null; + it('@flaky should resize in both direction in a grid when responsive: true', function(done) { + var numCols = 2; + var numRows = 2; + parent.style.display = 'grid'; + parent.style.gridTemplateColumns = 'repeat(' + numCols + ', 1fr)'; + parent.style.gridTemplateRows = 'repeat(' + numRows + ', 1fr)'; + fillParent(numRows, numCols); + + Plotly.plot(gd, data, {}, { responsive: true }) + .then(testResponsive) + .then(done); }); - Plotly.plot(gd, data, {autosize: true}, {responsive: true}) - .then(function() { - checkLayoutSize(700, 450); - expect(gd.clientWidth).toBe(700); - expect(gd.clientHeight).toBe(450); - }) - .then(function() { - return Plotly.newPlot(gd, data, {autosize: true}, {responsive: true}); - }) - // It is important to test newPlot to make sure an initially zero size container - // is still considered to have zero size after a plot is drawn into it. - .then(function() { - checkLayoutSize(700, 450); - expect(gd.clientWidth).toBe(700); - expect(gd.clientHeight).toBe(450); - }) - .catch(failTest) - .then(done); - }); - // The following test is to guarantee we're not breaking the existing (although maybe not ideal) behaviour. - // In a future version, one may prefer responsive/autosize:true winning over an explicit width/height when embedded in a webpage. - it('@flaky should use the explicitly provided width/height even if autosize/responsive:true', function(done) { - fillParent(1, 1, function() { - this.style.width = '1000px'; - this.style.height = '500px'; + it('@flaky should provide a fixed non-zero width/height when autosize/responsive: true and container\' size is zero', function(done) { + fillParent(1, 1, function() { + this.style.display = 'inline-block'; + this.style.width = null; + this.style.height = null; + }); + Plotly.plot(gd, data, {autosize: true}, {responsive: true}) + .then(function() { + checkLayoutSize(700, 450); + expect(gd.clientWidth).toBe(700); + expect(gd.clientHeight).toBe(450); + }) + .then(function() { + return Plotly.newPlot(gd, data, {autosize: true}, {responsive: true}); + }) + // It is important to test newPlot to make sure an initially zero size container + // is still considered to have zero size after a plot is drawn into it. + .then(function() { + checkLayoutSize(700, 450); + expect(gd.clientWidth).toBe(700); + expect(gd.clientHeight).toBe(450); + }) + .catch(failTest) + .then(done); }); - Plotly.plot(gd, data, {autosize: true, width: 1200, height: 700}, {responsive: true}) - .then(function() { - expect(gd.clientWidth).toBe(1000); - expect(gd.clientHeight).toBe(500); - // The plot should overflow the container! - checkLayoutSize(1200, 700); - }) - .catch(failTest) - .then(done); + // The following test is to guarantee we're not breaking the existing (although maybe not ideal) behaviour. + // In a future version, one may prefer responsive/autosize:true winning over an explicit width/height when embedded in a webpage. + it('@flaky should use the explicitly provided width/height even if autosize/responsive:true', function(done) { + fillParent(1, 1, function() { + this.style.width = '1000px'; + this.style.height = '500px'; + }); + + Plotly.plot(gd, data, {autosize: true, width: 1200, height: 700}, {responsive: true}) + .then(function() { + expect(gd.clientWidth).toBe(1000); + expect(gd.clientHeight).toBe(500); + // The plot should overflow the container! + checkLayoutSize(1200, 700); + }) + .catch(failTest) + .then(done); + }); }); }); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 6a16e2c3236..d4b84ed6bb5 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -27,12 +27,19 @@ describe('ModeBar', function() { return parent; } + function getMockModeBarTree() { + var el = document.createElement('div'); + el.className = 'modebar-container'; + return el; + } + function getMockGraphInfo(xaxes, yaxes) { return { _fullLayout: { _uid: '6ea6a7', dragmode: 'zoom', _paperdiv: d3.select(getMockContainerTree()), + _modebardiv: d3.select(getMockModeBarTree()), _has: Plots._hasPlotType, _subplots: {xaxis: xaxes || [], yaxis: yaxes || []}, modebar: { @@ -1349,13 +1356,13 @@ describe('ModeBar', function() { it('changes background color (displayModeBar: hover)', function(done) { Plotly.plot(gd, [], {modebar: { bgcolor: colors[0]}}) .then(function() { - style = window.getComputedStyle(gd._fullLayout._modeBar.element); + style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group')); expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)'); expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[0]); }) .then(function() { return Plotly.relayout(gd, 'modebar.bgcolor', colors[1]); }) .then(function() { - style = window.getComputedStyle(gd._fullLayout._modeBar.element); + style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group')); expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)'); expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[1]); }) @@ -1366,13 +1373,13 @@ describe('ModeBar', function() { it('changes background color (displayModeBar: true)', function(done) { Plotly.plot(gd, [], {modebar: {bgcolor: colors[0]}}, {displayModeBar: true}) .then(function() { - style = window.getComputedStyle(gd._fullLayout._modeBar.element); + style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group')); expect(style.backgroundColor).toBe(colors[0]); expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[0]); }) .then(function() { return Plotly.relayout(gd, 'modebar.bgcolor', colors[1]); }) .then(function() { - style = window.getComputedStyle(gd._fullLayout._modeBar.element); + style = window.getComputedStyle(gd._fullLayout._modeBar.element.querySelector('.modebar-group')); expect(style.backgroundColor).toBe(colors[1]); expect(getStyleRule().rules[3].style.backgroundColor).toBe(colors[1]); })