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

Clean subplots #2227

Merged
merged 17 commits into from
Jan 3, 2018
Merged

Clean subplots #2227

merged 17 commits into from
Jan 3, 2018

Conversation

alexcjohnson
Copy link
Collaborator

Closes #2125

Updates subplot creation logic. The meat of this is in 3625be4, which makes it so new subplots are created only by traces (visible OR invisible) and components (annotations, shapes, images), but NOT by axis, scene, or subplot objects.

The primary motivation for this is if you make a trace of one type and change it to another, you won't get subplots corresponding to the old type even if we automatically pushed information for the old subplot back into the layout (for example axis ranges). We can leave those objects in the layout without them being rendered, and yet if you come back to the old trace types later any configuration you had used previously will still be available - a nice side effect in editor contexts.

I added a few more _private attributes in the course of this work - which makes pieces a bit more coupled, not so great for testing... but my reasoning is that the logic is fairly complex (we don't want to be redoing it all the time) and nonlocal (the axes/subplots need to know about what's in data and component arrays) so it seemed cleaner to just do it right once during supplyDefaults and reference that forever after.

In order to get this to work right, I altered the svg structure a bit, and simplified some of the rendering logic such as Axes.doTicks a little. This had some happy side effects like removing excess subplot containers and redundant gridlines when you have multiple cartesian subplots (leads to smaller svg files and presumably a bit faster tick/grid rendering - this is why a few mocks in 2de217e got thinner gridlines, they're no longer being overdrawn), and along the way I fixed a couple of other random subtle bugs:

  • colorbar title-avoids-tick-labels algorithm was incorrect when the top of the colorbar wasn't at the top of the plot area (where it is by default). This is why the airfoil mock changed in 2de217e, that title was avoiding the wrong label. The fix for this one is wrapped up with other logic changes in the big commit 3625be4 - the code in colorbars/draw changed, and now it's fixed, but it's only right in conjunction with other changes in cartesian/axes so I couldn't split it out into a separate commit.
  • scene.cameraposition -> scene.camera conversion had a typo, fixed in 26aaffc. It turns out a couple of mocks do use cameraposition and changed as a result of this -> 276d1d3

@etpinard please review. Apologies for the giant monolithic commit, fraid I couldn't see a reasonable way to split it up. That probably speaks to the need to refactor this code more than I had the stamina for in this PR...

cc @bpostlethwaite @nicolaskruchten

@@ -1,12 +1,12 @@
Autoscale // components/modebar/buttons.js:139
Box Select // components/modebar/buttons.js:103
Click to enter Colorscale title // plots/plots.js:437
Click to enter Colorscale title // plots/plots.js:338
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hmm, I should have just ignored the changes to this file, since they're only line number changes... I should revisit this so it doesn't get regenerated just because you run test-syntax locally.

Copy link
Contributor

Choose a reason for hiding this comment

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

As a rule, I believe we should only commit changes to files in dist/ when running npm version.

// check whether the relevant position is the same.
var sideIndex = alignmentConstants.FROM_BL[side];
if(counterAx.side === side) {
return anchorAx.domain[sideIndex] === ax.domain[sideIndex];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This happens fairly often in our mocks, and is super easy for users to do: for example make stacked subplots xy and x2y2, then leave y.anchor out because everything looks right even though it defaults to x and not x2. Perhaps we should make better default logic (pick the first non-overlaying axis that will actually make a subplot with this axis, or perhaps the first axis that specifies this axis as its own anchor?) but that logic is kinda tricky and nearly circular. Anyway what this does is as long as the subplot you're drawing looks like the two axes are anchored together, it will join the lines neatly at the corner.

This wasn't needed before because we generated so many extra overlaying subplots, one of them was sure to make a crisp corner...

Copy link
Contributor

Choose a reason for hiding this comment

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

cc #1200

@@ -1847,7 +1774,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
if(axLetter === 'x') {
sides = ['bottom', 'top'];
transfn = function(d) {
return 'translate(' + ax.l2p(d.x) + ',0)';
return 'translate(' + (ax._offset + ax.l2p(d.x)) + ',0)';
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

At some point I think we'd be better off just merging ax._offset into ax.*2p and getting rid of all the <g> translations we have scattered about. The fact that it's not in there is a remnant of the days when plots were nested <svg> elements so within a subplot pixel coordinates always started at (0,0). Here at least I'm getting rid of the <g> translations associated with ticks and grids, but it means I have to put them back into the tick and grid paths until *2p catches up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm. My gut feeling tells me we're better off leaving _offset out of *2p for to make the *2p methods more easily reusable to non-cartesian subplot types that have an offset that isn't just a function of the domain and fullLayout._size (e.g. ternary and polar). But then again perhaps ternary, geo and polar could mock _offset and get the same results. It is something to think about for sure though 🤔

}
drawTicks(mainPlotinfo[axLetter + 'axislayer'], tickpath);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

instead of trying to draw all the ticks in a single loop, now I'm splitting it up into the main subplot (the main subplot is the one you're anchored to and may get ticks on one or both sides) and all other subplots (which, if they get any ticks, always get both sides).

This also simplifies figuring out where to put labels, since they always go only on at ax._mainLinePosition and we then no longer need to pay attention here to if that's an anchored or free position or which side of the subplot to pick out.


// keep track of which subplots (by main conteraxis) we've already
// drawn grids for, so we don't overdraw overlaying subplots
var finishedGrids = {};
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

note that you can't do something simple like pick the main axis for each overlaying subplot, as that subplot may not exist (say for example you have x2y2 overlaying xy - for y2 the main counteraxis is x but there is no xy2 subplot. So instead I just make sure we only draw the grid once for any given main axis.

@@ -65,7 +64,7 @@ function listNames(gd, axLetter, only2d) {
var names = filterAxis(fullLayout, '');
if(only2d) return names;

var sceneIds3D = Plots.getSubplotIds(fullLayout, 'gl3d') || [];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

removed one circular dep here (only 16 to go 🙄 ) by having _subplots already calculated. I removed Plots.getSubplotIds entirely since it had become a 1-liner just looking up entries in fullLayout._subplots.

Copy link
Contributor

Choose a reason for hiding this comment

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

Beautiful cc #236

// I kind of think we should just let all traces increment color, visible or not.
// see mock: axes-autotype-empty vs. a test of restyling visible: false that
// I can't find right now...
if(fullTrace._input.visible !== false) colorCnt++;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@etpinard thoughts? Should visible: false traces still be allowed to increment color? I guess one reason to permit disqualified traces to increment color is in case they become valid later on (like by streaming the first data point into one) but I feel like that argument applies to explicitly visible: false traces too, ie showing one would not be expected to change all the other colors. But I guess technically that would be a breaking change so maybe should wait for v2? As I have it here we preserve backward compatibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

You make a great point about streaming data. So yeah, I agree: visible: false traces should be allowed to increment color in v2. Let's add that to #420 🚬

Copy link
Contributor

Choose a reason for hiding this comment

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

done in #420 (comment)

@@ -149,7 +149,7 @@ describe('Plotly.toImage', function() {
.then(function() { return Plotly.toImage(gd, {format: 'png', imageDataOnly: true}); })
.then(function(d) {
expect(d.indexOf('data:image/')).toBe(-1);
expect(d.length).toBeWithin(53660, 1e3, 'png image length');
expect(d.length).toBeWithin(54660, 3e3, 'png image length');
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This had been failing on my computer for ages, though it always works on CI... now there's just one test, in gl3d code, that consistently fails for me, though I need to be careful to have the chrome window on my secondary monitor; if I let it live on my laptop screen (retina) a whole bunch of other tests fail too. Must be a devicePixelRatio dependence or something. Would love to sort this out at some point, makes it annoying to run the full test suite if I'm not at home, and god forbid I ever upgrade this monitor and only have high-res devices to work with!

Copy link
Contributor

@etpinard etpinard left a comment

Choose a reason for hiding this comment

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

A very nice display of perseverance in this PR 💪

If I were you, I probably would've given up three times by now 😉

This thing is looking really solid. Above all, I'm a big fan of fullLayout._subplots and includeBasePlot. In this first review, I made several comments, mostly questions, none of which feel major.

*
* @return {array} array of calcdata traces
*/
plots.getSubplotCalcData = function(calcData, type, subplotId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm curious. Why did you move this to a separate file? Does leaving it in plots/plots.js lead to a circular dependency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why did you move this to a separate file?

I don't recall if it made more circular deps or not, but I don't like plots/plots - it has its fingers in everything. Plus I was making 2 (3 as of the next commit) similar simple functions for filtering down data or calcdata that only depend on Registry so it seemed natural to group them in their own file.

xaListCheater = [],
xaListNonCheater = [],
xaCheater = {},
xaNonCheater = {},
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice 🐎

@@ -1,12 +1,12 @@
Autoscale // components/modebar/buttons.js:139
Box Select // components/modebar/buttons.js:103
Click to enter Colorscale title // plots/plots.js:437
Click to enter Colorscale title // plots/plots.js:338
Copy link
Contributor

Choose a reason for hiding this comment

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

As a rule, I believe we should only commit changes to files in dist/ when running npm version.

@@ -433,7 +436,7 @@ module.exports = function draw(gd, id) {
selection: d3.select(gd).selectAll('g.' + cbAxisOut._id + 'tick'),
side: opts.titleside,
offsetLeft: gs.l,
offsetTop: gs.t,
offsetTop: 0,
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this fix #970 ?

Copy link
Contributor

Choose a reason for hiding this comment

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

or maybe #1396

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think it fixes #970 but yes, it does fix #1396 🎉

* alphanumeric string sort, tailored for subplot IDs like scene2, scene10, x10y13 etc
*/
var char0 = 48;
var char9 = 57;
Copy link
Contributor

Choose a reason for hiding this comment

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

brilliant ✨

xref: 'x5', // will be converted to 'x' and xaxis should autorange
yref: 'y5', // same 'y' -> yaxis
xref: 'xq', // will be converted to 'x' and xaxis should autorange
yref: 'yz', // same 'y' -> yaxis
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you change this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why did you change this?

Now an annotation is enough to create a new subplot, so I had to switch these to invalid references.

expect(gd.layout.xaxis).toBeUndefined();
expect(gd.layout.yaxis).toBeUndefined();
expect(gd.layout.xaxis === undefined).toBe(true);
expect(gd.layout.yaxis === undefined).toBe(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Something is off with toBeUndefined?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

when you expect(somePotentiallyBigObject).toBeUndefined(), a failure swamps you with the whole stringified object. The killer was expect(scene) whose string representation was so big it took me forever to find the tracebacks. If the object you might get on failure is small, toBeUndefined is still a good idea as it shows you what you got.

@@ -758,4 +671,134 @@ describe('Test Plots', function() {
});
});
});

describe('subplot cleaning logic', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Lovely tests

@@ -9,6 +9,15 @@ var customAssertions = require('../assets/custom_assertions');
var assertDims = customAssertions.assertDims;
var assertStyle = customAssertions.assertStyle;


function supplyDataDefaults(dataIn, dataOut) {
return Plots.supplyDataDefaults(dataIn, dataOut, {}, {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's getting more difficult to call anything but Plots.supplyDefaults in tests due to the large amount of things to mocks. We should maybe start standardising around assets/supply_defaults.js instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

right, that's the disadvantage of this kind of precalculation, which I've added a lot of lately 🙈

👍 for moving toward assets/supply_defaults.js.

}

// ensure all cartesian axes have at least one subplot
if(layoutOut._has('cartesian') || layoutOut._has('gl2d')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need the || layoutOut._has('gl2d') part here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In fact we don't! Good 👀
We depend on gl2d inside finalizeSubplots but it's irrelevant if we have only gl2d. 6e62b35

@etpinard etpinard added status: reviewable bug something broken labels Dec 29, 2017
@etpinard etpinard added this to the v1.32.0 milestone Dec 29, 2017
var idRegex = Cartesian.idRegex;
var subplots = layoutOut._subplots;
var xaList = subplots.xaxis;
var yaList = subplots.yaxis;
Copy link
Contributor

Choose a reason for hiding this comment

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

There's a few more Axes.list that could be replaced by _subplots.xaxis and _subplots.yaxis

image

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah right - but actually listIds is the direct match to fullLayout._subplots.(x|y)axis (with some _subplots.gl3d shenanigans thrown in for good measure), as list returns the axis objects. I'll take a stab at removing listIds and simplifying the others.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cleaned up list and listIds in 0506f88 and 2aa1545

in particular, annotations autorange was including 3d axes, and axes we already knew didn't have annotations.
need to filter out (x|y)ref='paper'
@etpinard
Copy link
Contributor

etpinard commented Jan 3, 2018

A well-earned 💃

I'll take care of #2227 (comment) while trying to get the tests to pass on the regl branch.

@alexcjohnson alexcjohnson merged commit 3a4788b into master Jan 3, 2018
@alexcjohnson alexcjohnson deleted the clean-subplots branch January 3, 2018 17:07
@etpinard etpinard mentioned this pull request Jan 8, 2018
5 tasks
@alexcjohnson alexcjohnson mentioned this pull request Feb 6, 2018
alexcjohnson referenced this pull request Apr 30, 2018
- add 'expandIndex' for all traces
- add 'group' for traces with enable groupby transforms
- 🔪 itemNumber (which was undefined on double click, can
  add it back in future if someone asks for it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Automatically remove unused subplots
2 participants