-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathinteractive_maps.qmd
More file actions
229 lines (171 loc) · 8.97 KB
/
interactive_maps.qmd
File metadata and controls
229 lines (171 loc) · 8.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
---
title: "Interactive Maps"
filters:
- whitphx/stlite
---
## Interactive maps with Folium
Folium is an easy way to make interactive maps.
While they are not natively supported in streamlit, the st-folium component is a powerful custom component that is being actively supported and developed.
```{python}
#| eval: false
import geopandas # <1>
import pandas as pd
import streamlit as st
import folium # <2>
from streamlit_folium import st_folium # <3>
gp_list_gdf_sw = geopandas.read_file( # <4>
"https://files.catbox.moe/atzk26.gpkg"
)
# Filter out instances with no geometry
gp_list_gdf_sw = gp_list_gdf_sw[~gp_list_gdf_sw['geometry'].is_empty] , # <5>
# Create a geometry list from the GeoDataFrame
geo_df_list = [[point.xy[1][0], point.xy[0][0]] for point in gp_list_gdf_sw.geometry] # <6>
gp_map_tooltip = folium.Map( # <7>
location=[50.7, -4.2],
zoom_start=8,
tiles='openstreetmap',
)
for i, coordinates in enumerate(geo_df_list): # <8>
gp_map_tooltip = gp_map_tooltip.add_child( # <9>
folium.Marker(
location=coordinates,
tooltip=gp_list_gdf_sw['name'].values[i],
icon=folium.Icon(icon="user-md", prefix='fa', color="black") # <10>
)
)
st_folium(gp_map_tooltip) # <11>
```
1. To work with geographic data, we need to import the geopandas library.
2. We'll also need the folium library to help set up our interactive map.
3. Finally we need to use the streamlit_folium library, which we have to install separately (but is included in the hsma_webdev environment if you are following the HSMA course). From that library, we import just the function `st_folium()`.
4. We load in a geopackage file. We don't need to specify a coordinate reference system for this kind of file; it's recorded within the file itself and geopandas will automatically read and apply this, though as Folium expects the coordinates to be in latitude and longitude (not Northings and Eastings), you may need to convert the CRS of your own data. See the HSMA geographic book for more details.
5. Folium does not cope well with missing data, so we filter out any rows where our 'geometry' column is empty.
6. To set up our map of points, we will need to create a list of coordinate pairs, though Folium expects them in the order longitude, latitude, so we swap the order of the points from our geometry column when placing them in the list.
7. We then create a folium map, specifying the starting zoom level and the coordinates around which it should initially be centred.
8. We then iterate through the list of points we created.
9. In each round of our loop we add a Folium 'marker' to our original map.
10. In Folium, if we don't specify an icon to use for the marker, it will choose a default. However, this doesn't seem to reliably work in all instances of Streamlit, so you may need to specify a custom icon instead using the folium.Icon class to select an icon from a web service such as font awesome. More about this can be found in the [HSMA geographic book](https://hsma-programme.github.io/hsma6_geographic_optimisation_and_visualisation_book/python_geopandas_folium_point_data.html#web-markers).
11. Finally, we pass our map to the `st_folium()` function.
```{stlite-python}
import micropip
await micropip.install("folium")
await micropip.install("geopandas")
await micropip.install("matplotlib")
await micropip.install("streamlit-folium")
import streamlit as st
import pandas as pd
import folium
import geopandas
from matplotlib import pyplot as plt
from streamlit_folium import st_folium
from pyodide.http import pyfetch
import geopandas as gpd
res = await pyfetch("https://files.catbox.moe/wtglio.geojson")
data = await res.json()
gp_list_gdf_sw = gpd.GeoDataFrame.from_features(data)
# # Filter out instances with no geometry
gp_list_gdf_sw = gp_list_gdf_sw[~gp_list_gdf_sw['geometry'].is_empty]
# # Create a geometry list from the GeoDataFrame
geo_df_list = [[point.xy[1][0], point.xy[0][0]] for point in gp_list_gdf_sw.geometry]
gp_map_tooltip = folium.Map(
location=[50.7, -4.2],
zoom_start=8,
tiles='openstreetmap',
)
for i, coordinates in enumerate(geo_df_list):
gp_map_tooltip = gp_map_tooltip.add_child(
folium.Marker(
location=coordinates,
tooltip=gp_list_gdf_sw['name'].values[i],
icon=folium.Icon(icon="user-md", prefix='fa', color="black")
)
)
st_folium(gp_map_tooltip)
```
:::{.callout-tip}
Take a look at the [HSMA geographic modelling and visualisation book](https://hsma-programme.github.io/hsma6_geographic_optimisation_and_visualisation_book/python_geopandas_folium_intro.html) to find out more about creating and modifying interactive maps in Python.
:::
### Sneak Peak - Updating the map based on inputs
Let's use a simple text input to filter the dataframe we are passing to the map.
What happens to the map when we do this?
```{python}
#| eval: false
import geopandas
import pandas as pd
import streamlit as st
import folium
from streamlit_folium import st_folium
search_string = st.text_input("Enter a string to search the practice name field by") # <1>
gp_list_gdf_sw = geopandas.read_file("https://files.catbox.moe/atzk26.gpkg")
# Filter out instances with no geometry
gp_list_gdf_sw = gp_list_gdf_sw[~gp_list_gdf_sw['geometry'].is_empty]
# Filter to just the practice of interest (if given)
if search_string is not "": # <2>
gp_list_gdf_sw = gp_list_gdf_sw[gp_list_gdf_sw['name'].str.contains(search_string.upper())] # <3>
st.dataframe(gp_list_gdf_sw[['name', 'address_1', 'postcode', 'Total List Size']]) # <4>
# Create a geometry list from the GeoDataFrame
geo_df_list = [[point.xy[1][0], point.xy[0][0]] for point in gp_list_gdf_sw.geometry] # <5>
gp_map_tooltip = folium.Map(
location=[50.7, -4.2],
zoom_start=8,
tiles='openstreetmap',
)
for i, coordinates in enumerate(geo_df_list):
gp_map_tooltip = gp_map_tooltip.add_child(
folium.Marker(
location=coordinates,
tooltip=gp_list_gdf_sw['name'].values[i],
icon=folium.Icon(icon="user-md", prefix='fa', color="black")
)
)
st_folium(gp_map_tooltip)
```
1. We create a streamlit user input that is designed to take a text string from the user. Whatever the user enters is saved to the variable `search_string`.
2. We check whether this search string is equal to an empty string, which is `""` or `''` (but we can use either of those to check against - they are regarded as identical). If the value of `search_string` is `""`, we don't undertake the indented code and jump to the next step instead - i.e. we won't do any filtering.
3. If the `search_string` is anything other than a blank string, we filter the name column of the dataframe (which here is the GP practice name) to only include instances where the `search_string` appears somewhere in the name - e.g. if our search string is "Hill" it would match "Hill Practice", "Big Hill Surgery", "Chilly Bend Surgery" and so on. Other methods exist if we only want to match the exact string.
4. Here, we add in a display of the filtered dataframe, restricting it to only the columns specified in the list.
5. All of our Folium code is unchanged; we just pass the filtered (or unfiltered, if no search string is entered) dataframe instead.
```{stlite-python}
import micropip
await micropip.install("folium")
await micropip.install("geopandas")
await micropip.install("matplotlib")
await micropip.install("streamlit-folium")
import streamlit as st
import pandas as pd
import folium
import geopandas
from matplotlib import pyplot as plt
from streamlit_folium import st_folium
from pyodide.http import pyfetch
import geopandas as gpd
res = await pyfetch("https://files.catbox.moe/wtglio.geojson")
data = await res.json()
gp_list_gdf_sw = gpd.GeoDataFrame.from_features(data)
# Filter out instances with no geometry
gp_list_gdf_sw = gp_list_gdf_sw[~gp_list_gdf_sw['geometry'].is_empty]
search_string = st.text_input("Enter a search string")
# Filter to just the practice of interest (if given)
if search_string is not None:
gp_list_gdf_sw = gp_list_gdf_sw[gp_list_gdf_sw['name'].str.contains(search_string.upper())]
st.dataframe(gp_list_gdf_sw[['name', 'address_1', 'postcode', 'Total List Size']])
# Create a geometry list from the GeoDataFrame
geo_df_list = [[point.xy[1][0], point.xy[0][0]] for point in gp_list_gdf_sw.geometry]
gp_map_tooltip = folium.Map(
location=[50.7, -4.2],
zoom_start=8,
tiles='openstreetmap',
)
for i, coordinates in enumerate(geo_df_list):
gp_map_tooltip = gp_map_tooltip.add_child(
folium.Marker(
location=coordinates,
tooltip=gp_list_gdf_sw['name'].values[i],
icon=folium.Icon(icon="user-md", prefix='fa', color="black")
)
)
st_folium(gp_map_tooltip)
```
### Updating the app based on the map zoom
You can do things like filter a dataframe down to only the subset of points that are on the screen within the Folium component.
To find out more about this, head to the chapter [Bidirectional Inputs - Charts and Maps](bidirectional_maps.qmd)