Help plotting spatial polylines read from polyline shapefile

I’ve been trying to figure out how to plot shapefiles using plotly. I think I’ve figured out how to polygon and point shapefiles. But I’m still stuck on polylines.

The following code seems to plot polylines properly:

# Imports
import pandas as pd
import geopandas as gpd
import plotly.express as px

# Read data
sp_roads = gpd.read_file('/vsicurl/https://github.com/Alwayz247/spdata/raw/main/Roads.shp')
sp_roads_projected = sp_roads.to_crs(4326)

# Get coordinates
df_roads = sp_roads_projected.get_coordinates().reset_index()\
    .merge(sp_roads_projected[['FULLNAME']].reset_index(), on='index')

# Plot
fig = px.line_mapbox(df_roads,
                     lon=df_roads['x'],
                     lat=df_roads['y'],
                     line_group=df_roads['index'],
                     hover_name=df_roads['FULLNAME'],
                     mapbox_style="open-street-map",
                     center={"lat": 37.0902, "lon": -95.7129},
                     zoom=2)
fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
fig.show()

But if I want to add a legend called “Roads” so that I can turn this layer on or off, I get a separate entry in the legends for each line group.

fig = px.line_mapbox(df_roads,
                     lon=df_roads['x'],
                     lat=df_roads['y'],
                     line_group=df_roads['index'],
                     hover_name=df_roads['FULLNAME'],
                     mapbox_style="open-street-map",
                     center={"lat": 37.0902, "lon": -95.7129},
                     zoom=2)\
                     .update_traces(visible=True,
                                    name='Roads',
                                    showlegend=True)
fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
fig.show()

It’s easy to plot polylines using cloropleth_mapbox() without the need to extract the coordinates, since I can just use the geojson argument. That does not seem possible with line_mapbox. What is the best way to plot polylines using plotly?

Ultimately, I want a plot with multiple spatial features and legends which I can use to turn each layer on or off. Something like the following. In this case, plotly only plots the first polyline feature for some reason.

# Imports
import pandas as pd
import geopandas as gpd
import plotly.express as px

# Read data
sp_capitals = gpd.read_file('/vsicurl/https://github.com/Alwayz247/spdata/raw/main/Capitals.shp')
sp_roads = gpd.read_file('/vsicurl/https://github.com/Alwayz247/spdata/raw/main/Roads.shp')
sp_states = gpd.read_file('/vsicurl/https://github.com/Alwayz247/spdata/raw/main/States.shp')
sp_capitals _projected = sp_capitals.to_crs(4326)
sp_roads_projected = sp_roads.to_crs(4326)
sp_states _projected = sp_states.to_crs(4326)

# Get coordinates
df_roads = sp_roads_projected.get_coordinates().reset_index()\
    .merge(sp_roads_projected[['FULLNAME']].reset_index(), on='index')

# Plot
fig = px.choropleth_mapbox(mapbox_style="open-street-map",
                           center={"lat": 37.0902, "lon": -95.7129},
                           zoom=2)
fig1 = px.choropleth_mapbox(sp_states_projected.eval('prop_water=AWATER/ALAND'),
                            locations=sp_states_projected.index,
                            geojson=sp_states_projected.geometry,
                            color='prop_water',
                            hover_name=sp_states_projected['NAME'])\
                            .update_traces(visible=True,
                                           name='States',
                                           showlegend=True)
fig2 = px.line_mapbox(df_roads,
                      lon=df_roads['x'],
                      lat=df_roads['y'],
                      line_group=df_roads['index'],
                      hover_name=df_roads['FULLNAME'])\
                      .update_traces(visible=True,
                                     name='Roads',
                                     showlegend=True)
fig3 = px.scatter_mapbox(sp_capitals_projected,
                         lon=sp_capitals_projected.geometry.x,
                         lat=sp_capitals_projected.geometry.y,
                         hover_name=sp_capitals_projected['name'])\
                         .update_traces(visible=True,
                                        name='State Capitals',
                                        showlegend=True)
fig.add_trace(fig1.data[0])
fig.add_trace(fig2.data[0])
fig.add_trace(fig3.data[0])
fig.update_layout(legend=dict(xanchor='right',
                               yanchor='bottom',
                               x=1,
                               y=0.1),
                   coloraxis_colorbar=dict(title='Water%',
                                           orientation='h',
                                           xanchor='left',
                                           yanchor='bottom',
                                           x=0,
                                           y=0,
                                           thickness=10,
                                           len=0.5))
fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
fig.show()

Not all requests have been met, but the issue where the line is not displayed only at the beginning is in fig2.data[0]. Since there are multiple lines in the graph, they need to be added in the loop process. Next, to show or hide the line in the legend, it is possible by setting the legend group. It is also possible to add a legend group title. However, express will not have a single legend, so you will need to deal with this differently.

fig2 = px.line_mapbox(df_roads,
                      lon=df_roads['x'],
                      lat=df_roads['y'],
                      line_group=df_roads['index'],
                      hover_name=df_roads['FULLNAME'])\
                      .update_traces(visible=True,
                                     name='Roads',
                                     legendgroup='Roads',
                                     legendgrouptitle_text='All_Roads',
                                     showlegend=True)
...
#fig.add_trace(fig1.data[0])
for i in range(len(fig2.data)):
  fig.add_trace(fig2.data[i])
fig.add_trace(fig3.data[0])

Here is the resulting graph from this modification

1 Like

Duplicate legends can be resolved by adding the following

fig2 = px.line_mapbox(df_roads,
                      lon=df_roads['x'],
                      lat=df_roads['y'],
                      line_group=df_roads['index'],
                      hover_name=df_roads['FULLNAME'])\
                      .update_traces(visible=True,
                                     name='Roads',
                                     legendgroup='Roads',
                                     legendgrouptitle_text='All_Roads',
                                     showlegend=True)
for g in range(len(fig2.data)):
  if g == 0:
    fig2.data[g].showlegend = True
  else:
    fig2.data[g].showlegend = False

1 Like

Thanks, your solutions work. I should have thought about the multiple lines leading to only the first line being added thing. Combining duplicate legends is smart. However, this means that I cannot turn the roads layer off (which was the primary reason for me to add the legend).

It might be that this is the best possible solution given the features plotly has currently. I’ll wait a bit to see if there’s a better solution. If not, I’ll choose your response as the answer and try to put in a feature request. I’m a little disappointed that something as simple as plotting a polyline shapefile is this complicated to figure out right now. There should be a geojson option similar to that in cloropleth_mapbox for polylines.

When I click on a road in the legend, the road is no longer visible on the graph, is that not what you intended?

When i tried it, it only hid the first road feature as opposed to all features. I suppose because the code only showed the legend for the first feature and hid the legend for all the rest, that’s expected behavior. This, however, differs from my experiences with other layered spatial interfaces (any GIS software, folium, etc.), where I can turn off a layer, and all polylines in that layer gets turned off. Here, instead of having a single layer with all polylines, plotly seems to create separate layers for each single polyline.

I don’t know if the code I presented was partial or you are commenting without specifying a legend group, but I will post all the code for the graphing portion. Also, this result is running plotly:5.18.0. Please check your environment too just to be sure.

fig = px.choropleth_mapbox(mapbox_style="open-street-map",
                           center={"lat": 37.0902, "lon": -95.7129},
                           zoom=2)

fig1 = px.choropleth_mapbox(sp_states_projected.eval('prop_water=AWATER/ALAND'),
                            locations=sp_states_projected.index,
                            geojson=sp_states_projected.geometry,
                            color='prop_water',
                            opacity=0.3,
                            hover_name=sp_states_projected['NAME'])\
                            .update_traces(visible=True,
                                           name='States',
                                           showlegend=True)

fig2 = px.line_mapbox(df_roads,
                      lon=df_roads['x'],
                      lat=df_roads['y'],
                      line_group=df_roads['index'],
                      hover_name=df_roads['FULLNAME'])\
                      .update_traces(visible=True,
                                     name='Roads',
                                     legendgroup='Roads',
                                     legendgrouptitle_text='All_Roads',
                                     showlegend=True)
for g in range(len(fig2.data)):
  if g == 0:
    fig2.data[g].showlegend =True
  else:
    fig2.data[g].showlegend =False

fig3 = px.scatter_mapbox(sp_capitals_projected,
                         lon=sp_capitals_projected.geometry.x,
                         lat=sp_capitals_projected.geometry.y,
                         hover_name=sp_capitals_projected['name'])\
                         .update_traces(visible=True,
                                        name='State Capitals',
                                        showlegend=True)

#fig.add_trace(fig1.data[0])
for i in range(len(fig2.data)):
  fig.add_trace(fig2.data[i])
fig.add_trace(fig3.data[0])

fig.show()
2 Likes

Thank you so much for the full script. This works perfectly. It turns out I made a mistake. I forgot to add the legendgroup=‘Roads’ line. Just one small thing, I think it looks better (at least to me) to omit the legendgrouptitle_text=‘All_Roads’ line. Everything else worked perfectly.

1 Like