Skip to content

Multiple range sliders #1355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/components/rangeslider/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ module.exports = {
role: 'style',
description: 'Sets the border color of the range slider.'
},
autorange: {
valType: 'boolean',
dflt: true,
role: 'style',
description: [
'Determines whether or not the range slider range is',
'computed in relation to the input data.',
'If `range` is provided, then `autorange` is set to *false*.'
].join(' ')
},
range: {
valType: 'info_array',
role: 'info',
Expand Down
34 changes: 34 additions & 0 deletions src/components/rangeslider/calc_autorange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright 2012-2017, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Axes = require('../../plots/cartesian/axes');
var constants = require('./constants');

module.exports = function calcAutorange(gd) {
var axes = Axes.list(gd, 'x', true);

// Compute new slider range using axis autorange if necessary.
//
// Copy back range to input range slider container to skip
// this step in subsequent draw calls.

for(var i = 0; i < axes.length; i++) {
var ax = axes[i],
opts = ax[constants.name];

// Don't try calling getAutoRange if _min and _max are filled in.
// This happens on updates where the calc step is skipped.

if(opts && opts.visible && opts.autorange && ax._min.length && ax._max.length) {
opts._input.autorange = true;
opts._input.range = opts.range = Axes.getAutoRange(ax);
}
}
};
9 changes: 4 additions & 5 deletions src/components/rangeslider/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@ module.exports = {
grabAreaFill: 'transparent',
grabAreaCursor: 'col-resize',
grabAreaWidth: 10,
grabAreaMinOffset: -6,
grabAreaMaxOffset: -2,

handleWidth: 2,
handleWidth: 4,
handleRadius: 1,
handleFill: '#fff',
handleStroke: '#666',
handleStrokeWidth: 1,

extraPad: 15
};
15 changes: 9 additions & 6 deletions src/components/rangeslider/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
var Lib = require('../../lib');
var attributes = require('./attributes');


module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
if(!layoutIn[axName].rangeslider) return;

Expand All @@ -28,25 +27,29 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
}

var visible = coerce('visible');
if(!visible) return;

coerce('bgcolor', layoutOut.plot_bgcolor);
coerce('bordercolor');
coerce('borderwidth');
coerce('thickness');
coerce('visible');

coerce('autorange', !axOut.isValidRange(containerIn.range));
coerce('range');

// Expand slider range to the axis range
if(containerOut.range && !axOut.autorange) {
// TODO: what if the ranges are reversed?
// TODO: what if the ranges are reversed?
if(containerOut.range) {
var outRange = containerOut.range,
axRange = axOut.range;

outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0])));
outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1])));
} else {
axOut._needsExpand = true;
}

axOut.cleanRange('rangeslider.range');

// to map back range slider (auto) range
containerOut._input = containerIn;
};
103 changes: 63 additions & 40 deletions src/components/rangeslider/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,16 @@ module.exports = function(gd) {
// for all present range sliders
rangeSliders.each(function(axisOpts) {
var rangeSlider = d3.select(this),
opts = axisOpts[constants.name];

// compute new slider range using axis autorange if necessary
// copy back range to input range slider container to skip
// this step in subsequent draw calls
if(!opts.range) {
opts._input.range = opts.range = Axes.getAutoRange(axisOpts);
}
opts = axisOpts[constants.name],
oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)];

// update range slider dimensions

var margin = fullLayout.margin,
graphSize = fullLayout._size,
domain = axisOpts.domain;
domain = axisOpts.domain,
oppDomain = oppAxisOpts.domain,
tickHeight = (axisOpts._boundingBox || {}).height || 0;

opts._id = constants.name + axisOpts._id;
opts._clipId = opts._id + '-' + fullLayout._uid;
Expand All @@ -99,8 +95,13 @@ module.exports = function(gd) {
opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness;
opts._offsetShift = Math.floor(opts.borderwidth / 2);

var x = margin.l + (graphSize.w * domain[0]),
y = fullLayout.height - opts._height - margin.b;
var x = Math.round(margin.l + (graphSize.w * domain[0]));

var y = Math.round(
margin.t + graphSize.h * (1 - oppDomain[0]) +
tickHeight +
opts._offsetShift + constants.extraPad
);

rangeSlider.attr('transform', 'translate(' + x + ',' + y + ')');

Expand Down Expand Up @@ -138,23 +139,33 @@ module.exports = function(gd) {

// update margins

var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0;

Plots.autoMargin(gd, opts._id, {
x: 0, y: 0, l: 0, r: 0, t: 0,
b: opts._height + fullLayout.margin.b + bb,
pad: 15 + opts._offsetShift * 2
x: domain[0],
y: oppDomain[0],
l: 0,
r: 0,
t: 0,
b: opts._height + margin.b + tickHeight,
pad: constants.extraPad + opts._offsetShift * 2
});

});
};

function makeRangeSliderData(fullLayout) {
if(!fullLayout.xaxis) return [];
if(!fullLayout.xaxis[constants.name]) return [];
if(!fullLayout.xaxis[constants.name].visible) return [];
if(fullLayout._has('gl2d')) return [];
var axes = Axes.list({ _fullLayout: fullLayout }, 'x', true),
name = constants.name,
out = [];

return [fullLayout.xaxis];
if(fullLayout._has('gl2d')) return out;

for(var i = 0; i < axes.length; i++) {
var ax = axes[i];

if(ax[name] && ax[name].visible) out.push(ax);
}

return out;
}

function setupDragElement(rangeSlider, gd, axisOpts, opts) {
Expand Down Expand Up @@ -236,16 +247,21 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) {
dataMax = clamp(opts.p2d(opts._pixelMax));

window.requestAnimationFrame(function() {
Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]);
Plotly.relayout(gd, axisOpts._name + '.range', [dataMin, dataMax]);
});
}

function setPixelRange(rangeSlider, gd, axisOpts, opts) {
var hw2 = constants.handleWidth / 2;

function clamp(v) {
return Lib.constrain(v, 0, opts._width);
}

function clampHandle(v) {
return Lib.constrain(v, -hw2, opts._width + hw2);
}

var pixelMin = clamp(opts.d2p(axisOpts._rl[0])),
pixelMax = clamp(opts.d2p(axisOpts._rl[1]));

Expand All @@ -260,11 +276,18 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) {
.attr('x', pixelMax)
.attr('width', opts._width - pixelMax);

// add offset for crispier corners
// https://github.com/plotly/plotly.js/pull/1409
var offset = 0.5;

var xMin = Math.round(clampHandle(pixelMin - hw2)) - offset,
xMax = Math.round(clampHandle(pixelMax - hw2)) + offset;

rangeSlider.select('g.' + constants.grabberMinClassName)
.attr('transform', 'translate(' + (pixelMin - constants.handleWidth - 1) + ',0)');
.attr('transform', 'translate(' + xMin + ',' + offset + ')');

rangeSlider.select('g.' + constants.grabberMaxClassName)
.attr('transform', 'translate(' + pixelMax + ',0)');
.attr('transform', 'translate(' + xMax + ',' + offset + ')');
}

function drawBg(rangeSlider, gd, axisOpts, opts) {
Expand All @@ -284,14 +307,15 @@ function drawBg(rangeSlider, gd, axisOpts, opts) {
opts.borderwidth - 1;

var offsetShift = -opts._offsetShift;
var lw = Drawing.crispRound(gd, opts.borderwidth);

bg.attr({
width: opts._width + borderCorrect,
height: opts._height + borderCorrect,
transform: 'translate(' + offsetShift + ',' + offsetShift + ')',
fill: opts.bgcolor,
stroke: opts.bordercolor,
'stroke-width': opts.borderwidth,
'stroke-width': lw
});
}

Expand Down Expand Up @@ -404,7 +428,8 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) {

maskMin.enter().append('rect')
.classed(constants.maskMinClassName, true)
.attr({ x: 0, y: 0 });
.attr({ x: 0, y: 0 })
.attr('shape-rendering', 'crispEdges');

maskMin
.attr('height', opts._height)
Expand All @@ -415,7 +440,8 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) {

maskMax.enter().append('rect')
.classed(constants.maskMaxClassName, true)
.attr('y', 0);
.attr('y', 0)
.attr('shape-rendering', 'crispEdges');

maskMax
.attr('height', opts._height)
Expand All @@ -431,7 +457,8 @@ function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
slideBox.enter().append('rect')
.classed(constants.slideBoxClassName, true)
.attr('y', 0)
.attr('cursor', constants.slideBoxCursor);
.attr('cursor', constants.slideBoxCursor)
.attr('shape-rendering', 'crispEdges');

slideBox.attr({
height: opts._height,
Expand Down Expand Up @@ -459,14 +486,15 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
x: 0,
width: constants.handleWidth,
rx: constants.handleRadius,
fill: constants.handleFill,
stroke: constants.handleStroke,
fill: Color.background,
stroke: Color.defaultLine,
'stroke-width': constants.handleStrokeWidth,
'shape-rendering': 'crispEdges'
};

var handleDynamicAttrs = {
y: opts._height / 4,
height: opts._height / 2,
y: Math.round(opts._height / 4),
height: Math.round(opts._height / 2),
};

var handleMin = grabberMin.selectAll('rect.' + constants.handleMinClassName)
Expand All @@ -489,6 +517,7 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) {

var grabAreaFixAttrs = {
width: constants.grabAreaWidth,
x: 0,
y: 0,
fill: constants.grabAreaFill,
cursor: constants.grabAreaCursor
Expand All @@ -499,20 +528,14 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
grabAreaMin.enter().append('rect')
.classed(constants.grabAreaMinClassName, true)
.attr(grabAreaFixAttrs);
grabAreaMin.attr({
x: constants.grabAreaMinOffset,
height: opts._height
});
grabAreaMin.attr('height', opts._height);

var grabAreaMax = grabberMax.selectAll('rect.' + constants.grabAreaMaxClassName)
.data([0]);
grabAreaMax.enter().append('rect')
.classed(constants.grabAreaMaxClassName, true)
.attr(grabAreaFixAttrs);
grabAreaMax.attr({
x: constants.grabAreaMaxOffset,
height: opts._height
});
grabAreaMax.attr('height', opts._height);
}

function clearPushMargins(gd) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/rangeslider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ module.exports = {

layoutAttributes: require('./attributes'),
handleDefaults: require('./defaults'),

calcAutorange: require('./calc_autorange'),
draw: require('./draw')
};
4 changes: 2 additions & 2 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ Plotly.plot = function(gd, data, layout, config) {
}

// draw anything that can affect margins.
// currently this is legend and colorbars
function marginPushers() {
var calcdata = gd.calcdata;
var i, cd, trace;
Expand Down Expand Up @@ -253,7 +252,8 @@ Plotly.plot = function(gd, data, layout, config) {
return Lib.syncOrAsync([
Registry.getComponentMethod('shapes', 'calcAutorange'),
Registry.getComponentMethod('annotations', 'calcAutorange'),
doAutoRange
doAutoRange,
Registry.getComponentMethod('rangeslider', 'calcAutorange')
], gd);
}

Expand Down
4 changes: 3 additions & 1 deletion src/plots/cartesian/axes.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,9 @@ axes.saveRangeInitial = function(gd, overwrite) {
// tozero: (boolean) make sure to include zero if axis is linear,
// and make it a tight bound if possible
axes.expand = function(ax, data, options) {
if(!(ax.autorange || ax._needsExpand) || !data) return;
var needsAutorange = (ax.autorange || Lib.nestedProperty(ax, 'rangeslider.autorange'));
if(!needsAutorange || !data) return;

if(!ax._min) ax._min = [];
if(!ax._max) ax._max = [];
if(!options) options = {};
Expand Down
8 changes: 1 addition & 7 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

'use strict';

var isNumeric = require('fast-isnumeric');
var colorMix = require('tinycolor2').mix;

var Registry = require('../../registry');
Expand Down Expand Up @@ -93,12 +92,7 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
color: dfltFontColor
});

var validRange = (
(containerIn.range || []).length === 2 &&
isNumeric(containerOut.r2l(containerIn.range[0])) &&
isNumeric(containerOut.r2l(containerIn.range[1]))
);
var autoRange = coerce('autorange', !validRange);
var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range));

if(autoRange) coerce('rangemode');

Expand Down
Loading