diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js index a57a0038248..0aa03245154 100644 --- a/src/components/dragelement/index.js +++ b/src/components/dragelement/index.js @@ -177,6 +177,8 @@ function coverSlip() { return cover; } +dragElement.coverSlip = coverSlip; + function finishDrag(gd) { gd._dragging = false; if(gd._replotPending) Plotly.plot(gd); diff --git a/src/components/rangeslider/constants.js b/src/components/rangeslider/constants.js new file mode 100644 index 00000000000..0a60e7b33b3 --- /dev/null +++ b/src/components/rangeslider/constants.js @@ -0,0 +1,51 @@ +/** +* Copyright 2012-2016, 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 = { + + // attribute container name + name: 'rangeslider', + + // class names + + containerClassName: 'rangeslider-container', + bgClassName: 'rangeslider-bg', + rangePlotClassName: 'rangeslider-rangeplot', + + maskMinClassName: 'rangeslider-mask-min', + maskMaxClassName: 'rangeslider-mask-max', + slideBoxClassName: 'rangeslider-slidebox', + + grabberMinClassName: 'rangeslider-grabber-min', + grabAreaMinClassName: 'rangeslider-grabarea-min', + handleMinClassName: 'rangeslider-handle-min', + + grabberMaxClassName: 'rangeslider-grabber-max', + grabAreaMaxClassName: 'rangeslider-grabarea-max', + handleMaxClassName: 'rangeslider-handle-max', + + // style constants + + maskColor: 'rgba(0,0,0,0.4)', + + slideBoxFill: 'transparent', + slideBoxCursor: 'ew-resize', + + grabAreaFill: 'transparent', + grabAreaCursor: 'col-resize', + grabAreaWidth: 10, + grabAreaMinOffset: -6, + grabAreaMaxOffset: -2, + + handleWidth: 2, + handleRadius: 1, + handleFill: '#fff', + handleStroke: '#666', +}; diff --git a/src/components/rangeslider/create_slider.js b/src/components/rangeslider/create_slider.js deleted file mode 100644 index 9256fa31d4d..00000000000 --- a/src/components/rangeslider/create_slider.js +++ /dev/null @@ -1,287 +0,0 @@ -/** -* Copyright 2012-2016, 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 Plotly = require('../../plotly'); -var Axes = require('../../plots/cartesian/axes'); -var Lib = require('../../lib'); - -var svgNS = require('../../constants/xmlns_namespaces').svg; - -var helpers = require('./helpers'); -var rangePlot = require('./range_plot'); - - -module.exports = function createSlider(gd) { - var fullLayout = gd._fullLayout, - sliderContainer = fullLayout._infolayer.selectAll('g.range-slider'), - options = fullLayout.xaxis.rangeslider, - width = fullLayout._size.w, - height = (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) * options.thickness, - handleWidth = 2, - offsetShift = Math.floor(options.borderwidth / 2), - x = fullLayout.margin.l, - y = fullLayout.height - height - fullLayout.margin.b; - - var minStart = 0, - maxStart = width; - - var slider = document.createElementNS(svgNS, 'g'); - helpers.setAttributes(slider, { - 'class': 'range-slider', - 'data-min': minStart, - 'data-max': maxStart, - 'pointer-events': 'all', - 'transform': 'translate(' + x + ',' + y + ')' - }); - - - var sliderBg = document.createElementNS(svgNS, 'rect'), - borderCorrect = options.borderwidth % 2 === 0 ? options.borderwidth : options.borderwidth - 1; - helpers.setAttributes(sliderBg, { - 'fill': options.bgcolor, - 'stroke': options.bordercolor, - 'stroke-width': options.borderwidth, - 'height': height + borderCorrect, - 'width': width + borderCorrect, - 'transform': 'translate(-' + offsetShift + ', -' + offsetShift + ')', - 'shape-rendering': 'crispEdges' - }); - - - var maskMin = document.createElementNS(svgNS, 'rect'); - helpers.setAttributes(maskMin, { - 'x': 0, - 'width': minStart, - 'height': height, - 'fill': 'rgba(0,0,0,0.4)' - }); - - - var maskMax = document.createElementNS(svgNS, 'rect'); - helpers.setAttributes(maskMax, { - 'x': maxStart, - 'width': width - maxStart, - 'height': height, - 'fill': 'rgba(0,0,0,0.4)' - }); - - - var grabberMin = document.createElementNS(svgNS, 'g'), - grabAreaMin = document.createElementNS(svgNS, 'rect'), - handleMin = document.createElementNS(svgNS, 'rect'); - helpers.setAttributes(grabberMin, { 'transform': 'translate(' + (minStart - handleWidth - 1) + ')' }); - helpers.setAttributes(grabAreaMin, { - 'width': 10, - 'height': height, - 'x': -6, - 'fill': 'transparent', - 'cursor': 'col-resize' - }); - helpers.setAttributes(handleMin, { - 'width': handleWidth, - 'height': height / 2, - 'y': height / 4, - 'rx': 1, - 'fill': 'white', - 'stroke': '#666', - 'shape-rendering': 'crispEdges' - }); - helpers.appendChildren(grabberMin, [handleMin, grabAreaMin]); - - - var grabberMax = document.createElementNS(svgNS, 'g'), - grabAreaMax = document.createElementNS(svgNS, 'rect'), - handleMax = document.createElementNS(svgNS, 'rect'); - helpers.setAttributes(grabberMax, { 'transform': 'translate(' + maxStart + ')' }); - helpers.setAttributes(grabAreaMax, { - 'width': 10, - 'height': height, - 'x': -2, - 'fill': 'transparent', - 'cursor': 'col-resize' - }); - helpers.setAttributes(handleMax, { - 'width': handleWidth, - 'height': height / 2, - 'y': height / 4, - 'rx': 1, - 'fill': 'white', - 'stroke': '#666', - 'shape-rendering': 'crispEdges' - }); - helpers.appendChildren(grabberMax, [handleMax, grabAreaMax]); - - - var slideBox = document.createElementNS(svgNS, 'rect'); - helpers.setAttributes(slideBox, { - 'x': minStart, - 'width': maxStart - minStart, - 'height': height, - 'cursor': 'ew-resize', - 'fill': 'transparent' - }); - - - slider.addEventListener('mousedown', function(event) { - var target = event.target, - startX = event.clientX, - offsetX = startX - slider.getBoundingClientRect().left, - minVal = slider.getAttribute('data-min'), - maxVal = slider.getAttribute('data-max'); - - window.addEventListener('mousemove', mouseMove); - window.addEventListener('mouseup', mouseUp); - - function mouseMove(e) { - var delta = +e.clientX - startX, - pixelMin, - pixelMax; - - switch(target) { - case slideBox: - slider.style.cursor = 'ew-resize'; - - pixelMin = +minVal + delta; - pixelMax = +maxVal + delta; - - setPixelRange(pixelMin, pixelMax); - setDataRange(pixelToData(pixelMin), pixelToData(pixelMax)); - break; - - case grabAreaMin: - slider.style.cursor = 'col-resize'; - - pixelMin = +minVal + delta; - pixelMax = +maxVal; - - setPixelRange(pixelMin, pixelMax); - setDataRange(pixelToData(pixelMin), pixelToData(pixelMax)); - break; - - case grabAreaMax: - slider.style.cursor = 'col-resize'; - - pixelMin = +minVal; - pixelMax = +maxVal + delta; - - setPixelRange(pixelMin, pixelMax); - setDataRange(pixelToData(pixelMin), pixelToData(pixelMax)); - break; - - default: - slider.style.cursor = 'ew-resize'; - - pixelMin = offsetX; - pixelMax = offsetX + delta; - - setPixelRange(pixelMin, pixelMax); - setDataRange(pixelToData(pixelMin), pixelToData(pixelMax)); - break; - } - } - - function mouseUp() { - window.removeEventListener('mousemove', mouseMove); - window.removeEventListener('mouseup', mouseUp); - slider.style.cursor = 'auto'; - } - }); - - function pixelToData(pixel) { - var rangeMin = options.range[0], - rangeMax = options.range[1], - range = rangeMax - rangeMin, - dataValue = pixel / width * range + rangeMin; - - dataValue = Lib.constrain(dataValue, rangeMin, rangeMax); - - return dataValue; - } - - - function setRange(min, max) { - min = min || -Infinity; - max = max || Infinity; - - var rangeMin = options.range[0], - rangeMax = options.range[1], - range = rangeMax - rangeMin, - pixelMin = (min - rangeMin) / range * width, - pixelMax = (max - rangeMin) / range * width; - - setPixelRange(pixelMin, pixelMax); - } - - function setDataRange(dataMin, dataMax) { - window.requestAnimationFrame(function() { - Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]); - }); - } - - - function setPixelRange(pixelMin, pixelMax) { - - pixelMin = Lib.constrain(pixelMin, 0, width); - pixelMax = Lib.constrain(pixelMax, 0, width); - - if(pixelMax < pixelMin) { - var temp = pixelMax; - pixelMax = pixelMin; - pixelMin = temp; - } - - helpers.setAttributes(slider, { - 'data-min': pixelMin, - 'data-max': pixelMax - }); - - helpers.setAttributes(slideBox, { - 'x': pixelMin, - 'width': pixelMax - pixelMin - }); - - helpers.setAttributes(maskMin, { 'width': pixelMin }); - helpers.setAttributes(maskMax, { - 'x': pixelMax, - 'width': width - pixelMax - }); - - helpers.setAttributes(grabberMin, { 'transform': 'translate(' + (pixelMin - handleWidth - 1) + ')' }); - helpers.setAttributes(grabberMax, { 'transform': 'translate(' + pixelMax + ')' }); - } - - - // 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, [ - sliderBg, - rangePlots, - maskMin, - maskMax, - slideBox, - grabberMin, - grabberMax - ]); - - // Set initially selected range - setRange(fullLayout.xaxis.range[0], fullLayout.xaxis.range[1]); - - sliderContainer.data([0]) - .enter().append(function() { - options.setRange = setRange; - return slider; - }); -}; diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 936eb030680..6e745a5624c 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -15,15 +15,19 @@ var attributes = require('./attributes'); module.exports = function handleDefaults(layoutIn, layoutOut, axName, counterAxes) { if(!layoutIn[axName].rangeslider) return; - var containerIn = Lib.isPlainObject(layoutIn[axName].rangeslider) ? - layoutIn[axName].rangeslider : {}, + // not super proud of this (maybe store _ in axis object instead + if(!Lib.isPlainObject(layoutIn[axName].rangeslider)) { + layoutIn[axName].rangeslider = {}; + } + + var containerIn = layoutIn[axName].rangeslider, containerOut = layoutOut[axName].rangeslider = {}; function coerce(attr, dflt) { return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); } - coerce('bgcolor'); + coerce('bgcolor', layoutOut.plot_bgcolor); coerce('bordercolor'); coerce('borderwidth'); coerce('thickness'); @@ -48,4 +52,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName, counterAxe layoutOut[ax] = opposing; }); } + + // to map back range slider (auto) range + containerOut._input = containerIn; }; diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js new file mode 100644 index 00000000000..4d235cd8dfb --- /dev/null +++ b/src/components/rangeslider/draw.js @@ -0,0 +1,421 @@ +/** +* Copyright 2012-2016, 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 d3 = require('d3'); + +var Plotly = require('../../plotly'); +var Plots = require('../../plots/plots'); +var Axes = require('../../plots/cartesian/axes'); +var Lib = require('../../lib'); + +var dragElement = require('../dragelement'); +var setCursor = require('../../lib/setcursor'); + +var constants = require('./constants'); +var rangePlot = require('./range_plot'); + + +module.exports = function(gd) { + var fullLayout = gd._fullLayout, + rangeSliderData = makeRangeSliderData(fullLayout); + + /* + * + * + * < .... range plot /> + * + * + * + * + * + * + * + * + * + * + * ... + */ + + function keyFunction(axisOpts) { + return axisOpts._name; + } + + var rangeSliders = fullLayout._infolayer + .selectAll('g.' + constants.containerClassName) + .data(rangeSliderData, keyFunction); + + rangeSliders.enter().append('g') + .classed(constants.containerClassName, true) + .attr('pointer-events', 'all'); + + rangeSliders.exit().remove(); + + // remove push margin object(s) + if(rangeSliders.exit().size()) clearPushMargins(gd); + + // return early if no range slider is visible + if(rangeSliderData.length === 0) return; + + // 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); + } + + // update range slider dimensions + + var margin = fullLayout.margin, + graphSize = fullLayout._size, + domain = axisOpts.domain; + + opts._id = constants.name + axisOpts._id; + opts._width = graphSize.w * (domain[1] - domain[0]); + 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; + + rangeSlider.attr('transform', 'translate(' + x + ',' + y + ')'); + + // update inner nodes + + rangeSlider + .call(drawBg, gd, axisOpts, opts) + .call(drawRangePlot, gd, axisOpts, opts) + .call(drawMasks, gd, axisOpts, opts) + .call(drawSlideBox, gd, axisOpts, opts) + .call(drawGrabbers, gd, axisOpts, opts); + + // update data <--> pixel coordinate conversion methods + + var range0 = opts.range[0], + range1 = opts.range[1], + dist = range1 - range0; + + opts.p2d = function(v) { + return (v / opts._width) * dist + range0; + }; + + opts.d2p = function(v) { + return (v - range0) / dist * opts._width; + }; + + // setup drag element + setupDragElement(rangeSlider, gd, axisOpts, opts); + + // update current range + setPixelRange(rangeSlider, gd, axisOpts, opts); + + // 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 + }); + }); +}; + +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 []; + + return [fullLayout.xaxis]; +} + +function setupDragElement(rangeSlider, gd, axisOpts, opts) { + var slideBox = rangeSlider.select('rect.' + constants.slideBoxClassName).node(), + grabAreaMin = rangeSlider.select('rect.' + constants.grabAreaMinClassName).node(), + grabAreaMax = rangeSlider.select('rect.' + constants.grabAreaMaxClassName).node(); + + rangeSlider.on('mousedown', function() { + var event = d3.event, + target = event.target, + startX = event.clientX, + offsetX = startX - rangeSlider.node().getBoundingClientRect().left, + minVal = opts.d2p(axisOpts.range[0]), + maxVal = opts.d2p(axisOpts.range[1]); + + var dragCover = dragElement.coverSlip(); + + dragCover.addEventListener('mousemove', mouseMove); + dragCover.addEventListener('mouseup', mouseUp); + + function mouseMove(e) { + var delta = +e.clientX - startX; + var pixelMin, pixelMax, cursor; + + switch(target) { + case slideBox: + cursor = 'ew-resize'; + pixelMin = minVal + delta; + pixelMax = maxVal + delta; + break; + + case grabAreaMin: + cursor = 'col-resize'; + pixelMin = minVal + delta; + pixelMax = maxVal; + break; + + case grabAreaMax: + cursor = 'col-resize'; + pixelMin = minVal; + pixelMax = maxVal + delta; + break; + + default: + cursor = 'ew-resize'; + pixelMin = offsetX; + pixelMax = offsetX + delta; + break; + } + + if(pixelMax < pixelMin) { + var tmp = pixelMax; + pixelMax = pixelMin; + pixelMin = tmp; + } + + opts._pixelMin = pixelMin; + opts._pixelMax = pixelMax; + + setCursor(d3.select(dragCover), cursor); + setDataRange(rangeSlider, gd, axisOpts, opts); + } + + function mouseUp() { + dragCover.removeEventListener('mousemove', mouseMove); + dragCover.removeEventListener('mouseup', mouseUp); + Lib.removeElement(dragCover); + } + }); +} + +function setDataRange(rangeSlider, gd, axisOpts, opts) { + + function clamp(v) { + return Lib.constrain(v, opts.range[0], opts.range[1]); + } + + var dataMin = clamp(opts.p2d(opts._pixelMin)), + dataMax = clamp(opts.p2d(opts._pixelMax)); + + window.requestAnimationFrame(function() { + Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]); + }); +} + +function setPixelRange(rangeSlider, gd, axisOpts, opts) { + + function clamp(v) { + return Lib.constrain(v, 0, opts._width); + } + + var pixelMin = clamp(opts.d2p(axisOpts.range[0])), + pixelMax = clamp(opts.d2p(axisOpts.range[1])); + + rangeSlider.select('rect.' + constants.slideBoxClassName) + .attr('x', pixelMin) + .attr('width', pixelMax - pixelMin); + + rangeSlider.select('rect.' + constants.maskMinClassName) + .attr('width', pixelMin); + + rangeSlider.select('rect.' + constants.maskMaxClassName) + .attr('x', pixelMax) + .attr('width', opts._width - pixelMax); + + rangeSlider.select('g.' + constants.grabberMinClassName) + .attr('transform', 'translate(' + (pixelMin - constants.handleWidth - 1) + ',0)'); + + rangeSlider.select('g.' + constants.grabberMaxClassName) + .attr('transform', 'translate(' + pixelMax + ',0)'); +} + +function drawBg(rangeSlider, gd, axisOpts, opts) { + var bg = rangeSlider.selectAll('rect.' + constants.bgClassName) + .data([0]); + + bg.enter().append('rect') + .classed(constants.bgClassName, true) + .attr({ + x: 0, + y: 0, + 'shape-rendering': 'crispEdges' + }); + + var borderCorrect = (opts.borderwidth % 2) === 0 ? + opts.borderwidth : + opts.borderwidth - 1; + + var offsetShift = -opts._offsetShift; + + bg.attr({ + width: opts._width + borderCorrect, + height: opts._height + borderCorrect, + transform: 'translate(' + offsetShift + ',' + offsetShift + ')', + fill: opts.bgcolor, + stroke: opts.bordercolor, + 'stroke-width': opts.borderwidth, + }); +} + +function drawRangePlot(rangeSlider, gd, axisOpts, opts) { + var rangePlots = rangePlot(gd, opts._width, opts._height); + + var gRangePlot = rangeSlider.selectAll('g.' + constants.rangePlotClassName) + .data([0]); + + gRangePlot.enter().append('g') + .classed(constants.rangePlotClassName, true); + + gRangePlot.html(null); + gRangePlot.node().appendChild(rangePlots); +} + +function drawMasks(rangeSlider, gd, axisOpts, opts) { + var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName) + .data([0]); + + maskMin.enter().append('rect') + .classed(constants.maskMinClassName, true) + .attr({ x: 0, y: 0 }); + + maskMin.attr({ + height: opts._height, + fill: constants.maskColor + }); + + var maskMax = rangeSlider.selectAll('rect.' + constants.maskMaxClassName) + .data([0]); + + maskMax.enter().append('rect') + .classed(constants.maskMaxClassName, true) + .attr('y', 0); + + maskMax.attr({ + height: opts._height, + fill: constants.maskColor + }); +} + +function drawSlideBox(rangeSlider, gd, axisOpts, opts) { + var slideBox = rangeSlider.selectAll('rect.' + constants.slideBoxClassName) + .data([0]); + + slideBox.enter().append('rect') + .classed(constants.slideBoxClassName, true) + .attr('y', 0) + .attr('cursor', constants.slideBoxCursor); + + slideBox.attr({ + height: opts._height, + fill: constants.slideBoxFill + }); +} + +function drawGrabbers(rangeSlider, gd, axisOpts, opts) { + + // + + var grabberMin = rangeSlider.selectAll('g.' + constants.grabberMinClassName) + .data([0]); + grabberMin.enter().append('g') + .classed(constants.grabberMinClassName, true); + + var grabberMax = rangeSlider.selectAll('g.' + constants.grabberMaxClassName) + .data([0]); + grabberMax.enter().append('g') + .classed(constants.grabberMaxClassName, true); + + // + + var handleFixAttrs = { + x: 0, + width: constants.handleWidth, + rx: constants.handleRadius, + fill: constants.handleFill, + stroke: constants.handleStroke, + 'shape-rendering': 'crispEdges' + }; + + var handleDynamicAttrs = { + y: opts._height / 4, + height: opts._height / 2, + }; + + var handleMin = grabberMin.selectAll('rect.' + constants.handleMinClassName) + .data([0]); + handleMin.enter().append('rect') + .classed(constants.handleMinClassName, true) + .attr(handleFixAttrs); + handleMin.attr(handleDynamicAttrs); + + var handleMax = grabberMax.selectAll('rect.' + constants.handleMaxClassName) + .data([0]); + handleMax.enter().append('rect') + .classed(constants.handleMaxClassName, true) + .attr(handleFixAttrs); + handleMax.attr(handleDynamicAttrs); + + // + + var grabAreaFixAttrs = { + width: constants.grabAreaWidth, + y: 0, + fill: constants.grabAreaFill, + cursor: constants.grabAreaCursor + }; + + var grabAreaMin = grabberMin.selectAll('rect.' + constants.grabAreaMinClassName) + .data([0]); + grabAreaMin.enter().append('rect') + .classed(constants.grabAreaMinClassName, true) + .attr(grabAreaFixAttrs); + grabAreaMin.attr({ + x: constants.grabAreaMinOffset, + 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 + }); +} + +function clearPushMargins(gd) { + var pushMargins = gd._fullLayout._pushmargin || {}, + keys = Object.keys(pushMargins); + + for(var i = 0; i < keys.length; i++) { + var k = keys[i]; + + if(k.indexOf(constants.name) !== -1) { + Plots.autoMargin(gd, k); + } + } +} diff --git a/src/components/rangeslider/index.js b/src/components/rangeslider/index.js index cfcf373ceb0..1c8f3645b7a 100644 --- a/src/components/rangeslider/index.js +++ b/src/components/rangeslider/index.js @@ -8,52 +8,10 @@ 'use strict'; - -var Plots = require('../../plots/plots'); - -var createSlider = require('./create_slider'); -var layoutAttributes = require('./attributes'); -var handleDefaults = require('./defaults'); - - module.exports = { moduleType: 'component', name: 'rangeslider', - layoutAttributes: layoutAttributes, - handleDefaults: handleDefaults, - draw: draw + layoutAttributes: require('./attributes'), + handleDefaults: require('./defaults'), + draw: require('./draw') }; - -function draw(gd) { - if(!gd._fullLayout.xaxis) return; - - var fullLayout = gd._fullLayout, - sliderContainer = fullLayout._infolayer.selectAll('g.range-slider'), - options = fullLayout.xaxis.rangeslider; - - - if(!options || !options.visible) { - sliderContainer.data([]) - .exit().remove(); - - Plots.autoMargin(gd, 'range-slider'); - - return; - } - - - var height = (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) * options.thickness, - offsetShift = Math.floor(options.borderwidth / 2); - - if(sliderContainer[0].length === 0 && !fullLayout._has('gl2d')) createSlider(gd); - - // Need to default to 0 for when making gl plots - var bb = fullLayout.xaxis._boundingBox ? - fullLayout.xaxis._boundingBox.height : 0; - - Plots.autoMargin(gd, 'range-slider', { - x: 0, y: 0, l: 0, r: 0, t: 0, - b: height + fullLayout.margin.b + bb, - pad: 15 + offsetShift * 2 - }); -} diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 8b8be2bb265..1bb7d7b4e98 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -1696,7 +1696,6 @@ Plotly.relayout = function relayout(gd, astr, val) { if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); return plotDone.then(function() { - subroutines.setRangeSliderRange(gd, specs.eventData); gd.emit('plotly_relayout', specs.eventData); return gd; }); @@ -2093,8 +2092,6 @@ Plotly.update = function update(gd, traceUpdate, layoutUpdate, traces) { if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); return plotDone.then(function() { - subroutines.setRangeSliderRange(gd, relayoutSpecs.eventData); - gd.emit('plotly_update', { data: restyleSpecs.eventData, layout: relayoutSpecs.eventData diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 758a778df92..cdef6e227b9 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -318,22 +318,3 @@ exports.doModeBar = function(gd) { return Plots.previousPromises(gd); }; - -exports.setRangeSliderRange = function(gd, changes) { - var fullLayout = gd._fullLayout; - - var newMin = changes['xaxis.range'] ? changes['xaxis.range'][0] : changes['xaxis.range[0]'], - newMax = changes['xaxis.range'] ? changes['xaxis.range'][1] : changes['xaxis.range[1]']; - - var rangeSlider = fullLayout.xaxis && fullLayout.xaxis.rangeslider ? - fullLayout.xaxis.rangeslider : {}; - - if(rangeSlider.visible) { - if(newMin || newMax) { - fullLayout.xaxis.rangeslider.setRange(newMin, newMax); - } - else if(changes['xaxis.autorange']) { - fullLayout.xaxis.rangeslider.setRange(); - } - } -}; diff --git a/test/jasmine/assets/custom_matchers.js b/test/jasmine/assets/custom_matchers.js index 75d88f5bdb1..675bc02f70c 100644 --- a/test/jasmine/assets/custom_matchers.js +++ b/test/jasmine/assets/custom_matchers.js @@ -65,6 +65,26 @@ module.exports = { msgExtra ].join(' '); + return { + pass: passed, + message: message + }; + } + }; + }, + + toBeWithin: function() { + return { + compare: function(actual, expected, tolerance, msgExtra) { + var passed = Math.abs(actual - expected) < tolerance; + + var message = [ + 'Expected', actual, + 'to be close to', expected, + 'within', tolerance, + msgExtra + ].join(' '); + return { pass: passed, message: message diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 87ff2604b84..99f82f71d4d 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -1,10 +1,17 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); + var RangeSlider = require('@src/components/rangeslider'); +var constants = require('@src/components/rangeslider/constants'); +var mock = require('../../image/mocks/range_slider.json'); + var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); -var mock = require('../../image/mocks/range_slider.json'); var mouseEvent = require('../assets/mouse_event'); +var customMatchers = require('../assets/custom_matchers'); + +var TOL = 6; + describe('the range slider', function() { @@ -14,16 +21,33 @@ describe('the range slider', function() { var sliderY = 393; + function getRangeSlider() { + var className = constants.containerClassName; + return document.getElementsByClassName(className)[0]; + } + + function testTranslate1D(node, val) { + var transformParts = node.getAttribute('transform').split('('); + + expect(transformParts[0]).toEqual('translate'); + expect(+transformParts[1].split(',0)')[0]).toBeWithin(val, TOL); + } + describe('when specified as visible', function() { + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + beforeEach(function(done) { gd = createGraphDiv(); var mockCopy = Lib.extendDeep({}, mock); Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(function() { - rangeSlider = document.getElementsByClassName('range-slider')[0]; + rangeSlider = getRangeSlider(); children = rangeSlider.children; + done(); }); }); @@ -56,73 +80,73 @@ describe('the range slider', function() { it('should react to resizing the minimum handle', function(done) { var start = 85, end = 140, - dataMinStart = rangeSlider.getAttribute('data-min'), diff = end - start; + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + slide(start, sliderY, end, sliderY).then(function() { var maskMin = children[2], handleMin = children[5]; - expect(rangeSlider.getAttribute('data-min')).toEqual(String(+dataMinStart + diff)); + 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) + ')'); + expect(handleMin.getAttribute('transform')).toBe('translate(' + (diff - 3) + ',0)'); }).then(done); }); - function testTranslate1D(node, val) { - var transformParts = node.getAttribute('transform').split('('); - expect(transformParts[0]).toEqual('translate'); - expect(+transformParts[1].split(')')[0]).toBeCloseTo(val, 0); - } - it('should react to resizing the maximum handle', function(done) { var start = 695, end = 490, - dataMaxStart = rangeSlider.getAttribute('data-max'), + dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), diff = end - start; + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + slide(start, sliderY, end, sliderY).then(function() { var maskMax = children[3], handleMax = children[6]; - expect(+rangeSlider.getAttribute('data-max')).toBeCloseTo(+dataMaxStart + diff, 0); + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); - testTranslate1D(handleMax, +dataMaxStart + diff); + + testTranslate1D(handleMax, dataMaxStart + diff); }).then(done); }); it('should react to moving the slidebox left to right', function(done) { var start = 250, end = 300, - dataMinStart = rangeSlider.getAttribute('data-min'), + dataMinStart = gd._fullLayout.xaxis.rangeslider.d2p(0), diff = end - start; + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + slide(start, sliderY, end, sliderY).then(function() { var maskMin = children[2], handleMin = children[5]; - expect(+rangeSlider.getAttribute('data-min')).toBeCloseTo(String(+dataMinStart + diff)); + expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49], -0.5); expect(+maskMin.getAttribute('width')).toBeCloseTo(String(diff)); - testTranslate1D(handleMin, +dataMinStart + diff - 3); + testTranslate1D(handleMin, dataMinStart + diff - 3); }).then(done); }); it('should react to moving the slidebox right to left', function(done) { var start = 300, end = 250, - dataMaxStart = rangeSlider.getAttribute('data-max'), + dataMaxStart = gd._fullLayout.xaxis.rangeslider.d2p(49), diff = end - start; + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 49]); + slide(start, sliderY, end, sliderY).then(function() { var maskMax = children[3], handleMax = children[6]; - expect(+rangeSlider.getAttribute('data-max')).toBeCloseTo(+dataMaxStart + diff); + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); - testTranslate1D(handleMax, +dataMaxStart + diff); + testTranslate1D(handleMax, dataMaxStart + diff); }).then(done); - - }); it('should resize the main plot when rangeslider has moved', function(done) { @@ -147,22 +171,107 @@ describe('the range slider', function() { }); it('should relayout with relayout "array syntax"', function(done) { - Plotly.relayout(gd, 'xaxis.range', [10, 20]) - .then(function() { - expect(gd._fullLayout.xaxis.range).toEqual([10, 20]); - expect(+rangeSlider.getAttribute('data-min')).toBeCloseTo(124.69, -1); - expect(+rangeSlider.getAttribute('data-max')).toBeCloseTo(249.39, -1); - }) - .then(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 "element syntax"', function(done) { - Plotly.relayout(gd, 'xaxis.range[0]', 10) - .then(function() { - expect(gd._fullLayout.xaxis.range[0]).toEqual(10); - expect(+rangeSlider.getAttribute('data-min')).toBeCloseTo(124.69, -1); - }) - .then(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, 619); + }) + .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 on size / domain udpate', function(done) { + var maskMin = children[2], + maskMax = children[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.domain', [0.3, 0.7]); + }) + .then(function() { + var maskMin = children[2], + maskMax = children[3]; + + expect(+maskMin.getAttribute('width')).toBeWithin(25.26, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(197.06, TOL); + + return Plotly.relayout(gd, 'width', 400); + }) + .then(function() { + var maskMin = children[2], + maskMax = children[3]; + + expect(+maskMin.getAttribute('width')).toBeWithin(9.22, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(71.95, TOL); + + }) + .then(done); }); }); @@ -177,7 +286,7 @@ describe('the range slider', function() { 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 = document.getElementsByClassName('range-slider')[0]; + var rangeSlider = getRangeSlider(); expect(rangeSlider).not.toBeDefined(); }) .then(done); @@ -187,7 +296,7 @@ describe('the range slider', function() { Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) .then(function() { Plotly.relayout(gd, 'xaxis.rangeslider', 'exists'); }) .then(function() { - var rangeSlider = document.getElementsByClassName('range-slider')[0]; + var rangeSlider = getRangeSlider(); expect(rangeSlider).toBeDefined(); }) .then(done); @@ -197,7 +306,7 @@ describe('the range slider', function() { Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], {}) .then(function() { Plotly.relayout(gd, 'xaxis.rangeslider.visible', true); }) .then(function() { - var rangeSlider = document.getElementsByClassName('range-slider')[0]; + var rangeSlider = getRangeSlider(); expect(rangeSlider).toBeDefined(); }) .then(done); @@ -207,7 +316,7 @@ describe('the range slider', function() { Plotly.plot(gd, [{ x: [1, 2, 3], y: [2, 3, 4] }], { xaxis: { rangeslider: { visible: true }}}) .then(function() { Plotly.relayout(gd, 'xaxis.rangeslider.visible', false); }) .then(function() { - var rangeSlider = document.getElementsByClassName('range-slider')[0]; + var rangeSlider = getRangeSlider(); expect(rangeSlider).not.toBeDefined(); }) .then(done); @@ -252,13 +361,14 @@ describe('the range slider', function() { thickness: 0.15, bgcolor: '#fff', borderwidth: 0, - bordercolor: '#444' + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }, _needsExpand: true }, yaxis: { fixedrange: true - } + }, }; RangeSlider.handleDefaults(layoutIn, layoutOut, axName, counterAxes); @@ -278,7 +388,8 @@ describe('the range slider', function() { thickness: 0.15, bgcolor: '#fff', borderwidth: 0, - bordercolor: '#444' + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }, _needsExpand: true }, @@ -310,7 +421,8 @@ describe('the range slider', function() { thickness: 0.15, bgcolor: '#fff', borderwidth: 0, - bordercolor: '#444' + bordercolor: '#444', + _input: layoutIn.xaxis.rangeslider }, _needsExpand: true }, @@ -336,7 +448,8 @@ describe('the range slider', function() { thickness: 0.15, bgcolor: '#fff', borderwidth: 0, - bordercolor: '#444' + bordercolor: '#444', + _input: {} }, _needsExpand: true }, @@ -362,7 +475,8 @@ describe('the range slider', function() { bgcolor: '#fff', borderwidth: 0, bordercolor: '#444', - range: [1, 10] + range: [1, 10], + _input: layoutIn.xaxis.rangeslider }, range: [1, 10] }, @@ -386,7 +500,8 @@ describe('the range slider', function() { thickness: 0.15, bgcolor: '#fff', borderwidth: 0, - bordercolor: '#444' + bordercolor: '#444', + _input: {} }, range: [2, 40], _needsExpand: true @@ -398,6 +513,26 @@ describe('the range slider', function() { expect(layoutOut).toEqual(expected); }); + + it('should default \'bgcolor\' to layout \'plot_bgcolor\'', function() { + var layoutIn = { + xaxis: { rangeslider: true }, + yaxis: {}, + }; + + var layoutOut = { + xaxis: { range: [2, 40]}, + yaxis: {}, + plot_bgcolor: 'blue' + }; + + var axName = 'xaxis', + counterAxes = ['yaxis']; + + RangeSlider.handleDefaults(layoutIn, layoutOut, axName, counterAxes); + + expect(layoutOut.xaxis.rangeslider.bgcolor).toEqual('blue'); + }); }); describe('in general', function() { @@ -411,9 +546,9 @@ describe('the range slider', function() { it('should plot when only x data is provided', function(done) { Plotly.plot(gd, [{ x: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) .then(function() { - var rangeslider = document.getElementsByClassName('range-slider'); + var rangeSlider = getRangeSlider(); - expect(rangeslider.length).toBe(1); + expect(rangeSlider).toBeDefined(); }) .then(done); }); @@ -421,9 +556,9 @@ describe('the range slider', function() { it('should plot when only y data is provided', function(done) { Plotly.plot(gd, [{ y: [1, 2, 3] }], { xaxis: { rangeslider: {} }}) .then(function() { - var rangeslider = document.getElementsByClassName('range-slider'); + var rangeSlider = getRangeSlider(); - expect(rangeslider.length).toBe(1); + expect(rangeSlider).toBeDefined(); }) .then(done); });