-
-
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
Plotly.react #2341
Plotly.react #2341
Conversation
@@ -58,6 +56,8 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { | |||
|
|||
if(showLegend === false) return; | |||
|
|||
layoutOut.legend = containerOut; |
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.
legend's visibility is controlled by showlegend
, which is outside the legend
container itself, unlike most of our newer containers that have visible
inside the container. Unless and until we change that (v2?) the legend container should not exist when it's not needed. This was causing me problems as relinkPrivateKeys
was humorously removing newFullLayout.legend
if it was empty and oldFullLayout.legend
existed... so its existence would toggle on Plotly.react
calls, making it look like the legend was always in need of updating 💫
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 nice catch.
until we change that (v2?)
I'd vote +1 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.
added to #420
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 legend container should not exist when it's not needed.
so by default, in _fullLayout, the .legend key wouldn't be there?
we read from _fullLayout to display widgets in the editor, if there's no .legend key, or a ._fullLayout.legend.visible key, then the widget won't display..
We could have a work around, but it's kind of one of the basic premises of widget visibility in the editor..
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.
@VeraZab you must not have been dependent on the existence of a legend
container up to now, as it was unreliable ^^ - showlegend
is still there, but now it's reliably the case that if _fullLayout.showlegend=false
there will be NO _fullLayout.legend
container.
@@ -1632,26 +1639,6 @@ function _restyle(gd, aobj, traces) { | |||
} | |||
} | |||
|
|||
// check axes we've flagged for possible deletion | |||
// flagAxForDelete is a hash so we can make sure we only get each axis once | |||
var axListForDelete = Object.keys(flagAxForDelete); |
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 isn't really related to Plotly.react
but noticed as I was working here: post #2227 we don't need to delete unused axes anymore - in fact we don't want to.
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 but it's not self-contained here - I need to get rid of flagAxForDelete
above...
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.
finished deleting in aab4f11
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 has most probably caused:
#2392
|
||
// you can use this as the initial draw as well as to update | ||
if(!Lib.isPlotDiv(gd) || !oldFullData || !oldFullLayout) { | ||
plotDone = Plotly.newPlot(gd, data, layout, config); |
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.
Plotly.react
is a one-stop-shop, it can replace Plotly.newPlot
and Plotly.plot
entirely.
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 dream
if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined; | ||
|
||
// Note: what restyle/relayout use impliedEdits and clearAxisTypes for | ||
// must be handled by the user when using 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.
_restyle
and _relayout
perhaps into helper functions?), but I don't think we should be trying to do anything with impliedEdits
or clearAxisTypes
inside 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.
Can you clarify what you mean by: must be handling by 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.
impliedEdits
are things like turning off axis.autorange
when you set an explicit range
. If you use relayout
et al we do that for you, so Plotly.relayout(gd, 'yaxis.range', [5, 10])
automatically sets yaxis.autorange: false
- otherwise the range wouldn't actually change, as autorange would put it back to what you started with.
The equivalent with Plotly.react
would be:
layout.yaxis.range = [5, 10];
// you have to explicitly disable autorange when using `Plotly.react`
// even if you never explicitly enabled it in the first place, since plotly.js
// inserted `yaxis.autorange: true` in your layout object
layout.yaxis.autorange = false;
Plotly.react(gd, data, layout);
clearAxisTypes
is similar: using Plotly.restyle(gd, 'x', [['a', 'b', 'c']], [0])
will clear the axis types of the axes trace 0 is displayed on, so the autotype algorithm runs again. With Plotly.react
you'll have to explicitly remove xaxis.type
if you want to invoke autotype.
I suppose there's a discussion to be had about this: it would certainly be nice to just make the change you want, the same as you do with relayout
, and have us figure out what we need to do to make it happen. On the other hand, with Plotly.react
you're saying "this is the new plot state I want rendered", and for us to comb through it and say "Oh, I see you made this change, you must have wanted this other change too" seems heavy-handed and error-prone - what if you really don't want that other change, there's no way to tell us that (as there would be with relayout
eg Plotly.relayout(gd, {'yaxis.range': [5, 10], 'yaxis.autorange': true})
(which seems a contrived example, but you might actually do that for example to turn the range from decreasing autorange to increasing autorange).
So rather than bake this into Plotly.react
as we do with relayout
et al, I think we should try and find a way to help the user access the recommendations we would make with impliedEdits
and clearAxisTypes
, but leave it up to them whether to follow those recommendations.
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 you really don't want that other change, there's no way to tell us that
Thanks for writing this. This makes 💯 % total sense 📚
think we should try and find a way to help the user access the recommendations we would make with impliedEdits and clearAxisTypes, but leave it up to them whether to follow those recommendations.
Perhaps all we need is to (under a certain config option) log whenever we mutate user data or user layout. Alternatively, Plotly.validate
could be use here e.g.
Plotly.validate([{
y: [1, 2, 1]
}], {
xaxis: {
autorange: true,
range: [1, 2]
}
})
could log something like: increasing range detected, range values ignored under autorange. Then together with #1741, it should make it fairly easy for plotly.js users to debug.
if(frames) { | ||
gd._transitionData = {}; | ||
Plots.createTransitionData(gd); | ||
seq.push(addFrames); |
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.
anyone want to comment on how we should handle frames
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.
Looks good to me 👌
gd.data = data || []; | ||
helpers.cleanData(gd.data, []); | ||
gd.layout = layout || {}; | ||
helpers.cleanLayout(gd.layout); |
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 a little ambivalent about this - there's part of cleanData
, fillling in uids, that seems like it may not even belong to cleanData
but anyway we need to do it. But the rest, do we need to be cleaning obsolete usages on every Plotly.react
call? These routines mutate the inputs so after the first call the user shouldn't have any of them left; that said users can always provide totally new data/layout...
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 agree, the uid
generation routine should be taken out. It has no place in cleanData
.
In v2
, I'm thinking cleanData
and cleanLayout
should be called only under a special config flag (e.g. compatibility: true
, or a more granular compatibilityLevel: 'v1'
) whereas assigning uid to traces will always have to be done.
cc #2007
if((!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) || gd._context.staticPlot) return; | ||
if(gd._context.staticPlot) { | ||
// this sweeps up more than just cartesian drag elements... | ||
d3.select(gd).selectAll('.drag').remove(); |
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.
Far as I could tell this is the only change needed so config
can be altered with a regular Plotly.plot
call - everything else I tried just seemed to work. If we make more of a supplyDefaults
type framework for config at some point we may be able to minimize the update pathway there too, like modebar-only changes... but it's probably not a very common use case anyhow.
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 about other svg-based subplot types? For example, I'm thinking we could add a similar .remove()
statement for polar 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.
we could... though this one kills the polar draggers too, hence my comment. I originally put the remove
inside the one if((!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) || gd._context.staticPlot)
block, and it broke all polar drag and hover effects.
You were saying something about overly broad selectAll
statements the other day 😏 It wasn't obvious to me how to make this more specific though...
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.
though this one kills the polar draggers too,
Oh nice. Is this tested? If not, would you mind 🔒 ing this down?
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.
🔒 0548db3
src/plots/plots.js
Outdated
newData = gd.data || []; | ||
if(oldFullLayout._skipSD) { | ||
delete oldFullLayout._skipSD; | ||
return; |
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.
my hack to prevent running supplyDefaults
twice in full-replot update pathways.
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! Would you mind renaming it _skipDefaults
? SD
didn't ring a 🔔 for me at first sight, so I suspect it may confuse other devs.
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 -> _skipDefaults
in 1074804
so we can use it to make index arrays
@@ -30,7 +32,7 @@ module.exports = function pushUnique(array, item) { | |||
} | |||
array.push(item); | |||
} | |||
else if(item && array.indexOf(item) === -1) array.push(item); | |||
else if((item || item === 0) && array.indexOf(item) === -1) array.push(item); |
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.
Any problems with this? I'm assuming falsy items were mostly ignored for data filtering purposes, which also should accept 0 I would 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.
None from me. Let's just make sure it's documented in this its test suite.
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 oops, that's done already. Thanks!
@etpinard I still don't understand why I have this big |
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.
Well, this is pretty great. 🎉
I made a few comments and I'll take this PR out for a test drive tomorrow morning, but yeah, I'm really liking the strategy used.
About the package-lock file, I have no idea what's up. What node and npm versions are you using? Did you rebase off the freshest master
before rm -rf node_modules && npm i
?
src/plots/plots.js
Outdated
newData = gd.data || []; | ||
if(oldFullLayout._skipSD) { | ||
delete oldFullLayout._skipSD; | ||
return; |
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! Would you mind renaming it _skipDefaults
? SD
didn't ring a 🔔 for me at first sight, so I suspect it may confuse other devs.
|
||
function countCalls(counts) { | ||
var callsFinal = Lib.extendFlat({}, counts); | ||
// TODO: do we really need to do layoutStyles twice in Plotly.plot? |
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 hope not 😛
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 this is about automargins - right now the drawing pipeline is like:
- draw everything that can alter margins
- check if anything is too big and needs to change margins
- draw that stuff again if the margins changed
- then draw all the stuff that can't change the margins
we really should refactor it to something like:
- calculate the sizes of everything that can alter margins, without actually drawing it
- calculate all the automargins (which determines the positions of everything)
- draw everything
or perhaps:
- draw everything that can alter margins, but don't position it
- calculate all the automargins
- position the predrawn items
- draw everything else
countCalls({plot: 1}); | ||
|
||
// ideally we'd just do this with `surface` but colorbar attrs have editType 'calc' there | ||
// TODO: can we drop them to type: 'colorbars' even for the 3D types? |
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, this should work with editType: 'colorbars'
.
@@ -20,7 +20,7 @@ module.exports = { | |||
valType: 'boolean', | |||
role: 'info', | |||
dflt: true, | |||
editType: 'calcIfAutorange', | |||
editType: 'calcIfAutorange+arraydraw', |
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.
Can you clarify why we now need arraydraw
here and in shapes/attributes.js
?
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.
relayout
uses containerArrayMatch
to figure out what edits need to be handled with the special add/remove/edit syntax, and arraydraw
was just a placeholder for "relayout will handle this, don't worry about it".
Here we're letting the user do their own editing, so there's much less of that complexity. So now I use editType: 'arraydraw'
as the signal to try and draw components using their draw
or drawOne
component methods, hence it has to actually be there!
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 clear. Thanks 📚
@@ -58,6 +56,8 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { | |||
|
|||
if(showLegend === false) return; | |||
|
|||
layoutOut.legend = containerOut; |
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 nice catch.
until we change that (v2?)
I'd vote +1 here.
if(restyleFlags.calc || relayoutFlags.calc) gd.calcdata = undefined; | ||
|
||
// Note: what restyle/relayout use impliedEdits and clearAxisTypes for | ||
// must be handled by the user when using 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.
Can you clarify what you mean by: must be handling by user?
if(frames) { | ||
gd._transitionData = {}; | ||
Plots.createTransitionData(gd); | ||
seq.push(addFrames); |
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.
Looks good to me 👌
'incremental change.', | ||
'If NOT provided, `Plotly.react` assumes that data arrays are', | ||
'being treated as immutable, thus any data array with a', | ||
'different identity from its predecessor contains new data.' |
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 is brilliant ❇️
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.
yes, yes, yes!
if((!fullLayout._has('cartesian') && !fullLayout._has('gl2d')) || gd._context.staticPlot) return; | ||
if(gd._context.staticPlot) { | ||
// this sweeps up more than just cartesian drag elements... | ||
d3.select(gd).selectAll('.drag').remove(); |
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 about other svg-based subplot types? For example, I'm thinking we could add a similar .remove()
statement for polar here
@@ -30,7 +32,7 @@ module.exports = function pushUnique(array, item) { | |||
} | |||
array.push(item); | |||
} | |||
else if(item && array.indexOf(item) === -1) array.push(item); | |||
else if((item || item === 0) && array.indexOf(item) === -1) array.push(item); |
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 oops, that's done already. Thanks!
I don't know enough about the implementation at this point to comment on that, but the approach and API look pretty awesome from the point of view of |
width and height looked to Plotly.react like attributes -> _width, _height
so as to not muck up plotly.react immutable logic
instead just record the length as fullTrace._length
This is really great, the design with A couple questions:
Would that work? If so, then Dash users could potentially omit their data from even being transported if it remained the same.
|
the range_slider_multiple mock showed the first bug, but I updated it so neither bound matches the default [-1, 6] second bug was that impliedEdits were missing so some range updates wouldn't work
NaN !== NaN wat was breaking Plotly.react.
🤦 transforms...
thank you airfoil.json for including a config arg
Re: image diffs - I can't tell why |
no idea why this changed...
and then npm i using node 8.9.4, npm 5.6.0, so it seems to last...
There are a bunch of extra edge cases here if the x/y/z arrays don't have one of the expected sizes/dimensionalities... but this covers more than it used to anyhow, and gets the ribbons mock right.
coerce('y'); | ||
|
||
traceOut._xlength = (Array.isArray(x) && Array.isArray(x[0])) ? z.length : z[0].length; | ||
traceOut._ylength = z.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.
_xlength
and _ylength
are used in two places, to try and describe how much of each array to use - one in surface/convert
(only for the surface
type) and again in gl3d/scene
(for all 3d types). The two do things in different ways, and especially since surface can use x/y arrays of either 1 or 2 dimensions, they weren't getting things right. I was trying to do better at handling mismatched arrays but this at least gets it right again for arrays of the correct length (and brings back the original gl3d_ribbons
mock), and I've added an item for surface to #2353 to explore the mismatched cases more fully.
Awesome stuff 💃 Follow and/or comment on #2353 for more on this topic. |
Sounds dangerous to me. In some cases this would work, if you insert empty arrays for the data (if you don't even do that, we'll see no array vs used-to-be-an-array and do a recalc regardless of
Yes, as mentioned in the opening comment:
|
cc @VeraZab @nicolaskruchten this will part of |
Introduces
Plotly.react
- an idempotent, full-state, self-diffing update method with a signature matchingPlotly.newPlot
but using the faster update pathways ofPlotly.restyle/relayout/update
.Diffing is done on
fullData
andfullLayout
, not ondata
andlayout
, for a few reasons:data
andlayout
, so there's nothing to diff there if the user modifies them in place. But_full*
get regenerated as new objects with everysupplyDefaults
call (which must happen on every update regardless).supplyDefaults
cleans that up. Also we won't diff user attributes that don't go into the plot, or irrelevant leftovers of old plot types.We do NOT dive into data arrays (
valType: 'data_array'
orarrayOk: true
) in the diffing algorithm. There are two ways to tellPlotly.react
that the data have changed:layout.datarevision
: can take any value - string, number, whatever - and if it is not===
its previous value, we treat the data as changed. Data arrays themselves are ignored during diffing. Note that right now there's no difference in the pathway we'd take for one vs all data arrays changing, but in the future if we implement partial recalc we can make a similar flag for each array, or perhaps each trace.datarevision
, we assume data arrays are being used as immutable and compare them with===
to determine if anything changed. This has some known issues (resulting in slow recalc when it wouldn't be needed) that we should fix:===
each other. We should provide these later in the pipeline, not attached to_fullData
, or fill them in as_private
attributes which do not get diffed._length
or something instead.see #1850
cc @etpinard @chriddyp @nicolaskruchten @VeraZab @jackparmer