Build a Multi-Layer Map Using Streamlit
By: Alex Wagner
Tags: Streamlit, Geopandas, Map Layers, Geospatial, Plotly

Build a Multi-Layer Map Using Streamlit

Image by Author

“A map says to you, ‘Read me carefully, follow me closely, doubt me not.’ It says, ‘I am the earth in the palm of your hand. Without me, you are alone and lost.”

― 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 . . .

While these libraries are incredibly useful, actions like layer selection are not so straight forward; especially if you are used to a traditional geospatial software.

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:

Vocabulary

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 Libraries

import streamlit as st

import json

import geopandas as gpd

import pyproj

import 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.

Code

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 shapefile

polygon = 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:

GeoPandas Data Frame

Now let’s create and new object and use that to apply a specific coordinate reference system (CRS) to our data:

# project GeoPandas dataframe

map_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 shapefile

points = gpd.read_file(r"\Downloads\USA_Major_Cities.shp") # project GeoPandas dataframe

points.to_crs(pyproj.CRS.from_epsg(4326), inplace=True) # define lat, long for points

Lat = 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 path

path = r"C:\Users\project\geojson.json" # write GeoJSON to file

map_df.to_file(path, driver = "GeoJSON")

with open(path) as geofile:

j_file = json.load(geofile) # index geojson

i=1

for 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 token

mapboxt = 'MapBox Token'



# define layers and plot map

choro = 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 widget

layer1 = st.multiselect('Layer Selection', [choro, scatt],

format_func=lambda x: 'Polygon' if x==choro else 'Points') # assign Graph Objects figure

fig = go.Figure(data=layer1, layout=layout) # display streamlit map

st.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.

Initial Layer Selection

Polygon Layer Selection

Polygon and Point Layer Selection

Further Exploration

A great introduction to Streamlit:

2) If you prefer Dash for web apps:

3) What Makes Spatial Data Special: