Search code examples
pythonplotlymapboxdropdownlayer

Edit "layers" property in mapbox layout by using dropdown menu (Plotly, Python)


I am trying to create a Mapbox graph using Plotly. All goes well in creating the traces and selecting which ones to display using the dropdown.

However, I am trying to draw layers over the map as well. When I do this without using a dropdown, it all goes fine. But when I try to add the layers as options using the "updatemenus" function, the layers will not be displayed.

The traces and base layout are created like this and work fine (a bit simplified, normally in a loop):

fig_dict = {
    "data": [],
    "layout": {}
}

data = dict(type='scattermapbox',
                lat=dft['lat'],
                lon=dft['lon'],
                mode='markers',
                marker=dict(size=3,
                            color=dft['Color']),
                showlegend=False,
                 )
                                                                                                          
        fig_dict["data"].append(data)

layout = dict(legend=dict(orientation="h",yanchor="bottom",y=1.02,xanchor="right",x=1),
              mapbox= dict(accesstoken=mapboxtoken,
                           center=dict(lat=52.1436,lon=5.383),
                           style="dark", 
                           zoom=6.25)
            )


fig_dict["layout"] = layout    

The layers are created like so (in the same loop as the traces):

        R = stand.iloc[0]['Distance in KM']/66.8
        R2 = (3/5)*R
        
        center_lon = dft.iloc[0,9]
        center_lat = dft.iloc[0,8]
        t = np.linspace(0, 2*pi, 100)
        circle_lon =center_lon + R*cos(t)
        circle_lat =center_lat +  R2*sin(t)
        
        
        coords=[]
        for lo, la in zip(list(circle_lon), list(circle_lat)):
            coords.append([lo, la]) 
        
        layer=dict(sourcetype = 'geojson',
                      source={ "type": "Feature",
                              "geometry": {"type": "LineString",
                                          "coordinates": coords
                                          }
                            },
                      templateitemname=dft.iloc[0,2],
                      visible=True,
                      color=  dft.iloc[0,13],
                      type = 'fill',   
                      opacity=0.5,
                      fill = dict(outlinecolor= 'white')
                    )
        layers.append(layer)

Finally the updatemenus part is structured like this (based on multiple traces created in the loop):

fig_dict["layout"]["updatemenus"] = [dict(
            active=0,
            buttons=list([
                dict(label="Q3",
                     method="update",
                     args=[{"visible": [True, False],
                           {'title':'test1','mapbox':{'layers':layer1}}
                           ]),
                dict(label="Q4",
                     method="update",
                     args=[{"visible": [False, True],
                            {'title':'test2','mapbox':{'layers':layer2}}
                           ])
                    ])
                    )]

fig = go.Figure(fig_dict)

I have tried a lot of different ways to structure "updatemenus", but to no succes. The above is my last try. Like I said, I have cut down my original code by a bit to keep it a little more tidy (I'm quite new to this), but the essentials are the same. If it's better to have the full code, I will provide it!

Edit:

The data looks as followed:

I have one dataset through which the script loops to create traces for all the individual locations on the map. Each ID (approx. 3k) has its own dot on the map. The collor corresponds to the location (dft):

    id  Location    lat lon
0   1   Leeuwarden  53.3238509  6.864812147
1   2   Leeuwarden  53.20375983 5.806213178
2   3   Enschede    52.28652282 6.845358477
3   4   Leeuwarden  53.18575506 5.833720622
4   5   Breda   51.81459037 5.851585979
5   6   Leeuwarden  53.20965451 5.777285517
6   7   Groningen   53.32373805 6.695864432
7   8   Leeuwarden  52.95470465 5.919419689
8   9   Leeuwarden  52.82104961 5.96997389
9   10  Enschede    52.50522699 6.612134113
10  11  Leeuwarden  53.15089124 5.844570929

Then for each location a layer is created on the map based on this dataset (stand):

    Location    lon lat Average distance in KM
0   Breda       4.234844574 51.45673726 15.1
1   Enschede    6.123570731 52.23459517 71.3
2   Groningen   6.234960418 53.12302082 22.2
3   Leeuwarden  5.795739613 53.19886105 30.0

Solution

    • from sample data and code, have generated figure. No where in data do I find quarter as implied in updatemenus
    • have used geopandas to help with calculating geometry for circles
    • from this have created a dict which are the circle polygons by Location
    • finally updatemenus I did find you cannot just change the layers, it needs to be complete mapbox setup. i.e. style, zoom, center and layers
    import numpy as np
    import pandas as pd
    import geopandas as gpd
    import requests, io
    import plotly.express as px
    
    dft = pd.read_csv(io.StringIO("""    id  Location    lat lon
    0   1   Leeuwarden  53.3238509  6.864812147
    1   2   Leeuwarden  53.20375983 5.806213178
    2   3   Enschede    52.28652282 6.845358477
    3   4   Leeuwarden  53.18575506 5.833720622
    4   5   Breda   51.81459037 5.851585979
    5   6   Leeuwarden  53.20965451 5.777285517
    6   7   Groningen   53.32373805 6.695864432
    7   8   Leeuwarden  52.95470465 5.919419689
    8   9   Leeuwarden  52.82104961 5.96997389
    9   10  Enschede    52.50522699 6.612134113
    10  11  Leeuwarden  53.15089124 5.844570929"""), sep="\s+")
    
    
    stock = pd.read_csv(
        io.StringIO(
            """    Location    lon lat "Average distance in KM"
    0   Breda       4.234844574 51.45673726 15.1
    1   Enschede    6.123570731 52.23459517 71.3
    2   Groningen   6.234960418 53.12302082 22.2
    3   Leeuwarden  5.795739613 53.19886105 30.0"""
        ),
        sep="\s+",
    )
    
    # calculate geometry of areas with appropriate radius
    stock = gpd.GeoDataFrame(
        stock, geometry=gpd.points_from_xy(stock["lon"], stock["lat"]), crs="epsg:4326"
    )
    
    utm = stock.estimate_utm_crs()
    stock = gpd.GeoDataFrame(
        stock,
        geometry=stock.to_crs(utm).apply(
            lambda r: r["geometry"].buffer(r["Average distance in KM"] * 1000), axis=1
        ),
        crs=utm,
    ).to_crs("epsg:4326")
    
    
    
    # create scatter of points
    fig = px.scatter_mapbox(
        dft, lat="lat", lon="lon", color="Location"
    ).update_layout(
        mapbox={"style": "carto-positron", "zoom": 5},
        margin={"t": 25, "b": 0, "l": 0, "r": 0},
    )
    
    
    # build a dict of layers that corresponds to traces
    layers = {
        t.name: {
            "source": stock.loc[stock["Location"].eq(t.name), "geometry"].__geo_interface__,
            "type": "line",
            "color": t.marker.color,
        }
        for t in fig.data
    }
    
    # # add circles as layers...
    fig = fig.update_layout(mapbox_layers=list(layers.values()))
    
    # # finally build menus...
    fig.update_layout(
        updatemenus=[
            {
                "buttons": [
                    {
                        "label": q,
                        "method": "update",
                        "args": [
                            {"visible": [t2.name == q or q == "All" for t2 in fig.data]},
                            {
                                "title": q,
                                "mapbox": {
                                    "style": fig.layout.mapbox.style,
                                    "center": fig.layout.mapbox.center,
                                    "zoom": fig.layout.mapbox.zoom,
                                    "layers": [layers[q]]
                                    if q != "All"
                                    else list(layers.values()),
                                },
                            },
                        ],
                    }
                    for q in stock["Location"].unique().tolist() + ["All"]
                ],
            }
        ]
    )
    fig
    

    Location selected

    enter image description here

    all selected

    enter image description here