Search code examples
pythonplotly-python

Python/Plotly: How to annotate geojson layers in a scattermapbox


I'm trying to plot data from two pandas data frames in a Mapbox map using Python/Plotly while also including annotations for both datasets. The first dataset contains points with an associated value and I'm able to plot them using px.scatter_mapbox which automatically provides hovertext annotations for the points.

The second dataset contains polygons and I'm able to underlay them on the first dataset using update_layout. However, I'd also like to label the polygons. Ideally, I'd like to have an annotation associated with each polygon that appears when it is clicked on, while also having the hovertext labels appear for the location in the first dataset that is closest to the cursor. The example code below displays both datasets, along with the hovertext for the location dataset, but does not include a way to label the polygons with the cur_name variable.

Any suggestions would be appreciated.

layer_list = []
for idx,row in polygon_df.iterrows():
    cur_name = row['Name']
    cur_geom = row['Polygon']
    cur_source = json.loads(gpd.GeoSeries(cur_geom).to_json())
    layer_list.append({'name': str(cur_name),
                       'sourcetype': 'geojson',
                       'source': cur_source,
                       'type': "fill",
                       'below': "traces",
                       'opacity':0.2,
                       'color': "red"})

fig = px.scatter_mapbox(location_df,
                        lat="Latitude",
                        lon="Longitude",
                        color="Value",
                        opacity=0.6,
                        zoom=8,
                        mapbox_style="carto-positron")

fig.update_layout(mapbox = {'layers': layer_list})

fig.update_layout(
    margin=dict(l=20, r=20, t=20, b=20),
    paper_bgcolor="LightSteelBlue",
)

fig.show()

For clarification, here is a self-contained example of the current state of the code:

import plotly.express as px
import numpy as np
import pandas as pd
import geojson
import geopandas as gpd
import json
from io import StringIO
import shapely.wkt

location_csv = """
Latitude,Longitude,Value
33.4484110068466,-112.06200256059738,1.643
33.42952535283222,-112.06512832474168,3.531
33.43606430916848,-112.09102376100617,5.347
33.44372793070293,-112.01068625893538,8.424
"""
polygon_csv = """
Name,PolygonString
Example1,"MULTIPOLYGON(((-112.087104 33.435576,-112.089254 33.435576,-112.090329 33.436477,-112.090329 33.43828,-112.088179 33.440084,-112.090329 33.441887,-112.089254 33.442788,-112.084954 33.442788,-112.082803 33.444592,-112.080653 33.442788,-112.075278 33.447297,-112.077428 33.4491,-112.075278 33.450903,-112.075278 33.458116,-112.073128 33.459919,-112.075278 33.461722,-112.074203 33.462624,-112.069903 33.462624,-112.068828 33.463526,-112.068828 33.465329,-112.066678 33.467132,-112.066678 33.468935,-112.065602 33.469837,-112.064527 33.468935,-112.064527 33.467132,-112.063452 33.466231,-112.061302 33.466231,-112.059152 33.468034,-112.054852 33.468034,-112.050552 33.464427,-112.048401 33.464427,-112.047326 33.463526,-112.047326 33.461722,-112.041951 33.457214,-112.037651 33.457214,-112.033351 33.453608,-112.0312 33.453608,-112.027975 33.450903,-112.02905 33.450001,-112.0312 33.450001,-112.032276 33.4491,-112.0312 33.448198,-112.0226 33.448198,-112.021525 33.447297,-112.0226 33.446395,-112.0269 33.446395,-112.027975 33.445493,-112.025825 33.44369,-112.0269 33.442788,-112.0312 33.442788,-112.032276 33.441887,-112.030125 33.440084,-112.030125 33.43828,-112.034426 33.434674,-112.034426 33.431067,-112.035501 33.430166,-112.036576 33.431067,-112.036576 33.432871,-112.037651 33.433772,-112.038726 33.432871,-112.038726 33.431067,-112.039801 33.430166,-112.041951 33.430166,-112.043026 33.429264,-112.043026 33.427461,-112.044101 33.426559,-112.050552 33.426559,-112.051627 33.427461,-112.051627 33.429264,-112.054852 33.431969,-112.057002 33.431969,-112.059152 33.430166,-112.063452 33.430166,-112.064527 33.429264,-112.064527 33.427461,-112.062377 33.425658,-112.063452 33.424756,-112.067753 33.424756,-112.068828 33.425658,-112.068828 33.427461,-112.069903 33.428363,-112.074203 33.428363,-112.078503 33.431969,-112.080653 33.431969,-112.082803 33.433772,-112.084954 33.433772,-112.087104 33.435576)),((-112.0269 33.435576,-112.027975 33.436477,-112.02475 33.439182,-112.0226 33.437379,-112.02045 33.439182,-112.01615 33.439182,-112.013999 33.437379,-112.011849 33.437379,-112.010774 33.436477,-112.011849 33.435576,-112.0183 33.435576,-112.02045 33.437379,-112.0226 33.435576,-112.0269 33.435576)))"
Example2,"MULTIPOLYGON(((-112.118214 33.437402,-112.116064 33.437402,-112.113914 33.439205,-112.111764 33.437402,-112.109614 33.437402,-112.108539 33.438303,-112.108539 33.445516,-112.107464 33.446418,-112.106389 33.445516,-112.106389 33.443713,-112.105314 33.442811,-112.103164 33.442811,-112.102089 33.443713,-112.104239 33.445516,-112.102089 33.447319,-112.102089 33.449123,-112.101014 33.450024,-112.096714 33.450024,-112.095639 33.450926,-112.095639 33.452729,-112.094564 33.453631,-112.092414 33.453631,-112.091339 33.454532,-112.091339 33.456336,-112.093489 33.458139,-112.093489 33.459942,-112.095639 33.461745,-112.094564 33.462647,-112.090264 33.45904,-112.079514 33.45904,-112.078439 33.458139,-112.078439 33.456336,-112.077364 33.455434,-112.075214 33.455434,-112.074139 33.456336,-112.074139 33.458139,-112.073063 33.45904,-112.071988 33.458139,-112.071988 33.456336,-112.070913 33.455434,-112.069838 33.456336,-112.069838 33.459942,-112.068763 33.460844,-112.066613 33.45904,-112.064463 33.460844,-112.062313 33.45904,-112.060163 33.45904,-112.059088 33.458139,-112.059088 33.456336,-112.058013 33.455434,-112.055863 33.455434,-112.047263 33.448221,-112.040813 33.448221,-112.039738 33.447319,-112.042963 33.444615,-112.047263 33.444615,-112.048338 33.443713,-112.047263 33.442811,-112.045113 33.442811,-112.044038 33.44191,-112.046188 33.440107,-112.046188 33.438303,-112.042963 33.435598,-112.040813 33.437402,-112.038663 33.437402,-112.037588 33.4365,-112.038663 33.435598,-112.040813 33.435598,-112.042963 33.433795,-112.047263 33.433795,-112.048338 33.432894,-112.046188 33.43109,-112.046188 33.429287,-112.044038 33.427484,-112.044038 33.425681,-112.045113 33.424779,-112.047263 33.424779,-112.049413 33.422976,-112.051563 33.424779,-112.053713 33.424779,-112.054788 33.423877,-112.054788 33.422074,-112.055863 33.421173,-112.062313 33.421173,-112.064463 33.422976,-112.066613 33.421173,-112.068763 33.422976,-112.070913 33.422976,-112.073063 33.424779,-112.075214 33.422976,-112.081664 33.422976,-112.083814 33.421173,-112.085964 33.422976,-112.088114 33.422976,-112.090264 33.421173,-112.094564 33.421173,-112.095639 33.422074,-112.095639 33.423877,-112.096714 33.424779,-112.097789 33.423877,-112.097789 33.422074,-112.098864 33.421173,-112.103164 33.421173,-112.104239 33.422074,-112.102089 33.423877,-112.102089 33.427484,-112.103164 33.428385,-112.111764 33.428385,-112.112839 33.429287,-112.111764 33.430189,-112.109614 33.430189,-112.108539 33.43109,-112.108539 33.434697,-112.109614 33.435598,-112.118214 33.435598,-112.119289 33.4365,-112.118214 33.437402)),((-112.077364 33.412156,-112.078439 33.413058,-112.078439 33.414861,-112.076289 33.416664,-112.076289 33.420271,-112.075214 33.421173,-112.074139 33.420271,-112.074139 33.418468,-112.073063 33.417566,-112.070913 33.419369,-112.067688 33.416664,-112.067688 33.414861,-112.068763 33.41396,-112.070913 33.41396,-112.071988 33.413058,-112.069838 33.411255,-112.071988 33.409452,-112.071988 33.407648,-112.073063 33.406747,-112.074139 33.407648,-112.074139 33.411255,-112.075214 33.412156,-112.077364 33.412156)))"
"""
location_df = pd.read_csv(StringIO(location_csv))
polygon_df = pd.read_csv(StringIO(polygon_csv))
polygon_df['Polygon'] = polygon_df['PolygonString'].apply(shapely.wkt.loads)

layer_list = []
for idx,row in polygon_df.iterrows():
    cur_name = row['Name']
    cur_geom = row['Polygon']
    cur_source = json.loads(gpd.GeoSeries(cur_geom).to_json())
    layer_list.append({'name': str(cur_name),
                       'sourcetype': 'geojson',
                       'source': cur_source,
                       'type': "fill",
                       'below': "traces",
                       'opacity':0.2,
                       'color': "red"})

fig = px.scatter_mapbox(location_df,
                        lat="Latitude",
                        lon="Longitude",
                        color="Value",
                        opacity=0.6,
                        zoom=12,
                        mapbox_style="carto-positron")

fig.update_layout(mapbox = {'layers': layer_list})

fig.update_layout(
    margin=dict(l=20, r=20, t=20, b=20),
    paper_bgcolor="LightSteelBlue",
)

fig.show()

which generates the following:

Example Screenshot


Solution

  • You can add your polygons as choropleths so that they are interactive. It's not quite the same as layers as you cannot implement opacity in same way. The hover text will be at center of polygon / multi-polygon. Have looked at exploding multi-polygons as well so text is nearer pointer.

    import plotly.express as px
    import numpy as np
    import pandas as pd
    import plotly.graph_objects as go
    import geopandas as gpd
    import json
    from io import StringIO
    import shapely.wkt
    
    location_csv = """
    Latitude,Longitude,Value
    33.4484110068466,-112.06200256059738,1.643
    33.42952535283222,-112.06512832474168,3.531
    33.43606430916848,-112.09102376100617,5.347
    33.44372793070293,-112.01068625893538,8.424
    """
    polygon_csv = """
    Name,PolygonString
    Example1,"MULTIPOLYGON(((-112.087104 33.435576,-112.089254 33.435576,-112.090329 33.436477,-112.090329 33.43828,-112.088179 33.440084,-112.090329 33.441887,-112.089254 33.442788,-112.084954 33.442788,-112.082803 33.444592,-112.080653 33.442788,-112.075278 33.447297,-112.077428 33.4491,-112.075278 33.450903,-112.075278 33.458116,-112.073128 33.459919,-112.075278 33.461722,-112.074203 33.462624,-112.069903 33.462624,-112.068828 33.463526,-112.068828 33.465329,-112.066678 33.467132,-112.066678 33.468935,-112.065602 33.469837,-112.064527 33.468935,-112.064527 33.467132,-112.063452 33.466231,-112.061302 33.466231,-112.059152 33.468034,-112.054852 33.468034,-112.050552 33.464427,-112.048401 33.464427,-112.047326 33.463526,-112.047326 33.461722,-112.041951 33.457214,-112.037651 33.457214,-112.033351 33.453608,-112.0312 33.453608,-112.027975 33.450903,-112.02905 33.450001,-112.0312 33.450001,-112.032276 33.4491,-112.0312 33.448198,-112.0226 33.448198,-112.021525 33.447297,-112.0226 33.446395,-112.0269 33.446395,-112.027975 33.445493,-112.025825 33.44369,-112.0269 33.442788,-112.0312 33.442788,-112.032276 33.441887,-112.030125 33.440084,-112.030125 33.43828,-112.034426 33.434674,-112.034426 33.431067,-112.035501 33.430166,-112.036576 33.431067,-112.036576 33.432871,-112.037651 33.433772,-112.038726 33.432871,-112.038726 33.431067,-112.039801 33.430166,-112.041951 33.430166,-112.043026 33.429264,-112.043026 33.427461,-112.044101 33.426559,-112.050552 33.426559,-112.051627 33.427461,-112.051627 33.429264,-112.054852 33.431969,-112.057002 33.431969,-112.059152 33.430166,-112.063452 33.430166,-112.064527 33.429264,-112.064527 33.427461,-112.062377 33.425658,-112.063452 33.424756,-112.067753 33.424756,-112.068828 33.425658,-112.068828 33.427461,-112.069903 33.428363,-112.074203 33.428363,-112.078503 33.431969,-112.080653 33.431969,-112.082803 33.433772,-112.084954 33.433772,-112.087104 33.435576)),((-112.0269 33.435576,-112.027975 33.436477,-112.02475 33.439182,-112.0226 33.437379,-112.02045 33.439182,-112.01615 33.439182,-112.013999 33.437379,-112.011849 33.437379,-112.010774 33.436477,-112.011849 33.435576,-112.0183 33.435576,-112.02045 33.437379,-112.0226 33.435576,-112.0269 33.435576)))"
    Example2,"MULTIPOLYGON(((-112.118214 33.437402,-112.116064 33.437402,-112.113914 33.439205,-112.111764 33.437402,-112.109614 33.437402,-112.108539 33.438303,-112.108539 33.445516,-112.107464 33.446418,-112.106389 33.445516,-112.106389 33.443713,-112.105314 33.442811,-112.103164 33.442811,-112.102089 33.443713,-112.104239 33.445516,-112.102089 33.447319,-112.102089 33.449123,-112.101014 33.450024,-112.096714 33.450024,-112.095639 33.450926,-112.095639 33.452729,-112.094564 33.453631,-112.092414 33.453631,-112.091339 33.454532,-112.091339 33.456336,-112.093489 33.458139,-112.093489 33.459942,-112.095639 33.461745,-112.094564 33.462647,-112.090264 33.45904,-112.079514 33.45904,-112.078439 33.458139,-112.078439 33.456336,-112.077364 33.455434,-112.075214 33.455434,-112.074139 33.456336,-112.074139 33.458139,-112.073063 33.45904,-112.071988 33.458139,-112.071988 33.456336,-112.070913 33.455434,-112.069838 33.456336,-112.069838 33.459942,-112.068763 33.460844,-112.066613 33.45904,-112.064463 33.460844,-112.062313 33.45904,-112.060163 33.45904,-112.059088 33.458139,-112.059088 33.456336,-112.058013 33.455434,-112.055863 33.455434,-112.047263 33.448221,-112.040813 33.448221,-112.039738 33.447319,-112.042963 33.444615,-112.047263 33.444615,-112.048338 33.443713,-112.047263 33.442811,-112.045113 33.442811,-112.044038 33.44191,-112.046188 33.440107,-112.046188 33.438303,-112.042963 33.435598,-112.040813 33.437402,-112.038663 33.437402,-112.037588 33.4365,-112.038663 33.435598,-112.040813 33.435598,-112.042963 33.433795,-112.047263 33.433795,-112.048338 33.432894,-112.046188 33.43109,-112.046188 33.429287,-112.044038 33.427484,-112.044038 33.425681,-112.045113 33.424779,-112.047263 33.424779,-112.049413 33.422976,-112.051563 33.424779,-112.053713 33.424779,-112.054788 33.423877,-112.054788 33.422074,-112.055863 33.421173,-112.062313 33.421173,-112.064463 33.422976,-112.066613 33.421173,-112.068763 33.422976,-112.070913 33.422976,-112.073063 33.424779,-112.075214 33.422976,-112.081664 33.422976,-112.083814 33.421173,-112.085964 33.422976,-112.088114 33.422976,-112.090264 33.421173,-112.094564 33.421173,-112.095639 33.422074,-112.095639 33.423877,-112.096714 33.424779,-112.097789 33.423877,-112.097789 33.422074,-112.098864 33.421173,-112.103164 33.421173,-112.104239 33.422074,-112.102089 33.423877,-112.102089 33.427484,-112.103164 33.428385,-112.111764 33.428385,-112.112839 33.429287,-112.111764 33.430189,-112.109614 33.430189,-112.108539 33.43109,-112.108539 33.434697,-112.109614 33.435598,-112.118214 33.435598,-112.119289 33.4365,-112.118214 33.437402)),((-112.077364 33.412156,-112.078439 33.413058,-112.078439 33.414861,-112.076289 33.416664,-112.076289 33.420271,-112.075214 33.421173,-112.074139 33.420271,-112.074139 33.418468,-112.073063 33.417566,-112.070913 33.419369,-112.067688 33.416664,-112.067688 33.414861,-112.068763 33.41396,-112.070913 33.41396,-112.071988 33.413058,-112.069838 33.411255,-112.071988 33.409452,-112.071988 33.407648,-112.073063 33.406747,-112.074139 33.407648,-112.074139 33.411255,-112.075214 33.412156,-112.077364 33.412156)))"
    """
    location_df = pd.read_csv(StringIO(location_csv))
    polygon_df = pd.read_csv(StringIO(polygon_csv))
    polygon_df["Polygon"] = polygon_df["PolygonString"].apply(shapely.wkt.loads)
    
    # polygon "layers"
    traces = []
    for i, (n, d) in enumerate(polygon_df.groupby("Name")):
        gs = gpd.GeoSeries(d["Polygon"].apply(lambda g: g.geoms).explode()).reset_index(drop=True)
        # gs = gpd.GeoSeries(d["Polygon"])
    
        traces.append(
            go.Choroplethmapbox(
                name=n,
                uid=i + 1,
                below=i,
                geojson=gs.__geo_interface__,
                locations=gs.index,
                z=np.full(len(gs),i),
                hovertemplate=f"{n}<extra></extra>",
                showscale=True,
                coloraxis="coloraxis2",
            )
        )
    
    fig = px.scatter_mapbox(
        location_df,
        lat="Latitude",
        lon="Longitude",
        color="Value",
        opacity=0.6,
        zoom=12,
        mapbox_style="carto-positron",
    ).update_traces(uid=0, below="")
    
    fig.add_traces(traces)
    
    fig.update_layout(
        margin=dict(l=20, r=20, t=20, b=20),
        paper_bgcolor="LightSteelBlue",
        coloraxis2={"colorscale": [[0, "rgb(255,200,200)"], [1, "rgb(255,250,250)"]], "showscale": False},
    )
    # re-order traces so scatter is at top
    fig.data = fig.data[::-1]
    fig.show()