Bring Drag & Drop to Dash with Dashboard Engine. 💫 Learn how at our next webinar!

Keeping map selections and menu selections in sync in a callback

Hi,

I have an application with a map that shows the locations of some platforms, these same platforms are in a drop down menu. I would like the user to be able to select the platforms of interest using either the map or the dropdown menu and thus I’d like to keep them in sync.

I can get the map selections and update the menu value accordingly, but I’m having trouble figuring out how to do the reverse - change the map selections based on the changes in the menu. I can edit the “selectedData” object and return it, but it does not affect the selections show on the map. I haven’t tried constructing the “selectedData” object from whole cloth when the first menu item is selected since it has no effect.

A self-contained example is included below.

Thanks,
Roland

import dash
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import pandas as pd
import json
import plotly.express as px
from dash.dependencies import Output, Input, State

app = dash.Dash(__name__)

locations = "\"{\\\"site_code\\\":{\\\"0\\\":\\\"0n110w\\\",\\\"1\\\":\\\"0n140w\\\",\\\"2\\\":\\\"0n165e\\\",\\\"3\\\":\\\"0n170w\\\",\\\"4\\\":\\\"0n23w\\\",\\\"5\\\":\\\"0n80.5e\\\",\\\"6\\\":\\\"0n95w\\\",\\\"7\\\":\\\"10n95w\\\",\\\"8\\\":\\\"10s10w\\\",\\\"9\\\":\\\"12n23w\\\",\\\"10\\\":\\\"12n95w\\\",\\\"11\\\":\\\"15n38w\\\",\\\"12\\\":\\\"15n65e\\\",\\\"13\\\":\\\"15n90e\\\",\\\"14\\\":\\\"19s34w\\\",\\\"15\\\":\\\"20n38w\\\",\\\"16\\\":\\\"2n95w\\\",\\\"17\\\":\\\"2s95w\\\",\\\"18\\\":\\\"3.5n95w\\\",\\\"19\\\":\\\"5n95w\\\",\\\"20\\\":\\\"5s95w\\\",\\\"21\\\":\\\"6s8e\\\",\\\"22\\\":\\\"8n95w\\\",\\\"23\\\":\\\"8s67e\\\",\\\"24\\\":\\\"8s95w\\\"},\\\"wmo_platform_code\\\":{\\\"0\\\":\\\"32323\\\",\\\"1\\\":\\\"51311\\\",\\\"2\\\":\\\"52321\\\",\\\"3\\\":\\\"51010\\\",\\\"4\\\":\\\"31007\\\",\\\"5\\\":\\\"23001\\\",\\\"6\\\":\\\"32321\\\",\\\"7\\\":\\\"43008\\\",\\\"8\\\":\\\"15001\\\",\\\"9\\\":\\\"13001\\\",\\\"10\\\":\\\"43011\\\",\\\"11\\\":\\\"13008\\\",\\\"12\\\":\\\"23011\\\",\\\"13\\\":\\\"23009\\\",\\\"14\\\":\\\"31005\\\",\\\"15\\\":\\\"41139\\\",\\\"16\\\":\\\"32320\\\",\\\"17\\\":\\\"32322\\\",\\\"18\\\":\\\"32011\\\",\\\"19\\\":\\\"32303\\\",\\\"20\\\":\\\"32304\\\",\\\"21\\\":\\\"15007\\\",\\\"22\\\":\\\"43301\\\",\\\"23\\\":\\\"14040\\\",\\\"24\\\":\\\"32305\\\"},\\\"latitude\\\":{\\\"0\\\":0.0,\\\"1\\\":0.0,\\\"2\\\":0.0,\\\"3\\\":0.0,\\\"4\\\":0.0,\\\"5\\\":0.0,\\\"6\\\":0.0,\\\"7\\\":10.0,\\\"8\\\":-10.0,\\\"9\\\":12.0,\\\"10\\\":12.0,\\\"11\\\":15.0,\\\"12\\\":15.0,\\\"13\\\":15.0,\\\"14\\\":-19.0,\\\"15\\\":20.0,\\\"16\\\":2.0,\\\"17\\\":-2.0,\\\"18\\\":3.5,\\\"19\\\":5.0,\\\"20\\\":-5.0,\\\"21\\\":-6.0,\\\"22\\\":8.0,\\\"23\\\":-8.0,\\\"24\\\":-8.0},\\\"longitude\\\":{\\\"0\\\":-110.0,\\\"1\\\":-140.0,\\\"2\\\":165.0,\\\"3\\\":-170.0,\\\"4\\\":-23.0,\\\"5\\\":80.5,\\\"6\\\":-95.0,\\\"7\\\":-95.0,\\\"8\\\":-10.0,\\\"9\\\":-23.0,\\\"10\\\":-95.0,\\\"11\\\":-38.0,\\\"12\\\":65.0,\\\"13\\\":90.0,\\\"14\\\":-34.0,\\\"15\\\":-38.0,\\\"16\\\":-95.0,\\\"17\\\":-95.0,\\\"18\\\":-95.0,\\\"19\\\":-95.0,\\\"20\\\":-95.0,\\\"21\\\":8.0,\\\"22\\\":-95.0,\\\"23\\\":67.0,\\\"24\\\":-95.0}}\""

locs = json.loads(locations)
df = pd.read_json(locs, dtype={'wmo_platform_code': str, 'latitude': np.float64, 'longitude': np.float64})
menu_options = [{'label': platform, 'value': platform} for platform in sorted(df['wmo_platform_code'].to_list())]
app.layout = html.Div(children=[
    dcc.Graph(id='location-map'),
    dcc.Dropdown(id='platforms-dd', options=menu_options, multi=True),
    html.Div(id='hidden-div', style={'display': 'none'})
]
)


@app.callback(
    Output("location-map", "figure"),
    Input('hidden-div', 'children')
)
def show_map(adiv):

    location_map = px.scatter_geo(df,
                                  lat='latitude', lon='longitude',
                                  color='wmo_platform_code',
                                  custom_data=['wmo_platform_code'],
                                  labels={'title': 'Platform'},
                                  category_orders={'wmo_platform_code': sorted(df['wmo_platform_code'].to_list())},
                                  )
    location_map.update_layout(clickmode='select+event')
    location_map.update_traces(marker_size=10,
                               unselected=dict(marker=dict(size=10)),
                               selected=dict(marker=dict(size=14))
                               )

    return location_map


@app.callback(
    Output('platforms-dd', 'value'),
    Output('location-map', 'selectedData'),
    Input('platforms-dd', 'value'),
    Input('location-map', 'selectedData'),
    Input('location-map', 'clickData'),
    prevent_initial_call=True
)
def show_map(menu_values, selections, clicks):
    ctx = dash.callback_context
    oid = None
    if ctx.triggered:
        oid = ctx.triggered[0]['prop_id'].split('.')[0]
    else:
        print('call back with no trigger')

    if oid == 'location-map':
        # Map click, set the menu according to the map
        menu_values = []
        if clicks is not None:
            if selections is not None:
                if 'points' in selections:
                    for p in selections['points']:
                        plat = p['customdata'][0]
                        if plat not in menu_values:
                            menu_values.append(plat)
        print(str(menu_values))

    else:  # The menu changed
        remove = []
        if selections is not None:
            if 'points' in selections:
                for a_point in selections['points']:
                    plat_selected = a_point['customdata'][0]
                    if plat_selected not in menu_values:
                        remove.append(a_point)
            for r in remove:
                if r in selections['points']:
                    selections['points'].remove(r)
        print(str(selections))

    return menu_values, selections


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

Hi @roland!

There might be other ways to accomplish what you want to do, but here’s how I would approach this type of synchronisation (with a short explanation):

I would specifically draw a trace (scatter) for the selected values and another for unselected using some sort of color/size code. Basically what you are doing here, but instead of update the traces based on selected or unselected, do it explicitly in your figure with two different traces. One simple way could be to add a “selected” (type bool) column to your df and use color="selected", just to avoid changing from plotly.express to plotly.graph_object (it can be other encoding as well, like size… it is just more work).

Now, the reason why your approach does not work is because selectedData and clickData are read-only props in dcc.Graph, meaning that you can’t set which values are selected. On top of it, note that you can only have single selections clicking on the scatter points (one point at the time), which won’t work for a Dropdown with multi=True and the type of user interactions I imagine you are implementing in this app…

So, one alternative is to manage the selection manually using a dcc.Store component. In a nutshell, you can create a callback like:

@app.callback(
    Output("selection-state", "data"), # the dcc.Store component
    Input('location-map', 'clickData'),
    Input('platforms-dd', 'value'),
    State("selection-state", "data"),
)
def update_selection(click_event, dropdown_selection, current_state):
      # compare new click_event with current_state (if not None)
      # i.e. remove from current_state if point is selected or add otherwise

      # compare dropdown_selection (a list) with current_state
      # add and remove accordingly
      return new_state 

With this, you can in principle use the data in the selection-state in both the Graph, to map each row of df to selected or unselected and plot it accordingly, and in Dropdown values. Because of circular callbacks, you should probably do it in the same callback above (please refer to the examples in the end of this section). As a bonus, you can add some persistence to the selection via storage_type, so selections are preserved after refresh or quit (check the dcc.Store docs for that).

Apologies for the long reply and I hope that this helps! ::slight_smile:

@jlfsjunior

Thank you very much for your detailed suggestion. Using your approach of creating two traces (one for selected points and one for unselected points) gets me the close to the functionality that I want, but clicking on the same point twice results in unexpected behavior (no callback is generated).

This version has a similar problem in that you can add and remove single points either from the menu or from the map, but if you click on the same point twice in a row you don’t get a callback. So you can’t toggle it. If you click a different point everything works as expected and if you remove the last added point via the menu everything else works as expected as far as I can tell.

I was hoping keeping, uirevision, uid and selectionrevision constant would allow me to regenerate the map while setting the selectedpoints, but even with those set, I only get one point in selectedData when ‘selected+event’ is set, not the full list that was set with selectedpoints. So I’m just using the single point in the click for now. All this fooling around does get me different marker characteristics for selected and unselected points which was one of the goals.

I think it’s a bug. You should get the points from selectedpoints in the callback, but you don’t.

import dash
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import pandas as pd
import json
import plotly.graph_objects as go
from dash.dependencies import Output, Input, State
import datetime

app = dash.Dash(__name__)
graph_config = {'displaylogo': False, 'modeBarButtonsToRemove': ['select2d', 'lasso2d']}
locations = "\"{\\\"site_code\\\":{\\\"0\\\":\\\"0n110w\\\",\\\"1\\\":\\\"0n140w\\\",\\\"2\\\":\\\"0n165e\\\",\\\"3\\\":\\\"0n170w\\\",\\\"4\\\":\\\"0n23w\\\",\\\"5\\\":\\\"0n80.5e\\\",\\\"6\\\":\\\"0n95w\\\",\\\"7\\\":\\\"10n95w\\\",\\\"8\\\":\\\"10s10w\\\",\\\"9\\\":\\\"12n23w\\\",\\\"10\\\":\\\"12n95w\\\",\\\"11\\\":\\\"15n38w\\\",\\\"12\\\":\\\"15n65e\\\",\\\"13\\\":\\\"15n90e\\\",\\\"14\\\":\\\"19s34w\\\",\\\"15\\\":\\\"20n38w\\\",\\\"16\\\":\\\"2n95w\\\",\\\"17\\\":\\\"2s95w\\\",\\\"18\\\":\\\"3.5n95w\\\",\\\"19\\\":\\\"5n95w\\\",\\\"20\\\":\\\"5s95w\\\",\\\"21\\\":\\\"6s8e\\\",\\\"22\\\":\\\"8n95w\\\",\\\"23\\\":\\\"8s67e\\\",\\\"24\\\":\\\"8s95w\\\"},\\\"wmo_platform_code\\\":{\\\"0\\\":\\\"32323\\\",\\\"1\\\":\\\"51311\\\",\\\"2\\\":\\\"52321\\\",\\\"3\\\":\\\"51010\\\",\\\"4\\\":\\\"31007\\\",\\\"5\\\":\\\"23001\\\",\\\"6\\\":\\\"32321\\\",\\\"7\\\":\\\"43008\\\",\\\"8\\\":\\\"15001\\\",\\\"9\\\":\\\"13001\\\",\\\"10\\\":\\\"43011\\\",\\\"11\\\":\\\"13008\\\",\\\"12\\\":\\\"23011\\\",\\\"13\\\":\\\"23009\\\",\\\"14\\\":\\\"31005\\\",\\\"15\\\":\\\"41139\\\",\\\"16\\\":\\\"32320\\\",\\\"17\\\":\\\"32322\\\",\\\"18\\\":\\\"32011\\\",\\\"19\\\":\\\"32303\\\",\\\"20\\\":\\\"32304\\\",\\\"21\\\":\\\"15007\\\",\\\"22\\\":\\\"43301\\\",\\\"23\\\":\\\"14040\\\",\\\"24\\\":\\\"32305\\\"},\\\"latitude\\\":{\\\"0\\\":0.0,\\\"1\\\":0.0,\\\"2\\\":0.0,\\\"3\\\":0.0,\\\"4\\\":0.0,\\\"5\\\":0.0,\\\"6\\\":0.0,\\\"7\\\":10.0,\\\"8\\\":-10.0,\\\"9\\\":12.0,\\\"10\\\":12.0,\\\"11\\\":15.0,\\\"12\\\":15.0,\\\"13\\\":15.0,\\\"14\\\":-19.0,\\\"15\\\":20.0,\\\"16\\\":2.0,\\\"17\\\":-2.0,\\\"18\\\":3.5,\\\"19\\\":5.0,\\\"20\\\":-5.0,\\\"21\\\":-6.0,\\\"22\\\":8.0,\\\"23\\\":-8.0,\\\"24\\\":-8.0},\\\"longitude\\\":{\\\"0\\\":-110.0,\\\"1\\\":-140.0,\\\"2\\\":165.0,\\\"3\\\":-170.0,\\\"4\\\":-23.0,\\\"5\\\":80.5,\\\"6\\\":-95.0,\\\"7\\\":-95.0,\\\"8\\\":-10.0,\\\"9\\\":-23.0,\\\"10\\\":-95.0,\\\"11\\\":-38.0,\\\"12\\\":65.0,\\\"13\\\":90.0,\\\"14\\\":-34.0,\\\"15\\\":-38.0,\\\"16\\\":-95.0,\\\"17\\\":-95.0,\\\"18\\\":-95.0,\\\"19\\\":-95.0,\\\"20\\\":-95.0,\\\"21\\\":8.0,\\\"22\\\":-95.0,\\\"23\\\":67.0,\\\"24\\\":-95.0},\\\"platform_color\\\":{\\\"0\\\":\\\"#d60000\\\",\\\"1\\\":\\\"#8c3bff\\\",\\\"2\\\":\\\"#018700\\\",\\\"3\\\":\\\"#00acc6\\\",\\\"4\\\":\\\"#97ff00\\\",\\\"5\\\":\\\"#ff7ed1\\\",\\\"6\\\":\\\"#6b004f\\\",\\\"7\\\":\\\"#ffa52f\\\",\\\"8\\\":\\\"#00009c\\\",\\\"9\\\":\\\"#857067\\\",\\\"10\\\":\\\"#004942\\\",\\\"11\\\":\\\"#4f2a00\\\",\\\"12\\\":\\\"#00fdcf\\\",\\\"13\\\":\\\"#bcb6ff\\\",\\\"14\\\":\\\"#95b379\\\",\\\"15\\\":\\\"#bf03b8\\\",\\\"16\\\":\\\"#2466a1\\\",\\\"17\\\":\\\"#280041\\\",\\\"18\\\":\\\"#dbb3af\\\",\\\"19\\\":\\\"#fdf490\\\",\\\"20\\\":\\\"#4f445b\\\",\\\"21\\\":\\\"#a37c00\\\",\\\"22\\\":\\\"#ff7066\\\",\\\"23\\\":\\\"#3f806e\\\",\\\"24\\\":\\\"#82000c\\\"}}\""

locs = json.loads(locations)
df = pd.read_json(locs, dtype={'wmo_platform_code': str, 'latitude': np.float64, 'longitude': np.float64})
menu_options = [{'label': platform, 'value': platform} for platform in sorted(df['wmo_platform_code'].to_list())]
app.layout = html.Div(children=[
    dcc.Graph(id='location-map', config=graph_config),
    dcc.Dropdown(id='platforms-dd', options=menu_options, multi=True),
    html.Div(children=[
        html.Pre(id='click-pre', style={'display':'inline-block', 'width':'50%'}, children=[]),
        html.Pre(id='select-pre', style={'display':'inline-block', 'width':'50%'}, children=[])
    ]),
    html.Div(id='hidden-div', style={'display': 'none'})
]
)


@app.callback(
    Output("location-map", "figure"),
    Input('platforms-dd', 'value'),
    Input('hidden-div', 'children')
)
def show_map(menu_items, adiv):
    if menu_items is None:
        menu_items = []
    selected_points = []
    for idx, platform in enumerate(df['wmo_platform_code']):
        if platform in menu_items:
            selected_points.append(idx)
    location_map = go.Figure(go.Scattergeo(lat=df['latitude'], lon=df['longitude'],
                                           showlegend=False,
                                           text=df['wmo_platform_code'],
                                           uirevision='constant-will-be-dataset-id',
                                           uid='constant-will-be-dataset-id',
                                           marker=dict(color=df['platform_color'],size=10,),
                                           unselected=dict(marker=dict(size=10)),
                                           selected=dict(marker=dict(size=14)),
                                           selectedpoints=selected_points,
                                           ),
                             layout_selectionrevision='constant-will-be-dataset-id',
                             layout_clickmode='event'
                             )
    return location_map


@app.callback(
    Output('platforms-dd', 'value'),
    Output('click-pre', 'children'),
    Input('location-map', 'clickData'),
    State('platforms-dd', 'value'),
    prevent_initial_call=True
)
def show_map(clicks, previous_selections):
    # Map click, set the menu according to the map
    menu_values = []
    if previous_selections is not None:
        menu_values = previous_selections
    if clicks is not None:
        if 'points' in clicks:
            for p in clicks['points']:
                plat = p['text']
                if plat not in menu_values:
                    menu_values.append(plat)
                else:
                    menu_values.remove(plat)
        else:
            menu_values = []
    today = datetime.datetime. now()
    date_time = today.strftime("%m/%d/%Y, %H:%M:%S")
    click_or_none = date_time
    if click_or_none is not None:
        click_or_none = date_time + '\n' + json.dumps(clicks, indent=4)

    return menu_values, 'Click:\n' + click_or_none


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

I had to actually run your code and test a bit, then do a bit of investigation, but I think part of the problem can be solved!

So, first, it turns out that clickData is not so “read-only” as documented and you can actually reset it passing a None in the callback (see the point in this issue). If you switch your Figure to layout_clickmode='event+select' and pass None to clickData in the click handler callback, I believe you’ll get the behavior you want.

I was hoping keeping, uirevision, uid and selectionrevision constant would allow me to regenerate the map while setting the selectedpoints, but even with those set […]

The problem with uirevision in your case is that dcc.Graph is regenerated at every point selection, so it won’t have any effect. That might actually be a harder problem for you than the original question and it impact for example the zoom and pan selections (the figure will “reset zoom & pan” when a point is clicked).

@jlfsjunior

Sweet! Thank you for going to the trouble to do some tests and pointing out that I can reset the click data.

I added code to keep track of the map center and zoom after a click so I can set them when I regenerate the map. Since I’m using mapbox, I switched over in this final example. This is not the exact functionality I envisioned when I set out (shift-clicks for multiple, single click to reset to one selection), but I think it is very usable.

Thanks again. For those that come along later, this is what I ended up with:

import dash
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import pandas as pd
import json
import plotly.graph_objects as go
from dash.dependencies import Output, Input, State
import datetime

app = dash.Dash(__name__)
# with open('key.txt') as key:
#     ESRI_API_KEY = key.readline()
graph_config = {'displaylogo': False, 'modeBarButtonsToRemove': ['select2d', 'lasso2d']}
locations = "\"{\\\"site_code\\\":{\\\"0\\\":\\\"0n110w\\\",\\\"1\\\":\\\"0n140w\\\",\\\"2\\\":\\\"0n165e\\\",\\\"3\\\":\\\"0n170w\\\",\\\"4\\\":\\\"0n23w\\\",\\\"5\\\":\\\"0n80.5e\\\",\\\"6\\\":\\\"0n95w\\\",\\\"7\\\":\\\"10n95w\\\",\\\"8\\\":\\\"10s10w\\\",\\\"9\\\":\\\"12n23w\\\",\\\"10\\\":\\\"12n95w\\\",\\\"11\\\":\\\"15n38w\\\",\\\"12\\\":\\\"15n65e\\\",\\\"13\\\":\\\"15n90e\\\",\\\"14\\\":\\\"19s34w\\\",\\\"15\\\":\\\"20n38w\\\",\\\"16\\\":\\\"2n95w\\\",\\\"17\\\":\\\"2s95w\\\",\\\"18\\\":\\\"3.5n95w\\\",\\\"19\\\":\\\"5n95w\\\",\\\"20\\\":\\\"5s95w\\\",\\\"21\\\":\\\"6s8e\\\",\\\"22\\\":\\\"8n95w\\\",\\\"23\\\":\\\"8s67e\\\",\\\"24\\\":\\\"8s95w\\\"},\\\"wmo_platform_code\\\":{\\\"0\\\":\\\"32323\\\",\\\"1\\\":\\\"51311\\\",\\\"2\\\":\\\"52321\\\",\\\"3\\\":\\\"51010\\\",\\\"4\\\":\\\"31007\\\",\\\"5\\\":\\\"23001\\\",\\\"6\\\":\\\"32321\\\",\\\"7\\\":\\\"43008\\\",\\\"8\\\":\\\"15001\\\",\\\"9\\\":\\\"13001\\\",\\\"10\\\":\\\"43011\\\",\\\"11\\\":\\\"13008\\\",\\\"12\\\":\\\"23011\\\",\\\"13\\\":\\\"23009\\\",\\\"14\\\":\\\"31005\\\",\\\"15\\\":\\\"41139\\\",\\\"16\\\":\\\"32320\\\",\\\"17\\\":\\\"32322\\\",\\\"18\\\":\\\"32011\\\",\\\"19\\\":\\\"32303\\\",\\\"20\\\":\\\"32304\\\",\\\"21\\\":\\\"15007\\\",\\\"22\\\":\\\"43301\\\",\\\"23\\\":\\\"14040\\\",\\\"24\\\":\\\"32305\\\"},\\\"latitude\\\":{\\\"0\\\":0.0,\\\"1\\\":0.0,\\\"2\\\":0.0,\\\"3\\\":0.0,\\\"4\\\":0.0,\\\"5\\\":0.0,\\\"6\\\":0.0,\\\"7\\\":10.0,\\\"8\\\":-10.0,\\\"9\\\":12.0,\\\"10\\\":12.0,\\\"11\\\":15.0,\\\"12\\\":15.0,\\\"13\\\":15.0,\\\"14\\\":-19.0,\\\"15\\\":20.0,\\\"16\\\":2.0,\\\"17\\\":-2.0,\\\"18\\\":3.5,\\\"19\\\":5.0,\\\"20\\\":-5.0,\\\"21\\\":-6.0,\\\"22\\\":8.0,\\\"23\\\":-8.0,\\\"24\\\":-8.0},\\\"longitude\\\":{\\\"0\\\":-110.0,\\\"1\\\":-140.0,\\\"2\\\":165.0,\\\"3\\\":-170.0,\\\"4\\\":-23.0,\\\"5\\\":80.5,\\\"6\\\":-95.0,\\\"7\\\":-95.0,\\\"8\\\":-10.0,\\\"9\\\":-23.0,\\\"10\\\":-95.0,\\\"11\\\":-38.0,\\\"12\\\":65.0,\\\"13\\\":90.0,\\\"14\\\":-34.0,\\\"15\\\":-38.0,\\\"16\\\":-95.0,\\\"17\\\":-95.0,\\\"18\\\":-95.0,\\\"19\\\":-95.0,\\\"20\\\":-95.0,\\\"21\\\":8.0,\\\"22\\\":-95.0,\\\"23\\\":67.0,\\\"24\\\":-95.0},\\\"platform_color\\\":{\\\"0\\\":\\\"#d60000\\\",\\\"1\\\":\\\"#8c3bff\\\",\\\"2\\\":\\\"#018700\\\",\\\"3\\\":\\\"#00acc6\\\",\\\"4\\\":\\\"#97ff00\\\",\\\"5\\\":\\\"#ff7ed1\\\",\\\"6\\\":\\\"#6b004f\\\",\\\"7\\\":\\\"#ffa52f\\\",\\\"8\\\":\\\"#00009c\\\",\\\"9\\\":\\\"#857067\\\",\\\"10\\\":\\\"#004942\\\",\\\"11\\\":\\\"#4f2a00\\\",\\\"12\\\":\\\"#00fdcf\\\",\\\"13\\\":\\\"#bcb6ff\\\",\\\"14\\\":\\\"#95b379\\\",\\\"15\\\":\\\"#bf03b8\\\",\\\"16\\\":\\\"#2466a1\\\",\\\"17\\\":\\\"#280041\\\",\\\"18\\\":\\\"#dbb3af\\\",\\\"19\\\":\\\"#fdf490\\\",\\\"20\\\":\\\"#4f445b\\\",\\\"21\\\":\\\"#a37c00\\\",\\\"22\\\":\\\"#ff7066\\\",\\\"23\\\":\\\"#3f806e\\\",\\\"24\\\":\\\"#82000c\\\"}}\""

locs = json.loads(locations)
df = pd.read_json(locs, dtype={'wmo_platform_code': str, 'latitude': np.float64, 'longitude': np.float64})
menu_options = [{'label': platform, 'value': platform} for platform in sorted(df['wmo_platform_code'].to_list())]
app.layout = html.Div(children=[
    dcc.Graph(id='location-map', config=graph_config),
    dcc.Dropdown(id='platforms-dd', options=menu_options, multi=True),
    html.Div(children=[
        html.Pre(id='click-pre', style={'display': 'inline-block', 'width': '50%'}, children=[]),
        html.Pre(id='select-pre', style={'display': 'inline-block', 'width': '50%'}, children=[])
    ]),
    html.Div(id='hidden-div', style={'display': 'none'}),
    dcc.Store('map-settings')
]
)


@app.callback(
    Output("location-map", "figure"),
    Input('platforms-dd', 'value'),
    Input('hidden-div', 'children'),
    State('map-settings', 'data')
)
def show_map(menu_items, adiv, map_data):
    if menu_items is None:
        menu_items = []
    selected_points = []
    for idx, platform in enumerate(df['wmo_platform_code']):
        if platform in menu_items:
            selected_points.append(idx)
    center = {'lon': 0.0, 'lat': 0.0}
    zoom = 1
    if map_data is not None:
        map_settings = json.loads(map_data)
        if 'center' in map_settings:
            if map_settings['center'] != 'default':
                center = map_settings['center']
        if 'zoom' in map_settings:
            if map_settings['zoom'] != 'default':
                zoom = map_settings['zoom']
    location_map = go.Figure(go.Scattermapbox(lat=df['latitude'], lon=df['longitude'],
                                              showlegend=False,
                                              text=df['wmo_platform_code'],
                                              uirevision='constant-will-be-dataset-id',
                                              uid='constant-will-be-dataset-id',
                                              marker=dict(color=df['platform_color'], size=10, ),
                                              unselected=dict(marker=dict(size=10)),
                                              selected=dict(marker=dict(size=14)),
                                              selectedpoints=selected_points,
                                              ),
                             layout_selectionrevision='constant-will-be-dataset-id',
                             layout_clickmode='event+select',
                             layout_mapbox_style="white-bg",
                             layout_mapbox_layers=[
                                 {
                                     "below": 'traces',
                                     "sourcetype": "raster",
                                     "sourceattribution": "USGS",
                                     "source": [
                                         "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}"
                                         # "https://ibasemaps-api.arcgis.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}?token=" + ESRI_API_KEY
                                     ]
                                 }
                             ],
                             layout_mapbox_zoom=zoom,
                             layout_mapbox_center=center
                             )

    return location_map


@app.callback(
    Output('platforms-dd', 'value'),
    Output('click-pre', 'children'),
    Output('location-map', 'clickData'),
    Output('map-settings', 'data'),
    Input('location-map', 'clickData'),
    State('location-map', 'relayoutData'),
    State('platforms-dd', 'value'),
    prevent_initial_call=True
)
def show_map(clicks, relay_data, previous_selections):
    # Map click, set the menu according to the map
    center = 'default'
    zoom = 'default'
    if relay_data is not None:
        if 'mapbox.center' in relay_data:
            center = relay_data['mapbox.center']
        if 'mapbox.zoom' in relay_data:
            zoom = relay_data['mapbox.zoom']
    map_settings = {'center': center, 'zoom': zoom}
    menu_values = []
    if previous_selections is not None:
        menu_values = previous_selections
    if clicks is not None:
        if 'points' in clicks:
            for p in clicks['points']:
                plat = p['text']
                if plat not in menu_values:
                    menu_values.append(plat)
                else:
                    menu_values.remove(plat)
        else:
            menu_values = []
    today = datetime.datetime.now()
    date_time = today.strftime("%m/%d/%Y, %H:%M:%S")
    click_or_none = date_time
    if click_or_none is not None:
        click_or_none = date_time + '\n' + json.dumps(clicks, indent=4)

    return menu_values, 'Click:\n' + click_or_none, None, json.dumps(map_settings)


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