I have a bunch of Plotly plots and subplots (about 40 in total) that I need to update when the user clicks a button. Specifically, there’s one trace where I need to add a point every time the button is clicked, and this trace needs to be updated on all plots at the same time (same trace, replicated on all plots).
But this is very slow: about 15 seconds. It’s slower than loading the Panel from scratch, and much slower than refreshing it once loaded (when I save some changes to the Python script). Yet I’m doing this in a way that seems like it should be efficient: I keep a list of that trace’s points in memory, update the list once when the button is clicked, convert that list to a tuple once, and then replace the existing tuple for that specific trace on every plot with the new one.
But it takes a very long time, and I can clearly see each plot updating one after the other. This leads me to believe that perhaps when I update a given trace, Panel (or Plotly?) reloads/rebuilds/refreshes a bunch of stuff in the background, and that I’m paying this overhead 40 times instead of just once.
If this guess is correct, how I can stop this needless refreshing? Or am I just trying to be too clever for my own good here, and should I just somehow destroy the figures and rebuild them all from scratch? That doesn’t seem optimal, but perhaps it is.
Could you try posting a minmum, reproducible example? There could be so many things causing this, that without some code its almost impossible to identifify the cause.
The following bit of code is about as minimal as I can make it, I think. The update_meta_fig() function is the one that results in very slow behavior, the make_meta_fig() one just creates the subplots, and actually runs pretty fast, even though it seems like it should be doing a lot more work.
from functools import partial
import panel as pn
import plotly.graph_objects as go
from plotly.subplots import make_subplots
pn.extension('plotly')
def make_meta_fig(mandated_width, nb_points):
nb_figures = 40
meta_fig = make_subplots(cols=1, rows=nb_figures)
# Initially empty trace to be updated by the user later on
xSeries = list(range(nb_points))
ySeries = nb_points * [None]
events_trace = go.Scatter(x = xSeries, y = ySeries, name="registered events", mode="markers")
# Traces with fixed data
for i in range(nb_figures):
xSeries = list(range(nb_points))
ySeries = [i*x for x in xSeries]
my_trace = go.Scatter(x = xSeries, y = ySeries, name="fixed_data")
meta_fig.add_trace(my_trace, col=1, row=i+1)
meta_fig.add_trace(events_trace, col=1, row=i+1)
meta_fig.update_layout(height = nb_figures * 100, width = mandated_width)
return(meta_fig)
def update_meta_fig(meta_fig, nb_figures, nb_points, event):
list_trace = nb_points * [None]
list_trace[42] = 84
new_trace = tuple(list_trace)
for trace in meta_fig["data"]:
if trace["name"] == "registered events":
trace["y"] = new_trace
def main():
nb_figures = 40
pn.extension()
gb = pn.GridBox(ncols = 1)
FIXED_WIDTH = 600
nb_points = 500
meta_fig = make_meta_fig(FIXED_WIDTH, nb_points)
plot_panel = pn.pane.Plotly(meta_fig, width=FIXED_WIDTH)
gb.append(plot_panel)
validationButton = pn.widgets.Button(name='Validate', button_type='primary')
validationButton.on_click(partial(update_meta_fig, meta_fig, nb_figures, nb_points))
gb.append(validationButton)
return(gb)
if __name__.startswith("bokeh"):
dashboard = main()
dashboard.servable()
Looking at the code I see you update the meta_fig Plotly Figure object in place when the button is clicked. I actually don’t understand why this updates the plot. I would have expected you need to update the plot_panel.object to get it to update.
What you can do is instead take a copy of the existing object go.Figure(meta_fig), update it in place and then update the plot_panel.object.
It updates in ~1½ seconds for me.
from functools import partial
import panel as pn
import plotly.graph_objects as go
from plotly.subplots import make_subplots
pn.extension('plotly')
FIXED_WIDTH = 600
def make_meta_fig(mandated_width, nb_points):
nb_figures = 40
meta_fig = make_subplots(cols=1, rows=nb_figures)
# Initially empty trace to be updated by the user later on
xSeries = list(range(nb_points))
ySeries = nb_points * [None]
events_trace = go.Scatter(x = xSeries, y = ySeries, name="registered events", mode="markers")
# Traces with fixed data
for i in range(nb_figures):
xSeries = list(range(nb_points))
ySeries = [i*x for x in xSeries]
my_trace = go.Scatter(x = xSeries, y = ySeries, name="fixed_data")
meta_fig.add_trace(my_trace, col=1, row=i+1)
meta_fig.add_trace(events_trace, col=1, row=i+1)
meta_fig.update_layout(height = nb_figures * 100, width = mandated_width)
return meta_fig
def update_meta_fig(meta_fig, nb_figures, nb_points):
meta_fig = go.Figure(meta_fig)
list_trace = nb_points * [None]
list_trace[42] = 84
new_trace = tuple(list_trace)
for trace in meta_fig["data"]:
if trace["name"] == "registered events":
trace["y"] = new_trace
return meta_fig
def main():
nb_figures = 40
pn.extension()
gb = pn.GridBox(ncols = 1)
nb_points = 500
meta_fig = make_meta_fig(FIXED_WIDTH, nb_points)
plot_panel = pn.pane.Plotly(meta_fig, width=FIXED_WIDTH)
gb.append(plot_panel)
validationButton = pn.widgets.Button(name='Validate', button_type='primary')
@pn.depends(validationButton, watch=True)
def update(event):
plot_panel.object = update_meta_fig(meta_fig, nb_figures, nb_points)
gb.append(validationButton)
return(gb)
if __name__.startswith("bokeh"):
dashboard = main()
dashboard.servable()
Thank you very much, Marc, that is indeed much faster, including on my actual code in addition to the minimal example provided here. If I understand the code you referenced in the GitHub issue correctly, any change to a figure will send a message that Panel will interpret as a signal to refresh the pane, right?