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

[take 2] Implement scatter cliponaxis: false #1861

Merged
merged 21 commits into from
Jul 13, 2017
Merged

Conversation

etpinard
Copy link
Contributor

@etpinard etpinard commented Jul 7, 2017

Resolves #1385 (hopefully) and supersedes #1824

This PR implements cliponaxis: false by adding some logic around each subplot's <g.plot> layer. In brief, subplots that have 1 or more cliponaxis: false trace don't clip the <g.plot> layer (where all trace layers reside) and instead clip all trace module layers except <g.scatterlayer> separately. In <g.scatterlayer>, cliponaxis: false traces clip all line and error bars group while leaving marker and text node unclipped. Marker and text nodes visibility is then determined via ax.isWithinRange and set to 'hidden' when they fall outside the axis ranges. Note that, I chose to hide those outside range nodes instead of removing them like in #1824 to more easily handle panning (see 1909034)

Ternary scatter cliponaxis: false is implement similar by mocking a few thing like ax.isWithinRange and subplot options in scatterternary/plot.js. Note that I had to add another clip path definition (see 4b5c4bb). There might be a way to DRY this up 🌴

MAYBEs (as in #1824):

  • implement cliponaxis: false for scattergeo (might leave this for another PR)

etpinard added 8 commits July 7, 2017 11:49
- keep track of which subplots have 1 or more `cliponaxis: false` traces
  + if so, don't clip the subplot layer
  + instead, clip all trace module layers except for 'scatterlayer'
  + when `cliponaxis: false`, clip all lines and errorbar groups
- use ax.isWithinRange and Drawing.hideOutsideRangePoint to
  hide markers and text node that are out of range.
- similar to the cartesian case, keep track of
  subplots that have 1 or more `cliponaxis: false` traces
- override setConvert's ax.isPtWithinRange
- add relative clip path def (to appropriately clip lines group)
@etpinard etpinard added this to the 1.29.0 milestone Jul 7, 2017
@etpinard
Copy link
Contributor Author

etpinard commented Jul 7, 2017

@alexcjohnson @geocosmite in 7228d5d, you'll notice that the marker and text node appear below the axis labels and ticks. After some thoughts, we're thinking about adding another attribute (maybe (x|y)axis.abovetraces: true | false) to allow both behaviors.

Arguably, trace layers should always be above axis labels and ticks, except for axes with position: 'free'. This will be implemented in another PR shortly.

var x = d.xp || xa.c2p(d.x),
y = d.yp || ya.c2p(d.y);
var x = d.xp = xa.c2p(d.x);
var y = d.yp = ya.c2p(d.y);
Copy link
Collaborator

Choose a reason for hiding this comment

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

wow, || is 4 years old there... but it doesn't look to me like .xp and .yp are used anywhere except https://github.com/plotly/plotly.js/blob/master/src/traces/scatter/plot.js#L489 which is a near copy of this routine. Take it out? Must have been a perf optimization that we did long ago but is now mostly gone.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 62ab845

drawing.hideOutsideRangePoint = function(d, sel, xa, ya) {
sel.attr(
'visibility',
xa.isPtWithinRange(d) && ya.isPtWithinRange(d) ? null : 'hidden'
Copy link
Collaborator

Choose a reason for hiding this comment

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

possibly better to use display: 'none' instead? visibility: 'hidden' still includes the element in the rendering pipeline (so children could be made visible for example, as we've seen!), which is probably slower and also means it still contributes to the bounding box (potential confusion for illustrator? I suppose we could also add something to our static plot pipeline to remove visibility: 'hidden' and display: 'none' elements completely...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

possibly better to use display: 'none' instead?

Would be nice to benchmark these things, but you make a good argument for display. Good point above illustrator not liking visibility 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done in 9becb7c

errorbars.call(
Drawing.setClipUrl,
plotinfo._hasClipOnAxisFalse ? plotinfo.clipId : null
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

perhaps it would simplify things if when we calculate _hasClipOnAxisFalse we stash plotClipId and layerClipId (each of which is either clipId or null)?

Also a debatable style point, I've started avoiding .call when it's not part of a method chain - so this would just be Drawing.setClipUrl(errorbars, clipId)

plotinfo.plot.call(joinPlotLayers);

for(var i = 0; i < constants.layers.length; i++) {
joinLayer(plotinfo.plot, 'g', constants.layers[i]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

🎉

@@ -257,6 +285,9 @@ proto.adjustLayout = function(ternaryLayout, graphSize) {
_this.clipDef.select('path').attr('d', triangleClip);
_this.layers.plotbg.select('path').attr('d', triangleClip);

var triangleClipRelative = 'M0,' + h + 'h' + w + 'l-' + (w / 2) + ',-' + h + 'Z';
_this.clipDefRelative.select('path').attr('d', triangleClipRelative);
Copy link
Collaborator

Choose a reason for hiding this comment

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

heh, yet another consequence of when we had nested <svg> elements instead of clip paths - I suspect we could simplify a whole lot of stuff by not translating containers, but that's a big project. Until then we need stuff like this 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for tip. This simplified a lot of plot.js level logic. Done in 0496144

@alexcjohnson
Copy link
Collaborator

Feels great panning with these plots @etpinard - watching the points pop in and out of view! Two strange things I'm seeing right now while panning (both of which are fixed on mouseup, the issue is only during the pan interaction):

  • in ternary (using the ternary_markers mock), the clip path for lines seems to translate along with the pan:
    screen shot 2017-07-07 at 1 55 45 pm 2

  • in cartesian (using the cliponaxis_false mock) everything looks perfect for a while:
    screen shot 2017-07-07 at 2 07 24 pm 2
    but when I pan a bit farther, the lines and fills abruptly disappear:
    screen shot 2017-07-07 at 2 07 32 pm 2
    and then they're fixed again on mouseup:
    screen shot 2017-07-07 at 2 07 35 pm 2

@etpinard
Copy link
Contributor Author

Some updates:


Now, some questions came up in axis-abovetraces, I wouldn't mind hearing opinions from @alexcjohnson @cldougl and @geocosmite :

  • should cliponaxis: false override all auto range options passed to Axes.expand or should it only have an effect with traces mode: 'markers'? I'd vote for the former. I wouldn't mind a second opinion here.
  • I'm not a big fan of the aboveaxis attribute name. If it remains a boolean, it does not allow us to add other axis levels. Moreover, I don't see it play well with a future attribute to would determine if grid lines are drawn above traces. Any thoughts on this thing? Maybe an enumerated axislevel: 'above-traces' attribute would be better?
  • Essentially, the aboveaxis default value is true on master as all traces are drawn below axis lines and subplots. @alexcjohnson argued for making the aboveaxis default be false except for position: 'free' axes. I'm a little more reticent on the subject as this generate sub-pixel diffs in all our cartesian mocks (see test branch). To note, matplotlib drawn axes above traces by default. I'd vote for making the aboveaxis default true for now and perhaps revisit this topic in v2.

@alexcjohnson
Copy link
Collaborator

The cartesian pan issue described in the same #1861 (comment) seem to be caused by a Chrome bug. That is, the issue isn't reproducible in FF. We can fix this issue by wrapping each scatter <path> node with <g> and clipping that <g> instead of those <path>. I'm not sure it's worth it though.

Just played with this a bit on browserstack: the chrome bug came in between versions 31 and 32. That's pretty ancient, If we decide not to work around it (which I guess I'd be OK with, it's fairly obscure), we should at least lock down when it happens and make sure it's on their radar to fix.

That said, IE and Edge have a substantially worse problem with this type of panning, they don't seem to translate the clip path correctly:
screen shot 2017-07-10 at 6 43 57 pm 2
I'll note though that this is a problem with IE and Edge on master too... so could be dealt with as a separate bug, whereas the chrome bug seems to not affect us on master.

etpinard added 3 commits July 11, 2017 09:58
- `display: 'none'` completely removes the elements from
  the rendering pipeline as is likely faster is all browsers.
- to DRY up Drawing.setClipUrl calls downstream,
  where `if(plotinfo._hasClipOnAxisFalse)` statements
  are removed.
@alexcjohnson
Copy link
Collaborator

should cliponaxis: false override all auto range options passed to Axes.expand or should it only have an effect with traces mode: 'markers'? I'd vote for the former. I wouldn't mind a second opinion here.

Lets step back and look at what these options do and why, and what we're trying to accomplish with the changes we're making now. The options to Axes.expand are:

  • vpad(plus|minus)? - "value padding" ie give more space around the data points in data units. Not really needed, currently only used for convenience by box traces (where the box and jitter position and width are set in data units), we should probably just fold this into the expand data, as we must have already done for bars.
  • ppad(plus|minus)? - "pixel padding" ie give more space around the data points in pixels. Used to make sure markers, annotations, and shape borders fit within the plot boundaries.
  • padded - a generic 5% padding on both sides. Used to give some more context - implicitly I guess means that either we assume this axis has measured data (which is why for example line charts get padding on the y axis but not x) - or because we think it would generally look bad to squish the data right up against the axis (eg markers & error bars)
  • tozero - bars & area charts use this to ensure the axis goes to zero - so you see the whole bar/fill - but also this eliminates padding on the other side of zero (whichever side that is!) if the data all has the same sign.

cliponaxis: false serves a very specific purpose: to ensure that we give each point (marker) the correct visual weight without needing to expand the axis range beyond its data value - either to avoid meaningless axis values or just to ensure that the axis range reflects the data range. To me this seems to only apply to the ppad* and padded options associated with markers, and using cliponaxis: false to signify a more generic "keep autorange tightly bounded around these data" seems like it would be an unexpected result.

But that rationale brings up another way to do this, something like xaxis.maxautorange that would say "never let autorange go beyond this range regardless of the data" (for example percentage data would get maxautorange: [0, 100], that would let you constrain the autorange routine to physically meaningful values without us needing to monkey with the Axes.expand routine at all - if the data don't bump up against these limits we still pad them with this tried-and-true mechanism. We already offer one common variant of this with rangemode: 'nonnegative' to prevent autoranging past zero.

@alexcjohnson
Copy link
Collaborator

I'm not a big fan of the aboveaxis attribute name. If it remains a boolean, it does not allow us to add other axis levels. Moreover, I don't see it play well with a future attribute to would determine if grid lines are drawn above traces. Any thoughts on this thing? Maybe an enumerated axislevel: 'above-traces' attribute would be better?

Good idea. I can see for example wanting to layer the axis either above or below shapes, which are themselves above or below the data... Not sure about axislevel though, that sounds kind of data-related. axislayer? And can we use a space instead of '-', ie 'above traces'? Matches for example textposition: 'top left'. Do we have '-' used like this elsewhere?

Essentially, the aboveaxis default value is true on master as all traces are drawn below axis lines and subplots. @alexcjohnson argued for making the aboveaxis default be false except for position: 'free' axes. I'm a little more reticent on the subject as this generate sub-pixel diffs in all our cartesian mocks (see test branch). To note, matplotlib drawn axes above traces by default. I'd vote for making the aboveaxis default true for now and perhaps revisit this topic in v2.

Seems reasonable. If we're accidentally drawing axis lines somewhat underneath the data and giving them a thinned or ragged look, that could give us a subtly unpolished look for no apparent reason, whereas markers dropping underneath the axis are obvious and easy to fix - just make sure to mention cliponaxis and axislayer (or whatever) in each others description fields.

@cldougl
Copy link
Member

cldougl commented Jul 11, 2017

  • +1 for an enumerated option. I like axislayer a bit more than axislevel
  • I would vote for keeping this consistent with master for now (leaving aboveaxis as true or the enumerated equivalent)

@etpinard
Copy link
Contributor Author

Not sure about axislevel though, that sounds kind of data-related. axislayer?

Good call. Going with axislayer!

And can we use a space instead of '-', ie 'above traces'? Matches for example textposition: 'top left'. Do we have '-' used like this elsewhere?

Good call here too. Dropping those -.

etpinard added 3 commits July 11, 2017 17:24
- which is more meaningul
- other misc lint
- has 2 possible values: 'above traces' and 'below traces' for now,
  but opens the door for other values down the road.
- show off `(x|y)axis.layer`
- lock down behavior for `cliponaxis: true` traces
  on hasClipOnAxisFalse subplot
- add annotations for more descriptive mock
@etpinard
Copy link
Contributor Author

Not sure about axislevel though, that sounds kind of data-related. axislayer?
Good call. Going with axislayer!

And can we use a space instead of '-', ie 'above traces'? Matches for example textposition: 'top left'. Do we have '-' used like this elsewhere?
Good call here too. Dropping those -.

done in #1871

etpinard added 2 commits July 12, 2017 16:40
- which replaces all `require('d3')` with `require('./strict-d3')`
- so that strict-d3.js can more easily be used across all our tools
- activate when bundling plotly.js in jasmine tests,
  `npm run watch` and `npm run cibuild`
- 🔪 <script> injecting old IIFE
- require('fast-isnumeric') in strict-d3.js
- instead of on <clipPath> element itself
- see https://codepen.io/etpinard/pen/eRbxrp for more info
  on the problem
- lock down this rule by adding condition in strict-d3.js
@alexcjohnson
Copy link
Collaborator

Just spent a bit of time playing with this in various browsers - I think you've nailed it! 💃
One thing I noticed while playing is vector-effect: non-scaling-stroke hasn't made its way to errorbars yet. We can address that separately though.

@etpinard
Copy link
Contributor Author

One thing I noticed while playing is vector-effect: non-scaling-stroke

Ha. Yeah, I noticed the bug too, but I didn't know it had to do with the vector-effect attribute.

Track issue ➡️ #1879

@etpinard
Copy link
Contributor Author

etpinard commented Jul 13, 2017

Merging this thing. It's about time. But this leaves the item under MAYBEs unimplemented.

See companion issue ➡️ #1880

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature something new
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants