Dash plot figure performance
When plotting a lot of objects (e.g rectangles, lines) to a figure one by one using the add_trace, add_shape,etc. methods, it tends to be very slow, probably because it sends the plot request to the client side (browser on the user side) one by one.
However if preparing all the plot objects into a dictionary, and pass on the full dictionary of objects to a figure layout to plot, it seems to have some optimization behind the scene, probably via sending all the objects in one go to the client side, so it is much faster.
For plotting 200 rectangles / lines, the plot time is about 17seconds vs 0.5 second.
Here is the script. The two buttons plot the objects one by one, or all in one go.
import dash
from dash import dcc, html, ctx
import plotly.graph_objects as go
import numpy as np
from dash.dependencies import Input, Output
import pandas as pd
app = dash.Dash(__name__)
def draw_one_by_one():
num_shapes = 200
fig = go.Figure()
# Set up axes and layout first
fig.update_layout(
xaxis=dict(range=[0, 11], autorange=False),
yaxis=dict(range=[0, 11], autorange=False),
height=600,
width=600,
title="Complex Graph with Rectangles and Lines (Added One-by-One)"
)
# Add rectangles and lines one by one using add_shape
for i in range(num_shapes):
x0, y0 = np.random.rand(2) * 10
width, height = np.random.rand(2) * 0.5 + 0.1
fig.add_shape(
type="rect",
x0=x0,
y0=y0,
x1=x0 + width,
y1=y0 + height,
line=dict(color="RoyalBlue"),
fillcolor="LightSkyBlue",
opacity=0.5,
)
# Line
x_start, y_start = np.random.rand(2) * 10
x_end, y_end = x_start + np.random.rand(), y_start + np.random.rand()
fig.add_shape(
type="line",
x0=x_start,
y0=y_start,
x1=x_end,
y1=y_end,
line=dict(color="Red", width=2),
)
return fig
def draw_all():
num_shapes = 200
rectangles = []
lines = []
for i in range(num_shapes):
x0, y0 = np.random.rand(2) * 10
width, height = np.random.rand(2) * 0.5 + 0.1
rectangles.append(
dict(
type="rect",
x0=x0,
y0=y0,
x1=x0 + width,
y1=y0 + height,
line=dict(color="RoyalBlue"),
fillcolor="LightSkyBlue",
opacity=0.5,
)
)
x_start, y_start = np.random.rand(2) * 10
x_end, y_end = x_start + np.random.rand(), y_start + np.random.rand()
lines.append(
dict(
type="line",
x0=x_start,
y0=y_start,
x1=x_end,
y1=y_end,
line=dict(color="Red", width=2),
)
)
annotation = dict(
x=1,
y=2,
text="My Annotation",
showarrow=False,
font=dict(size=12, color="black"),
xanchor="center",
yanchor="middle"
)
fig = go.Figure()
# Add shapes (rectangles + lines)
# Add annotations separately as another parameter
fig.update_layout(
shapes=rectangles + lines,
annotations = [annotation],
xaxis=dict(range=[0, 11], autorange=False),
yaxis=dict(range=[0, 11], autorange=False),
height=600,
width=600,
title="Complex Graph with Rectangles and Lines",
)
return fig
app.layout = html.Div([
html.Button(id='btn1', children='draw one by one'),
html.Button(id='btn2', children='draw all'),
dcc.Graph(id="complex-graph", figure=None)
])
@app.callback(Output('complex-graph', 'figure'), Input("btn1", "n_clicks"), Input("btn2", "n_clicks"))
def draw(n1, n2):
st = pd.Timestamp.now()
if ctx.triggered_id == 'btn1':
fig = draw_one_by_one()
else:
fig = draw_all()
print(f'duration is {(pd.Timestamp.now() - st).total_seconds()} seconds')
return fig
if __name__ == '__main__':
app.run(debug=True)