8000 Transform react by alexcjohnson · Pull Request #2577 · plotly/plotly.js · GitHub
[go: up one dir, main page]

Skip to content

Transform react #2577

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

Merged
merged 26 commits into from
Apr 26, 2018
Merged

Transform react #2577

merged 26 commits into from
Apr 26, 2018

Conversation

alexcjohnson
Copy link
Collaborator

A collection of fixes for transforms, particularly in conjunction with Plotly.react - closing #2470 and #2508, and along the way completing #1410.

The primary issue with transforms and react was the remapping of transformed arrays that we do at the end of supplyDefaults - the signal not to do this before a recalc, as initiated by Plotly.restyle/relayout/update was that we had already deleted gd.calcdata; but in Plotly.react we call supplyDefaults before we know what kind of a change we have, so we haven't deleted calcdata yet. The solution was pretty simple, to conditionally break out the remapping step and apply it later as required. This is fcc459d

The other major thing I did here was to standardize the meaning of trace._length. It can either be a positive integer or null, and every trace type MUST define this during supplyDefaults:

  • If it's an integer, that means the trace data is 1D and all the arrayAttrs describe the same collection of objects, point-by-point.
    • Transforms are allowed, and they will operate on the first _length points in each array.
  • If it's null, that means either it has 2D data, or it's 1D but the arrays have different meanings - like nodes and links in sankey, or vertices and vertex indices in mesh3d.
    • Transforms are disabled. If we ever develop a transform that makes sense for any of these cases, we'll have to revise this condition, but currently all our transforms assume 1D data.

cc @etpinard @nicolaskruchten
cc @bpostlethwaite @VeraZab this may require some minor updates to the transforms in chart studio for robustness, though I suspect they'll mostly work already... one thing to note is I had the built-in transforms here update _length when they run, but I don't think that's strictly necessary, since the regular supplyDefaults runs after the transform anyway.

so aggregations and other common routines only have to look at _length
…forms

also makes transforms work with parcoords (and by extension splom) by
having findArrayAttributes dig into container arrays
plus whatever Array.sort is of course... O(n log(n)) or whatever
heatmap and carpet used opposite meanings here...
@nicolaskruchten
Copy link
Contributor

Exciting! So how can a UI like react-chart-editor determine if a given trace in a figure is elligible for transforms? Is this available in the schema or in _fullData or somewhere or should we inline a list of trace types that support transforms?

@alexcjohnson
Copy link
Collaborator Author

So how can a UI like react-chart-editor determine if a given trace in a figure is eligible for transforms?

If you have _fullData look at trace._length: if it's a positive integer you can transform it, if it's null you can't. One caveat though, if the trace isn't visible it does not need to define _length so you wouldn't know. I suppose that applies everything though so it must be standard for the editor to not be able to really edit invisible traces.

We could add this to the schema I guess, but there are a few cases where the type isn't enough to know whether you can transform it or not. Chiefly heatmap and contour, where if z is 1D you can transform it, but if it's 2D you can't.

else {
coerce('x0');
len = y.length;
}
} else if(x && x.length) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be hasX.

col2Calendar = trace[var2Name + 'calendar'];
var colLen = trace._length;
var col1 = trace[var1Name].slice(0, colLen);
var col2 = trace[var2Name].slice(0, colLen);
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh so nice to .slice() only once!

y = coerce('y');
var x = coerce('x');
var y = coerce('y');
var hasX = x && x.length;
Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect x && x.length is faster than Array.isArray(x) && x.length. Might be worth standardizing lib/is_array.js (as e.g. Lib.isNonEmptyArray) ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

e.g. Lib.isNonEmptyArray

Ah hmm, before I do that, I want to check this against our previous behavior for trace types that don't necessarily need both coordinates specified - like scatter, which can use x0/dx along with a y array. For example if x=[], y=[1, 2, 3] we should treat that as _length=0, not 3, right? I'll need to look over our test coverage for those cases...

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 I looked at empty-array behavior in more detail 6fae229 some of these went away. So I've not created Lib.isNonEmptyArray for the time being.

@@ -86,4 +86,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout
}

coerce('text');

// disable 1D transforms
traceOut._length = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, mesh3d only expects 1D data, so _length could have a meaning and transforms could work.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah nevermind, i/j/k and x/y/z lengths don't match for mesh3d traces. Can you add a comment about this above this line like you did for sankey traces?

}
}
for(i = 0; i < len; i++) {
indices[i] = sortedArray[i].i;
Copy link
Contributor

Choose a reason for hiding this comment

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

Brilliant ✨

*/
var INFO_PATTERNS = /(^|\.)((domain|range)(\.[xy])?|args|parallels)$/;
Copy link
Contributor
@etpinard etpinard Apr 23, 2018

Choose a reason for hiding this comment

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

TODO:

  • check that the old toolpanel does not break

Copy link
Contributor

Choose a reason for hiding this comment

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

Moreover @alexcjohnson, what if in the very unlikely scenario of someone writing in that their app relies on the old pruning behavior, what should we do?

I suspect reverting this commit would break some test(s) added in this PR (which one by the way?), so maybe we could formulate a solution for that hypothetical user?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The reason I made this commit now was the trace._length = null, which in some cases was getting pruned later on in supplyDefaults by attributes with dflt: null and breaking my redraw all the things test addition. I could have handled this in a few different ways:

  • use something other than null for _length - I considered false but to me that had the implication of "has no length" rather than what I wanted, "(1D) length does not apply." Anyway the whole idea that we're pruning in the middle of supplyDefaults which is really only supposed to be building the _full* objects I wanted to avoid.
  • make a special code path that does no pruning or unsetting - like nestedProperty.onlySet(val) - but that's adding complexity so not ideal.
  • get rid of all dflt: null (and perhaps even enforce that) throughout our attributes. I still think we should do that, but I opted not to do it here.

Anyway, to the question:

what if in the very unlikely scenario of someone writing in that their app relies on the old pruning behavior, what should we do?

If it leads to a visible change in the plot, we should fix it so the API does not contain such a dependence on the existence of empty containers ;)
If it just alters the data structures (data and layout I guess, after a restyle/relayout) I can't really predict why a user would be bothered by this, but we can help them wrap their restyle/relayout with something like if(val === null) { pruneContainersPulledFromThisCommit(...) }

Copy link
Contributor

Choose a reason for hiding this comment

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

Great answer. Thanks!

}

function is1D(a) {
return !isArrayOrTypedArray(a[0]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe isArray1D would be clearer as this assumes that you're passing an array (or at least something you can index in).

We might want to add a comment above about that assumption too, in case someone in the future wants to turn this into:

return Array.isArray(a) && !isArrayOrTypedArray(a[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.

2a41f9e -> isArray1D

// "true" skips updating calcdata and remapping arrays from calcTransforms,
// which supplyDefaults usually does at the end, but we may need to NOT do
// if the diff (which we haven't determined yet) says we'll recalc
Plots.supplyDefaults(gd, true);
Copy link
Contributor
@etpinard etpinard Apr 23, 2018

Choose a reason for hiding this comment

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

I'm not a big fan of this pattern.

At the very least we should turn this into Plots.supplyDefaults(gd, {skipDefaultsUpdateCalc: 1}) to make it more readable and extendable.

Personally, I'd vote for removing the newly added Plots.supplyDefaultsUpdateCalc() from Plots.supplyDefaults and call Plotly.supplyDefaultsUpdateCalc after Plots.supplyDefaults in the places that new it.

On master, we have:

image

where the calls in validate.js and rangeslider/draw.js (and maybe more) don't need to update calc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Perhaps the name supplyDefaultsUpdateCalc isn't the best - though you could argue the same about calcTransform, which happens during the calc step but updates the data arrays in _fullData. supplyDefaultsUpdateCalc has one simple function when there are no transforms, to attach the new traces to the old calcdata in case we're not going to run a recalc, but when there are transforms it also (first) pulls the transformed array attributes back from the old fullTrace to the new one. So I think in fact we do need this in most instances, and there are some external callers (like streambed) that depend on this as well.

So I'm going to keep it as part of the default supplyDefaults behavior, but I'm happy to make it into an options object for clarity.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

cleaner supplyDefaults API -> 2a41f9e

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.

Great PR.

I'll admit when you told us you were trying to fix react + transforms as part of a patch release, I was a little concerned of potential breaking changes. I'm still a little concerned about updating the pruning behavior in nestedProperty, but all in all this PR worked out pretty well I think.