-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Axis constraints #1522
Axis constraints #1522
Conversation
all constraints are satisfied on initial plot, but not yet on zooms
@@ -578,7 +580,7 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { | |||
|
|||
redrawObjs(fullLayout.annotations || [], Registry.getComponentMethod('annotations', 'drawOne')); | |||
redrawObjs(fullLayout.shapes || [], Registry.getComponentMethod('shapes', 'drawOne')); | |||
redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw')); | |||
redrawObjs(fullLayout.images || [], Registry.getComponentMethod('images', 'draw'), true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice touch 🐎
src/plots/cartesian/dragbox.js
Outdated
|
||
// This is specifically directed at scatter traces, applying an inverse | ||
// scale to individual points to counteract the scale of the trace | ||
// as a whole: | ||
.select('.scatterlayer').selectAll('.points').selectAll('.point') | ||
.call(Drawing.setPointGroupScale, 1 / xScaleFactor, 1 / yScaleFactor); | ||
.call(Drawing.setPointGroupScale, xScaleFactor, yScaleFactor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a bug currently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, I just flipped the definitions of x/yScaleFactor since I noticed they were being inverted in most of their uses.
// (or autoranged, if we have no initial range, to match the logic in | ||
// doubleClickConfig === 'reset' below), we reset. | ||
// If they are *all* at their initial ranges, then we autosize. | ||
if(doubleClickConfig === 'reset+autosize') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reorg was to fix a subtle bug:
- set explicit initial ranges for both x and y,
- autoscale one of them (doubleclick the middle of the x axis, for example)
- doubleclick the plot repeatedly: it will toggle between autoscaled x, initial y, and autoscaled y, initial x (with this change it toggles between fully autoscaled and fully initial-scaled).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great. Let's make sure to 🔒 this down before merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/plots/cartesian/dragbox.js
Outdated
@@ -135,6 +159,8 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { | |||
clearSelect(zoomlayer); | |||
} | |||
else if(isSelectOrLasso(dragModeNow)) { | |||
dragOptions.xaxes = xa; | |||
dragOptions.yaxes = ya; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing what I broke in 98900a2#diff-1bd3dd52cddb8638876bbe17cef28a0dL105 when I consolidated to use recomputeAxisLists
for the initial settings too (when dragOptions
isn't yet available, and I didn't see anyone using the xaxes/yaxes
properties). But it turns out it was subtly broken before anyway, as the version in recomputeAxisLists
used the wrong names (dragoptions.xa
instead of dragoptions.xaxes
).
dragbox.js
is still a bit of a mess, but... I think it's a little better now.
@alexcjohnson the zooming on your example looks great, and even does a sensible thing in extreme cases 🍻 I am, however, thrown out a bit by the choice of expanding the relevant axis range to match |
Hmm, yeah, I see that in your ggplot example #272 (comment) - it's obviously not the way I think about it, but I'm not sure if I've seen precedent for this way or not - other than I guess the way we handle maps. The only way I can really see shrinking the actual axis size making sense within our structure is some sort of algorithm to automatically reduce the total plot width to minimize white space, but that seems a bit tricky to get right. I'd also say this approach strikes me as rooted in a static presentation of the plot - those long narrow subplots are not so useful once you start zooming and panning. But of course we want to be able to replicate all the plots folks feed in! Do you think that for ggplotly's purposes you could pull out the overall plot dimensions from ggplot's results and just pass them along to plotly.js? In which case I guess it would basically be ggplot enforcing the initial constraints (with plotly.js just verifying and possibly correcting for small differences in layout) and plotly.js's role in this would primarily be in dynamic behavior. And re: the part that disappeared from your comment above - do you think the with/width confusion merits changing the name |
That's essentially what I'm doing in plotly/plotly.R#509. As you mention, it unfortunately requires estimating the axis dimensions, which is approximate, at best. It also significantly complicates the logic on my end, but I suppose it's possible.
I'm not highly opposed to the name, but I see your point. How about FWIW, at least for the R package, I'm going to add more robust name checking that should catch instances like this. |
👍 much better than |
I'd vote for skipping
That would require a few patches in |
👍
I'll take a look at it, that might not be too hard. I ran into a few more issues gl2d zoom on multi-subplot plots - the one I mentioned above seems to be dropped contexts, the relayout makes new ones for some reason; but there's another one that the grey overlay covers the whole plot (margins included) EXCEPT whatever other gl subplots happen to be in front of the plot you're zooming on. That probably wouldn't be hard to fix but if we just swap in dragbox.js it would be moot. |
After thinking about this again, I wouldn't be able to support aspect ratios in I guess what I'm trying to say is I would still slightly prefer axis shrinking, but I can make do with the current implementation (and understand why you prefer it). We'll just have to educate |
After a discussion with @cpsievert I think we'll leave the general structure as it is and let ggplotly handle explicitly setting the subplot shapes to match what ggplot generates. But in the discussion it came up that there are cases you want to constrain several x (or y) axis scales without a corresponding x<->y constraint. See eg plotly/plotly.R#908 - so I'll see if I can add that in here too, letting |
plus a couple small bugfixes encountered in the process
@@ -771,7 +771,7 @@ describe('Test click interactions:', function() { | |||
var translate = Drawing.getTranslate(mockEl), | |||
scale = Drawing.getScale(mockEl); | |||
|
|||
expect([translate.x, translate.y]).toBeCloseToArray([61.070, 97.712]); | |||
expect([translate.x, translate.y]).toBeCloseToArray([-25.941, 43.911]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test was actually incorrect, as wheel events weren't passing along the mouse coordinates before 86e0d5a#diff-d48932d4d7f81cc4201054d397d53f1aR17 and these matter to wheel zoom.
(the change from -1000 to -20 above is irrelevant, this event handler clips at +/-20 per event fired)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks 👍
ai.indexOf('calendar') !== -1 || | ||
ai.match(/^(bar|box|font)/)) { | ||
flags.docalc = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had to move this block above the following _has('gl2d')
block, so scaleanchor/scaleratio
get the necessary recalc. Seems like as a general rule, we might want to reorder these from biggest change to smallest, ie all the recalcs, then all the replots, then the ticks & styles... if we don't find an altogether better way to manage these flags (via the schema?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the idea of adding a recalc
true flag to the attributes that require a recalc on restyle / relayout. But that can wait. We should do this in one PR crossing off #648.
|
||
// gl-select-box clips to the plot area bounds, | ||
// which breaks the axis constraint, so don't allow | ||
// this box to go out of bounds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in contrast, svg lets you make these constrained boxes extend outside the plot area. Didn't seem worth diving into gl-select-box
to make this marginal feature match up precisely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. No need to do this for now 👍
// otherwise clamp small changes to the origin so we get 1D zoom | ||
else { | ||
if(smallDx) result.boxEnd[0] = result.boxStart[0]; | ||
if(smallDy) result.boxEnd[1] = result.boxStart[1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@etpinard while I was at it I added in a few of the bits of behavior used in svg, such as 1D zoom, minimum zoom size, and clamping panning to pure x (y) if dy (dx) is small enough. Do you want to take it for a spin and see what you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loving it. Thanks!
xaxis: this.xaxis, | ||
yaxis: this.yaxis | ||
}; | ||
enforceAxisConstraints({_fullLayout: mockLayout}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kind of hacky, but enforceAxisConstraints
has to happen after the Axes.doAutoRange
just above, and these aren't the gd._fullLayout
axes...
A bunch of things here would break if/when we support overlaid axes...
} | ||
|
||
// Remove hover effects if we're not over a point OR | ||
// if we're zooming or panning (in which case result is not set) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also to match svg
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice. Thanks 👍
@@ -246,12 +246,20 @@ describe('Test hover and click interactions', function() { | |||
pointNumber: 0 | |||
}); | |||
|
|||
// after the restyle, autorange changes the y range |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason I don't understand, when this test ran before my changes, the y-axis didn't get autoranged after the restyle
, even though when I make the same restyle
call on master in the test dashboard it does autorange. Anyway, now it does get autoranged during the test, as it should, which is why I needed to alter the run
the second time around.
// after the restyle, autorange changes the x AND y ranges | ||
// I don't get why the x range changes, nor why the y changes in | ||
// a different way than in the previous test, but they do look | ||
// correct on the screen during the test. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense that the axis ranges are different here than in the test above, as this is scattergl-fancy... but then why are they the same as the test above during the first run
? This test suite baffles me...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Maybe I tried to be too fancy in this test suite.
@etpinard ready for another review. Single gl2d plots work well with scale constraints now (please try it out and let me know what you think). I did not investigate multiple gl2d subplots (coupled or otherwise) as I was thrown off for a while by #1200 (comment) but now that I understand that I could look into this, if we think it's important. |
test/jasmine/assets/mouse_event.js
Outdated
@@ -14,7 +14,7 @@ module.exports = function(type, x, y, opts) { | |||
ev; | |||
|
|||
if(type === 'scroll') { | |||
ev = new window.WheelEvent('wheel', opts); | |||
ev = new window.WheelEvent('wheel', Object.assign({}, fullOpts, opts)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you use Lib.extendFlat
here instead - in case we run these tests on old browsers some day?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
afterEach(destroyGraphDiv); | ||
|
||
it('updates ranges when adding, removing, or changing a constraint', function(done) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice test 🎉
💃 if you don't feel like including a patch for #1200 in this PR. This gets my vote for PR of the year 🏆 |
@alexcjohnson, I am trying to build a plot that is:
|
Closes #272 - defines and enforces constraints between axis scales, so you can have a fixed aspect ratio plot. Allows arbitrary chaining of many axes together, each with its own aspect ratio, along with unconstrained subplots.
TODO:
fixedrange
- should we forbid this combination? Or make the whole constraint group fixed? Or something else? I can't think of a use case for this so I'm inclined to forbid it, at least for now.relayout
calls that don't come from the GUI. This may actually allow me to simplify some things in dragbox.js too...@etpinard @cpsievert @rreusser I'd be grateful if one or two of you could play with the interactions - zoom box, dragging an axis end, dragging a corner of a plot - and tell me if they feel right. The mock in the PR - http://localhost:3000/devtools/test_dashboard/#axes_scalewith if you run the test dashboard - should be a good starting point, as it has two linkages across two coupled subplots.