From a9b83bbcd1b53bd19406629a5b74cbf2d6732e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:12:39 -0400 Subject: [PATCH 01/14] dragelement: export cover slip creator --- src/components/dragelement/index.js | 2 ++ 1 file changed, 2 insertions(+) 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); From b45f116fbdbb0a56d7ac5bd2eab91bbb3f0eb90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:14:46 -0400 Subject: [PATCH 02/14] rangeslider: keep ref of input rangeslider container - so that computed rangeslider range can be copied into input container, ensuring that the autorange routine isn't called on updates. --- src/components/rangeslider/defaults.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index 936eb030680..d3a982a4ffe 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -15,8 +15,12 @@ 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) { @@ -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; }; From dd1f9a3c773dab61e79dc4795912f871f13bf36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:19:45 -0400 Subject: [PATCH 03/14] rangeslider: refactor draw routine - use d3 enter/exit/update pattern to handle drawing step for creation / deletion and update paths -> no longer need a relayout hook !!! - fix: range slider span depends on axis range - fix: range slider option now update on relayout - use coverslip to avoid select axis title --- src/components/rangeslider/constants.js | 51 +++ src/components/rangeslider/draw.js | 424 ++++++++++++++++++++++++ src/components/rangeslider/index.js | 48 +-- 3 files changed, 478 insertions(+), 45 deletions(-) create mode 100644 src/components/rangeslider/constants.js create mode 100644 src/components/rangeslider/draw.js 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/draw.js b/src/components/rangeslider/draw.js new file mode 100644 index 00000000000..e740a4a6cf8 --- /dev/null +++ b/src/components/rangeslider/draw.js @@ -0,0 +1,424 @@ +/** +* 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); + + var newRangeSliders = 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; + + // set new slider range using axis autorange if necessary + newRangeSliders.each(function(axisOpts) { + var opts = axisOpts[constants.name]; + + // 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); + } + }); + + // for all present range slides + rangeSliders.each(function(axisOpts) { + var rangeSlider = d3.select(this), + opts = axisOpts[constants.name]; + + // update range slider dimensions + + var margin = fullLayout.margin, + graphSize = fullLayout._size, + domain = axisOpts.domain; + + 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, constants.name + axisOpts._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 - }); -} From 1bb274b3241eac865c9e066a8bf58ec0e9dcea44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:29:54 -0400 Subject: [PATCH 04/14] rangeslider: rm (now useless) create_slider.js file --- src/components/rangeslider/create_slider.js | 287 -------------------- 1 file changed, 287 deletions(-) delete mode 100644 src/components/rangeslider/create_slider.js 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; - }); -}; From 432f5994e041287116235350d772660b3715f065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:05:26 -0400 Subject: [PATCH 05/14] plot api: rm (now useless) range slider setRangeSliderRange --- src/plot_api/plot_api.js | 3 --- src/plot_api/subroutines.js | 19 ------------------- 2 files changed, 22 deletions(-) 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(); - } - } -}; From 417435f8896217393fd0c17b29ee81ae0a9afdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:10:51 -0400 Subject: [PATCH 06/14] test: adapt range slider tests - with proper class name and full 'translate' values --- test/jasmine/tests/range_slider_test.js | 53 ++++++++++++++++--------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 87ff2604b84..8e03ef9ae5c 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -1,11 +1,15 @@ var Plotly = require('@lib/index'); var Lib = require('@src/lib'); + var RangeSlider = require('@src/components/rangeslider'); +var constants = require('@src/components/rangeslider/constants'); + 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'); + describe('the range slider', function() { var gd, @@ -14,6 +18,12 @@ describe('the range slider', function() { var sliderY = 393; + function getRangeSlider() { + var className = constants.containerClassName; + return document.getElementsByClassName(className)[0]; + } + + describe('when specified as visible', function() { beforeEach(function(done) { @@ -22,8 +32,9 @@ describe('the range slider', function() { 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(); }); }); @@ -65,14 +76,14 @@ describe('the range slider', function() { expect(rangeSlider.getAttribute('data-min')).toEqual(String(+dataMinStart + diff)); 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); + expect(+transformParts[1].split(',0)')[0]).toBeCloseTo(val, 0); } it('should react to resizing the maximum handle', function(done) { @@ -177,7 +188,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 +198,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 +208,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 +218,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 +263,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 +290,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 +323,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 +350,8 @@ describe('the range slider', function() { thickness: 0.15, bgcolor: '#fff', borderwidth: 0, - bordercolor: '#444' + bordercolor: '#444', + _input: {} }, _needsExpand: true }, @@ -362,7 +377,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 +402,8 @@ describe('the range slider', function() { thickness: 0.15, bgcolor: '#fff', borderwidth: 0, - bordercolor: '#444' + bordercolor: '#444', + _input: {} }, range: [2, 40], _needsExpand: true @@ -411,9 +428,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 +438,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); }); From 74607d4ead2726ff81d3ef2082f3012f9bfc77ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:06:49 -0400 Subject: [PATCH 07/14] test: replace data-min / data-max in range slider tests --- test/jasmine/tests/range_slider_test.js | 89 ++++++++++++++++--------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 8e03ef9ae5c..d13efb13b64 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -3,11 +3,12 @@ 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'); describe('the range slider', function() { @@ -23,9 +24,18 @@ describe('the range slider', function() { 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]).toBeCloseTo(val, 0); + } describe('when specified as visible', function() { + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + beforeEach(function(done) { gd = createGraphDiv(); @@ -67,73 +77,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.35, 49]); expect(maskMin.getAttribute('width')).toEqual(String(diff)); 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)')[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]); 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]); 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]); 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) { @@ -158,22 +168,35 @@ 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')).toBeCloseTo(126.32, 0); + expect(+maskMax.getAttribute('width')).toBeCloseTo(366.34, 0); + 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')).toBeCloseTo(126.32, 0); + expect(+maskMax.getAttribute('width')).toBeCloseTo(0); + testTranslate1D(handleMin, 123.32); + testTranslate1D(handleMax, 619); + }) + .then(done); + }); + }); }); From 1d7b066fdacd0589cd1389b2bb5e484fc00ea75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 18:07:06 -0400 Subject: [PATCH 08/14] test: add range slider rrelayout test cases --- test/jasmine/tests/range_slider_test.js | 72 +++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index d13efb13b64..05150c1dc7a 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -197,6 +197,78 @@ describe('the range slider', function() { .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')).toBeCloseTo(63.16, 0); + expect(+maskMax.getAttribute('width')).toBeCloseTo(492.67, 0); + + return Plotly.relayout(gd, 'xaxis.domain', [0.3, 0.7]); + }) + .then(function() { + var maskMin = children[2], + maskMax = children[3]; + + expect(+maskMin.getAttribute('width')).toBeCloseTo(25.26, 0); + expect(+maskMax.getAttribute('width')).toBeCloseTo(197.06, 0); + + return Plotly.relayout(gd, 'width', 400); + }) + .then(function() { + var maskMin = children[2], + maskMax = children[3]; + + expect(+maskMin.getAttribute('width')).toBeCloseTo(9.22, 0); + expect(+maskMax.getAttribute('width')).toBeCloseTo(71.95, 0); + + }) + .then(done); }); }); From c0ca4f54abf963dbd99e2aff895e2969d9ca8a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 21 Sep 2016 21:18:53 -0400 Subject: [PATCH 09/14] test: add a little tolerance for FF --- test/jasmine/tests/range_slider_test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 05150c1dc7a..8e5ecf32908 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -27,7 +27,7 @@ describe('the range slider', function() { function testTranslate1D(node, val) { var transformParts = node.getAttribute('transform').split('('); expect(transformParts[0]).toEqual('translate'); - expect(+transformParts[1].split(',0)')[0]).toBeCloseTo(val, 0); + expect(+transformParts[1].split(',0)')[0]).toBeCloseTo(val, -1); } describe('when specified as visible', function() { @@ -85,7 +85,7 @@ describe('the range slider', function() { var maskMin = children[2], handleMin = children[5]; - expect(gd.layout.xaxis.range).toBeCloseToArray([4.35, 49]); + 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)'); }).then(done); @@ -103,7 +103,7 @@ describe('the range slider', function() { var maskMax = children[3], handleMax = children[6]; - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77]); + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 32.77], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); testTranslate1D(handleMax, dataMaxStart + diff); @@ -122,7 +122,7 @@ describe('the range slider', function() { var maskMin = children[2], handleMin = children[5]; - expect(gd.layout.xaxis.range).toBeCloseToArray([3.96, 49]); + 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); @@ -140,7 +140,7 @@ describe('the range slider', function() { var maskMax = children[3], handleMax = children[6]; - expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04]); + expect(gd.layout.xaxis.range).toBeCloseToArray([0, 45.04], -0.5); expect(+maskMax.getAttribute('width')).toBeCloseTo(-diff); testTranslate1D(handleMax, dataMaxStart + diff); }).then(done); @@ -174,8 +174,8 @@ describe('the range slider', function() { handleMin = children[5], handleMax = children[6]; - expect(+maskMin.getAttribute('width')).toBeCloseTo(126.32, 0); - expect(+maskMax.getAttribute('width')).toBeCloseTo(366.34, 0); + expect(+maskMin.getAttribute('width')).toBeCloseTo(126.32, -0.5); + expect(+maskMax.getAttribute('width')).toBeCloseTo(366.34, -0.5); testTranslate1D(handleMin, 123.32); testTranslate1D(handleMax, 252.65); }) @@ -247,7 +247,7 @@ describe('the range slider', function() { Plotly.relayout(gd, 'xaxis.range', [5, 10]).then(function() { expect(+maskMin.getAttribute('width')).toBeCloseTo(63.16, 0); - expect(+maskMax.getAttribute('width')).toBeCloseTo(492.67, 0); + expect(+maskMax.getAttribute('width')).toBeCloseTo(492.67, -1); return Plotly.relayout(gd, 'xaxis.domain', [0.3, 0.7]); }) @@ -256,7 +256,7 @@ describe('the range slider', function() { maskMax = children[3]; expect(+maskMin.getAttribute('width')).toBeCloseTo(25.26, 0); - expect(+maskMax.getAttribute('width')).toBeCloseTo(197.06, 0); + expect(+maskMax.getAttribute('width')).toBeCloseTo(197.06, -1); return Plotly.relayout(gd, 'width', 400); }) From b062f0b90242da64a9763a9a183e078f91843327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 5 Oct 2016 16:03:03 -0400 Subject: [PATCH 10/14] test: add toBeWithin jasmine matcher --- test/jasmine/assets/custom_matchers.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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 From 85022066bf8109d3905368ddad68ff6bdae90338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 5 Oct 2016 16:03:20 -0400 Subject: [PATCH 11/14] test: use toBeWithin in range slider test --- test/jasmine/tests/range_slider_test.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 8e5ecf32908..0aae5c46df9 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -10,6 +10,8 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var mouseEvent = require('../assets/mouse_event'); var customMatchers = require('../assets/custom_matchers'); +var TOL = 6; + describe('the range slider', function() { @@ -26,8 +28,9 @@ describe('the range slider', function() { function testTranslate1D(node, val) { var transformParts = node.getAttribute('transform').split('('); + expect(transformParts[0]).toEqual('translate'); - expect(+transformParts[1].split(',0)')[0]).toBeCloseTo(val, -1); + expect(+transformParts[1].split(',0)')[0]).toBeWithin(val, TOL); } describe('when specified as visible', function() { @@ -174,8 +177,8 @@ describe('the range slider', function() { handleMin = children[5], handleMax = children[6]; - expect(+maskMin.getAttribute('width')).toBeCloseTo(126.32, -0.5); - expect(+maskMax.getAttribute('width')).toBeCloseTo(366.34, -0.5); + expect(+maskMin.getAttribute('width')).toBeWithin(125, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(365, TOL); testTranslate1D(handleMin, 123.32); testTranslate1D(handleMax, 252.65); }) @@ -189,8 +192,8 @@ describe('the range slider', function() { handleMin = children[5], handleMax = children[6]; - expect(+maskMin.getAttribute('width')).toBeCloseTo(126.32, 0); - expect(+maskMax.getAttribute('width')).toBeCloseTo(0); + expect(+maskMin.getAttribute('width')).toBeWithin(126, TOL); + expect(+maskMax.getAttribute('width')).toEqual(0); testTranslate1D(handleMin, 123.32); testTranslate1D(handleMax, 619); }) @@ -246,8 +249,8 @@ describe('the range slider', function() { maskMax = children[3]; Plotly.relayout(gd, 'xaxis.range', [5, 10]).then(function() { - expect(+maskMin.getAttribute('width')).toBeCloseTo(63.16, 0); - expect(+maskMax.getAttribute('width')).toBeCloseTo(492.67, -1); + 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]); }) @@ -255,8 +258,8 @@ describe('the range slider', function() { var maskMin = children[2], maskMax = children[3]; - expect(+maskMin.getAttribute('width')).toBeCloseTo(25.26, 0); - expect(+maskMax.getAttribute('width')).toBeCloseTo(197.06, -1); + expect(+maskMin.getAttribute('width')).toBeWithin(25.26, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(197.06, TOL); return Plotly.relayout(gd, 'width', 400); }) @@ -264,8 +267,8 @@ describe('the range slider', function() { var maskMin = children[2], maskMax = children[3]; - expect(+maskMin.getAttribute('width')).toBeCloseTo(9.22, 0); - expect(+maskMax.getAttribute('width')).toBeCloseTo(71.95, 0); + expect(+maskMin.getAttribute('width')).toBeWithin(9.22, TOL); + expect(+maskMax.getAttribute('width')).toBeWithin(71.95, TOL); }) .then(done); From 697ed3a1685311680756d7f3bd1b10db88d5e7e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 5 Oct 2016 21:28:11 -0400 Subject: [PATCH 12/14] move range slider 'range' compute in update - sometimes the rangeslider range is cleared when no enter selection present, e.g. on relayout(gd, xaxis.rangeslider: { thickness: 0.2 }) --- src/components/rangeslider/draw.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index e740a4a6cf8..1b5004e423c 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -51,7 +51,7 @@ module.exports = function(gd) { .selectAll('g.' + constants.containerClassName) .data(rangeSliderData, keyFunction); - var newRangeSliders = rangeSliders.enter().append('g') + rangeSliders.enter().append('g') .classed(constants.containerClassName, true) .attr('pointer-events', 'all'); @@ -63,21 +63,17 @@ module.exports = function(gd) { // return early if no range slider is visible if(rangeSliderData.length === 0) return; - // set new slider range using axis autorange if necessary - newRangeSliders.each(function(axisOpts) { - var opts = axisOpts[constants.name]; + // 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); } - }); - - // for all present range slides - rangeSliders.each(function(axisOpts) { - var rangeSlider = d3.select(this), - opts = axisOpts[constants.name]; // update range slider dimensions From b1155a3116f567288a75c4fc775580ba90049dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Fri, 23 Sep 2016 15:35:53 -0400 Subject: [PATCH 13/14] book-keep range slider id --- src/components/rangeslider/draw.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/rangeslider/draw.js b/src/components/rangeslider/draw.js index 1b5004e423c..4d235cd8dfb 100644 --- a/src/components/rangeslider/draw.js +++ b/src/components/rangeslider/draw.js @@ -81,6 +81,7 @@ module.exports = function(gd) { 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); @@ -123,7 +124,7 @@ module.exports = function(gd) { var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0; - Plots.autoMargin(gd, constants.name + axisOpts._id, { + 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 From 2a7696a4429dff71bf6e7896b7f2fa2c139a7583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 5 Oct 2016 21:34:20 -0400 Subject: [PATCH 14/14] rangeslider: inherit 'bgcolor' from 'plot_bgcolor' --- src/components/rangeslider/defaults.js | 2 +- test/jasmine/tests/range_slider_test.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/rangeslider/defaults.js b/src/components/rangeslider/defaults.js index d3a982a4ffe..6e745a5624c 100644 --- a/src/components/rangeslider/defaults.js +++ b/src/components/rangeslider/defaults.js @@ -27,7 +27,7 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName, counterAxe return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); } - coerce('bgcolor'); + coerce('bgcolor', layoutOut.plot_bgcolor); coerce('bordercolor'); coerce('borderwidth'); coerce('thickness'); diff --git a/test/jasmine/tests/range_slider_test.js b/test/jasmine/tests/range_slider_test.js index 0aae5c46df9..99f82f71d4d 100644 --- a/test/jasmine/tests/range_slider_test.js +++ b/test/jasmine/tests/range_slider_test.js @@ -513,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() {