Skip to content

Commit beaec8d

Browse files
authored
Merge pull request #2364 from TomDemulierChevret/rangeslider-allow-zoom-on-oppaxis
Rangeslider allow zoom on oppaxis
2 parents 2ff1c85 + 4f54f62 commit beaec8d

File tree

14 files changed

+1118
-620
lines changed

14 files changed

+1118
-620
lines changed

src/components/rangeslider/attributes.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@ module.exports = {
1515
valType: 'color',
1616
dflt: colorAttributes.background,
1717
role: 'style',
18-
editType: 'calc',
18+
editType: 'plot',
1919
description: 'Sets the background color of the range slider.'
2020
},
2121
bordercolor: {
2222
valType: 'color',
2323
dflt: colorAttributes.defaultLine,
2424
role: 'style',
25-
editType: 'calc',
25+
editType: 'plot',
2626
description: 'Sets the border color of the range slider.'
2727
},
2828
borderwidth: {
2929
valType: 'integer',
3030
dflt: 0,
3131
min: 0,
3232
role: 'style',
33-
editType: 'calc',
33+
editType: 'plot',
3434
description: 'Sets the border color of the range slider.'
3535
},
3636
autorange: {
@@ -73,7 +73,7 @@ module.exports = {
7373
min: 0,
7474
max: 1,
7575
role: 'style',
76-
editType: 'calc',
76+
editType: 'plot',
7777
description: [
7878
'The height of the range slider as a fraction of the',
7979
'total plot area height.'

src/components/rangeslider/constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ module.exports = {
3131
grabAreaMaxClassName: 'rangeslider-grabarea-max',
3232
handleMaxClassName: 'rangeslider-handle-max',
3333

34+
maskMinOppAxisClassName: 'rangeslider-mask-min-opp-axis',
35+
maskMaxOppAxisClassName: 'rangeslider-mask-max-opp-axis',
36+
3437
// style constants
3538

3639
maskColor: 'rgba(0,0,0,0.4)',
40+
maskOppAxisColor: 'rgba(0,0,0,0.2)',
3741

3842
slideBoxFill: 'transparent',
3943
slideBoxCursor: 'ew-resize',

src/components/rangeslider/defaults.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
var Lib = require('../../lib');
1212
var attributes = require('./attributes');
13+
var oppAxisAttrs = require('./oppaxis_attributes');
14+
var axisIds = require('../../plots/cartesian/axis_ids');
1315

1416
module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
1517
if(!layoutIn[axName].rangeslider) return;
@@ -27,6 +29,10 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
2729
return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
2830
}
2931

32+
function coerceRange(rangeContainerIn, rangeContainerOut, attr, dflt) {
33+
return Lib.coerce(rangeContainerIn, rangeContainerOut, oppAxisAttrs, attr, dflt);
34+
}
35+
3036
var visible = coerce('visible');
3137
if(!visible) return;
3238

@@ -35,9 +41,40 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
3541
coerce('borderwidth');
3642
coerce('thickness');
3743

38-
coerce('autorange', !axOut.isValidRange(containerIn.range));
44+
axOut._rangesliderAutorange = coerce('autorange', !axOut.isValidRange(containerIn.range));
3945
coerce('range');
4046

47+
var subplots = layoutOut._subplots;
48+
if(subplots) {
49+
var yIds = subplots.cartesian
50+
.filter(function(subplotId) {
51+
return subplotId.substr(0, subplotId.indexOf('y')) === axisIds.name2id(axName);
52+
})
53+
.map(function(subplotId) {
54+
return subplotId.substr(subplotId.indexOf('y'), subplotId.length);
55+
});
56+
var yNames = Lib.simpleMap(yIds, axisIds.id2name);
57+
for(var i = 0; i < yNames.length; i++) {
58+
var yName = yNames[i];
59+
60+
var rangeContainerIn = containerIn[yName] || {};
61+
var rangeContainerOut = containerOut[yName] = {};
62+
63+
var yAxOut = layoutOut[yName];
64+
65+
var rangemodeDflt;
66+
if(rangeContainerIn.range && yAxOut.isValidRange(rangeContainerIn.range)) {
67+
rangemodeDflt = 'fixed';
68+
}
69+
70+
var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt);
71+
if(rangeMode !== 'match') {
72+
coerceRange(rangeContainerIn, rangeContainerOut, 'range', yAxOut.range.slice());
73+
}
74+
yAxOut._rangesliderAutorange = (rangeMode === 'auto');
75+
}
76+
}
77+
4178
// to map back range slider (auto) range
4279
containerOut._input = containerIn;
4380
};

src/components/rangeslider/draw.js

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ 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];
80+
opts = axisOpts[constants.name],
81+
oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)],
82+
oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)];
8183

8284
// update range
8385
// Expand slider range to the axis range
@@ -141,21 +143,31 @@ module.exports = function(gd) {
141143

142144
opts._rl = [range0, range1];
143145

146+
if(oppAxisRangeOpts.rangemode !== 'match') {
147+
var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]),
148+
range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[1]),
149+
distOppAxis = range1OppAxis - range0OppAxis;
150+
151+
opts.d2pOppAxis = function(v) {
152+
return (v - range0OppAxis) / distOppAxis * opts._height;
153+
};
154+
}
155+
144156
// update inner nodes
145157

146158
rangeSlider
147159
.call(drawBg, gd, axisOpts, opts)
148160
.call(addClipPath, gd, axisOpts, opts)
149161
.call(drawRangePlot, gd, axisOpts, opts)
150-
.call(drawMasks, gd, axisOpts, opts)
162+
.call(drawMasks, gd, axisOpts, opts, oppAxisRangeOpts)
151163
.call(drawSlideBox, gd, axisOpts, opts)
152164
.call(drawGrabbers, gd, axisOpts, opts);
153165

154166
// setup drag element
155167
setupDragElement(rangeSlider, gd, axisOpts, opts);
156168

157169
// update current range
158-
setPixelRange(rangeSlider, gd, axisOpts, opts);
170+
setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts);
159171

160172
// title goes next to range slider instead of tick labels, so
161173
// just take it over and draw it from here
@@ -284,13 +296,17 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) {
284296
});
285297
}
286298

287-
function setPixelRange(rangeSlider, gd, axisOpts, opts) {
299+
function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts) {
288300
var hw2 = constants.handleWidth / 2;
289301

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

306+
function clampOppAxis(v) {
307+
return Lib.constrain(v, 0, opts._height);
308+
}
309+
294310
function clampHandle(v) {
295311
return Lib.constrain(v, -hw2, opts._width + hw2);
296312
}
@@ -309,6 +325,26 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) {
309325
.attr('x', pixelMax)
310326
.attr('width', opts._width - pixelMax);
311327

328+
if(oppAxisRangeOpts.rangemode !== 'match') {
329+
var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])),
330+
pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0]));
331+
332+
rangeSlider.select('rect.' + constants.maskMinOppAxisClassName)
333+
.attr('x', pixelMin)
334+
.attr('height', pixelMinOppAxis)
335+
.attr('width', pixelMax - pixelMin);
336+
337+
rangeSlider.select('rect.' + constants.maskMaxOppAxisClassName)
338+
.attr('x', pixelMin)
339+
.attr('y', pixelMaxOppAxis)
340+
.attr('height', opts._height - pixelMaxOppAxis)
341+
.attr('width', pixelMax - pixelMin);
342+
343+
rangeSlider.select('rect.' + constants.slideBoxClassName)
344+
.attr('y', pixelMinOppAxis)
345+
.attr('height', pixelMaxOppAxis - pixelMinOppAxis);
346+
}
347+
312348
// add offset for crispier corners
313349
// https://github.com/plotly/plotly.js/pull/1409
314350
var offset = 0.5;
@@ -391,7 +427,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
391427
isMainPlot = (i === 0);
392428

393429
var oppAxisOpts = Axes.getFromId(gd, id, 'y'),
394-
oppAxisName = oppAxisOpts._name;
430+
oppAxisName = oppAxisOpts._name,
431+
oppAxisRangeOpts = opts[oppAxisName];
395432

396433
var mockFigure = {
397434
data: [],
@@ -412,7 +449,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
412449
mockFigure.layout[oppAxisName] = {
413450
type: oppAxisOpts.type,
414451
domain: [0, 1],
415-
range: oppAxisOpts.range.slice(),
452+
range: oppAxisRangeOpts.rangemode !== 'match' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(),
416453
calendar: oppAxisOpts.calendar
417454
};
418455

@@ -453,7 +490,7 @@ function filterRangePlotCalcData(calcData, subplotId) {
453490
return out;
454491
}
455492

456-
function drawMasks(rangeSlider, gd, axisOpts, opts) {
493+
function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) {
457494
var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName)
458495
.data([0]);
459496

@@ -477,6 +514,34 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) {
477514
maskMax
478515
.attr('height', opts._height)
479516
.call(Color.fill, constants.maskColor);
517+
518+
// masks used for oppAxis zoom
519+
if(oppAxisRangeOpts.rangemode !== 'match') {
520+
var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName)
521+
.data([0]);
522+
523+
maskMinOppAxis.enter().append('rect')
524+
.classed(constants.maskMinOppAxisClassName, true)
525+
.attr('y', 0)
526+
.attr('shape-rendering', 'crispEdges');
527+
528+
maskMinOppAxis
529+
.attr('width', opts._width)
530+
.call(Color.fill, constants.maskOppAxisColor);
531+
532+
var maskMaxOppAxis = rangeSlider.selectAll('rect.' + constants.maskMaxOppAxisClassName)
533+
.data([0]);
534+
535+
maskMaxOppAxis.enter().append('rect')
536+
.classed(constants.maskMaxOppAxisClassName, true)
537+
.attr('y', 0)
538+
.attr('shape-rendering', 'crispEdges');
539+
540+
maskMaxOppAxis
541+
.attr('width', opts._width)
542+
.style('border-top', constants.maskOppBorder)
543+
.call(Color.fill, constants.maskOppAxisColor);
544+
}
480545
}
481546

482547
function drawSlideBox(rangeSlider, gd, axisOpts, opts) {

src/components/rangeslider/index.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88

99
'use strict';
1010

11+
var Lib = require('../../lib');
12+
var attrs = require('./attributes');
13+
var oppAxisAttrs = require('./oppaxis_attributes');
14+
1115
module.exports = {
1216
moduleType: 'component',
1317
name: 'rangeslider',
1418

1519
schema: {
1620
subplots: {
17-
xaxis: {rangeslider: require('./attributes')}
21+
xaxis: {
22+
rangeslider: Lib.extendFlat({}, attrs, {
23+
yaxis: oppAxisAttrs
24+
})
25+
}
1826
}
1927
},
2028

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright 2012-2018, 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+
module.exports = {
12+
// not really a 'subplot' attribute container,
13+
// but this is the flag we use to denote attributes that
14+
// support yaxis, yaxis2, yaxis3, ... counters
15+
_isSubplotObj: true,
16+
17+
rangemode: {
18+
valType: 'enumerated',
19+
values: ['auto', 'fixed', 'match'],
20+
dflt: 'match',
21+
role: 'style',
22+
editType: 'calc',
23+
description: [
24+
'Determines whether or not the range of this axis in',
25+
'the rangeslider use the same value than in the main plot',
26+
'when zooming in/out.',
27+
'If *auto*, the autorange will be used.',
28+
'If *fixed*, the `range` is used.',
29+
'If *match*, the current range of the corresponding y-axis on the main subplot is used.'
30+
].join(' ')
31+
},
32+
range: {
33+
valType: 'info_array',
34+
role: 'style',
35+
items: [
36+
{valType: 'any', editType: 'plot'},
37+
{valType: 'any', editType: 'plot'}
38+
],
39+
editType: 'plot',
40+
description: [
41+
'Sets the range of this axis for the rangeslider.'
42+
].join(' ')
43+
},
44+
editType: 'calc'
45+
};

src/plots/cartesian/autorange.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,11 @@ function makePadFn(ax) {
174174
}
175175

176176
function doAutoRange(ax) {
177-
ax.setScale();
177+
if(!ax._length) ax.setScale();
178178

179179
// TODO do we really need this?
180180
var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length);
181+
var axIn;
181182

182183
if(ax.autorange && hasDeps) {
183184
ax.range = getAutoRange(ax);
@@ -188,14 +189,29 @@ function doAutoRange(ax) {
188189
// doAutoRange will get called on fullLayout,
189190
// but we want to report its results back to layout
190191

191-
var axIn = ax._input;
192+
axIn = ax._input;
192193
axIn.range = ax.range.slice();
193194
axIn.autorange = ax.autorange;
194195
}
196+
197+
if(ax._anchorAxis && ax._anchorAxis.rangeslider) {
198+
var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name];
199+
if(axeRangeOpts) {
200+
if(axeRangeOpts.rangemode === 'auto') {
201+
if(hasDeps) {
202+
axeRangeOpts.range = getAutoRange(ax);
203+
} else {
204+
axeRangeOpts.range = ax._rangeInitial ? ax._rangeInitial.slice() : ax.range.slice();
205+
}
206+
}
207+
}
208+
axIn = ax._anchorAxis._input;
209+
axIn.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts);
210+
}
195211
}
196212

197213
function needsAutorange(ax) {
198-
return ax.autorange || !!(ax.rangeslider || {}).autorange;
214+
return ax.autorange || ax._rangesliderAutorange;
199215
}
200216

201217
/*

src/plots/cartesian/axis_defaults.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,
4949

5050
var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range));
5151

52+
// both x and y axes may need autorange done just for the range slider's purposes
53+
// the logic is complicated to figure this out later, particularly for y axes since
54+
// the settings can be spread out in the x axes... so instead we'll collect them
55+
// during supplyDefaults
56+
containerOut._rangesliderAutorange = false;
57+
5258
if(autoRange) coerce('rangemode');
5359

5460
coerce('range');
33.8 KB
Loading

0 commit comments

Comments
 (0)