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

Autosize / autorange fixes #2437

Merged
merged 11 commits into from
Mar 6, 2018
Merged

Autosize / autorange fixes #2437

merged 11 commits into from
Mar 6, 2018

Conversation

alexcjohnson
Copy link
Collaborator

Fixes #2428 - despite its recent posting, I think this is one of our oldest bugs. You see it in the way a lot of autoranged plots will change just a little bit if you double-click them to cause the autorange to rerun... including a whole lot of our test images. They don't do that anymore. This happened when the margins get pushed automatically (mostly by the legend) and the axes are padded, because Axes.expand (called during calc) depended on ax._length, which could change after calc due to margin pushers. As a nice side benefit, now that Axes.expand is independent of axis length all the layout attributes that affect axis lengths can have their editType downgraded from calc to plot, speeding up all these edits.

Fixes #2385 as well - Plotly.react now handles autosize and size changes correctly. This was mostly just a matter of adding the special autosize handling that we already had in relayout into react as well, but then when I dropped axis size changes to editType: 'plot' it also required some more careful handling of ax._min/_max.

cc @etpinard

// // To do so, we must manually set them back here using the _initialAutoSize cache.
// if(['width', 'height'].indexOf(ai) !== -1 && vi === null) {
// fullLayout[ai] = gd._initialAutoSize[ai];
// }
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

On closer inspection I think I understand what this is about, but I don't think it's actually the correct behavior, for example if gd was resized external to plotly.js it wouldn't make sense to revert to when the plot was first drawn. I'm not quite sure how to handle this though.

Copy link
Contributor

Choose a reason for hiding this comment

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

😬 auto-sizing always has a way to make my brain hurt.

Referencing #635 which did a lot of damage the streambed 🤕

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oy, I think we've all fought with that one at various points... If you don't mind, I'm going to leave this as it is now - with the TODO here to remind us about it if a related bug pops up, but without trying to implement anything around it. We have some great tests in config_test that I extended to make sure that all of those test cases work the same in relayout and react, I'd like to just freeze it until we have more test cases to look at.

doAutoRange: doAutoRange,
needsAutorange: needsAutorange,
expand: expand
};
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 initially pulled this module out of axes.js to avoid circular references; then I ended up moving clearCalc into setConvert instead of here, which mooted the circular reference problem, but I still like having these separate from the behemoth axes.js.

@@ -261,7 +260,7 @@ proto.updateSize = function(canvas) {
}

// make sure plots render right thing
if(this.redraw) this.redraw();
// if(this.redraw) this.redraw();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh I forgot to remove this line... but @dfcreative @etpinard I wanted to ask about it anyway: if I leave it in, then occasionally - most noticeably when I autorange an old gl2d plot (ie pointcloud) - I get some extra tick labels: the new tick values get placed on the old axis range (if they're visible) then the correct tick labels get drawn over them without removing the obsolete ones.

Anyway I couldn't see any ill effects from removing it. OK?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, looks like in scene2d.plot this

this.glplot.draw();

does the same thing. 🔪 away!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

:hocho -> 0ecaaaf

if(bounds[i] > bounds[i + 2]) {
bounds[i] = -1;
bounds[i + 2] = 1;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also seems like this.bounds has stopped being used, so I 🔪 it. Can't wait until we can 🔪 this whole subplot type...

if(!ax._m) ax.setScale();

var len = data.length;
var extrapad = options.padded || false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I'm wrong, but this used to be the only place in expand() that used ax._length as:

extrappad = options.padded ? ax._length * 0.05 : 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.

this used to be the only place in expand() that used ax._length

Exactly - taking that out, and instead holding on to extrapad inside the ax._min/_max entries, to be evaluated during getAutorange, is what fixed #2428 and enabled calc->plot

Copy link
Contributor

Choose a reason for hiding this comment

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

Great! That makePadFn helper above is brilliant ✨ and expand() is much clearer and well commented 📚

Now, would mind mentioning in the jsDoc above that expand() should not depend on pixel values (e.g ax._length), but only on calcdata things?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fuller docs -> 9913afe


var i, j, k, v, di, dmin, dmax, ppadiplus, ppadiminus, includeThis, vmin, vmax;

function getPad(item) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We could maybe rename this function. getPad might be confused with the output of makePadFn

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

haha I noticed that too... I'll change it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

renamed -> 9913afe

}
}

function needsAutorange(ax) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It feels awkward to export this now that auto-range routines are out of axes.js.

Maybe we should move fastAxisExpand out of scattergl/index.js and into this file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Or just bake the fastAxisExpand functionality into expand to be used automatically when the paddings are constant - then everyone wins! I think that should be possible... I'll try it.

Copy link
Contributor

Choose a reason for hiding this comment

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

then everyone wins!

That's an idea! But perhaps the TOO_MANY_POINTS constant should remain per trace type?

@@ -86,7 +86,7 @@ module.exports = {
values: ['normal', 'tozero', 'nonnegative'],
dflt: 'normal',
role: 'style',
editType: 'calc',
editType: 'plot',
Copy link
Contributor

@etpinard etpinard Mar 5, 2018

Choose a reason for hiding this comment

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

It would be nice to add a few tests like this one that 🔒 the new editType: 'plot'.

Copy link
Contributor

Choose a reason for hiding this comment

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

... as removing editType: 'calc' is a pretty be deal 🐎 we wouldn't want to regress from that in the future!

@etpinard etpinard added bug something broken status: reviewable labels Mar 5, 2018
}
calcAxisExpansion(gd, trace, xa, ya, x, y, ppad);
Copy link
Contributor

@etpinard etpinard Mar 5, 2018

Choose a reason for hiding this comment

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

Very clean. This is great!

To note though, this new combined pathway is little slower. Previously at 1e6 pts, fastAxisExpand would clock in at ~5ms for one coordinate, now we're more in a 20-30ms range. Even though this is significant, I think it's worthwhile to make scattergl autorange even more on-par with the rest of the library. 👍

The failing tests are in this new suite that got put in during #2404. Should be an easy fix.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Huh, let's unpack that performance issue. Previously the inner loop was:

for(var i = 0; i < vals.length; i += 2) {	
  var v = vals[i];	
  if(v < b0) b0 = v;	
  if(v > b1) b1 = v;	
}

this afternoon it was:

for(i = 0; i < data.length; i++) {
  v = data[i];
  if(Math.abs(v) < FP_SAFE) {
    // data is not linearized yet so we still have to filter out negative logs
    if(v < vmin && (!isLog || v > 0)) vmin = v;
    if(v > vmax) vmax = v;
  }
}

The old code was accidentally omitting half the points (i+= 2), so we should be comparing 10ms to 20-30 ms (unless the 20-30ms is for both coordinates?). Then I'm doing two extra things: 1) comparing to FP_SAFE so we don't get a broken graph from numbers very close to the limit of floating point numbers, and 2) ensuring positive numbers when on a log scale. The new code also has a bunch more logic before and after this loop, but nothing else scales with the data size, all that is presumably <1ms. In my own tests don't see the extra logic in the loop coming anywhere near doubling the time. What am I missing?

Anyway I noticed I could optimize it a bit by making two versions of the loop, one for log and one for other types. Seems worth a couple of extra lines since this gets so much traffic -> 247c626

Updated the failing test in cf48426

Copy link
Contributor

@etpinard etpinard Mar 6, 2018

Choose a reason for hiding this comment

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

Great! You're latest patch in 247c626 got Axes.expand within 5ms of fastAxisExpand with i++ swapped in for i += 2 at 1e6 pts. Considering it's more robust and works with traces other than scattergl, I'll call this a win. 🥇

});

Plotly.relayout(gd, attr, axLayoutEdits[attr]);
expectReplot(attr);
Copy link
Contributor

Choose a reason for hiding this comment

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

💯 / 💯 test!

expect(gd._fullLayout.xaxis.range).toBeCloseToArray([-0.05, 1.05], 'x range');
expect(gd._fullLayout.yaxis.range).toBeCloseToArray([-0.11, 1.11], 'y range');
expect(gd._fullLayout.xaxis.range).toBeCloseToArray([-0.119, 1.119], 2, 'x range');
expect(gd._fullLayout.yaxis.range).toBeCloseToArray([-0.199, 1.199], 2, 'y range');
Copy link
Contributor

Choose a reason for hiding this comment

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

In comparison, at 1e5 - 1 pts (i.e. below the big-data threshold), we get:

image

@alexcjohnson I'll reference #2417 - but honestly feel free to close it if you think Axes.expand is satisfactory now.

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, so I guess that result is saying that in this case the constant pixel padding we choose is a bit bigger than it would need to be... but this is a random case, a lot of real cases will have correlations, like all the biggest markers are near one edge, so I think this is a pretty good compromise.

Copy link
Contributor

Choose a reason for hiding this comment

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

so I think this is a pretty good compromise.

Yep. I think so too 👌

@etpinard
Copy link
Contributor

etpinard commented Mar 6, 2018

Great bug fixing / perf improvement PR 💃 💃

@alexcjohnson alexcjohnson merged commit fa85e21 into master Mar 6, 2018
@alexcjohnson alexcjohnson deleted the react-autosize branch March 6, 2018 15:28
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.

2 participants