From f5bccd5c07c1f58fe23827431dc631c810da5576 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 27 Sep 2016 23:56:55 -0400 Subject: [PATCH 01/40] First cut at slider --- src/components/slider/attributes.js | 203 ++++++++++++++++ src/components/slider/constants.js | 85 +++++++ src/components/slider/defaults.js | 103 ++++++++ src/components/slider/draw.js | 358 ++++++++++++++++++++++++++++ src/components/slider/index.js | 20 ++ src/components/updatemenus/draw.js | 7 +- src/core.js | 1 + src/plot_api/plot_api.js | 2 + src/plotly.js | 1 + src/plots/layout_attributes.js | 3 +- 10 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 src/components/slider/attributes.js create mode 100644 src/components/slider/constants.js create mode 100644 src/components/slider/defaults.js create mode 100644 src/components/slider/draw.js create mode 100644 src/components/slider/index.js diff --git a/src/components/slider/attributes.js b/src/components/slider/attributes.js new file mode 100644 index 00000000000..d7b2185f378 --- /dev/null +++ b/src/components/slider/attributes.js @@ -0,0 +1,203 @@ +/** +* 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 fontAttrs = require('../../plots/font_attributes'); +var colorAttrs = require('../color/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +var stepsAttrs = { + _isLinkedToArray: true, + + method: { + valType: 'enumerated', + values: ['restyle', 'relayout', 'animate', 'update'], + dflt: 'restyle', + role: 'info', + description: [ + 'Sets the Plotly method to be called on click.' + ].join(' ') + }, + args: { + valType: 'info_array', + role: 'info', + freeLength: true, + items: [ + { valType: 'any' }, + { valType: 'any' }, + { valType: 'any' } + ], + description: [ + 'Sets the arguments values to be passed to the Plotly', + 'method set in `method` on click.' + ].join(' ') + }, + label: { + valType: 'string', + role: 'info', + dflt: '', + description: 'Sets the text label to appear on the slider' + } +}; + +module.exports = { + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines whether or not the slider is visible.' + ].join(' ') + }, + + active: { + valType: 'string', + role: 'info', + min: -1, + dflt: 0, + description: [ + 'Determines which button (by index starting from 0) is', + 'considered active.' + ].join(' ') + }, + + steps: stepsAttrs, + + updateevent: { + valType: 'string', + arrayOk: true, + role: 'info', + description: [ + 'The name of the event to which this component subscribes', + 'in order to trigger updates. When the event is received', + 'the component will attempt to update the slider position', + 'to reflect the value passed as the data property of the', + 'event. The corresponding step\'s API method is assumed to', + 'have been triggered externally and so is not triggered again', + 'when the event is received. If an array is provided, multiple', + 'events will be subscribed to for updates.' + ].join(' ') + }, + + updatevalue: { + valType: 'string', + arrayOk: true, + role: 'info', + description: [ + 'The property of the event data that is matched to a slider', + 'value when an event of type `updateevent` is received. If', + 'undefined, the data argument itself is used. If a string,', + 'that property is used, and if a string with dots, e.g.', + '`item.0.label`, then `data[\'item\'][0][\'label\']` will', + 'be used. If an array, it is matched to the respective', + 'updateevent item or if there is no corresponding updatevalue', + 'for a particular updateevent, it is interpreted as `undefined` and defaults to the data property itself.' + ].join(' ') + }, + + lenmode: { + valType: 'enumerated', + values: ['fraction', 'pixels'], + role: 'info', + dflt: 'fraction', + description: [ + 'Determines whether this color bar\'s length', + '(i.e. the measure in the color variation direction)', + 'is set in units of plot *fraction* or in *pixels.', + 'Use `len` to set the value.' + ].join(' ') + }, + len: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: [ + 'Sets the length of the color bar', + 'This measure excludes the padding of both ends.', + 'That is, the color bar length is this length minus the', + 'padding on both ends.' + ].join(' ') + }, + x: { + valType: 'number', + min: -2, + max: 3, + dflt: -0.05, + role: 'style', + description: 'Sets the x position (in normalized coordinates) of the slider.' + }, + xpad: { + valType: 'number', + min: 0, + dflt: 10, + role: 'style', + description: 'Sets the amount of padding (in px) along the x direction' + }, + ypad: { + valType: 'number', + min: 0, + dflt: 10, + role: 'style', + description: 'Sets the amount of padding (in px) along the x direction' + }, + xanchor: { + valType: 'enumerated', + values: ['auto', 'left', 'center', 'right'], + dflt: 'left', + role: 'info', + description: [ + 'Sets the slider\'s horizontal position anchor.', + 'This anchor binds the `x` position to the *left*, *center*', + 'or *right* of the range selector.' + ].join(' ') + }, + y: { + valType: 'number', + min: -2, + max: 3, + dflt: 1, + role: 'style', + description: 'Sets the y position (in normalized coordinates) of the slider.' + }, + yanchor: { + valType: 'enumerated', + values: ['auto', 'top', 'middle', 'bottom'], + dflt: 'bottom', + role: 'info', + description: [ + 'Sets the slider\'s vertical position anchor', + 'This anchor binds the `y` position to the *top*, *middle*', + 'or *bottom* of the range selector.' + ].join(' ') + }, + + font: extendFlat({}, fontAttrs, { + description: 'Sets the font of the slider button text.' + }), + + bgcolor: { + valType: 'color', + role: 'style', + description: 'Sets the background color of the slider buttons.' + }, + bordercolor: { + valType: 'color', + dflt: colorAttrs.borderLine, + role: 'style', + description: 'Sets the color of the border enclosing the slider.' + }, + borderwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the border enclosing the slider.' + } +}; diff --git a/src/components/slider/constants.js b/src/components/slider/constants.js new file mode 100644 index 00000000000..92860f5d11c --- /dev/null +++ b/src/components/slider/constants.js @@ -0,0 +1,85 @@ +/** +* 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 = { + + // layout attribute names + name: 'sliders', + itemName: 'slider', + + // class names + containerClassName: 'slider-container', + groupClassName: 'slider-group', + inputAreaClass: 'slider-input-area', + railRectClass: 'slider-rail-rect', + railTouchRectClass: 'slider-rail-touch-rect', + gripRectClass: 'slider-grip-rect', + tickRectClass: 'slider-tick-rect', + inputProxyClass: 'slider-input-proxy', + + railHeight: 5, + + // DOM attribute name in button group keeping track + // of active update menu + menuIndexAttrName: 'slider-active-index', + + // id root pass to Plots.autoMargin + autoMarginIdRoot: 'slider-', + + // min item width / height + minWidth: 30, + minHeight: 30, + + // padding around item text + textPadX: 40, + + // font size to height scale + fontSizeToHeight: 1.3, + + // item rect radii + rx: 2, + ry: 2, + + // item text x offset off left edge + textOffsetX: 12, + + // item text y offset (w.r.t. middle) + textOffsetY: 3, + + // arrow offset off right edge + arrowOffsetX: 4, + + railRadius: 2, + railWidth: 5, + railBorder: 4, + railBorderColor: '#bec8d9', + railBgColor: '#ebedf0', + + + gripRadius: 10, + gripWidth: 20, + gripHeight: 20, + gripBorder: 20, + gripBorderWidth: 1, + gripBorderColor: '#bec8d9', + gripBgColor: '#ebedf0', + gripBgActiveColor: '#dbdde0', + + // Padding in the direction perpendicular to the length of the rail: + // (which, at the moment is always vertical, but for the sake of the future...) + widthPadding: 10, + + tickWidth: 1, + tickColor: '#333', + tickOffset: 25, + tickLength: 7, +}; diff --git a/src/components/slider/defaults.js b/src/components/slider/defaults.js new file mode 100644 index 00000000000..b7bb78e9204 --- /dev/null +++ b/src/components/slider/defaults.js @@ -0,0 +1,103 @@ +/** +* 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 Lib = require('../../lib'); + +var attributes = require('./attributes'); +var contants = require('./constants'); + +var name = contants.name; +var stepAttrs = attributes.steps; + + +module.exports = function slidersDefaults(layoutIn, layoutOut) { + var contIn = Array.isArray(layoutIn[name]) ? layoutIn[name] : [], + contOut = layoutOut[name] = []; + + for(var i = 0; i < contIn.length; i++) { + var sliderIn = contIn[i] || {}, + sliderOut = {}; + + sliderDefaults(sliderIn, sliderOut, layoutOut); + + // used on button click to update the 'active' field + sliderOut._input = sliderIn; + + // used to determine object constancy + sliderOut._index = i; + + contOut.push(sliderOut); + } +}; + +function sliderDefaults(sliderIn, sliderOut, layoutOut) { + + function coerce(attr, dflt) { + return Lib.coerce(sliderIn, sliderOut, attributes, attr, dflt); + } + + var steps = stepsDefaults(sliderIn, sliderOut); + + var visible = coerce('visible', steps.length > 0); + if(!visible) return; + + coerce('active'); + + coerce('x'); + coerce('y'); + Lib.noneOrAll(sliderIn, sliderOut, ['x', 'y']); + + coerce('xanchor'); + coerce('yanchor'); + + coerce('len'); + coerce('lenmode'); + + coerce('xpad'); + coerce('ypad'); + + coerce('updateevent'); + coerce('updatevalue'); + + Lib.coerceFont(coerce, 'font', layoutOut.font); + + coerce('bgcolor', layoutOut.paper_bgcolor); + coerce('bordercolor'); + coerce('borderwidth'); +} + +function stepsDefaults(sliderIn, sliderOut) { + var valuesIn = sliderIn.steps || [], + valuesOut = sliderOut.steps = []; + + var valueIn, valueOut; + + function coerce(attr, dflt) { + return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt); + } + + for(var i = 0; i < valuesIn.length; i++) { + valueIn = valuesIn[i]; + valueOut = {}; + + if(!Lib.isPlainObject(valueIn) || !Array.isArray(valueIn.args)) { + continue; + } + + coerce('method'); + coerce('args'); + coerce('label'); + + valueOut._index = i; + valuesOut.push(valueOut); + } + + return valuesOut; +} diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js new file mode 100644 index 00000000000..7f4a25b04f1 --- /dev/null +++ b/src/components/slider/draw.js @@ -0,0 +1,358 @@ +/** +* 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 Lib = require('../../lib'); +var Color = require('../color'); +var Drawing = require('../drawing'); +var svgTextUtils = require('../../lib/svg_text_utils'); +var anchorUtils = require('../legend/anchor_utils'); + +var constants = require('./constants'); + + +module.exports = function draw(gd) { + var fullLayout = gd._fullLayout, + sliderData = makeSliderData(fullLayout); + + // draw a container for *all* sliders: + var sliders = fullLayout._infolayer + .selectAll('g.' + constants.containerClassName) + .data(sliderData.length > 0 ? [0] : []); + + sliders.enter().append('g') + .classed(constants.containerClassName, true) + .style('cursor', 'pointer'); + + sliders.exit().remove(); + + // If no more sliders, clear the margisn: + if(sliders.exit().size()) clearPushMargins(gd); + + // Return early if no menus visible: + if(sliderData.length === 0) return; + + var sliderGroups = sliders.selectAll('g.'+ constants.groupClassName) + .data(sliderData, keyFunction); + + sliderGroups.enter().append('g') + .classed(constants.groupClassName, true); + + sliderGroups.exit().each(function(sliderOpts) { + d3.select(this).remove(); + + Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index); + }); + + // Find the dimensions of the sliders: + for(var i = 0; i < sliderData.length; i++) { + var sliderOpts = sliderData[i]; + findDimensions(gd, sliderOpts); + } + + sliderGroups.each(function(sliderOpts) { + computeDisplayedSteps(sliderOpts); + + drawSlider(gd, d3.select(this), sliderOpts); + + makeInputProxy(gd, d3.select(this), sliderOpts); + + }); +}; + +function makeInputProxy(gd, sliderGroup, sliderOpts) { + sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass) + .data([0]); +} + + + +// This really only just filters by visibility: +function makeSliderData(fullLayout) { + var contOpts = fullLayout[constants.name], + sliderData = []; + + for(var i = 0; i < contOpts.length; i++) { + var item = contOpts[i]; + if(item.visible) sliderData.push(item); + } + + return sliderData; +} + +// This is set in the defaults step: +function keyFunction(opts) { + return opts._index; +} + +// Compute the dimensions (mutates sliderOpts): +function findDimensions(gd, sliderOpts) { + sliderOpts._gd = gd; + + sliderOpts.inputAreaWidth = Math.max( + constants.railWidth, + constants.gripHeight + ); + + var graphSize = gd._fullLayout._size; + sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; + sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); + + if (sliderOpts.lenmode === 'fraction') { + // fraction: + sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len); + } else { + // pixels: + sliderOpts.outerLength = sliderOpts.len; + } + + // Set the length-wise padding so that the grip ends up *on* the end of + // the bar when at either extreme + sliderOpts.lenPad = Math.round(constants.gripWidth * 0.5); + + // The length of the rail, *excluding* padding on either end: + sliderOpts.inputAreaStart = 0; + sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.xpad * 2); + sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); + sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); + + // Hard-code this for now: + sliderOpts.height = 150; + + var xanchor = 'left'; + if(anchorUtils.isRightAnchor(sliderOpts)) { + sliderOpts.lx -= sliderOpts.outerLength; + xanchor = 'right'; + } + if(anchorUtils.isCenterAnchor(sliderOpts)) { + sliderOpts.lx -= sliderOpts.outerLength / 2; + xanchor = 'center'; + } + + var yanchor = 'top'; + if(anchorUtils.isBottomAnchor(sliderOpts)) { + sliderOpts.ly -= sliderOpts.height; + yanchor = 'bottom'; + } + if(anchorUtils.isMiddleAnchor(sliderOpts)) { + sliderOpts.ly -= sliderOpts.height / 2; + yanchor = 'middle'; + } + + sliderOpts.outerLength = Math.ceil(sliderOpts.outerLength); + sliderOpts.height = Math.ceil(sliderOpts.height); + sliderOpts.lx = Math.round(sliderOpts.lx); + sliderOpts.ly = Math.round(sliderOpts.ly); + + Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index, { + x: sliderOpts.x, + y: sliderOpts.y, + l: sliderOpts.outerLength * ({right: 1, center: 0.5}[xanchor] || 0), + r: sliderOpts.outerLength * ({left: 1, center: 0.5}[xanchor] || 0), + b: sliderOpts.height * ({top: 1, middle: 0.5}[yanchor] || 0), + t: sliderOpts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + }); +} + +function drawSlider(gd, group, sliderOpts) { + // These are carefully ordered for proper z-ordering: + group + .call(drawRail, sliderOpts) + .call(drawTouchRect, sliderOpts) + .call(drawTicks, sliderOpts) + .call(drawGrip, sliderOpts) + + group.call(setGripPosition, sliderOpts, 0); + group.call(attachFocusEvents, sliderOpts); + + // Position the rectangle: + Lib.setTranslate(group, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); +} + +function drawGrip(sliderGroup, sliderOpts) { + var grip = sliderGroup.selectAll('rect.' + constants.gripRectClass) + .data([0]); + + grip.enter().append('rect') + .classed(constants.gripRectClass, true) + .call(attachGripEvents, sliderGroup, sliderOpts) + .style('pointer-events', 'all'); + + grip.attr({ + width: constants.gripHeight, + height: constants.gripWidth, + rx: constants.gripRadius, + ry: constants.gripRadius, + }) + .call(Color.stroke, constants.gripBorderColor) + .call(Color.fill, constants.gripBgColor) + .style('stroke-width', constants.gripBorderWidth + 'px'); +} + +function handleInput(sliderGroup, sliderOpts, normalizedPosition) { + var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); + + if (quantizedPosition !== sliderOpts._active) { + setActive(sliderGroup, sliderOpts, quantizedPosition); + } +} + +function setActive(sliderGroup, sliderOpts, active) { + sliderOpts._active = active; + + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts._active / (sliderOpts.steps.length - 1)); + + var step = sliderOpts.steps[sliderOpts._active]; + + if (step && step.method) { + var args = step.args; + Plotly[step.method](gd, args[0], args[1], args[2]).catch(function(msg) { + // This is not a disaster. Some methods like `animate` reject if interrupted + // and *should* nicely log a warning. + Lib.warn('Warning: Plotly.' + step.method + ' was called and rejected.'); + }); + } +} + +function attachFocusEvents(sliderGroup, sliderOpts) { + sliderGroup.on('focus', function() { + }).on('blur', function() { + }); +} + +function attachGripEvents(item, sliderGroup, sliderOpts) { + var gd = d3.select(sliderOpts._gd); + var node = sliderGroup.node(); + + item.on('mousedown', function(event) { + var grip = sliderGroup.select('.' + constants.gripRectClass); + + d3.event.stopPropagation(); + d3.event.preventDefault(); + grip.call(Color.fill, constants.gripBgActiveColor) + + var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); + handleInput(sliderGroup, sliderOpts, normalizedPosition); + + gd.on('mousemove', function() { + var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); + handleInput(sliderGroup, sliderOpts, normalizedPosition); + }); + + gd.on('mouseup', function() { + grip.call(Color.fill, constants.gripBgColor) + gd.on('mouseup', null); + gd.on('mousemove', null); + }); + }); +} + +function drawTicks(sliderGroup, sliderOpts) { + var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass) + .data(sliderOpts.displayedSteps); + + tick.enter().append('rect') + .classed(constants.tickRectClass, true) + + tick.attr({ + width: constants.tickWidth, + height: constants.tickLength, + 'shape-rendering': 'crispEdges' + }) + .call(Color.fill, constants.tickColor); + + tick.each(function (d, i) { + Lib.setTranslate( + d3.select(this), + normalizedValueToPosition(sliderOpts, d.fraction) - 0.5 * constants.tickWidth, + constants.tickOffset + ); + }); + +} + +function computeDisplayedSteps(sliderOpts) { + sliderOpts.displayedSteps = []; + var i0 = 0; + var step = 1; + var nsteps = sliderOpts.steps.length; + + for (var i = i0; i < nsteps; i += step) { + sliderOpts.displayedSteps.push({ + fraction: i / (nsteps - 1), + step: sliderOpts.steps[i] + }); + } +} + +function setGripPosition(sliderGroup, sliderOpts, position) { + var grip = sliderGroup.select('rect.' + constants.gripRectClass); + + var x = normalizedValueToPosition(sliderOpts, position); + Lib.setTranslate(grip, x - constants.gripWidth * 0.5, 0); +} + +// Convert a number from [0-1] to a pixel position relative to the slider group container: +function normalizedValueToPosition(sliderOpts, normalizedPosition) { + return sliderOpts.inputAreaStart + sliderOpts.stepInset + + (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); +} + +// Convert a position relative to the slider group to a nubmer in [0, 1] +function positionToNormalizedValue(sliderOpts, position) { + return Math.min(1, Math.max(0, (position - sliderOpts.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset - 2 * sliderOpts.inputAreaStart))); +} + +function drawTouchRect(sliderGroup, sliderOpts) { + var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass) + .data([0]); + + rect.enter().append('rect') + .classed(constants.railTouchRectClass, true) + .call(attachGripEvents, sliderGroup, sliderOpts) + .style('pointer-events', 'all'); + + rect.attr({ + width: sliderOpts.inputAreaLength, + height: sliderOpts.inputAreaWidth + }) + .call(Color.fill, constants.gripBgColor) + .attr('opacity', 0) + + Lib.setTranslate(rect, 0, 0); +} + +function drawRail(sliderGroup, sliderOpts) { + var rect = sliderGroup.selectAll('rect.' + constants.railRectClass) + .data([0]); + + rect.enter().append('rect') + .classed(constants.railRectClass, true) + + var computedLength = sliderOpts.inputAreaLength - sliderOpts.railInset * 2; + + rect.attr({ + width: computedLength, + height: constants.railWidth, + rx: constants.railRadius, + ry: constants.railRadius, + 'shape-rendering': 'crispEdges' + }) + .call(Color.stroke, constants.railBorderColor) + .call(Color.fill, constants.railBgColor) + .style('stroke-width', '1px'); + + Lib.setTranslate(rect, sliderOpts.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); +} + diff --git a/src/components/slider/index.js b/src/components/slider/index.js new file mode 100644 index 00000000000..389368c4908 --- /dev/null +++ b/src/components/slider/index.js @@ -0,0 +1,20 @@ +/** +* 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'; + + +exports.moduleType = 'component'; + +exports.name = 'slider'; + +exports.layoutAttributes = require('./attributes'); + +exports.supplyLayoutDefaults = require('./defaults'); + +exports.draw = require('./draw'); diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 8acf51820ca..58473579bbc 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -108,7 +108,7 @@ module.exports = function draw(gd) { // find dimensions before plotting anything (this mutates menuOpts) for(var i = 0; i < menuData.length; i++) { var menuOpts = menuData[i]; - findDimenstions(gd, menuOpts); + findDimensions(gd, menuOpts); } // draw headers! @@ -221,6 +221,9 @@ function drawHeader(gd, gHeader, gButton, menuOpts) { } function drawButtons(gd, gHeader, gButton, menuOpts) { + // If this is a set of buttons, set pointer events = all since we play + // some minor games with which container is which in order to simplify + // the drawing of *either* buttons or menus if(!gButton) { gButton = gHeader; gButton.attr('pointer-events', 'all'); @@ -383,7 +386,7 @@ function styleOnMouseOut(item, menuOpts) { } // find item dimensions (this mutates menuOpts) -function findDimenstions(gd, menuOpts) { +function findDimensions(gd, menuOpts) { menuOpts.width1 = 0; menuOpts.height1 = 0; menuOpts.heights = []; diff --git a/src/core.js b/src/core.js index 00a266c962e..96931ac487b 100644 --- a/src/core.js +++ b/src/core.js @@ -58,6 +58,7 @@ exports.register([ require('./components/shapes'), require('./components/images'), require('./components/updatemenus'), + require('./components/slider'), require('./components/rangeslider'), require('./components/rangeselector') ]); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 72b40252361..51867a84881 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -178,6 +178,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('legend', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); + Registry.getComponentMethod('slider', 'draw')(gd); for(i = 0; i < calcdata.length; i++) { cd = calcdata[i]; @@ -303,6 +304,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('rangeslider', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); + Registry.getComponentMethod('slider', 'draw')(gd); } function cleanUp() { diff --git a/src/plotly.js b/src/plotly.js index 3f8cba139c0..02943617655 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -37,6 +37,7 @@ exports.Shapes = require('./components/shapes'); exports.Legend = require('./components/legend'); exports.Images = require('./components/images'); exports.UpdateMenus = require('./components/updatemenus'); +exports.Slider = require('./components/slider'); exports.ModeBar = require('./components/modebar'); // plot api diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 3aaba1ed04d..f0f9323adfd 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -183,6 +183,7 @@ module.exports = { 'annotations': 'annotations', 'shapes': 'shapes', 'images': 'images', - 'updatemenus': 'updatemenus' + 'updatemenus': 'updatemenus', + 'slider': 'slider' } }; From 83483fd052f375871b8cd6a4b7076684bda2b42e Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 02:48:23 -0400 Subject: [PATCH 02/40] Fix lint errors in slider --- src/components/slider/attributes.js | 4 +- src/components/slider/constants.js | 7 +- src/components/slider/defaults.js | 8 + src/components/slider/draw.js | 296 ++++++++++++++++++++-------- 4 files changed, 233 insertions(+), 82 deletions(-) diff --git a/src/components/slider/attributes.js b/src/components/slider/attributes.js index d7b2185f378..2f937f5afe5 100644 --- a/src/components/slider/attributes.js +++ b/src/components/slider/attributes.js @@ -57,9 +57,9 @@ module.exports = { }, active: { - valType: 'string', + valType: 'number', role: 'info', - min: -1, + min: -10, dflt: 0, description: [ 'Determines which button (by index starting from 0) is', diff --git a/src/components/slider/constants.js b/src/components/slider/constants.js index 92860f5d11c..6593b502b7e 100644 --- a/src/components/slider/constants.js +++ b/src/components/slider/constants.js @@ -25,6 +25,9 @@ module.exports = { gripRectClass: 'slider-grip-rect', tickRectClass: 'slider-tick-rect', inputProxyClass: 'slider-input-proxy', + labelsClass: 'slider-labels', + labelGroupClass: 'slider-label-group', + labelClass: 'slider-label', railHeight: 5, @@ -64,7 +67,6 @@ module.exports = { railBorderColor: '#bec8d9', railBgColor: '#ebedf0', - gripRadius: 10, gripWidth: 20, gripHeight: 20, @@ -78,8 +80,11 @@ module.exports = { // (which, at the moment is always vertical, but for the sake of the future...) widthPadding: 10, + labelPadding: 4, tickWidth: 1, tickColor: '#333', tickOffset: 25, tickLength: 7, + minorTickColor: '#333', + minorTickLength: 4, }; diff --git a/src/components/slider/defaults.js b/src/components/slider/defaults.js index b7bb78e9204..fd5503b3b1a 100644 --- a/src/components/slider/defaults.js +++ b/src/components/slider/defaults.js @@ -66,6 +66,14 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('updateevent'); coerce('updatevalue'); + if(!Array.isArray(sliderOut.updateevent)) { + sliderOut.updateevent = [sliderOut.updateevent]; + } + + if(!Array.isArray(sliderOut.udpatevalue)) { + sliderOut.udpatevalue = [sliderOut.updatevalue]; + } + Lib.coerceFont(coerce, 'font', layoutOut.font); coerce('bgcolor', layoutOut.paper_bgcolor); diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 7f4a25b04f1..417c63dedce 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -33,7 +33,7 @@ module.exports = function draw(gd) { sliders.enter().append('g') .classed(constants.containerClassName, true) - .style('cursor', 'pointer'); + .style('cursor', 'ew-resize'); sliders.exit().remove(); @@ -43,7 +43,7 @@ module.exports = function draw(gd) { // Return early if no menus visible: if(sliderData.length === 0) return; - var sliderGroups = sliders.selectAll('g.'+ constants.groupClassName) + var sliderGroups = sliders.selectAll('g.' + constants.groupClassName) .data(sliderData, keyFunction); sliderGroups.enter().append('g') @@ -62,7 +62,7 @@ module.exports = function draw(gd) { } sliderGroups.each(function(sliderOpts) { - computeDisplayedSteps(sliderOpts); + computeLabelSteps(sliderOpts); drawSlider(gd, d3.select(this), sliderOpts); @@ -76,8 +76,6 @@ function makeInputProxy(gd, sliderGroup, sliderOpts) { .data([0]); } - - // This really only just filters by visibility: function makeSliderData(fullLayout) { var contOpts = fullLayout[constants.name], @@ -98,7 +96,31 @@ function keyFunction(opts) { // Compute the dimensions (mutates sliderOpts): function findDimensions(gd, sliderOpts) { - sliderOpts._gd = gd; + var sliderLabels = gd._tester.selectAll('g.' + constants.labelGroupClass) + .data(sliderOpts.steps); + + sliderLabels.enter().append('g') + .classed(constants.labelGroupClass, true); + + // loop over fake buttons to find width / height + var maxLabelWidth = 0; + var labelHeight = 0; + sliderLabels.each(function(stepOpts) { + var labelGroup = d3.select(this); + + var text = drawLabel(labelGroup, {step: stepOpts}, sliderOpts); + + var tWidth = text.node() && Drawing.bBox(text.node()).width; + + // This just overwrites with the last. Which is fine as long as + // the bounding box (probably incorrectly) measures the text *on + // a single line*: + labelHeight = text.node() && Drawing.bBox(text.node()).height; + + maxLabelWidth = Math.max(maxLabelWidth, tWidth); + }); + + sliderLabels.remove(); sliderOpts.inputAreaWidth = Math.max( constants.railWidth, @@ -109,7 +131,7 @@ function findDimensions(gd, sliderOpts) { sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); - if (sliderOpts.lenmode === 'fraction') { + if(sliderOpts.lenmode === 'fraction') { // fraction: sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len); } else { @@ -127,8 +149,14 @@ function findDimensions(gd, sliderOpts) { sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); + var textableInputLength = sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset; + var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1); + var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; + sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); + sliderOpts.labelHeight = labelHeight; + // Hard-code this for now: - sliderOpts.height = 150; + sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.ypad * 2; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -165,59 +193,158 @@ function findDimensions(gd, sliderOpts) { }); } -function drawSlider(gd, group, sliderOpts) { +function drawSlider(gd, sliderGroup, sliderOpts) { // These are carefully ordered for proper z-ordering: - group + sliderGroup .call(drawRail, sliderOpts) - .call(drawTouchRect, sliderOpts) + .call(drawLabelGroup, sliderOpts) .call(drawTicks, sliderOpts) - .call(drawGrip, sliderOpts) - - group.call(setGripPosition, sliderOpts, 0); - group.call(attachFocusEvents, sliderOpts); + .call(drawTouchRect, gd, sliderOpts) + .call(drawGrip, gd, sliderOpts); // Position the rectangle: - Lib.setTranslate(group, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + + removeListeners(gd, sliderGroup, sliderOpts); + attachListeners(gd, sliderGroup, sliderOpts); + + setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true); +} + +function removeListeners(gd, sliderGroup, sliderOpts) { + var listeners = sliderOpts._input.listeners; + var eventNames = sliderOpts._input.eventNames; + if(!Array.isArray(listeners) || !Array.isArray(eventNames)) return; + while(listeners.length) { + gd._removeInternalListener(eventNames.pop(), listeners.pop()); + } +} + +function attachListeners(gd, sliderGroup, sliderOpts) { + var listeners = sliderOpts._input.listeners = []; + var eventNames = sliderOpts._input.eventNames = []; + + function makeListener(updatevalue) { + return function(data) { + var value = data; + if(updatevalue) { + value = Lib.nestedProperty(data, updatevalue).get(); + } + + setActiveByLabel(gd, sliderGroup, sliderOpts, value, false); + }; + } + + for(var i = 0; i < sliderOpts.updateevent.length; i++) { + var updateEventName = sliderOpts.updateevent[i]; + var updatevalue = sliderOpts.updatevalue; + + var updatelistener = makeListener(updatevalue); + + gd._internalEv.on(updateEventName, updatelistener); + + eventNames.push(updateEventName); + listeners.push(updatelistener); + } } -function drawGrip(sliderGroup, sliderOpts) { +function drawGrip(sliderGroup, gd, sliderOpts) { var grip = sliderGroup.selectAll('rect.' + constants.gripRectClass) .data([0]); grip.enter().append('rect') .classed(constants.gripRectClass, true) - .call(attachGripEvents, sliderGroup, sliderOpts) + .call(attachGripEvents, gd, sliderGroup, sliderOpts) .style('pointer-events', 'all'); grip.attr({ - width: constants.gripHeight, - height: constants.gripWidth, - rx: constants.gripRadius, - ry: constants.gripRadius, - }) + width: constants.gripHeight, + height: constants.gripWidth, + rx: constants.gripRadius, + ry: constants.gripRadius, + }) .call(Color.stroke, constants.gripBorderColor) .call(Color.fill, constants.gripBgColor) .style('stroke-width', constants.gripBorderWidth + 'px'); } -function handleInput(sliderGroup, sliderOpts, normalizedPosition) { +function drawLabel(item, data, sliderOpts) { + var text = item.selectAll('text') + .data([0]); + + text.enter().append('text') + .classed(constants.labelClass, true) + .classed('user-select-none', true) + .attr('text-anchor', 'middle'); + + text.call(Drawing.font, sliderOpts.font) + .text(data.step.label) + .call(svgTextUtils.convertToTspans); + + return text; +} + +function drawLabelGroup(sliderGroup, sliderOpts) { + var labels = sliderGroup.selectAll('g.' + constants.labelsClass) + .data([0]); + + labels.enter().append('g') + .classed(constants.labelsClass, true); + + var labelItems = labels.selectAll('g.' + constants.labelGroupClass) + .data(sliderOpts.labelSteps); + + labelItems.enter().append('g') + .classed(constants.labelGroupClass, true); + + labelItems.exit().remove(); + + labelItems.each(function(d) { + var item = d3.select(this); + + item.call(drawLabel, d, sliderOpts); + + Lib.setTranslate(item, + normalizedValueToPosition(sliderOpts, d.fraction), + constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + ); + }); + +} + +function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition) { var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); - if (quantizedPosition !== sliderOpts._active) { - setActive(sliderGroup, sliderOpts, quantizedPosition); + if(quantizedPosition !== sliderOpts.active) { + setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true); } } -function setActive(sliderGroup, sliderOpts, active) { - sliderOpts._active = active; +function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback) { + var index; + for(var i = 0; i < sliderOpts.steps.length; i++) { + var step = sliderOpts.steps[i]; + if(step.label === label) { + index = i; + break; + } + } - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts._active / (sliderOpts.steps.length - 1)); + if(index !== undefined) { + setActive(gd, sliderGroup, sliderOpts, index, doCallback); + } +} + +function setActive(gd, sliderGroup, sliderOpts, index, doCallback) { + sliderOpts._input.active = sliderOpts.active = index; - var step = sliderOpts.steps[sliderOpts._active]; + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1)); - if (step && step.method) { + var step = sliderOpts.steps[sliderOpts.active]; + + if(step && step.method && doCallback) { var args = step.args; - Plotly[step.method](gd, args[0], args[1], args[2]).catch(function(msg) { + Plotly[step.method](gd, args[0], args[1], args[2]).catch(function() { // This is not a disaster. Some methods like `animate` reject if interrupted // and *should* nicely log a warning. Lib.warn('Warning: Plotly.' + step.method + ' was called and rejected.'); @@ -225,71 +352,70 @@ function setActive(sliderGroup, sliderOpts, active) { } } -function attachFocusEvents(sliderGroup, sliderOpts) { - sliderGroup.on('focus', function() { - }).on('blur', function() { - }); -} - -function attachGripEvents(item, sliderGroup, sliderOpts) { - var gd = d3.select(sliderOpts._gd); +function attachGripEvents(item, gd, sliderGroup, sliderOpts) { var node = sliderGroup.node(); + var $gd = d3.select(gd); - item.on('mousedown', function(event) { + item.on('mousedown', function() { var grip = sliderGroup.select('.' + constants.gripRectClass); d3.event.stopPropagation(); d3.event.preventDefault(); - grip.call(Color.fill, constants.gripBgActiveColor) + grip.call(Color.fill, constants.gripBgActiveColor); var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); - gd.on('mousemove', function() { + $gd.on('mousemove', function() { var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); }); - gd.on('mouseup', function() { - grip.call(Color.fill, constants.gripBgColor) - gd.on('mouseup', null); - gd.on('mousemove', null); + $gd.on('mouseup', function() { + grip.call(Color.fill, constants.gripBgColor); + $gd.on('mouseup', null); + $gd.on('mousemove', null); }); }); } function drawTicks(sliderGroup, sliderOpts) { var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass) - .data(sliderOpts.displayedSteps); + .data(sliderOpts.steps); tick.enter().append('rect') - .classed(constants.tickRectClass, true) + .classed(constants.tickRectClass, true); + + tick.exit().remove(); tick.attr({ - width: constants.tickWidth, - height: constants.tickLength, - 'shape-rendering': 'crispEdges' - }) - .call(Color.fill, constants.tickColor); - - tick.each(function (d, i) { - Lib.setTranslate( - d3.select(this), - normalizedValueToPosition(sliderOpts, d.fraction) - 0.5 * constants.tickWidth, + width: constants.tickWidth, + 'shape-rendering': 'crispEdges' + }); + + tick.each(function(d, i) { + var isMajor = i % sliderOpts.labelStride === 0; + var item = d3.select(this); + + item + .attr({height: isMajor ? constants.tickLength : constants.minorTickLength}) + .call(Color.fill, isMajor ? constants.tickColor : constants.minorTickColor); + + Lib.setTranslate(item, + normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * constants.tickWidth, constants.tickOffset ); }); } -function computeDisplayedSteps(sliderOpts) { - sliderOpts.displayedSteps = []; +function computeLabelSteps(sliderOpts) { + sliderOpts.labelSteps = []; var i0 = 0; - var step = 1; var nsteps = sliderOpts.steps.length; - for (var i = i0; i < nsteps; i += step) { - sliderOpts.displayedSteps.push({ + for(var i = i0; i < nsteps; i += sliderOpts.labelStride) { + sliderOpts.labelSteps.push({ fraction: i / (nsteps - 1), step: sliderOpts.steps[i] }); @@ -314,21 +440,21 @@ function positionToNormalizedValue(sliderOpts, position) { return Math.min(1, Math.max(0, (position - sliderOpts.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset - 2 * sliderOpts.inputAreaStart))); } -function drawTouchRect(sliderGroup, sliderOpts) { +function drawTouchRect(sliderGroup, gd, sliderOpts) { var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass) .data([0]); rect.enter().append('rect') .classed(constants.railTouchRectClass, true) - .call(attachGripEvents, sliderGroup, sliderOpts) + .call(attachGripEvents, gd, sliderGroup, sliderOpts) .style('pointer-events', 'all'); rect.attr({ - width: sliderOpts.inputAreaLength, - height: sliderOpts.inputAreaWidth - }) + width: sliderOpts.inputAreaLength, + height: sliderOpts.inputAreaWidth + }) .call(Color.fill, constants.gripBgColor) - .attr('opacity', 0) + .attr('opacity', 0); Lib.setTranslate(rect, 0, 0); } @@ -338,17 +464,17 @@ function drawRail(sliderGroup, sliderOpts) { .data([0]); rect.enter().append('rect') - .classed(constants.railRectClass, true) + .classed(constants.railRectClass, true); var computedLength = sliderOpts.inputAreaLength - sliderOpts.railInset * 2; rect.attr({ - width: computedLength, - height: constants.railWidth, - rx: constants.railRadius, - ry: constants.railRadius, - 'shape-rendering': 'crispEdges' - }) + width: computedLength, + height: constants.railWidth, + rx: constants.railRadius, + ry: constants.railRadius, + 'shape-rendering': 'crispEdges' + }) .call(Color.stroke, constants.railBorderColor) .call(Color.fill, constants.railBgColor) .style('stroke-width', '1px'); @@ -356,3 +482,15 @@ function drawRail(sliderGroup, sliderOpts) { Lib.setTranslate(rect, sliderOpts.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); } +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.autoMarginIdRoot) !== -1) { + Plots.autoMargin(gd, k); + } + } +} From f69a3280317e00d370c75d77933951d588aea2b6 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 03:35:55 -0400 Subject: [PATCH 03/40] Add slider transitions --- src/components/slider/attributes.js | 18 +++++++++++++ src/components/slider/defaults.js | 7 ++++++ src/components/slider/draw.js | 39 ++++++++++++++++++----------- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/components/slider/attributes.js b/src/components/slider/attributes.js index 2f937f5afe5..a26f2909f7c 100644 --- a/src/components/slider/attributes.js +++ b/src/components/slider/attributes.js @@ -11,6 +11,7 @@ var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; +var animationAttrs = require('../../plots/animation_attributes'); var stepsAttrs = { _isLinkedToArray: true, @@ -178,6 +179,23 @@ module.exports = { ].join(' ') }, + transition: { + duration: { + valType: 'number', + role: 'info', + min: 0, + dflt: 150, + description: 'Sets the duration of the slider transition' + }, + easing: { + valType: 'enumerated', + values: animationAttrs.transition.easing.values, + role: 'info', + dflt: 'cubic-in-out', + description: 'Sets the easing function of the slider transition' + }, + }, + font: extendFlat({}, fontAttrs, { description: 'Sets the font of the slider button text.' }), diff --git a/src/components/slider/defaults.js b/src/components/slider/defaults.js index fd5503b3b1a..776ac8039d9 100644 --- a/src/components/slider/defaults.js +++ b/src/components/slider/defaults.js @@ -66,6 +66,13 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('updateevent'); coerce('updatevalue'); + if(!sliderIn.transition) { + sliderIn.transition = {}; + } + + coerce('transition.duration'); + coerce('transition.easing'); + if(!Array.isArray(sliderOut.updateevent)) { sliderOut.updateevent = [sliderOut.updateevent]; } diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 417c63dedce..093d8c7d5ea 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -208,7 +208,7 @@ function drawSlider(gd, sliderGroup, sliderOpts) { removeListeners(gd, sliderGroup, sliderOpts); attachListeners(gd, sliderGroup, sliderOpts); - setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true); + setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true, false); } function removeListeners(gd, sliderGroup, sliderOpts) { @@ -224,14 +224,14 @@ function attachListeners(gd, sliderGroup, sliderOpts) { var listeners = sliderOpts._input.listeners = []; var eventNames = sliderOpts._input.eventNames = []; - function makeListener(updatevalue) { + function makeListener(eventname, updatevalue) { return function(data) { var value = data; if(updatevalue) { value = Lib.nestedProperty(data, updatevalue).get(); } - setActiveByLabel(gd, sliderGroup, sliderOpts, value, false); + setActiveByLabel(gd, sliderGroup, sliderOpts, value, false, true); }; } @@ -239,7 +239,7 @@ function attachListeners(gd, sliderGroup, sliderOpts) { var updateEventName = sliderOpts.updateevent[i]; var updatevalue = sliderOpts.updatevalue; - var updatelistener = makeListener(updatevalue); + var updatelistener = makeListener(updateEventName, updatevalue); gd._internalEv.on(updateEventName, updatelistener); @@ -312,15 +312,16 @@ function drawLabelGroup(sliderGroup, sliderOpts) { } -function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition) { +function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransition) { var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); + if(quantizedPosition !== sliderOpts.active) { - setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true); + setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true, doTransition); } } -function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback) { +function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback, doTransition) { var index; for(var i = 0; i < sliderOpts.steps.length; i++) { var step = sliderOpts.steps[i]; @@ -331,14 +332,14 @@ function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback) { } if(index !== undefined) { - setActive(gd, sliderGroup, sliderOpts, index, doCallback); + setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition); } } -function setActive(gd, sliderGroup, sliderOpts, index, doCallback) { +function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) { sliderOpts._input.active = sliderOpts.active = index; - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1)); + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); var step = sliderOpts.steps[sliderOpts.active]; @@ -364,11 +365,11 @@ function attachGripEvents(item, gd, sliderGroup, sliderOpts) { grip.call(Color.fill, constants.gripBgActiveColor); var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, true); $gd.on('mousemove', function() { var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, false); }); $gd.on('mouseup', function() { @@ -422,11 +423,21 @@ function computeLabelSteps(sliderOpts) { } } -function setGripPosition(sliderGroup, sliderOpts, position) { +function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { var grip = sliderGroup.select('rect.' + constants.gripRectClass); var x = normalizedValueToPosition(sliderOpts, position); - Lib.setTranslate(grip, x - constants.gripWidth * 0.5, 0); + + var el = grip; + if(doTransition && sliderOpts.transition.duration > 0) { + el = el.transition() + .duration(sliderOpts.transition.duration) + .ease(sliderOpts.transition.easing); + } + + // Lib.setTranslate doesn't work here becasue of the transition duck-typing. + // It's also not necessary because there are no other transitions to preserve. + el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + 0 + ')'); } // Convert a number from [0-1] to a pixel position relative to the slider group container: From 82ea5581ae7cd96b636f0d062cc98a6bd61e9b36 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 03:45:30 -0400 Subject: [PATCH 04/40] Expand touchable slider area --- src/components/slider/draw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 093d8c7d5ea..7fe34a070bf 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -462,10 +462,10 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { rect.attr({ width: sliderOpts.inputAreaLength, - height: sliderOpts.inputAreaWidth + height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + constants.tickLength + sliderOpts.labelHeight) }) .call(Color.fill, constants.gripBgColor) - .attr('opacity', 0); + .attr('opacity', 0.0); Lib.setTranslate(rect, 0, 0); } From ca5f9f556c96499c1a08859e4f2ff20ed0b26060 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 10:46:46 -0400 Subject: [PATCH 05/40] Add slider mock --- src/components/slider/draw.js | 4 +- test/image/mocks/slider.json | 109 ++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 test/image/mocks/slider.json diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 7fe34a070bf..e70da914794 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -258,8 +258,8 @@ function drawGrip(sliderGroup, gd, sliderOpts) { .style('pointer-events', 'all'); grip.attr({ - width: constants.gripHeight, - height: constants.gripWidth, + width: constants.gripWidth, + height: constants.gripHeight, rx: constants.gripRadius, ry: constants.gripRadius, }) diff --git a/test/image/mocks/slider.json b/test/image/mocks/slider.json new file mode 100644 index 00000000000..60d665ce38f --- /dev/null +++ b/test/image/mocks/slider.json @@ -0,0 +1,109 @@ +{ + "data": [ + { + "x": [0, 1, 2], + "y": [0.5, 1, 2.5] + } + ], + "layout": { + "sliders": [{ + "active": 2, + "steps": [{ + "label": "red", + "method": "restyle", + "args": [{"line.color": "red"}] + }, { + "label": "orange", + "method": "restyle", + "args": [{"line.color": "orange"}] + }, { + "label": "yellow", + "method": "restyle", + "args": [{"line.color": "yellow"}] + }, { + "label": "green", + "method": "restyle", + "args": [{"line.color": "green"}] + }, { + "label": "blue", + "method": "restyle", + "args": [{"line.color": "blue"}] + }, { + "label": "purple", + "method": "restyle", + "args": [{"line.color": "purple"}] + }], + "visible": true, + "x": 0.5, + "len": 0.5, + "xanchor": "right", + "y": -0.1, + "yanchor": "top", + + "transition": { + "duration": 150, + "easing": "cubic-in-out" + }, + + "xpad": 20, + "ypad": 30, + + "font": {} + }, { + "active": 4, + "steps": [{ + "label": "red", + "method": "restyle", + "args": [{"marker.color": "red"}] + }, { + "label": "orange", + "method": "restyle", + "args": [{"marker.color": "orange"}] + }, { + "label": "yellow", + "method": "restyle", + "args": [{"marker.color": "yellow"}] + }, { + "label": "green", + "method": "restyle", + "args": [{"marker.color": "green"}] + }, { + "label": "blue", + "method": "restyle", + "args": [{"marker.color": "blue"}] + }, { + "label": "purple", + "method": "restyle", + "args": [{"marker.color": "purple"}] + }], + "visible": true, + "x": 0.5, + "len": 0.5, + "xanchor": "left", + "y": -0.1, + "yanchor": "top", + + "transition": { + "duration": 150, + "easing": "cubic-in-out" + }, + + "xpad": 20, + "ypad": 30, + + "font": {} + }], + "xaxis": { + "range": [0, 2], + "autorange": true + }, + "yaxis": { + "type": "linear", + "range": [0, 3], + "autorange": true + }, + "height": 450, + "width": 1100, + "autosize": true + } +} From c6a3e11bf1662b23783c29379cdf88bbbc295a27 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 11:57:01 -0400 Subject: [PATCH 06/40] Rename sliders and bump circular require tolerance --- src/components/{slider => sliders}/attributes.js | 0 src/components/{slider => sliders}/constants.js | 0 src/components/{slider => sliders}/defaults.js | 0 src/components/{slider => sliders}/draw.js | 3 ++- src/components/{slider => sliders}/index.js | 2 +- src/core.js | 2 +- src/plot_api/plot_api.js | 13 +++++++++++-- src/plotly.js | 2 +- tasks/test_syntax.js | 2 +- 9 files changed, 17 insertions(+), 7 deletions(-) rename src/components/{slider => sliders}/attributes.js (100%) rename src/components/{slider => sliders}/constants.js (100%) rename src/components/{slider => sliders}/defaults.js (100%) rename src/components/{slider => sliders}/draw.js (99%) rename src/components/{slider => sliders}/index.js (93%) diff --git a/src/components/slider/attributes.js b/src/components/sliders/attributes.js similarity index 100% rename from src/components/slider/attributes.js rename to src/components/sliders/attributes.js diff --git a/src/components/slider/constants.js b/src/components/sliders/constants.js similarity index 100% rename from src/components/slider/constants.js rename to src/components/sliders/constants.js diff --git a/src/components/slider/defaults.js b/src/components/sliders/defaults.js similarity index 100% rename from src/components/slider/defaults.js rename to src/components/sliders/defaults.js diff --git a/src/components/slider/draw.js b/src/components/sliders/draw.js similarity index 99% rename from src/components/slider/draw.js rename to src/components/sliders/draw.js index e70da914794..63872689f99 100644 --- a/src/components/slider/draw.js +++ b/src/components/sliders/draw.js @@ -155,7 +155,6 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - // Hard-code this for now: sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.ypad * 2; var xanchor = 'left'; @@ -205,6 +204,8 @@ function drawSlider(gd, sliderGroup, sliderOpts) { // Position the rectangle: Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + // Every time the slider is draw from scratch, just detach and reattach the event listeners. + // This could perhaps be avoided. removeListeners(gd, sliderGroup, sliderOpts); attachListeners(gd, sliderGroup, sliderOpts); diff --git a/src/components/slider/index.js b/src/components/sliders/index.js similarity index 93% rename from src/components/slider/index.js rename to src/components/sliders/index.js index 389368c4908..28e755fd68f 100644 --- a/src/components/slider/index.js +++ b/src/components/sliders/index.js @@ -11,7 +11,7 @@ exports.moduleType = 'component'; -exports.name = 'slider'; +exports.name = 'sliders'; exports.layoutAttributes = require('./attributes'); diff --git a/src/core.js b/src/core.js index 96931ac487b..c7da95352ce 100644 --- a/src/core.js +++ b/src/core.js @@ -58,7 +58,7 @@ exports.register([ require('./components/shapes'), require('./components/images'), require('./components/updatemenus'), - require('./components/slider'), + require('./components/sliders'), require('./components/rangeslider'), require('./components/rangeselector') ]); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 51867a84881..9784f00d5da 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -178,7 +178,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('legend', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); - Registry.getComponentMethod('slider', 'draw')(gd); + Registry.getComponentMethod('sliders', 'draw')(gd); for(i = 0; i < calcdata.length; i++) { cd = calcdata[i]; @@ -304,7 +304,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('rangeslider', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); - Registry.getComponentMethod('slider', 'draw')(gd); + Registry.getComponentMethod('sliders', 'draw')(gd); } function cleanUp() { @@ -1946,6 +1946,15 @@ function _relayout(gd, aobj) { for(i = 0; i < diff; i++) menus.push({}); flags.doplot = true; } + else if(p.parts[0] === 'sliders') { + Lib.extendDeepAll(gd.layout, Lib.objectFromPath(ai, vi)); + + var sliders = gd._fullLayout.sliders || []; + diff = (p.parts[2] + 1) - sliders.length; + + for(i = 0; i < diff; i++) sliders.push({}); + flags.doplot = true; + } // alter gd.layout else { // check whether we can short-circuit a full redraw diff --git a/src/plotly.js b/src/plotly.js index 02943617655..899696c639c 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -37,7 +37,7 @@ exports.Shapes = require('./components/shapes'); exports.Legend = require('./components/legend'); exports.Images = require('./components/images'); exports.UpdateMenus = require('./components/updatemenus'); -exports.Slider = require('./components/slider'); +exports.Sliders = require('./components/sliders'); exports.ModeBar = require('./components/modebar'); // plot api diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index 76d9f631f30..b1ffa7494a9 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -105,7 +105,7 @@ function assertCircularDeps() { // as of v1.17.0 - 2016/09/08 // see https://github.com/plotly/plotly.js/milestone/9 // for more details - var MAX_ALLOWED_CIRCULAR_DEPS = 33; + var MAX_ALLOWED_CIRCULAR_DEPS = 34; if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) { logs.push('some new circular dependencies were added to src/'); From 5b74652486fee6ae5e12547e0ff9294166d53598 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 12:19:23 -0400 Subject: [PATCH 07/40] Add trbl padding to sliders --- src/components/sliders/attributes.js | 29 +++++++------------- src/components/sliders/defaults.js | 6 +++-- src/components/sliders/draw.js | 6 ++--- src/plots/pad_attributes.js | 40 ++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 src/plots/pad_attributes.js diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index a26f2909f7c..49d0f2b647b 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -9,6 +9,7 @@ 'use strict'; var fontAttrs = require('../../plots/font_attributes'); +var padAttrs = require('../../plots/pad_attributes'); var colorAttrs = require('../color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var animationAttrs = require('../../plots/animation_attributes'); @@ -22,7 +23,7 @@ var stepsAttrs = { dflt: 'restyle', role: 'info', description: [ - 'Sets the Plotly method to be called on click.' + 'Sets the Plotly method to be called when the slider value is changed.' ].join(' ') }, args: { @@ -95,10 +96,11 @@ module.exports = { 'value when an event of type `updateevent` is received. If', 'undefined, the data argument itself is used. If a string,', 'that property is used, and if a string with dots, e.g.', - '`item.0.label`, then `data[\'item\'][0][\'label\']` will', - 'be used. If an array, it is matched to the respective', - 'updateevent item or if there is no corresponding updatevalue', - 'for a particular updateevent, it is interpreted as `undefined` and defaults to the data property itself.' + '`item.0.label`, then `data[0].label` is used. If an array,', + 'it is matched to the respective updateevent item or if there', + 'is no corresponding updatevalue for a particular updateevent,', + 'it is interpreted as `undefined` and defaults to the data', + 'property itself.' ].join(' ') }, @@ -134,20 +136,9 @@ module.exports = { role: 'style', description: 'Sets the x position (in normalized coordinates) of the slider.' }, - xpad: { - valType: 'number', - min: 0, - dflt: 10, - role: 'style', - description: 'Sets the amount of padding (in px) along the x direction' - }, - ypad: { - valType: 'number', - min: 0, - dflt: 10, - role: 'style', - description: 'Sets the amount of padding (in px) along the x direction' - }, + pad: extendFlat({}, padAttrs, { + description: 'Set the padding of the slider component along each side.' + }), xanchor: { valType: 'enumerated', values: ['auto', 'left', 'center', 'right'], diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 776ac8039d9..58a0f25310a 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -60,8 +60,10 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('len'); coerce('lenmode'); - coerce('xpad'); - coerce('ypad'); + coerce('pad.t'); + coerce('pad.r'); + coerce('pad.b'); + coerce('pad.l'); coerce('updateevent'); coerce('updatevalue'); diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 63872689f99..d826a6d4c2e 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -145,7 +145,7 @@ function findDimensions(gd, sliderOpts) { // The length of the rail, *excluding* padding on either end: sliderOpts.inputAreaStart = 0; - sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.xpad * 2); + sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); @@ -155,7 +155,7 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.ypad * 2; + sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -202,7 +202,7 @@ function drawSlider(gd, sliderGroup, sliderOpts) { .call(drawGrip, gd, sliderOpts); // Position the rectangle: - Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.pad.l, sliderOpts.ly + sliderOpts.pad.t); // Every time the slider is draw from scratch, just detach and reattach the event listeners. // This could perhaps be avoided. diff --git a/src/plots/pad_attributes.js b/src/plots/pad_attributes.js new file mode 100644 index 00000000000..7b274489928 --- /dev/null +++ b/src/plots/pad_attributes.js @@ -0,0 +1,40 @@ +/** +* 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 = { + t: { + valType: 'number', + min: 0, + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) along the top of the component.' + }, + r: { + valType: 'number', + min: 0, + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) on the right side of the component.' + }, + b: { + valType: 'number', + min: 0, + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) along the bottom of the component.' + }, + l: { + valType: 'number', + min: 0, + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) on the left side of the component.' + } +}; From e13e0eea87b744d2176b3a0af47e6634bf8f2a59 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 12:37:09 -0400 Subject: [PATCH 08/40] Remove fanciness from slider constants --- src/components/sliders/constants.js | 28 ++++++++++++++-------------- src/components/sliders/draw.js | 22 ++++++++++------------ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index 6593b502b7e..ddfe2bf8cd4 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -48,16 +48,6 @@ module.exports = { // font size to height scale fontSizeToHeight: 1.3, - // item rect radii - rx: 2, - ry: 2, - - // item text x offset off left edge - textOffsetX: 12, - - // item text y offset (w.r.t. middle) - textOffsetY: 3, - // arrow offset off right edge arrowOffsetX: 4, @@ -67,6 +57,16 @@ module.exports = { railBorderColor: '#bec8d9', railBgColor: '#ebedf0', + // The distance of the rail from the edge of the touchable area + // Slightly less than the step inset because of the curved edges + // of the rail + railInset: 8, + + // The distance from the extremal tick marks to the edge of the + // touchable area. This is basically the same as the grip radius, + // but for other styles it wouldn't really need to be. + stepInset: 10, + gripRadius: 10, gripWidth: 20, gripHeight: 20, @@ -76,15 +76,15 @@ module.exports = { gripBgColor: '#ebedf0', gripBgActiveColor: '#dbdde0', - // Padding in the direction perpendicular to the length of the rail: - // (which, at the moment is always vertical, but for the sake of the future...) - widthPadding: 10, + labelPadding: 8, + labelOffset: 0, - labelPadding: 4, tickWidth: 1, tickColor: '#333', tickOffset: 25, tickLength: 7, + + minorTickOffset: 25, minorTickColor: '#333', minorTickLength: 4, }; diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index d826a6d4c2e..692f4a2d0b1 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -146,16 +146,14 @@ function findDimensions(gd, sliderOpts) { // The length of the rail, *excluding* padding on either end: sliderOpts.inputAreaStart = 0; sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); - sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); - sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); - var textableInputLength = sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset; + var textableInputLength = sliderOpts.inputAreaLength - 2 * constants.stepInset; var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1); var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; + sliderOpts.height = constants.tickOffset + constants.tickLength + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -307,7 +305,7 @@ function drawLabelGroup(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, d.fraction), - constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.labelOffset ); }); @@ -405,7 +403,7 @@ function drawTicks(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * constants.tickWidth, - constants.tickOffset + isMajor ? constants.tickOffset : constants.minorTickOffset ); }); @@ -443,13 +441,13 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { // Convert a number from [0-1] to a pixel position relative to the slider group container: function normalizedValueToPosition(sliderOpts, normalizedPosition) { - return sliderOpts.inputAreaStart + sliderOpts.stepInset + - (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); + return sliderOpts.inputAreaStart + constants.stepInset + + (sliderOpts.inputAreaLength - 2 * constants.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); } // Convert a position relative to the slider group to a nubmer in [0, 1] function positionToNormalizedValue(sliderOpts, position) { - return Math.min(1, Math.max(0, (position - sliderOpts.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset - 2 * sliderOpts.inputAreaStart))); + return Math.min(1, Math.max(0, (position - constants.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * constants.stepInset - 2 * sliderOpts.inputAreaStart))); } function drawTouchRect(sliderGroup, gd, sliderOpts) { @@ -466,7 +464,7 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + constants.tickLength + sliderOpts.labelHeight) }) .call(Color.fill, constants.gripBgColor) - .attr('opacity', 0.0); + .attr('opacity', 0); Lib.setTranslate(rect, 0, 0); } @@ -478,7 +476,7 @@ function drawRail(sliderGroup, sliderOpts) { rect.enter().append('rect') .classed(constants.railRectClass, true); - var computedLength = sliderOpts.inputAreaLength - sliderOpts.railInset * 2; + var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2; rect.attr({ width: computedLength, @@ -491,7 +489,7 @@ function drawRail(sliderGroup, sliderOpts) { .call(Color.fill, constants.railBgColor) .style('stroke-width', '1px'); - Lib.setTranslate(rect, sliderOpts.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); + Lib.setTranslate(rect, constants.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); } function clearPushMargins(gd) { From 8fe184269ec3a37cc8d0e76dad9f123e9477b902 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 13:31:18 -0400 Subject: [PATCH 09/40] Tweak slider behavior to avoid event -> method loops --- src/components/sliders/draw.js | 44 ++++++++++++++---- test/image/baselines/sliders.png | Bin 0 -> 22241 bytes .../image/mocks/{slider.json => sliders.json} | 12 +++-- 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 test/image/baselines/sliders.png rename test/image/mocks/{slider.json => sliders.json} (95%) diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 692f4a2d0b1..d616d854346 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -230,6 +230,11 @@ function attachListeners(gd, sliderGroup, sliderOpts) { value = Lib.nestedProperty(data, updatevalue).get(); } + // If it's *currently* invoking a command an event is received, + // then we'll ignore the event in order to avoid complicated + // invinite loops. + if(sliderOpts._invokingCommand) return; + setActiveByLabel(gd, sliderGroup, sliderOpts, value, false, true); }; } @@ -338,17 +343,38 @@ function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback, doTran function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) { sliderOpts._input.active = sliderOpts.active = index; - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); - var step = sliderOpts.steps[sliderOpts.active]; + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); + if(step && step.method && doCallback) { - var args = step.args; - Plotly[step.method](gd, args[0], args[1], args[2]).catch(function() { - // This is not a disaster. Some methods like `animate` reject if interrupted - // and *should* nicely log a warning. - Lib.warn('Warning: Plotly.' + step.method + ' was called and rejected.'); - }); + if(sliderGroup._nextMethod) { + // If we've already queued up an update, just overwrite it with the most recent: + sliderGroup._nextMethod.step = step; + sliderGroup._nextMethod.doCallback = doCallback; + sliderGroup._nextMethod.doTransition = doTransition; + } else { + sliderGroup._nextMethod = {step: step, doCallback: doCallback, doTransition: doTransition}; + sliderGroup._nextMethodRaf = window.requestAnimationFrame(function() { + var _step = sliderGroup._nextMethod.step; + var args = _step.args; + if(!_step.method) return; + + sliderOpts._invokingCommand = true; + Plotly[_step.method](gd, args[0], args[1], args[2]).then(function() { + sliderOpts._invokingCommand = false; + }, function() { + sliderOpts._invokingCommand = false; + + // This is not a disaster. Some methods like `animate` reject if interrupted + // and *should* nicely log a warning. + Lib.warn('Warning: Plotly.' + _step.method + ' was called and rejected.'); + }); + + sliderGroup._nextMethod = null; + sliderGroup._nextMethodRaf = null; + }); + } } } @@ -428,7 +454,7 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { var x = normalizedValueToPosition(sliderOpts, position); var el = grip; - if(doTransition && sliderOpts.transition.duration > 0) { + if(doTransition && sliderOpts.transition.duration > 0 && !sliderOpts._invokingCommand) { el = el.transition() .duration(sliderOpts.transition.duration) .ease(sliderOpts.transition.easing); diff --git a/test/image/baselines/sliders.png b/test/image/baselines/sliders.png new file mode 100644 index 0000000000000000000000000000000000000000..2a4f3b4f7fa9fbe0b6e1eb45c78ece348e2f6520 GIT binary patch literal 22241 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/slider.json b/test/image/mocks/sliders.json similarity index 95% rename from test/image/mocks/slider.json rename to test/image/mocks/sliders.json index 60d665ce38f..c632fa18535 100644 --- a/test/image/mocks/slider.json +++ b/test/image/mocks/sliders.json @@ -45,8 +45,10 @@ "easing": "cubic-in-out" }, - "xpad": 20, - "ypad": 30, + "pad": { + "r": 20, + "t": 20 + }, "font": {} }, { @@ -88,8 +90,10 @@ "easing": "cubic-in-out" }, - "xpad": 20, - "ypad": 30, + "pad": { + "l": 20, + "t": 20 + }, "font": {} }], From 05ac5863c1b135df2c5f456f0fd30cd6ff50b2de Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 17:19:41 -0400 Subject: [PATCH 10/40] Improve padding logic for updatemenus --- src/components/updatemenus/attributes.js | 5 + src/components/updatemenus/defaults.js | 5 + src/components/updatemenus/draw.js | 29 +-- src/plots/pad_attributes.js | 36 ++++ .../baselines/updatemenus_positioning.png | Bin 37495 -> 38220 bytes test/image/mocks/updatemenus_positioning.json | 203 +++++++++++++----- 6 files changed, 216 insertions(+), 62 deletions(-) create mode 100644 src/plots/pad_attributes.js diff --git a/src/components/updatemenus/attributes.js b/src/components/updatemenus/attributes.js index eb3a55a766b..5efac53243b 100644 --- a/src/components/updatemenus/attributes.js +++ b/src/components/updatemenus/attributes.js @@ -11,6 +11,7 @@ var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; +var padAttrs = require('../../plots/pad_attributes'); var buttonsAttrs = { _isLinkedToArray: true, @@ -140,6 +141,10 @@ module.exports = { ].join(' ') }, + pad: extendFlat({}, padAttrs, { + description: 'Sets the padding around the buttons or dropdown menu.' + }), + font: extendFlat({}, fontAttrs, { description: 'Sets the font of the update menu button text.' }), diff --git a/src/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js index 39f662c9003..f97fdd34297 100644 --- a/src/components/updatemenus/defaults.js +++ b/src/components/updatemenus/defaults.js @@ -60,6 +60,11 @@ function menuDefaults(menuIn, menuOut, layoutOut) { coerce('xanchor'); coerce('yanchor'); + coerce('pad.t'); + coerce('pad.r'); + coerce('pad.b'); + coerce('pad.l'); + Lib.coerceFont(coerce, 'font', layoutOut.font); coerce('bgcolor', layoutOut.paper_bgcolor); diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index 8acf51820ca..afd4d809081 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -169,7 +169,7 @@ function drawHeader(gd, gHeader, gButton, menuOpts) { var active = menuOpts.active, headerOpts = menuOpts.buttons[active] || constants.blankHeaderOpts, - posOpts = { y: 0, yPad: 0, x: 0, xPad: 0, index: 0 }, + posOpts = { y: menuOpts.pad.t, yPad: 0, x: menuOpts.pad.l, xPad: 0, index: 0 }, positionOverrides = { width: menuOpts.headerWidth, height: menuOpts.headerHeight @@ -191,8 +191,8 @@ function drawHeader(gd, gHeader, gButton, menuOpts) { .text('â–¼'); arrow.attr({ - x: menuOpts.headerWidth - constants.arrowOffsetX, - y: menuOpts.headerHeight / 2 + constants.textOffsetY + x: menuOpts.headerWidth - constants.arrowOffsetX + menuOpts.pad.l, + y: menuOpts.headerHeight / 2 + constants.textOffsetY + menuOpts.pad.t }); header.on('click', function() { @@ -275,8 +275,8 @@ function drawButtons(gd, gHeader, gButton, menuOpts) { } var posOpts = { - x: x0, - y: y0, + x: x0 + menuOpts.pad.l, + y: y0 + menuOpts.pad.t, yPad: constants.gapButton, xPad: constants.gapButton, index: 0, @@ -468,27 +468,30 @@ function findDimenstions(gd, menuOpts) { fakeButtons.remove(); + var paddedWidth = menuOpts.totalWidth + menuOpts.pad.l + menuOpts.pad.r; + var paddedHeight = menuOpts.totalHeight + menuOpts.pad.t + menuOpts.pad.b; + var graphSize = gd._fullLayout._size; menuOpts.lx = graphSize.l + graphSize.w * menuOpts.x; menuOpts.ly = graphSize.t + graphSize.h * (1 - menuOpts.y); var xanchor = 'left'; if(anchorUtils.isRightAnchor(menuOpts)) { - menuOpts.lx -= menuOpts.totalWidth; + menuOpts.lx -= paddedWidth; xanchor = 'right'; } if(anchorUtils.isCenterAnchor(menuOpts)) { - menuOpts.lx -= menuOpts.totalWidth / 2; + menuOpts.lx -= paddedWidth / 2; xanchor = 'center'; } var yanchor = 'top'; if(anchorUtils.isBottomAnchor(menuOpts)) { - menuOpts.ly -= menuOpts.totalHeight; + menuOpts.ly -= paddedHeight; yanchor = 'bottom'; } if(anchorUtils.isMiddleAnchor(menuOpts)) { - menuOpts.ly -= menuOpts.totalHeight / 2; + menuOpts.ly -= paddedHeight / 2; yanchor = 'middle'; } @@ -500,10 +503,10 @@ function findDimenstions(gd, menuOpts) { Plots.autoMargin(gd, constants.autoMarginIdRoot + menuOpts._index, { x: menuOpts.x, y: menuOpts.y, - l: menuOpts.totalWidth * ({right: 1, center: 0.5}[xanchor] || 0), - r: menuOpts.totalWidth * ({left: 1, center: 0.5}[xanchor] || 0), - b: menuOpts.totalHeight * ({top: 1, middle: 0.5}[yanchor] || 0), - t: menuOpts.totalHeight * ({bottom: 1, middle: 0.5}[yanchor] || 0) + l: paddedWidth * ({right: 1, center: 0.5}[xanchor] || 0), + r: paddedWidth * ({left: 1, center: 0.5}[xanchor] || 0), + b: paddedHeight * ({top: 1, middle: 0.5}[yanchor] || 0), + t: paddedHeight * ({bottom: 1, middle: 0.5}[yanchor] || 0) }); } diff --git a/src/plots/pad_attributes.js b/src/plots/pad_attributes.js new file mode 100644 index 00000000000..bfadb4c54b0 --- /dev/null +++ b/src/plots/pad_attributes.js @@ -0,0 +1,36 @@ +/** +* 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 = { + t: { + valType: 'number', + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) along the top of the component.' + }, + r: { + valType: 'number', + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) on the right side of the component.' + }, + b: { + valType: 'number', + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) along the bottom of the component.' + }, + l: { + valType: 'number', + dflt: 0, + role: 'style', + description: 'The amount of padding (in px) on the left side of the component.' + } +}; diff --git a/test/image/baselines/updatemenus_positioning.png b/test/image/baselines/updatemenus_positioning.png index 4931a807f68b3623331d27dda9a2f65fedba1256..27699ee4298344db1f20fa62fcea09968841fccb 100644 GIT binary patch literal 38220 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 Iao_iU0a#jg1^@s6 literal 37495 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 diff --git a/test/image/mocks/updatemenus_positioning.json b/test/image/mocks/updatemenus_positioning.json index ecff0b0c906..77048b4b6d3 100644 --- a/test/image/mocks/updatemenus_positioning.json +++ b/test/image/mocks/updatemenus_positioning.json @@ -24,11 +24,11 @@ "updatemenus": [ { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "A0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "A1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "A2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "A3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "A4", "method": "restyle", "args": ["marker.color", "orange"]} ], "x": 0.3, "y": 1.0, @@ -37,11 +37,11 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "B1", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "B2", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "B3", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "B4", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "B5", "method": "restyle", "args": ["marker.color", "orange"]} ], "x": 0.3, "y": 0.66, @@ -51,11 +51,11 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "C0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "C1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "C2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "C3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "C4", "method": "restyle", "args": ["marker.color", "orange"]} ], "x": 0.3, "y": 0.33, @@ -65,25 +65,28 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "D0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "D1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "D2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "D3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "D4", "method": "restyle", "args": ["marker.color", "orange"]} ], "x": 0.3, "y": 0.0, "yanchor": "top", "xanchor": "left", - "direction": "up" + "direction": "up", + "pad": { + "t": 40 + } }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "E0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "E1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "E2longgg", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "E3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "E4", "method": "restyle", "args": ["marker.color", "orange"]} ], "type": "buttons", "x": -0.12, @@ -94,11 +97,11 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "F0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "F1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "F2longgg", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "F3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "F4", "method": "restyle", "args": ["marker.color", "orange"]} ], "type": "buttons", "x": -0.12, @@ -110,8 +113,8 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]} + {"label": "G0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "G1", "method": "restyle", "args": ["marker.color", "blue"]} ], "type": "buttons", "x": 1, @@ -121,8 +124,8 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]} + {"label": "H0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "H1", "method": "restyle", "args": ["marker.color", "blue"]} ], "type": "buttons", "x": 1, @@ -133,11 +136,11 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "I0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "I1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "I2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "I3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "I4", "method": "restyle", "args": ["marker.color", "orange"]} ], "x": 0.6, "y": 0.9, @@ -146,11 +149,11 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]}, - {"label": "green", "method": "restyle", "args": ["marker.color", "green"]}, - {"label": "yellow", "method": "restyle", "args": ["marker.color", "yellow"]}, - {"label": "orange", "method": "restyle", "args": ["marker.color", "orange"]} + {"label": "J0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "J1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "J2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "J3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "J4", "method": "restyle", "args": ["marker.color", "orange"]} ], "x": 0.6, "y": 0.9, @@ -159,8 +162,42 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]} + {"label": "N0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "N1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "N2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "N3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "N4", "method": "restyle", "args": ["marker.color", "orange"]} + ], + "x": 0.6, + "y": 0.9, + "yanchor": "bottom", + "xanchor": "right", + "pad": { + "b": 10, + "r": 20 + } + }, + { + "buttons": [ + {"label": "O0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "O1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "O2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "O3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "O4", "method": "restyle", "args": ["marker.color", "orange"]} + ], + "x": 0.6, + "y": 0.9, + "yanchor": "top", + "xanchor": "left", + "pad": { + "t": 10, + "l": 20 + } + }, + { + "buttons": [ + {"label": "K0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "K1", "method": "restyle", "args": ["marker.color", "blue"]} ], "x": 0.6, "y": 0.4, @@ -170,14 +207,82 @@ }, { "buttons": [ - {"label": "red", "method": "restyle", "args": ["marker.color", "red"]}, - {"label": "blue", "method": "restyle", "args": ["marker.color", "blue"]} + {"label": "P0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "P1", "method": "restyle", "args": ["marker.color", "blue"]} + ], + "type": "buttons", + "x": 0.6, + "y": 0.4, + "yanchor": "top", + "xanchor": "left", + "pad": { + "r": 50, + "b": 80 + } + }, + { + "buttons": [ + {"label": "Q0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "Q1", "method": "restyle", "args": ["marker.color", "blue"]} + ], + "type": "buttons", + "x": 0.6, + "y": 0.4, + "yanchor": "bottom", + "xanchor": "right", + "pad": { + "l": 50, + "t": 80 + } + }, + { + "buttons": [ + {"label": "L0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "L1", "method": "restyle", "args": ["marker.color", "blue"]} ], "type": "buttons", "x": 0.6, "y": 0.4, "yanchor": "bottom", "xanchor": "left" + }, + { + "buttons": [ + {"label": "M0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "M1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "M2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "M3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "M4", "method": "restyle", "args": ["marker.color", "orange"]} + ], + "x": 1.0, + "y": 0.5, + "yanchor": "middle", + "xanchor": "center", + "pad": { + "t": 80, + "r": 100, + "b": 80, + "l": 80 + } + }, + { + "buttons": [ + {"label": "R0", "method": "restyle", "args": ["marker.color", "red"]}, + {"label": "R1", "method": "restyle", "args": ["marker.color", "blue"]}, + {"label": "R2", "method": "restyle", "args": ["marker.color", "green"]}, + {"label": "R3", "method": "restyle", "args": ["marker.color", "yellow"]}, + {"label": "R4", "method": "restyle", "args": ["marker.color", "orange"]} + ], + "x": 0.9, + "y": 0.5, + "yanchor": "middle", + "xanchor": "center", + "pad": { + "t": 2, + "r": 50, + "b": 2, + "l": 40 + } } ], "xaxis": { From 46043418e883ed838dda44ebc04c69f64a282bec Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 29 Sep 2016 08:52:44 -0400 Subject: [PATCH 11/40] Add jasmine tests for updatemenus padding --- test/jasmine/tests/updatemenus_test.js | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/jasmine/tests/updatemenus_test.js b/test/jasmine/tests/updatemenus_test.js index e155bf27eff..f3e1e95a06e 100644 --- a/test/jasmine/tests/updatemenus_test.js +++ b/test/jasmine/tests/updatemenus_test.js @@ -7,6 +7,7 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var TRANSITION_DELAY = 100; +var fail = require('../assets/fail_test'); describe('update menus defaults', function() { 'use strict'; @@ -446,6 +447,60 @@ describe('update menus interactions', function() { }); }); + it('applies padding on all sides', function(done) { + var xy1, xy2; + var firstMenu = d3.select('.' + constants.headerGroupClassName); + var xpad = 80; + var ypad = 60; + + // Position it center-anchored and in the middle of the plot: + Plotly.relayout(gd, { + 'updatemenus[0].x': 0.2, + 'updatemenus[0].y': 0.5, + 'updatemenus[0].xanchor': 'center', + 'updatemenus[0].yanchor': 'middle', + }).then(function() { + // Convert to xy: + xy1 = firstMenu.attr('transform').match(/translate\(([^,]*),\s*([^\)]*)\)/).slice(1).map(parseFloat); + + // Set three of four paddings. This should move it. + return Plotly.relayout(gd, { + 'updatemenus[0].pad.t': ypad, + 'updatemenus[0].pad.r': xpad, + 'updatemenus[0].pad.b': ypad, + 'updatemenus[0].pad.l': xpad, + }); + }).then(function() { + xy2 = firstMenu.attr('transform').match(/translate\(([^,]*),\s*([^\)]*)\)/).slice(1).map(parseFloat); + + expect(xy1[0] - xy2[0]).toEqual(xpad); + expect(xy1[1] - xy2[1]).toEqual(ypad); + }).catch(fail).then(done); + }); + + it('appliesy padding on relayout', function(done) { + var x1, x2; + var firstMenu = d3.select('.' + constants.headerGroupClassName); + var padShift = 40; + + // Position the menu in the center of the plot horizontal so that + // we can test padding updates without worrying about margin pushing. + Plotly.relayout(gd, { + 'updatemenus[0].x': 0.5, + 'updatemenus[0].pad.r': 0, + }).then(function() { + // Extract the x-component of the translation: + x1 = parseInt(firstMenu.attr('transform').match(/translate\(([^,]*).*/)[1]); + + return Plotly.relayout(gd, 'updatemenus[0].pad.r', 40); + }).then(function() { + // Extract the x-component of the translation: + x2 = parseInt(firstMenu.attr('transform').match(/translate\(([^,]*).*/)[1]); + + expect(x1 - x2).toBeCloseTo(padShift, 1); + }).catch(fail).then(done); + }); + function assertNodeCount(query, cnt) { expect(d3.selectAll(query).size()).toEqual(cnt); } From 8558be8ef4d0b3e3dc6366b4a872b75d0af97656 Mon Sep 17 00:00:00 2001 From: Chelsea Date: Thu, 29 Sep 2016 11:13:10 -0400 Subject: [PATCH 12/40] add streaming maxpoints max and dflt --- src/plots/attributes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plots/attributes.js b/src/plots/attributes.js index a9c09f810fd..69496df0ee3 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -94,6 +94,8 @@ module.exports = { maxpoints: { valType: 'number', min: 0, + max: 10000, + dflt: 500, role: 'info', description: [ 'Sets the maximum number of points to keep on the plots from an', From 304174a7556d3e64f17f87c379cdc5f478637ad5 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 29 Sep 2016 12:04:16 -0400 Subject: [PATCH 13/40] Add current value output to sliders --- src/components/sliders/attributes.js | 44 ++++++++++++++- src/components/sliders/constants.js | 5 ++ src/components/sliders/defaults.js | 10 ++-- src/components/sliders/draw.js | 84 ++++++++++++++++++++++++++-- 4 files changed, 132 insertions(+), 11 deletions(-) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 49d0f2b647b..7b8ed56835f 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -187,8 +187,50 @@ module.exports = { }, }, + currentvalue: { + visible: { + valType: 'boolean', + dflt: true, + description: [ + 'Shows the currently-selected value above the slider.' + ].join(' ') + }, + + xanchor: { + valType: 'enumerated', + values: ['left', 'center', 'right'], + dflt: 'left', + description: [ + 'The alignment of the value readout relative to the length of the slider.' + ].join(' ') + }, + + offset: { + valType: 'number', + dflt: 10, + role: 'info', + description: [ + 'The amount of space, in pixels, between the current value label', + 'and the slider.' + ] + }, + + prefix: { + valType: 'string', + role: 'info', + description: [ + 'When `currentvalue.visible` is true, this sets the prefix of the lable. If provided,', + 'it will be joined to the current value with a single space between.' + ].join(' ') + }, + + font: extendFlat({}, fontAttrs, { + description: 'Sets the font of the current value lable text.' + }), + }, + font: extendFlat({}, fontAttrs, { - description: 'Sets the font of the slider button text.' + description: 'Sets the font of the slider step labels.' }), bgcolor: { diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index ddfe2bf8cd4..90b97c3cb99 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -28,6 +28,7 @@ module.exports = { labelsClass: 'slider-labels', labelGroupClass: 'slider-label-group', labelClass: 'slider-label', + currentValueClass: 'slider-current-value', railHeight: 5, @@ -87,4 +88,8 @@ module.exports = { minorTickOffset: 25, minorTickColor: '#333', minorTickLength: 4, + + // Extra space below the current value label: + currentValuePadding: 8, + currentValueInset: 0, }; diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 58a0f25310a..0515e66bc79 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -65,13 +65,14 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('pad.b'); coerce('pad.l'); + coerce('currentvalue.visible'); + coerce('currentvalue.xanchor'); + coerce('currentvalue.prefix'); + coerce('currentvalue.offset'); + coerce('updateevent'); coerce('updatevalue'); - if(!sliderIn.transition) { - sliderIn.transition = {}; - } - coerce('transition.duration'); coerce('transition.easing'); @@ -84,6 +85,7 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { } Lib.coerceFont(coerce, 'font', layoutOut.font); + Lib.coerceFont(coerce, 'currentvalue.font', layoutOut.font); coerce('bgcolor', layoutOut.paper_bgcolor); coerce('bordercolor'); diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index d616d854346..0d4dfc33854 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -127,6 +127,26 @@ function findDimensions(gd, sliderOpts) { constants.gripHeight ); + sliderOpts.currentValueMaxWidth = 0; + sliderOpts.currentValueHeight = 0; + sliderOpts.currentValueTotalHeight = 0; + + if(sliderOpts.currentvalue.visible) { + // Get the dimensions of the current value label: + var dummyGroup = gd._tester.append('g'); + + sliderLabels.each(function(stepOpts) { + var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label); + var curValSize = curValPrefix.node() && Drawing.bBox(curValPrefix.node()); + sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width)); + sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height)); + }); + + sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset; + + dummyGroup.remove(); + } + var graphSize = gd._fullLayout._size; sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); @@ -153,7 +173,7 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - sliderOpts.height = constants.tickOffset + constants.tickLength + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; + sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + constants.tickLength + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -193,6 +213,7 @@ function findDimensions(gd, sliderOpts) { function drawSlider(gd, sliderGroup, sliderOpts) { // These are carefully ordered for proper z-ordering: sliderGroup + .call(drawCurrentValue, sliderOpts) .call(drawRail, sliderOpts) .call(drawLabelGroup, sliderOpts) .call(drawTicks, sliderOpts) @@ -210,6 +231,53 @@ function drawSlider(gd, sliderGroup, sliderOpts) { setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true, false); } +function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { + if(!sliderOpts.currentvalue.visible) return; + + var x0, textAnchor; + var text = sliderGroup.selectAll('text') + .data([0]); + + switch(sliderOpts.currentvalue.xanchor) { + case 'right': + // This is anchored left and adjusted by the width of the longest label + // so that the prefix doesn't move. The goal of this is to emphasize + // what's actually changing and make the update less distracting. + x0 = sliderOpts.inputAreaLength - constants.currentValueInset - sliderOpts.currentValueMaxWidth; + textAnchor = 'left'; + break; + case 'center': + x0 = sliderOpts.inputAreaLength * 0.5; + textAnchor = 'middle'; + break; + default: + x0 = constants.currentValueInset; + textAnchor = 'left'; + } + + text.enter().append('text') + .classed(constants.labelClass, true) + .classed('user-select-none', true) + .attr('text-anchor', textAnchor); + + var str = sliderOpts.currentvalue.prefix ? (sliderOpts.currentvalue.prefix + ' ') : ''; + + if(typeof valueOverride === 'string') { + str += valueOverride; + } else { + var curVal = sliderOpts.steps[sliderOpts.active].label; + str += curVal; + } + + text.call(Drawing.font, sliderOpts.currentvalue.font) + .text(str) + .call(svgTextUtils.convertToTspans); + + Lib.setTranslate(text, x0, sliderOpts.currentValueHeight); + + return text; +} + function removeListeners(gd, sliderGroup, sliderOpts) { var listeners = sliderOpts._input.listeners; var eventNames = sliderOpts._input.eventNames; @@ -310,7 +378,7 @@ function drawLabelGroup(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, d.fraction), - constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.labelOffset + constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.labelOffset + sliderOpts.currentValueTotalHeight ); }); @@ -346,6 +414,7 @@ function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) var step = sliderOpts.steps[sliderOpts.active]; sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); + sliderGroup.call(drawCurrentValue, sliderOpts); if(step && step.method && doCallback) { if(sliderGroup._nextMethod) { @@ -429,7 +498,7 @@ function drawTicks(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * constants.tickWidth, - isMajor ? constants.tickOffset : constants.minorTickOffset + (isMajor ? constants.tickOffset : constants.minorTickOffset) + sliderOpts.currentValueTotalHeight ); }); @@ -462,7 +531,7 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { // Lib.setTranslate doesn't work here becasue of the transition duck-typing. // It's also not necessary because there are no other transitions to preserve. - el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + 0 + ')'); + el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + (sliderOpts.currentValueTotalHeight) + ')'); } // Convert a number from [0-1] to a pixel position relative to the slider group container: @@ -492,7 +561,7 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { .call(Color.fill, constants.gripBgColor) .attr('opacity', 0); - Lib.setTranslate(rect, 0, 0); + Lib.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight); } function drawRail(sliderGroup, sliderOpts) { @@ -515,7 +584,10 @@ function drawRail(sliderGroup, sliderOpts) { .call(Color.fill, constants.railBgColor) .style('stroke-width', '1px'); - Lib.setTranslate(rect, constants.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); + Lib.setTranslate(rect, + constants.railInset, + (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 + sliderOpts.currentValueTotalHeight + ); } function clearPushMargins(gd) { From 52b4a2a3a0dd98959128511ac74b219f5ffc08c3 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 27 Sep 2016 23:56:55 -0400 Subject: [PATCH 14/40] First cut at slider --- src/components/slider/attributes.js | 203 ++++++++++++++++ src/components/slider/constants.js | 85 +++++++ src/components/slider/defaults.js | 103 ++++++++ src/components/slider/draw.js | 358 ++++++++++++++++++++++++++++ src/components/slider/index.js | 20 ++ src/components/updatemenus/draw.js | 7 +- src/core.js | 1 + src/plot_api/plot_api.js | 2 + src/plotly.js | 1 + src/plots/layout_attributes.js | 3 +- 10 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 src/components/slider/attributes.js create mode 100644 src/components/slider/constants.js create mode 100644 src/components/slider/defaults.js create mode 100644 src/components/slider/draw.js create mode 100644 src/components/slider/index.js diff --git a/src/components/slider/attributes.js b/src/components/slider/attributes.js new file mode 100644 index 00000000000..d7b2185f378 --- /dev/null +++ b/src/components/slider/attributes.js @@ -0,0 +1,203 @@ +/** +* 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 fontAttrs = require('../../plots/font_attributes'); +var colorAttrs = require('../color/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +var stepsAttrs = { + _isLinkedToArray: true, + + method: { + valType: 'enumerated', + values: ['restyle', 'relayout', 'animate', 'update'], + dflt: 'restyle', + role: 'info', + description: [ + 'Sets the Plotly method to be called on click.' + ].join(' ') + }, + args: { + valType: 'info_array', + role: 'info', + freeLength: true, + items: [ + { valType: 'any' }, + { valType: 'any' }, + { valType: 'any' } + ], + description: [ + 'Sets the arguments values to be passed to the Plotly', + 'method set in `method` on click.' + ].join(' ') + }, + label: { + valType: 'string', + role: 'info', + dflt: '', + description: 'Sets the text label to appear on the slider' + } +}; + +module.exports = { + visible: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines whether or not the slider is visible.' + ].join(' ') + }, + + active: { + valType: 'string', + role: 'info', + min: -1, + dflt: 0, + description: [ + 'Determines which button (by index starting from 0) is', + 'considered active.' + ].join(' ') + }, + + steps: stepsAttrs, + + updateevent: { + valType: 'string', + arrayOk: true, + role: 'info', + description: [ + 'The name of the event to which this component subscribes', + 'in order to trigger updates. When the event is received', + 'the component will attempt to update the slider position', + 'to reflect the value passed as the data property of the', + 'event. The corresponding step\'s API method is assumed to', + 'have been triggered externally and so is not triggered again', + 'when the event is received. If an array is provided, multiple', + 'events will be subscribed to for updates.' + ].join(' ') + }, + + updatevalue: { + valType: 'string', + arrayOk: true, + role: 'info', + description: [ + 'The property of the event data that is matched to a slider', + 'value when an event of type `updateevent` is received. If', + 'undefined, the data argument itself is used. If a string,', + 'that property is used, and if a string with dots, e.g.', + '`item.0.label`, then `data[\'item\'][0][\'label\']` will', + 'be used. If an array, it is matched to the respective', + 'updateevent item or if there is no corresponding updatevalue', + 'for a particular updateevent, it is interpreted as `undefined` and defaults to the data property itself.' + ].join(' ') + }, + + lenmode: { + valType: 'enumerated', + values: ['fraction', 'pixels'], + role: 'info', + dflt: 'fraction', + description: [ + 'Determines whether this color bar\'s length', + '(i.e. the measure in the color variation direction)', + 'is set in units of plot *fraction* or in *pixels.', + 'Use `len` to set the value.' + ].join(' ') + }, + len: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: [ + 'Sets the length of the color bar', + 'This measure excludes the padding of both ends.', + 'That is, the color bar length is this length minus the', + 'padding on both ends.' + ].join(' ') + }, + x: { + valType: 'number', + min: -2, + max: 3, + dflt: -0.05, + role: 'style', + description: 'Sets the x position (in normalized coordinates) of the slider.' + }, + xpad: { + valType: 'number', + min: 0, + dflt: 10, + role: 'style', + description: 'Sets the amount of padding (in px) along the x direction' + }, + ypad: { + valType: 'number', + min: 0, + dflt: 10, + role: 'style', + description: 'Sets the amount of padding (in px) along the x direction' + }, + xanchor: { + valType: 'enumerated', + values: ['auto', 'left', 'center', 'right'], + dflt: 'left', + role: 'info', + description: [ + 'Sets the slider\'s horizontal position anchor.', + 'This anchor binds the `x` position to the *left*, *center*', + 'or *right* of the range selector.' + ].join(' ') + }, + y: { + valType: 'number', + min: -2, + max: 3, + dflt: 1, + role: 'style', + description: 'Sets the y position (in normalized coordinates) of the slider.' + }, + yanchor: { + valType: 'enumerated', + values: ['auto', 'top', 'middle', 'bottom'], + dflt: 'bottom', + role: 'info', + description: [ + 'Sets the slider\'s vertical position anchor', + 'This anchor binds the `y` position to the *top*, *middle*', + 'or *bottom* of the range selector.' + ].join(' ') + }, + + font: extendFlat({}, fontAttrs, { + description: 'Sets the font of the slider button text.' + }), + + bgcolor: { + valType: 'color', + role: 'style', + description: 'Sets the background color of the slider buttons.' + }, + bordercolor: { + valType: 'color', + dflt: colorAttrs.borderLine, + role: 'style', + description: 'Sets the color of the border enclosing the slider.' + }, + borderwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the border enclosing the slider.' + } +}; diff --git a/src/components/slider/constants.js b/src/components/slider/constants.js new file mode 100644 index 00000000000..92860f5d11c --- /dev/null +++ b/src/components/slider/constants.js @@ -0,0 +1,85 @@ +/** +* 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 = { + + // layout attribute names + name: 'sliders', + itemName: 'slider', + + // class names + containerClassName: 'slider-container', + groupClassName: 'slider-group', + inputAreaClass: 'slider-input-area', + railRectClass: 'slider-rail-rect', + railTouchRectClass: 'slider-rail-touch-rect', + gripRectClass: 'slider-grip-rect', + tickRectClass: 'slider-tick-rect', + inputProxyClass: 'slider-input-proxy', + + railHeight: 5, + + // DOM attribute name in button group keeping track + // of active update menu + menuIndexAttrName: 'slider-active-index', + + // id root pass to Plots.autoMargin + autoMarginIdRoot: 'slider-', + + // min item width / height + minWidth: 30, + minHeight: 30, + + // padding around item text + textPadX: 40, + + // font size to height scale + fontSizeToHeight: 1.3, + + // item rect radii + rx: 2, + ry: 2, + + // item text x offset off left edge + textOffsetX: 12, + + // item text y offset (w.r.t. middle) + textOffsetY: 3, + + // arrow offset off right edge + arrowOffsetX: 4, + + railRadius: 2, + railWidth: 5, + railBorder: 4, + railBorderColor: '#bec8d9', + railBgColor: '#ebedf0', + + + gripRadius: 10, + gripWidth: 20, + gripHeight: 20, + gripBorder: 20, + gripBorderWidth: 1, + gripBorderColor: '#bec8d9', + gripBgColor: '#ebedf0', + gripBgActiveColor: '#dbdde0', + + // Padding in the direction perpendicular to the length of the rail: + // (which, at the moment is always vertical, but for the sake of the future...) + widthPadding: 10, + + tickWidth: 1, + tickColor: '#333', + tickOffset: 25, + tickLength: 7, +}; diff --git a/src/components/slider/defaults.js b/src/components/slider/defaults.js new file mode 100644 index 00000000000..b7bb78e9204 --- /dev/null +++ b/src/components/slider/defaults.js @@ -0,0 +1,103 @@ +/** +* 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 Lib = require('../../lib'); + +var attributes = require('./attributes'); +var contants = require('./constants'); + +var name = contants.name; +var stepAttrs = attributes.steps; + + +module.exports = function slidersDefaults(layoutIn, layoutOut) { + var contIn = Array.isArray(layoutIn[name]) ? layoutIn[name] : [], + contOut = layoutOut[name] = []; + + for(var i = 0; i < contIn.length; i++) { + var sliderIn = contIn[i] || {}, + sliderOut = {}; + + sliderDefaults(sliderIn, sliderOut, layoutOut); + + // used on button click to update the 'active' field + sliderOut._input = sliderIn; + + // used to determine object constancy + sliderOut._index = i; + + contOut.push(sliderOut); + } +}; + +function sliderDefaults(sliderIn, sliderOut, layoutOut) { + + function coerce(attr, dflt) { + return Lib.coerce(sliderIn, sliderOut, attributes, attr, dflt); + } + + var steps = stepsDefaults(sliderIn, sliderOut); + + var visible = coerce('visible', steps.length > 0); + if(!visible) return; + + coerce('active'); + + coerce('x'); + coerce('y'); + Lib.noneOrAll(sliderIn, sliderOut, ['x', 'y']); + + coerce('xanchor'); + coerce('yanchor'); + + coerce('len'); + coerce('lenmode'); + + coerce('xpad'); + coerce('ypad'); + + coerce('updateevent'); + coerce('updatevalue'); + + Lib.coerceFont(coerce, 'font', layoutOut.font); + + coerce('bgcolor', layoutOut.paper_bgcolor); + coerce('bordercolor'); + coerce('borderwidth'); +} + +function stepsDefaults(sliderIn, sliderOut) { + var valuesIn = sliderIn.steps || [], + valuesOut = sliderOut.steps = []; + + var valueIn, valueOut; + + function coerce(attr, dflt) { + return Lib.coerce(valueIn, valueOut, stepAttrs, attr, dflt); + } + + for(var i = 0; i < valuesIn.length; i++) { + valueIn = valuesIn[i]; + valueOut = {}; + + if(!Lib.isPlainObject(valueIn) || !Array.isArray(valueIn.args)) { + continue; + } + + coerce('method'); + coerce('args'); + coerce('label'); + + valueOut._index = i; + valuesOut.push(valueOut); + } + + return valuesOut; +} diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js new file mode 100644 index 00000000000..7f4a25b04f1 --- /dev/null +++ b/src/components/slider/draw.js @@ -0,0 +1,358 @@ +/** +* 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 Lib = require('../../lib'); +var Color = require('../color'); +var Drawing = require('../drawing'); +var svgTextUtils = require('../../lib/svg_text_utils'); +var anchorUtils = require('../legend/anchor_utils'); + +var constants = require('./constants'); + + +module.exports = function draw(gd) { + var fullLayout = gd._fullLayout, + sliderData = makeSliderData(fullLayout); + + // draw a container for *all* sliders: + var sliders = fullLayout._infolayer + .selectAll('g.' + constants.containerClassName) + .data(sliderData.length > 0 ? [0] : []); + + sliders.enter().append('g') + .classed(constants.containerClassName, true) + .style('cursor', 'pointer'); + + sliders.exit().remove(); + + // If no more sliders, clear the margisn: + if(sliders.exit().size()) clearPushMargins(gd); + + // Return early if no menus visible: + if(sliderData.length === 0) return; + + var sliderGroups = sliders.selectAll('g.'+ constants.groupClassName) + .data(sliderData, keyFunction); + + sliderGroups.enter().append('g') + .classed(constants.groupClassName, true); + + sliderGroups.exit().each(function(sliderOpts) { + d3.select(this).remove(); + + Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index); + }); + + // Find the dimensions of the sliders: + for(var i = 0; i < sliderData.length; i++) { + var sliderOpts = sliderData[i]; + findDimensions(gd, sliderOpts); + } + + sliderGroups.each(function(sliderOpts) { + computeDisplayedSteps(sliderOpts); + + drawSlider(gd, d3.select(this), sliderOpts); + + makeInputProxy(gd, d3.select(this), sliderOpts); + + }); +}; + +function makeInputProxy(gd, sliderGroup, sliderOpts) { + sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass) + .data([0]); +} + + + +// This really only just filters by visibility: +function makeSliderData(fullLayout) { + var contOpts = fullLayout[constants.name], + sliderData = []; + + for(var i = 0; i < contOpts.length; i++) { + var item = contOpts[i]; + if(item.visible) sliderData.push(item); + } + + return sliderData; +} + +// This is set in the defaults step: +function keyFunction(opts) { + return opts._index; +} + +// Compute the dimensions (mutates sliderOpts): +function findDimensions(gd, sliderOpts) { + sliderOpts._gd = gd; + + sliderOpts.inputAreaWidth = Math.max( + constants.railWidth, + constants.gripHeight + ); + + var graphSize = gd._fullLayout._size; + sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; + sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); + + if (sliderOpts.lenmode === 'fraction') { + // fraction: + sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len); + } else { + // pixels: + sliderOpts.outerLength = sliderOpts.len; + } + + // Set the length-wise padding so that the grip ends up *on* the end of + // the bar when at either extreme + sliderOpts.lenPad = Math.round(constants.gripWidth * 0.5); + + // The length of the rail, *excluding* padding on either end: + sliderOpts.inputAreaStart = 0; + sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.xpad * 2); + sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); + sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); + + // Hard-code this for now: + sliderOpts.height = 150; + + var xanchor = 'left'; + if(anchorUtils.isRightAnchor(sliderOpts)) { + sliderOpts.lx -= sliderOpts.outerLength; + xanchor = 'right'; + } + if(anchorUtils.isCenterAnchor(sliderOpts)) { + sliderOpts.lx -= sliderOpts.outerLength / 2; + xanchor = 'center'; + } + + var yanchor = 'top'; + if(anchorUtils.isBottomAnchor(sliderOpts)) { + sliderOpts.ly -= sliderOpts.height; + yanchor = 'bottom'; + } + if(anchorUtils.isMiddleAnchor(sliderOpts)) { + sliderOpts.ly -= sliderOpts.height / 2; + yanchor = 'middle'; + } + + sliderOpts.outerLength = Math.ceil(sliderOpts.outerLength); + sliderOpts.height = Math.ceil(sliderOpts.height); + sliderOpts.lx = Math.round(sliderOpts.lx); + sliderOpts.ly = Math.round(sliderOpts.ly); + + Plots.autoMargin(gd, constants.autoMarginIdRoot + sliderOpts._index, { + x: sliderOpts.x, + y: sliderOpts.y, + l: sliderOpts.outerLength * ({right: 1, center: 0.5}[xanchor] || 0), + r: sliderOpts.outerLength * ({left: 1, center: 0.5}[xanchor] || 0), + b: sliderOpts.height * ({top: 1, middle: 0.5}[yanchor] || 0), + t: sliderOpts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + }); +} + +function drawSlider(gd, group, sliderOpts) { + // These are carefully ordered for proper z-ordering: + group + .call(drawRail, sliderOpts) + .call(drawTouchRect, sliderOpts) + .call(drawTicks, sliderOpts) + .call(drawGrip, sliderOpts) + + group.call(setGripPosition, sliderOpts, 0); + group.call(attachFocusEvents, sliderOpts); + + // Position the rectangle: + Lib.setTranslate(group, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); +} + +function drawGrip(sliderGroup, sliderOpts) { + var grip = sliderGroup.selectAll('rect.' + constants.gripRectClass) + .data([0]); + + grip.enter().append('rect') + .classed(constants.gripRectClass, true) + .call(attachGripEvents, sliderGroup, sliderOpts) + .style('pointer-events', 'all'); + + grip.attr({ + width: constants.gripHeight, + height: constants.gripWidth, + rx: constants.gripRadius, + ry: constants.gripRadius, + }) + .call(Color.stroke, constants.gripBorderColor) + .call(Color.fill, constants.gripBgColor) + .style('stroke-width', constants.gripBorderWidth + 'px'); +} + +function handleInput(sliderGroup, sliderOpts, normalizedPosition) { + var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); + + if (quantizedPosition !== sliderOpts._active) { + setActive(sliderGroup, sliderOpts, quantizedPosition); + } +} + +function setActive(sliderGroup, sliderOpts, active) { + sliderOpts._active = active; + + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts._active / (sliderOpts.steps.length - 1)); + + var step = sliderOpts.steps[sliderOpts._active]; + + if (step && step.method) { + var args = step.args; + Plotly[step.method](gd, args[0], args[1], args[2]).catch(function(msg) { + // This is not a disaster. Some methods like `animate` reject if interrupted + // and *should* nicely log a warning. + Lib.warn('Warning: Plotly.' + step.method + ' was called and rejected.'); + }); + } +} + +function attachFocusEvents(sliderGroup, sliderOpts) { + sliderGroup.on('focus', function() { + }).on('blur', function() { + }); +} + +function attachGripEvents(item, sliderGroup, sliderOpts) { + var gd = d3.select(sliderOpts._gd); + var node = sliderGroup.node(); + + item.on('mousedown', function(event) { + var grip = sliderGroup.select('.' + constants.gripRectClass); + + d3.event.stopPropagation(); + d3.event.preventDefault(); + grip.call(Color.fill, constants.gripBgActiveColor) + + var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); + handleInput(sliderGroup, sliderOpts, normalizedPosition); + + gd.on('mousemove', function() { + var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); + handleInput(sliderGroup, sliderOpts, normalizedPosition); + }); + + gd.on('mouseup', function() { + grip.call(Color.fill, constants.gripBgColor) + gd.on('mouseup', null); + gd.on('mousemove', null); + }); + }); +} + +function drawTicks(sliderGroup, sliderOpts) { + var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass) + .data(sliderOpts.displayedSteps); + + tick.enter().append('rect') + .classed(constants.tickRectClass, true) + + tick.attr({ + width: constants.tickWidth, + height: constants.tickLength, + 'shape-rendering': 'crispEdges' + }) + .call(Color.fill, constants.tickColor); + + tick.each(function (d, i) { + Lib.setTranslate( + d3.select(this), + normalizedValueToPosition(sliderOpts, d.fraction) - 0.5 * constants.tickWidth, + constants.tickOffset + ); + }); + +} + +function computeDisplayedSteps(sliderOpts) { + sliderOpts.displayedSteps = []; + var i0 = 0; + var step = 1; + var nsteps = sliderOpts.steps.length; + + for (var i = i0; i < nsteps; i += step) { + sliderOpts.displayedSteps.push({ + fraction: i / (nsteps - 1), + step: sliderOpts.steps[i] + }); + } +} + +function setGripPosition(sliderGroup, sliderOpts, position) { + var grip = sliderGroup.select('rect.' + constants.gripRectClass); + + var x = normalizedValueToPosition(sliderOpts, position); + Lib.setTranslate(grip, x - constants.gripWidth * 0.5, 0); +} + +// Convert a number from [0-1] to a pixel position relative to the slider group container: +function normalizedValueToPosition(sliderOpts, normalizedPosition) { + return sliderOpts.inputAreaStart + sliderOpts.stepInset + + (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); +} + +// Convert a position relative to the slider group to a nubmer in [0, 1] +function positionToNormalizedValue(sliderOpts, position) { + return Math.min(1, Math.max(0, (position - sliderOpts.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset - 2 * sliderOpts.inputAreaStart))); +} + +function drawTouchRect(sliderGroup, sliderOpts) { + var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass) + .data([0]); + + rect.enter().append('rect') + .classed(constants.railTouchRectClass, true) + .call(attachGripEvents, sliderGroup, sliderOpts) + .style('pointer-events', 'all'); + + rect.attr({ + width: sliderOpts.inputAreaLength, + height: sliderOpts.inputAreaWidth + }) + .call(Color.fill, constants.gripBgColor) + .attr('opacity', 0) + + Lib.setTranslate(rect, 0, 0); +} + +function drawRail(sliderGroup, sliderOpts) { + var rect = sliderGroup.selectAll('rect.' + constants.railRectClass) + .data([0]); + + rect.enter().append('rect') + .classed(constants.railRectClass, true) + + var computedLength = sliderOpts.inputAreaLength - sliderOpts.railInset * 2; + + rect.attr({ + width: computedLength, + height: constants.railWidth, + rx: constants.railRadius, + ry: constants.railRadius, + 'shape-rendering': 'crispEdges' + }) + .call(Color.stroke, constants.railBorderColor) + .call(Color.fill, constants.railBgColor) + .style('stroke-width', '1px'); + + Lib.setTranslate(rect, sliderOpts.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); +} + diff --git a/src/components/slider/index.js b/src/components/slider/index.js new file mode 100644 index 00000000000..389368c4908 --- /dev/null +++ b/src/components/slider/index.js @@ -0,0 +1,20 @@ +/** +* 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'; + + +exports.moduleType = 'component'; + +exports.name = 'slider'; + +exports.layoutAttributes = require('./attributes'); + +exports.supplyLayoutDefaults = require('./defaults'); + +exports.draw = require('./draw'); diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index afd4d809081..6da09277e69 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -108,7 +108,7 @@ module.exports = function draw(gd) { // find dimensions before plotting anything (this mutates menuOpts) for(var i = 0; i < menuData.length; i++) { var menuOpts = menuData[i]; - findDimenstions(gd, menuOpts); + findDimensions(gd, menuOpts); } // draw headers! @@ -221,6 +221,9 @@ function drawHeader(gd, gHeader, gButton, menuOpts) { } function drawButtons(gd, gHeader, gButton, menuOpts) { + // If this is a set of buttons, set pointer events = all since we play + // some minor games with which container is which in order to simplify + // the drawing of *either* buttons or menus if(!gButton) { gButton = gHeader; gButton.attr('pointer-events', 'all'); @@ -383,7 +386,7 @@ function styleOnMouseOut(item, menuOpts) { } // find item dimensions (this mutates menuOpts) -function findDimenstions(gd, menuOpts) { +function findDimensions(gd, menuOpts) { menuOpts.width1 = 0; menuOpts.height1 = 0; menuOpts.heights = []; diff --git a/src/core.js b/src/core.js index 00a266c962e..96931ac487b 100644 --- a/src/core.js +++ b/src/core.js @@ -58,6 +58,7 @@ exports.register([ require('./components/shapes'), require('./components/images'), require('./components/updatemenus'), + require('./components/slider'), require('./components/rangeslider'), require('./components/rangeselector') ]); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 72b40252361..51867a84881 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -178,6 +178,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('legend', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); + Registry.getComponentMethod('slider', 'draw')(gd); for(i = 0; i < calcdata.length; i++) { cd = calcdata[i]; @@ -303,6 +304,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('rangeslider', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); + Registry.getComponentMethod('slider', 'draw')(gd); } function cleanUp() { diff --git a/src/plotly.js b/src/plotly.js index 3f8cba139c0..02943617655 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -37,6 +37,7 @@ exports.Shapes = require('./components/shapes'); exports.Legend = require('./components/legend'); exports.Images = require('./components/images'); exports.UpdateMenus = require('./components/updatemenus'); +exports.Slider = require('./components/slider'); exports.ModeBar = require('./components/modebar'); // plot api diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 3aaba1ed04d..f0f9323adfd 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -183,6 +183,7 @@ module.exports = { 'annotations': 'annotations', 'shapes': 'shapes', 'images': 'images', - 'updatemenus': 'updatemenus' + 'updatemenus': 'updatemenus', + 'slider': 'slider' } }; From b161d55c4bcce14662669d96f633df27bd59461d Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 02:48:23 -0400 Subject: [PATCH 15/40] Fix lint errors in slider --- src/components/slider/attributes.js | 4 +- src/components/slider/constants.js | 7 +- src/components/slider/defaults.js | 8 + src/components/slider/draw.js | 296 ++++++++++++++++++++-------- 4 files changed, 233 insertions(+), 82 deletions(-) diff --git a/src/components/slider/attributes.js b/src/components/slider/attributes.js index d7b2185f378..2f937f5afe5 100644 --- a/src/components/slider/attributes.js +++ b/src/components/slider/attributes.js @@ -57,9 +57,9 @@ module.exports = { }, active: { - valType: 'string', + valType: 'number', role: 'info', - min: -1, + min: -10, dflt: 0, description: [ 'Determines which button (by index starting from 0) is', diff --git a/src/components/slider/constants.js b/src/components/slider/constants.js index 92860f5d11c..6593b502b7e 100644 --- a/src/components/slider/constants.js +++ b/src/components/slider/constants.js @@ -25,6 +25,9 @@ module.exports = { gripRectClass: 'slider-grip-rect', tickRectClass: 'slider-tick-rect', inputProxyClass: 'slider-input-proxy', + labelsClass: 'slider-labels', + labelGroupClass: 'slider-label-group', + labelClass: 'slider-label', railHeight: 5, @@ -64,7 +67,6 @@ module.exports = { railBorderColor: '#bec8d9', railBgColor: '#ebedf0', - gripRadius: 10, gripWidth: 20, gripHeight: 20, @@ -78,8 +80,11 @@ module.exports = { // (which, at the moment is always vertical, but for the sake of the future...) widthPadding: 10, + labelPadding: 4, tickWidth: 1, tickColor: '#333', tickOffset: 25, tickLength: 7, + minorTickColor: '#333', + minorTickLength: 4, }; diff --git a/src/components/slider/defaults.js b/src/components/slider/defaults.js index b7bb78e9204..fd5503b3b1a 100644 --- a/src/components/slider/defaults.js +++ b/src/components/slider/defaults.js @@ -66,6 +66,14 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('updateevent'); coerce('updatevalue'); + if(!Array.isArray(sliderOut.updateevent)) { + sliderOut.updateevent = [sliderOut.updateevent]; + } + + if(!Array.isArray(sliderOut.udpatevalue)) { + sliderOut.udpatevalue = [sliderOut.updatevalue]; + } + Lib.coerceFont(coerce, 'font', layoutOut.font); coerce('bgcolor', layoutOut.paper_bgcolor); diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 7f4a25b04f1..417c63dedce 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -33,7 +33,7 @@ module.exports = function draw(gd) { sliders.enter().append('g') .classed(constants.containerClassName, true) - .style('cursor', 'pointer'); + .style('cursor', 'ew-resize'); sliders.exit().remove(); @@ -43,7 +43,7 @@ module.exports = function draw(gd) { // Return early if no menus visible: if(sliderData.length === 0) return; - var sliderGroups = sliders.selectAll('g.'+ constants.groupClassName) + var sliderGroups = sliders.selectAll('g.' + constants.groupClassName) .data(sliderData, keyFunction); sliderGroups.enter().append('g') @@ -62,7 +62,7 @@ module.exports = function draw(gd) { } sliderGroups.each(function(sliderOpts) { - computeDisplayedSteps(sliderOpts); + computeLabelSteps(sliderOpts); drawSlider(gd, d3.select(this), sliderOpts); @@ -76,8 +76,6 @@ function makeInputProxy(gd, sliderGroup, sliderOpts) { .data([0]); } - - // This really only just filters by visibility: function makeSliderData(fullLayout) { var contOpts = fullLayout[constants.name], @@ -98,7 +96,31 @@ function keyFunction(opts) { // Compute the dimensions (mutates sliderOpts): function findDimensions(gd, sliderOpts) { - sliderOpts._gd = gd; + var sliderLabels = gd._tester.selectAll('g.' + constants.labelGroupClass) + .data(sliderOpts.steps); + + sliderLabels.enter().append('g') + .classed(constants.labelGroupClass, true); + + // loop over fake buttons to find width / height + var maxLabelWidth = 0; + var labelHeight = 0; + sliderLabels.each(function(stepOpts) { + var labelGroup = d3.select(this); + + var text = drawLabel(labelGroup, {step: stepOpts}, sliderOpts); + + var tWidth = text.node() && Drawing.bBox(text.node()).width; + + // This just overwrites with the last. Which is fine as long as + // the bounding box (probably incorrectly) measures the text *on + // a single line*: + labelHeight = text.node() && Drawing.bBox(text.node()).height; + + maxLabelWidth = Math.max(maxLabelWidth, tWidth); + }); + + sliderLabels.remove(); sliderOpts.inputAreaWidth = Math.max( constants.railWidth, @@ -109,7 +131,7 @@ function findDimensions(gd, sliderOpts) { sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); - if (sliderOpts.lenmode === 'fraction') { + if(sliderOpts.lenmode === 'fraction') { // fraction: sliderOpts.outerLength = Math.round(graphSize.w * sliderOpts.len); } else { @@ -127,8 +149,14 @@ function findDimensions(gd, sliderOpts) { sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); + var textableInputLength = sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset; + var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1); + var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; + sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); + sliderOpts.labelHeight = labelHeight; + // Hard-code this for now: - sliderOpts.height = 150; + sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.ypad * 2; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -165,59 +193,158 @@ function findDimensions(gd, sliderOpts) { }); } -function drawSlider(gd, group, sliderOpts) { +function drawSlider(gd, sliderGroup, sliderOpts) { // These are carefully ordered for proper z-ordering: - group + sliderGroup .call(drawRail, sliderOpts) - .call(drawTouchRect, sliderOpts) + .call(drawLabelGroup, sliderOpts) .call(drawTicks, sliderOpts) - .call(drawGrip, sliderOpts) - - group.call(setGripPosition, sliderOpts, 0); - group.call(attachFocusEvents, sliderOpts); + .call(drawTouchRect, gd, sliderOpts) + .call(drawGrip, gd, sliderOpts); // Position the rectangle: - Lib.setTranslate(group, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + + removeListeners(gd, sliderGroup, sliderOpts); + attachListeners(gd, sliderGroup, sliderOpts); + + setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true); +} + +function removeListeners(gd, sliderGroup, sliderOpts) { + var listeners = sliderOpts._input.listeners; + var eventNames = sliderOpts._input.eventNames; + if(!Array.isArray(listeners) || !Array.isArray(eventNames)) return; + while(listeners.length) { + gd._removeInternalListener(eventNames.pop(), listeners.pop()); + } +} + +function attachListeners(gd, sliderGroup, sliderOpts) { + var listeners = sliderOpts._input.listeners = []; + var eventNames = sliderOpts._input.eventNames = []; + + function makeListener(updatevalue) { + return function(data) { + var value = data; + if(updatevalue) { + value = Lib.nestedProperty(data, updatevalue).get(); + } + + setActiveByLabel(gd, sliderGroup, sliderOpts, value, false); + }; + } + + for(var i = 0; i < sliderOpts.updateevent.length; i++) { + var updateEventName = sliderOpts.updateevent[i]; + var updatevalue = sliderOpts.updatevalue; + + var updatelistener = makeListener(updatevalue); + + gd._internalEv.on(updateEventName, updatelistener); + + eventNames.push(updateEventName); + listeners.push(updatelistener); + } } -function drawGrip(sliderGroup, sliderOpts) { +function drawGrip(sliderGroup, gd, sliderOpts) { var grip = sliderGroup.selectAll('rect.' + constants.gripRectClass) .data([0]); grip.enter().append('rect') .classed(constants.gripRectClass, true) - .call(attachGripEvents, sliderGroup, sliderOpts) + .call(attachGripEvents, gd, sliderGroup, sliderOpts) .style('pointer-events', 'all'); grip.attr({ - width: constants.gripHeight, - height: constants.gripWidth, - rx: constants.gripRadius, - ry: constants.gripRadius, - }) + width: constants.gripHeight, + height: constants.gripWidth, + rx: constants.gripRadius, + ry: constants.gripRadius, + }) .call(Color.stroke, constants.gripBorderColor) .call(Color.fill, constants.gripBgColor) .style('stroke-width', constants.gripBorderWidth + 'px'); } -function handleInput(sliderGroup, sliderOpts, normalizedPosition) { +function drawLabel(item, data, sliderOpts) { + var text = item.selectAll('text') + .data([0]); + + text.enter().append('text') + .classed(constants.labelClass, true) + .classed('user-select-none', true) + .attr('text-anchor', 'middle'); + + text.call(Drawing.font, sliderOpts.font) + .text(data.step.label) + .call(svgTextUtils.convertToTspans); + + return text; +} + +function drawLabelGroup(sliderGroup, sliderOpts) { + var labels = sliderGroup.selectAll('g.' + constants.labelsClass) + .data([0]); + + labels.enter().append('g') + .classed(constants.labelsClass, true); + + var labelItems = labels.selectAll('g.' + constants.labelGroupClass) + .data(sliderOpts.labelSteps); + + labelItems.enter().append('g') + .classed(constants.labelGroupClass, true); + + labelItems.exit().remove(); + + labelItems.each(function(d) { + var item = d3.select(this); + + item.call(drawLabel, d, sliderOpts); + + Lib.setTranslate(item, + normalizedValueToPosition(sliderOpts, d.fraction), + constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + ); + }); + +} + +function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition) { var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); - if (quantizedPosition !== sliderOpts._active) { - setActive(sliderGroup, sliderOpts, quantizedPosition); + if(quantizedPosition !== sliderOpts.active) { + setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true); } } -function setActive(sliderGroup, sliderOpts, active) { - sliderOpts._active = active; +function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback) { + var index; + for(var i = 0; i < sliderOpts.steps.length; i++) { + var step = sliderOpts.steps[i]; + if(step.label === label) { + index = i; + break; + } + } - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts._active / (sliderOpts.steps.length - 1)); + if(index !== undefined) { + setActive(gd, sliderGroup, sliderOpts, index, doCallback); + } +} + +function setActive(gd, sliderGroup, sliderOpts, index, doCallback) { + sliderOpts._input.active = sliderOpts.active = index; - var step = sliderOpts.steps[sliderOpts._active]; + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1)); - if (step && step.method) { + var step = sliderOpts.steps[sliderOpts.active]; + + if(step && step.method && doCallback) { var args = step.args; - Plotly[step.method](gd, args[0], args[1], args[2]).catch(function(msg) { + Plotly[step.method](gd, args[0], args[1], args[2]).catch(function() { // This is not a disaster. Some methods like `animate` reject if interrupted // and *should* nicely log a warning. Lib.warn('Warning: Plotly.' + step.method + ' was called and rejected.'); @@ -225,71 +352,70 @@ function setActive(sliderGroup, sliderOpts, active) { } } -function attachFocusEvents(sliderGroup, sliderOpts) { - sliderGroup.on('focus', function() { - }).on('blur', function() { - }); -} - -function attachGripEvents(item, sliderGroup, sliderOpts) { - var gd = d3.select(sliderOpts._gd); +function attachGripEvents(item, gd, sliderGroup, sliderOpts) { var node = sliderGroup.node(); + var $gd = d3.select(gd); - item.on('mousedown', function(event) { + item.on('mousedown', function() { var grip = sliderGroup.select('.' + constants.gripRectClass); d3.event.stopPropagation(); d3.event.preventDefault(); - grip.call(Color.fill, constants.gripBgActiveColor) + grip.call(Color.fill, constants.gripBgActiveColor); var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); - gd.on('mousemove', function() { + $gd.on('mousemove', function() { var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); }); - gd.on('mouseup', function() { - grip.call(Color.fill, constants.gripBgColor) - gd.on('mouseup', null); - gd.on('mousemove', null); + $gd.on('mouseup', function() { + grip.call(Color.fill, constants.gripBgColor); + $gd.on('mouseup', null); + $gd.on('mousemove', null); }); }); } function drawTicks(sliderGroup, sliderOpts) { var tick = sliderGroup.selectAll('rect.' + constants.tickRectClass) - .data(sliderOpts.displayedSteps); + .data(sliderOpts.steps); tick.enter().append('rect') - .classed(constants.tickRectClass, true) + .classed(constants.tickRectClass, true); + + tick.exit().remove(); tick.attr({ - width: constants.tickWidth, - height: constants.tickLength, - 'shape-rendering': 'crispEdges' - }) - .call(Color.fill, constants.tickColor); - - tick.each(function (d, i) { - Lib.setTranslate( - d3.select(this), - normalizedValueToPosition(sliderOpts, d.fraction) - 0.5 * constants.tickWidth, + width: constants.tickWidth, + 'shape-rendering': 'crispEdges' + }); + + tick.each(function(d, i) { + var isMajor = i % sliderOpts.labelStride === 0; + var item = d3.select(this); + + item + .attr({height: isMajor ? constants.tickLength : constants.minorTickLength}) + .call(Color.fill, isMajor ? constants.tickColor : constants.minorTickColor); + + Lib.setTranslate(item, + normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * constants.tickWidth, constants.tickOffset ); }); } -function computeDisplayedSteps(sliderOpts) { - sliderOpts.displayedSteps = []; +function computeLabelSteps(sliderOpts) { + sliderOpts.labelSteps = []; var i0 = 0; - var step = 1; var nsteps = sliderOpts.steps.length; - for (var i = i0; i < nsteps; i += step) { - sliderOpts.displayedSteps.push({ + for(var i = i0; i < nsteps; i += sliderOpts.labelStride) { + sliderOpts.labelSteps.push({ fraction: i / (nsteps - 1), step: sliderOpts.steps[i] }); @@ -314,21 +440,21 @@ function positionToNormalizedValue(sliderOpts, position) { return Math.min(1, Math.max(0, (position - sliderOpts.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset - 2 * sliderOpts.inputAreaStart))); } -function drawTouchRect(sliderGroup, sliderOpts) { +function drawTouchRect(sliderGroup, gd, sliderOpts) { var rect = sliderGroup.selectAll('rect.' + constants.railTouchRectClass) .data([0]); rect.enter().append('rect') .classed(constants.railTouchRectClass, true) - .call(attachGripEvents, sliderGroup, sliderOpts) + .call(attachGripEvents, gd, sliderGroup, sliderOpts) .style('pointer-events', 'all'); rect.attr({ - width: sliderOpts.inputAreaLength, - height: sliderOpts.inputAreaWidth - }) + width: sliderOpts.inputAreaLength, + height: sliderOpts.inputAreaWidth + }) .call(Color.fill, constants.gripBgColor) - .attr('opacity', 0) + .attr('opacity', 0); Lib.setTranslate(rect, 0, 0); } @@ -338,17 +464,17 @@ function drawRail(sliderGroup, sliderOpts) { .data([0]); rect.enter().append('rect') - .classed(constants.railRectClass, true) + .classed(constants.railRectClass, true); var computedLength = sliderOpts.inputAreaLength - sliderOpts.railInset * 2; rect.attr({ - width: computedLength, - height: constants.railWidth, - rx: constants.railRadius, - ry: constants.railRadius, - 'shape-rendering': 'crispEdges' - }) + width: computedLength, + height: constants.railWidth, + rx: constants.railRadius, + ry: constants.railRadius, + 'shape-rendering': 'crispEdges' + }) .call(Color.stroke, constants.railBorderColor) .call(Color.fill, constants.railBgColor) .style('stroke-width', '1px'); @@ -356,3 +482,15 @@ function drawRail(sliderGroup, sliderOpts) { Lib.setTranslate(rect, sliderOpts.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); } +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.autoMarginIdRoot) !== -1) { + Plots.autoMargin(gd, k); + } + } +} From 8d7c40d95566a6d71c876ad762afddc7bc2b7986 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 03:35:55 -0400 Subject: [PATCH 16/40] Add slider transitions --- src/components/slider/attributes.js | 18 +++++++++++++ src/components/slider/defaults.js | 7 ++++++ src/components/slider/draw.js | 39 ++++++++++++++++++----------- 3 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/components/slider/attributes.js b/src/components/slider/attributes.js index 2f937f5afe5..a26f2909f7c 100644 --- a/src/components/slider/attributes.js +++ b/src/components/slider/attributes.js @@ -11,6 +11,7 @@ var fontAttrs = require('../../plots/font_attributes'); var colorAttrs = require('../color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; +var animationAttrs = require('../../plots/animation_attributes'); var stepsAttrs = { _isLinkedToArray: true, @@ -178,6 +179,23 @@ module.exports = { ].join(' ') }, + transition: { + duration: { + valType: 'number', + role: 'info', + min: 0, + dflt: 150, + description: 'Sets the duration of the slider transition' + }, + easing: { + valType: 'enumerated', + values: animationAttrs.transition.easing.values, + role: 'info', + dflt: 'cubic-in-out', + description: 'Sets the easing function of the slider transition' + }, + }, + font: extendFlat({}, fontAttrs, { description: 'Sets the font of the slider button text.' }), diff --git a/src/components/slider/defaults.js b/src/components/slider/defaults.js index fd5503b3b1a..776ac8039d9 100644 --- a/src/components/slider/defaults.js +++ b/src/components/slider/defaults.js @@ -66,6 +66,13 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('updateevent'); coerce('updatevalue'); + if(!sliderIn.transition) { + sliderIn.transition = {}; + } + + coerce('transition.duration'); + coerce('transition.easing'); + if(!Array.isArray(sliderOut.updateevent)) { sliderOut.updateevent = [sliderOut.updateevent]; } diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 417c63dedce..093d8c7d5ea 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -208,7 +208,7 @@ function drawSlider(gd, sliderGroup, sliderOpts) { removeListeners(gd, sliderGroup, sliderOpts); attachListeners(gd, sliderGroup, sliderOpts); - setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true); + setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true, false); } function removeListeners(gd, sliderGroup, sliderOpts) { @@ -224,14 +224,14 @@ function attachListeners(gd, sliderGroup, sliderOpts) { var listeners = sliderOpts._input.listeners = []; var eventNames = sliderOpts._input.eventNames = []; - function makeListener(updatevalue) { + function makeListener(eventname, updatevalue) { return function(data) { var value = data; if(updatevalue) { value = Lib.nestedProperty(data, updatevalue).get(); } - setActiveByLabel(gd, sliderGroup, sliderOpts, value, false); + setActiveByLabel(gd, sliderGroup, sliderOpts, value, false, true); }; } @@ -239,7 +239,7 @@ function attachListeners(gd, sliderGroup, sliderOpts) { var updateEventName = sliderOpts.updateevent[i]; var updatevalue = sliderOpts.updatevalue; - var updatelistener = makeListener(updatevalue); + var updatelistener = makeListener(updateEventName, updatevalue); gd._internalEv.on(updateEventName, updatelistener); @@ -312,15 +312,16 @@ function drawLabelGroup(sliderGroup, sliderOpts) { } -function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition) { +function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransition) { var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); + if(quantizedPosition !== sliderOpts.active) { - setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true); + setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true, doTransition); } } -function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback) { +function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback, doTransition) { var index; for(var i = 0; i < sliderOpts.steps.length; i++) { var step = sliderOpts.steps[i]; @@ -331,14 +332,14 @@ function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback) { } if(index !== undefined) { - setActive(gd, sliderGroup, sliderOpts, index, doCallback); + setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition); } } -function setActive(gd, sliderGroup, sliderOpts, index, doCallback) { +function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) { sliderOpts._input.active = sliderOpts.active = index; - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1)); + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); var step = sliderOpts.steps[sliderOpts.active]; @@ -364,11 +365,11 @@ function attachGripEvents(item, gd, sliderGroup, sliderOpts) { grip.call(Color.fill, constants.gripBgActiveColor); var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, true); $gd.on('mousemove', function() { var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); - handleInput(gd, sliderGroup, sliderOpts, normalizedPosition); + handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, false); }); $gd.on('mouseup', function() { @@ -422,11 +423,21 @@ function computeLabelSteps(sliderOpts) { } } -function setGripPosition(sliderGroup, sliderOpts, position) { +function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { var grip = sliderGroup.select('rect.' + constants.gripRectClass); var x = normalizedValueToPosition(sliderOpts, position); - Lib.setTranslate(grip, x - constants.gripWidth * 0.5, 0); + + var el = grip; + if(doTransition && sliderOpts.transition.duration > 0) { + el = el.transition() + .duration(sliderOpts.transition.duration) + .ease(sliderOpts.transition.easing); + } + + // Lib.setTranslate doesn't work here becasue of the transition duck-typing. + // It's also not necessary because there are no other transitions to preserve. + el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + 0 + ')'); } // Convert a number from [0-1] to a pixel position relative to the slider group container: From 30b5f2dffb0358c43afc85f3eae968d180055515 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 03:45:30 -0400 Subject: [PATCH 17/40] Expand touchable slider area --- src/components/slider/draw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 093d8c7d5ea..7fe34a070bf 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -462,10 +462,10 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { rect.attr({ width: sliderOpts.inputAreaLength, - height: sliderOpts.inputAreaWidth + height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + constants.tickLength + sliderOpts.labelHeight) }) .call(Color.fill, constants.gripBgColor) - .attr('opacity', 0); + .attr('opacity', 0.0); Lib.setTranslate(rect, 0, 0); } From 19442d9eceb2d98677913ba8abe80a4828390e05 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 10:46:46 -0400 Subject: [PATCH 18/40] Add slider mock --- src/components/slider/draw.js | 4 +- test/image/mocks/slider.json | 109 ++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 test/image/mocks/slider.json diff --git a/src/components/slider/draw.js b/src/components/slider/draw.js index 7fe34a070bf..e70da914794 100644 --- a/src/components/slider/draw.js +++ b/src/components/slider/draw.js @@ -258,8 +258,8 @@ function drawGrip(sliderGroup, gd, sliderOpts) { .style('pointer-events', 'all'); grip.attr({ - width: constants.gripHeight, - height: constants.gripWidth, + width: constants.gripWidth, + height: constants.gripHeight, rx: constants.gripRadius, ry: constants.gripRadius, }) diff --git a/test/image/mocks/slider.json b/test/image/mocks/slider.json new file mode 100644 index 00000000000..60d665ce38f --- /dev/null +++ b/test/image/mocks/slider.json @@ -0,0 +1,109 @@ +{ + "data": [ + { + "x": [0, 1, 2], + "y": [0.5, 1, 2.5] + } + ], + "layout": { + "sliders": [{ + "active": 2, + "steps": [{ + "label": "red", + "method": "restyle", + "args": [{"line.color": "red"}] + }, { + "label": "orange", + "method": "restyle", + "args": [{"line.color": "orange"}] + }, { + "label": "yellow", + "method": "restyle", + "args": [{"line.color": "yellow"}] + }, { + "label": "green", + "method": "restyle", + "args": [{"line.color": "green"}] + }, { + "label": "blue", + "method": "restyle", + "args": [{"line.color": "blue"}] + }, { + "label": "purple", + "method": "restyle", + "args": [{"line.color": "purple"}] + }], + "visible": true, + "x": 0.5, + "len": 0.5, + "xanchor": "right", + "y": -0.1, + "yanchor": "top", + + "transition": { + "duration": 150, + "easing": "cubic-in-out" + }, + + "xpad": 20, + "ypad": 30, + + "font": {} + }, { + "active": 4, + "steps": [{ + "label": "red", + "method": "restyle", + "args": [{"marker.color": "red"}] + }, { + "label": "orange", + "method": "restyle", + "args": [{"marker.color": "orange"}] + }, { + "label": "yellow", + "method": "restyle", + "args": [{"marker.color": "yellow"}] + }, { + "label": "green", + "method": "restyle", + "args": [{"marker.color": "green"}] + }, { + "label": "blue", + "method": "restyle", + "args": [{"marker.color": "blue"}] + }, { + "label": "purple", + "method": "restyle", + "args": [{"marker.color": "purple"}] + }], + "visible": true, + "x": 0.5, + "len": 0.5, + "xanchor": "left", + "y": -0.1, + "yanchor": "top", + + "transition": { + "duration": 150, + "easing": "cubic-in-out" + }, + + "xpad": 20, + "ypad": 30, + + "font": {} + }], + "xaxis": { + "range": [0, 2], + "autorange": true + }, + "yaxis": { + "type": "linear", + "range": [0, 3], + "autorange": true + }, + "height": 450, + "width": 1100, + "autosize": true + } +} From ecb7e878d941bbc65660a5469fb13391dc1a0c98 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 11:57:01 -0400 Subject: [PATCH 19/40] Rename sliders and bump circular require tolerance --- src/components/{slider => sliders}/attributes.js | 0 src/components/{slider => sliders}/constants.js | 0 src/components/{slider => sliders}/defaults.js | 0 src/components/{slider => sliders}/draw.js | 3 ++- src/components/{slider => sliders}/index.js | 2 +- src/core.js | 2 +- src/plot_api/plot_api.js | 13 +++++++++++-- src/plotly.js | 2 +- tasks/test_syntax.js | 2 +- 9 files changed, 17 insertions(+), 7 deletions(-) rename src/components/{slider => sliders}/attributes.js (100%) rename src/components/{slider => sliders}/constants.js (100%) rename src/components/{slider => sliders}/defaults.js (100%) rename src/components/{slider => sliders}/draw.js (99%) rename src/components/{slider => sliders}/index.js (93%) diff --git a/src/components/slider/attributes.js b/src/components/sliders/attributes.js similarity index 100% rename from src/components/slider/attributes.js rename to src/components/sliders/attributes.js diff --git a/src/components/slider/constants.js b/src/components/sliders/constants.js similarity index 100% rename from src/components/slider/constants.js rename to src/components/sliders/constants.js diff --git a/src/components/slider/defaults.js b/src/components/sliders/defaults.js similarity index 100% rename from src/components/slider/defaults.js rename to src/components/sliders/defaults.js diff --git a/src/components/slider/draw.js b/src/components/sliders/draw.js similarity index 99% rename from src/components/slider/draw.js rename to src/components/sliders/draw.js index e70da914794..63872689f99 100644 --- a/src/components/slider/draw.js +++ b/src/components/sliders/draw.js @@ -155,7 +155,6 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - // Hard-code this for now: sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.ypad * 2; var xanchor = 'left'; @@ -205,6 +204,8 @@ function drawSlider(gd, sliderGroup, sliderOpts) { // Position the rectangle: Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + // Every time the slider is draw from scratch, just detach and reattach the event listeners. + // This could perhaps be avoided. removeListeners(gd, sliderGroup, sliderOpts); attachListeners(gd, sliderGroup, sliderOpts); diff --git a/src/components/slider/index.js b/src/components/sliders/index.js similarity index 93% rename from src/components/slider/index.js rename to src/components/sliders/index.js index 389368c4908..28e755fd68f 100644 --- a/src/components/slider/index.js +++ b/src/components/sliders/index.js @@ -11,7 +11,7 @@ exports.moduleType = 'component'; -exports.name = 'slider'; +exports.name = 'sliders'; exports.layoutAttributes = require('./attributes'); diff --git a/src/core.js b/src/core.js index 96931ac487b..c7da95352ce 100644 --- a/src/core.js +++ b/src/core.js @@ -58,7 +58,7 @@ exports.register([ require('./components/shapes'), require('./components/images'), require('./components/updatemenus'), - require('./components/slider'), + require('./components/sliders'), require('./components/rangeslider'), require('./components/rangeselector') ]); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 51867a84881..9784f00d5da 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -178,7 +178,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('legend', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); - Registry.getComponentMethod('slider', 'draw')(gd); + Registry.getComponentMethod('sliders', 'draw')(gd); for(i = 0; i < calcdata.length; i++) { cd = calcdata[i]; @@ -304,7 +304,7 @@ Plotly.plot = function(gd, data, layout, config) { Registry.getComponentMethod('rangeslider', 'draw')(gd); Registry.getComponentMethod('rangeselector', 'draw')(gd); Registry.getComponentMethod('updatemenus', 'draw')(gd); - Registry.getComponentMethod('slider', 'draw')(gd); + Registry.getComponentMethod('sliders', 'draw')(gd); } function cleanUp() { @@ -1946,6 +1946,15 @@ function _relayout(gd, aobj) { for(i = 0; i < diff; i++) menus.push({}); flags.doplot = true; } + else if(p.parts[0] === 'sliders') { + Lib.extendDeepAll(gd.layout, Lib.objectFromPath(ai, vi)); + + var sliders = gd._fullLayout.sliders || []; + diff = (p.parts[2] + 1) - sliders.length; + + for(i = 0; i < diff; i++) sliders.push({}); + flags.doplot = true; + } // alter gd.layout else { // check whether we can short-circuit a full redraw diff --git a/src/plotly.js b/src/plotly.js index 02943617655..899696c639c 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -37,7 +37,7 @@ exports.Shapes = require('./components/shapes'); exports.Legend = require('./components/legend'); exports.Images = require('./components/images'); exports.UpdateMenus = require('./components/updatemenus'); -exports.Slider = require('./components/slider'); +exports.Sliders = require('./components/sliders'); exports.ModeBar = require('./components/modebar'); // plot api diff --git a/tasks/test_syntax.js b/tasks/test_syntax.js index 76d9f631f30..b1ffa7494a9 100644 --- a/tasks/test_syntax.js +++ b/tasks/test_syntax.js @@ -105,7 +105,7 @@ function assertCircularDeps() { // as of v1.17.0 - 2016/09/08 // see https://github.com/plotly/plotly.js/milestone/9 // for more details - var MAX_ALLOWED_CIRCULAR_DEPS = 33; + var MAX_ALLOWED_CIRCULAR_DEPS = 34; if(circularDeps.length > MAX_ALLOWED_CIRCULAR_DEPS) { logs.push('some new circular dependencies were added to src/'); From 9c0f9ac790bd0749e06760f03cc82c7cc4fce1e2 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 29 Sep 2016 12:15:50 -0400 Subject: [PATCH 20/40] Add trbl padding to sliders Conflicts: src/plots/pad_attributes.js --- src/components/sliders/attributes.js | 29 ++++++++++------------------ src/components/sliders/defaults.js | 6 ++++-- src/components/sliders/draw.js | 6 +++--- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index a26f2909f7c..49d0f2b647b 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -9,6 +9,7 @@ 'use strict'; var fontAttrs = require('../../plots/font_attributes'); +var padAttrs = require('../../plots/pad_attributes'); var colorAttrs = require('../color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var animationAttrs = require('../../plots/animation_attributes'); @@ -22,7 +23,7 @@ var stepsAttrs = { dflt: 'restyle', role: 'info', description: [ - 'Sets the Plotly method to be called on click.' + 'Sets the Plotly method to be called when the slider value is changed.' ].join(' ') }, args: { @@ -95,10 +96,11 @@ module.exports = { 'value when an event of type `updateevent` is received. If', 'undefined, the data argument itself is used. If a string,', 'that property is used, and if a string with dots, e.g.', - '`item.0.label`, then `data[\'item\'][0][\'label\']` will', - 'be used. If an array, it is matched to the respective', - 'updateevent item or if there is no corresponding updatevalue', - 'for a particular updateevent, it is interpreted as `undefined` and defaults to the data property itself.' + '`item.0.label`, then `data[0].label` is used. If an array,', + 'it is matched to the respective updateevent item or if there', + 'is no corresponding updatevalue for a particular updateevent,', + 'it is interpreted as `undefined` and defaults to the data', + 'property itself.' ].join(' ') }, @@ -134,20 +136,9 @@ module.exports = { role: 'style', description: 'Sets the x position (in normalized coordinates) of the slider.' }, - xpad: { - valType: 'number', - min: 0, - dflt: 10, - role: 'style', - description: 'Sets the amount of padding (in px) along the x direction' - }, - ypad: { - valType: 'number', - min: 0, - dflt: 10, - role: 'style', - description: 'Sets the amount of padding (in px) along the x direction' - }, + pad: extendFlat({}, padAttrs, { + description: 'Set the padding of the slider component along each side.' + }), xanchor: { valType: 'enumerated', values: ['auto', 'left', 'center', 'right'], diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 776ac8039d9..58a0f25310a 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -60,8 +60,10 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('len'); coerce('lenmode'); - coerce('xpad'); - coerce('ypad'); + coerce('pad.t'); + coerce('pad.r'); + coerce('pad.b'); + coerce('pad.l'); coerce('updateevent'); coerce('updatevalue'); diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 63872689f99..d826a6d4c2e 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -145,7 +145,7 @@ function findDimensions(gd, sliderOpts) { // The length of the rail, *excluding* padding on either end: sliderOpts.inputAreaStart = 0; - sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.xpad * 2); + sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); @@ -155,7 +155,7 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.ypad * 2; + sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -202,7 +202,7 @@ function drawSlider(gd, sliderGroup, sliderOpts) { .call(drawGrip, gd, sliderOpts); // Position the rectangle: - Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.xpad, sliderOpts.ly + sliderOpts.ypad); + Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.pad.l, sliderOpts.ly + sliderOpts.pad.t); // Every time the slider is draw from scratch, just detach and reattach the event listeners. // This could perhaps be avoided. From 2a6f9877c18cd9bfb05c4af53c56ce6373d23f23 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 12:37:09 -0400 Subject: [PATCH 21/40] Remove fanciness from slider constants --- src/components/sliders/constants.js | 28 ++++++++++++++-------------- src/components/sliders/draw.js | 22 ++++++++++------------ 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index 6593b502b7e..ddfe2bf8cd4 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -48,16 +48,6 @@ module.exports = { // font size to height scale fontSizeToHeight: 1.3, - // item rect radii - rx: 2, - ry: 2, - - // item text x offset off left edge - textOffsetX: 12, - - // item text y offset (w.r.t. middle) - textOffsetY: 3, - // arrow offset off right edge arrowOffsetX: 4, @@ -67,6 +57,16 @@ module.exports = { railBorderColor: '#bec8d9', railBgColor: '#ebedf0', + // The distance of the rail from the edge of the touchable area + // Slightly less than the step inset because of the curved edges + // of the rail + railInset: 8, + + // The distance from the extremal tick marks to the edge of the + // touchable area. This is basically the same as the grip radius, + // but for other styles it wouldn't really need to be. + stepInset: 10, + gripRadius: 10, gripWidth: 20, gripHeight: 20, @@ -76,15 +76,15 @@ module.exports = { gripBgColor: '#ebedf0', gripBgActiveColor: '#dbdde0', - // Padding in the direction perpendicular to the length of the rail: - // (which, at the moment is always vertical, but for the sake of the future...) - widthPadding: 10, + labelPadding: 8, + labelOffset: 0, - labelPadding: 4, tickWidth: 1, tickColor: '#333', tickOffset: 25, tickLength: 7, + + minorTickOffset: 25, minorTickColor: '#333', minorTickLength: 4, }; diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index d826a6d4c2e..692f4a2d0b1 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -146,16 +146,14 @@ function findDimensions(gd, sliderOpts) { // The length of the rail, *excluding* padding on either end: sliderOpts.inputAreaStart = 0; sliderOpts.inputAreaLength = Math.round(sliderOpts.outerLength - sliderOpts.pad.l - sliderOpts.pad.r); - sliderOpts.railInset = Math.round(Math.max(0, constants.gripWidth - constants.railWidth) * 0.5); - sliderOpts.stepInset = Math.round(Math.max(sliderOpts.railInset, constants.gripWidth * 0.5)); - var textableInputLength = sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset; + var textableInputLength = sliderOpts.inputAreaLength - 2 * constants.stepInset; var availableSpacePerLabel = textableInputLength / (sliderOpts.steps.length - 1); var computedSpacePerLabel = maxLabelWidth + constants.labelPadding; sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - sliderOpts.height = constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; + sliderOpts.height = constants.tickOffset + constants.tickLength + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -307,7 +305,7 @@ function drawLabelGroup(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, d.fraction), - constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.labelOffset ); }); @@ -405,7 +403,7 @@ function drawTicks(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * constants.tickWidth, - constants.tickOffset + isMajor ? constants.tickOffset : constants.minorTickOffset ); }); @@ -443,13 +441,13 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { // Convert a number from [0-1] to a pixel position relative to the slider group container: function normalizedValueToPosition(sliderOpts, normalizedPosition) { - return sliderOpts.inputAreaStart + sliderOpts.stepInset + - (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); + return sliderOpts.inputAreaStart + constants.stepInset + + (sliderOpts.inputAreaLength - 2 * constants.stepInset) * Math.min(1, Math.max(0, normalizedPosition)); } // Convert a position relative to the slider group to a nubmer in [0, 1] function positionToNormalizedValue(sliderOpts, position) { - return Math.min(1, Math.max(0, (position - sliderOpts.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * sliderOpts.stepInset - 2 * sliderOpts.inputAreaStart))); + return Math.min(1, Math.max(0, (position - constants.stepInset - sliderOpts.inputAreaStart) / (sliderOpts.inputAreaLength - 2 * constants.stepInset - 2 * sliderOpts.inputAreaStart))); } function drawTouchRect(sliderGroup, gd, sliderOpts) { @@ -466,7 +464,7 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + constants.tickLength + sliderOpts.labelHeight) }) .call(Color.fill, constants.gripBgColor) - .attr('opacity', 0.0); + .attr('opacity', 0); Lib.setTranslate(rect, 0, 0); } @@ -478,7 +476,7 @@ function drawRail(sliderGroup, sliderOpts) { rect.enter().append('rect') .classed(constants.railRectClass, true); - var computedLength = sliderOpts.inputAreaLength - sliderOpts.railInset * 2; + var computedLength = sliderOpts.inputAreaLength - constants.railInset * 2; rect.attr({ width: computedLength, @@ -491,7 +489,7 @@ function drawRail(sliderGroup, sliderOpts) { .call(Color.fill, constants.railBgColor) .style('stroke-width', '1px'); - Lib.setTranslate(rect, sliderOpts.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); + Lib.setTranslate(rect, constants.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); } function clearPushMargins(gd) { From 5bd6bddc3ed558729e84c2afc5ff9b8a5909e0aa Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 28 Sep 2016 13:31:18 -0400 Subject: [PATCH 22/40] Tweak slider behavior to avoid event -> method loops --- src/components/sliders/draw.js | 44 ++++++++++++++---- test/image/baselines/sliders.png | Bin 0 -> 22241 bytes .../image/mocks/{slider.json => sliders.json} | 12 +++-- 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 test/image/baselines/sliders.png rename test/image/mocks/{slider.json => sliders.json} (95%) diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 692f4a2d0b1..d616d854346 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -230,6 +230,11 @@ function attachListeners(gd, sliderGroup, sliderOpts) { value = Lib.nestedProperty(data, updatevalue).get(); } + // If it's *currently* invoking a command an event is received, + // then we'll ignore the event in order to avoid complicated + // invinite loops. + if(sliderOpts._invokingCommand) return; + setActiveByLabel(gd, sliderGroup, sliderOpts, value, false, true); }; } @@ -338,17 +343,38 @@ function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback, doTran function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) { sliderOpts._input.active = sliderOpts.active = index; - sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); - var step = sliderOpts.steps[sliderOpts.active]; + sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); + if(step && step.method && doCallback) { - var args = step.args; - Plotly[step.method](gd, args[0], args[1], args[2]).catch(function() { - // This is not a disaster. Some methods like `animate` reject if interrupted - // and *should* nicely log a warning. - Lib.warn('Warning: Plotly.' + step.method + ' was called and rejected.'); - }); + if(sliderGroup._nextMethod) { + // If we've already queued up an update, just overwrite it with the most recent: + sliderGroup._nextMethod.step = step; + sliderGroup._nextMethod.doCallback = doCallback; + sliderGroup._nextMethod.doTransition = doTransition; + } else { + sliderGroup._nextMethod = {step: step, doCallback: doCallback, doTransition: doTransition}; + sliderGroup._nextMethodRaf = window.requestAnimationFrame(function() { + var _step = sliderGroup._nextMethod.step; + var args = _step.args; + if(!_step.method) return; + + sliderOpts._invokingCommand = true; + Plotly[_step.method](gd, args[0], args[1], args[2]).then(function() { + sliderOpts._invokingCommand = false; + }, function() { + sliderOpts._invokingCommand = false; + + // This is not a disaster. Some methods like `animate` reject if interrupted + // and *should* nicely log a warning. + Lib.warn('Warning: Plotly.' + _step.method + ' was called and rejected.'); + }); + + sliderGroup._nextMethod = null; + sliderGroup._nextMethodRaf = null; + }); + } } } @@ -428,7 +454,7 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { var x = normalizedValueToPosition(sliderOpts, position); var el = grip; - if(doTransition && sliderOpts.transition.duration > 0) { + if(doTransition && sliderOpts.transition.duration > 0 && !sliderOpts._invokingCommand) { el = el.transition() .duration(sliderOpts.transition.duration) .ease(sliderOpts.transition.easing); diff --git a/test/image/baselines/sliders.png b/test/image/baselines/sliders.png new file mode 100644 index 0000000000000000000000000000000000000000..2a4f3b4f7fa9fbe0b6e1eb45c78ece348e2f6520 GIT binary patch literal 22241 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 literal 0 HcmV?d00001 diff --git a/test/image/mocks/slider.json b/test/image/mocks/sliders.json similarity index 95% rename from test/image/mocks/slider.json rename to test/image/mocks/sliders.json index 60d665ce38f..c632fa18535 100644 --- a/test/image/mocks/slider.json +++ b/test/image/mocks/sliders.json @@ -45,8 +45,10 @@ "easing": "cubic-in-out" }, - "xpad": 20, - "ypad": 30, + "pad": { + "r": 20, + "t": 20 + }, "font": {} }, { @@ -88,8 +90,10 @@ "easing": "cubic-in-out" }, - "xpad": 20, - "ypad": 30, + "pad": { + "l": 20, + "t": 20 + }, "font": {} }], From 4520a482cb8db661ad388de7a97c6072ae0ea088 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 29 Sep 2016 12:04:16 -0400 Subject: [PATCH 23/40] Add current value output to sliders --- src/components/sliders/attributes.js | 44 ++++++++++++++- src/components/sliders/constants.js | 5 ++ src/components/sliders/defaults.js | 10 ++-- src/components/sliders/draw.js | 84 ++++++++++++++++++++++++++-- 4 files changed, 132 insertions(+), 11 deletions(-) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 49d0f2b647b..7b8ed56835f 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -187,8 +187,50 @@ module.exports = { }, }, + currentvalue: { + visible: { + valType: 'boolean', + dflt: true, + description: [ + 'Shows the currently-selected value above the slider.' + ].join(' ') + }, + + xanchor: { + valType: 'enumerated', + values: ['left', 'center', 'right'], + dflt: 'left', + description: [ + 'The alignment of the value readout relative to the length of the slider.' + ].join(' ') + }, + + offset: { + valType: 'number', + dflt: 10, + role: 'info', + description: [ + 'The amount of space, in pixels, between the current value label', + 'and the slider.' + ] + }, + + prefix: { + valType: 'string', + role: 'info', + description: [ + 'When `currentvalue.visible` is true, this sets the prefix of the lable. If provided,', + 'it will be joined to the current value with a single space between.' + ].join(' ') + }, + + font: extendFlat({}, fontAttrs, { + description: 'Sets the font of the current value lable text.' + }), + }, + font: extendFlat({}, fontAttrs, { - description: 'Sets the font of the slider button text.' + description: 'Sets the font of the slider step labels.' }), bgcolor: { diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index ddfe2bf8cd4..90b97c3cb99 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -28,6 +28,7 @@ module.exports = { labelsClass: 'slider-labels', labelGroupClass: 'slider-label-group', labelClass: 'slider-label', + currentValueClass: 'slider-current-value', railHeight: 5, @@ -87,4 +88,8 @@ module.exports = { minorTickOffset: 25, minorTickColor: '#333', minorTickLength: 4, + + // Extra space below the current value label: + currentValuePadding: 8, + currentValueInset: 0, }; diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 58a0f25310a..0515e66bc79 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -65,13 +65,14 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('pad.b'); coerce('pad.l'); + coerce('currentvalue.visible'); + coerce('currentvalue.xanchor'); + coerce('currentvalue.prefix'); + coerce('currentvalue.offset'); + coerce('updateevent'); coerce('updatevalue'); - if(!sliderIn.transition) { - sliderIn.transition = {}; - } - coerce('transition.duration'); coerce('transition.easing'); @@ -84,6 +85,7 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { } Lib.coerceFont(coerce, 'font', layoutOut.font); + Lib.coerceFont(coerce, 'currentvalue.font', layoutOut.font); coerce('bgcolor', layoutOut.paper_bgcolor); coerce('bordercolor'); diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index d616d854346..0d4dfc33854 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -127,6 +127,26 @@ function findDimensions(gd, sliderOpts) { constants.gripHeight ); + sliderOpts.currentValueMaxWidth = 0; + sliderOpts.currentValueHeight = 0; + sliderOpts.currentValueTotalHeight = 0; + + if(sliderOpts.currentvalue.visible) { + // Get the dimensions of the current value label: + var dummyGroup = gd._tester.append('g'); + + sliderLabels.each(function(stepOpts) { + var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label); + var curValSize = curValPrefix.node() && Drawing.bBox(curValPrefix.node()); + sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width)); + sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height)); + }); + + sliderOpts.currentValueTotalHeight = sliderOpts.currentValueHeight + sliderOpts.currentvalue.offset; + + dummyGroup.remove(); + } + var graphSize = gd._fullLayout._size; sliderOpts.lx = graphSize.l + graphSize.w * sliderOpts.x; sliderOpts.ly = graphSize.t + graphSize.h * (1 - sliderOpts.y); @@ -153,7 +173,7 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - sliderOpts.height = constants.tickOffset + constants.tickLength + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; + sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + constants.tickLength + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -193,6 +213,7 @@ function findDimensions(gd, sliderOpts) { function drawSlider(gd, sliderGroup, sliderOpts) { // These are carefully ordered for proper z-ordering: sliderGroup + .call(drawCurrentValue, sliderOpts) .call(drawRail, sliderOpts) .call(drawLabelGroup, sliderOpts) .call(drawTicks, sliderOpts) @@ -210,6 +231,53 @@ function drawSlider(gd, sliderGroup, sliderOpts) { setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true, false); } +function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { + if(!sliderOpts.currentvalue.visible) return; + + var x0, textAnchor; + var text = sliderGroup.selectAll('text') + .data([0]); + + switch(sliderOpts.currentvalue.xanchor) { + case 'right': + // This is anchored left and adjusted by the width of the longest label + // so that the prefix doesn't move. The goal of this is to emphasize + // what's actually changing and make the update less distracting. + x0 = sliderOpts.inputAreaLength - constants.currentValueInset - sliderOpts.currentValueMaxWidth; + textAnchor = 'left'; + break; + case 'center': + x0 = sliderOpts.inputAreaLength * 0.5; + textAnchor = 'middle'; + break; + default: + x0 = constants.currentValueInset; + textAnchor = 'left'; + } + + text.enter().append('text') + .classed(constants.labelClass, true) + .classed('user-select-none', true) + .attr('text-anchor', textAnchor); + + var str = sliderOpts.currentvalue.prefix ? (sliderOpts.currentvalue.prefix + ' ') : ''; + + if(typeof valueOverride === 'string') { + str += valueOverride; + } else { + var curVal = sliderOpts.steps[sliderOpts.active].label; + str += curVal; + } + + text.call(Drawing.font, sliderOpts.currentvalue.font) + .text(str) + .call(svgTextUtils.convertToTspans); + + Lib.setTranslate(text, x0, sliderOpts.currentValueHeight); + + return text; +} + function removeListeners(gd, sliderGroup, sliderOpts) { var listeners = sliderOpts._input.listeners; var eventNames = sliderOpts._input.eventNames; @@ -310,7 +378,7 @@ function drawLabelGroup(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, d.fraction), - constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.labelOffset + constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.labelOffset + sliderOpts.currentValueTotalHeight ); }); @@ -346,6 +414,7 @@ function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) var step = sliderOpts.steps[sliderOpts.active]; sliderGroup.call(setGripPosition, sliderOpts, sliderOpts.active / (sliderOpts.steps.length - 1), doTransition); + sliderGroup.call(drawCurrentValue, sliderOpts); if(step && step.method && doCallback) { if(sliderGroup._nextMethod) { @@ -429,7 +498,7 @@ function drawTicks(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * constants.tickWidth, - isMajor ? constants.tickOffset : constants.minorTickOffset + (isMajor ? constants.tickOffset : constants.minorTickOffset) + sliderOpts.currentValueTotalHeight ); }); @@ -462,7 +531,7 @@ function setGripPosition(sliderGroup, sliderOpts, position, doTransition) { // Lib.setTranslate doesn't work here becasue of the transition duck-typing. // It's also not necessary because there are no other transitions to preserve. - el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + 0 + ')'); + el.attr('transform', 'translate(' + (x - constants.gripWidth * 0.5) + ',' + (sliderOpts.currentValueTotalHeight) + ')'); } // Convert a number from [0-1] to a pixel position relative to the slider group container: @@ -492,7 +561,7 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { .call(Color.fill, constants.gripBgColor) .attr('opacity', 0); - Lib.setTranslate(rect, 0, 0); + Lib.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight); } function drawRail(sliderGroup, sliderOpts) { @@ -515,7 +584,10 @@ function drawRail(sliderGroup, sliderOpts) { .call(Color.fill, constants.railBgColor) .style('stroke-width', '1px'); - Lib.setTranslate(rect, constants.railInset, (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5); + Lib.setTranslate(rect, + constants.railInset, + (sliderOpts.inputAreaWidth - constants.railWidth) * 0.5 + sliderOpts.currentValueTotalHeight + ); } function clearPushMargins(gd) { From 88d6728740de35de28c45ca9086c544320a436f2 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 29 Sep 2016 12:14:04 -0400 Subject: [PATCH 24/40] Tweak slider colors --- src/components/sliders/constants.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index 90b97c3cb99..004efabd7ef 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -56,7 +56,7 @@ module.exports = { railWidth: 5, railBorder: 4, railBorderColor: '#bec8d9', - railBgColor: '#ebedf0', + railBgColor: '#f8fafc', // The distance of the rail from the edge of the touchable area // Slightly less than the step inset because of the curved edges @@ -68,13 +68,13 @@ module.exports = { // but for other styles it wouldn't really need to be. stepInset: 10, - gripRadius: 10, + gripRadius: 3, gripWidth: 20, gripHeight: 20, gripBorder: 20, gripBorderWidth: 1, gripBorderColor: '#bec8d9', - gripBgColor: '#ebedf0', + gripBgColor: '#f6f8fa', gripBgActiveColor: '#dbdde0', labelPadding: 8, From 6da5b0f63db5a49ef9473ce030393c6506b371a0 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 29 Sep 2016 12:45:38 -0400 Subject: [PATCH 25/40] Fix sliders in plot schema --- src/plots/layout_attributes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index f0f9323adfd..44e051ee539 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -184,6 +184,6 @@ module.exports = { 'shapes': 'shapes', 'images': 'images', 'updatemenus': 'updatemenus', - 'slider': 'slider' + 'sliders': 'sliders' } }; From 4a9edd0daf8b19db4a5877059c3fd51053469711 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 29 Sep 2016 16:59:44 -0400 Subject: [PATCH 26/40] Working through slider testing issues --- src/components/sliders/attributes.js | 3 +- test/jasmine/tests/sliders_test.js | 149 +++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 test/jasmine/tests/sliders_test.js diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 7b8ed56835f..7b3565bbd27 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -200,6 +200,7 @@ module.exports = { valType: 'enumerated', values: ['left', 'center', 'right'], dflt: 'left', + role: 'info', description: [ 'The alignment of the value readout relative to the length of the slider.' ].join(' ') @@ -219,7 +220,7 @@ module.exports = { valType: 'string', role: 'info', description: [ - 'When `currentvalue.visible` is true, this sets the prefix of the lable. If provided,', + 'When currentvalue.visible is true, this sets the prefix of the lable. If provided,', 'it will be joined to the current value with a single space between.' ].join(' ') }, diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js new file mode 100644 index 00000000000..16a62aafec2 --- /dev/null +++ b/test/jasmine/tests/sliders_test.js @@ -0,0 +1,149 @@ +var Sliders = require('@src/components/sliders'); +// var constants = require('@src/components/sliders/constants'); + +// var d3 = require('d3'); +// var Plotly = require('@lib'); +// var Lib = require('@src/lib'); +// var createGraphDiv = require('../assets/create_graph_div'); +// var destroyGraphDiv = require('../assets/destroy_graph_div'); + +describe('sliders defaults', function() { + 'use strict'; + + var supply = Sliders.supplyLayoutDefaults; + + var layoutIn, layoutOut; + + beforeEach(function() { + layoutIn = {}; + layoutOut = {}; + }); + + it('should set \'visible\' to false when no steps are present', function() { + layoutIn.sliders = [{ + steps: [{ + method: 'relayout', + args: ['title', 'Hello World'] + }, { + method: 'update', + args: [ { 'marker.size': 20 }, { 'xaxis.range': [0, 10] }, [0, 1] ] + }, { + method: 'animate', + args: [ 'frame1', { transition: { duration: 500, ease: 'cubic-in-out' }}] + }] + }, { + bgcolor: 'red' + }, { + visible: false, + steps: [{ + method: 'relayout', + args: ['title', 'Hello World'] + }] + }]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].visible).toBe(true); + expect(layoutOut.sliders[0].active).toEqual(0); + expect(layoutOut.sliders[0].steps[0].args.length).toEqual(2); + expect(layoutOut.sliders[0].steps[1].args.length).toEqual(3); + expect(layoutOut.sliders[0].steps[2].args.length).toEqual(2); + + expect(layoutOut.sliders[1].visible).toBe(false); + expect(layoutOut.sliders[1].active).toBeUndefined(); + + expect(layoutOut.sliders[2].visible).toBe(false); + expect(layoutOut.sliders[2].active).toBeUndefined(); + }); + + it('should skip over non-object steps', function() { + layoutIn.sliders = [{ + steps: [ + null, + { + method: 'relayout', + args: ['title', 'Hello World'] + }, + 'remove' + ] + }]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].steps.length).toEqual(1); + expect(layoutOut.sliders[0].steps[0]).toEqual({ + method: 'relayout', + args: ['title', 'Hello World'], + label: '', + _index: 1 + }); + }); + + it('should skip over steps with array \'args\' field', function() { + layoutIn.sliders = [{ + steps: [{ + method: 'restyle', + }, { + method: 'relayout', + args: ['title', 'Hello World'] + }, { + method: 'relayout', + args: null + }, {}] + }]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].steps.length).toEqual(1); + expect(layoutOut.sliders[0].steps[0]).toEqual({ + method: 'relayout', + args: ['title', 'Hello World'], + label: '', + _index: 1 + }); + }); + + it('should keep ref to input update menu container', function() { + layoutIn.sliders = [{ + steps: [{ + method: 'relayout', + args: ['title', 'Hello World'] + }] + }, { + bgcolor: 'red' + }, { + visible: false, + steps: [{ + method: 'relayout', + args: ['title', 'Hello World'] + }] + }]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0]._input).toBe(layoutIn.sliders[0]); + expect(layoutOut.sliders[1]._input).toBe(layoutIn.sliders[1]); + expect(layoutOut.sliders[2]._input).toBe(layoutIn.sliders[2]); + }); + + it('should default \'bgcolor\' to layout \'paper_bgcolor\'', function() { + var steps = [{ + method: 'relayout', + args: ['title', 'Hello World'] + }]; + + layoutIn.sliders = [{ + steps: steps, + }, { + bgcolor: 'red', + steps: steps + }]; + + layoutOut.paper_bgcolor = 'blue'; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].bgcolor).toEqual('blue'); + expect(layoutOut.sliders[1].bgcolor).toEqual('red'); + }); +}); From 6cb6d4293ca0c16b5c4003802f3620efdec437e1 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 3 Oct 2016 11:10:32 -0400 Subject: [PATCH 27/40] Add slider tests --- src/components/sliders/attributes.js | 11 ++- src/components/sliders/constants.js | 2 +- src/components/sliders/defaults.js | 4 +- src/components/sliders/draw.js | 14 +-- test/jasmine/assets/fail_test.js | 3 + test/jasmine/tests/sliders_test.js | 129 ++++++++++++++++++++++++--- 6 files changed, 142 insertions(+), 21 deletions(-) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 7b3565bbd27..0fd4116f80b 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -43,8 +43,15 @@ var stepsAttrs = { label: { valType: 'string', role: 'info', - dflt: '', description: 'Sets the text label to appear on the slider' + }, + value: { + valType: 'string', + role: 'info', + description: [ + 'Sets the value of the slider step, used to refer to the step programatically.', + 'Defaults to the slider label if not provided.' + ].join(' ') } }; @@ -213,7 +220,7 @@ module.exports = { description: [ 'The amount of space, in pixels, between the current value label', 'and the slider.' - ] + ].join(' ') }, prefix: { diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index 004efabd7ef..e772428ada1 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -68,7 +68,7 @@ module.exports = { // but for other styles it wouldn't really need to be. stepInset: 10, - gripRadius: 3, + gripRadius: 10, gripWidth: 20, gripHeight: 20, gripBorder: 20, diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 0515e66bc79..f521e908fea 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -112,9 +112,9 @@ function stepsDefaults(sliderIn, sliderOut) { coerce('method'); coerce('args'); - coerce('label'); + coerce('label', 'step-' + i); + coerce('value', valueOut.label); - valueOut._index = i; valuesOut.push(valueOut); } diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 0d4dfc33854..a7aa77d7bcb 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -62,19 +62,22 @@ module.exports = function draw(gd) { } sliderGroups.each(function(sliderOpts) { + // If it has fewer than two options, it's not really a slider: + if(sliderOpts.steps.length < 2) return; + computeLabelSteps(sliderOpts); drawSlider(gd, d3.select(this), sliderOpts); - makeInputProxy(gd, d3.select(this), sliderOpts); + // makeInputProxy(gd, d3.select(this), sliderOpts); }); }; -function makeInputProxy(gd, sliderGroup, sliderOpts) { +/* function makeInputProxy(gd, sliderGroup, sliderOpts) { sliderOpts.inputProxy = gd._fullLayout._paperdiv.selectAll('input.' + constants.inputProxyClass) .data([0]); -} +}*/ // This really only just filters by visibility: function makeSliderData(fullLayout) { @@ -83,7 +86,8 @@ function makeSliderData(fullLayout) { for(var i = 0; i < contOpts.length; i++) { var item = contOpts[i]; - if(item.visible) sliderData.push(item); + if(!item.visible || !item.steps.length) continue; + sliderData.push(item); } return sliderData; @@ -228,7 +232,7 @@ function drawSlider(gd, sliderGroup, sliderOpts) { removeListeners(gd, sliderGroup, sliderOpts); attachListeners(gd, sliderGroup, sliderOpts); - setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, true, false); + setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, false, false); } function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { diff --git a/test/jasmine/assets/fail_test.js b/test/jasmine/assets/fail_test.js index 468a7640c59..32cb8a178f9 100644 --- a/test/jasmine/assets/fail_test.js +++ b/test/jasmine/assets/fail_test.js @@ -23,4 +23,7 @@ module.exports = function failTest(error) { } else { expect(error).toBeUndefined(); } + if(error && error.stack) { + console.error(error.stack); + } }; diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 16a62aafec2..8a5a97c48a1 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -1,11 +1,12 @@ var Sliders = require('@src/components/sliders'); -// var constants = require('@src/components/sliders/constants'); +var constants = require('@src/components/sliders/constants'); -// var d3 = require('d3'); -// var Plotly = require('@lib'); -// var Lib = require('@src/lib'); -// var createGraphDiv = require('../assets/create_graph_div'); -// var destroyGraphDiv = require('../assets/destroy_graph_div'); +var d3 = require('d3'); +var Plotly = require('@lib'); +var Lib = require('@src/lib'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var fail = require('../assets/fail_test'); describe('sliders defaults', function() { 'use strict'; @@ -56,6 +57,39 @@ describe('sliders defaults', function() { expect(layoutOut.sliders[2].active).toBeUndefined(); }); + it('should set the default values equal to the labels', function() { + layoutIn.sliders = [{ + steps: [{ + method: 'relayout', args: [], + label: 'Label #1', + value: 'label-1' + }, { + method: 'update', args: [], + label: 'Label #2' + }, { + method: 'animate', args: [], + value: 'lacks-label' + }] + }]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].steps.length).toEqual(3); + expect(layoutOut.sliders[0].steps).toEqual([{ + method: 'relayout', args: [], + label: 'Label #1', + value: 'label-1' + }, { + method: 'update', args: [], + label: 'Label #2', + value: 'Label #2' + }, { + method: 'animate', args: [], + label: 'step-2', + value: 'lacks-label' + }]); + }); + it('should skip over non-object steps', function() { layoutIn.sliders = [{ steps: [ @@ -74,12 +108,12 @@ describe('sliders defaults', function() { expect(layoutOut.sliders[0].steps[0]).toEqual({ method: 'relayout', args: ['title', 'Hello World'], - label: '', - _index: 1 + label: 'step-1', + value: 'step-1', }); }); - it('should skip over steps with array \'args\' field', function() { + it('should skip over steps with non-array \'args\' field', function() { layoutIn.sliders = [{ steps: [{ method: 'restyle', @@ -98,8 +132,8 @@ describe('sliders defaults', function() { expect(layoutOut.sliders[0].steps[0]).toEqual({ method: 'relayout', args: ['title', 'Hello World'], - label: '', - _index: 1 + label: 'step-1', + value: 'step-1', }); }); @@ -147,3 +181,76 @@ describe('sliders defaults', function() { expect(layoutOut.sliders[1].bgcolor).toEqual('red'); }); }); + +describe('update sliders interactions', function() { + 'use strict'; + + var mock = require('@mocks/sliders.json'); + + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('should draw only visible sliders', function(done) { + expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); + expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); + + Plotly.relayout(gd, 'sliders[0].visible', false).then(function() { + assertNodeCount('.' + constants.groupClassName, 1); + expect(gd._fullLayout._pushmargin['slider-0']).toBeUndefined(); + expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); + + return Plotly.relayout(gd, 'sliders[1]', null); + }) + .then(function() { + assertNodeCount('.' + constants.groupClassName, 0); + expect(gd._fullLayout._pushmargin['slider-0']).toBeUndefined(); + expect(gd._fullLayout._pushmargin['slider-1']).toBeUndefined(); + + return Plotly.relayout(gd, { + 'sliders[0].visible': true, + 'sliders[1].visible': true + }); + }).then(function() { + assertNodeCount('.' + constants.groupClassName, 1); + expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); + expect(gd._fullLayout._pushmargin['slider-1']).toBeUndefined(); + + return Plotly.relayout(gd, { + 'sliders[1]': { + steps: [{ + method: 'relayout', + args: ['title', 'new title'], + label: '1970' + }, { + method: 'relayout', + args: ['title', 'new title'], + label: '1971' + }] + } + }); + }) + .then(function() { + assertNodeCount('.' + constants.groupClassName, 2); + expect(gd._fullLayout._pushmargin['slider-0']).toBeDefined(); + expect(gd._fullLayout._pushmargin['slider-1']).toBeDefined(); + }) + .catch(fail).then(done); + }); + + function assertNodeCount(query, cnt) { + expect(d3.selectAll(query).size()).toEqual(cnt); + } + +}); From 4775a47e94aee888d730bca810e423e33a12da7b Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 3 Oct 2016 15:36:27 -0400 Subject: [PATCH 28/40] Test updateevent for sliders --- src/components/sliders/defaults.js | 16 +++++-- src/components/sliders/draw.js | 8 +++- test/jasmine/tests/sliders_test.js | 75 ++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index f521e908fea..d525195d77e 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -76,12 +76,20 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('transition.duration'); coerce('transition.easing'); - if(!Array.isArray(sliderOut.updateevent)) { - sliderOut.updateevent = [sliderOut.updateevent]; + if(sliderOut.updateevent) { + if(!Array.isArray(sliderOut.updateevent)) { + sliderOut.updateevent = [sliderOut.updateevent]; + } + } else { + sliderOut.updateevent = []; } - if(!Array.isArray(sliderOut.udpatevalue)) { - sliderOut.udpatevalue = [sliderOut.updatevalue]; + if(sliderOut.updatevalue) { + if(!Array.isArray(sliderOut.updatevalue)) { + sliderOut.updatevalue = [sliderOut.updatevalue]; + } + } else { + sliderOut.updatevalue = []; } Lib.coerceFont(coerce, 'font', layoutOut.font); diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index a7aa77d7bcb..ae012f34a8a 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -292,6 +292,10 @@ function removeListeners(gd, sliderGroup, sliderOpts) { } function attachListeners(gd, sliderGroup, sliderOpts) { + if(!sliderOpts.updateevent || !sliderOpts.updateevent.length) { + return; + } + var listeners = sliderOpts._input.listeners = []; var eventNames = sliderOpts._input.eventNames = []; @@ -304,7 +308,7 @@ function attachListeners(gd, sliderGroup, sliderOpts) { // If it's *currently* invoking a command an event is received, // then we'll ignore the event in order to avoid complicated - // invinite loops. + // infinite loops. if(sliderOpts._invokingCommand) return; setActiveByLabel(gd, sliderGroup, sliderOpts, value, false, true); @@ -313,7 +317,7 @@ function attachListeners(gd, sliderGroup, sliderOpts) { for(var i = 0; i < sliderOpts.updateevent.length; i++) { var updateEventName = sliderOpts.updateevent[i]; - var updatevalue = sliderOpts.updatevalue; + var updatevalue = (sliderOpts.updatevalue || [])[i]; var updatelistener = makeListener(updateEventName, updatevalue); diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 8a5a97c48a1..d60a2f5de3a 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -252,5 +252,80 @@ describe('update sliders interactions', function() { function assertNodeCount(query, cnt) { expect(d3.selectAll(query).size()).toEqual(cnt); } +}); + +describe('updateevent and updatevalue', function() { + 'use strict'; + + var mock = require('@mocks/sliders.json'); + + var gd; + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockCopy = Lib.extendDeep({}, mock); + + mockCopy.layout.sliders[0].updateevent = 'plotly_someevent'; + mockCopy.layout.sliders[0].updateevent = 'plotly_someevent'; + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + it('updates a slider when an event is triggered', function(done) { + Plotly.relayout(gd, { + 'sliders[0].updateevent': 'plotly_someevent', + 'sliders[0].updatevalue': 'value' + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(2); + gd.emit('plotly_someevent', {value: 'green'}); + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(3); + }).catch(fail).then(done); + }); + + it('updates a slider when updatevalue unspecified', function(done) { + Plotly.relayout(gd, { + 'sliders[0].updateevent': 'plotly_someevent' + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(2); + gd.emit('plotly_someevent', 'green'); + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(3); + }).catch(fail).then(done); + }); + it('updates a slider when any of multiple updateevents occurs', function(done) { + Plotly.relayout(gd, { + 'sliders[0].updateevent': ['plotly_someevent', 'plotly_anotherevent'] + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(2); + gd.emit('plotly_someevent', 'green'); + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(3); + gd.emit('plotly_anotherevent', 'yellow'); + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(2); + }).catch(fail).then(done); + }); + + it('matches update events with update values', function(done) { + Plotly.relayout(gd, { + 'sliders[0].updateevent': ['plotly_someevent', 'plotly_anotherevent'], + 'sliders[0].updatevalue': ['foo', 'bar'] + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(2); + gd.emit('plotly_someevent', {foo: 'green'}); + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(3); + gd.emit('plotly_anotherevent', {bar: 'yellow'}); + }).then(function() { + expect(gd._fullLayout.sliders[0].active).toEqual(2); + }).catch(fail).then(done); + }); }); From a08f08431c3fafa605fd00fe6af491ac1e910000 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 3 Oct 2016 15:54:19 -0400 Subject: [PATCH 29/40] Update sliders baseline to reflect bugfix --- test/image/baselines/sliders.png | Bin 22241 -> 24905 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/image/baselines/sliders.png b/test/image/baselines/sliders.png index 2a4f3b4f7fa9fbe0b6e1eb45c78ece348e2f6520..42b8013eb79004b41c2984d86b2f6114b73778fc 100644 GIT binary patch literal 24905 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

p}bxjuhiUP41K52HYjvw70=b=)v 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 Z5EGW`P8M`{{bulLv{cF literal 22241 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 From 5816c240f9e361e98b650139b44fa7cf3566ed41 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Mon, 3 Oct 2016 16:00:32 -0400 Subject: [PATCH 30/40] Fix plot schema error --- src/components/sliders/attributes.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 0fd4116f80b..8ec3cd330f6 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -197,6 +197,7 @@ module.exports = { currentvalue: { visible: { valType: 'boolean', + role: 'info', dflt: true, description: [ 'Shows the currently-selected value above the slider.' From 2cfa81b3b83eeb62b2df531da20839efc67b3bc9 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Tue, 4 Oct 2016 14:45:22 -0400 Subject: [PATCH 31/40] Remove unneeded lines from sliders test --- test/jasmine/tests/sliders_test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index d60a2f5de3a..26b7549e9fc 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -266,9 +266,6 @@ describe('updateevent and updatevalue', function() { var mockCopy = Lib.extendDeep({}, mock); - mockCopy.layout.sliders[0].updateevent = 'plotly_someevent'; - mockCopy.layout.sliders[0].updateevent = 'plotly_someevent'; - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); }); From 1d52b51e18aab1a2cea6c164043be5d3aa931937 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 09:09:08 -0400 Subject: [PATCH 32/40] Remove updatevalue and updateevent --- src/components/sliders/attributes.js | 33 ------------- src/components/sliders/defaults.js | 19 -------- src/components/sliders/draw.js | 51 ------------------- test/jasmine/tests/sliders_test.js | 73 ---------------------------- 4 files changed, 176 deletions(-) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 8ec3cd330f6..9b2e9830ae4 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -78,39 +78,6 @@ module.exports = { steps: stepsAttrs, - updateevent: { - valType: 'string', - arrayOk: true, - role: 'info', - description: [ - 'The name of the event to which this component subscribes', - 'in order to trigger updates. When the event is received', - 'the component will attempt to update the slider position', - 'to reflect the value passed as the data property of the', - 'event. The corresponding step\'s API method is assumed to', - 'have been triggered externally and so is not triggered again', - 'when the event is received. If an array is provided, multiple', - 'events will be subscribed to for updates.' - ].join(' ') - }, - - updatevalue: { - valType: 'string', - arrayOk: true, - role: 'info', - description: [ - 'The property of the event data that is matched to a slider', - 'value when an event of type `updateevent` is received. If', - 'undefined, the data argument itself is used. If a string,', - 'that property is used, and if a string with dots, e.g.', - '`item.0.label`, then `data[0].label` is used. If an array,', - 'it is matched to the respective updateevent item or if there', - 'is no corresponding updatevalue for a particular updateevent,', - 'it is interpreted as `undefined` and defaults to the data', - 'property itself.' - ].join(' ') - }, - lenmode: { valType: 'enumerated', values: ['fraction', 'pixels'], diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index d525195d77e..aed4e3b8e53 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -70,28 +70,9 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('currentvalue.prefix'); coerce('currentvalue.offset'); - coerce('updateevent'); - coerce('updatevalue'); - coerce('transition.duration'); coerce('transition.easing'); - if(sliderOut.updateevent) { - if(!Array.isArray(sliderOut.updateevent)) { - sliderOut.updateevent = [sliderOut.updateevent]; - } - } else { - sliderOut.updateevent = []; - } - - if(sliderOut.updatevalue) { - if(!Array.isArray(sliderOut.updatevalue)) { - sliderOut.updatevalue = [sliderOut.updatevalue]; - } - } else { - sliderOut.updatevalue = []; - } - Lib.coerceFont(coerce, 'font', layoutOut.font); Lib.coerceFont(coerce, 'currentvalue.font', layoutOut.font); diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index ae012f34a8a..dcaa4640142 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -227,11 +227,6 @@ function drawSlider(gd, sliderGroup, sliderOpts) { // Position the rectangle: Lib.setTranslate(sliderGroup, sliderOpts.lx + sliderOpts.pad.l, sliderOpts.ly + sliderOpts.pad.t); - // Every time the slider is draw from scratch, just detach and reattach the event listeners. - // This could perhaps be avoided. - removeListeners(gd, sliderGroup, sliderOpts); - attachListeners(gd, sliderGroup, sliderOpts); - setActive(gd, sliderGroup, sliderOpts, sliderOpts.active, false, false); } @@ -282,52 +277,6 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { return text; } -function removeListeners(gd, sliderGroup, sliderOpts) { - var listeners = sliderOpts._input.listeners; - var eventNames = sliderOpts._input.eventNames; - if(!Array.isArray(listeners) || !Array.isArray(eventNames)) return; - while(listeners.length) { - gd._removeInternalListener(eventNames.pop(), listeners.pop()); - } -} - -function attachListeners(gd, sliderGroup, sliderOpts) { - if(!sliderOpts.updateevent || !sliderOpts.updateevent.length) { - return; - } - - var listeners = sliderOpts._input.listeners = []; - var eventNames = sliderOpts._input.eventNames = []; - - function makeListener(eventname, updatevalue) { - return function(data) { - var value = data; - if(updatevalue) { - value = Lib.nestedProperty(data, updatevalue).get(); - } - - // If it's *currently* invoking a command an event is received, - // then we'll ignore the event in order to avoid complicated - // infinite loops. - if(sliderOpts._invokingCommand) return; - - setActiveByLabel(gd, sliderGroup, sliderOpts, value, false, true); - }; - } - - for(var i = 0; i < sliderOpts.updateevent.length; i++) { - var updateEventName = sliderOpts.updateevent[i]; - var updatevalue = (sliderOpts.updatevalue || [])[i]; - - var updatelistener = makeListener(updateEventName, updatevalue); - - gd._internalEv.on(updateEventName, updatelistener); - - eventNames.push(updateEventName); - listeners.push(updatelistener); - } -} - function drawGrip(sliderGroup, gd, sliderOpts) { var grip = sliderGroup.selectAll('rect.' + constants.gripRectClass) .data([0]); diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 26b7549e9fc..924e286c900 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -253,76 +253,3 @@ describe('update sliders interactions', function() { expect(d3.selectAll(query).size()).toEqual(cnt); } }); - -describe('updateevent and updatevalue', function() { - 'use strict'; - - var mock = require('@mocks/sliders.json'); - - var gd; - - beforeEach(function(done) { - gd = createGraphDiv(); - - var mockCopy = Lib.extendDeep({}, mock); - - Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); - }); - - afterEach(function() { - Plotly.purge(gd); - destroyGraphDiv(); - }); - - it('updates a slider when an event is triggered', function(done) { - Plotly.relayout(gd, { - 'sliders[0].updateevent': 'plotly_someevent', - 'sliders[0].updatevalue': 'value' - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(2); - gd.emit('plotly_someevent', {value: 'green'}); - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(3); - }).catch(fail).then(done); - }); - - it('updates a slider when updatevalue unspecified', function(done) { - Plotly.relayout(gd, { - 'sliders[0].updateevent': 'plotly_someevent' - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(2); - gd.emit('plotly_someevent', 'green'); - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(3); - }).catch(fail).then(done); - }); - - it('updates a slider when any of multiple updateevents occurs', function(done) { - Plotly.relayout(gd, { - 'sliders[0].updateevent': ['plotly_someevent', 'plotly_anotherevent'] - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(2); - gd.emit('plotly_someevent', 'green'); - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(3); - gd.emit('plotly_anotherevent', 'yellow'); - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(2); - }).catch(fail).then(done); - }); - - it('matches update events with update values', function(done) { - Plotly.relayout(gd, { - 'sliders[0].updateevent': ['plotly_someevent', 'plotly_anotherevent'], - 'sliders[0].updatevalue': ['foo', 'bar'] - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(2); - gd.emit('plotly_someevent', {foo: 'green'}); - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(3); - gd.emit('plotly_anotherevent', {bar: 'yellow'}); - }).then(function() { - expect(gd._fullLayout.sliders[0].active).toEqual(2); - }).catch(fail).then(done); - }); -}); From 892583de207ea8f2a0b1cf8fac326cdb615dacf4 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 09:12:53 -0400 Subject: [PATCH 33/40] Removed unused slider function --- src/components/sliders/draw.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index dcaa4640142..0e6462c480b 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -350,21 +350,6 @@ function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransiti } } -function setActiveByLabel(gd, sliderGroup, sliderOpts, label, doCallback, doTransition) { - var index; - for(var i = 0; i < sliderOpts.steps.length; i++) { - var step = sliderOpts.steps[i]; - if(step.label === label) { - index = i; - break; - } - } - - if(index !== undefined) { - setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition); - } -} - function setActive(gd, sliderGroup, sliderOpts, index, doCallback, doTransition) { sliderOpts._input.active = sliderOpts.active = index; From 5015710e502c39f4ef28521ddefeb0ff98f6ab05 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 10:09:00 -0400 Subject: [PATCH 34/40] Clean up slider options --- src/components/sliders/attributes.js | 56 ++++++++++++++++++---- src/components/sliders/constants.js | 1 + src/components/sliders/defaults.js | 17 ++++--- src/components/sliders/draw.js | 32 ++++++------- src/components/updatemenus/defaults.js | 4 +- test/image/baselines/sliders.png | Bin 24905 -> 27434 bytes test/image/mocks/sliders.json | 63 ++++++++++++++++++++++++- 7 files changed, 137 insertions(+), 36 deletions(-) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index 9b2e9830ae4..ddcd04cc516 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -10,9 +10,9 @@ var fontAttrs = require('../../plots/font_attributes'); var padAttrs = require('../../plots/pad_attributes'); -var colorAttrs = require('../color/attributes'); var extendFlat = require('../../lib/extend').extendFlat; var animationAttrs = require('../../plots/animation_attributes'); +var constants = require('./constants'); var stepsAttrs = { _isLinkedToArray: true, @@ -37,7 +37,7 @@ var stepsAttrs = { ], description: [ 'Sets the arguments values to be passed to the Plotly', - 'method set in `method` on click.' + 'method set in `method` on slide.' ].join(' ') }, label: { @@ -84,8 +84,7 @@ module.exports = { role: 'info', dflt: 'fraction', description: [ - 'Determines whether this color bar\'s length', - '(i.e. the measure in the color variation direction)', + 'Determines whether this slider length', 'is set in units of plot *fraction* or in *pixels.', 'Use `len` to set the value.' ].join(' ') @@ -96,9 +95,9 @@ module.exports = { dflt: 1, role: 'style', description: [ - 'Sets the length of the color bar', + 'Sets the length of the slider', 'This measure excludes the padding of both ends.', - 'That is, the color bar length is this length minus the', + 'That is, the slider\'s length is this length minus the', 'padding on both ends.' ].join(' ') }, @@ -209,22 +208,59 @@ module.exports = { description: 'Sets the font of the slider step labels.' }), + activebgcolor: { + valType: 'color', + role: 'style', + dflt: constants.gripBgActiveColor, + description: [ + 'Sets the background color of the slider grip', + 'while dragging.' + ].join(' ') + }, bgcolor: { valType: 'color', role: 'style', - description: 'Sets the background color of the slider buttons.' + dflt: constants.railBgColor, + description: 'Sets the background color of the slider.' }, bordercolor: { valType: 'color', - dflt: colorAttrs.borderLine, + dflt: constants.railBorderColor, role: 'style', description: 'Sets the color of the border enclosing the slider.' }, borderwidth: { valType: 'number', min: 0, - dflt: 1, + dflt: constants.railBorderWidth, role: 'style', description: 'Sets the width (in px) of the border enclosing the slider.' - } + }, + ticklen: { + valType: 'number', + min: 0, + dflt: constants.tickLength, + role: 'style', + description: 'Sets the length in pixels of step tick marks' + }, + tickcolor: { + valType: 'color', + dflt: constants.tickColor, + role: 'style', + description: 'Sets the color of the border enclosing the slider.' + }, + tickwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the tick width (in px).' + }, + minorticklen: { + valType: 'number', + min: 0, + dflt: constants.minorTickLength, + role: 'style', + description: 'Sets the length in pixels of minor step tick marks' + }, }; diff --git a/src/components/sliders/constants.js b/src/components/sliders/constants.js index e772428ada1..cb13d711d56 100644 --- a/src/components/sliders/constants.js +++ b/src/components/sliders/constants.js @@ -55,6 +55,7 @@ module.exports = { railRadius: 2, railWidth: 5, railBorder: 4, + railBorderWidth: 1, railBorderColor: '#bec8d9', railBgColor: '#f8fafc', diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index aed4e3b8e53..2e3217020ba 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -11,9 +11,9 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); -var contants = require('./constants'); +var constants = require('./constants'); -var name = contants.name; +var name = constants.name; var stepAttrs = attributes.steps; @@ -73,12 +73,17 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('transition.duration'); coerce('transition.easing'); - Lib.coerceFont(coerce, 'font', layoutOut.font); - Lib.coerceFont(coerce, 'currentvalue.font', layoutOut.font); - - coerce('bgcolor', layoutOut.paper_bgcolor); + coerce('bgcolor'); + coerce('activebgcolor'); coerce('bordercolor'); coerce('borderwidth'); + coerce('ticklen'); + coerce('tickwidth'); + coerce('tickcolor'); + coerce('minorticklen'); + + Lib.coerceFont(coerce, 'font', layoutOut.font); + Lib.coerceFont(coerce, 'currentvalue.font', sliderOut.font); } function stepsDefaults(sliderIn, sliderOut) { diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 0e6462c480b..238561e6319 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -177,7 +177,7 @@ function findDimensions(gd, sliderOpts) { sliderOpts.labelStride = Math.max(1, Math.ceil(computedSpacePerLabel / availableSpacePerLabel)); sliderOpts.labelHeight = labelHeight; - sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + constants.tickLength + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; + sliderOpts.height = sliderOpts.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + constants.labelOffset + sliderOpts.labelHeight + sliderOpts.pad.t + sliderOpts.pad.b; var xanchor = 'left'; if(anchorUtils.isRightAnchor(sliderOpts)) { @@ -292,9 +292,9 @@ function drawGrip(sliderGroup, gd, sliderOpts) { rx: constants.gripRadius, ry: constants.gripRadius, }) - .call(Color.stroke, constants.gripBorderColor) - .call(Color.fill, constants.gripBgColor) - .style('stroke-width', constants.gripBorderWidth + 'px'); + .call(Color.stroke, sliderOpts.bordercolor) + .call(Color.fill, sliderOpts.bgcolor) + .style('stroke-width', sliderOpts.borderwidth + 'px'); } function drawLabel(item, data, sliderOpts) { @@ -335,7 +335,7 @@ function drawLabelGroup(sliderGroup, sliderOpts) { Lib.setTranslate(item, normalizedValueToPosition(sliderOpts, d.fraction), - constants.tickOffset + constants.tickLength + sliderOpts.labelHeight + constants.labelOffset + sliderOpts.currentValueTotalHeight + constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight + constants.labelOffset + sliderOpts.currentValueTotalHeight ); }); @@ -398,7 +398,7 @@ function attachGripEvents(item, gd, sliderGroup, sliderOpts) { d3.event.stopPropagation(); d3.event.preventDefault(); - grip.call(Color.fill, constants.gripBgActiveColor); + grip.call(Color.fill, sliderOpts.activebgcolor); var normalizedPosition = positionToNormalizedValue(sliderOpts, d3.mouse(node)[0]); handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, true); @@ -409,7 +409,7 @@ function attachGripEvents(item, gd, sliderGroup, sliderOpts) { }); $gd.on('mouseup', function() { - grip.call(Color.fill, constants.gripBgColor); + grip.call(Color.fill, sliderOpts.bgcolor); $gd.on('mouseup', null); $gd.on('mousemove', null); }); @@ -426,7 +426,7 @@ function drawTicks(sliderGroup, sliderOpts) { tick.exit().remove(); tick.attr({ - width: constants.tickWidth, + width: sliderOpts.tickwidth + 'px', 'shape-rendering': 'crispEdges' }); @@ -435,11 +435,11 @@ function drawTicks(sliderGroup, sliderOpts) { var item = d3.select(this); item - .attr({height: isMajor ? constants.tickLength : constants.minorTickLength}) - .call(Color.fill, isMajor ? constants.tickColor : constants.minorTickColor); + .attr({height: isMajor ? sliderOpts.ticklen : sliderOpts.minorticklen}) + .call(Color.fill, isMajor ? sliderOpts.tickcolor : sliderOpts.tickcolor); Lib.setTranslate(item, - normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * constants.tickWidth, + normalizedValueToPosition(sliderOpts, i / (sliderOpts.steps.length - 1)) - 0.5 * sliderOpts.tickwidth, (isMajor ? constants.tickOffset : constants.minorTickOffset) + sliderOpts.currentValueTotalHeight ); }); @@ -498,9 +498,9 @@ function drawTouchRect(sliderGroup, gd, sliderOpts) { rect.attr({ width: sliderOpts.inputAreaLength, - height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + constants.tickLength + sliderOpts.labelHeight) + height: Math.max(sliderOpts.inputAreaWidth, constants.tickOffset + sliderOpts.ticklen + sliderOpts.labelHeight) }) - .call(Color.fill, constants.gripBgColor) + .call(Color.fill, sliderOpts.bgcolor) .attr('opacity', 0); Lib.setTranslate(rect, 0, sliderOpts.currentValueTotalHeight); @@ -522,9 +522,9 @@ function drawRail(sliderGroup, sliderOpts) { ry: constants.railRadius, 'shape-rendering': 'crispEdges' }) - .call(Color.stroke, constants.railBorderColor) - .call(Color.fill, constants.railBgColor) - .style('stroke-width', '1px'); + .call(Color.stroke, sliderOpts.bordercolor) + .call(Color.fill, sliderOpts.bgcolor) + .style('stroke-width', sliderOpts.borderwidth + 'px'); Lib.setTranslate(rect, constants.railInset, diff --git a/src/components/updatemenus/defaults.js b/src/components/updatemenus/defaults.js index f97fdd34297..d32a8c1892a 100644 --- a/src/components/updatemenus/defaults.js +++ b/src/components/updatemenus/defaults.js @@ -11,9 +11,9 @@ var Lib = require('../../lib'); var attributes = require('./attributes'); -var contants = require('./constants'); +var constants = require('./constants'); -var name = contants.name; +var name = constants.name; var buttonAttrs = attributes.buttons; diff --git a/test/image/baselines/sliders.png b/test/image/baselines/sliders.png index 42b8013eb79004b41c2984d86b2f6114b73778fc..17a81063ac80ac9498c8e6df02e369cad7944d12 100644 GIT binary patch literal 27434 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

p}bxjuhiUP41K52HYjvw70=b=)v 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 Z5EGW`P8M`{{bulLv{cF diff --git a/test/image/mocks/sliders.json b/test/image/mocks/sliders.json index c632fa18535..1c4447e6a37 100644 --- a/test/image/mocks/sliders.json +++ b/test/image/mocks/sliders.json @@ -39,6 +39,9 @@ "xanchor": "right", "y": -0.1, "yanchor": "top", + "currentvalue": { + "visible": false + }, "transition": { "duration": 150, @@ -47,7 +50,7 @@ "pad": { "r": 20, - "t": 20 + "t": 60 }, "font": {} @@ -69,6 +72,42 @@ "label": "green", "method": "restyle", "args": [{"marker.color": "green"}] + }, { + "label": "orange", + "method": "restyle", + "args": [{"marker.color": "orange"}] + }, { + "label": "yellow", + "method": "restyle", + "args": [{"marker.color": "yellow"}] + }, { + "label": "green", + "method": "restyle", + "args": [{"marker.color": "green"}] + }, { + "label": "orange", + "method": "restyle", + "args": [{"marker.color": "orange"}] + }, { + "label": "yellow", + "method": "restyle", + "args": [{"marker.color": "yellow"}] + }, { + "label": "green", + "method": "restyle", + "args": [{"marker.color": "green"}] + }, { + "label": "orange", + "method": "restyle", + "args": [{"marker.color": "orange"}] + }, { + "label": "yellow", + "method": "restyle", + "args": [{"marker.color": "yellow"}] + }, { + "label": "green", + "method": "restyle", + "args": [{"marker.color": "green"}] }, { "label": "blue", "method": "restyle", @@ -95,7 +134,27 @@ "t": 20 }, - "font": {} + "bgcolor": "red", + "bordercolor": "blue", + "borderwidth": 2, + "activebgcolor": "green", + "ticklen": 20, + "tickcolor": "purple", + "minorticklen": 10, + "tickwidth": 2, + + "font": { + "color": "purple", + "size": 15 + }, + "currentvalue": { + "prefix": "color:", + "xanchor": "right", + "font": { + "color": "orange", + "size": 20 + } + } }], "xaxis": { "range": [0, 2], From 582c6432d4c67860db7abac23d8a0ee6a70a21a1 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 10:18:55 -0400 Subject: [PATCH 35/40] Remove unneeded/failing slider test --- test/jasmine/tests/sliders_test.js | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 924e286c900..355b8b007f4 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -159,27 +159,6 @@ describe('sliders defaults', function() { expect(layoutOut.sliders[1]._input).toBe(layoutIn.sliders[1]); expect(layoutOut.sliders[2]._input).toBe(layoutIn.sliders[2]); }); - - it('should default \'bgcolor\' to layout \'paper_bgcolor\'', function() { - var steps = [{ - method: 'relayout', - args: ['title', 'Hello World'] - }]; - - layoutIn.sliders = [{ - steps: steps, - }, { - bgcolor: 'red', - steps: steps - }]; - - layoutOut.paper_bgcolor = 'blue'; - - supply(layoutIn, layoutOut); - - expect(layoutOut.sliders[0].bgcolor).toEqual('blue'); - expect(layoutOut.sliders[1].bgcolor).toEqual('red'); - }); }); describe('update sliders interactions', function() { From 70610ec80a1ea8eb8ff85bd5da48093e97ffc016 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 13:14:18 -0400 Subject: [PATCH 36/40] Add suffix to currentvalue slider label --- src/components/sliders/attributes.js | 13 ++++++++----- src/components/sliders/defaults.js | 1 + src/components/sliders/draw.js | 6 +++++- test/image/baselines/sliders.png | Bin 27434 -> 28692 bytes test/image/mocks/sliders.json | 3 ++- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/sliders/attributes.js b/src/components/sliders/attributes.js index ddcd04cc516..420287f7ded 100644 --- a/src/components/sliders/attributes.js +++ b/src/components/sliders/attributes.js @@ -193,14 +193,17 @@ module.exports = { prefix: { valType: 'string', role: 'info', - description: [ - 'When currentvalue.visible is true, this sets the prefix of the lable. If provided,', - 'it will be joined to the current value with a single space between.' - ].join(' ') + description: 'When currentvalue.visible is true, this sets the prefix of the label.' + }, + + suffix: { + valType: 'string', + role: 'info', + description: 'When currentvalue.visible is true, this sets the suffix of the label.' }, font: extendFlat({}, fontAttrs, { - description: 'Sets the font of the current value lable text.' + description: 'Sets the font of the current value label text.' }), }, diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 2e3217020ba..6bfb4295d7f 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -68,6 +68,7 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('currentvalue.visible'); coerce('currentvalue.xanchor'); coerce('currentvalue.prefix'); + coerce('currentvalue.suffix'); coerce('currentvalue.offset'); coerce('transition.duration'); diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 238561e6319..26ea31340bb 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -259,7 +259,7 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { .classed('user-select-none', true) .attr('text-anchor', textAnchor); - var str = sliderOpts.currentvalue.prefix ? (sliderOpts.currentvalue.prefix + ' ') : ''; + var str = sliderOpts.currentvalue.prefix ? sliderOpts.currentvalue.prefix : ''; if(typeof valueOverride === 'string') { str += valueOverride; @@ -268,6 +268,10 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { str += curVal; } + if(sliderOpts.currentvalue.suffix) { + str += sliderOpts.currentvalue.suffix; + } + text.call(Drawing.font, sliderOpts.currentvalue.font) .text(str) .call(svgTextUtils.convertToTspans); diff --git a/test/image/baselines/sliders.png b/test/image/baselines/sliders.png index 17a81063ac80ac9498c8e6df02e369cad7944d12..bb8cbabdb04e91e8eae1d794e016f0e50c3c21d7 100644 GIT binary patch literal 28692 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 literal 27434 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 Date: Thu, 6 Oct 2016 13:38:27 -0400 Subject: [PATCH 37/40] don't coerce slider currentvalue attrs when !visible --- src/components/sliders/defaults.js | 20 ++++++++++++-------- test/jasmine/tests/sliders_test.js | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/components/sliders/defaults.js b/src/components/sliders/defaults.js index 6bfb4295d7f..b4b3bdce900 100644 --- a/src/components/sliders/defaults.js +++ b/src/components/sliders/defaults.js @@ -65,11 +65,18 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('pad.b'); coerce('pad.l'); - coerce('currentvalue.visible'); - coerce('currentvalue.xanchor'); - coerce('currentvalue.prefix'); - coerce('currentvalue.suffix'); - coerce('currentvalue.offset'); + Lib.coerceFont(coerce, 'font', layoutOut.font); + + var currentValueIsVisible = coerce('currentvalue.visible'); + + if(currentValueIsVisible) { + coerce('currentvalue.xanchor'); + coerce('currentvalue.prefix'); + coerce('currentvalue.suffix'); + coerce('currentvalue.offset'); + + Lib.coerceFont(coerce, 'currentvalue.font', sliderOut.font); + } coerce('transition.duration'); coerce('transition.easing'); @@ -82,9 +89,6 @@ function sliderDefaults(sliderIn, sliderOut, layoutOut) { coerce('tickwidth'); coerce('tickcolor'); coerce('minorticklen'); - - Lib.coerceFont(coerce, 'font', layoutOut.font); - Lib.coerceFont(coerce, 'currentvalue.font', sliderOut.font); } function stepsDefaults(sliderIn, sliderOut) { diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 355b8b007f4..8b58bfb79db 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -57,6 +57,23 @@ describe('sliders defaults', function() { expect(layoutOut.sliders[2].active).toBeUndefined(); }); + it('should not coerce currentvalue defaults unless currentvalue is visible', function() { + layoutIn.sliders = [{ + currentvalue: { + visible: false, + xanchor: 'left' + }, + steps: [ + {method: 'restyle', args: [], label: 'step0'}, + {method: 'restyle', args: [], label: 'step1'} + ] + }]; + + supply(layoutIn, layoutOut); + + expect(layoutOut.sliders[0].currentvalue.xanchor).toBeUndefined(); + }); + it('should set the default values equal to the labels', function() { layoutIn.sliders = [{ steps: [{ From 4b608eeb29e9c737f831491ffa7bda3b7987153c Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 13:40:26 -0400 Subject: [PATCH 38/40] Be more specific about *all* unused slider attrs not set --- test/jasmine/tests/sliders_test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 8b58bfb79db..68d8abadeff 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -72,6 +72,10 @@ describe('sliders defaults', function() { supply(layoutIn, layoutOut); expect(layoutOut.sliders[0].currentvalue.xanchor).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.prefix).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.suffix).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.offset).toBeUndefined(); + expect(layoutOut.sliders[0].currentvalue.font).toBeUndefined(); }); it('should set the default values equal to the labels', function() { From 6c9034d64fdbee21f42483fbfb338c1f4a749ec1 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 14:20:49 -0400 Subject: [PATCH 39/40] Test mouse movement for sliders --- src/components/sliders/draw.js | 1 - test/jasmine/tests/sliders_test.js | 43 +++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index 26ea31340bb..e711883b959 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -348,7 +348,6 @@ function drawLabelGroup(sliderGroup, sliderOpts) { function handleInput(gd, sliderGroup, sliderOpts, normalizedPosition, doTransition) { var quantizedPosition = Math.round(normalizedPosition * (sliderOpts.steps.length - 1)); - if(quantizedPosition !== sliderOpts.active) { setActive(gd, sliderGroup, sliderOpts, quantizedPosition, true, doTransition); } diff --git a/test/jasmine/tests/sliders_test.js b/test/jasmine/tests/sliders_test.js index 68d8abadeff..f23a6926f4e 100644 --- a/test/jasmine/tests/sliders_test.js +++ b/test/jasmine/tests/sliders_test.js @@ -186,13 +186,14 @@ describe('update sliders interactions', function() { 'use strict'; var mock = require('@mocks/sliders.json'); + var mockCopy; var gd; beforeEach(function(done) { gd = createGraphDiv(); - var mockCopy = Lib.extendDeep({}, mock); + mockCopy = Lib.extendDeep({}, mock); Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); }); @@ -249,6 +250,46 @@ describe('update sliders interactions', function() { .catch(fail).then(done); }); + it('should respond to mouse clicks', function(done) { + var firstGroup = gd._fullLayout._infolayer.select('.' + constants.railTouchRectClass); + var firstGrip = gd._fullLayout._infolayer.select('.' + constants.gripRectClass); + var railNode = firstGroup.node(); + var touchRect = railNode.getBoundingClientRect(); + + var originalFill = firstGrip.style('fill'); + + // Dispatch a click on the right side of the bar: + railNode.dispatchEvent(new MouseEvent('mousedown', { + clientY: touchRect.top + 5, + clientX: touchRect.left + touchRect.width - 5, + })); + + expect(mockCopy.layout.sliders[0].active).toEqual(5); + var mousedownFill = firstGrip.style('fill'); + expect(mousedownFill).not.toEqual(originalFill); + + // Drag to the left side: + gd.dispatchEvent(new MouseEvent('mousemove', { + clientY: touchRect.top + 5, + clientX: touchRect.left + 5, + })); + + var mousemoveFill = firstGrip.style('fill'); + expect(mousemoveFill).toEqual(mousedownFill); + + setTimeout(function() { + expect(mockCopy.layout.sliders[0].active).toEqual(0); + + gd.dispatchEvent(new MouseEvent('mouseup')); + + var mouseupFill = firstGrip.style('fill'); + expect(mouseupFill).toEqual(originalFill); + expect(mockCopy.layout.sliders[0].active).toEqual(0); + + done(); + }, 100); + }); + function assertNodeCount(query, cnt) { expect(d3.selectAll(query).size()).toEqual(cnt); } From 21e2f997586f2018e36a438cc3bf8fd101181c22 Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Thu, 6 Oct 2016 15:27:59 -0400 Subject: [PATCH 40/40] Make sure slider Drawing.bBox calls fall back to defined --- src/components/sliders/draw.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index e711883b959..c24e322c385 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -114,12 +114,12 @@ function findDimensions(gd, sliderOpts) { var text = drawLabel(labelGroup, {step: stepOpts}, sliderOpts); - var tWidth = text.node() && Drawing.bBox(text.node()).width; + var tWidth = (text.node() && Drawing.bBox(text.node()).width) || 0; // This just overwrites with the last. Which is fine as long as // the bounding box (probably incorrectly) measures the text *on // a single line*: - labelHeight = text.node() && Drawing.bBox(text.node()).height; + labelHeight = (text.node() && Drawing.bBox(text.node()).height) || 0; maxLabelWidth = Math.max(maxLabelWidth, tWidth); }); @@ -141,7 +141,7 @@ function findDimensions(gd, sliderOpts) { sliderLabels.each(function(stepOpts) { var curValPrefix = drawCurrentValue(dummyGroup, sliderOpts, stepOpts.label); - var curValSize = curValPrefix.node() && Drawing.bBox(curValPrefix.node()); + var curValSize = (curValPrefix.node() && Drawing.bBox(curValPrefix.node())) || {width: 0, height: 0}; sliderOpts.currentValueMaxWidth = Math.max(sliderOpts.currentValueMaxWidth, Math.ceil(curValSize.width)); sliderOpts.currentValueHeight = Math.max(sliderOpts.currentValueHeight, Math.ceil(curValSize.height)); });