Very laggy plot when update_plot callback includes trace colour, marker, line, and size

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)

Fixed by using go.Scattergl