-
-
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
Transform react #2577
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
else { | ||
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.
src/traces/mesh3d/defaults.js
Outdated
@@ -86,4 +86,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout | |||
} | |||
|
|||
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 ✨
*/ | ||
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
null
for_length
- I consideredfalse
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 ofsupplyDefaults
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(...) }
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!
src/lib/is_array.js
Outdated
} | ||
|
||
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
@@ -37,12 +39,14 @@ module.exports = function handleXYZDefaults(traceIn, traceOut, coerce, layout, x | |||
if(!isValidZ(z)) return 0; | |||
|
|||
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.
*/ | ||
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 |
test/jasmine/tests/plot_api_test.js
Outdated
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.
@@ -79,6 +79,22 @@ describe('Test scatter', function() { | |||
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 😃
@@ -159,9 +159,6 @@ module.exports = function plot(gd, plotinfo, cd) { | |||
bPosPxOffset = boxLineWidth; | |||
} | |||
|
|||
// 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 🌈
@@ -73,7 +73,8 @@ exports.attributes = { | |||
'For example, with `groups` set to *[\'a\', \'b\', \'a\', \'b\']*', | |||
'and `styles` set to *[{target: \'a\', value: { marker: { color: \'red\' } }}]', | |||
'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 👏
test/jasmine/tests/plot_api_test.js
Outdated
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.
@@ -156,5 +156,13 @@ module.exports = { | |||
].join(' ') | |||
}, | |||
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 😬
@@ -99,7 +99,7 @@ function heatmappishCalc(gd, trace) { | |||
z: z, | |||
}; | |||
|
|||
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
@@ -56,7 +56,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout | |||
var isConstraint = (coerce('contours.type') === 'constraint'); | |||
|
|||
// 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
react
was 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/update
was that we had already deletedgd.calcdata
; but inPlotly.react
we callsupplyDefaults
before we know what kind of a change we have, so we haven't deletedcalcdata
yet. 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
:arrayAttrs
describe the same collection of objects, point-by-point._length
points 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
_length
when they run, but I don't think that's strictly necessary, since the regularsupplyDefaults
runs after the transform anyway.