Problems having scattemapbox react to hover events of other graphs

Hello! So I am trying to accomplish something specific. I have an application with a scattermapbox component and a couple regular scatter components. I want to make it so when the user hovers over a point on the line graph, the corresponding gps marker on the scattermapbox changes color from purple to yellow. The data is based off a time interval selected by the user, and the map is initially populated using this callback.

@app.callback(
    Output('map-figure-store', 'data'),
    [Input('current-time-range-data', 'data')])
def update_map(data):
    if data is None:
      raise PreventUpdate
    reel_motor_currents = data['reel_motor_current']
    traction_motor_currents = data['traction_motor_current']
    reel_motor_speeds = data['reel_motor_speed']
    traction_motor_speeds = data['traction_motor_speed']
    timestamps = data['timestamps']
    latitudes = []
    longitudes = []
    colors = []
    sizes = []
    opacities = []
    for gps_entry in data['gps_data']:
        latitudes.append(gps_entry['lat'])
        longitudes.append(gps_entry['lon'])
        colors.append('rgb(138,43,226)')
        sizes.append(15)
        opacities.append(0.5)
    center_lat = 0
    center_lon = 0
    if len(latitudes) is 0 or len(longitudes) is 0:
      center_lat = 41.2606
      center_lon = -81.90
    else:
      center_lat = latitudes[0]
      center_lon = longitudes[0]
    return {
        'data': [
            go.Scattermapbox(
                lat=latitudes,
                lon=longitudes,
                mode='markers',
                customdata=timestamps,
                marker=go.scattermapbox.Marker(
                    size=sizes, opacity=opacities, color=colors
                ),
                hoverinfo='text',
                hovertext=[('<b>Traction</b><br>' +
                            'Motor Current: {}<br>' +
                            'Motor Speed: {}<br>' +
                            '<b>Reel</b><br>' +
                            'Motor Current: {}<br>' +
                            'Motor Speed: {}<br><br>'+
                            '{}').format(tmc['tmc'], 
                                         tms['t_speed'],
                                         rmc['rmc'],
                                         rms['r_speed'],
                                         timestamp)for rmc, tmc, rms, tms, timestamp in zip(reel_motor_currents,
                                                                                            traction_motor_currents, 
                                                                                            reel_motor_speeds,
                                                                                            traction_motor_speeds,
                                                                                            timestamps)]
            )],
        'layout':
            go.Layout(
                margin={'l': 40, 'b': 40, 't': 10, 'r': 40},
                hovermode='closest',
                mapbox=dict(bearing=0, center=dict(lat=center_lat, lon=center_lon),
                            pitch=0, zoom=14, style='satellite',
                            accesstoken=[redacted]),
                paper_bgcolor='#f9f9f9',
                plot_bgcolor='#f9f9f9'
        )
    }

The figure data is put in a dcc.Store component that is the input for a clientside callback with the following inputs and outputs:

app.clientside_callback(
    ClientsideFunction("clientside", "figure"),
    Output("map", "figure"),
    [Input("map-figure-store", "data"),
    Input("motor-current-graph", 'hoverData')])

And JS code:

if (!window.dash_clientside) {
    window.dash_clientside = {}
}

window.dash_clientside.clientside = {

   figure: function (fig_dict, hover_data) {

       if (!fig_dict) {
           throw "Figure data not loaded, aborting update."
       }
       if(!hover_data)
            return fig_dict

       // Copy the fig_data so we can modify it
       var fig_dict_copy = {...fig_dict}
       var hover_point_value = hover_data['points'][0]['x']
       const entries = Object.entries(fig_dict_copy['data'][0]['customdata'])
       for (const [index, time] of entries) {
            if(time === hover_point_value){
               fig_dict_copy['data'][0]['marker']['color'][index] = 'rgb(254,196,36)'
               fig_dict_copy['data'][0]['marker']['opacity'][index] = 1.0
            }
      }
       console.log(fig_dict_copy)
       return fig_dict_copy
   },

}

However when I put this into practice the console output shows that the color values are being changed. However the color of the markers do not change. I do know that the gps graph is being updated because if I hover over a point on the line graph while zoomed in on the gps graph, then the gps graph zooms back out.

Anyone know why the markers would not be showing their new assigned color value?

Edit: Please let me know if I need to provide any more information. I am very confused as to why this isn’t working.

Edit2: So I added clear_on_unhover to the motor current graph which got rid of a random yellow dot that would show up after re rendering the map by selecting a new period of time to look at, however this did not fix my issue and I have more proof that what should be happening, should in fact be happening but just isn’t.
I implemented a simple callback that prints the map’s figure every time it changes. Here you can see from the print that the figure does have different colors and opacities but none of these are reflected on the map itself.

Hi @Krichardson would it be possible to share here a standalone app (one that we can copy and paste in a file and execute), possibly with dummy data or downloading files from the Internet, which also reproduces the problem? The answer here is not obvious so one would have to try it to make a diagnosis.

Yes. Just one moment I’ll write a quick standalone version.

Okay so if you use this code

import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
from dash.dependencies import Input, Output, State, ClientsideFunction

# Dash setup
app = dash.Dash(__name__)

timestamps = ["2019-04-16 15:21:22", "2019-04-16 15:21:23", "2019-04-16 15:21:24", "2019-04-16 15:21:25", "2019-04-16 15:21:26"]
motor_currents1 = [20, 40, 34, 42, 50]
motor_currents2 = [10, 60, 22, 44, 43]
latitudes = [41.26319, 41.26318833333333, 41.26319, 41.263205, 41.263215]
longitudes = [-81.903775,-81.90377833333334,-81.90377666666667, -81.90378, -81.90378]
colors = ['rgb(138,43,226)','rgb(138,43,226)','rgb(138,43,226)','rgb(138,43,226)','rgb(138,43,226)']
opacities = [0.5,0.5,0.5,0.5,0.5]

fig1 = go.Figure()
fig1.add_trace(go.Scatter(x=timestamps, y=motor_currents1, name='Reel Motor Current',
                              connectgaps=False, line=dict(color='rgb(255,140,0)')))
fig1.add_trace(go.Scatter(x=timestamps, y=motor_currents2, name='Traction Motor Current',
                              connectgaps=False, line=dict(color='rgb(139,69,19)')))
fig1.update_layout(title='Motor Currents', xaxis_title='Time', yaxis_title='Current',
                       paper_bgcolor='#f9f9f9', plot_bgcolor='#f9f9f9')

app.layout = html.Div(
            [
                html.Div(
                    [
                        dcc.Graph(id='map',
                                  figure={
                                        'data': [
                                            go.Scattermapbox(
                                                lat=latitudes,
                                                lon=longitudes,
                                                mode='markers',
                                                customdata=timestamps,
                                                marker=go.scattermapbox.Marker(
                                                    size=15, opacity=opacities, color=colors
                                                ),
                                            )],
                                        'layout':
                                            go.Layout(
                                                margin={'l': 40, 'b': 40, 't': 10, 'r': 40},
                                                hovermode='closest',
                                                mapbox=dict(bearing=0, center=dict(lat=latitudes[0], lon=longitudes[0]),
                                                            pitch=0, zoom=17, style='satellite',
                                                            accesstoken='pk.eyJ1Ijoia3JpY2hhcmRzb244NDYiLCJhIjoiY2sybmpmMjNoMDMzZjNjbWJzdm96c'
                                                                        'jVzdiJ9.5RAOHjWaON5ePlvSQ4lrCQ'), 
                                                paper_bgcolor='#f9f9f9',
                                                plot_bgcolor='#f9f9f9'
                                        )
                                    }
                                  )
                    ]
                ),
                html.Div(
                    [
                        dcc.Graph(id='motor-current-graph', clear_on_unhover=True, figure=fig1)
                    ]
                )
            ]
        )
app.clientside_callback(
    ClientsideFunction("clientside", "figure"),
    Output("map", "figure"),
    [Input("motor-current-graph", 'hoverData')],
    [State("map", "figure")])

if __name__ == '__main__':
    app.run_server(debug=True, host='0.0.0.0', port=8080)

which this JS code in your assets folder:

if (!window.dash_clientside) {
    window.dash_clientside = {}
}

window.dash_clientside.clientside = {

   figure: function (hover_data, fig_dict) {

       if (!fig_dict) {
           throw "Figure data not loaded, aborting update."
       }
       if(!hover_data)
            return fig_dict

       // Copy the fig_data so we can modify it
       var fig_dict_copy = {...fig_dict}
       var hover_point_value = hover_data['points'][0]['x']
       const entries = Object.entries(fig_dict_copy['data'][0]['customdata'])
       for (const [index, time] of entries) {
            if(time === hover_point_value){
               fig_dict_copy['data'][0]['marker']['color'][index] = 'rgb(254,196,36)'
               fig_dict_copy['data'][0]['marker']['opacity'][index] = 1.0
            }
      }
       console.log(fig_dict_copy)
       return fig_dict_copy
   },

}

That should recreate the problem

So I’m not sure I understand what’s going on but the following JS code works as expected

if (!window.dash_clientside) {
    window.dash_clientside = {}
}

window.dash_clientside.clientside = {

   figure: function (hover_data, fig_dict) {

       if (!fig_dict) {
           throw "Figure data not loaded, aborting update."
       }
       if(!hover_data)
            return fig_dict

       // Copy the fig_data so we can modify it
       var fig_dict_copy = {...fig_dict}
       var hover_point_value = hover_data['points'][0]['x']
       const entries = Object.entries(fig_dict_copy['data'][0]['customdata'])
       var colors = ['red', 'red', 'red', 'red', 'red']
       for (const [index, time] of entries) {
            if(time === hover_point_value){
               console.log("Point found !")
               console.log(index)
               colors[index] = 'yellow'

            }  
      }     
       fig_dict_copy['data'][0]['marker']['color'] = colors
       return fig_dict_copy
   },

}

I don’t know enough Javascript but maybe the type of fig_dict_copy['data'][0]['marker'] is not a usual list / array ?? If you have the time to investigate more it would be great if you could post a follow-up here if you understand more what’s going on.

1 Like

I know next to nothing about Javascript. But I suppose I could try to use your code to implement some sort of workaround. something like instead of var colors = ['red', 'red', 'red', 'red', 'red'] I could set it to var colors = fig_dict_copy['data'][0]['marker']['color] and then modify that array? Seems silly that I would have to do that. But if I have to than I suppose I do. If I figure out whats going on I’ll post it here. Thank you by the way for looking into this!

So This code here works fine for me:

if (!window.dash_clientside) {
    window.dash_clientside = {}
}

window.dash_clientside.clientside = {

   figure: function (fig_dict, hover_data) {

       if (!fig_dict) {
           throw "Figure data not loaded, aborting update."
       }
       if(!hover_data)
            return fig_dict

       // Copy the fig_data so we can modify it
       var fig_dict_copy = {...fig_dict}
       var hover_point_value = hover_data['points'][0]['x']
       const entries = Object.entries(fig_dict_copy['data'][0]['customdata'])
       var colors = [...fig_dict_copy['data'][0]['marker']['color']]
       var opacities = [...fig_dict_copy['data'][0]['marker']['opacity']]
       for (const [index, time] of entries) {
            if(time === hover_point_value){
               colors[index] = 'rgb(254,196,36)'
               opacities[index] = 1.0
            }
      }
       fig_dict_copy['data'][0]['marker']['color'] = colors
       fig_dict_copy['data'][0]['marker']['opacity'] = opacities
       console.log(fig_dict_copy)
       return fig_dict_copy
   },

}

My guess is that it has something to do with the fact that assignment in javascript is done almost entirely by reference. To be completely honest I’m not sure why that would matter, but maybe a java expert can explain it to us one day. For now I’m just glad I could get this to work so thank you for your help!

1 Like