-
-
Couldn't load subscription status.
- Fork 1.9k
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
Transform react #2577
Conversation
so aggregations and other common routines only have to look at _length
plus whatever Array.sort is of course... O(n log(n)) or whatever
heatmap and carpet used opposite meanings here...
…forms if it's falsy
|
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 |
If you have We could add this to the schema I guess, but there are a few cases where the |
src/traces/box/defaults.js
Outdated
| coerce('x0'); | ||
| len = y.length; | ||
| } | ||
| } else if(x && x.length) { |
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 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); |
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.
Oh so nice to .slice() only once!
src/traces/histogram/defaults.js
Outdated
| y = coerce('y'); | ||
| var x = coerce('x'); | ||
| var y = coerce('y'); | ||
| var hasX = x && x.length; |
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 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) ?
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.
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...
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.
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.
| coerce('text'); | ||
|
|
||
| // disable 1D transforms | ||
| traceOut._length = null; |
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.
Actually, mesh3d only expects 1D data, so _length could have a meaning and transforms could work.
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 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; |
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.
Brilliant ✨
| * in restyle/relayout/update for "delete this value" whereas undefined means | ||
| * "ignore this edit" | ||
| */ | ||
| var INFO_PATTERNS = /(^|\.)((domain|range)(\.[xy])?|args|parallels)$/; |
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.
TODO:
- check that the old toolpanel does not break
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.
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?
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.
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
nullfor_length- I consideredfalsebut 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 ofsupplyDefaultswhich 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(...) }
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 answer. Thanks!
| } | ||
|
|
||
| function is1D(a) { | ||
| return !isArrayOrTypedArray(a[0]); |
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.
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]);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.
2a41f9e -> isArray1D
src/plot_api/plot_api.js
Outdated
| // "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); |
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 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:
where the calls in validate.js and rangeslider/draw.js (and maybe more) don't need to update calc.
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.
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.
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.
cleaner supplyDefaults API -> 2a41f9e
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 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.
src/plots/plots.js
Outdated
| var cd0 = oldCalcdata[i][0]; | ||
| if(cd0 && cd0.trace) { | ||
| if(cd0.trace._hasCalcTransform) { | ||
| remapTransformedArrays(cd0, newTrace); |
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.
remapTransformedArrays is only called from here, maybe we could merge it in here?
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.
merged and 🐎 remapTransformedArrays -> 2a41f9e
test/jasmine/tests/plot_api_test.js
Outdated
| }; | ||
| } | ||
|
|
||
| function reactWith(fig) { |
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 cute tests, especially this function name 💟
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.
... and I'm impressed that bbe3533 and standardizing _length was all we needed to get transforms to work with splom and parcoords 🎉
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 would actually name this reactTo personally, in both senses of the word 'to' (with and towards) :)
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.
good call @nicolaskruchten -> reactTo in cd08479
|
|
||
| coerce('transpose'); | ||
|
|
||
| traceOut._length = null; |
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.
What if instead of null, 2D arrays would link a length-2 array to _length of row and column lengths? Of course, this won't solve the transform-and-2d-array-traces problems by itself, but it might help us clean up some indexing downstream logic like extracting values for hover labels and event data (e.g. here and here)
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.
What if instead of null, 2D arrays would link a length-2 array to _length of row and column lengths?
I could imagine that being helpful but it sounds like a bit of a project in itself - not quite sure how it would mesh with the logic you linked for example, which may be at least partially after reshaping from columns to a grid. Anyway we can add that later, so I'd like to defer it for now.
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'd like to defer it for now.
Sounds good.
| * in restyle/relayout/update for "delete this value" whereas undefined means | ||
| * "ignore this edit" | ||
| */ | ||
| var INFO_PATTERNS = /(^|\.)((domain|range)(\.[xy])?|args|parallels)$/; |
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.
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?
|
Oh, one more thing. I don't think the checklist in #2508 (comment) got completed. |
- is1D -> isArray1D - improved comments - opts object instead of confusing boolean to Plots.supplyDefaults - merge & simplify remapTransformedArrays - hasX
it's not an attribute, and box/plot doesn't mind it being undefined
especially pushing auto values back to input when there's a groupby transform
completed in 965bcfb |
| it('tested every trace & transform type at least once', function() { | ||
| for(var itemType in typesTested) { | ||
| expect(typesTested[itemType]).toBeGreaterThan(0, itemType + ' was not tested'); | ||
| } |
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 when a new trace or transform type is added we'll be forced to add it to this suite as well. (note above I excluded contourgl and, on CI, scattermapbox)
I couldn't think of a good way to ensure that every component gets tested as well... in fact looking over the list now I don't see shapes or 3D annotations, I'll have to add those. If anyone has ideas how to enforce this please chime in!
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 when a new trace or transform type is added we'll be forced to add it to this suite as well
🥇
If anyone has ideas how to enforce this please chime in!
There's probably a way using Registry.componentsRegistry and the components' schema declaration (e.g. this one for range sliders). Instead of looping over traces and checking the type, we'd have to loop over gd.layout keys.
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.
OK - still sounds a bit finicky... perhaps a later iteration we could add a test that ensures we've covered every attribute anywhere in the schema - that would be a significantly more stringent coverage test for traces and transforms as well! But if you don't mind I'll defer this for now.
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.
But if you don't mind I'll defer this for now.
No problem 👍
test/jasmine/tests/plot_api_test.js
Outdated
| .then(done); | ||
| } | ||
|
|
||
| mockList.forEach(function(mockSpec) { |
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 there anything we can think of that could be asserted about the cartesian product here? I.e. given M1 and M2 is there anything in the JSON representation that we want to try to assert about M1.reactWith(M2).reactWith(M1) ?
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 are likely some things that are desirable that are already true in all cases and might be worth locking down, and some things that are desirable that are true in all but a small subset of cases and could be fixed easily (low-hanging fruits). There are also probably a large number of desirable things that aren't true and that are hard to fix, which I'm less interested in in the short run.
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 tested a few of those transitions in fcc459d#diff-5a140ce1cc87a420496039bbf8d699f1R3114 - and in fact after this PR (knock wood) I kind of don't think there will be a lot of nasty issues uncovered by testing this cartesian product. It's also going to be a very long test if you want to cover every combination, so it sounds to me more like we'd want to test them all once, offline, find the subset that fail, and incorporate just those into a new test.
src/plots/plots.js
Outdated
| * trace into the new one. Use skipUpdateCalc to defer this (needed by Plotly.react) | ||
| * | ||
| * gd.data, gd.layout: | ||
| * are precisely what the user specified, |
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.
not quite precisely. cleanData and cleanLayout happen before.
| expect(traceOut.visible).toBe(false); | ||
| }); | ||
|
|
||
| [{letter: 'y', counter: 'x'}, {letter: 'x', counter: 'y'}].forEach(function(spec) { |
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(s) here 😃
| } | ||
|
|
||
| // do not draw whiskers on inner boxes | ||
| trace.whiskerwidth = 0; |
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 find!
| // We pushed a colorscale back to input, which will change the default autocolorscale next time | ||
| // to avoid spurious redraws from Plotly.react, update resulting autocolorscale now | ||
| // This is a conscious decision so that changing the data later does not unexpectedly | ||
| // give you a new colorscale |
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 📚
|
|
||
| function styleUpdater(groupName, transformIndex) { | ||
| return function(trace, attr, value) { | ||
| Lib.keyedContainer( |
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.
Ricky's dream lives on 🌈
| 'marker points in group *\'a\'* will be drawn in red.' | ||
| ].join(' ') | ||
| ].join(' '), | ||
| _compareAsJSON: 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 solution 👏
| it('tested every trace & transform type at least once', function() { | ||
| for(var itemType in typesTested) { | ||
| expect(typesTested[itemType]).toBeGreaterThan(0, itemType + ' was not tested'); | ||
| } |
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 when a new trace or transform type is added we'll be forced to add it to this suite as well
🥇
If anyone has ideas how to enforce this please chime in!
There's probably a way using Registry.componentsRegistry and the components' schema declaration (e.g. this one for range sliders). Instead of looping over traces and checking the type, we'd have to loop over gd.layout keys.
| }, | ||
| editType: 'calc' | ||
| }, | ||
| transforms: { |
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.
Wow. This means transforms will appear on https://plot.ly/javascript/reference/. I guess they are now an official feature 😬
| }; | ||
|
|
||
| if(trace.contours.type === 'levels') { | ||
| if(trace.contours.type === 'levels' && trace.contours.coloring !== 'none') { |
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.
Did omitting coloring !== 'none' lead to a bug outside of Plotly.react?
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.
possibly for the editor - zmin, zmax, and zauto were set in _fullData, implying that these were useful attributes for this case when in fact they are ignored.
src/traces/contourcarpet/defaults.js
Outdated
|
|
||
| // Unimplemented: | ||
| // coerce('connectgaps', Lib.is1D(traceOut.z)); | ||
| // coerce('connectgaps', Lib.isArray1D(traceOut.z)); |
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.
Probably best to 🔪 these lines completely as well as the connectgaps declaration in ./attributes.js.
|
@alexcjohnson you might need to add a longer test timeout in one of your new tests in Apart from that, this PR is 🏅 💃 for me. |
|
Toolpanel works fine, the only thing I had to do was remove this line: As far as I can tell |


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
reactwas the remapping of transformed arrays that we do at the end ofsupplyDefaults- the signal not to do this before arecalc, as initiated byPlotly.restyle/relayout/updatewas that we had already deletedgd.calcdata; but inPlotly.reactwe callsupplyDefaultsbefore we know what kind of a change we have, so we haven't deletedcalcdatayet. The solution was pretty simple, to conditionally break out the remapping step and apply it later as required. This is fcc459dThe other major thing I did here was to standardize the meaning of
trace._length. It can either be a positive integer ornull, and every trace type MUST define this duringsupplyDefaults:arrayAttrsdescribe the same collection of objects, point-by-point._lengthpoints in each array.null, that means either it has 2D data, or it's 1D but the arrays have different meanings - like nodes and links insankey, or vertices and vertex indices inmesh3d.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
_lengthwhen they run, but I don't think that's strictly necessary, since the regularsupplyDefaultsruns after the transform anyway.