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/src/components/rangeslider/constants.js b/src/components/rangeslider/constants.js index 0b535b82826..e3551a406a3 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)', + 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..5202aa2a7a6 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 oppAxisAttrs = require('./oppaxis_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(rangeContainerIn, rangeContainerOut, attr, dflt) { + return Lib.coerce(rangeContainerIn, rangeContainerOut, oppAxisAttrs, attr, dflt); + } + var visible = coerce('visible'); if(!visible) return; @@ -35,9 +41,40 @@ 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; + if(subplots) { + 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]; + + var rangeContainerIn = containerIn[yName] || {}; + var rangeContainerOut = containerOut[yName] = {}; + + var yAxOut = layoutOut[yName]; + + var rangemodeDflt; + if(rangeContainerIn.range && yAxOut.isValidRange(rangeContainerIn.range)) { + rangemodeDflt = 'fixed'; + } + + var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt); + if(rangeMode !== 'match') { + coerceRange(rangeContainerIn, rangeContainerOut, 'range', yAxOut.range.slice()); + } + yAxOut._rangesliderAutorange = (rangeMode === 'auto'); + } + } + // 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 7edd7e0da32..9fb61dfedd5 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -77,7 +77,9 @@ module.exports = function(gd) { // for all present range sliders rangeSliders.each(function(axisOpts) { var rangeSlider = d3.select(this), - opts = axisOpts[constants.name]; + opts = axisOpts[constants.name], + oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)], + oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)]; // update range // Expand slider range to the axis range @@ -141,13 +143,23 @@ module.exports = function(gd) { opts._rl = [range0, range1]; + if(oppAxisRangeOpts.rangemode !== 'match') { + var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]), + range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[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, oppAxisRangeOpts) .call(drawSlideBox, gd, axisOpts, opts) .call(drawGrabbers, gd, axisOpts, opts); @@ -155,7 +167,7 @@ module.exports = function(gd) { setupDragElement(rangeSlider, gd, axisOpts, opts); // update current range - setPixelRange(rangeSlider, gd, axisOpts, opts); + setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts); // title goes next to range slider instead of tick labels, so // just take it over and draw it from here @@ -284,13 +296,17 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) { }); } -function setPixelRange(rangeSlider, gd, axisOpts, opts) { +function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts) { 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); } @@ -309,6 +325,26 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); + if(oppAxisRangeOpts.rangemode !== 'match') { + 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; @@ -391,7 +427,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { isMainPlot = (i === 0); var oppAxisOpts = Axes.getFromId(gd, id, 'y'), - oppAxisName = oppAxisOpts._name; + oppAxisName = oppAxisOpts._name, + oppAxisRangeOpts = opts[oppAxisName]; var mockFigure = { data: [], @@ -412,7 +449,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) { mockFigure.layout[oppAxisName] = { type: oppAxisOpts.type, domain: [0, 1], - range: oppAxisOpts.range.slice(), + range: oppAxisRangeOpts.rangemode !== 'match' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(), calendar: oppAxisOpts.calendar }; @@ -453,7 +490,7 @@ function filterRangePlotCalcData(calcData, subplotId) { return out; } -function drawMasks(rangeSlider, gd, axisOpts, opts) { +function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) { var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName) .data([0]); @@ -477,6 +514,34 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) { maskMax .attr('height', opts._height) .call(Color.fill, constants.maskColor); + + // masks used for oppAxis zoom + if(oppAxisRangeOpts.rangemode !== 'match') { + 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.maskOppAxisColor); + + 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.maskOppAxisColor); + } } function drawSlideBox(rangeSlider, gd, axisOpts, opts) { 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 new file mode 100644 index 00000000000..a02d5e7ad5c --- /dev/null +++ b/src/components/rangeslider/oppaxis_attributes.js @@ -0,0 +1,45 @@ +/** +* 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 = { + // 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'], + dflt: 'match', + 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 `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: 'plot'}, + {valType: 'any', editType: 'plot'} + ], + editType: 'plot', + description: [ + 'Sets the range of this axis for the rangeslider.' + ].join(' ') + }, + editType: 'calc' +}; diff --git a/src/plots/cartesian/autorange.js b/src/plots/cartesian/autorange.js index f7639b5b7aa..797add39ac6 100644 --- a/src/plots/cartesian/autorange.js +++ b/src/plots/cartesian/autorange.js @@ -174,10 +174,11 @@ function makePadFn(ax) { } function doAutoRange(ax) { - ax.setScale(); + if(!ax._length) ax.setScale(); // 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 = getAutoRange(ax); @@ -188,14 +189,29 @@ function doAutoRange(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._anchorAxis && ax._anchorAxis.rangeslider) { + var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name]; + if(axeRangeOpts) { + if(axeRangeOpts.rangemode === 'auto') { + if(hasDeps) { + axeRangeOpts.range = getAutoRange(ax); + } else { + axeRangeOpts.range = ax._rangeInitial ? ax._rangeInitial.slice() : ax.range.slice(); + } + } + } + axIn = ax._anchorAxis._input; + axIn.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts); + } } 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 00000000000..0915d37b06d Binary files /dev/null and b/test/image/baselines/range_slider_rangemode.png differ 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/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/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index d88987fde43..1b4fc6cd83d 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -11,59 +11,63 @@ 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; -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 style and size and be able to update these', function(done) { + plotMock().then(function() { + var bg = getRangeSliderChild(0); var options = mock.layout.xaxis.rangeslider, expectedWidth = gd._fullLayout._size.w + options.borderwidth; @@ -71,656 +75,912 @@ describe('the range slider', function() { // width incorporates border widths expect(+bg.getAttribute('width')).toEqual(expectedWidth); expect(+bg.getAttribute('height')).toEqual(66); - }); - - it('should have the correct style', function() { - var bg = children[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); + }); - 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)'); - }).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); - }).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; + + var dataMinStart; - 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; + 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); - }).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); - }).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); - }) - .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); - }) - .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('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 return early if *visible: false*', function() { + var layoutIn = { xaxis: { rangeslider: { visible: false, range: [10, 20] }} }, + layoutOut = { xaxis: { rangeslider: {}} }; - supplyAllDefaults(mock); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual({ visible: false }); + }); - 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); - }); + 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 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 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); + }); + + it('should default \'bgcolor\' to layout \'plot_bgcolor\'', function() { + var layoutIn = { + xaxis: { rangeslider: true } + }; - 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); - }); + var layoutOut = { + xaxis: { range: [2, 40]}, + plot_bgcolor: 'blue' + }; + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider.bgcolor).toEqual('blue'); }); +}); - describe('in general', function() { +describe('Rangeslider yaxis options', function() { - beforeEach(function() { - gd = createGraphDiv(); - }); + it('should be set one yaxis is present', function() { + var mock = { + layout: { + xaxis: { rangeslider: {} }, + yaxis: { } + } + }; - afterEach(destroyGraphDiv); + supplyAllDefaults(mock); - function assertRange(axRange, rsRange) { - // lower toBeCloseToArray precision for FF38 on CI - var precision = 1e-2; + expect(mock._fullLayout.xaxis.rangeslider.yaxis).toEqual({ rangemode: 'match' }); + }); - expect(gd.layout.xaxis.range).toBeCloseToArray(axRange, precision); - expect(gd.layout.xaxis.rangeslider.range).toBeCloseToArray(rsRange, precision); - } + 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 plot when only x data is provided', function(done) { - Plotly.plot(gd, [{ x: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) - .then(function() { - var rangeSlider = getRangeSlider(); + supplyAllDefaults(mock); - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); + 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 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 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: { } + } + }; - expect(rangeSlider).toBeDefined(); - }) - .then(done); - }); + supplyAllDefaults(mock); - 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(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] }); + }); +}); - 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); - }); +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); }); + + 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' } + } + }; + + 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); + }); + }); +describe('rangesliders in general', function() { + var gd; + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + 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); + } + + 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(); + + expect(rangeSlider).toBeDefined(); + }) + .catch(failTest) + .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(); + + 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); + }); + + 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) { return new Promise(function(resolve) { @@ -730,7 +990,7 @@ function slide(fromX, fromY, toX, toY) { mouseEvent('mouseup', toX, toY); setTimeout(function() { - return resolve(); + resolve(); }, 20); }); } 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(); + }); });