8000 Implementing matching axes by etpinard · Pull Request #3506 · plotly/plotly.js · GitHub
[go: up one dir, main page]

Skip to content

Implementing matching axes #3506

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 23 commits into from
Feb 18, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6813865
mv handleConstraintDefaults into constraints.js file
etpinard Jan 24, 2019
6f2bb78
first cut at static `ax.matches` behavior
etpinard Jan 23, 2019
5ddbf2a
some linting & commenting in dragbox.js
etpinard Feb 1, 2019
c60424b
first cut at zoom/pan/scroll `ax.matches` behavior
etpinard Feb 1, 2019
13007e0
add doScroll wrapper for dragbox tests
etpinard Jan 31, 2019
e962e96
mv dragbox tests calling makePlot to own describe block
etpinard Jan 31, 2019
ca1de5d
DRY-up drag-start/assert/drag-end tests
etpinard Jan 31, 2019
3b314fe
add toBeWithinArray custom jasmine matcher
etpinard Feb 1, 2019
6621419
add mucho matching axes dragbox tests
etpinard Jan 29, 2019
9118505
align autobinning of histogram traces on matching axes
etpinard Jan 28, 2019
d9c2d4e
link ax._categories & ax._categoriesMap to same ref for matching axes
etpinard Jan 28, 2019
c290adf
add axis.matches boolean to splom dims
etpinard Feb 1, 2019
ef256db
generalize "update matched ax rng" logic
etpinard Feb 1, 2019
9f2fad1
disallow constraining AND matching range
etpinard Feb 1, 2019
8c09944
add "matches" + "scaleanchor" mock
etpinard Feb 1, 2019
c5f9e74
fix partial ax-range relayout calls for matching axes
etpinard Feb 1, 2019
d0581e2
fix typo (ax.setScale has no arg)
etpinard Feb 5, 2019
6b34ae3
use ax._matchGroup to improve matching axes relayout perf
etpinard Feb 5, 2019
99a9edb
allow fixedrange subplots to scaleanchor with `constrain:domain`
etpinard Feb 6, 2019
91431ec
drop *scaleanchor* constraints for axes under *matches* constraint
etpinard Feb 12, 2019
70b10ff
add safe-guard in dragbox.js
etpinard Feb 18, 2019
772efe5
add info about matching axis type in attr description
etpinard Feb 18, 2019
1713c83
fix typo
etpinard Feb 18, 2019
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
Prev Previous commit
Next Next commit
drop *scaleanchor* constraints for axes under *matches* constraint
... for now, until we found a way to apply domain scaleanchor constraints
    on matching axes (which is theoretically possible).
  • Loading branch information
etpinard committed Feb 12, 2019
commit 91431ec425052d0a5ee90182b08adafc6c53cd15
68 changes: 31 additions & 37 deletions src/plots/cartesian/constraints.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a
// coerce the constraint mechanics even if this axis has no scaleanchor
// because it may be the anchor of another axis.
var constrain = coerce('constrain');

Lib.coerce(containerIn, containerOut, {
constraintoward: {
valType: 'enumerated',
Expand All @@ -38,38 +37,41 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a
}
}, 'constraintoward');

var scaleOpts = containerIn.scaleanchor && !(containerOut.fixedrange && constrain !== 'domain') ?
getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut, constrain) :
{};
var matches, matchOpts;

var matchOpts = (containerIn.matches || splomStash.matches) && !containerOut.fixedrange ?
getConstraintOpts(matchGroups, thisID, allAxisIds, layoutOut) :
{};
if((containerIn.matches || splomStash.matches) && !containerOut.fixedrange) {
matchOpts = getConstraintOpts(matchGroups, thisID, allAxisIds, layoutOut);
matches = Lib.coerce(containerIn, containerOut, {
matches: {
valType: 'enumerated',
values: matchOpts.linkableAxes || [],
dflt: splomStash.matches
}
}, 'matches');
}

var scaleanchor = Lib.coerce(containerIn, containerOut, {
scaleanchor: {
valType: 'enumerated',
values: scaleOpts.linkableAxes || []
}
}, 'scaleanchor');
// 'matches' wins over 'scaleanchor' (for now)
var scaleanchor, scaleOpts;

var matches = Lib.coerce(containerIn, containerOut, {
matches: {
valType: 'enumerated',
values: matchOpts.linkableAxes || [],
dflt: splomStash.matches
}
}, 'matches');
if(!matches && containerIn.scaleanchor && !(containerOut.fixedrange && constrain !== 'domain')) {
scaleOpts = getConstraintOpts(constraintGroups, thisID, allAxisIds, layoutOut, constrain);
scaleanchor = Lib.coerce(containerIn, containerOut, {
scaleanchor: {
valType: 'enumerated',
values: scaleOpts.linkableAxes || []
}
}, 'scaleanchor');
}

// disallow constraining AND matching range
if(constrain === 'range' && scaleanchor && matches && scaleanchor === matches) {
delete containerOut.scaleanchor;
if(matches) {
delete containerOut.constrain;
scaleanchor = null;
updateConstraintGroups(matchGroups, matchOpts.thisGroup, thisID, matches, 1);
} else if(allAxisIds.indexOf(containerIn.matches) !== -1) {
Lib.warn('ignored ' + containerOut._name + '.matches: "' +
containerIn.matches + '" to avoid either an infinite loop ' +
'or because the target axis has fixed range.');
}

var found = false;

if(scaleanchor) {
var scaleratio = coerce('scaleratio');

Expand All @@ -81,19 +83,11 @@ exports.handleConstraintDefaults = function(containerIn, containerOut, coerce, a
if(!scaleratio) scaleratio = containerOut.scaleratio = 1;

updateConstraintGroups(constraintGroups, scaleOpts.thisGroup, thisID, scaleanchor, scaleratio);
found = true;
}

if(matches) {
updateConstraintGroups(matchGroups, matchOpts.thisGroup, thisID, matches, 1);
found = true;
}

if(!found && allAxisIds.indexOf(containerIn.scaleanchor) !== -1) {
} else if(allAxisIds.indexOf(containerIn.scaleanchor) !== -1) {
Lib.warn('ignored ' + containerOut._name + '.scaleanchor: "' +
containerIn.scaleanchor + '" to avoid either an infinite loop ' +
'and possibly inconsistent scaleratios, or because the target' +
'axis has fixed range.');
'and possibly inconsistent scaleratios, or because the target ' +
'axis has fixed range or this axis declares a *matches* constraint.');
}
};

Expand Down
8 changes: 5 additions & 3 deletions src/plots/cartesian/layout_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ module.exports = {
'or the same letter (to match scales across subplots).',
'Loops (`yaxis: {scaleanchor: *x*}, xaxis: {scaleanchor: *y*}` or longer) are redundant',
'and the last constraint encountered will be ignored to avoid possible',
'inconsistent constraints via `scaleratio`.'
'inconsistent constraints via `scaleratio`.',
'Note that setting axes simultaneously in both a `scaleanchor` and a `matches` constraint',
'is currently forbidden.'
].join(' ')
},
scaleratio: {
Expand Down Expand Up @@ -224,8 +226,8 @@ module.exports = {
'will match the range of the corresponding axis in data-coordinates space.',
'Moreover, matching axes share auto-range values, category lists and',
'histogram auto-bins.',
'Note that setting `matches` and `scaleratio` under a *range* `constrain`',
'to the same axis id is forbidden.'
'Note that setting axes simultaneously in both a `scaleanchor` and a `matches` constraint',
'is currently forbidden.'
].join(' ')
},
// ticks
Expand Down
35 changes: 29 additions & 6 deletions src/plots/cartesian/layout_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,9 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {

// sets of axes linked by `scaleanchor` along with the scaleratios compounded
// together, populated in handleConstraintDefaults
layoutOut._axisConstraintGroups = [];
var constraintGroups = layoutOut._axisConstraintGroups = [];
// similar to _axisConstraintGroups, but for matching axes
layoutOut._axisMatchGroups = [];
var matchGroups = layoutOut._axisMatchGroups = [];

for(i = 0; i < axNames.length; i++) {
axName = axNames[i];
Expand All @@ -267,20 +267,22 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
handleConstraintDefaults(axLayoutIn, axLayoutOut, coerce, allAxisIds, layoutOut);
}

for(i = 0; i < layoutOut._axisMatchGroups.length; i++) {
var group = layoutOut._axisMatchGroups[i];
for(i = 0; i < matchGroups.length; i++) {
var group = matchGroups[i];
var rng = null;
var autorange = null;
var axId;

// find 'matching' range attrs
for(axId in group) {
axLayoutOut = layoutOut[id2name(axId)];
if(!axLayoutOut.matches) {
rng = axLayoutOut.range;
autorange = axLayoutOut.autorange;
}
}

// if `ax.matches` values are reciprocal,
// pick values of first axis in group
if(rng === null || autorange === null) {
for(axId in group) {
axLayoutOut = layoutOut[id2name(axId)];
Expand All @@ -289,7 +291,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
break;
}
}

// apply matching range attrs
for(axId in group) {
axLayoutOut = layoutOut[id2name(axId)];
if(axLayoutOut.matches) {
Expand All @@ -298,5 +300,26 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
}
axLayoutOut._matchGroup = group;
}

// remove matching axis from scaleanchor constraint groups (for now)
if(constraintGroups.length) {
for(axId in group) {
for(j = 0; j < constraintGroups.length; j++) {
var group2 = constraintGroups[j];
for(var axId2 in group2) {
if(axId === axId2) {
Lib.warn('Axis ' + axId2 + ' is set with both ' +
'a *scaleanchor* and *matches* constraint; ' +
'ignoring the scale constraint.');

delete group2[axId2];
if(Object.keys(group2).length < 2) {
constraintGroups.splice(j, 1);
}
}
}
}
}
}
}
};
Binary file modified test/image/baselines/axes_scaleanchor-with-matches.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 11 additions & 9 deletions test/image/mocks/axes_scaleanchor-with-matches.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
{
"data":[
{"x": [0,1,1,0,0,1,1,2,2,3,3,2,2,3], "y": [0,0,1,1,3,3,2,2,3,3,1,1,0,0]},
{"x": [0,1,2,3], "y": [1,2,4,8], "yaxis":"y2"}
{"x": [0,1,2,3], "y": [1,2,4,8], "xaxis": "x2", "yaxis":"y2"}
],
"layout":{
"width": 500,
"height": 500,
"title": {"text": "Bottom subplot has scaleanchor constrained axes<br>Top subplot has matching axes"},
"xaxis": {
"constrain": "domain"
"constrain": "range"
},
"yaxis": {
"scaleanchor": "x",
"constrain": "domain",
"constrain": "range",
"domain": [0, 0.45],
"title": {"text": "1:1<br>matching y range above"}
"title": {"text": "1:1<br>x|y scale constrain range"}
},
"xaxis2": {
"anchor": "y2"
},
"yaxis2": {
"matches": "y",
"constrain": "domain",
"scaleanchor": "x",
"scaleratio": 0.2,
"anchor": "x2",
"matches": "x2",
"domain": [0.55, 1],
"title": {"text": "1:5<br>matching y range below"}
"title": {"text": "matching x|y"}
},
"showlegend": false
}
Expand Down
46 changes: 44 additions & 2 deletions test/jasmine/tests/axes_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,8 @@ describe('Test axes', function() {
});

var warnTxt = ' to avoid either an infinite loop and possibly ' +
'inconsistent scaleratios, or because the targetaxis has ' +
'fixed range.';
'inconsistent scaleratios, or because the target axis has ' +
'fixed range or this axis declares a *matches* constraint.';

it('breaks scaleanchor loops and drops conflicting ratios', function() {
var warnings = [];
Expand Down Expand Up @@ -707,6 +707,48 @@ describe('Test axes', function() {
expect(layoutOut._axisMatchGroups).toEqual([{x: 1, x2: 1}]);
});

it('remove axes from constraint groups if they are in a match group', function() {
layoutIn = {
// this one is ok
xaxis: {},
yaxis: {scaleanchor: 'x'},
// this one too
xaxis2: {},
yaxis2: {matches: 'x2'},
// not these ones
xaxis3: {scaleanchor: 'x2'},
yaxis3: {scaleanchor: 'y2'}
};
layoutOut._subplots.cartesian.push('x2y2, x3y3');
layoutOut._subplots.xaxis.push('x2', 'x3');
layoutOut._subplots.yaxis.push('y2', 'y3');

supplyLayoutDefaults(layoutIn, layoutOut, fullData);

expect(layoutOut._axisMatchGroups.length).toBe(1);
expect(layoutOut._axisMatchGroups).toContain({x2: 1, y2: 1});

expect(layoutOut._axisConstraintGroups.length).toBe(1);
expect(layoutOut._axisConstraintGroups).toContain({x: 1, y: 1});
});

it('remove constraint group if they are one or zero items left in it', function() {
layoutIn = {
xaxis: {},
yaxis: {matches: 'x'},
xaxis2: {scaleanchor: 'y'}
};
layoutOut._subplots.cartesian.push('x2y');
layoutOut._subplots.xaxis.push('x2');

supplyLayoutDefaults(layoutIn, layoutOut, fullData);

expect(layoutOut._axisMatchGroups.length).toBe(1);
expect(layoutOut._axisMatchGroups).toContain({x: 1, y: 1});

expect(layoutOut._axisConstraintGroups.length).toBe(0);
});

it('drops scaleanchor settings if either the axis or target has fixedrange', function() {
// some of these will create warnings... not too important, so not going to test,
// just want to keep the output clean
Expand Down
0