Skip to content

Commit 027d97f

Browse files
authored
Merge pull request #1355 from plotly/multiple-rangesliders
Multiple range sliders
2 parents 143749e + 91838f9 commit 027d97f

23 files changed

+386
-154
lines changed

src/components/rangeslider/attributes.js

+10
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ module.exports = {
3030
role: 'style',
3131
description: 'Sets the border color of the range slider.'
3232
},
33+
autorange: {
34+
valType: 'boolean',
35+
dflt: true,
36+
role: 'style',
37+
description: [
38+
'Determines whether or not the range slider range is',
39+
'computed in relation to the input data.',
40+
'If `range` is provided, then `autorange` is set to *false*.'
41+
].join(' ')
42+
},
3343
range: {
3444
valType: 'info_array',
3545
role: 'info',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright 2012-2017, Plotly, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
'use strict';
10+
11+
var Axes = require('../../plots/cartesian/axes');
12+
var constants = require('./constants');
13+
14+
module.exports = function calcAutorange(gd) {
15+
var axes = Axes.list(gd, 'x', true);
16+
17+
// Compute new slider range using axis autorange if necessary.
18+
//
19+
// Copy back range to input range slider container to skip
20+
// this step in subsequent draw calls.
21+
22+
for(var i = 0; i < axes.length; i++) {
23+
var ax = axes[i],
24+
opts = ax[constants.name];
25+
26+
// Don't try calling getAutoRange if _min and _max are filled in.
27+
// This happens on updates where the calc step is skipped.
28+
29+
if(opts && opts.visible && opts.autorange && ax._min.length && ax._max.length) {
30+
opts._input.autorange = true;
31+
opts._input.range = opts.range = Axes.getAutoRange(ax);
32+
}
33+
}
34+
};

src/components/rangeslider/constants.js

+4-5
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,10 @@ module.exports = {
4141
grabAreaFill: 'transparent',
4242
grabAreaCursor: 'col-resize',
4343
grabAreaWidth: 10,
44-
grabAreaMinOffset: -6,
45-
grabAreaMaxOffset: -2,
4644

47-
handleWidth: 2,
45+
handleWidth: 4,
4846
handleRadius: 1,
49-
handleFill: '#fff',
50-
handleStroke: '#666',
47+
handleStrokeWidth: 1,
48+
49+
extraPad: 15
5150
};

src/components/rangeslider/defaults.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
var Lib = require('../../lib');
1212
var attributes = require('./attributes');
1313

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

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

30+
var visible = coerce('visible');
31+
if(!visible) return;
32+
3133
coerce('bgcolor', layoutOut.plot_bgcolor);
3234
coerce('bordercolor');
3335
coerce('borderwidth');
3436
coerce('thickness');
35-
coerce('visible');
37+
38+
coerce('autorange', !axOut.isValidRange(containerIn.range));
3639
coerce('range');
3740

3841
// Expand slider range to the axis range
39-
if(containerOut.range && !axOut.autorange) {
40-
// TODO: what if the ranges are reversed?
42+
// TODO: what if the ranges are reversed?
43+
if(containerOut.range) {
4144
var outRange = containerOut.range,
4245
axRange = axOut.range;
4346

4447
outRange[0] = axOut.l2r(Math.min(axOut.r2l(outRange[0]), axOut.r2l(axRange[0])));
4548
outRange[1] = axOut.l2r(Math.max(axOut.r2l(outRange[1]), axOut.r2l(axRange[1])));
46-
} else {
47-
axOut._needsExpand = true;
4849
}
4950

51+
axOut.cleanRange('rangeslider.range');
52+
5053
// to map back range slider (auto) range
5154
containerOut._input = containerIn;
5255
};

src/components/rangeslider/draw.js

+63-40
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,16 @@ module.exports = function(gd) {
7777
// for all present range sliders
7878
rangeSliders.each(function(axisOpts) {
7979
var rangeSlider = d3.select(this),
80-
opts = axisOpts[constants.name];
81-
82-
// compute new slider range using axis autorange if necessary
83-
// copy back range to input range slider container to skip
84-
// this step in subsequent draw calls
85-
if(!opts.range) {
86-
opts._input.range = opts.range = Axes.getAutoRange(axisOpts);
87-
}
80+
opts = axisOpts[constants.name],
81+
oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)];
8882

8983
// update range slider dimensions
9084

9185
var margin = fullLayout.margin,
9286
graphSize = fullLayout._size,
93-
domain = axisOpts.domain;
87+
domain = axisOpts.domain,
88+
oppDomain = oppAxisOpts.domain,
89+
tickHeight = (axisOpts._boundingBox || {}).height || 0;
9490

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

102-
var x = margin.l + (graphSize.w * domain[0]),
103-
y = fullLayout.height - opts._height - margin.b;
98+
var x = Math.round(margin.l + (graphSize.w * domain[0]));
99+
100+
var y = Math.round(
101+
margin.t + graphSize.h * (1 - oppDomain[0]) +
102+
tickHeight +
103+
opts._offsetShift + constants.extraPad
104+
);
104105

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

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

139140
// update margins
140141

141-
var bb = axisOpts._boundingBox ? axisOpts._boundingBox.height : 0;
142-
143142
Plots.autoMargin(gd, opts._id, {
144-
x: 0, y: 0, l: 0, r: 0, t: 0,
145-
b: opts._height + fullLayout.margin.b + bb,
146-
pad: 15 + opts._offsetShift * 2
143+
x: domain[0],
144+
y: oppDomain[0],
145+
l: 0,
146+
r: 0,
147+
t: 0,
148+
b: opts._height + margin.b + tickHeight,
149+
pad: constants.extraPad + opts._offsetShift * 2
147150
});
151+
148152
});
149153
};
150154

151155
function makeRangeSliderData(fullLayout) {
152-
if(!fullLayout.xaxis) return [];
153-
if(!fullLayout.xaxis[constants.name]) return [];
154-
if(!fullLayout.xaxis[constants.name].visible) return [];
155-
if(fullLayout._has('gl2d')) return [];
156+
var axes = Axes.list({ _fullLayout: fullLayout }, 'x', true),
157+
name = constants.name,
158+
out = [];
156159

157-
return [fullLayout.xaxis];
160+
if(fullLayout._has('gl2d')) return out;
161+
162+
for(var i = 0; i < axes.length; i++) {
163+
var ax = axes[i];
164+
165+
if(ax[name] && ax[name].visible) out.push(ax);
166+
}
167+
168+
return out;
158169
}
159170

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

238249
window.requestAnimationFrame(function() {
239-
Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]);
250+
Plotly.relayout(gd, axisOpts._name + '.range', [dataMin, dataMax]);
240251
});
241252
}
242253

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

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

261+
function clampHandle(v) {
262+
return Lib.constrain(v, -hw2, opts._width + hw2);
263+
}
264+
249265
var pixelMin = clamp(opts.d2p(axisOpts._rl[0])),
250266
pixelMax = clamp(opts.d2p(axisOpts._rl[1]));
251267

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

279+
// add offset for crispier corners
280+
// https://github.com/plotly/plotly.js/pull/1409
281+
var offset = 0.5;
282+
283+
var xMin = Math.round(clampHandle(pixelMin - hw2)) - offset,
284+
xMax = Math.round(clampHandle(pixelMax - hw2)) + offset;
285+
263286
rangeSlider.select('g.' + constants.grabberMinClassName)
264-
.attr('transform', 'translate(' + (pixelMin - constants.handleWidth - 1) + ',0)');
287+
.attr('transform', 'translate(' + xMin + ',' + offset + ')');
265288

266289
rangeSlider.select('g.' + constants.grabberMaxClassName)
267-
.attr('transform', 'translate(' + pixelMax + ',0)');
290+
.attr('transform', 'translate(' + xMax + ',' + offset + ')');
268291
}
269292

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

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

288312
bg.attr({
289313
width: opts._width + borderCorrect,
290314
height: opts._height + borderCorrect,
291315
transform: 'translate(' + offsetShift + ',' + offsetShift + ')',
292316
fill: opts.bgcolor,
293317
stroke: opts.bordercolor,
294-
'stroke-width': opts.borderwidth,
318+
'stroke-width': lw
295319
});
296320
}
297321

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

405429
maskMin.enter().append('rect')
406430
.classed(constants.maskMinClassName, true)
407-
.attr({ x: 0, y: 0 });
431+
.attr({ x: 0, y: 0 })
432+
.attr('shape-rendering', 'crispEdges');
408433

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

416441
maskMax.enter().append('rect')
417442
.classed(constants.maskMaxClassName, true)
418-
.attr('y', 0);
443+
.attr('y', 0)
444+
.attr('shape-rendering', 'crispEdges');
419445

420446
maskMax
421447
.attr('height', opts._height)
@@ -431,7 +457,8 @@ function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
431457
slideBox.enter().append('rect')
432458
.classed(constants.slideBoxClassName, true)
433459
.attr('y', 0)
434-
.attr('cursor', constants.slideBoxCursor);
460+
.attr('cursor', constants.slideBoxCursor)
461+
.attr('shape-rendering', 'crispEdges');
435462

436463
slideBox.attr({
437464
height: opts._height,
@@ -459,14 +486,15 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
459486
x: 0,
460487
width: constants.handleWidth,
461488
rx: constants.handleRadius,
462-
fill: constants.handleFill,
463-
stroke: constants.handleStroke,
489+
fill: Color.background,
490+
stroke: Color.defaultLine,
491+
'stroke-width': constants.handleStrokeWidth,
464492
'shape-rendering': 'crispEdges'
465493
};
466494

467495
var handleDynamicAttrs = {
468-
y: opts._height / 4,
469-
height: opts._height / 2,
496+
y: Math.round(opts._height / 4),
497+
height: Math.round(opts._height / 2),
470498
};
471499

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

490518
var grabAreaFixAttrs = {
491519
width: constants.grabAreaWidth,
520+
x: 0,
492521
y: 0,
493522
fill: constants.grabAreaFill,
494523
cursor: constants.grabAreaCursor
@@ -499,20 +528,14 @@ function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
499528
grabAreaMin.enter().append('rect')
500529
.classed(constants.grabAreaMinClassName, true)
501530
.attr(grabAreaFixAttrs);
502-
grabAreaMin.attr({
503-
x: constants.grabAreaMinOffset,
504-
height: opts._height
505-
});
531+
grabAreaMin.attr('height', opts._height);
506532

507533
var grabAreaMax = grabberMax.selectAll('rect.' + constants.grabAreaMaxClassName)
508534
.data([0]);
509535
grabAreaMax.enter().append('rect')
510536
.classed(constants.grabAreaMaxClassName, true)
511537
.attr(grabAreaFixAttrs);
512-
grabAreaMax.attr({
513-
x: constants.grabAreaMaxOffset,
514-
height: opts._height
515-
});
538+
grabAreaMax.attr('height', opts._height);
516539
}
517540

518541
function clearPushMargins(gd) {

src/components/rangeslider/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ module.exports = {
2020

2121
layoutAttributes: require('./attributes'),
2222
handleDefaults: require('./defaults'),
23-
23+
calcAutorange: require('./calc_autorange'),
2424
draw: require('./draw')
2525
};

src/plot_api/plot_api.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ Plotly.plot = function(gd, data, layout, config) {
189189
}
190190

191191
// draw anything that can affect margins.
192-
// currently this is legend and colorbars
193192
function marginPushers() {
194193
var calcdata = gd.calcdata;
195194
var i, cd, trace;
@@ -253,7 +252,8 @@ Plotly.plot = function(gd, data, layout, config) {
253252
return Lib.syncOrAsync([
254253
Registry.getComponentMethod('shapes', 'calcAutorange'),
255254
Registry.getComponentMethod('annotations', 'calcAutorange'),
256-
doAutoRange
255+
doAutoRange,
256+
Registry.getComponentMethod('rangeslider', 'calcAutorange')
257257
], gd);
258258
}
259259

src/plots/cartesian/axes.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,9 @@ axes.saveRangeInitial = function(gd, overwrite) {
378378
// tozero: (boolean) make sure to include zero if axis is linear,
379379
// and make it a tight bound if possible
380380
axes.expand = function(ax, data, options) {
381-
if(!(ax.autorange || ax._needsExpand) || !data) return;
381+
var needsAutorange = (ax.autorange || Lib.nestedProperty(ax, 'rangeslider.autorange'));
382+
if(!needsAutorange || !data) return;
383+
382384
if(!ax._min) ax._min = [];
383385
if(!ax._max) ax._max = [];
384386
if(!options) options = {};

src/plots/cartesian/axis_defaults.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
'use strict';
1111

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

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

96-
var validRange = (
97-
(containerIn.range || []).length === 2 &&
98-
isNumeric(containerOut.r2l(containerIn.range[0])) &&
99-
isNumeric(containerOut.r2l(containerIn.range[1]))
100-
);
101-
var autoRange = coerce('autorange', !validRange);
95+
var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range));
10296

10397
if(autoRange) coerce('rangemode');
10498

0 commit comments

Comments
 (0)