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

a version of .newPlot that does the .update logic behind the scenes #1850

Closed
chriddyp opened this issue Jul 4, 2017 · 11 comments
Closed

a version of .newPlot that does the .update logic behind the scenes #1850

chriddyp opened this issue Jul 4, 2017 · 11 comments
Labels
feature something new
Milestone

Comments

@chriddyp
Copy link
Member

chriddyp commented Jul 4, 2017

In Dash, I'd like the Graph component to do smart restyle and relayout updates when I call newPlot or plot (or some new method). Dash users deal with figures and not diffs.

Programming this logic myself is a lot of work that other groups could benefit from if it was done centrally in plotly.js. I wrote a lot of the logic in the workspace and it took a while to work through all the kinks. Some of the edge cases included:

  • calling newPlot instead of update at the start
  • calling addTraces manually instead of update
  • calling newPlot instead of update when switching between 2D, 3D, or ternary
  • calling newPlot when switching to a financial chart type
  • deleting cartesian axes when swithing to ternary or mapbox

Here is that code

This would form the basis of a plotly.js React component.

Dash users will benefit from this immediately with:

  • Faster updates enabling their apps to refresh every 50ms and mimic live-streaming
  • Mapbox updates with full map redraws

Ideally, this logic would work its way into a robust .animate method as well (.animate method that can handle any type of figure diff #1849)


cc @rreusser @alexcjohnson @etpinard @cpsievert @monfera @jackparmer

@chriddyp chriddyp added this to the Dash milestone Jul 4, 2017
@etpinard
Copy link
Contributor

etpinard commented Jul 4, 2017

  • calling newPlot instead of update when switching between 2D, 3D, or ternary
  • calling newPlot when switching to a financial chart type
  • deleting cartesian axes when swithing to ternary or mapbox

Those are arguably update bugs.

@etpinard
Copy link
Contributor

etpinard commented Jul 4, 2017

I'd vote for adding a new Plotly method (roll the 🥁 ) Plotly.react(gd, fig).

@monfera
Copy link
Contributor

monfera commented Jul 4, 2017

Arriving at the delta spec between two subsequent specs sounds similar to a DOM diffing algo. With Étienne's Plotly.react it'd be black box to Dash / Python but internally, at least from some point onward, it'd be good to handle updates incrementally. The current Plotly.restyle calls occupy the middle ground, not granular enough for the fastest updates and not aggregate enough to allow full restatement of a plot.

@rreusser
Copy link
Contributor

rreusser commented Aug 23, 2017

This has been bugging me a bit, but the one problem I do see with this is that there's not currently a internal/virtual state to diff against. That is, gd.data is the state so that if you change your original inpt data and pass it to Plotly.react again, you can update the plot, but you're not really diffing anything at all.

One alternative is to allow passing the diffs themselves (like what the workspace does—specific attrs to update). The downside here is that you're just doing react's job for it, which does help you integrate plotly into a react app, but it's a bit pointless and awkward, declaratively speaking.

In order to really say it's a properly reactified, we'd have to duplicate the state internally. That is, we'd clone gd.data so that the copy Plotly works with is not the same that you passed in. This has the advantage that if you pass in a modified data (which at the moment would also identically be gd.data which is why it's weird now), then it can properly diff it and perform only the necessary updates. It would also mean plotly would never mutate your input data, which would be nice.

The downside is that it requires a full copy of the data (with the possible exception of data arrays which could potentially be aliased).

The most natural way to accomplish this would be to simply always clone the input when you plot. Perhaps Plotly.react could do this cloning and handle calling Plotly.newPlot on the initial plot. In fact, this approach would basically mean that when you use use Plotly.react, it'd be the only API command you'd interface with.

The alternative is that the react component could manage the cloned data.

/cc @etpinard @alexcjohnson @bpostlethwaite

@monfera
Copy link
Contributor

monfera commented Aug 23, 2017

Feel free to skim as I'm not sure if it's relevant here, but in addition to the API perimeter (input data in long arrays) there are also lots of calculations that happen inside the plots and traces, and some kind of diffing would be interesting there as well, for example, new data that breaches current axis domains may need to trigger axis tick etc. recalc, but if new data is within current bounds, it doesn't. Wouldn't the react / diffing metaphor break down because:

  • plotly.js plots have view (DOM scene graph) and model aspects intermingled,
  • data arrays can be very large, even when resulting DOM elements are few (eg. consolidated into bins, box plots, heat maps)

which could maybe tackled by separating the API to a purely view layer (e.g. you specify the boxplot quantiles) and a purely model layer (e.g. calculating quartiles, doing the binning).

The model layer could be plain JS like right now (serviceable!), or could use some reactive concept, MobX, most.js or our crosslink to avoid unneeded recalculations. The view layer would rely on structures that, in cardinality (number of data points / objects) would correspond to the DOM scenegraph structures such as markers.

@monfera
Copy link
Contributor

monfera commented Aug 23, 2017

...before I forget, Mike Bostock's heavy use of functional components is somewhere in between full batch recalculation that can be achieved by plain JS (imperatively or via functional programming), and the use of a reactive library. The setter methods can be written such a way that other inputs are considered constant, ie. some manual code can perform a more efficient update. Internally the functional component can cache (eagerly or lazily) things however it fits best.

@chriddyp
Copy link
Member Author

but the one problem I do see with this is that there's not currently a internal/virtual state to diff against. That is, gd.data is the state so that if you change your original inpt data and pass it to Plotly.react again, you can update the plot, but you're not really diffing anything at all.

@rreusser - This is assuming that the user is modifying their data and layout in-place rather than creating new data and layout instances, right?

In other words, this is assuming the user is doing something like:

data = [{'x': [1, 2, 3]}]
Plotly.react(data)
data[0]['x'][1] = 10
Plotly.react(data)

And so therefore gd.data[0]['x'] == data[0]['x'] even though gd.data[0]['x'][1] !== data[0]['x'][1]. Am I understanding this correctly?

In my case (Dash), we're creating entirely new data and layout instances. It's more like:

data = [{'x': [1, 2, 3]}]
Plotly.react(data)
data = [{'x': [1, 10, 3]}]
Plotly.react(data)

In which case, the objects will always be different (gd.data !== data) but the values and the keys may be the same. And so the diff would be constructed by comparing each of the values.

@rreusser
Copy link
Contributor

rreusser commented Aug 23, 2017

@chriddyp that's correct. The loophole is this case:

data = [{'x': [1, 2, 3]}]
Plotly.react(data)
data[0].marker = {color: 'blue'};
data = [{'x': [1, 10, 3]}]
Plotly.react(data)

It's not perfectly clear to me whether the marker will end up blue or not. Perhaps you shouldn't do this, but this approach doesn't prevent you from being able to do that. That's why I thought perhaps Plotly.react should never pass the input object to plotly itself (i.e. gd.data). That would have the downside of potentially needing to clone all the input data as well. (That's where I said we could maybe skip data_array cloning and just duplicate references, but then you'd still be able to mutate data after the fact.)

@rreusser
Copy link
Contributor

Side note: lots of people (mis?)use the plotly API this way:

data = [{'x': [1, 2, 3]}]
Plotly.newPlot(gd, data)
data[0].marker = {color: 'blue'};
Plotly.redraw(gd)

@alexcjohnson
Copy link
Collaborator

An alternative that has some interesting (good and bad) side effects: diff fullData and fullLayout, which we already clone all the time. We'd still need to figure out something to do with data arrays which we don't clone...

@alexcjohnson
Copy link
Collaborator

Closed by #2341

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

No branches or pull requests

5 participants