Clickable location on Map as a filter for line chart. How to connect them?

Hello,

I am new to Plotly Dash. I created some Dashboard in Tableau, in Tableau I have a dashboard with a map and a line chart next to it where I can select locations on the map and the line chart shows me the result. I try to do this in Dash and learn some new tools like Dash since I often used Plotly with its beautiful graphs.

I have internal data on a database with some complex logic where I have the locations on a seperate file, since I want to have as less data as possible I don’t want to include lat und lon in the SQL View. For this question I use data available on the web.

I have sensor measuremt data like no2, no, o3 and more from sensors at different locations. My goal ist to have a map with the locations of the sensors. When I click on a location it should show me the line chart and I want to be able to select as many locations as I want so that I can compare the line charts.

Is this possible? I have the line chart, but I can’t connect the part with the map to work. I will use this as a foundation to include many more graphs like bar charts and so on. The top selector will be a dropdown to select the parameter like no2, pm25. Many thanks in advance.

This is the line chart:

import plotly.express as px
from jupyter_dash import JupyterDash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
import pandas as pd

# Load Data
df = pd.read_csv('no_data.txt') # file with the measurement data
mp = pd.read_csv('location_sensors.csv') # file with the location data, 3 locations are used in this sample data

# Build App
app = JupyterDash(__name__)

app.layout = html.Div([
    dcc.Dropdown(id='dropdown_parameter',
            options=[{'label':x, 'value':x} for x in df['parameter'].unique()],
            value='no2',
            multi=False,
            disabled=False,
            clearable=True,
            searchable=True,
            placeholder='Choose parameter...'
            ),  
    
    html.Div([
        dcc.Graph(id='map_filter'),
        dcc.Graph(id='line_chart')
    ]),
])

@app.callback(
    Output('line_chart', 'figure'),
    Input('dropdown_parameter', 'value'),
)
def update_graph(parameter_chosen):
    dff = df[(df['parameter']==parameter_chosen)]
    fig = px.line(dff, x='date.utc', y='value', color='location')
    return fig

app.run_server(debug=True)

The data is located here:

measurement data: pandas/air_quality_long.csv at master · pandas-dev/pandas · GitHub

location data:

location,latitude,longitude
BETR801,51.20966,4.43182
FR04014,48.83722,2.3939
London Westminster,51.49467,-0.13193

Hi,

Welcome to the community!

Yes, this is possible to be done. Take a look on this documentation page to see how this is actually implemented. In a nutshell, you would need to have a callback with Input("map_filter", "selectedData"), so it will be triggered every time the point selection changes. Note that the event data has a special format and you will need to find which field works best for you to filter the location list.

Please follow up if you have specific problems or questions with this functionality. I will be happy to help!

1 Like

@jlfsjunior Thanks for your comment. Helped me really a lot!

I followed the documentation and adapted it to my work. I created a map where the location is clickable. My problem is, how can I store the data in a list so that I can use this list to filter my dataframe to the selected locations? If I select two sensors for example, I want to safe this so that I can use it. Thanks in advance.

With my solution I get following error:

TypeError                                 Traceback (most recent call last)
<ipython-input-1-341ea97339a6> in display_selected_data(selectedData=None)
     40     Input('map_sensors', 'selectedData'))
     41 def display_selected_data(selectedData):
---> 42     sel_sensor = selectedData['points'][0]['customdata'][0]
        sel_sensor = undefined
        selectedData = None
     43     dff = df[df.location.isin(sel_sensor)]
     44     fig = px.line(data_frame=dff, x='date.utc', y='value', color='location')

TypeError: 'NoneType' object is not subscriptable

My callback right now looks like this:

@app.callback(
    Output('time_series1', 'figure'),
    Input('map_sensors', 'selectedData'))
def display_selected_data(selectedData):
    sel_sensor = selectedData['points'][0]['customdata'][0]
    dff = df[df.location.isin(sel_sensor)]
    fig = px.line(data_frame=dff, x='date.utc', y='value', color='location')
    fig.update_traces(mode='lines+markers')
    return fig

This is the whole app:

import dash_leaflet as dl
from jupyter_dash import JupyterDash
import plotly.graph_objs as go
from dash import Dash, dcc, html, Input, Output, State
import pandas as pd
import plotly.express as px
import json

loc = pd.read_csv('location_sensors.csv')
df = pd.read_csv('measurement.csv')
style = {'width': '50%', 'height': '500px', 'float': 'left'}

# Build small example app.
app = JupyterDash(__name__)

styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}

fig_map = px.scatter_mapbox(loc, lat="lat", lon="lon", hover_name="location", 
                           hover_data={'location':True, 'lat':False, 'lon':False}, zoom=3, height=600,
                           color='location', mapbox_style="open-street-map")
fig_map.update_layout(clickmode='event+select')

app.layout =   html.Div([
        dcc.Graph(id='map_sensors', figure=fig_map , className='six columns'),
      
    html.Div([dcc.Graph(
                    id='time_series1',
                    style={'height': 400}
                ),
])
])

@app.callback(
    Output('time_series1', 'figure'),
    Input('map_sensors', 'selectedData'))
def display_selected_data(selectedData):
    sel_sensor = selectedData['points'][0]['customdata'][0]
    dff = df[df.location.isin(sel_sensor)]
    fig = px.line(data_frame=dff, x='date.utc', y='value', color='location')
    fig.update_traces(mode='lines+markers')
    return fig

if __name__ == '__main__':
    app.run_server(mode='external', port='8025')

Glad it was helpful!

So, starting with the error… I believe the issue is because selectedData can be None (for example, when the app renders the first time), therefore the type error raised. You can add a conditional testing for not None to avoid it.

As for the multiple selections and saving the selected list, there are at least two approaches:

  • layout.clickmode = 'event+select' makes it possible to select multiple points simultaneously by holding shift. While this does not save the selection anywhere, it does allow you to have multiple points selected at once. It can work in simple cases, but maybe not in yours

  • A better approach, where the selection is actually saved, can be found in this old post. Take a look and see if it fits your case.

1 Like

@jlfsjunior @roland wow thank you both, I can’t thank you enough. The solution you both provided is what I was looking for and it works. I have small adjustment to make like what graph is displayed at the start when nothing is selected, but otherwise it is working perfect.

Right now I have two problems and I can’t find a solution right now:

  1. I have a file with the sensor locations. Since this file will not change I added a column color with color codes for every location like roland has. Now the problem is, my color of the line chart is not in sync with the colors of the locations. When I select for example a location with a red color, the corresponding line in the chart is blue.
    How can I sync the color so that the loactions and line in the chart have the same color?

  2. When I use css styling to place the map on the left side and the line chart on the right side in one row (six columns each) the map looses any function and I can’t even zoom in there or select anyting. Has this something to do with the way the map was created?

Edit: What I also observed is the error I get in debug mode: too much recursion. Why is there an error even when everything is working fine? I wanted to check the callbacks in debug mode but it forwards me to the recursion error.

Ok, about your follow-up questions:

  1. You should be able to set the trace color in the callback. Express does it for you via color_discrete_sequence, which you might have to tweak a bit to preserve the trace order (in case you allow for multiple traces). Otherwise you can always modify the traces directly via fig.data.

  2. It is hard to know about this without a reproducible example. This shouldn’t happen in any case… Anyway, my recommendation would be to use layout components from dash-bootstrap-components for this purpose instead of handling the css yourself.

On the edit, it is also a bit hard to find the cause without a reproducible example.

1 Like