pythonplotlymapboxplotly-dashmapbox-marker

Toggle geometry layer within plotly dash mapbox


I've used the following post to plot maki symbols over a plotly mapbox.

Plotly Mapbox Markers not rendering (other than circle)

import dash
from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import plotly.express as px
import plotly.graph_objs as go
import numpy as np
import requests
import svgpath2mpl, shapely.geometry, shapely.affinity
from pathlib import Path
from zipfile import ZipFile
import pandas as pd
import geopandas as gpd
import json

# download maki icons
# https://github.com/mapbox/maki/tree/main/icons
f = Path.cwd().joinpath("maki")
if not f.is_dir():
    f.mkdir()
f = f.joinpath("maki.zip")
if not f.exists():
    r = requests.get("https://github.com/mapbox/maki/zipball/main")
    with open(f, "wb") as f:
        for chunk in r.iter_content(chunk_size=128):
            f.write(chunk)

fz = ZipFile(f)
fz.extractall(f.parent)

def to_shapely(mpl, simplify=0):
    p = shapely.geometry.MultiPolygon([shapely.geometry.Polygon(a).simplify(simplify) for a in mpl])
    p = shapely.affinity.affine_transform(p,[1, 0, 0, -1, 0, 0],)
    p = shapely.affinity.affine_transform(p,[1, 0, 0, 1, -p.centroid.x, -p.centroid.y],)
    return p

# convert SVG icons to matplolib geometries and then into shapely geometries
# keep icons in dataframe for further access...
SIMPLIFY=.1
dfi = pd.concat(
    [
        pd.read_xml(sf).assign(
            name=sf.stem,
            mpl=lambda d: d["d"].apply(
                lambda p: svgpath2mpl.parse_path(p).to_polygons()
            ),
            shapely=lambda d: d["mpl"].apply(lambda p: to_shapely(p, simplify=SIMPLIFY)),
        )
        for sf in f.parent.glob("**/*.svg")
    ]
).set_index("name")

# build a geojson layer that can be used in plotly mapbox figure layout
def marker(df, marker="marker", size=1, color="green", lat=51.379997, lon=-0.406042):
    m = df.loc[marker, "shapely"]
    if isinstance(lat, float):
        gs = gpd.GeoSeries(
            [shapely.affinity.affine_transform(m, [size, 0, 0, size, lon, lat])]
        )
    elif isinstance(lat, (list, pd.Series, np.ndarray)):
        gs = gpd.GeoSeries(
            [
                shapely.affinity.affine_transform(m, [size, 0, 0, size, lonm, latm])
                for latm, lonm in zip(lat, lon)
            ]
        )
    return {"source":json.loads(gs.to_json()), "type":"fill", "color":color}

This works fine when plotting straight onto the map using the method outlined in the post. But I want to include a component that allows the user to toggle these symbols off and on. I'm trying to append them to a single layer and using that to update the layout. Is it possible to do so?

us_cities = pd.read_csv(
    'https://raw.githubusercontent.com/plotly/datasets/master/us-cities-top-1k.csv'
)

external_stylesheets = [dbc.themes.SPACELAB, dbc.icons.BOOTSTRAP]

app = dash.Dash(__name__, external_stylesheets = external_stylesheets)

app.layout = html.Div([

    dcc.Checklist(
            id="symbol_on",
            options=[{"label": "Symbol", "value": True}],
            value=[],
            inline=True
            ),
             
    html.Div([
        dcc.Graph(id="the_graph")
    ]),

])

@app.callback(
    Output("the_graph", "figure"),
    Input('symbol_on', 'value')
)

def update_graph(symbol_on):

    fig = go.Figure()

    scatter = px.scatter_mapbox(data_frame = us_cities, 
                               lat = 'lat', 
                               lon = 'lon',
                               zoom = 0,
                               hover_data = ['State', 'lat', 'lon']
                               )

    fig.add_traces(list(scatter.select_traces()))

    fig.update_layout(
        height = 750,
        mapbox=dict(
        style='carto-positron',
        ),
    )

    star = marker(
                dfi, "star", size=.1, color="red", lon=[-70, -80, -90], lat=[30, 40, 45]
            ),

    airport = marker(
                dfi, "airport", size=.1, color="green", lon=[-70, -80, -90], lat=[30, 40, 45]
            ),

    layers = []

    for lyr in symbol_on:
        layers.append(star)
        layers.append(airport)

    fig.update_layout(mapbox={"layers": layers})

    return fig

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

Solution

  • The issue is the trailing commas:

    star = marker(
                    dfi, "star", size=.1, color="red", lon=[-70, -80, -90], lat=[30, 40, 45]
                ), 
    
        airport = marker(
                    dfi, "airport", size=.1, color="green", lon=[-70, -80, -90], lat=[30, 40, 45]
                ), 
    

    This causes both star and airport to be a tuple instead of a dict, and breaks the creation of the mapbox layer. If we remove the commas (and offset the airport markers slightly, just so that they are distinct from the star markers), the app renders correctly:

    star = marker(
                    dfi, "star", size=.1, color="red", lon=[-70, -80, -90], lat=[30, 40, 45]
                )
    
        airport = marker(
                    dfi, "airport", size=.1, color="green", lon=[-71, -81, -91], lat=[31, 41, 41]
                )
    

    enter image description here