Hello,
I am using Streamlit to create a Plotly Express choropleth map of counties in Georgia (USA) with shading based on the county population. I also have a slider widget above the map for the user to change the opacity of the map. Here is the full working example of the app, including URL for reading in the data:
import streamlit as st
import geopandas as gpd
import plotly.express as px
# set slider for map layer opacity
opacity = st.slider(
label='Set map layer opacity',
min_value=0.2,
max_value=1.0,
value=0.6,
step=0.1
)
# Define a function to fetch and cache the data
@st.cache_data
def fetch_data(url):
# clean the input URL
url = url.replace(" ", "%20")
# Read the GeoJSON data from the URL
gdf = gpd.read_file(url)
# Select the required columns for choropleth map
gdf = gdf[['GEOID', 'TotPop_e22', 'geometry']]
return gdf
# read in population, geometry of Georgia counties
url = "https://services1.arcgis.com/Ug5xGQbHsD8zuZzM/arcgis/rest/services/ACS 2022 Demographic Population GeoSplitJoined/FeatureServer/9/query?outFields=*&where=1%3D1&f=geojson"
gdf = fetch_data(url)
# create mapping figure
fig = px.choropleth_mapbox(
gdf,
geojson=gdf.geometry,
locations=gdf.index,
color='TotPop_e22',
center={"lat": 32.90, "lon": -83.50},
mapbox_style='carto-positron',
zoom=6,
opacity=opacity,
height=650
)
# plot the mapping figure
st.plotly_chart(
fig,
theme='streamlit',
use_container_width=True
)
The app works fine, but I can’t figure out why the map (indeed the entire application) will re-render each time the slider changes. As the slider only changes the visual style of the map, not the underlying data structure or fig
object, I am unsure how to keep this from happening.
Desired outcome: I would like the app to only load the data once, when the app initially opens, by using the st.cache_data
decorator. Once the app is open, I would like to interact with the map by zooming, panning, etc. and change the opacity via the slider widget WITHOUT the entire application reloading & the map resetting each time the slider widget gets a new value for the opacity. Note that I’m already using the decorator to initially load the data from the API. Thank you for the help.
Hi @wwright1,
Thanks for sharing this question!
To achieve your desired outcome of running the map independently of the other parts of the app upon interacting with the ruler, you can use the decorator @st.experimental_fragment to decorate the function to renders the map. Wrap the map rendering code in a function and decorate it with fragment and it will only be reloaded and not the entire app.
You can read more on it in our docs below.
Thank you for the help, @tonykip!
I have looked at the @st.experimental_fragment
documentation and made the following edits to my code to incorporate this decorator:
import streamlit as st
import geopandas as gpd
import plotly.express as px
# Define a function to fetch and cache the data
@st.cache_data
def fetch_data(url):
# clean the input URL
url = url.replace(" ", "%20")
# Read the GeoJSON data from the URL
gdf = gpd.read_file(url)
# Select the required columns for choropleth map
gdf = gdf[['GEOID', 'TotPop_e22', 'geometry']]
return gdf
# read in population, geometry of Georgia counties
url = "https://services1.arcgis.com/Ug5xGQbHsD8zuZzM/arcgis/rest/services/ACS 2022 Demographic Population GeoSplitJoined/FeatureServer/9/query?outFields=*&where=1%3D1&f=geojson"
gdf = fetch_data(url)
# create a function to render the map
@st.experimental_fragment
def render_map(gdf, opacity):
# create mapping figure
fig = px.choropleth_mapbox(
gdf,
geojson=gdf.geometry,
locations=gdf.index,
color='TotPop_e22',
center={"lat": 32.90, "lon": -83.50},
mapbox_style='carto-positron',
zoom=6,
height=650
)
# update marker opacity based on slider value
fig.update_traces(marker=dict(opacity=opacity))
return fig
# set slider for map layer opacity
opacity = st.slider(
label='Set map layer opacity',
min_value=0.2,
max_value=1.0,
value=0.6,
step=0.1
)
# create fig using defined function
fig = render_map(gdf, opacity)
# plot the mapping figure
st.plotly_chart(
fig,
theme='streamlit',
use_container_width=True
)
However, the app still does not have the desired effect. To summarize, I would like to be able to zoom in on the map and then make a change to the opacity via the slider widget in the app and have the opacity of the blue choropleth map layer change without the entire app re-running and, essentially, “starting over.” If you copy-paste the above code and run it, you’ll see that the app will still re-run any time a change is made to the opacity slider.
Have I incorrectly used the decorator or wrapped the map-rendering portion of the code inside a function? Thank you for your help!