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
Show file tree
Hide file tree
Changes from 39 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
4 changes: 4 additions & 0 deletions src/components/rangeslider/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
34 changes: 34 additions & 0 deletions src/components/rangeslider/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -38,6 +44,34 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a bit tricky actually - we don't want all y axes, we only want the ones that apply to this x axis. It might be sufficient to look at subplots.cartesian, filtering on the x axis ID and taking the y axis IDs (note that you can't do this with subplotId.indexOf(xId) !== -1 because x is a valid ID - you have to do something like subplotId.substr(0, subplotId.indexOf('y')) === xId, like here)

I say might because I'm not sure what happens with rangesliders now if we have overlaying x axes... that would be very weird and confusing, so I think I'd be OK ignoring the y data for these axes, but we should confirm that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Couldn't we just use : layoutOut[yName].anchor === axisIds.name2id(axName) in the for loop ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

In principle you can have a y axis shared across several x axes, or positioned as a free axis, and not anchored to the one with the range slider, so unfortunately I don't think that will work.

for(var i = 0; i < yNames.length; i++) {
var yName = yNames[i];

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

var rangemodeDflt;
if(rangeContainerIn.range && layoutOut[yName].isValidRange(rangeContainerIn.range)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. The default rangemode value should be 'match' to match (:laughing: ) the current behavior. We could change that in v2, but in the meantime we'll have to make sure this PR doesn't induce a breaking change,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

rangemodeDflt = 'fixed';
}

var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt);
if(rangeMode !== 'match') {
coerceRange(rangeContainerIn, rangeContainerOut, 'range', layoutOut[yName].range.slice());
}
}
}

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

// update range
// Expand slider range to the axis range
Expand Down Expand Up @@ -135,21 +136,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);

// update margins

Expand Down Expand Up @@ -265,13 +276,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);
}
Expand All @@ -290,6 +305,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;
Expand Down Expand Up @@ -372,7 +407,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: [],
Expand All @@ -393,7 +429,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
};

Expand Down Expand Up @@ -434,7 +470,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]);

Expand All @@ -458,6 +494,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) {
Expand Down
10 changes: 9 additions & 1 deletion src/components/rangeslider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
}
},

Expand Down
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: 'calc',
description: [
'Sets the range of this axis for the rangeslider.'
].join(' ')
},
editType: 'calc'
};
20 changes: 18 additions & 2 deletions src/plots/cartesian/autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -188,10 +189,25 @@ 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) {
Expand Down
12 changes: 9 additions & 3 deletions test/jasmine/tests/plotschema_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading