diff --git a/src/components/rangeslider/attributes.js b/src/components/rangeslider/attributes.js index 118d06bd0c6..e6d58be6e06 100644 --- a/src/components/rangeslider/attributes.js +++ b/src/components/rangeslider/attributes.js @@ -30,6 +30,22 @@ module.exports = { role: 'style', description: 'Sets the border color of the range slider.' }, + range: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number'}, + {valType: 'number'} + ], + description: [ + 'Sets the range of the range slider.', + 'If not set, defaults to the full xaxis range.', + 'If the axis `type` is *log*, then you must take the', + 'log of your desired range.', + 'If the axis `type` is *date*, then you must convert', + 'the date to unix time in milliseconds.' + ].join(' ') + }, thickness: { valType: 'number', dflt: 0.15, diff --git a/src/components/rangeslider/create_slider.js b/src/components/rangeslider/create_slider.js index 0aaf2b98315..61d6854e783 100644 --- a/src/components/rangeslider/create_slider.js +++ b/src/components/rangeslider/create_slider.js @@ -10,6 +10,7 @@ var Plotly = require('../../plotly'); +var Axes = require('../../plots/cartesian/axes'); var Lib = require('../../lib'); var svgNS = require('../../constants/xmlns_namespaces').svg; @@ -18,7 +19,7 @@ var helpers = require('./helpers'); var rangePlot = require('./range_plot'); -module.exports = function createSlider(gd, minStart, maxStart) { +module.exports = function createSlider(gd) { var fullLayout = gd._fullLayout, sliderContainer = fullLayout._infolayer.selectAll('g.range-slider'), options = fullLayout.xaxis.rangeslider, @@ -29,8 +30,8 @@ module.exports = function createSlider(gd, minStart, maxStart) { x = fullLayout.margin.l, y = fullLayout.height - height - fullLayout.margin.b; - minStart = minStart || 0; - maxStart = maxStart || width; + var minStart = 0, + maxStart = width; var slider = document.createElementNS(svgNS, 'g'); helpers.setAttributes(slider, { @@ -177,8 +178,8 @@ module.exports = function createSlider(gd, minStart, maxStart) { min = min || -Infinity; max = max || Infinity; - var rangeMin = fullLayout.xaxis.range[0], - rangeMax = fullLayout.xaxis.range[1], + var rangeMin = options.range[0], + rangeMax = options.range[1], range = rangeMax - rangeMin, pixelMin = (min - rangeMin) / range * width, pixelMax = (max - rangeMin) / range * width; @@ -217,9 +218,8 @@ module.exports = function createSlider(gd, minStart, maxStart) { helpers.setAttributes(grabberMin, { 'transform': 'translate(' + (min - handleWidth - 1) + ')' }); helpers.setAttributes(grabberMax, { 'transform': 'translate(' + max + ')' }); - // call to set range on plot here - var rangeMin = fullLayout.xaxis.range[0], - rangeMax = fullLayout.xaxis.range[1], + var rangeMin = options.range[0], + rangeMax = options.range[1], range = rangeMax - rangeMin, dataMin = min / width * range + rangeMin, dataMax = max / width * range + rangeMin; @@ -236,6 +236,11 @@ module.exports = function createSlider(gd, minStart, maxStart) { } + // Set slider range using axis autorange if necessary. + if(!options.range) { + options.range = Axes.getAutoRange(fullLayout.xaxis); + } + var rangePlots = rangePlot(gd, width, height); helpers.appendChildren(slider, [ @@ -248,6 +253,9 @@ module.exports = function createSlider(gd, minStart, maxStart) { grabberMax ]); + // Set initially selected range + setRange(fullLayout.xaxis.range[0], fullLayout.xaxis.range[1]); + sliderContainer.data([0]) .enter().append(function() { options.setRange = setRange; diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index c1b4cb3fb45..f0d0efeebd7 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -25,11 +25,23 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, axName, coun attributes, attr, dflt); } - coerce('visible'); - coerce('thickness'); coerce('bgcolor'); coerce('bordercolor'); coerce('borderwidth'); + coerce('thickness'); + coerce('visible'); + coerce('range'); + + // Expand slider range to the axis range + if(containerOut.range && !layoutOut[axName].autorange) { + var outRange = containerOut.range, + axRange = layoutOut[axName].range; + + outRange[0] = Math.min(outRange[0], axRange[0]); + outRange[1] = Math.max(outRange[1], axRange[1]); + } else { + layoutOut[axName]._needsExpand = true; + } if(containerOut.visible) { counterAxes.forEach(function(ax) { diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js index 77a4c092430..31bd1175c8e 100644 --- a/src/components/rangeslider/index.js +++ b/src/components/rangeslider/index.js @@ -20,7 +20,7 @@ module.exports = { supplyLayoutDefaults: supplyLayoutDefaults }; -function draw(gd, minStart, maxStart) { +function draw(gd) { if(!gd._fullLayout.xaxis) return; var fullLayout = gd._fullLayout, @@ -41,7 +41,7 @@ function draw(gd, minStart, maxStart) { var height = (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) * options.thickness, offsetShift = Math.floor(options.borderwidth / 2); - if(sliderContainer[0].length === 0 && !fullLayout._hasGL2D) createSlider(gd, minStart, maxStart); + if(sliderContainer[0].length === 0 && !fullLayout._hasGL2D) createSlider(gd); // Need to default to 0 for when making gl plots var bb = fullLayout.xaxis._boundingBox ? diff --git a/src/components/rangeslider/range_plot.js b/src/components/rangeslider/range_plot.js index e1c119ba09c..39dc6e1aa5d 100644 --- a/src/components/rangeslider/range_plot.js +++ b/src/components/rangeslider/range_plot.js @@ -16,11 +16,12 @@ var svgNS = require('../../constants/xmlns_namespaces').svg; module.exports = function rangePlot(gd, w, h) { - var traces = gd._fullData, - xaxis = gd._fullLayout.xaxis, - yaxis = gd._fullLayout.yaxis, - minX = xaxis.range[0], - maxX = xaxis.range[1], + var fullLayout = gd._fullLayout, + traces = gd._fullData, + xaxis = fullLayout.xaxis, + yaxis = fullLayout.yaxis, + minX = xaxis.rangeslider.range[0], + maxX = xaxis.rangeslider.range[1], minY = yaxis.range[0], maxY = yaxis.range[1]; @@ -62,7 +63,9 @@ module.exports = function rangePlot(gd, w, h) { var posX = w * (x[k] - minX) / (maxX - minX), posY = h * (1 - (y[k] - minY) / (maxY - minY)); - pointPairs.push([posX, posY]); + if(!isNaN(posX) && !isNaN(posY)) { + pointPairs.push([posX, posY]); + } } // more trace type range plots can be added here diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index cc42a7e39c4..24f06adc781 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -245,7 +245,6 @@ Plotly.plot = function(gd, data, layout, config) { function drawAxes() { // draw ticks, titles, and calculate axis scaling (._b, ._m) - RangeSlider.draw(gd); return Plotly.Axes.doTicks(gd, 'redraw'); } @@ -310,6 +309,7 @@ Plotly.plot = function(gd, data, layout, config) { Shapes.drawAll(gd); Plotly.Annotations.drawAll(gd); Legend.draw(gd); + RangeSlider.draw(gd); RangeSelector.draw(gd); } @@ -2191,6 +2191,10 @@ Plotly.relayout = function relayout(gd, astr, val) { docalc = true; } + if(pleafPlus.indexOf('rangeslider') !== -1) { + docalc = true; + } + // toggling log without autorange: need to also recalculate ranges // logical XOR (ie are we toggling log) if(pleaf==='type' && ((parentFull.type === 'log') !== (vi === 'log'))) { diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index d5610fca05b..e7ba503c140 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -110,91 +110,106 @@ axes.minDtick = function(ax,newDiff,newFirst,allow) { } }; -axes.doAutoRange = function(ax) { - if(!ax._length) ax.setScale(); +axes.getAutoRange = function(ax) { + var newRange = []; - if(ax.autorange && ax._min && ax._max && - ax._min.length && ax._max.length) { - var minmin = ax._min[0].val, - maxmax = ax._max[0].val, - i; - - for(i = 1; i < ax._min.length; i++) { - if(minmin !== maxmax) break; - minmin = Math.min(minmin, ax._min[i].val); - } - for(i = 1; i < ax._max.length; i++) { - if(minmin !== maxmax) break; - maxmax = Math.max(maxmax, ax._max[i].val); - } - - var j,minpt,maxpt,minbest,maxbest,dp,dv, - mbest = 0, - axReverse = (ax.range && ax.range[1]0 && dp>0 && dv/dp > mbest) { - minbest = minpt; - maxbest = maxpt; - mbest = dv/dp; - } + var minmin = ax._min[0].val, + maxmax = ax._max[0].val, + i; + + for(i = 1; i < ax._min.length; i++) { + if(minmin !== maxmax) break; + minmin = Math.min(minmin, ax._min[i].val); + } + for(i = 1; i < ax._max.length; i++) { + if(minmin !== maxmax) break; + maxmax = Math.max(maxmax, ax._max[i].val); + } + + var j,minpt,maxpt,minbest,maxbest,dp,dv, + mbest = 0, + axReverse = (ax.range && ax.range[1]0 && dp>0 && dv/dp > mbest) { + minbest = minpt; + maxbest = maxpt; + mbest = dv/dp; } } - if(minmin===maxmax) { - ax.range = axReverse ? - [minmin+1, ax.rangemode!=='normal' ? 0 : minmin-1] : - [ax.rangemode!=='normal' ? 0 : minmin-1, minmin+1]; - } - else if(mbest) { - if(ax.type==='linear' || ax.type==='-') { - if(ax.rangemode==='tozero' && minbest.val>=0) { + } + + if(minmin === maxmax) { + newRange = axReverse ? + [minmin+1, ax.rangemode!=='normal' ? 0 : minmin-1] : + [ax.rangemode!=='normal' ? 0 : minmin-1, minmin+1]; + } + else if(mbest) { + if(ax.type==='linear' || ax.type==='-') { + if(ax.rangemode==='tozero' && minbest.val>=0) { + minbest = {val: 0, pad: 0}; + } + else if(ax.rangemode==='nonnegative') { + if(minbest.val - mbest*minbest.pad<0) { minbest = {val: 0, pad: 0}; } - else if(ax.rangemode==='nonnegative') { - if(minbest.val - mbest*minbest.pad<0) { - minbest = {val: 0, pad: 0}; - } - if(maxbest.val<0) { - maxbest = {val: 1, pad: 0}; - } + if(maxbest.val<0) { + maxbest = {val: 1, pad: 0}; } - - // in case it changed again... - mbest = (maxbest.val-minbest.val) / - (ax._length-minbest.pad-maxbest.pad); } - ax.range = [ - minbest.val - mbest*minbest.pad, - maxbest.val + mbest*maxbest.pad - ]; + // in case it changed again... + mbest = (maxbest.val-minbest.val) / + (ax._length-minbest.pad-maxbest.pad); + } - // don't let axis have zero size - if(ax.range[0]===ax.range[1]) { - ax.range = [ax.range[0]-1, ax.range[0]+1]; - } + newRange = [ + minbest.val - mbest*minbest.pad, + maxbest.val + mbest*maxbest.pad + ]; - // maintain reversal - if(axReverse) { - ax.range.reverse(); - } + // don't let axis have zero size + if(newRange[0] === newRange[1]) { + newRange = [newRange[0]-1, newRange[0]+1]; } + // maintain reversal + if(axReverse) { + newRange.reverse(); + } + } + + return newRange; +}; + +axes.doAutoRange = function(ax) { + if(!ax._length) ax.setScale(); + + // TODO do we really need this? + var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length); + + if(ax.autorange && hasDeps) { + ax.range = axes.getAutoRange(ax); + // doAutoRange will get called on fullLayout, // but we want to report its results back to layout var axIn = ax._gd.layout[ax._name]; + if(!axIn) ax._gd.layout[ax._name] = axIn = {}; - if(axIn!==ax) { + + if(axIn !== ax) { axIn.range = ax.range.slice(); axIn.autorange = ax.autorange; } @@ -241,7 +256,8 @@ axes.saveRangeInitial = function(gd, overwrite) { // and make it a tight bound if possible var FP_SAFE = Number.MAX_VALUE/2; axes.expand = function(ax, data, options) { - if(!ax.autorange || !data) return; + // if(!(ax.autorange || (ax.rangeslider || {}).visible) || !data) return; + if(!(ax.autorange || ax._needsExpand) || !data) return; if(!ax._min) ax._min = []; if(!ax._max) ax._max = []; if(!options) options = {}; diff --git a/test/image/baselines/range_slider_ranges.png b/test/image/baselines/range_slider_ranges.png new file mode 100644 index 00000000000..0dbc81bef79 Binary files /dev/null and b/test/image/baselines/range_slider_ranges.png differ diff --git a/test/image/mocks/range_slider_ranges.json b/test/image/mocks/range_slider_ranges.json new file mode 100644 index 00000000000..686338f8ae6 --- /dev/null +++ b/test/image/mocks/range_slider_ranges.json @@ -0,0 +1,37 @@ +{ + "data": [{ + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49], + "y": [61.54407276195499, 53.01489477283346, 54.27395817178983, 53.55001411571369, 78.9775866748205, 31.884243474727825, 56.77854253236383, 4.735201078365101, 63.12484997271903, 43.09882925106262, 43.724822753247224, 24.844310873420152, 34.89218498332549, 36.0460327216264, 28.2174958198741, 29.273481072578775, 70.89188615102645, 59.76636236708869, 66.76528535512163, 11.281051334701928, 78.37153154211197, 66.08349166666542, 50.09727630364335, 58.04698479499695, 59.74272576303902, 41.86433349552978, 48.22485560857029, 37.70855269433609, 21.119967245011217, 75.10194864698312, 55.48369213601815, 59.735088561204854, 60.42028696627966, 0.10938359086638982, 77.3773017472981, 15.086946383114501, 35.82836446307611, 23.689280033989295, 2.362484581742592, 53.40692073882494, 15.134375465281735, 7.3434664768826075, 54.52433252576499, 35.798885397722806, 38.26472971248782, 17.920068000491725, 37.0479742805594, 45.83239633915396, 43.99138484769564, 69.43228444571429], + "line": { + "shape": "spline", + "smoothing": 1 + }, + "fill": "tozeroy", + "name": "Bird 1" + }, { + "x": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49], + "y": [42.61173967030771, 77.84512822099697, 55.39613813617828, 4.0231685798691785, 34.55285260557025, 15.153333721685467, 77.54957038861798, 75.64407363993858, 62.12340236506287, 68.02318161065071, 34.816275599609554, 12.97751450053445, 25.16704221023005, 39.27425976595971, 41.98830052124046, 32.88934713126341, 64.3908328308427, 31.152165702544288, 53.28979283850701, 47.831576036783474, 48.46112255786373, 13.019775174742367, 44.95556684431092, 62.13365414186855, 22.796080092935433, 53.88638249610922, 9.378276407428476, 54.87772961864103, 10.622983213205028, 31.428637782080635, 63.81786719853103, 68.85325763593313, 30.412750136966178, 29.161036950263153, 19.125624934754217, 1.7788679495654591, 31.62390207284494, 33.722958647807374, 35.3846908579102, 41.364380830218224, 24.993662278935265, 39.083235916470116, 59.576257951955824, 33.620531907520075, 11.721918308606636, 10.943094423029738, 20.385230482706316, 39.62659948914896, 72.20357742148559, 38.48711164016425], + "name": "Bird 2" + }], + "layout": { + "title": "Seagull Positions", + "xaxis": { + "rangeslider": { + "visible": true, + "thickness": 0.2, + "bgcolor": "#fafafa", + "bordercolor": "black", + "borderwidth": 2, + "range": [20, 30] + }, + "title": "Time (Hours)", + "range": [10, 45] + }, + "yaxis": { + "title": "Height (ft)" + }, + "paper_bgcolor": "#eee", + "height": 500, + "width": 800 + } +} diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index c2faaeac982..35197a6dbbd 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -1,4 +1,5 @@ var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); var RangeSlider = require('@src/components/rangeslider'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); @@ -16,7 +17,9 @@ describe('the range slider', function() { beforeEach(function(done) { gd = createGraphDiv(); - Plotly.plot(gd, mock.data, mock.layout).then(function() { + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { rangeSlider = document.getElementsByClassName('range-slider')[0]; children = rangeSlider.children; done(); @@ -223,7 +226,8 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444' - } + }, + _needsExpand: true }, yaxis: { fixedrange: true @@ -248,7 +252,8 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444' - } + }, + _needsExpand: true }, yaxis: { fixedrange: true @@ -271,15 +276,21 @@ describe('the range slider', function() { layoutOut = { xaxis: {}, yaxis: {}}, axName = 'xaxis', counterAxes = ['yaxis'], - expected = { xaxis: { rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444' - }}, yaxis: { - fixedrange: true - }}; + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444' + }, + _needsExpand: true + }, + yaxis: { + fixedrange: true + } + }; RangeSlider.supplyLayoutDefaults(layoutIn, layoutOut, axName, counterAxes); @@ -287,18 +298,21 @@ describe('the range slider', function() { }); it('should set all counterAxes to fixedrange', function() { - var layoutIn = { xaxis: { rangeslider: true}, yaxis: {}, yaxis2: {}}, + var layoutIn = { xaxis: { rangeslider: true }, yaxis: {}, yaxis2: {}}, layoutOut = { xaxis: {}, yaxis: {}, yaxis2: {}}, axName = 'xaxis', counterAxes = ['yaxis', 'yaxis2'], expected = { - xaxis: { rangeslider: { - visible: true, - thickness: 0.15, - bgcolor: '#fff', - borderwidth: 0, - bordercolor: '#444' - }}, + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444' + }, + _needsExpand: true + }, yaxis: { fixedrange: true}, yaxis2: { fixedrange: true } }; @@ -307,6 +321,56 @@ describe('the range slider', function() { expect(layoutOut).toEqual(expected); }); + + it('should expand the rangeslider range to axis range', function() { + var layoutIn = { xaxis: { rangeslider: { range: [5,6] } }, yaxis: {}}, + layoutOut = { xaxis: { range: [1, 10]}, yaxis: {}}, + axName = 'xaxis', + counterAxes = ['yaxis'], + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444', + range: [1, 10] + }, + range: [1, 10] + }, + yaxis: { fixedrange: true } + }; + + RangeSlider.supplyLayoutDefaults(layoutIn, layoutOut, axName, counterAxes); + + expect(layoutOut).toEqual(expected); + }); + + it('should set _needsExpand when an axis range is set', function() { + var layoutIn = { xaxis: { rangeslider: true }, yaxis: {}}, + layoutOut = { xaxis: { range: [2, 40]}, yaxis: {}}, + axName = 'xaxis', + counterAxes = ['yaxis'], + expected = { + xaxis: { + rangeslider: { + visible: true, + thickness: 0.15, + bgcolor: '#fff', + borderwidth: 0, + bordercolor: '#444' + }, + range: [2, 40], + _needsExpand: true + }, + yaxis: { fixedrange: true } + }; + + RangeSlider.supplyLayoutDefaults(layoutIn, layoutOut, axName, counterAxes); + + expect(layoutOut).toEqual(expected); + }); }); describe('in general', function() {