Build a Multi-Layer Map Using Streamlit | by Alex Wagner | Towards Data Science

― Beryl Markham, West With the Night
When it comes to visualizing data, everyone loves a good map. I think that is because a map makes us feel we are a part of the story being told. It gives us a space we can share with data and makes for a richer and more powerful message.
Thankfully, working with maps has become more democratized as open-source libraries have extended the reach of more traditional geospatial software like ArcMap and QGIS.
In addition, web-application libraries like Streamlit and Dash allow maps to be easily displayed through a web browser.
However . . .
This tutorial outlines an easy process to add selectable point and polygon layers to a Streamlit project. In my experience, giving a user the ability to add and subtract layers as needed adds alot to their experience.
Our map will be a simple one of the United States with major cities represented as points and the cartographic boundaries of 500 US cities represented as polygons.
Before we start it will be helpful to establish some vocabulary encountered in this tutorial:
Layer: If you are coming from the world of Plotly/Dash this translates to a “trace”. Layers are spatial data sets that are literally ‘layered’ on one another to build complexity into a map
Vector data: Points, lines, or polygons. Also know as “not raster” data
Point: A spatial feature with a single x,y coordinate
Line: A spatial feature connecting at least two x,y coordinates
Polygon: A spatial feature comprised of at least three connected and closed vertices
CRS (Coordinate Reference System): A set of attributes that tells a software where to position your data in space as well which mathematical model to use to flatten the globe into a 2D surface. Spatial data is not spatial without the CRS
import streamlit as stimport jsonimport geopandas as gpdimport pyprojimport plotly.graph_objs as go
A quick note: If you are using an Anaconda install of python, I would recommend creating a new environment in which to import GeoPandas. It is an incredibly useful spatial library and it has a significant number of version-specific dependencies. I recommend doing a clean install of this first and then installing the rest of the packages into that environment.
Our first step is to read in our data. For this project I have uploaded point and polygon data to the project github page that you can download locally here: https://github.com/wagneaj/mapLayers.
Let’s start with the polygon data:
# reading in the polygon shapefilepolygon = gpd.read_file(r"\Downloads\CityBoundaries.shp")
Here we used GeoPandas to read in our first bit of data. This will now hold our data as a GeoDataFrame object that you can call and manipulate much as you would a Pandas DataFrame Object. The ‘.shp’ file type indicates a shapefile which is really a set of files that help to define both the spatial and tabular information of the data. Now we can view the tabular data in a Jupyter Notebook if we wish:
Now let’s create and new object and use that to apply a specific coordinate reference system (CRS) to our data:
# project GeoPandas dataframemap_df = polygon map_df.to_crs(pyproj.CRS.from_epsg(4326), inplace=True)
‘epsg 4326’ is a coded CRS within the European Petroleum Survey Group (EPSG). It maps to the WGS 84 CRS.
Next, we will do the same thing for our point data.
# reading in the points shapefilepoints = gpd.read_file(r"\Downloads\USA_Major_Cities.shp")# project GeoPandas dataframepoints.to_crs(pyproj.CRS.from_epsg(4326), inplace=True)# define lat, long for pointsLat = points['Lat']Long = points['Long']
One key difference is the additional step of designating which columns in our data set correspond to the latitude and longitude of the points. In this case, the columns are named ‘Lat’ and ‘Long’, respectively.
Once we have all our data read in and in the correct CRS we will need to covert the polygon data set into a GeoJSON file. This GeoJSON will be used as an argument when we call our Plotly Graph Objects library.
If you are unfamiliar, GeoJSON is an open standard file format specifically used to represent spatial data. It is based on the JSON format and is how we will communicate spatial references for our polygon data with Plotly.
# set GeoJSON file pathpath = r"C:\Users\project\geojson.json"# write GeoJSON to filemap_df.to_file(path, driver = "GeoJSON")   with open(path) as geofile:   j_file = json.load(geofile)# index geojsoni=1for feature in j_file["features"]:   feature ['id'] = str(i).zfill(2)   i += 1
Here, we have set a path to export the GeoJSON ; we have called the file back into our analysis; and then we indexed each record of the file.
After we have created and indexed the GeoJSON we have one more step before we put everything together and that is to generate a mapbox token.
Mapbox provides online maps which can be accessed via an API using a mapbox token. I have generated my own token but it is a fairly painless process to do so and directions can be found here: https://docs.mapbox.com/help/glossary/access-token
With a mapbox token generated we can now begin to put our map layers together.
# mapbox tokenmapboxt = 'MapBox Token' # define layers and plot mapchoro = go.Choroplethmapbox(z=map_df['STFIPS'], locations =          map_df.index, colorscale = 'Viridis', geojson = j_file,         text = map_df['NAME'], marker_line_width=0.1) scatt = go.Scattermapbox(lat=Lat, lon=Long,mode='markers+text',            below='False', marker=dict( size=12, color ='rgb(56, 44,         100)'))layout = go.Layout(title_text ='USA Cities', title_x =0.5,           width=950, height=700,mapbox = dict(center= dict(lat=37,           lon=-95),accesstoken= mapboxt, zoom=4,style="stamen-           terrain"))
Here we have defined our choropleth map layer, our scatterplot map layer, and our layout all using Plotly Graph Objects. Graph Objects are dictionary-like structures that can used to conveniently store and call different Plotly figures.
Lastly, we create a Streamlit multiselect widget for layer selection; we assign our Graph Objects figure; and we make a Streamlit call to display the final map.
# streamlit multiselect widgetlayer1 = st.multiselect('Layer Selection', [choro, scatt],          format_func=lambda x: 'Polygon' if x==choro else 'Points')# assign Graph Objects figurefig = go.Figure(data=layer1, layout=layout)# display streamlit mapst.plotly_chart(fig)
That’s it! Now you have the structure to add and subtract layers to a multi-layer web map. Screenshots of the finished product are below.
A great introduction to Streamlit:
A great introduction to Streamlit:
towardsdatascience.com
2) If you prefer Dash for web apps:
plotly.com
3) What Makes Spatial Data Special: