I’m putting together a data visualisation tool to view datasets with 10-50000 data points. My test dataset has 30 distinct subdatasets, each of which has 4 traces of 20000 points (~40 Mb text file). I choose a subdataset and plot the traces.
I have an issue when trying to incorporate customisable trace colour, marker/line styles, and size; when I include these in my update_plot callback, the app becomes unusably laggy. I am running this locally. It takes about 8 s to see the first plot, and any update to the trace marker/colour/line takes about 3 s to take effect. Zooming is very slow. Using the callback visual investigating thing in debug mode shows that the callbacks take ~250 ms, much shorter than the actual wait time.
If I don’t have those things in my update_plot callback, then everything is snappy and responsive. I don’t alter how I choose the data to plot; it’s just the styling of it.
Is it possible to separate out the styling of each trace to a different callback? Everything I’ve looked at seems to say that I need to return a complete figure from the callback.
My code is below. I’ve condensed it as much as I can do, whilst maintaining the essence of what is going on. As I’m reusuing several components, their layouts are stored in functions that I call several times, just to cut down on code repetition.
import dash
from dash import dcc
from dash import html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State, MATCH
import dash_daq as daq
import plotly.graph_objs as go
import pandas as pd
import numpy as np
# Read the data. Eventually, this will be uploaded by the user
df = pd.read_table("data.txt")
# what data is available to plot? There are many datasets in the df
data_list = [{"label": str(val), "value": str(val)} for val in df["_id"].unique()]
# make lists to use in the dropdowns to choose the X and Y ordinates to plot - these are the only possible values
x_list = [{"label": "x1", "value": "x1"}, {"label": "x2", "value": "x2"}]
y_list = [{"label": "y1", "value": "y1"}, {"label": "y2", "value": "y2"}, {"label": "y3", "value": "y3"}]
# I made a function to create the dropdown things, as it makes the layout clearer to read
def data_chooser(label, idn, options, value):
return html.Div([
html.Div(label, style={'display': 'inline-block', 'vertical-align': 'middle', "width": "20%"}),
html.Div(dcc.Dropdown(id=idn, options=options, value=value),
style={'display': 'inline-block', 'vertical-align': 'middle', "width": "80%"}),
])
# Probably need to refactor this to give it the same name as data_chooser, or do both to give a common func name...
def x_ordinate_chooser(label, idn, options, value):
return data_chooser(label, idn, options, value)
# This I haven't shortened, as it is a key thing in what is making things slow.
# This is used to style the traces: colour, line/marker, and size.
def y_ordinate_modal(idn, header, colour_default, lm_default, marker_default, line_default, size_default):
return dbc.Modal(
[
dbc.ModalHeader(header),
dbc.ModalBody([html.Div([ # left div
daq.ColorPicker(
id={"type": "modal-colour-picker", "id": idn},
label='Color Picker',
value=colour_default
)
], style={"width": "50%", 'display': 'inline-block', "align": "top"}),
html.Div([ # right div
html.Div(["Lines/markers"]),
html.Div([dcc.Dropdown(id={"type": "modal-linesmarkers-dropdown", "id": idn},
options=[{"label": "Lines", "value": 'lines'},
{"label": "Markers", "value": "markers"},
{"label": "Lines & markers", "value": "lines+markers"}],
value=lm_default
)]),
html.Div(["Marker style"]),
html.Div([dcc.Dropdown(id={"type": "modal-markerstyle-dropdown", "id": idn},
options=[{"label": "circle", "value": "circle"},
{"label": "diamond", "value": "diamond"},
{"label": "cross", "value": "cross"}],
value=marker_default
)]),
html.Div(["Line style"]),
html.Div([dcc.Dropdown(id={"type": "modal-linestyle-dropdown", "id": idn},
options=[{"label": "Solid", "value": 'solid'},
{"label": "Dot", "value": "dot"},
{"label": "Dash", "value": "dash"}],
value=line_default
)]),
html.Div(["Size"]), # does both line and marker
html.Div([dcc.Dropdown(id={"type": "modal-size-dropdown", "id": idn},
options=[{"label": str(size), "value": size} for size in range(1, 30)],
value=size_default
)]),
], style={"width": "48%", 'display': 'inline-block', "align": "top", "padding-left": "2%"})
]),
dbc.ModalFooter(dbc.Button("Close", id={"type": "close-modal", "id": idn}, n_clicks=0)),
],
id={"type": "modal", "id": idn}, is_open=False
)
# This function gets used several times to add many traces to the plot.
# Each trace would have between 1000 and 50000 points
def y_ordinate_chooser(label, idn, options, value,
colour_default, lm_default, marker_default, line_default, size_default):
return html.Div([
html.Div(label, style={'display': 'inline-block', 'vertical-align': 'middle', "width": "20%"}),
html.Div(dcc.Dropdown(id=idn, options=options, value=value),
style={'display': 'inline-block', 'vertical-align': 'middle', "width": "55%"}),
html.Div([dbc.Button("Options", id={"type": "options", "id": idn}, n_clicks=0),
y_ordinate_modal(idn, label, colour_default, lm_default, marker_default, line_default, size_default)],
style={'display': 'inline-block', 'vertical-align': 'middle', "width": "25%"}),
])
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.CERULEAN])
app.layout = html.Div(children= # main div
[
html.Div(children= # left div
[
html.Div(children=[dcc.Graph("plot")],
style={'width': "75%", 'display': 'inline-block', 'vertical-align': 'top'}),
html.Div(children= # right div
[
html.Div(children=[data_chooser("Data", 'data-chooser-dropdown', data_list, data_list[0]["value"])],
style={'height': "10vh", 'vertical-align': 'middle'}),
html.Div(children= # plot controls
[
x_ordinate_chooser("X axis", "x-chooser-dropdown", x_list, x_list[0]["value"]),
y_ordinate_chooser("Y axis 1", "y-chooser-dropdown-1", y_list, y_list[0]["value"],
{"hex": '#0000FF'}, "lines+markers", "circle", "solid", 6),
y_ordinate_chooser("Y axis 2", "y-chooser-dropdown-2", y_list, y_list[1]["value"],
{"hex": '#00FF00'}, "lines+markers", "square", "dash", 6),
],
)
],
style={'width': "25%", 'display': 'inline-block', 'vertical-align': 'top'} # right div
),
],
style={'width': "100%", 'height': "100vh"} # main div
)
])
# Which callback do you want to run? Slow or not slow?
slow = 0
if slow:
# this is the slow one. It allows for changing the marker colours and the like, but is unusably slow
@app.callback(Output("plot", component_property="figure"),
[Input("data-chooser-dropdown", component_property="value"),
Input("x-chooser-dropdown", component_property="value"),
Input("y-chooser-dropdown-1", component_property="value"),
Input("y-chooser-dropdown-2", component_property="value"),
Input({"type": "modal-colour-picker", "id": "y-chooser-dropdown-1"}, component_property="value"),
Input({"type": "modal-linesmarkers-dropdown", "id": "y-chooser-dropdown-1"},
component_property="value"),
Input({"type": "modal-markerstyle-dropdown", "id": "y-chooser-dropdown-1"},
component_property="value"),
Input({"type": "modal-linestyle-dropdown", "id": "y-chooser-dropdown-1"},
component_property="value"),
Input({"type": "modal-size-dropdown", "id": "y-chooser-dropdown-1"}, component_property="value"),
Input({"type": "modal-colour-picker", "id": "y-chooser-dropdown-2"}, component_property="value"),
Input({"type": "modal-linesmarkers-dropdown", "id": "y-chooser-dropdown-2"},
component_property="value"),
Input({"type": "modal-markerstyle-dropdown", "id": "y-chooser-dropdown-2"},
component_property="value"),
Input({"type": "modal-linestyle-dropdown", "id": "y-chooser-dropdown-2"},
component_property="value"),
Input({"type": "modal-size-dropdown", "id": "y-chooser-dropdown-2"}, component_property="value"),
])
def update_plot_traces(data, x_ordinate, y1_ordinate, y2_ordinate,
y1_colour, y1_linesmarkers, y1_markerstyle, y1_linestyle, y1_size,
y2_colour, y2_linesmarkers, y2_markerstyle, y2_linestyle, y2_size):
filtered_df = df[df["_id"] == data]
x = filtered_df[x_ordinate]
# a little hacky, but allows me to clear y dropdowns and get them properly removed from the plot
traces = []
try:
traces.append(go.Scatter(x=x,
y=filtered_df[y1_ordinate],
name=y1_ordinate,
meta="y1",
mode=y1_linesmarkers,
line={"dash": y1_linestyle, "color": y1_colour["hex"], "width": y1_size / 3},
marker={"symbol": y1_markerstyle, "color": y1_colour["hex"], "size": y1_size}
)
)
except KeyError:
pass
try:
traces.append(go.Scatter(x=x,
y=filtered_df[y2_ordinate],
name=y2_ordinate,
meta="y2",
mode=y2_linesmarkers,
line={"dash": y2_linestyle, "color": y2_colour["hex"], "width": y2_size / 3},
marker={"symbol": y2_markerstyle, "color": y2_colour["hex"], "size": y2_size}
)
)
except KeyError:
pass
layout = go.Layout(title={"text": data}, xaxis={"title": "x axis"}, yaxis={"title": "y axis"})
return {"data": traces, "layout": layout}
else:
# this is the fast version - but it doesn't allow changing of the trace colours, markers...
@app.callback(Output("plot", component_property="figure"),
[Input("data-chooser-dropdown", component_property="value"),
Input("x-chooser-dropdown", component_property="value"),
Input("y-chooser-dropdown-1", component_property="value"),
Input("y-chooser-dropdown-2", component_property="value")
])
def update_plot_traces(data, x_ordinate, y1_ordinate, y2_ordinate):
filtered_df = df[df["_id"] == data]
x = filtered_df[x_ordinate]
# a little hacky, but allows me to clear y dropdowns and get them properly removed from the plot
traces = []
try:
traces.append(go.Scatter(x=x, y=filtered_df[y1_ordinate], name=y1_ordinate, meta="y1"))
except KeyError:
pass
try:
traces.append(go.Scatter(x=x, y=filtered_df[y2_ordinate], name=y2_ordinate, meta="y2"))
except KeyError:
pass
layout = go.Layout(title={"text": data}, xaxis={"title": "x axis"}, yaxis={"title": "y axis"})
return {"data": traces, "layout": layout}
# this is the callback to open and close the modal windows for the trace colouring and styling.
@app.callback(
Output({"type": "modal", "id": MATCH}, "is_open"),
[Input({"type": "options", "id": MATCH}, "n_clicks"),
Input({"type": "close-modal", "id": MATCH}, "n_clicks")],
[State({"type": "modal", "id": MATCH}, "is_open")]
)
def toggle_modal(n1, n2, is_open):
if n1 or n2:
return not is_open
return is_open
if __name__ == "__main__":
app.run_server(debug=True, port=8050)