Skip to content
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

Rangeslider allow zoom on oppaxis #2364

Merged
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
519e1a7
better visual cue of zoom on oppaxis with rangeslider
TomDemulierChevret Feb 14, 2018
044e633
add a new attributes to rangeslider to switch back to the old behaviour
TomDemulierChevret Feb 16, 2018
f335258
fix check when using new data
TomDemulierChevret Feb 16, 2018
707f432
fix default value rangeslider test
TomDemulierChevret Feb 16, 2018
6a7f652
renammed new attributes to "fixedyrange"
TomDemulierChevret Feb 16, 2018
fc4f082
Introduce new attributes to axes to allow to specifiy fixed range to …
TomDemulierChevret Feb 19, 2018
92c6550
Fix broken test from attribute removal
TomDemulierChevret Feb 19, 2018
b30167c
Add missing gl attribute
TomDemulierChevret Feb 19, 2018
91caff1
Merge branch 'master' into rangeslider-allow-zoom-on-oppaxis
TomDemulierChevret Feb 19, 2018
3dd91b5
Renammed new attributes for more clarity and portability
TomDemulierChevret Feb 20, 2018
00fd1d7
Coerce of new attributes is only made for y axis
TomDemulierChevret Feb 20, 2018
5e0486a
Merge remote-tracking branch 'upstream/master' into rangeslider-allow…
TomDemulierChevret Feb 20, 2018
32488f1
Changed rangeslidermode values for more clarity
TomDemulierChevret Feb 20, 2018
bc2a40a
Switch to new rangeslider range opts (default missing)
TomDemulierChevret Feb 28, 2018
d3b6071
Added default attributes
TomDemulierChevret Feb 28, 2018
b496641
Remove old attributes
TomDemulierChevret Feb 28, 2018
149ef70
Add autorange computation
TomDemulierChevret Feb 28, 2018
54d8b58
Merge remote-tracking branch 'upstream/master' into rangeslider-allow…
TomDemulierChevret Feb 28, 2018
dd48914
Add missing check
TomDemulierChevret Feb 28, 2018
a8a3ee5
Add missing catch to test to retrieve the tracebacks
TomDemulierChevret Mar 2, 2018
0ae2831
Change yAxis rangeslider rangeOpts computation + fix yAxis rangeslide…
TomDemulierChevret Mar 2, 2018
8764e4d
Add missing check
TomDemulierChevret Mar 2, 2018
a17d3a8
Fix rangeslider test
TomDemulierChevret Mar 2, 2018
e85e3d0
Renamed parameter to keep name consistency across the file
TomDemulierChevret Mar 2, 2018
0dd8562
Merge remote-tracking branch 'upstream/master' into rangeslider-allow…
TomDemulierChevret Mar 2, 2018
3929be6
Changes requested by @alexcjohnson
TomDemulierChevret Mar 2, 2018
e2ad33d
Changes requested by @alexcjohnson
TomDemulierChevret Mar 2, 2018
df85f35
Changes requested by @etpinard
TomDemulierChevret Mar 5, 2018
fe83e07
Default range for 'fixed' rangemode value is now the range of the axe
TomDemulierChevret Mar 5, 2018
3b6b81a
Changes requested by @etpinard
TomDemulierChevret Mar 6, 2018
9de4e3e
Changes requested by @etpinard
TomDemulierChevret Mar 6, 2018
f2819c8
Changes requested by @etpinard
TomDemulierChevret Mar 6, 2018
df96a13
Merge branch 'master' into rangeslider-allow-zoom-on-oppaxis
TomDemulierChevret Mar 6, 2018
9c66f81
Add test for new attributes default and coerce
TomDemulierChevret Mar 7, 2018
f092c44
Add interaction test for new attributes
TomDemulierChevret Mar 7, 2018
041ce3d
Fixed autorange test precision
TomDemulierChevret Mar 7, 2018
23c8401
Fixed autorange test precision
TomDemulierChevret Mar 7, 2018
73560f6
Merge branch 'master' into rangeslider-allow-zoom-on-oppaxis
alexcjohnson Mar 7, 2018
c556f8c
rename range_attributes file -> oppaxis_attributes
etpinard Mar 7, 2018
43f15a7
add rangeslider.yaxis? to schema flagging it with _isSubplotObj
etpinard Mar 7, 2018
ccb6091
fix rangeslider.y.rangemode=auto and add a test image
alexcjohnson Mar 7, 2018
6a88b24
Merge branch 'rangeslider-allow-zoom-on-oppaxis' of github.com:TomDem…
alexcjohnson Mar 7, 2018
95b8c10
test turning on autorange via rangeslider attrs
alexcjohnson Mar 7, 2018
4f54f62
drop rangeslider style attributes to editType: plot and test
alexcjohnson Mar 7, 2018
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
8 changes: 4 additions & 4 deletions src/components/rangeslider/attributes.js
Original file line number Diff line number Diff line change
@@ -15,22 +15,22 @@ module.exports = {
valType: 'color',
dflt: colorAttributes.background,
role: 'style',
editType: 'calc',
editType: 'plot',
description: 'Sets the background color of the range slider.'
},
bordercolor: {
valType: 'color',
dflt: colorAttributes.defaultLine,
role: 'style',
editType: 'calc',
editType: 'plot',
description: 'Sets the border color of the range slider.'
},
borderwidth: {
valType: 'integer',
dflt: 0,
min: 0,
role: 'style',
editType: 'calc',
editType: 'plot',
description: 'Sets the border color of the range slider.'
},
autorange: {
@@ -73,7 +73,7 @@ module.exports = {
min: 0,
max: 1,
role: 'style',
editType: 'calc',
editType: 'plot',
description: [
'The height of the range slider as a fraction of the',
'total plot area height.'
4 changes: 4 additions & 0 deletions src/components/rangeslider/constants.js
Original file line number Diff line number Diff line change
@@ -31,9 +31,13 @@ module.exports = {
grabAreaMaxClassName: 'rangeslider-grabarea-max',
handleMaxClassName: 'rangeslider-handle-max',

maskMinOppAxisClassName: 'rangeslider-mask-min-opp-axis',
maskMaxOppAxisClassName: 'rangeslider-mask-max-opp-axis',

// style constants

maskColor: 'rgba(0,0,0,0.4)',
maskOppAxisColor: 'rgba(0,0,0,0.2)',

slideBoxFill: 'transparent',
slideBoxCursor: 'ew-resize',
39 changes: 38 additions & 1 deletion src/components/rangeslider/defaults.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@

var Lib = require('../../lib');
var attributes = require('./attributes');
var oppAxisAttrs = require('./oppaxis_attributes');
var axisIds = require('../../plots/cartesian/axis_ids');

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

function coerceRange(rangeContainerIn, rangeContainerOut, attr, dflt) {
return Lib.coerce(rangeContainerIn, rangeContainerOut, oppAxisAttrs, attr, dflt);
}

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

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

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

var subplots = layoutOut._subplots;
if(subplots) {
var yIds = subplots.cartesian
.filter(function(subplotId) {
return subplotId.substr(0, subplotId.indexOf('y')) === axisIds.name2id(axName);
})
.map(function(subplotId) {
return subplotId.substr(subplotId.indexOf('y'), subplotId.length);
});
var yNames = Lib.simpleMap(yIds, axisIds.id2name);
for(var i = 0; i < yNames.length; i++) {
var yName = yNames[i];

var rangeContainerIn = containerIn[yName] || {};
var rangeContainerOut = containerOut[yName] = {};

var yAxOut = layoutOut[yName];

var rangemodeDflt;
if(rangeContainerIn.range && yAxOut.isValidRange(rangeContainerIn.range)) {
rangemodeDflt = 'fixed';
}

var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt);
if(rangeMode !== 'match') {
coerceRange(rangeContainerIn, rangeContainerOut, 'range', yAxOut.range.slice());
}
yAxOut._rangesliderAutorange = (rangeMode === 'auto');
}
}

// to map back range slider (auto) range
containerOut._input = containerIn;
};
79 changes: 72 additions & 7 deletions src/components/rangeslider/draw.js
Original file line number Diff line number Diff line change
@@ -77,7 +77,9 @@ module.exports = function(gd) {
// for all present range sliders
rangeSliders.each(function(axisOpts) {
var rangeSlider = d3.select(this),
opts = axisOpts[constants.name];
opts = axisOpts[constants.name],
oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)],
oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)];

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

opts._rl = [range0, range1];

if(oppAxisRangeOpts.rangemode !== 'match') {
var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]),
range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[1]),
distOppAxis = range1OppAxis - range0OppAxis;

opts.d2pOppAxis = function(v) {
return (v - range0OppAxis) / distOppAxis * opts._height;
};
}

// update inner nodes

rangeSlider
.call(drawBg, gd, axisOpts, opts)
.call(addClipPath, gd, axisOpts, opts)
.call(drawRangePlot, gd, axisOpts, opts)
.call(drawMasks, gd, axisOpts, opts)
.call(drawMasks, gd, axisOpts, opts, oppAxisRangeOpts)
.call(drawSlideBox, gd, axisOpts, opts)
.call(drawGrabbers, gd, axisOpts, opts);

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

// update current range
setPixelRange(rangeSlider, gd, axisOpts, opts);
setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts);

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

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

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

function clampOppAxis(v) {
return Lib.constrain(v, 0, opts._height);
}

function clampHandle(v) {
return Lib.constrain(v, -hw2, opts._width + hw2);
}
@@ -309,6 +325,26 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) {
.attr('x', pixelMax)
.attr('width', opts._width - pixelMax);

if(oppAxisRangeOpts.rangemode !== 'match') {
var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])),
pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0]));

rangeSlider.select('rect.' + constants.maskMinOppAxisClassName)
.attr('x', pixelMin)
.attr('height', pixelMinOppAxis)
.attr('width', pixelMax - pixelMin);

rangeSlider.select('rect.' + constants.maskMaxOppAxisClassName)
.attr('x', pixelMin)
.attr('y', pixelMaxOppAxis)
.attr('height', opts._height - pixelMaxOppAxis)
.attr('width', pixelMax - pixelMin);

rangeSlider.select('rect.' + constants.slideBoxClassName)
.attr('y', pixelMinOppAxis)
.attr('height', pixelMaxOppAxis - pixelMinOppAxis);
}

// add offset for crispier corners
// https://github.com/plotly/plotly.js/pull/1409
var offset = 0.5;
@@ -391,7 +427,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
isMainPlot = (i === 0);

var oppAxisOpts = Axes.getFromId(gd, id, 'y'),
oppAxisName = oppAxisOpts._name;
oppAxisName = oppAxisOpts._name,
oppAxisRangeOpts = opts[oppAxisName];

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

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

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

@@ -477,6 +514,34 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) {
maskMax
.attr('height', opts._height)
.call(Color.fill, constants.maskColor);

// masks used for oppAxis zoom
if(oppAxisRangeOpts.rangemode !== 'match') {
var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName)
.data([0]);

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

maskMinOppAxis
.attr('width', opts._width)
.call(Color.fill, constants.maskOppAxisColor);

var maskMaxOppAxis = rangeSlider.selectAll('rect.' + constants.maskMaxOppAxisClassName)
.data([0]);

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

maskMaxOppAxis
.attr('width', opts._width)
.style('border-top', constants.maskOppBorder)
.call(Color.fill, constants.maskOppAxisColor);
}
}

function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
10 changes: 9 additions & 1 deletion src/components/rangeslider/index.js
Original file line number Diff line number Diff line change
@@ -8,13 +8,21 @@

'use strict';

var Lib = require('../../lib');
var attrs = require('./attributes');
var oppAxisAttrs = require('./oppaxis_attributes');

module.exports = {
moduleType: 'component',
name: 'rangeslider',

schema: {
subplots: {
xaxis: {rangeslider: require('./attributes')}
xaxis: {
rangeslider: Lib.extendFlat({}, attrs, {
yaxis: oppAxisAttrs
})
}
}
},

45 changes: 45 additions & 0 deletions src/components/rangeslider/oppaxis_attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright 2012-2018, 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 = {
// not really a 'subplot' attribute container,
// but this is the flag we use to denote attributes that
// support yaxis, yaxis2, yaxis3, ... counters
_isSubplotObj: true,

rangemode: {
valType: 'enumerated',
values: ['auto', 'fixed', 'match'],
dflt: 'match',
role: 'style',
editType: 'calc',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alexcjohnson we should be able to change these to editType: 'plot', correct?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh hmm... actually I don't think we can, it's like ax.autorange which is still calc, because this affects _rangesliderAutorange which determines whether we even bother calculating ax._min/_max during the calc step.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I tried switching them to 'plot', but that didn't break any test 😕

description: [
'Determines whether or not the range of this axis in',
'the rangeslider use the same value than in the main plot',
'when zooming in/out.',
'If *auto*, the autorange will be used.',
'If *fixed*, the `range` is used.',
'If *match*, the current range of the corresponding y-axis on the main subplot is used.'
].join(' ')
},
range: {
valType: 'info_array',
role: 'style',
items: [
{valType: 'any', editType: 'plot'},
{valType: 'any', editType: 'plot'}
],
editType: 'plot',
description: [
'Sets the range of this axis for the rangeslider.'
].join(' ')
},
editType: 'calc'
};
22 changes: 19 additions & 3 deletions src/plots/cartesian/autorange.js
Original file line number Diff line number Diff line change
@@ -174,10 +174,11 @@ function makePadFn(ax) {
}

function doAutoRange(ax) {
ax.setScale();
if(!ax._length) ax.setScale();

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

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

var axIn = ax._input;
axIn = ax._input;
axIn.range = ax.range.slice();
axIn.autorange = ax.autorange;
}

if(ax._anchorAxis && ax._anchorAxis.rangeslider) {
var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name];
if(axeRangeOpts) {
if(axeRangeOpts.rangemode === 'auto') {
if(hasDeps) {
axeRangeOpts.range = getAutoRange(ax);
} else {
axeRangeOpts.range = ax._rangeInitial ? ax._rangeInitial.slice() : ax.range.slice();
}
}
}
axIn = ax._anchorAxis._input;
axIn.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts);
}
}

function needsAutorange(ax) {
return ax.autorange || !!(ax.rangeslider || {}).autorange;
return ax.autorange || ax._rangesliderAutorange;
}

/*
6 changes: 6 additions & 0 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
@@ -49,6 +49,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,

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

// both x and y axes may need autorange done just for the range slider's purposes
// the logic is complicated to figure this out later, particularly for y axes since
// the settings can be spread out in the x axes... so instead we'll collect them
// during supplyDefaults
containerOut._rangesliderAutorange = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clear comment. Thanks!


if(autoRange) coerce('rangemode');

coerce('range');
Binary file added test/image/baselines/range_slider_rangemode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions test/image/mocks/range_slider_rangemode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"data": [
{"y": [4, 7e7, 5e5, 6e8, 3e2], "type": "bar"},
{"y": [1, 4, 2, 6, 3], "xaxis": "x2", "yaxis": "y2"}
],
"layout": {
"xaxis": {
"domain": [0, 0.42],
"range": [1, 3],
"rangeslider": {
"yaxis": {"rangemode": "auto"}
},
"title": "Rangeslider Y rangemode auto"
},
"xaxis2": {
"anchor": "y2",
"domain": [0.58, 1],
"range": [1.5, 3.5],
"rangeslider": {
"range": [-2, 4],
"yaxis2": {"rangemode": "fixed", "range": [0, 10]}
},
"title": "Rangeslider Y2 rangemode fixed"
},
"yaxis": {"type": "log", "range": [2, 6], "title": "Y explicit range"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice mock!

"yaxis2": {"anchor": "x2", "title": "Y2 autoranged"},
"width": 700,
"height": 500
}
}
2 changes: 1 addition & 1 deletion test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
@@ -1876,7 +1876,7 @@ describe('Test axes', function() {
data = [2, 5];

ax.autorange = false;
ax.rangeslider = { autorange: true };
ax._rangesliderAutorange = true;

expand(ax, data, {});
expect(ax._min).toEqual([{val: 2, pad: 0, extrapad: false}]);
12 changes: 9 additions & 3 deletions test/jasmine/tests/plotschema_test.js
Original file line number Diff line number Diff line change
@@ -126,9 +126,15 @@ describe('plot schema', function() {
});

it('all subplot objects should contain _isSubplotObj', function() {
var IS_SUBPLOT_OBJ = '_isSubplotObj',
astrs = ['xaxis', 'yaxis', 'scene', 'geo', 'ternary', 'mapbox', 'polar'],
cnt = 0;
var IS_SUBPLOT_OBJ = '_isSubplotObj';
var cnt = 0;

var astrs = [
'xaxis', 'yaxis', 'scene', 'geo', 'ternary', 'mapbox', 'polar',
// not really a 'subplot' object but supports yaxis, yaxis2, yaxis3,
// ... counters, so list it here
'xaxis.rangeslider.yaxis'
];

// check if the subplot objects have '_isSubplotObj'
astrs.forEach(function(astr) {
1,460 changes: 860 additions & 600 deletions test/jasmine/tests/range_slider_test.js

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions test/jasmine/tests/validate_test.js
Original file line number Diff line number Diff line change
@@ -505,4 +505,25 @@ describe('Plotly.validate', function() {
'In layout, key grid.subplots[2][0] is set to an invalid value (5)'
);
});

it('should detect opposite axis range slider attributes', function() {
var out = Plotly.validate([
{y: [1, 2]},
{y: [1, 2], yaxis: 'y2'},
{y: [1, 2], yaxis: 'y3'}
], {
xaxis: {
rangeslider: {
yaxis: { rangemode: 'auto' },
yaxis2: { rangemode: 'fixed' },
yaxis3: { range: [0, 1] }
}
},
yaxis: {},
yaxis2: {},
yaxis3: {}
});

expect(out).toBeUndefined();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As promised in https://github.com/plotly/plotly.js/pull/2364/files#r171957082

Please note, the _isSubplotObj flag in the range slider opposite axis attributes is very important. Without it, this test would spit out:

image

that is Plotly.validate would think that rangeslider.yaxis2 and rangeslider.yaxis3 aren't part of the schema.

Copy link
Contributor

@etpinard etpinard Mar 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this appears to work in plotly.py aswell.

Copying the new plot-schema.json over there gives:

image

i.e. does not fail like:

image

cc @cldougl @Kully

});
});