plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.
https://plotly.com/dash
MIT License
21.44k stars 2.07k forks source link

Restyling clustered points in scatter_mapbox via callback breaks dcc.Graph figure #2521

Open ndymlls opened 1 year ago

ndymlls commented 1 year ago

Applying different colors to clustered points via a callback appears to break the figure and raises the following errors in the browser console:

Example code provided below. To recreate the error, toggle the filter checkbox on and off. Additional clustering checkbox shows that the error does not happen if clustering not enabled.

from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd

print('Mapbox token required')
mapbox_token='xxx'

sitesDF = data = pd.DataFrame({'latitude': [1,1,2,2,1,1,2,2],
                               'longitude': [1,2,1,2,-11,-12,-11,-12],
                               'name':['A','B','C','D','E','F','G','H']})

def create_map(included,excluded,clustering):
    included['color'] = 'included'
    excluded['color'] = 'excluded'
    data = pd.concat([included,excluded])

    color_discrete_map = {'included': 'rgb(64,217,18)','excluded': 'grey'}
    fig = px.scatter_mapbox(data, lat='latitude', lon='longitude', color='color',color_discrete_map=color_discrete_map)
    fig.update_layout(mapbox_style='dark', mapbox_accesstoken=mapbox_token)
    fig.update_layout(mapbox={'zoom': 4})
    fig.update_layout(margin={'r': 0, 't': 0, 'l': 0, 'b': 0})
    fig.update_layout(legend={'yanchor':'top','y':0.99,'xanchor':'left','x':0.01})
    fig.update_traces(cluster_enabled=clustering)
    return fig

app = Dash(__name__)
app.layout = dbc.Container([dcc.Loading(dcc.Graph(id='index-map')),
                       html.Br(),
                       dbc.Checkbox(id='clustering',label='Cluster',value=True),
                       dbc.Checkbox(id='filtering',label='Filter',value=False)])

@app.callback(Output('index-map', 'figure'),
              Input('clustering', 'value'),
              Input('filtering', 'value'))
def render_map_extras(clustering,filtering):
    included = sitesDF.iloc[:4] if filtering else sitesDF
    excluded = sitesDF[~sitesDF['name'].isin(list(included['name']))]
    fig = create_map(included,excluded,clustering)
    return fig

app.run_server(debug=True)

Package versions:

    dash==2.9.3
    dash-bootstrap-components==1.4.1
    dash-core-components==2.0.0
    dash-html-components==2.0.0
    dash-renderer==1.9.1
    dash-table==5.0.0

Bug tested and seen in various browsers in Windows 10.

nickmelnikov82 commented 1 year ago

Hello! Try this updated code that will avoid the error:

from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import pandas as pd

print('Mapbox token required')
mapbox_token = 'xxx'

sitesDF = pd.DataFrame({'latitude': [1, 1, 2, 2, 1, 1, 2, 2],
                        'longitude': [1, 2, 1, 2, -11, -12, -11, -12],
                        'name': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']})

def create_map(included, excluded, clustering):
    included.loc[:, 'color'] = 'included'
    excluded.loc[:, 'color'] = 'excluded'
    data = pd.concat([included, excluded])

    color_discrete_map = {'included': 'rgb(64,217,18)', 'excluded': 'grey'}
    fig = px.scatter_mapbox(data, lat='latitude', lon='longitude', color='color', color_discrete_map=color_discrete_map)
    fig.update_layout(mapbox_style='dark', mapbox_accesstoken=mapbox_token)
    fig.update_layout(mapbox={'zoom': 4})
    fig.update_layout(margin={'r': 0, 't': 0, 'l': 0, 'b': 0})
    fig.update_layout(legend={'yanchor': 'top', 'y': 0.99, 'xanchor': 'left', 'x': 0.01})
    fig.update_traces(cluster_enabled=clustering)
    return fig.to_html(full_html=False)

app = Dash(__name__)
app.layout = dbc.Container([
    html.Iframe(id='index-map', style={'border': 'none', 'width': '100%', 'height': '500px'}),
    html.Br(),
    dbc.Checkbox(id='clustering', label='Cluster', value=True),
    dbc.Checkbox(id='filtering', label='Filter', value=False)
])

@app.callback(Output('index-map', 'srcDoc'),
              Input('clustering', 'value'),
              Input('filtering', 'value'))
def render_map_extras(clustering, filtering):
    included = sitesDF.iloc[:4] if filtering else sitesDF
    excluded = sitesDF[~sitesDF['name'].isin(list(included['name']))]
    map_html = create_map(included, excluded, clustering)
    return map_html

app.run_server(debug=True)

This code uses the fig.to_html(full_html=False) function to get the HTML code of the graph. This code is then passed to the srcDoc attribute of the HTML-IFrame element.

To update the HTML code of the graph, the dcc.Iframe component uses Output('index-map', 'srcDoc').

To create an HTML-IFrame element, use html.Iframe.

To create selection components (checkboxes), use dbc.Checkbox in the markup.

The render_map_extras callback is called when the values of the clustering and filtering checkboxes are changed and returns the HTML code of the graph.

ndymlls commented 1 year ago

Hey Nick. Thank you for your response.

Although your suggestion does fix the example, it doesn't work in my actual app.

The map selection is done by lassoing points on the map which triggers a callback for the restyling of the points. The iframe does not have the required selectedData and relayoutData triggers.

ndymlls commented 7 months ago

I am still hoping to get this bug fixed. Others in the Plotly forum have also raised this.

https://community.plotly.com/t/scattermapbox-cluster-bug-the-layer-does-not-exist-in-the-maps-style/80132

https://community.plotly.com/t/restyling-clustered-points-in-px-scatter-mapbox-via-callback-breaks-dcc-graph-figure/75987

Coding-with-Adam commented 7 months ago

hi @ndymlls I can confirm I'm getting the same error on Windows 11 as well.

mapcluster

ndymlls commented 7 months ago

@Coding-with-Adam, thank you for looking into this. I have learned a little more which I thought I would share and could perhaps help.

I have ended up implementing my own code to produce the clustering effect. In doing so I did encounter similar console errors (even though I was not using the cluster argument). I have built the figure using different traces either styled as clusters or separate points, and again styled either as selected or unselected. I have been able to get the error to reproduce itself if the 'name' of the traces did not match another. I wonder if this is happening within your cluster logic. Having different names resolves the errors seen.

Screenshot 2024-03-28 085532