-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Constrain axes by domain #1767
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
Constrain axes by domain #1767
Conversation
I don't understand why we need to initInteractions twice, but it seems necessary to do both before and after finalDraw
standardize on assets/delay and increase the updateCamera delay
if you mouseout of the gl2d plot while hovering on a point, this ensures that unhover will be triggered. prior to this the test "scattergl after visibility restyle" consistently failed for me locally
seems like chrome has a more efficient image encoder now
src/plots/cartesian/dragbox.js
Outdated
var axId = activeAxIds[i]; | ||
doTicks(gd, axId, true); | ||
var ax = getFromId(gd, axId); | ||
updates[ax._name + '.range'] = ax.range.slice(); |
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 this is significantly simpler than what I was doing before (calculating the range edits again in dragTail) and more robust since it's explicitly WYSIWYG - whenever you update tick labels you record that and use it as the basis of the update object to be sent to relayout
.
Zoombox has a counterpart here
Oh, I'm noticing now that previously we sent each end of the range as a separate edit in the update object (ie 'xaxis.range[0]': 1, 'xaxis.range[1]': 5
rather than 'xaxis.range': [1, 5]
) - this way is more compact but I bet it'll break some applications that listen to relayout events to respond to zooms and inspect the event data to find the range changes, so I think I should change it back to the previous form.
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.
note that one effect of doing it this way is when you drag axis ends or make a zoombox (any of the GUI interactions) while using constrain: 'domain'
, the shape and constrained domains of the subplot will never change because we always make proportional updates to all linked ranges. This is NOT the case for generic relayout
calls: if you relayout
a single range, it can reshape the subplot, potentially changing which axis is constrained, but always keeping the domains as large as possible within the constraint.
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.
so I think I should change it back to the previous form.
I agree. Listening to plotly_relayout
and inspecting the event data for xaxis.range[0]
or xaxis.range[1]
must be pretty common in userland.
We should try to clean that up in v2 or maybe we could add a plotly_rerange
?
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.
-> back to independent attributes in 51fc2c6
src/plots/cartesian/dragbox.js
Outdated
} | ||
|
||
// updateSubplots - find all plot viewboxes that should be | ||
// affected by this drag, and update them. look for all plots | ||
// sharing an affected axis (including the one being dragged) | ||
// returns all the new axis ranges as an update object |
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.
oops, this was an aborted attempt, I'm not using thisattrs
so will remove it.
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.
-> removed this attrs
in 3931973
One tricky aspect of this implementation: whenever I considered doing this by either 1) letting the One pitfall with this approach though is that when a plot is saved, |
cc @cpsievert who might be interested in this feature. |
if(sel.node() === null) { | ||
expect(expected.noHoverLabel).toBe(true); | ||
return; | ||
} | ||
|
||
var path = sel.select('path'); | ||
expect(path.style('fill')).toEqual(expected.bgColor, 'bgcolor'); | ||
expect(path.style('stroke')).toEqual(expected.borderColor, 'bordercolor'); |
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.
Too bad that toEqual
can't print a failure msg
. Oh jasmine.
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 very much for those test tweaks!
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, didn't it used to be able to do that? super annoying.
var MINIMUM_LENGTH = 1e5; | ||
// decreased from 1e5 - perhaps chrome got better at encoding these | ||
// because I get 99330 and the image still looks correct | ||
var MINIMUM_LENGTH = 8e4; |
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. As long as the image looks ok! Some background info -> #1598
@@ -383,6 +383,7 @@ Plotly.plot = function(gd, data, layout, config) { | |||
drawFramework, | |||
marginPushers, | |||
marginPushersAgain, | |||
initInteractions, |
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.
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.
just want to call out a note I put in a commit message:
I don't understand why we need to initInteractions twice,
but it seems necessary to do both before and after finalDraw
I'll have a perf PR coming after this merges and will look into that in more detail.
@@ -190,8 +190,7 @@ Plotly.plot = function(gd, data, layout, config) { | |||
|
|||
return Lib.syncOrAsync([ | |||
subroutines.layoutStyles, | |||
drawAxes, | |||
initInteractions | |||
drawAxes |
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, I guess initialising interactions after pushing the margins makes sense 👍
src/plots/cartesian/axes.js
Outdated
@@ -455,6 +455,13 @@ axes.expand = function(ax, data, options) { | |||
i, j, v, di, dmin, dmax, | |||
ppadiplus, ppadiminus, includeThis, vmin, vmax; | |||
|
|||
// domain-constrained axes: base extrappad on the unconstrained | |||
// domain so it's consistent as the domain changes | |||
if(extrappad && ax._inputDomain) { |
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.
Interesting choice for having a condition based on ax._inputDomain
.
Wouldn't ax.constrain === 'domain'
be more robust?
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.
ah possibly... this might get called before enforceAxisConstraints
though, so I guess I should filter on both.
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.
-> tighter condition in 1271382
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.
FWIW I think there's a good argument that basing this padding on _inputDomain
is the right thing to do (rather than having the padding be 5% of the constrained domain), but a big part of MY motivation here was that trying to do it the other way means you'd have to make a totally separate attribute in the ax._min/_max
items in addition to val
and pad
because you'd no longer be able to express the padding either in data (val
) or pixel (pad
) units, then both the auto-range and the auto-domain routines get more complicated... also it makes the conditions more complicated for which points might possibly be the extremes so must be kept in ax._min/max
. I didn't want to open that can of worms.
But if anyone has a strong opinion that padding based on the constrained domain is truly the correct behavior we can certainly discuss. The biggest argument in that direction that I can see is that if your data make a perfect square but it's padded on constrained axes, the subplot itself will not end up precisely square. But if you've gotten to that point, perhaps you could be bothered to explicitly set up the domains and dimensions so the axes have equal length :)
Lib.coerce(containerIn, containerOut, { | ||
constraintoward: { | ||
valType: 'enumerated', | ||
values: letter === 'x' ? ['left', 'center', 'right'] : ['bottom', 'middle', 'top'], |
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.
it might time to consider having separate attribute declarations for x axes and y axes (similarly for x,y and z error bars #621). The reference page https://plot.ly/javascript/reference/ would benefit.
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'm curious, does Plotly.validate
gives the correct answer here e.g. where constraintoward: 'bottom'
in an xaxis container?
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.
heh, no, Plotly.validate
doesn't catch constraintoward: 'bottom'
on an x axis, but it also incorrectly complains about anchor
and scaleanchor
:
LOG: In layout, key xaxis.scaleanchor is set to an invalid value (y)
LOG: In layout, key xaxis.anchor is set to an invalid value (y)
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 found a way to fix this. Can I push a commit to this branch?
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.
Sure, push it!
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 Plotly.validate
for scaleanchor
, anchor
and overlaying
was a little harder than I thought actually, so I made a separate PR -> #1769
module.exports = { | ||
// from bottom left: this is the origin of our paper-reference | ||
// positioning system | ||
FROM_BL: { |
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.
First steps towards #1577
Nice.
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.
Ah, wasn't even thinking of that but good point. There are tons of other places we could make use of these (potentially adding FROM_BR
and FROM_TR
...) to make things clearer and more efficient. see eg ag middle:
in the src directory. I guess perhaps all of these could also allow [0, 1]
.
src/plots/cartesian/constraints.js
Outdated
|
||
if(normScale !== matchScale) { | ||
scaleZoom(axes[axisID], normScale / matchScale); | ||
// TODO |
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.
still TODO
?
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.
ignore. my bad
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.
removed in a later commit I 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.
yeah sorry - TODO removed here
so that user apps that listen to relayouts get the same form as before
- so that attribute that declare possible values as regex e.g. axis anchor and overlaying pass `Lib.validate()`
- e.g. axis 'anchor' declare both x and y values, but coerce x or y values depending on the container. - similarly for 'overlaying' and 'contraintoward'
Validate dynamic enumerated
src/plots/cartesian/dragbox.js
Outdated
@@ -733,6 +735,8 @@ module.exports = function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { | |||
var plotDx = xa2._offset - clipDx / xScaleFactor2, | |||
plotDy = ya2._offset - clipDy / yScaleFactor2; | |||
|
|||
// console.log(subplot.id, editX2, editY2, xScaleFactor2, yScaleFactor2, clipDx, clipDy, plotDx, plotDy); |
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.
🔪 please
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 -> fa78376
); | ||
var passed; | ||
|
||
if(Array.isArray(actual) && Array.isArray(expected)) { |
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.
🎉
|
||
it('can change per-axis constrain:domain/range and constraintoward', function(done) { | ||
Plotly.plot(gd, | ||
// start with a heatmap as it has no padding so calculations are easy |
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 for all these comments in this suite. They make my life a lot easier.
.then(done); | ||
}); | ||
|
||
it('can constrain date axes', 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.
Would it be too much to ask you to add a 'can constrain category axes'
test case?
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.
hah, I set myself up for that one didn't I. Sure. I'll include 'log'
too.
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.
category and log axis domain and range constraint tests to match the category one -> 67ea3a8
💃 💃 💃 |
An alternative mechanism to enforce axis scale constraints: instead of just altering
range
as in #1522 this PR allows you to keep exactly the specified ranges, and restrictsdomain
instead. That way no extraneous regions of the data axes are visible.We add two new attributes to axes to handle this:
constrain
: default isconstrain: 'range'
(gives the behavior in Axis constraints #1522), the new behavior of this PR is invoked byconstrain: 'domain'
constraintoward
: which direction to allow the data (either range or domain depending onconstrain
) to float. default is'center'
(x) or'middle'
(y) but you can also use'left'
and'right'
for x axes, and'top'
and'bottom'
for y. For example,'left'
means that the left end of the range or domain is unchanged when enforcing the constraint, so only the right end will change. This is most useful withconstrain: 'domain'
to control where the whitespace shows up, for example if you want the subplot to push next to a neighboring subplot or next to a legend.scaleanchor
in one of them.constrain: 'domain'
in both axes.Also included here are a bunch of tweaks to unrelated test suites, so that the whole
npm run test-jasmine
passes on my machine. One of these fixes uncovered a bug - fixed in #1753 which was merged into this branch so this PR will get that fix into master.cc @etpinard