Webgl context lost after updating 10+ times

For example if I have two webgl-based figures (say scatter3d or scattergl). If I use interactive features of Dash to update one of the figure, eventually after updating the figure 10+ times, the other figure (that was not updated) will be lost and displays nothing.

I understand how this happens. Since browser has a upper limit of number of webgl contexts and Dash creates a new webgl context everytime the plot is updated, the oldest webgl context will eventually be dropped.

Currently my workaround is redrawing all the webgl figures even if only one of them get updated. This is obviously non-optimal so I am wondering whether Dash can handle this in a better way (no expert on this, could figures retain the old webgl context when updated? Or could Dash handles redrawing of a figure when it gets lost?).

1 Like

Thanks for reporting and triaging! Yes, Dash should handle this better. I’ll loop in with some other engineers on this and update this thread with progress.

2 Likes

@jzh - I’m having some trouble reproducing this. I created a simple JS example here: https://codepen.io/chriddyp/pen/xYmbVQ?editors=1010 that continuously replots using scatter gl. Although there are a couple of console warnings, the code does continue to replot.

Could you help me recreate the bug? If you don’t know JS, then a small, simple, reproducable Dash example in Python would be really helpful.

This is a Dash example that can reproduce it (use scrollbar a few times to update the bottom three plots - the top one will Disappear). I used three plots to make it appear faster but replotting one is enough to make it happen.

# -*- coding: utf-8 -*-
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import numpy as np
import plotly.graph_objs as go

app = dash.Dash(__name__)
server = app.server

x, y, z = np.random.multivariate_normal(
    np.array([0, 0, 0]), np.eye(3), 2).transpose()

app.layout = html.Div([
        dcc.Slider(
            id='slider',
            min=0,
            max=20,
            value=0,
            step=None,
            marks={str(n): str(n) for n in range(20)},
        ),
        dcc.Graph(
            id='scatter_3d_0',
            figure={
                'data': [
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ],
            }
        ),
        dcc.Graph(
            id='scatter_3d_1',
            figure={
                'data': [
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ],
            }
        ),
        dcc.Graph(
            id='scatter_3d_1',
            figure={
                'data': [
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ],
            }
        ),
        dcc.Graph(
            id='scatter_3d_2',
            figure={
                'data': [
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ],
            }
        ),
        dcc.Graph(
            id='scatter_3d_3',
            figure={
                'data': [
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ],
            }
        )
])



@app.callback(
    Output('scatter_3d_1', 'figure'),
    [ Input('slider', 'value')],
    [State('scatter_3d_1', 'figure')])

def display_scatter_3d_1(value,figure):
    x, y, z = np.random.multivariate_normal(
     np.array([0, 0, 0]), np.eye(3), 2).transpose()
    figure['data']=[
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ]
    return figure


@app.callback(
    Output('scatter_3d_2', 'figure'),
    [ Input('slider', 'value')],
    [State('scatter_3d_2', 'figure')])

def display_scatter_3d_2(value,figure):
    x, y, z = np.random.multivariate_normal(
     np.array([0, 0, 0]), np.eye(3), 2).transpose()
    figure['data']=[
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ]
    return figure


@app.callback(
    Output('scatter_3d_3', 'figure'),
    [ Input('slider', 'value')],
    [State('scatter_3d_3', 'figure')])

def display_scatter_3d_3(value,figure):
    x, y, z = np.random.multivariate_normal(
     np.array([0, 0, 0]), np.eye(3), 2).transpose()
    figure['data']=[
                    go.Scatter3d(
                        x=x,
                        y=y,
                        z=z,
                        mode='markers',
                    )
                ]
    return figure



if __name__ == '__main__':
    app.run_server(debug=True)

Thank you! I have reported the issue here: https://github.com/plotly/plotly.js/issues/2423

@jzh - I submitted a fix (https://github.com/plotly/dash-core-components/pull/170) and released a new version. Could you try upgrading and see if the issue is fixed for you? Upgrade with

pip install dash-core-components==0.20.0

Thank you!

1 Like

This has fixed it. Thank you so much!!

2 Likes

@chriddyp maybe I should start a different thread for this? The change to “.react” fixed this problem but also changed some interactions when the plot is updated. Specificly I had some unexpected result when I used it with a callback that update the plot to highlight selected data. The built-in selection effect (highlight selected points and fade unselected points) would not show in the old version probably because a new plot is made with the callback. For the new version, however, it seems like the selection effect overlays with the updated plot generated by the callback, resulting in only a seemingly random subset of points being highlighted (the selecteddata is updated correctly but the visualization looks wrong).

This is the old dash-core-components (0.18.1) interaction
old

This is the new dash-core-components (0.20.1) interaction
old

This should be reproducible with https://github.com/plotly/dash-recipes/blob/master/dash_crossfilter.py
by changing scatter to scattergl too.

Thanks for reporting again @jzh! I’ve reported the issue in plotly.js here: https://github.com/plotly/plotly.js/issues/2433. I’ll keep this thread updated with progress.

@jzh - Could you click on the “Edit in chart studio” button in the toolbar, save the graph in a plotly account, and share the link here? That’ll help the plotlyjs engineers reproduce the example

This is a reproducible example in dash - does this works? Not knowing the internals but I guess this is happening because the highlight and drawing of updated graph are executed at the same time, so part of the point are correctly plotted as in updated plot but part of the points are plotted as what the highlighted original plot should look like

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html

import numpy as np
import pandas as pd

app = dash.Dash()

df = pd.DataFrame({
    'Column {}'.format(i): np.random.rand(50) + i*10
    for i in range(6)})

app.layout = html.Div([
    html.Div(
        dcc.Graph(
            id='g1',
            selectedData={'points': [], 'range': None}
        ), className="four columns"
    ),
    html.Div(
        dcc.Graph(
            id='g2',
            selectedData={'points': [], 'range': None}
        ), className="four columns"),
    html.Div(
        dcc.Graph(
            id='g3',
            selectedData={'points': [], 'range': None}
        ), className="four columns")
], className="row")


def highlight(x, y):
    def callback(*selectedDatas):
        index = df.index
        for i, hover_data in enumerate(selectedDatas):
            selected_index = [
                p['customdata'] for p in selectedDatas[i]['points']
                # the first trace that includes all the data
                if p['curveNumber'] == 0
            ]
            if len(selected_index) > 0:
                index = np.intersect1d(index, selected_index)

        dff = df.iloc[index, :]


        figure = {
            'data': [
                dict({
                    'x': df[x], 'y': df[y], 'text': df.index,
                    'customdata': df.index,
                    'mode':'markers',
                    'type': 'scattergl', 'opacity': 0.1
                }),
                dict({
                    'x': dff[x], 'y': dff[y], 'text': dff.index,
                    'mode':'markers',
                    'type': 'scattergl', 'textposition': 'top',
                }),
            ],
            'layout': {
                'margin': {'l': 20, 'r': 0, 'b': 20, 't': 5},
                'dragmode': 'select',
                'hovermode': 'closest',
                'showlegend': False
            }
        }


        return figure

    return callback


app.css.append_css({
    "external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})

# app.callback is a decorator which means that it takes a function
# as its argument.
# highlight is a function "generator": it's a function that returns function
app.callback(
    Output('g1', 'figure'),
    [Input('g1', 'selectedData'),
     Input('g2', 'selectedData'),
     Input('g3', 'selectedData')]
)(highlight('Column 0', 'Column 1'))

app.callback(
    Output('g2', 'figure'),
    [Input('g2', 'selectedData'),
     Input('g1', 'selectedData'),
     Input('g3', 'selectedData')]
)(highlight('Column 2', 'Column 3'))

app.callback(
    Output('g3', 'figure'),
    [Input('g3', 'selectedData'),
     Input('g1', 'selectedData'),
     Input('g2', 'selectedData')]
)(highlight('Column 4', 'Column 5'))

if __name__ == '__main__':
    app.run_server(debug=True)

Another likely related issue with this update to ‘.react()’ is selection (lasso or box selection) now triggers an indefinite number of callbacks instead of just one per selection. This is also causing some performance issues.

Thanks for reporting. Looking into this today.

It looks like it is attaching new event listeners on every plot instead of destroying them between plots. I’ll work on a fix for this particular issue now.

This will be fixed in Remove listeners before reapplying them by chriddyp · Pull Request #172 · plotly/dash-core-components · GitHub.

This issue is being discussed in addTraces upon plotly_selected event with scattergl not rendering correctly · Issue #2298 · plotly/plotly.js · GitHub. It looks like this may be the new behaviour that the plotly.js team wants to settle on but let’s continue that discussion in the issue.

In either case, the new way to achieve this effect is with the selected attribute that styles the selected points (instead of plotting a new trace) and the selectedpoints attribute which specifies which points should be in the “selected” state. So, inside the highlight function, this would look like:

        figure = {
            'data': [
                dict({
                    'x': df[x],
                    'y': df[y],
                    'text': df.index,
                    'selectedpoints': index,
                    'customdata': df.index,
                    'mode':'markers',
                    'type': 'scattergl',
                    'marker': {
                        'color': '#0074D9'
                    },
                    'selected': {
                        'marker': {
                            'color': '#FF851B'
                        },
                    },
                    'unselected': {
                        'marker': {
                            'opacity': 0.3,
                            'color': '#0074D9'
                        }
                    }
                }),
            ],
            'layout': {
                'margin': {'l': 20, 'r': 0, 'b': 20, 't': 5},
                'dragmode': 'select',
                'hovermode': 'closest',
                'showlegend': False
            }
        }

Does this work for your use case or are there still certain behaviours that you are only able to accomplish by using a second trace?

2 Likes

@chriddyp Thanks for the fix for the callbacks! I haven’t tried it but will do when it is released. For selection, this does solve my problem. Thanks!

1 Like

Great! You can find the fix in v0.20.2, upgrade with:

pip install dash-core-components==0.20.2