From 519e1a785140f8b68afad17fa92d182ffec48bbf Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 14 Feb 2018 17:59:24 +0100 Subject: [PATCH 01/37] better visual cue of zoom on oppaxis with rangeslider --- src/components/rangeslider/constants.js | 4 ++ src/components/rangeslider/draw.js | 72 +++++++++++++++++++++++-- src/plots/cartesian/axes.js | 4 ++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/components/rangeslider/constants.js b/src/components/rangeslider/constants.js index 0b535b82826..b5e707b668e 100644 --- a/src/components/rangeslider/constants.js +++ b/src/components/rangeslider/constants.js @@ -31,9 +31,13 @@ module.exports = { grabAreaMaxClassName: 'rangeslider-grabarea-max', handleMaxClassName: 'rangeslider-handle-max', + maskMinOppAxisClassName: 'rangeslider-mask-min-opp-axis', + maskMaxOppAxisClassName: 'rangeslider-mask-max-opp-axis', + // style constants maskColor: 'rgba(0,0,0,0.4)', + maskOppColor: 'rgba(0,0,0,0.2)', slideBoxFill: 'transparent', slideBoxCursor: 'ew-resize', diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 2690438d9b1..a9d11cafa7a 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -136,13 +136,23 @@ module.exports = function(gd) { opts._rl = [range0, range1]; + if(oppAxisOpts.fixedrange === false) { + var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangeBeforeZoom[0]), + range1OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangeBeforeZoom[1]), + distOppAxis = range1OppAxis - range0OppAxis; + + opts.d2pOppAxis = function(v) { + return (v - range0OppAxis) / distOppAxis * opts._height; + }; + } + // update inner nodes rangeSlider .call(drawBg, gd, axisOpts, opts) .call(addClipPath, gd, axisOpts, opts) .call(drawRangePlot, gd, axisOpts, opts) - .call(drawMasks, gd, axisOpts, opts) + .call(drawMasks, gd, axisOpts, opts, oppAxisOpts) .call(drawSlideBox, gd, axisOpts, opts) .call(drawGrabbers, gd, axisOpts, opts); @@ -150,7 +160,7 @@ module.exports = function(gd) { setupDragElement(rangeSlider, gd, axisOpts, opts); // update current range - setPixelRange(rangeSlider, gd, axisOpts, opts); + setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts); // update margins @@ -266,13 +276,17 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) { }); } -function setPixelRange(rangeSlider, gd, axisOpts, opts) { +function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { var hw2 = constants.handleWidth / 2; function clamp(v) { return Lib.constrain(v, 0, opts._width); } + function clampOppAxis(v) { + return Lib.constrain(v, 0, opts._height); + } + function clampHandle(v) { return Lib.constrain(v, -hw2, opts._width + hw2); } @@ -291,6 +305,26 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); + if(oppAxisOpts.fixedrange === false) { + var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), + pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); + + rangeSlider.select('rect.' + constants.maskMinOppAxisClassName) + .attr('x', pixelMin) + .attr('height', pixelMinOppAxis) + .attr('width', pixelMax - pixelMin); + + rangeSlider.select('rect.' + constants.maskMaxOppAxisClassName) + .attr('x', pixelMin) + .attr('y', pixelMaxOppAxis) + .attr('height', opts._height - pixelMaxOppAxis) + .attr('width', pixelMax - pixelMin); + + rangeSlider.select('rect.' + constants.slideBoxClassName) + .attr('y', pixelMinOppAxis) + .attr('height', pixelMaxOppAxis - pixelMinOppAxis); + } + // add offset for crispier corners // https://github.com/plotly/plotly.js/pull/1409 var offset = 0.5; @@ -394,7 +428,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: oppAxisOpts.range.slice(), + range: oppAxisOpts._rangeBeforeZoom ? oppAxisOpts._rangeBeforeZoom.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -435,7 +469,7 @@ function filterRangePlotCalcData(calcData, subplotId) { return out; } -function drawMasks(rangeSlider, gd, axisOpts, opts) { +function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName) .data([0]); @@ -459,6 +493,34 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) { maskMax .attr('height', opts._height) .call(Color.fill, constants.maskColor); + + // masks used for oppAxis zoom + if(oppAxisOpts.fixedrange === false) { + var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) + .data([0]); + + maskMinOppAxis.enter().append('rect') + .classed(constants.maskMinOppAxisClassName, true) + .attr('y', 0) + .attr('shape-rendering', 'crispEdges'); + + maskMinOppAxis + .attr('width', opts._width) + .call(Color.fill, constants.maskOppColor); + + var maskMaxOppAxis = rangeSlider.selectAll('rect.' + constants.maskMaxOppAxisClassName) + .data([0]); + + maskMaxOppAxis.enter().append('rect') + .classed(constants.maskMaxOppAxisClassName, true) + .attr('y', 0) + .attr('shape-rendering', 'crispEdges'); + + maskMaxOppAxis + .attr('width', opts._width) + .style('border-top', constants.maskOppBorder) + .call(Color.fill, constants.maskOppColor); + } } function drawSlideBox(rangeSlider, gd, axisOpts, opts) { diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 01f892f748f..21630377a90 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -389,6 +389,10 @@ axes.saveRangeInitial = function(gd, overwrite) { ax._rangeInitial = ax.range.slice(); hasOneAxisChanged = true; } + // store the initial range for the rangeslider if we zoom on oppaxis + if((isNew && ax.fixedrange === false) || (overwrite && hasChanged)) { + ax._rangeBeforeZoom = ax.range.slice(); + } } return hasOneAxisChanged; From 044e6335c8f849b8def6db14a76c09b341c3eae4 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 16 Feb 2018 11:18:17 +0100 Subject: [PATCH 02/37] add a new attributes to rangeslider to switch back to the old behaviour --- src/components/rangeslider/attributes.js | 12 ++++++++++++ src/components/rangeslider/defaults.js | 1 + src/components/rangeslider/draw.js | 12 ++++++------ src/plots/cartesian/axes.js | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index 37268dff026..6d7a2645482 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -89,5 +89,17 @@ module.exports = { 'If visible, perpendicular axes will be set to `fixedrange`' ].join(' ') }, + perpendicularaxesinitialrange: { + valType: 'boolean', + dflt: true, + role: 'style', + editType: 'calc', + description: [ + 'Determine whether the perpendicular axes in the rangeslider', + 'use (or not) their initial range when they are zoomed in the plot', + 'To allow zoom one (or more) perpendicular axe(s) while the rangeslider is visible', + 'you must set the attributes `fixedrange` to *true* on said axe(s).' + ].join(' ') + }, editType: 'calc' }; diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 106934833fa..b52670a3b58 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -37,6 +37,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); + coerce('perpendicularaxesinitialrange'); // to map back range slider (auto) range containerOut._input = containerIn; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index a9d11cafa7a..408acd67dbd 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -136,9 +136,9 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(oppAxisOpts.fixedrange === false) { - var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangeBeforeZoom[0]), - range1OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangeBeforeZoom[1]), + if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false) { + var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[0]), + range1OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[1]), distOppAxis = range1OppAxis - range0OppAxis; opts.d2pOppAxis = function(v) { @@ -305,7 +305,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(oppAxisOpts.fixedrange === false) { + if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false) { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -428,7 +428,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: oppAxisOpts._rangeBeforeZoom ? oppAxisOpts._rangeBeforeZoom.slice() : oppAxisOpts.range.slice(), + range: axisOpts.rangeslider.perpendicularaxesinitialrange ? oppAxisOpts._rangesliderInitialRange.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -495,7 +495,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(oppAxisOpts.fixedrange === false) { + if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false) { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 21630377a90..6a65959437d 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -391,7 +391,7 @@ axes.saveRangeInitial = function(gd, overwrite) { } // store the initial range for the rangeslider if we zoom on oppaxis if((isNew && ax.fixedrange === false) || (overwrite && hasChanged)) { - ax._rangeBeforeZoom = ax.range.slice(); + ax._rangesliderInitialRange = ax.range.slice(); } } From f335258406f3cc34656996272d15daa7d5025c1e Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 16 Feb 2018 11:59:01 +0100 Subject: [PATCH 03/37] fix check when using new data --- src/components/rangeslider/draw.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 408acd67dbd..e016969f6f3 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -136,7 +136,7 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false) { + if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[0]), range1OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[1]), distOppAxis = range1OppAxis - range0OppAxis; @@ -305,7 +305,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false) { + if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -428,7 +428,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: axisOpts.rangeslider.perpendicularaxesinitialrange ? oppAxisOpts._rangesliderInitialRange.slice() : oppAxisOpts.range.slice(), + range: axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange ? oppAxisOpts._rangesliderInitialRange.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -495,7 +495,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false) { + if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); From 707f432bc5165e212acc3616ebed85726817ac23 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 16 Feb 2018 15:17:27 +0100 Subject: [PATCH 04/37] fix default value rangeslider test --- test/jasmine/tests/range_slider_test.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index d88987fde43..f1e2452fdd9 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -461,7 +461,8 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider + _input: layoutIn.xaxis.rangeslider, + perpendicularaxesinitialrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -478,7 +479,8 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider + _input: layoutIn.xaxis.rangeslider, + perpendicularaxesinitialrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -499,7 +501,8 @@ describe('the range slider', function() { thickness: 'invalid', bgcolor: 42, bordercolor: 42, - borderwidth: 'superfat' + borderwidth: 'superfat', + perpendicularaxesinitialrange: null }}}, layoutOut = { xaxis: {} }, expected = { @@ -509,7 +512,8 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider + _input: layoutIn.xaxis.rangeslider, + perpendicularaxesinitialrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -526,7 +530,8 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider + _input: layoutIn.xaxis.rangeslider, + perpendicularaxesinitialrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); From 6a7f652e6973a974870735b4138cbe307feae461 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 16 Feb 2018 18:32:00 +0100 Subject: [PATCH 05/37] renammed new attributes to "fixedyrange" --- src/components/rangeslider/attributes.js | 2 +- src/components/rangeslider/defaults.js | 2 +- src/components/rangeslider/draw.js | 8 ++++---- test/jasmine/tests/range_slider_test.js | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index 6d7a2645482..b71dd3114dc 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -89,7 +89,7 @@ module.exports = { 'If visible, perpendicular axes will be set to `fixedrange`' ].join(' ') }, - perpendicularaxesinitialrange: { + fixedyrange: { valType: 'boolean', dflt: true, role: 'style', diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index b52670a3b58..9475d56e10d 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -37,7 +37,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); - coerce('perpendicularaxesinitialrange'); + coerce('fixedyrange'); // to map back range slider (auto) range containerOut._input = containerIn; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index e016969f6f3..21e41d6e858 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -136,7 +136,7 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { + if(axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[0]), range1OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[1]), distOppAxis = range1OppAxis - range0OppAxis; @@ -305,7 +305,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { + if(axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -428,7 +428,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange ? oppAxisOpts._rangesliderInitialRange.slice() : oppAxisOpts.range.slice(), + range: axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange ? oppAxisOpts._rangesliderInitialRange.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -495,7 +495,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(axisOpts.rangeslider.perpendicularaxesinitialrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { + if(axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index f1e2452fdd9..d58283298f4 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -462,7 +462,7 @@ describe('the range slider', function() { borderwidth: 0, bordercolor: '#444', _input: layoutIn.xaxis.rangeslider, - perpendicularaxesinitialrange: true + fixedyrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -480,7 +480,7 @@ describe('the range slider', function() { borderwidth: 0, bordercolor: '#444', _input: layoutIn.xaxis.rangeslider, - perpendicularaxesinitialrange: true + fixedyrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -502,7 +502,7 @@ describe('the range slider', function() { bgcolor: 42, bordercolor: 42, borderwidth: 'superfat', - perpendicularaxesinitialrange: null + fixedyrange: null }}}, layoutOut = { xaxis: {} }, expected = { @@ -513,7 +513,7 @@ describe('the range slider', function() { borderwidth: 0, bordercolor: '#444', _input: layoutIn.xaxis.rangeslider, - perpendicularaxesinitialrange: true + fixedyrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -531,7 +531,7 @@ describe('the range slider', function() { borderwidth: 0, bordercolor: '#444', _input: layoutIn.xaxis.rangeslider, - perpendicularaxesinitialrange: true + fixedyrange: true }; _supply(layoutIn, layoutOut, 'xaxis'); From fc4f082a840b396cfdf235d5b45b0dc7efc3663a Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 19 Feb 2018 16:18:01 +0100 Subject: [PATCH 06/37] Introduce new attributes to axes to allow to specifiy fixed range to rangeslider and save them --- src/components/rangeslider/attributes.js | 12 ----------- src/components/rangeslider/defaults.js | 1 - src/components/rangeslider/draw.js | 12 +++++------ src/plots/cartesian/axes.js | 13 +++++++----- src/plots/cartesian/axis_defaults.js | 6 ++++++ src/plots/cartesian/layout_attributes.js | 27 ++++++++++++++++++++++++ 6 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index b71dd3114dc..37268dff026 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -89,17 +89,5 @@ module.exports = { 'If visible, perpendicular axes will be set to `fixedrange`' ].join(' ') }, - fixedyrange: { - valType: 'boolean', - dflt: true, - role: 'style', - editType: 'calc', - description: [ - 'Determine whether the perpendicular axes in the rangeslider', - 'use (or not) their initial range when they are zoomed in the plot', - 'To allow zoom one (or more) perpendicular axe(s) while the rangeslider is visible', - 'you must set the attributes `fixedrange` to *true* on said axe(s).' - ].join(' ') - }, editType: 'calc' }; diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 9475d56e10d..106934833fa 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -37,7 +37,6 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); - coerce('fixedyrange'); // to map back range slider (auto) range containerOut._input = containerIn; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 21e41d6e858..1cf44af2599 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -136,9 +136,9 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { - var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[0]), - range1OppAxis = oppAxisOpts.r2l(oppAxisOpts._rangesliderInitialRange[1]), + if(oppAxisOpts.rangesliderFixedrange) { + var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderRange[0]), + range1OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderRange[1]), distOppAxis = range1OppAxis - range0OppAxis; opts.d2pOppAxis = function(v) { @@ -305,7 +305,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { + if(oppAxisOpts.rangesliderFixedrange) { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -428,7 +428,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange ? oppAxisOpts._rangesliderInitialRange.slice() : oppAxisOpts.range.slice(), + range: oppAxisOpts.rangesliderFixedrange ? oppAxisOpts.rangesliderRange.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -495,7 +495,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(axisOpts.rangeslider.fixedyrange && oppAxisOpts.fixedrange === false && oppAxisOpts._rangesliderInitialRange) { + if(oppAxisOpts.rangesliderFixedrange) { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 6a65959437d..236f2885136 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -352,6 +352,7 @@ axes.doAutoRange = function(ax) { // TODO do we really need this? var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length); + var axIn; if(ax.autorange && hasDeps) { ax.range = axes.getAutoRange(ax); @@ -362,10 +363,16 @@ axes.doAutoRange = function(ax) { // doAutoRange will get called on fullLayout, // but we want to report its results back to layout - var axIn = ax._input; + axIn = ax._input; axIn.range = ax.range.slice(); axIn.autorange = ax.autorange; } + + if(ax.rangesliderFixedrange === 'auto') { + ax.rangesliderRange = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); + axIn = ax._input; + axIn.rangesliderRange = ax.rangesliderRange.slice(); + } }; // save a copy of the initial axis ranges in fullLayout @@ -389,10 +396,6 @@ axes.saveRangeInitial = function(gd, overwrite) { ax._rangeInitial = ax.range.slice(); hasOneAxisChanged = true; } - // store the initial range for the rangeslider if we zoom on oppaxis - if((isNew && ax.fixedrange === false) || (overwrite && hasChanged)) { - ax._rangesliderInitialRange = ax.range.slice(); - } } return hasOneAxisChanged; diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 6cc66906ebf..1c22883ca67 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -54,6 +54,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); + coerce('rangesliderFixedrange', containerIn.rangesliderRange ? + containerOut.isValidRange(containerIn.rangesliderRange) : 'auto'); + + coerce('rangesliderRange'); + containerOut.cleanRange('rangesliderRange'); + handleCategoryOrderDefaults(containerIn, containerOut, coerce); containerOut._initialCategories = axType === 'category' ? orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 225c409e249..e0589e17748 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -128,6 +128,33 @@ module.exports = { 'If true, then zoom is disabled.' ].join(' ') }, + rangesliderFixedrange: { + valType: 'enumerated', + values: ['auto', true, false], + dflt: 'auto', + role: 'style', + editType: 'calc', + description: [ + 'Determines whether or not the range of this axis in', + 'the rangeslider use the same value than in the main plot', + 'when zooming in/out.', + 'If *auto*, the autorange will be used.', + 'If *true*, the `rangesliderRange` is used.', + 'If *false*, the current range is used.' + ].join(' ') + }, + rangesliderRange: { + valType: 'info_array', + role: 'style', + items: [ + {valType: 'any', editType: 'calc'}, + {valType: 'any', editType: 'calc'} + ], + editType: 'calc', + description: [ + 'Sets the range of this axis for the rangeslider.' + ].join(' ') + }, // scaleanchor: not used directly, just put here for reference // values are any opposite-letter axis id scaleanchor: { From 92c6550040074c4df96abb368e9d3e7129e0aa6b Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 19 Feb 2018 16:58:08 +0100 Subject: [PATCH 07/37] Fix broken test from attribute removal --- test/jasmine/tests/range_slider_test.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index d58283298f4..d88987fde43 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -461,8 +461,7 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider, - fixedyrange: true + _input: layoutIn.xaxis.rangeslider }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -479,8 +478,7 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider, - fixedyrange: true + _input: layoutIn.xaxis.rangeslider }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -501,8 +499,7 @@ describe('the range slider', function() { thickness: 'invalid', bgcolor: 42, bordercolor: 42, - borderwidth: 'superfat', - fixedyrange: null + borderwidth: 'superfat' }}}, layoutOut = { xaxis: {} }, expected = { @@ -512,8 +509,7 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider, - fixedyrange: true + _input: layoutIn.xaxis.rangeslider }; _supply(layoutIn, layoutOut, 'xaxis'); @@ -530,8 +526,7 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider, - fixedyrange: true + _input: layoutIn.xaxis.rangeslider }; _supply(layoutIn, layoutOut, 'xaxis'); From b30167c415dd5b92b1111778523183972e8ea9ce Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 19 Feb 2018 16:58:31 +0100 Subject: [PATCH 08/37] Add missing gl attribute --- src/plots/gl3d/layout/axis_attributes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index 78309a1deb5..f8f362b094c 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -78,6 +78,8 @@ module.exports = overrideAll({ autorange: axesAttrs.autorange, rangemode: axesAttrs.rangemode, range: axesAttrs.range, + rangesliderFixedrange: axesAttrs.rangesliderFixedrange, + rangesliderRange: axesAttrs.rangesliderRange, // ticks tickmode: axesAttrs.tickmode, nticks: axesAttrs.nticks, From 3dd91b52558b8ebcdacad71dd354e698aac87a10 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Tue, 20 Feb 2018 13:39:00 +0100 Subject: [PATCH 09/37] Renammed new attributes for more clarity and portability --- src/components/rangeslider/draw.js | 12 ++++++------ src/plots/cartesian/axes.js | 6 +++--- src/plots/cartesian/axis_defaults.js | 8 ++++---- src/plots/cartesian/layout_attributes.js | 6 +++--- src/plots/gl3d/layout/axis_attributes.js | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 1cf44af2599..0cd4aa0a815 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -136,9 +136,9 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(oppAxisOpts.rangesliderFixedrange) { - var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderRange[0]), - range1OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderRange[1]), + if(oppAxisOpts.rangeslidermode) { + var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderrange[0]), + range1OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderrange[1]), distOppAxis = range1OppAxis - range0OppAxis; opts.d2pOppAxis = function(v) { @@ -305,7 +305,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(oppAxisOpts.rangesliderFixedrange) { + if(oppAxisOpts.rangeslidermode) { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -428,7 +428,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: oppAxisOpts.rangesliderFixedrange ? oppAxisOpts.rangesliderRange.slice() : oppAxisOpts.range.slice(), + range: oppAxisOpts.rangeslidermode ? oppAxisOpts.rangesliderrange.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -495,7 +495,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(oppAxisOpts.rangesliderFixedrange) { + if(oppAxisOpts.rangeslidermode) { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 236f2885136..57bf72a902f 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -368,10 +368,10 @@ axes.doAutoRange = function(ax) { axIn.autorange = ax.autorange; } - if(ax.rangesliderFixedrange === 'auto') { - ax.rangesliderRange = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); + if(ax.rangeslidermode === 'auto') { + ax.rangesliderrange = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); axIn = ax._input; - axIn.rangesliderRange = ax.rangesliderRange.slice(); + axIn.rangesliderrange = ax.rangesliderrange.slice(); } }; diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 1c22883ca67..314235e2633 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -54,11 +54,11 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - coerce('rangesliderFixedrange', containerIn.rangesliderRange ? - containerOut.isValidRange(containerIn.rangesliderRange) : 'auto'); + coerce('rangeslidermode', containerIn.rangesliderrange ? + containerOut.isValidRange(containerIn.rangesliderrange) : 'auto'); - coerce('rangesliderRange'); - containerOut.cleanRange('rangesliderRange'); + coerce('rangesliderrange'); + containerOut.cleanRange('rangesliderrange'); handleCategoryOrderDefaults(containerIn, containerOut, coerce); containerOut._initialCategories = axType === 'category' ? diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index e0589e17748..a9ed78eaf50 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -128,7 +128,7 @@ module.exports = { 'If true, then zoom is disabled.' ].join(' ') }, - rangesliderFixedrange: { + rangeslidermode: { valType: 'enumerated', values: ['auto', true, false], dflt: 'auto', @@ -139,11 +139,11 @@ module.exports = { 'the rangeslider use the same value than in the main plot', 'when zooming in/out.', 'If *auto*, the autorange will be used.', - 'If *true*, the `rangesliderRange` is used.', + 'If *true*, the `rangesliderrange` is used.', 'If *false*, the current range is used.' ].join(' ') }, - rangesliderRange: { + rangesliderrange: { valType: 'info_array', role: 'style', items: [ diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index f8f362b094c..b5bcd34bf3f 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -78,8 +78,8 @@ module.exports = overrideAll({ autorange: axesAttrs.autorange, rangemode: axesAttrs.rangemode, range: axesAttrs.range, - rangesliderFixedrange: axesAttrs.rangesliderFixedrange, - rangesliderRange: axesAttrs.rangesliderRange, + rangeslidermode: axesAttrs.rangeslidermode, + rangesliderrange: axesAttrs.rangesliderrange, // ticks tickmode: axesAttrs.tickmode, nticks: axesAttrs.nticks, From 00fd1d7c7e64de9591944472815c19d1c6857b37 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Tue, 20 Feb 2018 13:48:53 +0100 Subject: [PATCH 10/37] Coerce of new attributes is only made for y axis --- src/plots/cartesian/axis_defaults.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 314235e2633..a71ab5bc7a4 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -54,11 +54,13 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - coerce('rangeslidermode', containerIn.rangesliderrange ? - containerOut.isValidRange(containerIn.rangesliderrange) : 'auto'); + if(letter === 'y') { + coerce('rangeslidermode', containerIn.rangesliderrange ? + containerOut.isValidRange(containerIn.rangesliderrange) : 'auto'); - coerce('rangesliderrange'); - containerOut.cleanRange('rangesliderrange'); + coerce('rangesliderrange'); + containerOut.cleanRange('rangesliderrange'); + } handleCategoryOrderDefaults(containerIn, containerOut, coerce); containerOut._initialCategories = axType === 'category' ? From 32488f17dc44e62618780d03b3724a5cb11ec1eb Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Tue, 20 Feb 2018 14:45:55 +0100 Subject: [PATCH 11/37] Changed rangeslidermode values for more clarity --- src/components/rangeslider/draw.js | 8 ++++---- src/plots/cartesian/axis_defaults.js | 9 +++++++-- src/plots/cartesian/layout_attributes.js | 6 +++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 0cd4aa0a815..e69a5d5d1d4 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -136,7 +136,7 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(oppAxisOpts.rangeslidermode) { + if(oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') { var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderrange[0]), range1OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderrange[1]), distOppAxis = range1OppAxis - range0OppAxis; @@ -305,7 +305,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(oppAxisOpts.rangeslidermode) { + if(oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -428,7 +428,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: oppAxisOpts.rangeslidermode ? oppAxisOpts.rangesliderrange.slice() : oppAxisOpts.range.slice(), + range: (oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') ? oppAxisOpts.rangesliderrange.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -495,7 +495,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(oppAxisOpts.rangeslidermode) { + if(oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index a71ab5bc7a4..0a658fd1487 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -55,8 +55,13 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, containerOut.cleanRange(); if(letter === 'y') { - coerce('rangeslidermode', containerIn.rangesliderrange ? - containerOut.isValidRange(containerIn.rangesliderrange) : 'auto'); + if(containerIn.rangesliderrange) { + coerce('rangeslidermode', 'auto'); + } else if(containerOut.isValidRange(containerIn.rangesliderrange)) { + coerce('rangeslidermode', 'fixed'); + } else { + coerce('rangeslidermode', 'match'); + } coerce('rangesliderrange'); containerOut.cleanRange('rangesliderrange'); diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index a9ed78eaf50..270d1a4fd1e 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -130,7 +130,7 @@ module.exports = { }, rangeslidermode: { valType: 'enumerated', - values: ['auto', true, false], + values: ['auto', 'fixed', 'match'], dflt: 'auto', role: 'style', editType: 'calc', @@ -139,8 +139,8 @@ module.exports = { 'the rangeslider use the same value than in the main plot', 'when zooming in/out.', 'If *auto*, the autorange will be used.', - 'If *true*, the `rangesliderrange` is used.', - 'If *false*, the current range is used.' + 'If *fixed*, the `rangesliderrange` is used.', + 'If *match*, the current range is used.' ].join(' ') }, rangesliderrange: { From bc2a40afde397a1c0e1034785ea0d5e0ba0dbf3a Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 28 Feb 2018 13:49:31 +0100 Subject: [PATCH 12/37] Switch to new rangeslider range opts (default missing) --- src/components/rangeslider/constants.js | 2 +- src/components/rangeslider/defaults.js | 1 + src/components/rangeslider/draw.js | 31 +++++++++++++------------ src/plots/cartesian/axis_defaults.js | 2 +- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/components/rangeslider/constants.js b/src/components/rangeslider/constants.js index b5e707b668e..e3551a406a3 100644 --- a/src/components/rangeslider/constants.js +++ b/src/components/rangeslider/constants.js @@ -37,7 +37,7 @@ module.exports = { // style constants maskColor: 'rgba(0,0,0,0.4)', - maskOppColor: 'rgba(0,0,0,0.2)', + maskOppAxisColor: 'rgba(0,0,0,0.2)', slideBoxFill: 'transparent', slideBoxCursor: 'ew-resize', diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 106934833fa..7362fa08d8f 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -37,6 +37,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); + containerOut.yaxis = containerIn.yaxis; // to map back range slider (auto) range containerOut._input = containerIn; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index e69a5d5d1d4..2cf9f7efa80 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -78,7 +78,8 @@ module.exports = function(gd) { rangeSliders.each(function(axisOpts) { var rangeSlider = d3.select(this), opts = axisOpts[constants.name], - oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)]; + oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)], + oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)]; // update range // Expand slider range to the axis range @@ -136,9 +137,9 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') { - var range0OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderrange[0]), - range1OppAxis = oppAxisOpts.r2l(oppAxisOpts.rangesliderrange[1]), + if(oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed') { + var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]), + range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[1]), distOppAxis = range1OppAxis - range0OppAxis; opts.d2pOppAxis = function(v) { @@ -151,8 +152,8 @@ module.exports = function(gd) { rangeSlider .call(drawBg, gd, axisOpts, opts) .call(addClipPath, gd, axisOpts, opts) - .call(drawRangePlot, gd, axisOpts, opts) - .call(drawMasks, gd, axisOpts, opts, oppAxisOpts) + .call(drawRangePlot, gd, axisOpts, opts, oppAxisRangeOpts) + .call(drawMasks, gd, axisOpts, opts, oppAxisRangeOpts) .call(drawSlideBox, gd, axisOpts, opts) .call(drawGrabbers, gd, axisOpts, opts); @@ -160,7 +161,7 @@ module.exports = function(gd) { setupDragElement(rangeSlider, gd, axisOpts, opts); // update current range - setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts); + setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts); // update margins @@ -276,7 +277,7 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) { }); } -function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { +function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts) { var hw2 = constants.handleWidth / 2; function clamp(v) { @@ -305,7 +306,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') { + if(oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed') { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -385,7 +386,7 @@ function addClipPath(rangeSlider, gd, axisOpts, opts) { }); } -function drawRangePlot(rangeSlider, gd, axisOpts, opts) { +function drawRangePlot(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) { var subplotData = Axes.getSubplots(gd, axisOpts), calcData = gd.calcdata; @@ -428,7 +429,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: (oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') ? oppAxisOpts.rangesliderrange.slice() : oppAxisOpts.range.slice(), + range: (oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed') ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -469,7 +470,7 @@ function filterRangePlotCalcData(calcData, subplotId) { return out; } -function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { +function drawMasks(rangeSlider, gd, axisOpts, opts, yAxisOpts) { var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName) .data([0]); @@ -495,7 +496,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(oppAxisOpts.rangeslidermode === 'auto' || oppAxisOpts.rangeslidermode === 'fixed') { + if(yAxisOpts.rangemode === 'auto' || yAxisOpts.rangemode === 'fixed') { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); @@ -506,7 +507,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { maskMinOppAxis .attr('width', opts._width) - .call(Color.fill, constants.maskOppColor); + .call(Color.fill, constants.maskOppAxisColor); var maskMaxOppAxis = rangeSlider.selectAll('rect.' + constants.maskMaxOppAxisClassName) .data([0]); @@ -519,7 +520,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { maskMaxOppAxis .attr('width', opts._width) .style('border-top', constants.maskOppBorder) - .call(Color.fill, constants.maskOppColor); + .call(Color.fill, constants.maskOppAxisColor); } } diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 0a658fd1487..fd1dfd6a37f 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -55,7 +55,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, containerOut.cleanRange(); if(letter === 'y') { - if(containerIn.rangesliderrange) { + if(!containerIn.rangesliderrange) { coerce('rangeslidermode', 'auto'); } else if(containerOut.isValidRange(containerIn.rangesliderrange)) { coerce('rangeslidermode', 'fixed'); From d3b6071bc2e0d34143ceab0d483b9ff09e9b6742 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 28 Feb 2018 15:53:17 +0100 Subject: [PATCH 13/37] Added default attributes --- src/components/rangeslider/defaults.js | 31 +++++++++++++- .../rangeslider/range_attributes.js | 40 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/components/rangeslider/range_attributes.js diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 7362fa08d8f..e99b56bddbe 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -10,6 +10,8 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); +var rangeAttributes = require('./range_attributes'); +var axisIds = require('../../plots/cartesian/axis_ids'); module.exports = function handleDefaults(layoutIn, layoutOut, axName) { if(!layoutIn[axName].rangeslider) return; @@ -27,6 +29,10 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); } + function coerceRange(yName, attr, dflt) { + return Lib.coerce(containerIn[yName], containerOut[yName], rangeAttributes, attr, dflt); + } + var visible = coerce('visible'); if(!visible) return; @@ -37,7 +43,30 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); - containerOut.yaxis = containerIn.yaxis; + + var subplots = layoutOut._subplots; + var yIds = subplots.yaxis; + var yNames = Lib.simpleMap(yIds, axisIds.id2name); + for(var i = 0; i < yNames.length; i++) { + var yName = yNames[i]; + if(!containerIn[yName]) { + containerIn[yName] = {}; + } + if(!containerOut[yName]) { + containerOut[yName] = {}; + } + + if(containerIn[yName].range && layoutOut[yName].isValidRange(containerIn[yName].range)) { + coerceRange(yName, 'rangemode', 'fixed'); + } else { + coerceRange(yName, 'rangemode', 'auto'); + } + + coerceRange(yName, 'range'); + layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); + layoutOut[axName].rangeslider[yName].range = layoutOut[yName].rangeslider[yName].range.slice(); + delete layoutOut[yName].rangeslider; + } // to map back range slider (auto) range containerOut._input = containerIn; diff --git a/src/components/rangeslider/range_attributes.js b/src/components/rangeslider/range_attributes.js new file mode 100644 index 00000000000..6299a3d58ed --- /dev/null +++ b/src/components/rangeslider/range_attributes.js @@ -0,0 +1,40 @@ +/** +* 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'; + +module.exports = { + rangemode: { + valType: 'enumerated', + values: ['auto', 'fixed', 'match'], + dflt: 'auto', + role: 'style', + editType: 'calc', + description: [ + 'Determines whether or not the range of this axis in', + 'the rangeslider use the same value than in the main plot', + 'when zooming in/out.', + 'If *auto*, the autorange will be used.', + 'If *fixed*, the `rangesliderrange` is used.', + 'If *match*, the current range is used.' + ].join(' ') + }, + range: { + valType: 'info_array', + role: 'style', + items: [ + {valType: 'any', editType: 'calc'}, + {valType: 'any', editType: 'calc'} + ], + editType: 'calc', + description: [ + 'Sets the range of this axis for the rangeslider.' + ].join(' ') + }, + editType: 'calc' +}; From b496641346cab217305e4d57349204ca51ec9a89 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 28 Feb 2018 15:55:12 +0100 Subject: [PATCH 14/37] Remove old attributes --- src/components/rangeslider/defaults.js | 6 +++++ .../rangeslider/range_attributes.js | 2 +- src/plots/cartesian/axes.js | 6 ----- src/plots/cartesian/axis_defaults.js | 13 --------- src/plots/cartesian/layout_attributes.js | 27 ------------------- src/plots/gl3d/layout/axis_attributes.js | 2 -- 6 files changed, 7 insertions(+), 49 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index e99b56bddbe..1846693e152 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -66,6 +66,12 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); layoutOut[axName].rangeslider[yName].range = layoutOut[yName].rangeslider[yName].range.slice(); delete layoutOut[yName].rangeslider; + + /*if(ax.rangeslidermode === 'auto') { + ax.rangesliderrange = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); + axIn = ax._input; + axIn.rangesliderrange = ax.rangesliderrange.slice(); + }*/ } // to map back range slider (auto) range diff --git a/src/components/rangeslider/range_attributes.js b/src/components/rangeslider/range_attributes.js index 6299a3d58ed..85a3b5e3c99 100644 --- a/src/components/rangeslider/range_attributes.js +++ b/src/components/rangeslider/range_attributes.js @@ -20,7 +20,7 @@ module.exports = { 'the rangeslider use the same value than in the main plot', 'when zooming in/out.', 'If *auto*, the autorange will be used.', - 'If *fixed*, the `rangesliderrange` is used.', + 'If *fixed*, the `range` is used.', 'If *match*, the current range is used.' ].join(' ') }, diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 57bf72a902f..5fbf6ce15da 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -367,12 +367,6 @@ axes.doAutoRange = function(ax) { axIn.range = ax.range.slice(); axIn.autorange = ax.autorange; } - - if(ax.rangeslidermode === 'auto') { - ax.rangesliderrange = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); - axIn = ax._input; - axIn.rangesliderrange = ax.rangesliderrange.slice(); - } }; // save a copy of the initial axis ranges in fullLayout diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index fd1dfd6a37f..6cc66906ebf 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -54,19 +54,6 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, coerce('range'); containerOut.cleanRange(); - if(letter === 'y') { - if(!containerIn.rangesliderrange) { - coerce('rangeslidermode', 'auto'); - } else if(containerOut.isValidRange(containerIn.rangesliderrange)) { - coerce('rangeslidermode', 'fixed'); - } else { - coerce('rangeslidermode', 'match'); - } - - coerce('rangesliderrange'); - containerOut.cleanRange('rangesliderrange'); - } - handleCategoryOrderDefaults(containerIn, containerOut, coerce); containerOut._initialCategories = axType === 'category' ? orderedCategories(letter, containerOut.categoryorder, containerOut.categoryarray, options.data) : diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index 270d1a4fd1e..225c409e249 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -128,33 +128,6 @@ module.exports = { 'If true, then zoom is disabled.' ].join(' ') }, - rangeslidermode: { - valType: 'enumerated', - values: ['auto', 'fixed', 'match'], - dflt: 'auto', - role: 'style', - editType: 'calc', - description: [ - 'Determines whether or not the range of this axis in', - 'the rangeslider use the same value than in the main plot', - 'when zooming in/out.', - 'If *auto*, the autorange will be used.', - 'If *fixed*, the `rangesliderrange` is used.', - 'If *match*, the current range is used.' - ].join(' ') - }, - rangesliderrange: { - valType: 'info_array', - role: 'style', - items: [ - {valType: 'any', editType: 'calc'}, - {valType: 'any', editType: 'calc'} - ], - editType: 'calc', - description: [ - 'Sets the range of this axis for the rangeslider.' - ].join(' ') - }, // scaleanchor: not used directly, just put here for reference // values are any opposite-letter axis id scaleanchor: { diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index b5bcd34bf3f..78309a1deb5 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -78,8 +78,6 @@ module.exports = overrideAll({ autorange: axesAttrs.autorange, rangemode: axesAttrs.rangemode, range: axesAttrs.range, - rangeslidermode: axesAttrs.rangeslidermode, - rangesliderrange: axesAttrs.rangesliderrange, // ticks tickmode: axesAttrs.tickmode, nticks: axesAttrs.nticks, From 149ef70d2668b8fb25278fae0ad82732f2a6b6e3 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 28 Feb 2018 16:16:46 +0100 Subject: [PATCH 15/37] Add autorange computation --- src/components/rangeslider/defaults.js | 9 +-------- src/plots/cartesian/axes.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 1846693e152..94f1462ac6b 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -62,16 +62,9 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerceRange(yName, 'rangemode', 'auto'); } - coerceRange(yName, 'range'); layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); - layoutOut[axName].rangeslider[yName].range = layoutOut[yName].rangeslider[yName].range.slice(); + coerceRange(yName, 'range', layoutOut[yName].rangeslider[yName].range.slice()); delete layoutOut[yName].rangeslider; - - /*if(ax.rangeslidermode === 'auto') { - ax.rangesliderrange = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); - axIn = ax._input; - axIn.rangesliderrange = ax.rangesliderrange.slice(); - }*/ } // to map back range slider (auto) range diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 5fbf6ce15da..e5edc76c698 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -367,6 +367,17 @@ axes.doAutoRange = function(ax) { axIn.range = ax.range.slice(); axIn.autorange = ax.autorange; } + + if(ax.rangeslider) { + var anchorAxis = ax._anchorAxis; + if(ax.rangeslider[anchorAxis._name].rangemode === 'auto') { + hasDeps = (anchorAxis._min && anchorAxis._max && anchorAxis._min.length && anchorAxis._max.length); + ax.rangeslider[anchorAxis._name].range = hasDeps ? axes.getAutoRange(anchorAxis) : anchorAxis.range.slice(); + axIn = ax._input; + axIn.rangeslider[anchorAxis._name].range = ax.rangeslider[anchorAxis._name].range.slice(); + axIn.rangeslider[anchorAxis._name].rangemode = ax.rangeslider[anchorAxis._name].rangemode; + } + } }; // save a copy of the initial axis ranges in fullLayout From dd48914e78b5a54a4050cf3fe838b113b88dc49b Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 28 Feb 2018 16:41:44 +0100 Subject: [PATCH 16/37] Add missing check --- src/components/rangeslider/defaults.js | 38 ++++++++++++++------------ src/plots/cartesian/axes.js | 2 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 94f1462ac6b..18fa93b08ce 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -45,26 +45,28 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('range'); var subplots = layoutOut._subplots; - var yIds = subplots.yaxis; - var yNames = Lib.simpleMap(yIds, axisIds.id2name); - for(var i = 0; i < yNames.length; i++) { - var yName = yNames[i]; - if(!containerIn[yName]) { - containerIn[yName] = {}; - } - if(!containerOut[yName]) { - containerOut[yName] = {}; - } + if(subplots) { + var yIds = subplots.yaxis; + var yNames = Lib.simpleMap(yIds, axisIds.id2name); + for(var i = 0; i < yNames.length; i++) { + var yName = yNames[i]; + if(!containerIn[yName]) { + containerIn[yName] = {}; + } + if(!containerOut[yName]) { + containerOut[yName] = {}; + } - if(containerIn[yName].range && layoutOut[yName].isValidRange(containerIn[yName].range)) { - coerceRange(yName, 'rangemode', 'fixed'); - } else { - coerceRange(yName, 'rangemode', 'auto'); - } + if(containerIn[yName].range && layoutOut[yName].isValidRange(containerIn[yName].range)) { + coerceRange(yName, 'rangemode', 'fixed'); + } else { + coerceRange(yName, 'rangemode', 'auto'); + } - layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); - coerceRange(yName, 'range', layoutOut[yName].rangeslider[yName].range.slice()); - delete layoutOut[yName].rangeslider; + layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); + coerceRange(yName, 'range', layoutOut[yName].rangeslider[yName].range.slice()); + delete layoutOut[yName].rangeslider; + } } // to map back range slider (auto) range diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 36622c3893b..c5e90e47a16 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -370,7 +370,7 @@ axes.doAutoRange = function(ax) { if(ax.rangeslider) { var anchorAxis = ax._anchorAxis; - if(ax.rangeslider[anchorAxis._name].rangemode === 'auto') { + if(ax.rangeslider[anchorAxis._name] && ax.rangeslider[anchorAxis._name].rangemode === 'auto') { hasDeps = (anchorAxis._min && anchorAxis._max && anchorAxis._min.length && anchorAxis._max.length); ax.rangeslider[anchorAxis._name].range = hasDeps ? axes.getAutoRange(anchorAxis) : anchorAxis.range.slice(); axIn = ax._input; From a8a3ee50ca3f8e4b86911abe266849d8907e8230 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 2 Mar 2018 10:21:17 +0100 Subject: [PATCH 17/37] Add missing catch to test to retrieve the tracebacks --- test/jasmine/tests/range_slider_test.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index d88987fde43..a539db6504a 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -11,6 +11,7 @@ var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); var supplyAllDefaults = require('../assets/supply_defaults'); +var failTest = require('../assets/fail_test'); var TOL = 6; @@ -95,7 +96,9 @@ describe('the range slider', function() { expect(gd.layout.xaxis.range).toBeCloseToArray([4, 49], -0.5); expect(maskMin.getAttribute('width')).toEqual(String(diff)); expect(handleMin.getAttribute('transform')).toBe('translate(' + (diff - 2.5) + ',0.5)'); - }).then(done); + }) + .catch(failTest) + .then(done); }); it('should react to resizing the maximum handle', function(done) { @@ -114,7 +117,9 @@ describe('the range slider', function() { expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); testTranslate1D(handleMax, dataMaxStart + diff); - }).then(done); + }) + .catch(failTest) + .then(done); }); it('should react to moving the slidebox left to right', function(done) { @@ -132,7 +137,9 @@ describe('the range slider', function() { expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); expect(+maskMin.getAttribute('width')).toBeCloseTo(String(diff)); testTranslate1D(handleMin, dataMinStart + diff - 3); - }).then(done); + }) + .catch(failTest) + .then(done); }); it('should react to moving the slidebox right to left', function(done) { @@ -150,7 +157,9 @@ describe('the range slider', function() { expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); testTranslate1D(handleMax, dataMaxStart + diff); - }).then(done); + }) + .catch(failTest) + .then(done); }); it('should resize the main plot when rangeslider has moved', function(done) { @@ -186,6 +195,7 @@ describe('the range slider', function() { testTranslate1D(handleMin, 123.32); testTranslate1D(handleMax, 252.65); }) + .catch(failTest) .then(done); }); @@ -201,6 +211,7 @@ describe('the range slider', function() { testTranslate1D(handleMin, 123.32); testTranslate1D(handleMax, 617); }) + .catch(failTest) .then(done); }); From 0ae28317a93491a8beb8541f33ff2c7004f21bf8 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 2 Mar 2018 14:44:58 +0100 Subject: [PATCH 18/37] Change yAxis rangeslider rangeOpts computation + fix yAxis rangeslider rangeOpts used to draw the trace --- src/components/rangeslider/draw.js | 9 +++++---- src/plots/cartesian/axes.js | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 2cf9f7efa80..f24d730f997 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -152,7 +152,7 @@ module.exports = function(gd) { rangeSlider .call(drawBg, gd, axisOpts, opts) .call(addClipPath, gd, axisOpts, opts) - .call(drawRangePlot, gd, axisOpts, opts, oppAxisRangeOpts) + .call(drawRangePlot, gd, axisOpts, opts) .call(drawMasks, gd, axisOpts, opts, oppAxisRangeOpts) .call(drawSlideBox, gd, axisOpts, opts) .call(drawGrabbers, gd, axisOpts, opts); @@ -386,7 +386,7 @@ function addClipPath(rangeSlider, gd, axisOpts, opts) { }); } -function drawRangePlot(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) { +function drawRangePlot(rangeSlider, gd, axisOpts, opts) { var subplotData = Axes.getSubplots(gd, axisOpts), calcData = gd.calcdata; @@ -408,7 +408,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) { isMainPlot = (i === 0); var oppAxisOpts = Axes.getFromId(gd, id, 'y'), - oppAxisName = oppAxisOpts._name; + oppAxisName = oppAxisOpts._name, + oppAxisRangeOpts = opts[oppAxisName]; var mockFigure = { data: [], @@ -429,7 +430,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: (oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed') ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(), + range: oppAxisRangeOpts.range.slice(), calendar: oppAxisOpts.calendar }; diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index c5e90e47a16..858ac68b039 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -368,15 +368,22 @@ axes.doAutoRange = function(ax) { axIn.autorange = ax.autorange; } - if(ax.rangeslider) { - var anchorAxis = ax._anchorAxis; - if(ax.rangeslider[anchorAxis._name] && ax.rangeslider[anchorAxis._name].rangemode === 'auto') { - hasDeps = (anchorAxis._min && anchorAxis._max && anchorAxis._min.length && anchorAxis._max.length); - ax.rangeslider[anchorAxis._name].range = hasDeps ? axes.getAutoRange(anchorAxis) : anchorAxis.range.slice(); - axIn = ax._input; - axIn.rangeslider[anchorAxis._name].range = ax.rangeslider[anchorAxis._name].range.slice(); - axIn.rangeslider[anchorAxis._name].rangemode = ax.rangeslider[anchorAxis._name].rangemode; + if(ax._anchorAxis && ax._anchorAxis.rangeslider) { + var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name]; + switch(axeRangeOpts.rangemode) { + case 'auto': + axeRangeOpts.range = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); + break; + case 'match': + axeRangeOpts.range = ax.range.slice(); + break; + default: + axeRangeOpts.range = ax.range.slice(); + break; } + axIn = ax._anchorAxis._input; + axIn.rangeslider[ax._name].range = axeRangeOpts.range.slice(); + axIn.rangeslider[ax._name].rangemode = axeRangeOpts.rangemode; } }; From 8764e4d56b10e3a9f802a17e9fce200a25efc224 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 2 Mar 2018 14:58:42 +0100 Subject: [PATCH 19/37] Add missing check --- src/plots/cartesian/axes.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 858ac68b039..6ea9c1cc006 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -370,20 +370,22 @@ axes.doAutoRange = function(ax) { if(ax._anchorAxis && ax._anchorAxis.rangeslider) { var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name]; - switch(axeRangeOpts.rangemode) { - case 'auto': - axeRangeOpts.range = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); - break; - case 'match': - axeRangeOpts.range = ax.range.slice(); - break; - default: - axeRangeOpts.range = ax.range.slice(); - break; + if(axeRangeOpts) { + switch(axeRangeOpts.rangemode) { + case 'auto': + axeRangeOpts.range = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); + break; + case 'match': + axeRangeOpts.range = ax.range.slice(); + break; + default: + axeRangeOpts.range = ax.range.slice(); + break; + } + axIn = ax._anchorAxis._input; + axIn.rangeslider[ax._name].range = axeRangeOpts.range.slice(); + axIn.rangeslider[ax._name].rangemode = axeRangeOpts.rangemode; } - axIn = ax._anchorAxis._input; - axIn.rangeslider[ax._name].range = axeRangeOpts.range.slice(); - axIn.rangeslider[ax._name].rangemode = axeRangeOpts.rangemode; } }; From a17d3a8d1fa761bcac888d6757323b6c6d46d470 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 2 Mar 2018 15:19:29 +0100 Subject: [PATCH 20/37] Fix rangeslider test --- test/jasmine/tests/range_slider_test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index a539db6504a..2c530f8ea3d 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -91,7 +91,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMin = children[2], - handleMin = children[5]; + handleMin = children[7]; expect(gd.layout.xaxis.range).toBeCloseToArray([4, 49], -0.5); expect(maskMin.getAttribute('width')).toEqual(String(diff)); @@ -111,7 +111,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMax = children[3], - handleMax = children[6]; + handleMax = children[8]; expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); @@ -132,7 +132,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMin = children[2], - handleMin = children[5]; + handleMin = children[7]; expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); expect(+maskMin.getAttribute('width')).toBeCloseTo(String(diff)); @@ -152,7 +152,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMax = children[3], - handleMax = children[6]; + handleMax = children[8]; expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); @@ -187,8 +187,8 @@ describe('the range slider', function() { Plotly.relayout(gd, 'xaxis.range', [10, 20]).then(function() { var maskMin = children[2], maskMax = children[3], - handleMin = children[5], - handleMax = children[6]; + handleMin = children[7], + handleMax = children[8]; expect(+maskMin.getAttribute('width')).toBeWithin(125, TOL); expect(+maskMax.getAttribute('width')).toBeWithin(365, TOL); @@ -203,8 +203,8 @@ describe('the range slider', function() { Plotly.relayout(gd, 'xaxis.range[0]', 10).then(function() { var maskMin = children[2], maskMax = children[3], - handleMin = children[5], - handleMax = children[6]; + handleMin = children[7], + handleMax = children[8]; expect(+maskMin.getAttribute('width')).toBeWithin(126, TOL); expect(+maskMax.getAttribute('width')).toEqual(0); From e85e3d0e06acfe5ed8ff6c495d71fe8bb902de6f Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 2 Mar 2018 15:25:46 +0100 Subject: [PATCH 21/37] Renamed parameter to keep name consistency across the file --- src/components/rangeslider/draw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index f24d730f997..e76bb851014 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -471,7 +471,7 @@ function filterRangePlotCalcData(calcData, subplotId) { return out; } -function drawMasks(rangeSlider, gd, axisOpts, opts, yAxisOpts) { +function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName) .data([0]); @@ -497,7 +497,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, yAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(yAxisOpts.rangemode === 'auto' || yAxisOpts.rangemode === 'fixed') { + if(oppAxisOpts.rangemode === 'auto' || oppAxisOpts.rangemode === 'fixed') { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); From 3929be6015a594452a96477cf3d4fd5938b40f2c Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 2 Mar 2018 18:08:08 +0100 Subject: [PATCH 22/37] Changes requested by @alexcjohnson --- src/components/rangeslider/defaults.js | 21 +++++++++------------ src/plots/cartesian/axes.js | 19 ++++++------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 18fa93b08ce..f67e5f9d033 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -29,8 +29,8 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); } - function coerceRange(yName, attr, dflt) { - return Lib.coerce(containerIn[yName], containerOut[yName], rangeAttributes, attr, dflt); + function coerceRange(rangeContainerIn, rangeContainerOut, attr, dflt) { + return Lib.coerce(rangeContainerIn, rangeContainerOut, rangeAttributes, attr, dflt); } var visible = coerce('visible'); @@ -50,21 +50,18 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { var yNames = Lib.simpleMap(yIds, axisIds.id2name); for(var i = 0; i < yNames.length; i++) { var yName = yNames[i]; - if(!containerIn[yName]) { - containerIn[yName] = {}; - } - if(!containerOut[yName]) { - containerOut[yName] = {}; - } - if(containerIn[yName].range && layoutOut[yName].isValidRange(containerIn[yName].range)) { - coerceRange(yName, 'rangemode', 'fixed'); + var rangeContainerIn = containerIn[yName] || {}; + var rangeContainerOut = containerOut[yName] = {}; + + if(rangeContainerIn.range && layoutOut[yName].isValidRange(rangeContainerIn.range)) { + coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', 'fixed'); } else { - coerceRange(yName, 'rangemode', 'auto'); + coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', 'auto'); } layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); - coerceRange(yName, 'range', layoutOut[yName].rangeslider[yName].range.slice()); + coerceRange(rangeContainerIn, rangeContainerOut, 'range', layoutOut[yName].rangeslider[yName].range.slice()); delete layoutOut[yName].rangeslider; } } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index d16afa67b69..44ef8f3264c 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -371,21 +371,14 @@ axes.doAutoRange = function(ax) { if(ax._anchorAxis && ax._anchorAxis.rangeslider) { var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name]; if(axeRangeOpts) { - switch(axeRangeOpts.rangemode) { - case 'auto': - axeRangeOpts.range = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); - break; - case 'match': - axeRangeOpts.range = ax.range.slice(); - break; - default: - axeRangeOpts.range = ax.range.slice(); - break; + if(axeRangeOpts.rangemode === 'auto') { + axeRangeOpts.range = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); + } else { + axeRangeOpts.range = ax.range.slice(); } - axIn = ax._anchorAxis._input; - axIn.rangeslider[ax._name].range = axeRangeOpts.range.slice(); - axIn.rangeslider[ax._name].rangemode = axeRangeOpts.rangemode; } + axIn = ax._anchorAxis._input; + axIn.rangeslider[ax._name] = Object.assign({}, axeRangeOpts); } }; From e2ad33de21389d698e8cc1ba9018a15a8fd7957c Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Fri, 2 Mar 2018 18:18:18 +0100 Subject: [PATCH 23/37] Changes requested by @alexcjohnson --- src/components/rangeslider/defaults.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index f67e5f9d033..a45d92739ed 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -46,7 +46,13 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { var subplots = layoutOut._subplots; if(subplots) { - var yIds = subplots.yaxis; + var yIds = subplots.cartesian + .filter(function(subplotId) { + return subplotId.substr(0, subplotId.indexOf('y')) === axisIds.name2id(axName); + }) + .map(function(subplotId) { + return subplotId.substr(subplotId.indexOf('y'), subplotId.length); + }); var yNames = Lib.simpleMap(yIds, axisIds.id2name); for(var i = 0; i < yNames.length; i++) { var yName = yNames[i]; From df85f35b675c9b047d0003277395d4b788933c25 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 5 Mar 2018 11:40:39 +0100 Subject: [PATCH 24/37] Changes requested by @etpinard --- src/components/rangeslider/defaults.js | 2 +- src/components/rangeslider/draw.js | 2 +- src/components/rangeslider/range_attributes.js | 2 +- src/plots/cartesian/axes.js | 4 +--- test/jasmine/tests/range_slider_test.js | 16 ++++++++-------- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index a45d92739ed..962e1c8ad84 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -63,7 +63,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { if(rangeContainerIn.range && layoutOut[yName].isValidRange(rangeContainerIn.range)) { coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', 'fixed'); } else { - coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', 'auto'); + coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode'); } layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index e76bb851014..9ffd2308073 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -430,7 +430,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: oppAxisRangeOpts.range.slice(), + range: oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; diff --git a/src/components/rangeslider/range_attributes.js b/src/components/rangeslider/range_attributes.js index 85a3b5e3c99..cb14ddb471c 100644 --- a/src/components/rangeslider/range_attributes.js +++ b/src/components/rangeslider/range_attributes.js @@ -12,7 +12,7 @@ module.exports = { rangemode: { valType: 'enumerated', values: ['auto', 'fixed', 'match'], - dflt: 'auto', + dflt: 'match', role: 'style', editType: 'calc', description: [ diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 44ef8f3264c..0ea40af1955 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -373,12 +373,10 @@ axes.doAutoRange = function(ax) { if(axeRangeOpts) { if(axeRangeOpts.rangemode === 'auto') { axeRangeOpts.range = hasDeps ? axes.getAutoRange(ax) : ax.range.slice(); - } else { - axeRangeOpts.range = ax.range.slice(); } } axIn = ax._anchorAxis._input; - axIn.rangeslider[ax._name] = Object.assign({}, axeRangeOpts); + axIn.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts); } }; diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 2c530f8ea3d..a539db6504a 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -91,7 +91,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMin = children[2], - handleMin = children[7]; + handleMin = children[5]; expect(gd.layout.xaxis.range).toBeCloseToArray([4, 49], -0.5); expect(maskMin.getAttribute('width')).toEqual(String(diff)); @@ -111,7 +111,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMax = children[3], - handleMax = children[8]; + handleMax = children[6]; expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); @@ -132,7 +132,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMin = children[2], - handleMin = children[7]; + handleMin = children[5]; expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); expect(+maskMin.getAttribute('width')).toBeCloseTo(String(diff)); @@ -152,7 +152,7 @@ describe('the range slider', function() { slide(start, sliderY, end, sliderY).then(function() { var maskMax = children[3], - handleMax = children[8]; + handleMax = children[6]; expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); @@ -187,8 +187,8 @@ describe('the range slider', function() { Plotly.relayout(gd, 'xaxis.range', [10, 20]).then(function() { var maskMin = children[2], maskMax = children[3], - handleMin = children[7], - handleMax = children[8]; + handleMin = children[5], + handleMax = children[6]; expect(+maskMin.getAttribute('width')).toBeWithin(125, TOL); expect(+maskMax.getAttribute('width')).toBeWithin(365, TOL); @@ -203,8 +203,8 @@ describe('the range slider', function() { Plotly.relayout(gd, 'xaxis.range[0]', 10).then(function() { var maskMin = children[2], maskMax = children[3], - handleMin = children[7], - handleMax = children[8]; + handleMin = children[5], + handleMax = children[6]; expect(+maskMin.getAttribute('width')).toBeWithin(126, TOL); expect(+maskMax.getAttribute('width')).toEqual(0); From fe83e07bcd9159830de54078a21b89bf6d7feb4b Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Mon, 5 Mar 2018 16:41:09 +0100 Subject: [PATCH 25/37] Default range for 'fixed' rangemode value is now the range of the axe --- src/components/rangeslider/defaults.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 962e1c8ad84..010888383eb 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -66,9 +66,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode'); } - layoutOut[yName].cleanRange('rangeslider.' + yName + '.range'); - coerceRange(rangeContainerIn, rangeContainerOut, 'range', layoutOut[yName].rangeslider[yName].range.slice()); - delete layoutOut[yName].rangeslider; + coerceRange(rangeContainerIn, rangeContainerOut, 'range', layoutOut[yName].range.slice()); } } From 3b6b81a94fc4c4945265c4e6d55f9e93ce849365 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Tue, 6 Mar 2018 18:14:37 +0100 Subject: [PATCH 26/37] Changes requested by @etpinard --- src/components/rangeslider/range_attributes.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/rangeslider/range_attributes.js b/src/components/rangeslider/range_attributes.js index cb14ddb471c..d2dc7e80fbb 100644 --- a/src/components/rangeslider/range_attributes.js +++ b/src/components/rangeslider/range_attributes.js @@ -21,15 +21,15 @@ module.exports = { 'when zooming in/out.', 'If *auto*, the autorange will be used.', 'If *fixed*, the `range` is used.', - 'If *match*, the current range is used.' + 'If *match*, the current range of the corresponding y-axis on the main subplot is used.' ].join(' ') }, range: { valType: 'info_array', role: 'style', items: [ - {valType: 'any', editType: 'calc'}, - {valType: 'any', editType: 'calc'} + {valType: 'any', editType: 'plot'}, + {valType: 'any', editType: 'plot'} ], editType: 'calc', description: [ From 9de4e3ef07d162a665b0da17d7ab39062bf8be81 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Tue, 6 Mar 2018 18:35:38 +0100 Subject: [PATCH 27/37] Changes requested by @etpinard --- src/components/rangeslider/draw.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 9ffd2308073..f318fb17e2b 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -137,7 +137,7 @@ module.exports = function(gd) { opts._rl = [range0, range1]; - if(oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed') { + if(oppAxisRangeOpts.rangemode !== 'match') { var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]), range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[1]), distOppAxis = range1OppAxis - range0OppAxis; @@ -306,7 +306,7 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRang .attr('x', pixelMax) .attr('width', opts._width - pixelMax); - if(oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed') { + if(oppAxisRangeOpts.rangemode !== 'match') { var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])), pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0])); @@ -430,7 +430,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: oppAxisRangeOpts.rangemode === 'auto' || oppAxisRangeOpts.rangemode === 'fixed' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(), + range: oppAxisRangeOpts.rangemode !== 'match' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -471,7 +471,7 @@ function filterRangePlotCalcData(calcData, subplotId) { return out; } -function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { +function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) { var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName) .data([0]); @@ -497,7 +497,7 @@ function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisOpts) { .call(Color.fill, constants.maskColor); // masks used for oppAxis zoom - if(oppAxisOpts.rangemode === 'auto' || oppAxisOpts.rangemode === 'fixed') { + if(oppAxisRangeOpts.rangemode !== 'match') { var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName) .data([0]); From f2819c8e7e1e9fbc1218d6fdf78d9c193282ed6b Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Tue, 6 Mar 2018 18:57:09 +0100 Subject: [PATCH 28/37] Changes requested by @etpinard --- src/components/rangeslider/defaults.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 010888383eb..23f44788ed9 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -60,13 +60,15 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { var rangeContainerIn = containerIn[yName] || {}; var rangeContainerOut = containerOut[yName] = {}; + var rangemodeDflt; if(rangeContainerIn.range && layoutOut[yName].isValidRange(rangeContainerIn.range)) { - coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', 'fixed'); - } else { - coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode'); + rangemodeDflt = 'fixed'; } - coerceRange(rangeContainerIn, rangeContainerOut, 'range', layoutOut[yName].range.slice()); + var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt); + if(rangeMode !== 'match') { + coerceRange(rangeContainerIn, rangeContainerOut, 'range', layoutOut[yName].range.slice()); + } } } From 9c66f81fbf4429915974325166ea51e1ce81de5b Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 7 Mar 2018 11:26:00 +0100 Subject: [PATCH 29/37] Add test for new attributes default and coerce --- test/jasmine/tests/range_slider_test.js | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index a539db6504a..0550f15246d 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -559,6 +559,69 @@ describe('the range slider', function() { }); }); + fdescribe('yaxis options', function() { + + it('should be set one yaxis is present', function() { + var mock = { + layout: { + xaxis: { rangeslider: {} }, + yaxis: { } + } + }; + + supplyAllDefaults(mock); + + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + }); + + it('should set multiple yaxis with data are present', function() { + var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'} + ], + layout: { + xaxis: { rangeslider: {} }, + yaxis: { }, + yaxis2: { }, + yaxis3: { } + } + }; + + supplyAllDefaults(mock); + + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'match' }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual(undefined); + }); + + it('should honor user settings', function() { + var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'}, + {y: [1, 2], yaxis: 'y3'} + ], + layout: { + xaxis: { rangeslider: { + yaxis: { rangemode: 'auto' }, + yaxis2: { rangemode: 'fixed' }, + yaxis3: { range: [0, 1] } + } }, + yaxis: { }, + yaxis2: { }, + yaxis3: { } + } + }; + + supplyAllDefaults(mock); + + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'auto', range: [-1, 4] }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'fixed', range: [-1, 4] }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual({ rangemode: 'fixed', range: [0, 1] }); + }); + }); + describe('anchored axes fixedrange', function() { it('should default to *true* when range slider is visible', function() { From f092c44fb7447fabce65648ba945325c865cb8bc Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 7 Mar 2018 11:43:03 +0100 Subject: [PATCH 30/37] Add interaction test for new attributes --- test/jasmine/tests/range_slider_test.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 0550f15246d..0a95da554a5 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -559,7 +559,7 @@ describe('the range slider', function() { }); }); - fdescribe('yaxis options', function() { + describe('yaxis options', function() { it('should be set one yaxis is present', function() { var mock = { @@ -792,6 +792,28 @@ describe('the range slider', function() { }) .then(done); }); + + it('should configure yaxis opts on relayout', function(done) { + Plotly.plot(gd, [{ + y: [2, 1, 2] + }], { + xaxis: { rangeslider: { yaxis: { range: [-10, 20] } } } + }) + .then(function() { + expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'fixed', range: [-10, 20] }); + + return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'auto'); + }) + .then(function() { + expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'auto', range: [0.9201631701631702, 2.0798368298368297] }); + + return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'match'); + }) + .then(function() { + expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + }) + .then(done); + }); }); }); From 041ce3d570fb93aac4420fdaa44c626eb47932f2 Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 7 Mar 2018 11:56:41 +0100 Subject: [PATCH 31/37] Fixed autorange test precision --- test/jasmine/tests/range_slider_test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 0a95da554a5..f1355f6dfc1 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -805,7 +805,11 @@ describe('the range slider', function() { return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'auto'); }) .then(function() { - expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'auto', range: [0.9201631701631702, 2.0798368298368297] }); + var precision = 1e-2; + + expect(gd.layout.xaxis.rangeslider.yaxis.rangemode).toEqual('auto'); + expect(gd.layout.xaxis.rangeslider.yaxis.rangemode.range) + .toBeCloseToArray([0.920, 2.079], precision); return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'match'); }) From 23c8401a2c0a852e5e064a080a1cc569b092ebac Mon Sep 17 00:00:00 2001 From: Tom Demulielr--Chevret Date: Wed, 7 Mar 2018 12:06:06 +0100 Subject: [PATCH 32/37] Fixed autorange test precision --- test/jasmine/tests/range_slider_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index f1355f6dfc1..88e150df8a0 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -808,7 +808,7 @@ describe('the range slider', function() { var precision = 1e-2; expect(gd.layout.xaxis.rangeslider.yaxis.rangemode).toEqual('auto'); - expect(gd.layout.xaxis.rangeslider.yaxis.rangemode.range) + expect(gd.layout.xaxis.rangeslider.yaxis.range) .toBeCloseToArray([0.920, 2.079], precision); return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'match'); From c556f8cc511e346592e816f3f80c83ca1f01b6dc Mon Sep 17 00:00:00 2001 From: etienne Date: Wed, 7 Mar 2018 11:56:18 -0500 Subject: [PATCH 33/37] rename range_attributes file -> oppaxis_attributes --- src/components/rangeslider/defaults.js | 4 ++-- .../{range_attributes.js => oppaxis_attributes.js} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/components/rangeslider/{range_attributes.js => oppaxis_attributes.js} (100%) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 23f44788ed9..6e6d2037271 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -10,7 +10,7 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); -var rangeAttributes = require('./range_attributes'); +var oppAxisAttrs = require('./oppaxis_attributes'); var axisIds = require('../../plots/cartesian/axis_ids'); module.exports = function handleDefaults(layoutIn, layoutOut, axName) { @@ -30,7 +30,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { } function coerceRange(rangeContainerIn, rangeContainerOut, attr, dflt) { - return Lib.coerce(rangeContainerIn, rangeContainerOut, rangeAttributes, attr, dflt); + return Lib.coerce(rangeContainerIn, rangeContainerOut, oppAxisAttrs, attr, dflt); } var visible = coerce('visible'); diff --git a/src/components/rangeslider/range_attributes.js b/src/components/rangeslider/oppaxis_attributes.js similarity index 100% rename from src/components/rangeslider/range_attributes.js rename to src/components/rangeslider/oppaxis_attributes.js From 43f15a761006f8d431279ec160b2c1765db7b7ce Mon Sep 17 00:00:00 2001 From: etienne Date: Wed, 7 Mar 2018 11:56:50 -0500 Subject: [PATCH 34/37] add rangeslider.yaxis? to schema flagging it with _isSubplotObj --- src/components/rangeslider/index.js | 10 ++++++++- .../rangeslider/oppaxis_attributes.js | 5 +++++ test/jasmine/tests/plotschema_test.js | 12 ++++++++--- test/jasmine/tests/validate_test.js | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js index 61e530325fc..2983d72c58e 100644 --- a/src/components/rangeslider/index.js +++ b/src/components/rangeslider/index.js @@ -8,13 +8,21 @@ 'use strict'; +var Lib = require('../../lib'); +var attrs = require('./attributes'); +var oppAxisAttrs = require('./oppaxis_attributes'); + module.exports = { moduleType: 'component', name: 'rangeslider', schema: { subplots: { - xaxis: {rangeslider: require('./attributes')} + xaxis: { + rangeslider: Lib.extendFlat({}, attrs, { + yaxis: oppAxisAttrs + }) + } } }, diff --git a/src/components/rangeslider/oppaxis_attributes.js b/src/components/rangeslider/oppaxis_attributes.js index d2dc7e80fbb..decfbe73ac9 100644 --- a/src/components/rangeslider/oppaxis_attributes.js +++ b/src/components/rangeslider/oppaxis_attributes.js @@ -9,6 +9,11 @@ 'use strict'; module.exports = { + // not really a 'subplot' attribute container, + // but this is the flag we use to denote attributes that + // support yaxis, yaxis2, yaxis3, ... counters + _isSubplotObj: true, + rangemode: { valType: 'enumerated', values: ['auto', 'fixed', 'match'], diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index 98e6fa66c9c..c071673f909 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -126,9 +126,15 @@ describe('plot schema', function() { }); it('all subplot objects should contain _isSubplotObj', function() { - var IS_SUBPLOT_OBJ = '_isSubplotObj', - astrs = ['xaxis', 'yaxis', 'scene', 'geo', 'ternary', 'mapbox', 'polar'], - cnt = 0; + var IS_SUBPLOT_OBJ = '_isSubplotObj'; + var cnt = 0; + + var astrs = [ + 'xaxis', 'yaxis', 'scene', 'geo', 'ternary', 'mapbox', 'polar', + // not really a 'subplot' object but supports yaxis, yaxis2, yaxis3, + // ... counters, so list it here + 'xaxis.rangeslider.yaxis' + ]; // check if the subplot objects have '_isSubplotObj' astrs.forEach(function(astr) { diff --git a/test/jasmine/tests/validate_test.js b/test/jasmine/tests/validate_test.js index ad12aeefd50..cd868ee9782 100644 --- a/test/jasmine/tests/validate_test.js +++ b/test/jasmine/tests/validate_test.js @@ -505,4 +505,25 @@ describe('Plotly.validate', function() { 'In layout, key grid.subplots[2][0] is set to an invalid value (5)' ); }); + + it('should detect opposite axis range slider attributes', function() { + var out = Plotly.validate([ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'}, + {y: [1, 2], yaxis: 'y3'} + ], { + xaxis: { + rangeslider: { + yaxis: { rangemode: 'auto' }, + yaxis2: { rangemode: 'fixed' }, + yaxis3: { range: [0, 1] } + } + }, + yaxis: {}, + yaxis2: {}, + yaxis3: {} + }); + + expect(out).toBeUndefined(); + }); }); From ccb609186d1f607e342871d799e173508f4ec119 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 7 Mar 2018 11:10:17 -0500 Subject: [PATCH 35/37] fix rangeslider.y.rangemode=auto and add a test image --- src/components/rangeslider/defaults.js | 9 +- src/plots/cartesian/autorange.js | 2 +- src/plots/cartesian/axis_defaults.js | 6 + .../baselines/range_slider_rangemode.png | Bin 0 -> 34627 bytes test/image/mocks/range_slider_rangemode.json | 30 + test/jasmine/tests/axes_test.js | 2 +- test/jasmine/tests/range_slider_test.js | 1463 +++++++++-------- 7 files changed, 821 insertions(+), 691 deletions(-) create mode 100644 test/image/baselines/range_slider_rangemode.png create mode 100644 test/image/mocks/range_slider_rangemode.json diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 23f44788ed9..3daa3841ab2 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -41,7 +41,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { coerce('borderwidth'); coerce('thickness'); - coerce('autorange', !axOut.isValidRange(containerIn.range)); + axOut._rangesliderAutorange = coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); var subplots = layoutOut._subplots; @@ -60,15 +60,18 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { var rangeContainerIn = containerIn[yName] || {}; var rangeContainerOut = containerOut[yName] = {}; + var yAxOut = layoutOut[yName]; + var rangemodeDflt; - if(rangeContainerIn.range && layoutOut[yName].isValidRange(rangeContainerIn.range)) { + if(rangeContainerIn.range && yAxOut.isValidRange(rangeContainerIn.range)) { rangemodeDflt = 'fixed'; } var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt); if(rangeMode !== 'match') { - coerceRange(rangeContainerIn, rangeContainerOut, 'range', layoutOut[yName].range.slice()); + coerceRange(rangeContainerIn, rangeContainerOut, 'range', yAxOut.range.slice()); } + yAxOut._rangesliderAutorange = (rangeMode === 'auto'); } } diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index 2ed49a8cf1c..797add39ac6 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -211,7 +211,7 @@ function doAutoRange(ax) { } function needsAutorange(ax) { - return ax.autorange || !!(ax.rangeslider || {}).autorange; + return ax.autorange || ax._rangesliderAutorange; } /* diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 3610f101ce2..41d88f83ab4 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -49,6 +49,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range)); + // both x and y axes may need autorange done just for the range slider's purposes + // the logic is complicated to figure this out later, particularly for y axes since + // the settings can be spread out in the x axes... so instead we'll collect them + // during supplyDefaults + containerOut._rangesliderAutorange = false; + if(autoRange) coerce('rangemode'); coerce('range'); diff --git a/test/image/baselines/range_slider_rangemode.png b/test/image/baselines/range_slider_rangemode.png new file mode 100644 index 0000000000000000000000000000000000000000..0915d37b06d845b1dbea9c5e0c210462abf69f48 GIT binary patch literal 34627 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

B0&0~jj-XsLxfc)=OY4XWJgFQ$qK8{FGGPI}p{ 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/range_slider_rangemode.json b/test/image/mocks/range_slider_rangemode.json new file mode 100644 index 00000000000..ee38d51a3db --- /dev/null +++ b/test/image/mocks/range_slider_rangemode.json @@ -0,0 +1,30 @@ +{ + "data": [ + {"y": [4, 7e7, 5e5, 6e8, 3e2], "type": "bar"}, + {"y": [1, 4, 2, 6, 3], "xaxis": "x2", "yaxis": "y2"} + ], + "layout": { + "xaxis": { + "domain": [0, 0.42], + "range": [1, 3], + "rangeslider": { + "yaxis": {"rangemode": "auto"} + }, + "title": "Rangeslider Y rangemode auto" + }, + "xaxis2": { + "anchor": "y2", + "domain": [0.58, 1], + "range": [1.5, 3.5], + "rangeslider": { + "range": [-2, 4], + "yaxis2": {"rangemode": "fixed", "range": [0, 10]} + }, + "title": "Rangeslider Y2 rangemode fixed" + }, + "yaxis": {"type": "log", "range": [2, 6], "title": "Y explicit range"}, + "yaxis2": {"anchor": "x2", "title": "Y2 autoranged"}, + "width": 700, + "height": 500 + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index de8c216ff2b..e74f882c211 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -1876,7 +1876,7 @@ describe('Test axes', function() { data = [2, 5]; ax.autorange = false; - ax.rangeslider = { autorange: true }; + ax._rangesliderAutorange = true; expand(ax, data, {}); expect(ax._min).toEqual([{val: 2, pad: 0, extrapad: false}]); diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 88e150df8a0..2003499549d 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -16,55 +16,58 @@ var failTest = require('../assets/fail_test'); var TOL = 6; -describe('the range slider', function() { - - var gd, - rangeSlider, - children; - - var sliderY = 393; +function getRangeSlider() { + var className = constants.containerClassName; + return document.getElementsByClassName(className)[0]; +} - function getRangeSlider() { - var className = constants.containerClassName; - return document.getElementsByClassName(className)[0]; - } +function getRangeSliderChild(index) { + return getRangeSlider().children[index]; +} - function countRangeSliderClipPaths() { - return d3.selectAll('defs').selectAll('*').filter(function() { - return this.id.indexOf('rangeslider') !== -1; - }).size(); - } +function countRangeSliderClipPaths() { + return d3.selectAll('defs').selectAll('*').filter(function() { + return this.id.indexOf('rangeslider') !== -1; + }).size(); +} - function testTranslate1D(node, val) { - var transformParts = node.getAttribute('transform').split('('); +function testTranslate1D(node, val) { + var transformParts = node.getAttribute('transform').split('('); - expect(transformParts[0]).toEqual('translate'); - expect(+transformParts[1].split(',0.5)')[0]).toBeWithin(val, TOL); - } + expect(transformParts[0]).toEqual('translate'); + expect(+transformParts[1].split(',0.5)')[0]).toBeWithin(val, TOL); +} - describe('when specified as visible', function() { +describe('Visible rangesliders', function() { + var gd, sliderY; - beforeEach(function(done) { - gd = createGraphDiv(); + beforeEach(function() { + gd = createGraphDiv(); + }); - var mockCopy = Lib.extendDeep({}, mock); + afterEach(destroyGraphDiv); - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - rangeSlider = getRangeSlider(); - children = rangeSlider.children; + function plotMock() { + var mockCopy = Lib.extendDeep({}, mock); - done(); - }); + return Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { + var bb = getRangeSlider().getBoundingClientRect(); + sliderY = bb.top + bb.height / 2; + expect(sliderY).toBeCloseTo(387, -1); }); + } - afterEach(destroyGraphDiv); - - it('should be added to the DOM when specified', function() { - expect(rangeSlider).toBeDefined(); - }); + it('should be added to the DOM when specified', function(done) { + plotMock().then(function() { + expect(getRangeSlider()).toBeDefined(); + }) + .catch(failTest) + .then(done); + }); - it('should have the correct width and height', function() { - var bg = children[0]; + it('should have the correct width and height', function(done) { + plotMock().then(function() { + var bg = getRangeSliderChild(0); var options = mock.layout.xaxis.rangeslider, expectedWidth = gd._fullLayout._size.w + options.borderwidth; @@ -72,755 +75,843 @@ describe('the range slider', function() { // width incorporates border widths expect(+bg.getAttribute('width')).toEqual(expectedWidth); expect(+bg.getAttribute('height')).toEqual(66); - }); + }) + .catch(failTest) + .then(done); + }); - it('should have the correct style', function() { - var bg = children[0]; + it('should have the correct style', function(done) { + plotMock().then(function() { + var bg = getRangeSliderChild(0); expect(bg.getAttribute('fill')).toBe('#fafafa'); expect(bg.getAttribute('stroke')).toBe('black'); expect(bg.getAttribute('stroke-width')).toBe('2'); - }); + }) + .catch(failTest) + .then(done); + }); - it('should react to resizing the minimum handle', function(done) { - var start = 85, - end = 140, - diff = end - start; + it('should react to resizing the minimum handle', function(done) { + var start = 85, + end = 140, + diff = end - start; + plotMock().then(function() { expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - slide(start, sliderY, end, sliderY).then(function() { - var maskMin = children[2], - handleMin = children[5]; + return slide(start, sliderY, end, sliderY); + }) + .then(function() { + var maskMin = getRangeSliderChild(2), + handleMin = getRangeSliderChild(5); + + expect(gd.layout.xaxis.range).toBeCloseToArray([4, 49], -0.5); + expect(maskMin.getAttribute('width')).toEqual(String(diff)); + expect(handleMin.getAttribute('transform')).toBe('translate(' + (diff - 2.5) + ',0.5)'); + }) + .catch(failTest) + .then(done); + }); - expect(gd.layout.xaxis.range).toBeCloseToArray([4, 49], -0.5); - expect(maskMin.getAttribute('width')).toEqual(String(diff)); - expect(handleMin.getAttribute('transform')).toBe('translate(' + (diff - 2.5) + ',0.5)'); - }) - .catch(failTest) - .then(done); - }); + it('should react to resizing the maximum handle', function(done) { + var start = 695; + var end = 490; + var diff = end - start; - it('should react to resizing the maximum handle', function(done) { - var start = 695, - end = 490, - dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), - diff = end - start; + var dataMaxStart; + + plotMock().then(function() { + dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49); expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - slide(start, sliderY, end, sliderY).then(function() { - var maskMax = children[3], - handleMax = children[6]; + return slide(start, sliderY, end, sliderY); + }) + .then(function() { + var maskMax = getRangeSliderChild(3), + handleMax = getRangeSliderChild(6); - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); - expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); + expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); - testTranslate1D(handleMax, dataMaxStart + diff); - }) - .catch(failTest) - .then(done); - }); + testTranslate1D(handleMax, dataMaxStart + diff); + }) + .catch(failTest) + .then(done); + }); + + it('should react to moving the slidebox left to right', function(done) { + var start = 250; + var end = 300; + var diff = end - start; - it('should react to moving the slidebox left to right', function(done) { - var start = 250, - end = 300, - dataMinStart = gd._fullLayout.xaxis.rangeslider.d2p(0), - diff = end - start; + var dataMinStart; + + plotMock().then(function() { + dataMinStart = gd._fullLayout.xaxis.rangeslider.d2p(0); expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - slide(start, sliderY, end, sliderY).then(function() { - var maskMin = children[2], - handleMin = children[5]; + return slide(start, sliderY, end, sliderY); + }) + .then(function() { + var maskMin = getRangeSliderChild(2), + handleMin = getRangeSliderChild(5); + + expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); + expect(+maskMin.getAttribute('width')).toBeCloseTo(String(diff)); + testTranslate1D(handleMin, dataMinStart + diff - 3); + }) + .catch(failTest) + .then(done); + }); - expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); - expect(+maskMin.getAttribute('width')).toBeCloseTo(String(diff)); - testTranslate1D(handleMin, dataMinStart + diff - 3); - }) - .catch(failTest) - .then(done); - }); + it('should react to moving the slidebox right to left', function(done) { + var start = 300; + var end = 250; + var diff = end - start; - it('should react to moving the slidebox right to left', function(done) { - var start = 300, - end = 250, - dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), - diff = end - start; + var dataMaxStart; - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + plotMock().then(function() { + dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49); - slide(start, sliderY, end, sliderY).then(function() { - var maskMax = children[3], - handleMax = children[6]; + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); - expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); - testTranslate1D(handleMax, dataMaxStart + diff); - }) - .catch(failTest) - .then(done); - }); + return slide(start, sliderY, end, sliderY); + }) + .then(function() { + var maskMax = getRangeSliderChild(3), + handleMax = getRangeSliderChild(6); + + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); + expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); + testTranslate1D(handleMax, dataMaxStart + diff); + }) + .catch(failTest) + .then(done); + }); - it('should resize the main plot when rangeslider has moved', function(done) { - var start = 300, - end = 400, - rangeDiff1 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0], - rangeDiff2, - rangeDiff3; - - slide(start, sliderY, end, sliderY).then(function() { - rangeDiff2 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0]; - expect(rangeDiff2).toBeLessThan(rangeDiff1); - }).then(function() { - start = 400; - end = 200; - - return slide(start, sliderY, end, sliderY); - }).then(function() { - rangeDiff3 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0]; - expect(rangeDiff3).toBeLessThan(rangeDiff2); - }).then(done); - }); + it('should resize the main plot when rangeslider has moved', function(done) { + var start = 300; + var end = 400; + var rangeDiff1; + + var rangeDiff2, rangeDiff3; + + plotMock().then(function() { + rangeDiff1 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0]; + + return slide(start, sliderY, end, sliderY); + }) + .then(function() { + rangeDiff2 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0]; + expect(rangeDiff2).toBeLessThan(rangeDiff1); + }) + .then(function() { + start = 400; + end = 200; + + return slide(start, sliderY, end, sliderY); + }) + .then(function() { + rangeDiff3 = gd._fullLayout.xaxis.range[1] - gd._fullLayout.xaxis.range[0]; + expect(rangeDiff3).toBeLessThan(rangeDiff2); + }) + .catch(failTest) + .then(done); + }); - it('should relayout with relayout "array syntax"', function(done) { - Plotly.relayout(gd, 'xaxis.range', [10, 20]).then(function() { - var maskMin = children[2], - maskMax = children[3], - handleMin = children[5], - handleMax = children[6]; - - expect(+maskMin.getAttribute('width')).toBeWithin(125, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(365, TOL); - testTranslate1D(handleMin, 123.32); - testTranslate1D(handleMax, 252.65); - }) - .catch(failTest) - .then(done); - }); + it('should relayout with relayout "array syntax"', function(done) { + plotMock().then(function() { + return Plotly.relayout(gd, 'xaxis.range', [10, 20]); + }) + .then(function() { + var maskMin = getRangeSliderChild(2), + maskMax = getRangeSliderChild(3), + handleMin = getRangeSliderChild(5), + handleMax = getRangeSliderChild(6); + + expect(+maskMin.getAttribute('width')).toBeWithin(125, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(365, TOL); + testTranslate1D(handleMin, 123.32); + testTranslate1D(handleMax, 252.65); + }) + .catch(failTest) + .then(done); + }); - it('should relayout with relayout "element syntax"', function(done) { - Plotly.relayout(gd, 'xaxis.range[0]', 10).then(function() { - var maskMin = children[2], - maskMax = children[3], - handleMin = children[5], - handleMax = children[6]; - - expect(+maskMin.getAttribute('width')).toBeWithin(126, TOL); - expect(+maskMax.getAttribute('width')).toEqual(0); - testTranslate1D(handleMin, 123.32); - testTranslate1D(handleMax, 617); - }) - .catch(failTest) - .then(done); - }); + it('should relayout with relayout "element syntax"', function(done) { + plotMock().then(function() { + return Plotly.relayout(gd, 'xaxis.range[0]', 10); + }) + .then(function() { + var maskMin = getRangeSliderChild(2), + maskMax = getRangeSliderChild(3), + handleMin = getRangeSliderChild(5), + handleMax = getRangeSliderChild(6); + + expect(+maskMin.getAttribute('width')).toBeWithin(126, TOL); + expect(+maskMax.getAttribute('width')).toEqual(0); + testTranslate1D(handleMin, 123.32); + testTranslate1D(handleMax, 617); + }) + .catch(failTest) + .then(done); + }); - it('should relayout with style options', function(done) { - var bg = children[0], - maskMin = children[2], - maskMax = children[3]; - - var maskMinWidth, maskMaxWidth; - - Plotly.relayout(gd, 'xaxis.range', [5, 10]).then(function() { - maskMinWidth = +maskMin.getAttribute('width'), - maskMaxWidth = +maskMax.getAttribute('width'); - - return Plotly.relayout(gd, 'xaxis.rangeslider.bgcolor', 'red'); - }) - .then(function() { - expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); - expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - - expect(bg.getAttribute('fill')).toBe('red'); - expect(bg.getAttribute('stroke')).toBe('black'); - expect(bg.getAttribute('stroke-width')).toBe('2'); - - return Plotly.relayout(gd, 'xaxis.rangeslider.bordercolor', 'blue'); - }) - .then(function() { - expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); - expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - - expect(bg.getAttribute('fill')).toBe('red'); - expect(bg.getAttribute('stroke')).toBe('blue'); - expect(bg.getAttribute('stroke-width')).toBe('2'); - - return Plotly.relayout(gd, 'xaxis.rangeslider.borderwidth', 3); - }) - .then(function() { - expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); - expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - - expect(bg.getAttribute('fill')).toBe('red'); - expect(bg.getAttribute('stroke')).toBe('blue'); - expect(bg.getAttribute('stroke-width')).toBe('3'); - }) - .then(done); - }); + it('should relayout with style options', function(done) { + var bg, maskMin, maskMax, maskMinWidth, maskMaxWidth; - it('should relayout on size / domain udpate', function(done) { - var maskMin = children[2], - maskMax = children[3]; + plotMock().then(function() { + bg = getRangeSliderChild(0); + maskMin = getRangeSliderChild(2); + maskMax = getRangeSliderChild(3); - Plotly.relayout(gd, 'xaxis.range', [5, 10]).then(function() { - expect(+maskMin.getAttribute('width')).toBeWithin(63.16, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(492.67, TOL); + return Plotly.relayout(gd, 'xaxis.range', [5, 10]); + }) + .then(function() { + maskMinWidth = +maskMin.getAttribute('width'), + maskMaxWidth = +maskMax.getAttribute('width'); - return Plotly.relayout(gd, 'xaxis.domain', [0.3, 0.7]); - }) - .then(function() { - var maskMin = children[2], - maskMax = children[3]; + return Plotly.relayout(gd, 'xaxis.rangeslider.bgcolor', 'red'); + }) + .then(function() { + expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); + expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - expect(+maskMin.getAttribute('width')).toBeWithin(25.26, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(197.06, TOL); + expect(bg.getAttribute('fill')).toBe('red'); + expect(bg.getAttribute('stroke')).toBe('black'); + expect(bg.getAttribute('stroke-width')).toBe('2'); - return Plotly.relayout(gd, 'width', 400); - }) - .then(function() { - var maskMin = children[2], - maskMax = children[3]; + return Plotly.relayout(gd, 'xaxis.rangeslider.bordercolor', 'blue'); + }) + .then(function() { + expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); + expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); - expect(+maskMin.getAttribute('width')).toBeWithin(9.22, TOL); - expect(+maskMax.getAttribute('width')).toBeWithin(71.95, TOL); + expect(bg.getAttribute('fill')).toBe('red'); + expect(bg.getAttribute('stroke')).toBe('blue'); + expect(bg.getAttribute('stroke-width')).toBe('2'); - }) - .then(done); - }); + return Plotly.relayout(gd, 'xaxis.rangeslider.borderwidth', 3); + }) + .then(function() { + expect(+maskMin.getAttribute('width')).toEqual(maskMinWidth); + expect(+maskMax.getAttribute('width')).toEqual(maskMaxWidth); + + expect(bg.getAttribute('fill')).toBe('red'); + expect(bg.getAttribute('stroke')).toBe('blue'); + expect(bg.getAttribute('stroke-width')).toBe('3'); + }) + .catch(failTest) + .then(done); }); + it('should relayout on size / domain udpate', function(done) { + var maskMin, maskMax; - describe('visibility property', function() { - beforeEach(function() { - gd = createGraphDiv(); - }); + plotMock().then(function() { + maskMin = getRangeSliderChild(2), + maskMax = getRangeSliderChild(3); - afterEach(destroyGraphDiv); + return Plotly.relayout(gd, 'xaxis.range', [5, 10]); + }) + .then(function() { + expect(+maskMin.getAttribute('width')).toBeWithin(63.16, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(492.67, TOL); - it('should not add the slider to the DOM by default', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).not.toBeDefined(); - }) - .then(done); - }); + return Plotly.relayout(gd, 'xaxis.domain', [0.3, 0.7]); + }) + .then(function() { + var maskMin = getRangeSliderChild(2), + maskMax = getRangeSliderChild(3); - it('should add the slider if rangeslider is set to anything', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) - .then(function() { - return Plotly.relayout(gd, 'xaxis.rangeslider', 'exists'); - }) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); + expect(+maskMin.getAttribute('width')).toBeWithin(25.26, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(197.06, TOL); - it('should add the slider if visible changed to `true`', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) - .then(function() { - return Plotly.relayout(gd, 'xaxis.rangeslider.visible', true); - }) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).toBeDefined(); - expect(countRangeSliderClipPaths()).toEqual(1); - }) - .then(done); - }); + return Plotly.relayout(gd, 'width', 400); + }) + .then(function() { + var maskMin = getRangeSliderChild(2), + maskMax = getRangeSliderChild(3); - it('should remove the slider if changed to `false` or `undefined`', function(done) { - Plotly.plot(gd, [{ - x: [1, 2, 3], - y: [2, 3, 4] - }], { - xaxis: { - rangeslider: { visible: true } - } - }) - .then(function() { - return Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); - }) - .then(function() { - var rangeSlider = getRangeSlider(); - expect(rangeSlider).not.toBeDefined(); - expect(countRangeSliderClipPaths()).toEqual(0); - }) - .then(done); - }); + expect(+maskMin.getAttribute('width')).toBeWithin(9.22, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(71.95, TOL); - it('should clear traces in range plot when needed', function(done) { + }) + .catch(failTest) + .then(done); + }); +}); - function count(query) { - return d3.select(getRangeSlider()).selectAll(query).size(); - } +describe('Rangeslider visibility property', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); - Plotly.plot(gd, [{ - type: 'scatter', - x: [1, 2, 3], - y: [2, 1, 2] - }, { - type: 'bar', - x: [1, 2, 3], - y: [2, 5, 2] - }], { - xaxis: { - rangeslider: { visible: true } - } - }) - .then(function() { - expect(count('g.scatterlayer > g.trace')).toEqual(1); - expect(count('g.barlayer > g.trace')).toEqual(1); - - return Plotly.restyle(gd, 'visible', false); - }) - .then(function() { - expect(count('g.scatterlayer > g.trace')).toEqual(0); - expect(count('g.barlayer > g.trace')).toEqual(0); - - return Plotly.restyle(gd, 'visible', true); - }) - .then(function() { - expect(count('g.scatterlayer > g.trace')).toEqual(1); - expect(count('g.barlayer > g.trace')).toEqual(1); - - return Plotly.deleteTraces(gd, [0, 1]); - }) - .then(function() { - expect(count('g.scatterlayer > g.trace')).toEqual(0); - expect(count('g.barlayer > g.trace')).toEqual(0); - - return Plotly.addTraces(gd, [{ - type: 'heatmap', - z: [[1, 2, 3], [2, 1, 3]] - }]); - }) - .then(function() { - expect(count('g.imagelayer > g.hm')).toEqual(1); - - return Plotly.restyle(gd, 'visible', false); - }) - .then(function() { - expect(count('g.imagelayer > g.hm')).toEqual(0); - - return Plotly.restyle(gd, { - visible: true, - type: 'contour' - }); - }) - .then(function() { - expect(count('g.maplayer > g.contour')).toEqual(1); - - return Plotly.restyle(gd, 'type', 'heatmap'); - }) - .then(function() { - expect(count('g.imagelayer > g.hm')).toEqual(1); - expect(count('g.maplayer > g.contour')).toEqual(0); - - return Plotly.restyle(gd, 'type', 'contour'); - }) - .then(function() { - expect(count('g.imagelayer > g.hm')).toEqual(0); - expect(count('g.maplayer > g.contour')).toEqual(1); - - return Plotly.deleteTraces(gd, [0]); - }) - .then(function() { - expect(count('g.imagelayer > g.hm')).toEqual(0); - expect(count('g.maplayer > g.contour')).toEqual(0); - }) - .then(done); + afterEach(destroyGraphDiv); - }); + it('should not add the slider to the DOM by default', function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).not.toBeDefined(); + }) + .catch(failTest) + .then(done); }); - describe('handleDefaults function', function() { + it('should add the slider if rangeslider is set to anything', function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + .then(function() { + return Plotly.relayout(gd, 'xaxis.rangeslider', 'exists'); + }) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).toBeDefined(); + }) + .catch(failTest) + .then(done); + }); - function _supply(layoutIn, layoutOut, axName) { - setConvert(layoutOut[axName]); - RangeSlider.handleDefaults(layoutIn, layoutOut, axName); - } + it('should add the slider if visible changed to `true`', function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) + .then(function() { + return Plotly.relayout(gd, 'xaxis.rangeslider.visible', true); + }) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).toBeDefined(); + expect(countRangeSliderClipPaths()).toEqual(1); + }) + .catch(failTest) + .then(done); + }); - it('should not coerce anything if rangeslider isn\'t set', function() { - var layoutIn = { xaxis: {} }, - layoutOut = { xaxis: {} }, - expected = { xaxis: {} }; + it('should remove the slider if changed to `false` or `undefined`', function(done) { + Plotly.plot(gd, [{ + x: [1, 2, 3], + y: [2, 3, 4] + }], { + xaxis: { + rangeslider: { visible: true } + } + }) + .then(function() { + return Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); + }) + .then(function() { + var rangeSlider = getRangeSlider(); + expect(rangeSlider).not.toBeDefined(); + expect(countRangeSliderClipPaths()).toEqual(0); + }) + .catch(failTest) + .then(done); + }); - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutIn).toEqual(expected); - }); + it('should clear traces in range plot when needed', function(done) { - it('should not mutate layoutIn', function() { - var layoutIn = { xaxis: { rangeslider: { visible: true }} }, - layoutOut = { xaxis: { rangeslider: {}} }, - expected = { xaxis: { rangeslider: { visible: true}} }; + function count(query) { + return d3.select(getRangeSlider()).selectAll(query).size(); + } - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutIn).toEqual(expected); - }); + Plotly.plot(gd, [{ + type: 'scatter', + x: [1, 2, 3], + y: [2, 1, 2] + }, { + type: 'bar', + x: [1, 2, 3], + y: [2, 5, 2] + }], { + xaxis: { + rangeslider: { visible: true } + } + }) + .then(function() { + expect(count('g.scatterlayer > g.trace')).toEqual(1); + expect(count('g.barlayer > g.trace')).toEqual(1); + + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + expect(count('g.scatterlayer > g.trace')).toEqual(0); + expect(count('g.barlayer > g.trace')).toEqual(0); + + return Plotly.restyle(gd, 'visible', true); + }) + .then(function() { + expect(count('g.scatterlayer > g.trace')).toEqual(1); + expect(count('g.barlayer > g.trace')).toEqual(1); + + return Plotly.deleteTraces(gd, [0, 1]); + }) + .then(function() { + expect(count('g.scatterlayer > g.trace')).toEqual(0); + expect(count('g.barlayer > g.trace')).toEqual(0); + + return Plotly.addTraces(gd, [{ + type: 'heatmap', + z: [[1, 2, 3], [2, 1, 3]] + }]); + }) + .then(function() { + expect(count('g.imagelayer > g.hm')).toEqual(1); + + return Plotly.restyle(gd, 'visible', false); + }) + .then(function() { + expect(count('g.imagelayer > g.hm')).toEqual(0); + + return Plotly.restyle(gd, { + visible: true, + type: 'contour' + }); + }) + .then(function() { + expect(count('g.maplayer > g.contour')).toEqual(1); + + return Plotly.restyle(gd, 'type', 'heatmap'); + }) + .then(function() { + expect(count('g.imagelayer > g.hm')).toEqual(1); + expect(count('g.maplayer > g.contour')).toEqual(0); + + return Plotly.restyle(gd, 'type', 'contour'); + }) + .then(function() { + expect(count('g.imagelayer > g.hm')).toEqual(0); + expect(count('g.maplayer > g.contour')).toEqual(1); + + return Plotly.deleteTraces(gd, [0]); + }) + .then(function() { + expect(count('g.imagelayer > g.hm')).toEqual(0); + expect(count('g.maplayer > g.contour')).toEqual(0); + }) + .catch(failTest) + .then(done); + }); +}); - it('should set defaults if rangeslider is set to anything truthy', function() { - var layoutIn = { xaxis: { rangeslider: {} }}, - layoutOut = { xaxis: {} }, - expected = { - visible: true, - autorange: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }; - - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); - }); +describe('Rangeslider handleDefaults function', function() { - it('should set defaults if rangeslider.visible is true', function() { - var layoutIn = { xaxis: { rangeslider: { visible: true }} }, - layoutOut = { xaxis: { rangeslider: {}} }, - expected = { - visible: true, - autorange: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }; - - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); - }); + function _supply(layoutIn, layoutOut, axName) { + setConvert(layoutOut[axName]); + RangeSlider.handleDefaults(layoutIn, layoutOut, axName); + } - it('should return early if *visible: false*', function() { - var layoutIn = { xaxis: { rangeslider: { visible: false, range: [10, 20] }} }, - layoutOut = { xaxis: { rangeslider: {}} }; + it('should not coerce anything if rangeslider isn\'t set', function() { + var layoutIn = { xaxis: {} }, + layoutOut = { xaxis: {} }, + expected = { xaxis: {} }; - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual({ visible: false }); - }); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutIn).toEqual(expected); + }); - it('should set defaults if properties are invalid', function() { - var layoutIn = { xaxis: { rangeslider: { - visible: 'invalid', - thickness: 'invalid', - bgcolor: 42, - bordercolor: 42, - borderwidth: 'superfat' - }}}, - layoutOut = { xaxis: {} }, - expected = { - visible: true, - autorange: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }; - - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); - }); + it('should not mutate layoutIn', function() { + var layoutIn = { xaxis: { rangeslider: { visible: true }} }, + layoutOut = { xaxis: { rangeslider: {}} }, + expected = { xaxis: { rangeslider: { visible: true}} }; - it('should set autorange to true when range input is invalid', function() { - var layoutIn = { xaxis: { rangeslider: { range: 'not-gonna-work'}} }, - layoutOut = { xaxis: {} }, - expected = { - visible: true, - autorange: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }; - - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider).toEqual(expected); - }); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutIn).toEqual(expected); + }); - it('should default \'bgcolor\' to layout \'plot_bgcolor\'', function() { - var layoutIn = { - xaxis: { rangeslider: true } + it('should set defaults if rangeslider is set to anything truthy', function() { + var layoutIn = { xaxis: { rangeslider: {} }}, + layoutOut = { xaxis: {} }, + expected = { + visible: true, + autorange: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - var layoutOut = { - xaxis: { range: [2, 40]}, - plot_bgcolor: 'blue' + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); + }); + + it('should set defaults if rangeslider.visible is true', function() { + var layoutIn = { xaxis: { rangeslider: { visible: true }} }, + layoutOut = { xaxis: { rangeslider: {}} }, + expected = { + visible: true, + autorange: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - _supply(layoutIn, layoutOut, 'xaxis'); - expect(layoutOut.xaxis.rangeslider.bgcolor).toEqual('blue'); - }); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); }); - describe('yaxis options', function() { + it('should return early if *visible: false*', function() { + var layoutIn = { xaxis: { rangeslider: { visible: false, range: [10, 20] }} }, + layoutOut = { xaxis: { rangeslider: {}} }; - it('should be set one yaxis is present', function() { - var mock = { - layout: { - xaxis: { rangeslider: {} }, - yaxis: { } - } - }; + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual({ visible: false }); + }); - supplyAllDefaults(mock); + it('should set defaults if properties are invalid', function() { + var layoutIn = { xaxis: { rangeslider: { + visible: 'invalid', + thickness: 'invalid', + bgcolor: 42, + bordercolor: 42, + borderwidth: 'superfat' + }}}, + layoutOut = { xaxis: {} }, + expected = { + visible: true, + autorange: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider + }; - expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); - }); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); + }); - it('should set multiple yaxis with data are present', function() { - var mock = { - data: [ - {y: [1, 2]}, - {y: [1, 2], yaxis: 'y2'} - ], - layout: { - xaxis: { rangeslider: {} }, - yaxis: { }, - yaxis2: { }, - yaxis3: { } - } + it('should set autorange to true when range input is invalid', function() { + var layoutIn = { xaxis: { rangeslider: { range: 'not-gonna-work'}} }, + layoutOut = { xaxis: {} }, + expected = { + visible: true, + autorange: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - supplyAllDefaults(mock); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); + }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'match' }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual(undefined); - }); + it('should default \'bgcolor\' to layout \'plot_bgcolor\'', function() { + var layoutIn = { + xaxis: { rangeslider: true } + }; - it('should honor user settings', function() { - var mock = { - data: [ - {y: [1, 2]}, - {y: [1, 2], yaxis: 'y2'}, - {y: [1, 2], yaxis: 'y3'} - ], - layout: { - xaxis: { rangeslider: { - yaxis: { rangemode: 'auto' }, - yaxis2: { rangemode: 'fixed' }, - yaxis3: { range: [0, 1] } - } }, - yaxis: { }, - yaxis2: { }, - yaxis3: { } - } - }; + var layoutOut = { + xaxis: { range: [2, 40]}, + plot_bgcolor: 'blue' + }; - supplyAllDefaults(mock); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider.bgcolor).toEqual('blue'); + }); +}); - expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'auto', range: [-1, 4] }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'fixed', range: [-1, 4] }); - expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual({ rangemode: 'fixed', range: [0, 1] }); - }); +describe('Rangeslider yaxis options', function() { + + it('should be set one yaxis is present', function() { + var mock = { + layout: { + xaxis: { rangeslider: {} }, + yaxis: { } + } + }; + + supplyAllDefaults(mock); + + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); }); - describe('anchored axes fixedrange', function() { - - it('should default to *true* when range slider is visible', function() { - var mock = { - data: [ - {y: [1, 2]}, - {y: [1, 2], yaxis: 'y2'}, - {y: [1, 2], yaxis: 'y3'} - ], - layout: { - xaxis: { rangeslider: {} }, - yaxis: { anchor: 'x' }, - yaxis2: { anchor: 'x' }, - yaxis3: { anchor: 'free' } - } - }; + it('should set multiple yaxis with data are present', function() { + var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'} + ], + layout: { + xaxis: { rangeslider: {} }, + yaxis: { }, + yaxis2: { }, + yaxis3: { } + } + }; - supplyAllDefaults(mock); + supplyAllDefaults(mock); - expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); - expect(mock._fullLayout.yaxis.fixedrange).toBe(true); - expect(mock._fullLayout.yaxis2.fixedrange).toBe(true); - expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); - }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'match' }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual(undefined); + }); - it('should honor user settings', function() { - var mock = { - data: [ - {y: [1, 2]}, - {y: [1, 2], yaxis: 'y2'}, - {y: [1, 2], yaxis: 'y3'} - ], - layout: { - xaxis: { rangeslider: {} }, - yaxis: { anchor: 'x', fixedrange: false }, - yaxis2: { anchor: 'x', fixedrange: false }, - yaxis3: { anchor: 'free' } - } - }; + it('should honor user settings', function() { + var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'}, + {y: [1, 2], yaxis: 'y3'} + ], + layout: { + xaxis: { rangeslider: { + yaxis: { rangemode: 'auto' }, + yaxis2: { rangemode: 'fixed' }, + yaxis3: { range: [0, 1] } + } }, + yaxis: { }, + yaxis2: { }, + yaxis3: { } + } + }; - supplyAllDefaults(mock); + supplyAllDefaults(mock); - expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); - expect(mock._fullLayout.yaxis.fixedrange).toBe(false); - expect(mock._fullLayout.yaxis2.fixedrange).toBe(false); - expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); - }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'auto', range: [-1, 4] }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis2).toEqual({ rangemode: 'fixed', range: [-1, 4] }); + expect(mock._fullLayout.xaxis.rangeslider.yaxis3).toEqual({ rangemode: 'fixed', range: [0, 1] }); + }); +}); +describe('Rangeslider anchored axes fixedrange', function() { + + it('should default to *true* when range slider is visible', function() { + var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'}, + {y: [1, 2], yaxis: 'y3'} + ], + layout: { + xaxis: { rangeslider: {} }, + yaxis: { anchor: 'x' }, + yaxis2: { anchor: 'x' }, + yaxis3: { anchor: 'free' } + } + }; + + supplyAllDefaults(mock); + + expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); + expect(mock._fullLayout.yaxis.fixedrange).toBe(true); + expect(mock._fullLayout.yaxis2.fixedrange).toBe(true); + expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); }); - describe('in general', function() { + it('should honor user settings', function() { + var mock = { + data: [ + {y: [1, 2]}, + {y: [1, 2], yaxis: 'y2'}, + {y: [1, 2], yaxis: 'y3'} + ], + layout: { + xaxis: { rangeslider: {} }, + yaxis: { anchor: 'x', fixedrange: false }, + yaxis2: { anchor: 'x', fixedrange: false }, + yaxis3: { anchor: 'free' } + } + }; - beforeEach(function() { - gd = createGraphDiv(); - }); + supplyAllDefaults(mock); - afterEach(destroyGraphDiv); + expect(mock._fullLayout.xaxis.rangeslider.visible).toBe(true); + expect(mock._fullLayout.yaxis.fixedrange).toBe(false); + expect(mock._fullLayout.yaxis2.fixedrange).toBe(false); + expect(mock._fullLayout.yaxis3.fixedrange).toBe(false); + }); - function assertRange(axRange, rsRange) { - // lower toBeCloseToArray precision for FF38 on CI - var precision = 1e-2; +}); - expect(gd.layout.xaxis.range).toBeCloseToArray(axRange, precision); - expect(gd.layout.xaxis.rangeslider.range).toBeCloseToArray(rsRange, precision); - } +describe('rangesliders in general', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); - it('should plot when only x data is provided', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) - .then(function() { - var rangeSlider = getRangeSlider(); + afterEach(destroyGraphDiv); - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); + function assertRange(axRange, rsRange) { + // lower toBeCloseToArray precision for FF38 on CI + var precision = 1e-2; - it('should plot when only y data is provided', function(done) { - Plotly.plot(gd, [{ y: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) - .then(function() { - var rangeSlider = getRangeSlider(); + expect(gd.layout.xaxis.range).toBeCloseToArray(axRange, precision); + expect(gd.layout.xaxis.rangeslider.range).toBeCloseToArray(rsRange, precision); + } - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); + it('should plot when only x data is provided', function(done) { + Plotly.plot(gd, [{ x: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) + .then(function() { + var rangeSlider = getRangeSlider(); - it('should expand its range in accordance with new data arrays', function(done) { - Plotly.plot(gd, [{ - y: [2, 1, 2] - }], { - xaxis: { rangeslider: {} } - }) - .then(function() { - assertRange([-0.13, 2.13], [-0.13, 2.13]); - - return Plotly.restyle(gd, 'y', [[2, 1, 2, 1]]); - }) - .then(function() { - assertRange([-0.19, 3.19], [-0.19, 3.19]); - - return Plotly.extendTraces(gd, { y: [[2, 1]] }, [0]); - }) - .then(function() { - assertRange([-0.32, 5.32], [-0.32, 5.32]); - - return Plotly.addTraces(gd, { x: [0, 10], y: [2, 1] }); - }) - .then(function() { - assertRange([-0.68, 10.68], [-0.68, 10.68]); - - return Plotly.deleteTraces(gd, [1]); - }) - .then(function() { - assertRange([-0.31, 5.31], [-0.31, 5.31]); - }) - .then(done); - }); + expect(rangeSlider).toBeDefined(); + }) + .catch(failTest) + .then(done); + }); - it('should not expand its range when range slider range is set', function(done) { - Plotly.plot(gd, [{ - y: [2, 1, 2] - }], { - xaxis: { rangeslider: { range: [-1, 11] } } - }) - .then(function() { - assertRange([-0.13, 2.13], [-1, 11]); - - return Plotly.restyle(gd, 'y', [[2, 1, 2, 1]]); - }) - .then(function() { - assertRange([-0.19, 3.19], [-1, 11]); - - return Plotly.extendTraces(gd, { y: [[2, 1]] }, [0]); - }) - .then(function() { - assertRange([-0.32, 5.32], [-1, 11]); - - return Plotly.addTraces(gd, { x: [0, 10], y: [2, 1] }); - }) - .then(function() { - assertRange([-0.68, 10.68], [-1, 11]); - - return Plotly.deleteTraces(gd, [1]); - }) - .then(function() { - assertRange([-0.31, 5.31], [-1, 11]); - - return Plotly.update(gd, { - y: [[2, 1, 2, 1, 2]] - }, { - 'xaxis.rangeslider.autorange': true - }); - }) - .then(function() { - assertRange([-0.26, 4.26], [-0.26, 4.26]); - - // smaller than xaxis.range - won't be accepted - return Plotly.relayout(gd, {'xaxis.rangeslider.range': [0, 2]}); - }) - .then(function() { - assertRange([-0.26, 4.26], [-0.26, 4.26]); - - // will be accepted (and autorange is disabled by impliedEdits) - return Plotly.relayout(gd, {'xaxis.rangeslider.range': [-2, 12]}); - }) - .then(function() { - assertRange([-0.26, 4.26], [-2, 12]); - }) - .then(done); - }); + it('should plot when only y data is provided', function(done) { + Plotly.plot(gd, [{ y: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) + .then(function() { + var rangeSlider = getRangeSlider(); - it('should configure yaxis opts on relayout', function(done) { - Plotly.plot(gd, [{ - y: [2, 1, 2] - }], { - xaxis: { rangeslider: { yaxis: { range: [-10, 20] } } } - }) - .then(function() { - expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'fixed', range: [-10, 20] }); - - return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'auto'); - }) - .then(function() { - var precision = 1e-2; - - expect(gd.layout.xaxis.rangeslider.yaxis.rangemode).toEqual('auto'); - expect(gd.layout.xaxis.rangeslider.yaxis.range) - .toBeCloseToArray([0.920, 2.079], precision); - - return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'match'); - }) - .then(function() { - expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); - }) - .then(done); - }); + expect(rangeSlider).toBeDefined(); + }) + .catch(failTest) + .then(done); + }); + + it('should expand its range in accordance with new data arrays', function(done) { + Plotly.plot(gd, [{ + y: [2, 1, 2] + }], { + xaxis: { rangeslider: {} } + }) + .then(function() { + assertRange([-0.13, 2.13], [-0.13, 2.13]); + + return Plotly.restyle(gd, 'y', [[2, 1, 2, 1]]); + }) + .then(function() { + assertRange([-0.19, 3.19], [-0.19, 3.19]); + + return Plotly.extendTraces(gd, { y: [[2, 1]] }, [0]); + }) + .then(function() { + assertRange([-0.32, 5.32], [-0.32, 5.32]); + + return Plotly.addTraces(gd, { x: [0, 10], y: [2, 1] }); + }) + .then(function() { + assertRange([-0.68, 10.68], [-0.68, 10.68]); + + return Plotly.deleteTraces(gd, [1]); + }) + .then(function() { + assertRange([-0.31, 5.31], [-0.31, 5.31]); + }) + .catch(failTest) + .then(done); }); -}); + it('should not expand its range when range slider range is set', function(done) { + Plotly.plot(gd, [{ + y: [2, 1, 2] + }], { + xaxis: { rangeslider: { range: [-1, 11] } } + }) + .then(function() { + assertRange([-0.13, 2.13], [-1, 11]); + + return Plotly.restyle(gd, 'y', [[2, 1, 2, 1]]); + }) + .then(function() { + assertRange([-0.19, 3.19], [-1, 11]); + + return Plotly.extendTraces(gd, { y: [[2, 1]] }, [0]); + }) + .then(function() { + assertRange([-0.32, 5.32], [-1, 11]); + + return Plotly.addTraces(gd, { x: [0, 10], y: [2, 1] }); + }) + .then(function() { + assertRange([-0.68, 10.68], [-1, 11]); + + return Plotly.deleteTraces(gd, [1]); + }) + .then(function() { + assertRange([-0.31, 5.31], [-1, 11]); + + return Plotly.update(gd, { + y: [[2, 1, 2, 1, 2]] + }, { + 'xaxis.rangeslider.autorange': true + }); + }) + .then(function() { + assertRange([-0.26, 4.26], [-0.26, 4.26]); + + // smaller than xaxis.range - won't be accepted + return Plotly.relayout(gd, {'xaxis.rangeslider.range': [0, 2]}); + }) + .then(function() { + assertRange([-0.26, 4.26], [-0.26, 4.26]); + + // will be accepted (and autorange is disabled by impliedEdits) + return Plotly.relayout(gd, {'xaxis.rangeslider.range': [-2, 12]}); + }) + .then(function() { + assertRange([-0.26, 4.26], [-2, 12]); + }) + .catch(failTest) + .then(done); + }); + + it('should configure yaxis opts on relayout', function(done) { + Plotly.plot(gd, [{ + y: [2, 1, 2] + }], { + xaxis: { rangeslider: { yaxis: { range: [-10, 20] } } } + }) + .then(function() { + expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'fixed', range: [-10, 20] }); + + return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'auto'); + }) + .then(function() { + var precision = 2; + + expect(gd.layout.xaxis.rangeslider.yaxis.rangemode).toEqual('auto'); + expect(gd.layout.xaxis.rangeslider.yaxis.range) + .toBeCloseToArray([0.920, 2.079], precision); + + return Plotly.relayout(gd, 'xaxis.rangeslider.yaxis.rangemode', 'match'); + }) + .then(function() { + expect(gd.layout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + }) + .catch(failTest) + .then(done); + }); + + it('should update rangeslider x/y ranges when data changes even if main axes are not autoranged', function(done) { + Plotly.plot(gd, [{ + // use a heatmap because it doesn't add any padding + x0: 0, dx: 1, + y0: 1, dy: 1, + z: [[1, 2, 3], [2, 3, 4], [3, 4, 5]], + type: 'heatmap' + }], { + xaxis: { + range: [0, 2], + rangeslider: {yaxis: {rangemode: 'auto'}} + }, + yaxis: {range: [1.1, 3.1]} + }) + .then(function() { + expect(gd._fullLayout.xaxis.rangeslider.range).toBeCloseToArray([-0.5, 2.5], 3); + expect(gd._fullLayout.xaxis.rangeslider.yaxis.range).toBeCloseToArray([0.5, 3.5], 3); + + return Plotly.restyle(gd, {dx: 2, dy: 4}); + }) + .then(function() { + expect(gd._fullLayout.xaxis.rangeslider.range).toBeCloseToArray([-1, 5], 3); + expect(gd._fullLayout.xaxis.rangeslider.yaxis.range).toBeCloseToArray([-1, 11], 3); + }) + .catch(failTest) + .then(done); + }); +}); function slide(fromX, fromY, toX, toY) { return new Promise(function(resolve) { @@ -830,7 +921,7 @@ function slide(fromX, fromY, toX, toY) { mouseEvent('mouseup', toX, toY); setTimeout(function() { - return resolve(); + resolve(); }, 20); }); } From 95b8c1001768fa88070891a62a11e7a3f9bc9cf7 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 7 Mar 2018 14:41:08 -0500 Subject: [PATCH 36/37] test turning on autorange via rangeslider attrs this test fails if rangeslider.yaxis.rangemode or rangeslider.(auto)range doesn't have editType: 'calc' --- .../rangeslider/oppaxis_attributes.js | 2 +- test/jasmine/tests/range_slider_test.js | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/components/rangeslider/oppaxis_attributes.js b/src/components/rangeslider/oppaxis_attributes.js index decfbe73ac9..a02d5e7ad5c 100644 --- a/src/components/rangeslider/oppaxis_attributes.js +++ b/src/components/rangeslider/oppaxis_attributes.js @@ -36,7 +36,7 @@ module.exports = { {valType: 'any', editType: 'plot'}, {valType: 'any', editType: 'plot'} ], - editType: 'calc', + editType: 'plot', description: [ 'Sets the range of this axis for the rangeslider.' ].join(' ') diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 2003499549d..c1fde29f186 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -911,6 +911,67 @@ describe('rangesliders in general', function() { .catch(failTest) .then(done); }); + + it('should be able to turn on rangeslider x/y autorange if initially specified', function(done) { + Plotly.plot(gd, [{ + // use a heatmap because it doesn't add any padding + x0: 0, dx: 1, + y0: 1, dy: 1, + z: [[1, 2, 3], [2, 3, 4], [3, 4, 5]], + type: 'heatmap' + }], { + xaxis: { + range: [0.1, 1.9], + rangeslider: {range: [0, 2], yaxis: {range: [1, 3]}} + }, + yaxis: {range: [1.1, 2.9]} + }) + .then(function() { + expect(gd._fullLayout.xaxis.rangeslider.range).toBeCloseToArray([0, 2], 3); + expect(gd._fullLayout.xaxis.rangeslider.yaxis.range).toBeCloseToArray([1, 3], 3); + + return Plotly.relayout(gd, {'xaxis.rangeslider.yaxis.rangemode': 'auto'}); + }) + .then(function() { + expect(gd._fullLayout.xaxis.rangeslider.range).toBeCloseToArray([0, 2], 3); + expect(gd._fullLayout.xaxis.rangeslider.yaxis.range).toBeCloseToArray([0.5, 3.5], 3); + + return Plotly.relayout(gd, {'xaxis.rangeslider.autorange': true}); + }) + .then(function() { + expect(gd._fullLayout.xaxis.rangeslider.range).toBeCloseToArray([-0.5, 2.5], 3); + expect(gd._fullLayout.xaxis.rangeslider.yaxis.range).toBeCloseToArray([0.5, 3.5], 3); + }) + .catch(failTest) + .then(done); + }); + + it('should be able to turn on rangeslider x/y autorange implicitly by deleting x range', function(done) { + // this does not apply to y ranges, because the default there is 'match' + Plotly.plot(gd, [{ + // use a heatmap because it doesn't add any padding + x0: 0, dx: 1, + y0: 1, dy: 1, + z: [[1, 2, 3], [2, 3, 4], [3, 4, 5]], + type: 'heatmap' + }], { + xaxis: { + range: [0.1, 1.9], + rangeslider: {range: [0, 2], yaxis: {range: [1, 3]}} + }, + yaxis: {range: [1.1, 2.9]} + }) + .then(function() { + expect(gd._fullLayout.xaxis.rangeslider.range).toBeCloseToArray([0, 2], 3); + + return Plotly.relayout(gd, {'xaxis.rangeslider.range': null}); + }) + .then(function() { + expect(gd._fullLayout.xaxis.rangeslider.range).toBeCloseToArray([-0.5, 2.5], 3); + }) + .catch(failTest) + .then(done); + }); }); function slide(fromX, fromY, toX, toY) { From 4f54f624cb1ce087b186d40734906932f0e28696 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Wed, 7 Mar 2018 14:59:40 -0500 Subject: [PATCH 37/37] drop rangeslider style attributes to editType: plot and test --- src/components/rangeslider/attributes.js | 8 +++---- test/jasmine/tests/range_slider_test.js | 28 +++++++++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index 37268dff026..c5a57f61145 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -15,14 +15,14 @@ module.exports = { valType: 'color', dflt: colorAttributes.background, role: 'style', - editType: 'calc', + editType: 'plot', description: 'Sets the background color of the range slider.' }, bordercolor: { valType: 'color', dflt: colorAttributes.defaultLine, role: 'style', - editType: 'calc', + editType: 'plot', description: 'Sets the border color of the range slider.' }, borderwidth: { @@ -30,7 +30,7 @@ module.exports = { dflt: 0, min: 0, role: 'style', - editType: 'calc', + editType: 'plot', description: 'Sets the border color of the range slider.' }, autorange: { @@ -73,7 +73,7 @@ module.exports = { min: 0, max: 1, role: 'style', - editType: 'calc', + editType: 'plot', description: [ 'The height of the range slider as a fraction of the', 'total plot area height.' diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index c1fde29f186..1b4fc6cd83d 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -65,7 +65,7 @@ describe('Visible rangesliders', function() { .then(done); }); - it('should have the correct width and height', function(done) { + it('should have the correct style and size and be able to update these', function(done) { plotMock().then(function() { var bg = getRangeSliderChild(0); @@ -75,18 +75,26 @@ describe('Visible rangesliders', function() { // width incorporates border widths expect(+bg.getAttribute('width')).toEqual(expectedWidth); expect(+bg.getAttribute('height')).toEqual(66); - }) - .catch(failTest) - .then(done); - }); - - it('should have the correct style', function(done) { - plotMock().then(function() { - var bg = getRangeSliderChild(0); expect(bg.getAttribute('fill')).toBe('#fafafa'); expect(bg.getAttribute('stroke')).toBe('black'); - expect(bg.getAttribute('stroke-width')).toBe('2'); + expect(+bg.getAttribute('stroke-width')).toBe(2); + + return Plotly.relayout(gd, { + 'xaxis.rangeslider.thickness': 0.1, + 'xaxis.rangeslider.bgcolor': '#ffff80', + 'xaxis.rangeslider.bordercolor': '#404040', + 'xaxis.rangeslider.borderwidth': 1 + }); + }) + .then(function() { + var bg = getRangeSliderChild(0); + + expect(+bg.getAttribute('height')).toEqual(32); + + expect(bg.getAttribute('fill')).toBe('#ffff80'); + expect(bg.getAttribute('stroke')).toBe('#404040'); + expect(+bg.getAttribute('stroke-width')).toBe(1); }) .catch(failTest) .then(done);