Skip to content

Commit

Permalink
addresses #409, adding option to view and interact with spatial indic…
Browse files Browse the repository at this point in the history
…ator choropleth map for analysed regions in the GHSCI web app
  • Loading branch information
carlhiggs committed Apr 5, 2024
1 parent 1a2ab11 commit b82653f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 66 deletions.
36 changes: 18 additions & 18 deletions process/configuration/assets/output_data_dictionary.csv
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@ Linked covariates,"Semi-colon separated list of names of Köppen-Geiger climate
Linked covariates,"Average temperature calculated from annual average estimates for time interval centred on the year 2015 (the interval spans from 2012 to 2015) within the spatial domain of the Urban Centre, and expressed in Celsius degrees (°C) (Harris et al., 2014).",E_WR_T_14,city
Linked covariates,"Average precipitations calculated from annual average estimates for time interval centred on the year 2015 (the interval spans from 2012 to 2015) within the spatial domain of the Urban Centre; and expressed in millimetres (mm), the amount of rain per square meter in one hour) (Harris et al., 2014).",E_WR_P_14,city
Analytical statistic,Sample points used in this analysis (generated along pedestrian network for populated grid areas),urban_sample_point_count,"city, grid"
Indicator estimates,Score (/1) for access within 500 m to a fresh food market / supermarket (source: OpenStreetMap or custom),access_500m_fresh_food_market_score,grid
Indicator estimates,Score (/1) for access within 500 m to a convenience store (source: OpenStreetMap or custom),access_500m_convenience_score,grid
Indicator estimates,Score (/1) for access within 500 m to a public transport (source: OpenStreetMap or custom),access_500m_pt_osm_any_score,grid
Indicator estimates,Score (/1) for access within 500 m to a any public open space (source: OpenStreetMap),access_500m_public_open_space_any_score,grid
Indicator estimates,Score (/1) for access within 500 m to a public open space larger than 1.5 hectares (source: OpenStreetMap),access_500m_public_open_space_large_score,grid
Indicator estimates,Score (/1) for access within 500 m to a public transport (source: GTFS),access_500m_pt_gtfs_any_score,grid
Indicator estimates,Score (/1) for access within 500 m to a public transport with average daytime weekday service frequency of 30 minutes or better (source: GTFS),access_500m_pt_gtfs_freq_30_score,grid
Indicator estimates,Score (/1) for access within 500 m to a public transport with average daytime weekday service frequency of 20 minutes or better (source: GTFS),access_500m_pt_gtfs_freq_20_score,grid
Indicator estimates,Score (/1) for access within 500 m to a any public transport stop (source: GTFS or OpenStreetMap/custom),access_500m_pt_any_score,grid
Indicator estimates,Percentage of population with access within 500 m to a fresh food market / supermarket (source: OpenStreetMap or custom),pop_pct_access_500m_fresh_food_market_score,city
Indicator estimates,Percentage of population with access within 500 m to a convenience store (source: OpenStreetMap or custom),pop_pct_access_500m_convenience_score,city
Indicator estimates,Percentage of population with access within 500 m to a public transport (source: OpenStreetMap or custom),pop_pct_access_500m_pt_osm_any_score,city
Indicator estimates,Percentage of population with access within 500 m to a any public open space (source: OpenStreetMap),pop_pct_access_500m_public_open_space_any_score,city
Indicator estimates,Percentage of population with access within 500 m to a public open space larger than 1.5 hectares (source: OpenStreetMap),pop_pct_access_500m_public_open_space_large_score,city
Indicator estimates,Percentage of population with access within 500 m to a public transport (source: GTFS),pop_pct_access_500m_pt_gtfs_any_score,city
Indicator estimates,Percentage of population with access within 500 m to a public transport with average daytime weekday service frequency of 30 minutes or better (source: GTFS),pop_pct_access_500m_pt_gtfs_freq_30_score,city
Indicator estimates,Percentage of population with access within 500 m to a public transport with average daytime weekday service frequency of 20 minutes or better (source: GTFS),pop_pct_access_500m_pt_gtfs_freq_20_score,city
Indicator estimates,Percentage of population with access within 500 m to a any public transport stop (source: GTFS or OpenStreetMap/custom),pop_pct_access_500m_pt_any_score,city
Indicator estimates,Score (/1) for access within 500 m to a fresh food market / supermarket (source: OpenStreetMap or custom),access_500m_fresh_food_market_score,grid
Indicator estimates,Score (/1) for access within 500 m to a convenience store (source: OpenStreetMap or custom),access_500m_convenience_score,grid
Indicator estimates,Score (/1) for access within 500 m to public transport (source: OpenStreetMap or custom),access_500m_pt_osm_any_score,grid
Indicator estimates,Score (/1) for access within 500 m to any public open space (source: OpenStreetMap),access_500m_public_open_space_any_score,grid
Indicator estimates,Score (/1) for access within 500 m to a public open space larger than 1.5 hectares (source: OpenStreetMap),access_500m_public_open_space_large_score,grid
Indicator estimates,Score (/1) for access within 500 m to public transport (source: GTFS),access_500m_pt_gtfs_any_score,grid
Indicator estimates,Score (/1) for access within 500 m to public transport with average daytime weekday service frequency of 30 minutes or better (source: GTFS),access_500m_pt_gtfs_freq_30_score,grid
Indicator estimates,Score (/1) for access within 500 m to public transport with average daytime weekday service frequency of 20 minutes or better (source: GTFS),access_500m_pt_gtfs_freq_20_score,grid
Indicator estimates,Score (/1) for access within 500 m to any public transport stop (source: GTFS or OpenStreetMap/custom),access_500m_pt_any_score,grid
Indicator estimates,Percentage of population with access within 500 m to a fresh food market / supermarket (source: OpenStreetMap or custom),pop_pct_access_500m_fresh_food_market_score,city
Indicator estimates,Percentage of population with access within 500 m to a convenience store (source: OpenStreetMap or custom),pop_pct_access_500m_convenience_score,city
Indicator estimates,Percentage of population with access within 500 m to public transport (source: OpenStreetMap or custom),pop_pct_access_500m_pt_osm_any_score,city
Indicator estimates,Percentage of population with access within 500 m to any public open space (source: OpenStreetMap),pop_pct_access_500m_public_open_space_any_score,city
Indicator estimates,Percentage of population with access within 500 m to public open space larger than 1.5 hectares (source: OpenStreetMap),pop_pct_access_500m_public_open_space_large_score,city
Indicator estimates,Percentage of population with access within 500 m to public transport (source: GTFS),pop_pct_access_500m_pt_gtfs_any_score,city
Indicator estimates,Percentage of population with access within 500 m to public transport with average daytime weekday service frequency of 30 minutes or better (source: GTFS),pop_pct_access_500m_pt_gtfs_freq_30_score,city
Indicator estimates,Percentage of population with access within 500 m to public transport with average daytime weekday service frequency of 20 minutes or better (source: GTFS),pop_pct_access_500m_pt_gtfs_freq_20_score,city
Indicator estimates,Percentage of population with access within 500 m to any public transport stop (source: GTFS or OpenStreetMap/custom),pop_pct_access_500m_pt_any_score,city
Indicator estimates,Average walkable neighbourhood poulation density (population weighted) ,pop_nh_pop_density,city
Indicator estimates,Average walkable neighbourhood intersection density (population weighted) ,pop_nh_intersection_density,city
Indicator estimates,Average daily living score (population weighted),pop_daily_living,city
Expand Down
Binary file modified process/configuration/assets/output_data_dictionary.xlsx
Binary file not shown.
121 changes: 120 additions & 1 deletion process/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,66 @@ def get_region(codename) -> dict:
return region


def map_to_html(m, title, file=None, wrap_length=80) -> str:
"""Convert folium map to html and reformat for display."""
import re

# m.get_root().html.add_child(folium.Element(map_style_html_css))
# html = m.get_root().render()
html = m.get_root()._repr_html_()
## Wrap legend text if too long
## 65 chars seems to work well, conservatively)
if len(title) > wrap_length:
import textwrap

legend_lines = textwrap.wrap(title, wrap_length)
legend_length = len(title)
n_lines = len(legend_lines)
legend_height = 25 + 15 * n_lines
old = f'''.attr("class", "caption")\n .attr("y", 21)\n .text("{title}");'''
new = '.append("tspan")'.join(
[
'''.attr("class","caption")
.attr("x", 0)
.attr("y", {pos})
.text("{x}")
'''.format(
x=x, pos=21 + 15 * legend_lines.index(x),
)
for x in legend_lines
],
)
html = html.replace(old, new)
html = html.replace(
'.attr("height", 40);',
f'.attr("height", {legend_height});',
)

# move legend to lower right corner
html = html.replace(
'''legend = L.control({position: 'topright''',
'''legend = L.control({position: 'bottomright''',
)

# give legend white background
old = '''</style>'''
new = ''' .legend.leaflet-control {
background-color: #FFF;
}
.leaflet-control-attribution.leaflet-control {
width: 72%;
}
</style>'''
html = html.replace(old, new)
# export or return
if file is not None:
# save map
fid = open(f'{file}.html', 'wb')
fid.write(html.encode('utf8'))
else:
return html


async def get_regions(map):
global regions
regions = {}
Expand Down Expand Up @@ -166,8 +226,9 @@ def summary_table():
return None
region['summary'] = region['summary'].transpose().dropna()
row_key = region['summary'].index.name
indicator_dictionary = ghsci.dictionary['Description'].to_dict()
region['summary'].index = region['summary'].index.map(
ghsci.dictionary['Description'].to_dict(), na_action='ignore',
indicator_dictionary, na_action='ignore',
)
region['summary'] = region['summary'].reset_index()
values = region['summary'].to_dict('records')
Expand Down Expand Up @@ -208,6 +269,64 @@ def toggle() -> None:
icon='fullscreen',
on_click=toggle,
).props('flat')
with table.add_slot('top-right'):

async def get_choropleth(indicator) -> None:
if (
indicator
in ghsci.indicators['output'][
'neighbourhood_variables'
]
):
r = ghsci.Region(region['codename'])
choropleth = r.choropleth(
field=indicator,
layer=r.config['grid_summary'],
title=indicator_dictionary[
indicator.replace('pct', 'pop_pct')
],
save=False,
)
choropleth = map_to_html(
choropleth,
title=indicator_dictionary[
indicator.replace('pct', 'pop_pct')
],
)
return choropleth

async def popup_choropleth(indicator) -> None:
if indicator is not None:
print(indicator)
choropleth = await get_choropleth(indicator)
if choropleth is not None:
with ui.dialog() as map_dialog, ui.card().style(
'min-width: 75%',
):
ui.html(choropleth).style(
'min-width: 100%;',
)
map_dialog.open()

with ui.select(
options=ghsci.indicators['output'][
'neighbourhood_variables'
],
label='View indicator map',
value='None',
with_input=True,
).props('flat') as indicator:
ui.tooltip(
'For example, "AU_Melbourne_2023" is a codename for the city of Melbourne, Australia in 2023',
).props(
'anchor="bottom middle" self="bottom left"',
).style(
'color: white;background-color: #6e93d6;',
)
indicator.on(
'update:model-value',
lambda e: popup_choropleth(indicator.value),
)
dialog.open()


Expand Down
62 changes: 43 additions & 19 deletions process/subprocesses/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2131,32 +2131,49 @@ def generate_pdf(

def plot_choropleth_map(
r,
field: str,
layer: str = 'indicators_grid_100m',
field: str = 'local_walkability',
layer: str = None,
layer_id: str = 'grid_id',
fill_color: str = 'YlGn',
fill_opacity: float = 0.7,
line_opacity: float = 0.1,
title: str = '',
attribution: str = '',
auto_alias: bool = True,
aliases: list = None,
**args,
):
"""Given a region, field, layer and layer id, plot an interactive map."""
from ghsci import dictionary

if layer is None:
layer = r.config['grid_summary']
columns = [layer_id, field]
if auto_alias:
indicator_dictionary = dictionary['Description'].to_dict()
try:
aliases = [
layer_id,
indicator_dictionary[field.replace('pct', 'pop_pct')],
]
except KeyError:
print(
'Attempted to use indicator dictionary for choropleth tooltip alias but did not succeed; using column names instead.',
)
aliases = columns
elif aliases is None:
aliases = columns

geojson = r.get_geojson(
f'(SELECT {layer_id},{field},geom FROM {layer}) as sql',
include_columns=[layer_id, field],
include_columns=columns,
)
df = r.get_df(layer)[[layer_id, field]]
df = r.get_df(layer, columns=columns)
map = choropleth_map(
geojson=geojson,
df=df[[layer_id, field]],
df=df,
boundary_centroid=tuple(r.get_centroid()),
key_on=layer_id,
fields=[layer_id, field],
fill_color=fill_color,
fill_opacity=fill_opacity,
line_opacity=line_opacity,
fields=columns,
title=title,
attribution=attribution,
aliases=aliases,
**args,
)
return map

Expand All @@ -2167,11 +2184,13 @@ def choropleth_map(
key_on: str,
fields: list,
boundary_centroid: tuple,
fill_color: str,
fill_opacity: float,
line_opacity: float,
title: str,
attribution: str,
aliases: list,
line_opacity: float = 0.1,
fill_color: str = 'YlGn',
fill_opacity: float = 0.7,
attribution: str = 'Global Healthy and Sustainable City Indicators Collaboration',
**args,
):
import folium

Expand Down Expand Up @@ -2217,9 +2236,14 @@ def choropleth_map(
fill_opacity=fill_opacity,
line_opacity=0.1,
legend_name=title,
**args,
).add_to(m)
folium.features.GeoJsonTooltip(
fields=fields, labels=True, sticky=True,
fields=fields,
aliases=aliases,
labels=True,
sticky=True,
localize=True,
).add_to(data_layer.geojson)
folium.LayerControl(collapsed=True).add_to(m)
m.fit_bounds(m.get_bounds())
Expand Down
27 changes: 7 additions & 20 deletions process/subprocesses/ghsci.py
Original file line number Diff line number Diff line change
Expand Up @@ -1278,37 +1278,24 @@ def raster_to_db(

def choropleth(
self,
field: str,
layer: str,
id: str,
fill_color: str,
fill_opacity: float,
line_opacity: float,
title: str,
save=True,
attribution: str = 'Global Healthy and Sustainable City Indicators Collaboration',
field: str = 'local_walkability',
layer: str = None,
save: bool = True,
**args,
):
"""Plot a choropleth map of a specified field in a specified layer, with a custom title and attribution, with optional saving to an html file."""
from _utils import plot_choropleth_map

if layer is None:
layer = self.config['grid_summary']
tables = self.get_tables()
if layer not in tables:
print(
f"Layer {layer} not found in current list of database tables ({', '.join(tables)}).",
)
return None
else:
map = plot_choropleth_map(
self,
field=field,
layer=layer,
layer_id=id,
title=title,
fill_color=fill_color,
fill_opacity=fill_opacity,
line_opacity=line_opacity,
attribution=attribution,
)
map = plot_choropleth_map(self, field=field, layer=layer, **args)
if save:
file = f'{self.config["region_dir"]}/{layer} - {field}.html'
map.save(file)
Expand Down
22 changes: 14 additions & 8 deletions process/subprocesses/leaflet.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@ def __init__(self) -> None:
super().__init__()
ui.add_head_html(
"""<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>""",
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>""",
)
ui.add_head_html(
"""<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>""",
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>""",
)
ui.add_head_html(
"""<style>
.leaflet-tile {
filter: grayscale(100%);
}
</style>""",
.leaflet-tile {
filter: grayscale(100%);
}
.legend.leaflet-control {
background-color: #FFF;
}
.leaflet-control-attribution.leaflet-control {
width: 50%;
}
</style>""",
)

def set_location(self, location: Tuple[float, float], zoom: int) -> None:
Expand Down

0 comments on commit b82653f

Please sign in to comment.