Skip to content

Commit

Permalink
Merge pull request #36 from OCHA-DAP/anomaly-map
Browse files Browse the repository at this point in the history
Anomaly map
  • Loading branch information
hannahker authored Dec 17, 2024
2 parents c6e25c1 + 7f57ed8 commit ea79282
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 54 deletions.
4 changes: 2 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from dash import Dash, dcc
from utils.log_utils import setup_logging

from callbacks.callbacks import register_callbacks
from layouts.content import content
from layouts.modal import disclaimer_modal
from layouts.navbar import module_bar, navbar
from utils.log_utils import setup_logging

app = Dash(__name__, update_title=None, suppress_callback_exceptions=True)
server = app.server
Expand All @@ -18,7 +18,7 @@
navbar(),
module_bar(),
content(),
dcc.Store(id="selected-pcode"),
dcc.Store(id="selected-data"),
]


Expand Down
4 changes: 4 additions & 0 deletions assets/bootstrap.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 25 additions & 15 deletions assets/dashExtensions_default.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
window.dashExtensions = Object.assign({}, window.dashExtensions, {
default: {
function0: function(feature, context) {
const selected = context.hideout.selected
const {
colorscale,
style,
colorProp,
selected
} = context.hideout; // get props from hideout
const value = feature.properties[colorProp]; // get value that determines the color
let featureStyle = {
...style
};

if (selected.includes(feature.properties.pcode)) {
return {
fillColor: '#1f77b4',
weight: 0.8,
opacity: 1,
color: 'white',
fillOpacity: 0.8
}
// Only modify opacity if this feature's pcode matches selected
if (selected === feature.properties.pcode) {
featureStyle.fillOpacity = 1;
featureStyle.color = "black";
featureStyle.weight = 1;
}
return {
fillColor: '#1f77b4',
weight: 0.8,
opacity: 1,
color: 'white',
fillOpacity: 0.3

// Set color based on value
if (value === -1) {
featureStyle.fillColor = colorscale[0];
} else if (value === 0) {
featureStyle.fillColor = colorscale[1];
} else if (value === 1) {
featureStyle.fillColor = colorscale[2];
}

return featureStyle;
}

}
Expand Down
116 changes: 87 additions & 29 deletions callbacks/callbacks.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
import json

import dash_leaflet as dl
import dash_leaflet.express as dlx
import dash_mantine_components as dmc
import pandas as pd
from dash import Input, Output, State, dcc, html, no_update
from dash_extensions.javascript import arrow_function, assign

from constants import ATTRIBUTION, URL, URL_LABELS
from utils.chart_utils import create_return_period_plot, create_timeseries_plot
from utils.data_utils import (
calculate_return_periods,
fetch_flood_data,
get_current_terciles,
get_summary,
process_flood_data,
)
from utils.log_utils import get_logger

from constants import ATTRIBUTION, URL, URL_LABELS

logger = get_logger("callbacks")

style_handle = assign(
"""
function(feature, context) {
const selected = context.hideout.selected
if(selected.includes(feature.properties.pcode)){
return {
fillColor: '#1f77b4',
weight: 0.8,
opacity: 1,
color: 'white',
fillOpacity: 0.8
}
const {colorscale, style, colorProp, selected} = context.hideout; // get props from hideout
const value = feature.properties[colorProp]; // get value that determines the color
let featureStyle = {...style};
// Only modify opacity if this feature's pcode matches selected
if (selected === feature.properties.pcode) {
featureStyle.fillOpacity = 1;
featureStyle.color = "black";
featureStyle.weight = 1;
}
return {
fillColor: '#1f77b4',
weight: 0.8,
opacity: 1,
color: 'white',
fillOpacity: 0.3
// Set color based on value
if (value === -1) {
featureStyle.fillColor = colorscale[0];
} else if (value === 0) {
featureStyle.fillColor = colorscale[1];
} else if (value === 1) {
featureStyle.fillColor = colorscale[2];
}
return featureStyle;
}
"""
)


def register_callbacks(app):
@app.callback(
Output("selected-pcode", "data"),
Output("selected-data", "data"),
Output("geojson", "hideout"),
Input("geojson", "n_clicks"),
State("adm-level", "value"),
Expand All @@ -58,23 +66,66 @@ def toggle_select(_, adm_level, feature, hideout):
return no_update

name = feature["properties"]["pcode"]
tercile = feature["properties"]["tercile"]
print(tercile)
if hideout["selected"] == name:
hideout["selected"] = ""
else:
hideout["selected"] = name
return name, hideout
return feature["properties"], hideout

@app.callback(Output("map", "children"), Input("adm-level", "value"))
def set_adm_value(adm_level):
with open(f"assets/geo/adm{adm_level}.json", "r") as file:
data = json.load(file)

df_tercile = get_current_terciles(adm_level)
features_df = pd.DataFrame(
[feature["properties"] for feature in data["features"]]
)
df_joined = features_df.merge(
df_tercile[["pcode", "tercile"]], on="pcode", how="left"
)
for feature, tercile in zip(data["features"], df_joined["tercile"]):
feature["properties"]["tercile"] = tercile

colorscale = ["#6baed6", "#dbdbdb", "#fcae91"]
colorbar = dlx.categorical_colorbar(
categories=["Below normal", "Normal", "Above normal"],
colorscale=colorscale,
width=300,
height=15,
position="bottomleft",
)
title = html.Div(
"Population exposed to flooding is...",
style={
"position": "absolute",
"bottom": "40px",
"left": "10px",
"zIndex": 1000,
"fontSize": "12px",
"paddingBottom": "5px",
"fontWeight": "bold",
},
)

style = dict(weight=1, opacity=1, color="white", fillOpacity=0.5)

geojson = dl.GeoJSON(
url=f"assets/geo/adm{adm_level}.json",
data=data,
id="geojson",
style=style_handle,
hideout=dict(selected=""),
hideout=dict(
colorscale=colorscale,
style=style,
colorProp="tercile",
selected="",
),
hoverStyle=arrow_function(
{"fillColor": "#1f77b4", "fillOpacity": 0.8}
{"fillOpacity": 1, "weight": 1, "color": "black"}
),
zoomToBounds=True,
zoomToBounds=False,
)
adm0 = dl.GeoJSON(
url="assets/geo/adm0_outline.json",
Expand All @@ -91,19 +142,21 @@ def set_adm_value(adm_level):
name="tile",
style={"zIndex": 1002},
),
title,
colorbar,
]

@app.callback(
Output("exposure-chart", "children"),
Output("rp-chart", "children"),
Output("place-name", "children"),
Output("num-exposed", "children"),
Input("selected-pcode", "data"),
Input("selected-data", "data"),
State("adm-level", "value"),
prevent_initial_call=False,
)
def update_plot(pcode, adm_level):
if not pcode:
def update_plot(selected_data, adm_level):
if not selected_data:
blank_children = [
dmc.Space(h=100),
dmc.Center(html.Div("Select a location from the map above")),
Expand All @@ -114,6 +167,10 @@ def update_plot(pcode, adm_level):
dmc.Center("No location selected"),
"",
)

pcode = selected_data["pcode"]
tercile = selected_data["tercile"]

df_exposure, df_adm = fetch_flood_data(pcode, adm_level)

if len(df_exposure) == 0:
Expand Down Expand Up @@ -145,10 +202,11 @@ def update_plot(pcode, adm_level):
config={"displayModeBar": False}, figure=fig_timeseries
)
rp_chart = dcc.Graph(config={"displayModeBar": False}, figure=fig_rp)
name, exposed_summary = get_summary(df_processed, df_adm, adm_level)
name, exposed_summary = get_summary(
df_processed, df_adm, adm_level, tercile
)
return exposure_chart, rp_chart, name, exposed_summary

# TODO: Would be better as a clientside callback, but couldn't seem to get it to work...
@app.callback(
Output("hover-place-name", "children"), Input("geojson", "hoverData")
)
Expand Down
4 changes: 2 additions & 2 deletions layouts/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ def map_container():
dl.Map(
[dl.TileLayer(url=URL, attribution=ATTRIBUTION)],
style={"width": "100%", "height": "100%"},
center=[0, 0],
zoom=2,
center=[8, 22],
zoom=4,
id="map",
),
dmc.Select(
Expand Down
40 changes: 34 additions & 6 deletions utils/data_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Literal

import pandas as pd
from dash import dcc
from sqlalchemy import create_engine, text

from constants import (
Expand Down Expand Up @@ -63,7 +64,7 @@ def fetch_flood_data(pcode, adm_level):

elapsed = time.time() - start
logger.debug(
f"Retrieved {len(df_exposure)} rows from database in {elapsed:.2f}s"
f"Retrieved {len(df_exposure)} rows from database in {elapsed:.2f}s" # noqa
)
return df_exposure, df_adm

Expand Down Expand Up @@ -117,9 +118,29 @@ def calculate_return_periods(df_peaks, rp: int = 3):
return df_peaks.sort_values(by="rp"), peak_years


def get_summary(df_exposure, df_adm, adm_level):
def get_current_terciles(adm_level):

tercile_table = (
"current_tercile_regions"
if adm_level == "region"
else "current_tercile"
)

engine = get_engine()
query = text(
f"""
select * from app.{tercile_table}
where adm_level=:adm_level
"""
)
with engine.connect() as con:
df = pd.read_sql_query(query, con, params={"adm_level": adm_level})
return df


def get_summary(df_exposure, df_adm, adm_level, tercile):
name = df_adm.iloc[0][f"adm{adm_level}_name"]
max_date = f"{df_exposure['date'].max():%Y-%m-%d}"
max_date = f"{df_exposure['date'].max():%Y-%m-%d}" # noqa
val_col = f"roll{ROLLING_WINDOW}"

df_ = df_exposure[df_exposure["date"] == max_date]
Expand All @@ -132,7 +153,14 @@ def get_summary(df_exposure, df_adm, adm_level):
)
people_exposed_formatted = "{:,}".format(people_exposed)

return (
name,
f"{people_exposed_formatted} people exposed to flooding as of {max_date}.",
tercile_label = {-1: "below normal", 0: "normal", 1: "above normal"}

summary_text = dcc.Markdown(
f"""
**{people_exposed_formatted}** people exposed to flooding as of **{max_date}**.
This is **{tercile_label[tercile]}** for this day of the year.
"""
)

return (name, summary_text)

0 comments on commit ea79282

Please sign in to comment.