diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index d81dfbfaecf..299a471b014 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -30,6 +30,16 @@ module.exports = { role: 'style', description: 'Sets the border color of the range slider.' }, + autorange: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the range slider range is', + 'computed in relation to the input data.', + 'If `range` is provided, then `autorange` is set to *false*.' + ].join(' ') + }, range: { valType: 'info_array', role: 'info', diff --git a/src/components/rangeslider/calc_autorange.js b/src/components/rangeslider/calc_autorange.js new file mode 100644 index 00000000000..6bd3fc03b5e --- /dev/null +++ b/src/components/rangeslider/calc_autorange.js @@ -0,0 +1,34 @@ +/** +* Copyright 2012-2017, 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'; + +var Axes = require('../../plots/cartesian/axes'); +var constants = require('./constants'); + +module.exports = function calcAutorange(gd) { + var axes = Axes.list(gd, 'x', true); + + // Compute new slider range using axis autorange if necessary. + // + // Copy back range to input range slider container to skip + // this step in subsequent draw calls. + + for(var i = 0; i < axes.length; i++) { + var ax = axes[i], + opts = ax[constants.name]; + + // Don't try calling getAutoRange if _min and _max are filled in. + // This happens on updates where the calc step is skipped. + + if(opts && opts.visible && opts.autorange && ax._min.length && ax._max.length) { + opts._input.autorange = true; + opts._input.range = opts.range = Axes.getAutoRange(ax); + } + } +}; diff --git a/src/components/rangeslider/constants.js b/src/components/rangeslider/constants.js index 9adc42e6407..b7cabcccac2 100644 --- a/src/components/rangeslider/constants.js +++ b/src/components/rangeslider/constants.js @@ -41,11 +41,10 @@ module.exports = { grabAreaFill: 'transparent', grabAreaCursor: 'col-resize', grabAreaWidth: 10, - grabAreaMinOffset: -6, - grabAreaMaxOffset: -2, - handleWidth: 2, + handleWidth: 4, handleRadius: 1, - handleFill: '#fff', - handleStroke: '#666', + handleStrokeWidth: 1, + + extraPad: 15 }; diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 379a4596b84..773ba9cd505 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -11,7 +11,6 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); - module.exports = function handleDefaults(layoutIn, layoutOut, axName) { if(!layoutIn[axName].rangeslider) return; @@ -28,25 +27,29 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); } + var visible = coerce('visible'); + if(!visible) return; + coerce('bgcolor', layoutOut.plot_bgcolor); coerce('bordercolor'); coerce('borderwidth'); coerce('thickness'); - coerce('visible'); + + coerce('autorange', !axOut.isValidRange(containerIn.range)); coerce('range'); // Expand slider range to the axis range - if(containerOut.range && !axOut.autorange) { - // TODO: what if the ranges are reversed? + // TODO: what if the ranges are reversed? + if(containerOut.range) { var outRange = containerOut.range, axRange = axOut.range; outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0]))); outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1]))); - } else { - axOut._needsExpand = true; } + axOut.cleanRange('rangeslider.range'); + // 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 ded3198d316..10e07a06992 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -77,20 +77,16 @@ module.exports = function(gd) { // for all present range sliders rangeSliders.each(function(axisOpts) { var rangeSlider = d3.select(this), - opts = axisOpts[constants.name]; - - // compute new slider range using axis autorange if necessary - // copy back range to input range slider container to skip - // this step in subsequent draw calls - if(!opts.range) { - opts._input.range = opts.range = Axes.getAutoRange(axisOpts); - } + opts = axisOpts[constants.name], + oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)]; // update range slider dimensions var margin = fullLayout.margin, graphSize = fullLayout._size, - domain = axisOpts.domain; + domain = axisOpts.domain, + oppDomain = oppAxisOpts.domain, + tickHeight = (axisOpts._boundingBox || {}).height || 0; opts._id = constants.name + axisOpts._id; opts._clipId = opts._id + '-' + fullLayout._uid; @@ -99,8 +95,13 @@ module.exports = function(gd) { opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness; opts._offsetShift = Math.floor(opts.borderwidth / 2); - var x = margin.l + (graphSize.w * domain[0]), - y = fullLayout.height - opts._height - margin.b; + var x = Math.round(margin.l + (graphSize.w * domain[0])); + + var y = Math.round( + margin.t + graphSize.h * (1 - oppDomain[0]) + + tickHeight + + opts._offsetShift + constants.extraPad + ); rangeSlider.attr('transform', 'translate(' + x + ',' + y + ')'); @@ -138,23 +139,33 @@ module.exports = function(gd) { // update margins - var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0; - Plots.autoMargin(gd, opts._id, { - x: 0, y: 0, l: 0, r: 0, t: 0, - b: opts._height + fullLayout.margin.b + bb, - pad: 15 + opts._offsetShift * 2 + x: domain[0], + y: oppDomain[0], + l: 0, + r: 0, + t: 0, + b: opts._height + margin.b + tickHeight, + pad: constants.extraPad + opts._offsetShift * 2 }); + }); }; function makeRangeSliderData(fullLayout) { - if(!fullLayout.xaxis) return []; - if(!fullLayout.xaxis[constants.name]) return []; - if(!fullLayout.xaxis[constants.name].visible) return []; - if(fullLayout._has('gl2d')) return []; + var axes = Axes.list({ _fullLayout: fullLayout }, 'x', true), + name = constants.name, + out = []; - return [fullLayout.xaxis]; + if(fullLayout._has('gl2d')) return out; + + for(var i = 0; i < axes.length; i++) { + var ax = axes[i]; + + if(ax[name] && ax[name].visible) out.push(ax); + } + + return out; } function setupDragElement(rangeSlider, gd, axisOpts, opts) { @@ -236,16 +247,21 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) { dataMax = clamp(opts.p2d(opts._pixelMax)); window.requestAnimationFrame(function() { - Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]); + Plotly.relayout(gd, axisOpts._name + '.range', [dataMin, dataMax]); }); } function setPixelRange(rangeSlider, gd, axisOpts, opts) { + var hw2 = constants.handleWidth / 2; function clamp(v) { return Lib.constrain(v, 0, opts._width); } + function clampHandle(v) { + return Lib.constrain(v, -hw2, opts._width + hw2); + } + var pixelMin = clamp(opts.d2p(axisOpts._rl[0])), pixelMax = clamp(opts.d2p(axisOpts._rl[1])); @@ -260,11 +276,18 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) { .attr('x', pixelMax) .attr('width', opts._width - pixelMax); + // add offset for crispier corners + // https://github.com/plotly/plotly.js/pull/1409 + var offset = 0.5; + + var xMin = Math.round(clampHandle(pixelMin - hw2)) - offset, + xMax = Math.round(clampHandle(pixelMax - hw2)) + offset; + rangeSlider.select('g.' + constants.grabberMinClassName) - .attr('transform', 'translate(' + (pixelMin - constants.handleWidth - 1) + ',0)'); + .attr('transform', 'translate(' + xMin + ',' + offset + ')'); rangeSlider.select('g.' + constants.grabberMaxClassName) - .attr('transform', 'translate(' + pixelMax + ',0)'); + .attr('transform', 'translate(' + xMax + ',' + offset + ')'); } function drawBg(rangeSlider, gd, axisOpts, opts) { @@ -284,6 +307,7 @@ function drawBg(rangeSlider, gd, axisOpts, opts) { opts.borderwidth - 1; var offsetShift = -opts._offsetShift; + var lw = Drawing.crispRound(gd, opts.borderwidth); bg.attr({ width: opts._width + borderCorrect, @@ -291,7 +315,7 @@ function drawBg(rangeSlider, gd, axisOpts, opts) { transform: 'translate(' + offsetShift + ',' + offsetShift + ')', fill: opts.bgcolor, stroke: opts.bordercolor, - 'stroke-width': opts.borderwidth, + 'stroke-width': lw }); } @@ -404,7 +428,8 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) { maskMin.enter().append('rect') .classed(constants.maskMinClassName, true) - .attr({ x: 0, y: 0 }); + .attr({ x: 0, y: 0 }) + .attr('shape-rendering', 'crispEdges'); maskMin .attr('height', opts._height) @@ -415,7 +440,8 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) { maskMax.enter().append('rect') .classed(constants.maskMaxClassName, true) - .attr('y', 0); + .attr('y', 0) + .attr('shape-rendering', 'crispEdges'); maskMax .attr('height', opts._height) @@ -431,7 +457,8 @@ function drawSlideBox(rangeSlider, gd, axisOpts, opts) { slideBox.enter().append('rect') .classed(constants.slideBoxClassName, true) .attr('y', 0) - .attr('cursor', constants.slideBoxCursor); + .attr('cursor', constants.slideBoxCursor) + .attr('shape-rendering', 'crispEdges'); slideBox.attr({ height: opts._height, @@ -459,14 +486,15 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) { x: 0, width: constants.handleWidth, rx: constants.handleRadius, - fill: constants.handleFill, - stroke: constants.handleStroke, + fill: Color.background, + stroke: Color.defaultLine, + 'stroke-width': constants.handleStrokeWidth, 'shape-rendering': 'crispEdges' }; var handleDynamicAttrs = { - y: opts._height / 4, - height: opts._height / 2, + y: Math.round(opts._height / 4), + height: Math.round(opts._height / 2), }; var handleMin = grabberMin.selectAll('rect.' + constants.handleMinClassName) @@ -489,6 +517,7 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) { var grabAreaFixAttrs = { width: constants.grabAreaWidth, + x: 0, y: 0, fill: constants.grabAreaFill, cursor: constants.grabAreaCursor @@ -499,20 +528,14 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) { grabAreaMin.enter().append('rect') .classed(constants.grabAreaMinClassName, true) .attr(grabAreaFixAttrs); - grabAreaMin.attr({ - x: constants.grabAreaMinOffset, - height: opts._height - }); + grabAreaMin.attr('height', opts._height); var grabAreaMax = grabberMax.selectAll('rect.' + constants.grabAreaMaxClassName) .data([0]); grabAreaMax.enter().append('rect') .classed(constants.grabAreaMaxClassName, true) .attr(grabAreaFixAttrs); - grabAreaMax.attr({ - x: constants.grabAreaMaxOffset, - height: opts._height - }); + grabAreaMax.attr('height', opts._height); } function clearPushMargins(gd) { diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js index 2d29e3b16fd..91a15ee14f3 100644 --- a/src/components/rangeslider/index.js +++ b/src/components/rangeslider/index.js @@ -20,6 +20,6 @@ module.exports = { layoutAttributes: require('./attributes'), handleDefaults: require('./defaults'), - + calcAutorange: require('./calc_autorange'), draw: require('./draw') }; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index e1c31b6f788..8c5e97c340e 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -189,7 +189,6 @@ Plotly.plot = function(gd, data, layout, config) { } // draw anything that can affect margins. - // currently this is legend and colorbars function marginPushers() { var calcdata = gd.calcdata; var i, cd, trace; @@ -253,7 +252,8 @@ Plotly.plot = function(gd, data, layout, config) { return Lib.syncOrAsync([ Registry.getComponentMethod('shapes', 'calcAutorange'), Registry.getComponentMethod('annotations', 'calcAutorange'), - doAutoRange + doAutoRange, + Registry.getComponentMethod('rangeslider', 'calcAutorange') ], gd); } diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index 603a933d548..8bf6c3376eb 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -378,7 +378,9 @@ axes.saveRangeInitial = function(gd, overwrite) { // tozero: (boolean) make sure to include zero if axis is linear, // and make it a tight bound if possible axes.expand = function(ax, data, options) { - if(!(ax.autorange || ax._needsExpand) || !data) return; + var needsAutorange = (ax.autorange || Lib.nestedProperty(ax, 'rangeslider.autorange')); + if(!needsAutorange || !data) return; + if(!ax._min) ax._min = []; if(!ax._max) ax._max = []; if(!options) options = {}; diff --git a/src/plots/cartesian/axis_defaults.js b/src/plots/cartesian/axis_defaults.js index 21fbe0585cd..73722da0334 100644 --- a/src/plots/cartesian/axis_defaults.js +++ b/src/plots/cartesian/axis_defaults.js @@ -9,7 +9,6 @@ 'use strict'; -var isNumeric = require('fast-isnumeric'); var colorMix = require('tinycolor2').mix; var Registry = require('../../registry'); @@ -93,12 +92,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce, color: dfltFontColor }); - var validRange = ( - (containerIn.range || []).length === 2 && - isNumeric(containerOut.r2l(containerIn.range[0])) && - isNumeric(containerOut.r2l(containerIn.range[1])) - ); - var autoRange = coerce('autorange', !validRange); + var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range)); if(autoRange) coerce('rangemode'); diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 4c757bff288..e6036bd8ef9 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -254,7 +254,7 @@ module.exports = function setConvert(ax) { */ ax.cleanRange = function(rangeAttr) { if(!rangeAttr) rangeAttr = 'range'; - var range = ax[rangeAttr], + var range = Lib.nestedProperty(ax, rangeAttr).get(), axLetter = (ax._id || 'x').charAt(0), i, dflt; @@ -266,7 +266,7 @@ module.exports = function setConvert(ax) { dflt = dflt.slice(); if(!range || range.length !== 2) { - ax[rangeAttr] = dflt; + Lib.nestedProperty(ax, rangeAttr).set(dflt); return; } @@ -402,6 +402,15 @@ module.exports = function setConvert(ax) { return arrayOut; }; + ax.isValidRange = function(range) { + return ( + Array.isArray(range) && + range.length === 2 && + isNumeric(ax.r2l(range[0])) && + isNumeric(ax.r2l(range[1])) + ); + }; + // for autoranging: arrays of objects: // {val: axis value, pad: pixel padding} // on the low and high sides diff --git a/src/plots/plots.js b/src/plots/plots.js index 3253aba48f7..dae65f3bf22 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1223,7 +1223,7 @@ plots.sanitizeMargins = function(fullLayout) { } }; -// called by legend and colorbar routines to see if we need to +// called by components to see if we need to // expand the margins to show them // o is {x,l,r,y,t,b} where x and y are plot fractions, // the rest are pixels in each direction @@ -1263,7 +1263,7 @@ plots.doAutoMargin = function(gd) { var gs = fullLayout._size, oldmargins = JSON.stringify(gs); - // adjust margins for outside legends and colorbars + // adjust margins for outside components // fullLayout.margin is the requested margin, // fullLayout._size has margins and plotsize after adjustment var ml = Math.max(fullLayout.margin.l || 0, 0), @@ -1273,6 +1273,7 @@ plots.doAutoMargin = function(gd) { pm = fullLayout._pushmargin; if(fullLayout.margin.autoexpand !== false) { + // fill in the requested margins pm.base = { l: {val: 0, size: ml}, @@ -1280,19 +1281,29 @@ plots.doAutoMargin = function(gd) { t: {val: 1, size: mt}, b: {val: 0, size: mb} }; + // now cycle through all the combinations of l and r // (and t and b) to find the required margins - Object.keys(pm).forEach(function(k1) { + + var pmKeys = Object.keys(pm); + + for(var i = 0; i < pmKeys.length; i++) { + var k1 = pmKeys[i]; + var pushleft = pm[k1].l || {}, pushbottom = pm[k1].b || {}, fl = pushleft.val, pl = pushleft.size, fb = pushbottom.val, pb = pushbottom.size; - Object.keys(pm).forEach(function(k2) { + + for(var j = 0; j < pmKeys.length; j++) { + var k2 = pmKeys[j]; + if(isNumeric(pl) && pm[k2].r) { var fr = pm[k2].r.val, pr = pm[k2].r.size; + if(fr > fl) { var newl = (pl * fr + (pr - fullLayout.width) * fl) / (fr - fl), @@ -1304,9 +1315,11 @@ plots.doAutoMargin = function(gd) { } } } + if(isNumeric(pb) && pm[k2].t) { var ft = pm[k2].t.val, pt = pm[k2].t.size; + if(ft > fb) { var newb = (pb * ft + (pt - fullLayout.height) * fb) / (ft - fb), @@ -1318,8 +1331,8 @@ plots.doAutoMargin = function(gd) { } } } - }); - }); + } + } } gs.l = Math.round(ml); diff --git a/test/image/baselines/candlestick_double-y-axis.png b/test/image/baselines/candlestick_double-y-axis.png index 8c6c223bc13..9f96621d952 100644 Binary files a/test/image/baselines/candlestick_double-y-axis.png and b/test/image/baselines/candlestick_double-y-axis.png differ diff --git a/test/image/baselines/candlestick_rangeslider_thai.png b/test/image/baselines/candlestick_rangeslider_thai.png index 60880772a07..48682b25459 100644 Binary files a/test/image/baselines/candlestick_rangeslider_thai.png and b/test/image/baselines/candlestick_rangeslider_thai.png differ diff --git a/test/image/baselines/ohlc_first.png b/test/image/baselines/ohlc_first.png index 4ce83c5aa83..1324765b138 100644 Binary files a/test/image/baselines/ohlc_first.png and b/test/image/baselines/ohlc_first.png differ diff --git a/test/image/baselines/range_slider.png b/test/image/baselines/range_slider.png index 942bb6b9909..705a1a03e29 100644 Binary files a/test/image/baselines/range_slider.png and b/test/image/baselines/range_slider.png differ diff --git a/test/image/baselines/range_slider_axes_double.png b/test/image/baselines/range_slider_axes_double.png index e2d04a225cc..a944a7ad35c 100644 Binary files a/test/image/baselines/range_slider_axes_double.png and b/test/image/baselines/range_slider_axes_double.png differ diff --git a/test/image/baselines/range_slider_box.png b/test/image/baselines/range_slider_box.png index 983d97aa3e0..ad5a4aef193 100644 Binary files a/test/image/baselines/range_slider_box.png and b/test/image/baselines/range_slider_box.png differ diff --git a/test/image/baselines/range_slider_initial_expanded.png b/test/image/baselines/range_slider_initial_expanded.png index e2347407ca5..1f01352b5da 100644 Binary files a/test/image/baselines/range_slider_initial_expanded.png and b/test/image/baselines/range_slider_initial_expanded.png differ diff --git a/test/image/baselines/range_slider_initial_valid.png b/test/image/baselines/range_slider_initial_valid.png index bd90642f2bc..1dc3f03000d 100644 Binary files a/test/image/baselines/range_slider_initial_valid.png and b/test/image/baselines/range_slider_initial_valid.png differ diff --git a/test/image/baselines/range_slider_multiple.png b/test/image/baselines/range_slider_multiple.png new file mode 100644 index 00000000000..49e835105b4 Binary files /dev/null and b/test/image/baselines/range_slider_multiple.png differ diff --git a/test/image/mocks/range_slider_multiple.json b/test/image/mocks/range_slider_multiple.json new file mode 100644 index 00000000000..ad88f263890 --- /dev/null +++ b/test/image/mocks/range_slider_multiple.json @@ -0,0 +1,41 @@ +{ + "data": [ + { + "x": [ 1, 2, 3 ], + "y": [ 4, 5, 6 ], + "type": "bar" + }, + { + "z": [ + [ 20, 30, 40 ], + [ 50, 60, 70 ] + ], + "showscale": false, + "xaxis": "x2", + "yaxis": "y2", + "type": "heatmap" + } + ], + "layout": { + "xaxis": { + "domain": [ 0, 0.45 ], + "range": [ 1, 2 ], + "rangeslider": {} + }, + "xaxis2": { + "anchor": "y2", + "domain": [ 0.55, 1 ], + "rangeslider": { + "range": [ -1, 4 ] + } + }, + "yaxis": { + "domain": [ 0.3, 0.8 ], + "type": "linear" + }, + "yaxis2": { + "anchor": "x2", + "domain": [ 0.5, 1 ] + } + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 3883cedb617..f4bad2f2478 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -413,6 +413,37 @@ describe('Test axes', function() { expect(layoutOut.xaxis.calendar).toBe('coptic'); expect(layoutOut.yaxis.calendar).toBe('thai'); }); + + it('should set autorange to true when input range is invalid', function() { + layoutIn = { + xaxis: { range: 'not-gonna-work' }, + xaxis2: { range: [1, 2, 3] }, + yaxis: { range: ['a', 2] }, + yaxis2: { range: [1, 'b'] }, + yaxis3: { range: [null, {}] } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + Axes.list({ _fullLayout: layoutOut }).forEach(function(ax) { + expect(ax.autorange).toBe(true, ax._name); + }); + }); + + it('should set autorange to false when input range is valid', function() { + layoutIn = { + xaxis: { range: [1, 2] }, + xaxis2: { range: [-2, 1] }, + yaxis: { range: ['1', 2] }, + yaxis2: { range: [1, '2'] } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + + Axes.list({ _fullLayout: layoutOut }).forEach(function(ax) { + expect(ax.autorange).toBe(false, ax._name); + }); + }); }); describe('categoryorder', function() { diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 0348c8cfaac..170f464e2e5 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -39,7 +39,7 @@ describe('the range slider', function() { var transformParts = node.getAttribute('transform').split('('); expect(transformParts[0]).toEqual('translate'); - expect(+transformParts[1].split(',0)')[0]).toBeWithin(val, TOL); + expect(+transformParts[1].split(',0.5)')[0]).toBeWithin(val, TOL); } describe('when specified as visible', function() { @@ -99,7 +99,7 @@ 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 - 3) + ',0)'); + expect(handleMin.getAttribute('transform')).toBe('translate(' + (diff - 2.5) + ',0.5)'); }).then(done); }); @@ -204,7 +204,7 @@ describe('the range slider', function() { expect(+maskMin.getAttribute('width')).toBeWithin(126, TOL); expect(+maskMax.getAttribute('width')).toEqual(0); testTranslate1D(handleMin, 123.32); - testTranslate1D(handleMax, 619); + testTranslate1D(handleMax, 617); }) .then(done); }); @@ -336,13 +336,17 @@ describe('the range slider', function() { describe('handleDefaults function', function() { + function _supply(layoutIn, layoutOut, axName) { + setConvert(layoutOut[axName]); + RangeSlider.handleDefaults(layoutIn, layoutOut, axName); + } + it('should not coerce anything if rangeslider isn\'t set', function() { var layoutIn = { xaxis: {} }, layoutOut = { xaxis: {} }, expected = { xaxis: {} }; - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - + _supply(layoutIn, layoutOut, 'xaxis'); expect(layoutIn).toEqual(expected); }); @@ -351,8 +355,7 @@ describe('the range slider', function() { layoutOut = { xaxis: { rangeslider: {}} }, expected = { xaxis: { rangeslider: { visible: true }} }; - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - + _supply(layoutIn, layoutOut, 'xaxis'); expect(layoutIn).toEqual(expected); }); @@ -360,44 +363,44 @@ describe('the range slider', function() { var layoutIn = { xaxis: { rangeslider: {} }}, layoutOut = { xaxis: {} }, expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }, - _needsExpand: true - } + visible: true, + autorange: true, + range: [-1, 6], + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut).toEqual(expected); + _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 = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }, - _needsExpand: true - } + visible: true, + autorange: true, + range: [-1, 6], + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); + }); + + it('should return early if *visible: false*', function() { + var layoutIn = { xaxis: { rangeslider: { visible: false, range: [10, 20] }} }, + layoutOut = { xaxis: { rangeslider: {}} }; - expect(layoutOut).toEqual(expected); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual({ visible: false }); }); it('should set defaults if properties are invalid', function() { @@ -410,72 +413,57 @@ describe('the range slider', function() { }}}, layoutOut = { xaxis: {} }, expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: layoutIn.xaxis.rangeslider - }, - _needsExpand: true - } + visible: true, + autorange: true, + range: [-1, 6], + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut).toEqual(expected); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); }); it('should expand the rangeslider range to axis range', function() { var layoutIn = { xaxis: { rangeslider: { range: [5, 6] } } }, layoutOut = { xaxis: { range: [1, 10], type: 'linear'} }, expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - range: [1, 10], - _input: layoutIn.xaxis.rangeslider - }, - range: [1, 10] - } + visible: true, + autorange: false, + range: [1, 10], + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - setConvert(layoutOut.xaxis); - - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); + _supply(layoutIn, layoutOut, 'xaxis'); // don't compare the whole layout, because we had to run setConvert which // attaches all sorts of other stuff to xaxis - expect(layoutOut.xaxis.rangeslider).toEqual(expected.xaxis.rangeslider); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); }); - it('should set _needsExpand when an axis range is set', function() { - var layoutIn = { xaxis: { rangeslider: true } }, - layoutOut = { xaxis: { range: [2, 40]} }, + it('should set autorange to true when range input is invalid', function() { + var layoutIn = { xaxis: { rangeslider: { range: 'not-gonna-work'}} }, + layoutOut = { xaxis: {} }, expected = { - xaxis: { - rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444', - _input: {} - }, - range: [2, 40], - _needsExpand: true - }, + visible: true, + autorange: true, + range: [-1, 6], + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }; - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - - expect(layoutOut).toEqual(expected); + _supply(layoutIn, layoutOut, 'xaxis'); + expect(layoutOut.xaxis.rangeslider).toEqual(expected); }); it('should default \'bgcolor\' to layout \'plot_bgcolor\'', function() { @@ -488,8 +476,7 @@ describe('the range slider', function() { plot_bgcolor: 'blue' }; - RangeSlider.handleDefaults(layoutIn, layoutOut, 'xaxis'); - + _supply(layoutIn, layoutOut, 'xaxis'); expect(layoutOut.xaxis.rangeslider.bgcolor).toEqual('blue'); }); }); @@ -536,12 +523,24 @@ describe('the range slider', function() { describe('in general', function() { + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + 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() { @@ -561,6 +560,80 @@ describe('the range slider', function() { }) .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]); + }) + .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]); + + }) + .then(done); + }); }); });