Skip to content

Commit fffa294

Browse files
committed
HEA-592 first version of Inventory Dashboard
1 parent f821c6f commit fffa294

34 files changed

+26935
-107
lines changed

apps/viz/dash/inventory_dashboard/app.py

Lines changed: 98 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,98 @@
1+
import logging
12
import plotly.express as px
23
from dash import dash_table, dcc, html
34
from dash.dependencies import Input, Output
45
from django_plotly_dash import DjangoDash
6+
import dash_bootstrap_components as dbc
7+
from datetime import datetime
58

6-
from .functions import fetch_data, prepare_livelihood_data, prepare_wealth_group_data
7-
8-
# API Endpoints
9-
LIVELIHOOD_STRATEGY_URL = "https://headev.fews.net/api/livelihoodstrategy/"
10-
WEALTH_GROUP_URL = "https://headev.fews.net/api/wealthgroupcharacteristicvalue/"
11-
12-
# Fetch and prepare data
13-
livelihood_data = fetch_data(LIVELIHOOD_STRATEGY_URL)
14-
wealth_group_data = fetch_data(WEALTH_GROUP_URL)
15-
16-
livelihood_data = prepare_livelihood_data(livelihood_data)
17-
wealth_group_data = prepare_wealth_group_data(wealth_group_data)
9+
from .functions import clean_livelihood_data, clean_wealth_group_data
1810

1911
# Unique countries and livelihood zones for dropdowns
20-
unique_countries = sorted(livelihood_data["country_code"].unique())
21-
unique_zones = sorted(livelihood_data["livelihood_zone"].unique())
12+
unique_countries = sorted(clean_livelihood_data["country_code"].dropna().unique())
13+
unique_zones = sorted(clean_livelihood_data["livelihood_zone_baseline_label"].dropna().unique())
14+
15+
logger = logging.getLogger(__name__)
2216

2317
# Dash app
24-
app = DjangoDash("Inventory_dashboard")
18+
app = DjangoDash(
19+
"Inventory_dashboard",
20+
suppress_callback_exceptions=True,
21+
external_stylesheets=[dbc.themes.BOOTSTRAP]
22+
)
2523
app.title = "HEA Dashboard"
2624

2725
# Layout
2826
app.layout = html.Div(
2927
[
3028
html.H1("HEA Data Inventory Dashboard", style={"textAlign": "center"}),
31-
dcc.Dropdown(
32-
id="country-dropdown",
33-
options=[{"label": country, "value": country} for country in unique_countries],
34-
placeholder="Select Country",
35-
multi=False,
36-
),
37-
dcc.Dropdown(
38-
id="livelihood-zone-dropdown",
39-
options=[{"label": zone, "value": zone} for zone in unique_zones],
40-
placeholder="Select Livelihood Zone(s)",
41-
multi=True,
29+
html.Div(
30+
[
31+
html.Div(
32+
[
33+
dcc.Dropdown(
34+
id="country-dropdown",
35+
options=[{"label": country, "value": country} for country in unique_countries],
36+
placeholder="Select Country(s)",
37+
multi=True,
38+
),
39+
],
40+
style={"flex": "1", "marginRight": "10px"}, # Set flex and spacing
41+
),
42+
html.Div(
43+
[
44+
dcc.Dropdown(
45+
id="livelihood-zone-dropdown",
46+
options=[{"label": zone, "value": zone} for zone in unique_zones],
47+
placeholder="Select Livelihood Zone(s)",
48+
multi=True,
49+
),
50+
],
51+
style={"flex": "1"},
52+
),
53+
],
54+
style={
55+
"display": "flex",
56+
"width": "100%",
57+
"justifyContent": "space-between",
58+
"marginBottom": "20px",
59+
},
4260
),
4361
html.Div(
4462
[
45-
html.Div(dcc.Graph(id="wealth-chart"), style={"width": "48%", "display": "inline-block"}),
46-
html.Div(dcc.Graph(id="livelihood-chart"), style={"width": "48%", "display": "inline-block"}),
47-
]
63+
html.Div(
64+
dcc.Graph(id="wealth-chart"),
65+
style={"width": "30%", "display": "inline-block"},
66+
),
67+
html.Div(
68+
dcc.Graph(id="livelihood-chart"),
69+
style={"width": "30%", "display": "inline-block"},
70+
),
71+
html.Div(
72+
dcc.Graph(id="wealth-monthly-chart"),
73+
style={"width": "30%", "display": "inline-block"},
74+
),
75+
],
4876
),
4977
html.Div(
5078
[
5179
html.H3("Data Table", style={"textAlign": "center"}),
5280
dash_table.DataTable(
5381
id="data-table",
5482
columns=[
55-
{"name": "Livelihood Zone", "id": "livelihood_zone"},
83+
{"name": "Livelihood Zone", "id": "livelihood_zone_baseline_label"},
5684
{"name": "Strategy Type Count", "id": "Count"},
5785
{"name": "Wealth Characteristics Count", "id": "Wealth Characteristics Count"},
5886
],
5987
style_table={"overflowX": "auto"},
6088
style_cell={"textAlign": "left"},
61-
page_size=10,
89+
page_size=12,
6290
),
63-
]
91+
],
92+
className="inventory-filter inventory-filter-last",
6493
),
65-
]
94+
],
95+
className="div-wrapper control-panel-wrapper",
6696
)
6797

6898

@@ -72,54 +102,72 @@
72102
Output("livelihood-zone-dropdown", "options"),
73103
Output("wealth-chart", "figure"),
74104
Output("livelihood-chart", "figure"),
105+
Output("wealth-monthly-chart", "figure"),
75106
Output("data-table", "data"),
76107
],
77108
[Input("country-dropdown", "value"), Input("livelihood-zone-dropdown", "value")],
78109
)
79-
def update_charts(selected_country, selected_zones):
80-
# Filter data based on selected country
81-
if selected_country:
82-
filtered_livelihood = livelihood_data[livelihood_data["country_code"] == selected_country]
83-
filtered_wealth = wealth_group_data[wealth_group_data["country_code"] == selected_country]
84-
filtered_zones = sorted(filtered_livelihood["livelihood_zone"].unique())
110+
def update_charts(selected_countries, selected_zones):
111+
# Handle multi-country selection
112+
if selected_countries:
113+
filtered_livelihood = clean_livelihood_data[clean_livelihood_data["country_code"].isin(selected_countries)]
114+
filtered_wealth = clean_wealth_group_data[clean_wealth_group_data["country_code"].isin(selected_countries)]
115+
filtered_zones = sorted(filtered_livelihood["livelihood_zone_baseline_label"].unique())
85116
else:
86-
filtered_livelihood = livelihood_data
87-
filtered_wealth = wealth_group_data
117+
filtered_livelihood = clean_livelihood_data
118+
filtered_wealth = clean_wealth_group_data
88119
filtered_zones = unique_zones
89120

90121
# Update options for livelihood zone dropdown
91122
zone_options = [{"label": zone, "value": zone} for zone in filtered_zones]
92123

93124
# Filter data based on selected livelihood zones
94125
if selected_zones:
95-
filtered_livelihood = filtered_livelihood[filtered_livelihood["livelihood_zone"].isin(selected_zones)]
96-
filtered_wealth = filtered_wealth[filtered_wealth["livelihood_zone"].isin(selected_zones)]
126+
filtered_livelihood = filtered_livelihood[filtered_livelihood["livelihood_zone_baseline_label"].isin(selected_zones)]
127+
filtered_wealth = filtered_wealth[filtered_wealth["livelihood_zone_baseline_label"].isin(selected_zones)]
97128

98129
# Group data for charts
99130
livelihood_grouped = (
100-
filtered_livelihood.groupby(["livelihood_zone", "strategy_type_label"]).size().reset_index(name="Count")
131+
filtered_livelihood.groupby(["livelihood_zone_baseline_label", "strategy_type_label"]).size().reset_index(name="Count")
132+
)
133+
wealth_grouped = filtered_wealth.groupby("livelihood_zone_baseline_label").size().reset_index(name="Wealth Characteristics Count")
134+
wealth_monthly_grouped = (
135+
filtered_wealth.groupby(["created_month", "livelihood_zone_baseline_label"]).size().reset_index(name="Wealth Characteristics Count")
101136
)
102-
wealth_grouped = filtered_wealth.groupby("livelihood_zone").size().reset_index(name="Wealth Characteristics Count")
103137

104138
wealth_fig = px.bar(
105139
wealth_grouped,
106-
x="livelihood_zone",
140+
x="livelihood_zone_baseline_label",
107141
y="Wealth Characteristics Count",
108142
title="Wealth Characteristics per Baseline",
109-
labels={"livelihood_zone": "Baseline", "Wealth Characteristics Count": "Count"},
143+
labels={"livelihood_zone_baseline_label": "Baseline", "Wealth Characteristics Count": "No. of Wealth Characteristics"},
110144
)
111145

112146
livelihood_fig = px.bar(
113147
livelihood_grouped,
114148
x="strategy_type_label",
115149
y="Count",
116-
color="livelihood_zone",
150+
color="livelihood_zone_baseline_label",
117151
title="Livelihood Strategies per Baseline",
118-
labels={"strategy_type_label": "Strategy Type", "Count": "Count", "livelihood_zone": "Baseline"},
152+
labels={"strategy_type_label": "Strategy Type", "Count": "No. of Livelihood Strategies", "livelihood_zone_baseline_label": "Baseline"},
119153
)
120154

121-
return zone_options, wealth_fig, livelihood_fig, livelihood_grouped.to_dict("records")
155+
# Stacked/multiple column chart for wealth characteristics by month
156+
wealth_monthly_fig = px.bar(
157+
wealth_monthly_grouped,
158+
x="created_month",
159+
y="Wealth Characteristics Count",
160+
color="livelihood_zone_baseline_label",
161+
barmode="stack", # Use 'group' for multiple column chart
162+
title="Wealth Characteristics by Month and Baseline",
163+
labels={
164+
"created_month": "Month",
165+
"Wealth Characteristics Count": "No. of Wealth Characteristics",
166+
"livelihood_zone_baseline_label": "Baseline",
167+
},
168+
)
122169

170+
return zone_options, wealth_fig, livelihood_fig, wealth_monthly_fig, livelihood_grouped.to_dict("records")
123171

124172
# Run the app
125173
if __name__ == "__main__":

apps/viz/dash/inventory_dashboard/functions.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import pandas as pd
22
import requests
33

4+
# API Endpoints
5+
LIVELIHOOD_STRATEGY_URL = "https://headev.fews.net/api/livelihoodstrategy/"
6+
WEALTH_GROUP_URL = "https://headev.fews.net/api/wealthgroupcharacteristicvalue/"
7+
LIVELIHOOD_ACTIVITY_URL = "https://headev.fews.net/api/livelihoodactivity/"
48

59
def fetch_data(api_url):
610
"""
@@ -15,20 +19,52 @@ def fetch_data(api_url):
1519
print(f"Error fetching data: {e}")
1620
return pd.DataFrame()
1721

18-
1922
def prepare_livelihood_data(df):
2023
"""
2124
Prepare livelihood strategy data for visualization.
2225
"""
2326
df.rename(columns={"livelihood_zone_country": "country_code"}, inplace=True)
2427
df["ls_baseline_date"] = df["livelihood_zone_baseline_label"].str.split(": ").str[1]
25-
df["ls_baseline_month"] = pd.to_datetime(df["ls_baseline_date"], errors="coerce").dt.month_name()
28+
df["ls_baseline_month"] = pd.to_datetime(df["ls_baseline_date"], errors="coerce").dt.month
2629
return df
2730

28-
2931
def prepare_wealth_group_data(df):
3032
"""
3133
Prepare wealth group data for visualization.
3234
"""
35+
# Rename columns for consistency
3336
df.rename(columns={"livelihood_zone_country_code": "country_code"}, inplace=True)
37+
38+
# Extract baseline date from 'livelihood_zone_baseline_label'
39+
if "livelihood_zone_baseline_label" in df.columns:
40+
df["ls_baseline_date"] = df["livelihood_zone_baseline_label"].str.split(": ").str[1]
41+
else:
42+
df["ls_baseline_date"] = None # Assign None if the column is missing
43+
44+
# Convert baseline date to datetime and extract the month
45+
df["ls_baseline_month"] = pd.to_datetime(df["ls_baseline_date"], errors="coerce").dt.month
46+
47+
# Define month mapping dictionary
48+
month_mapping = {
49+
1: "January", 2: "February", 3: "March", 4: "April",
50+
5: "May", 6: "June", 7: "July", 8: "August",
51+
9: "September", 10: "October", 11: "November", 12: "December"
52+
}
53+
54+
# Extract 'created_month' from 'created_date' or fallback to 'ls_baseline_date'
55+
if "created_date" in df.columns:
56+
df["created_month"] = pd.to_datetime(df["created_date"], errors="coerce").dt.month.map(month_mapping)
57+
else:
58+
print("Warning: 'created_date' column is missing. Using 'ls_baseline_date' instead.")
59+
df["created_month"] = pd.to_datetime(df["ls_baseline_date"], errors="coerce").dt.month.map(month_mapping)
60+
3461
return df
62+
63+
64+
65+
# Fetch and prepare data
66+
livelihood_data = fetch_data(LIVELIHOOD_STRATEGY_URL)
67+
wealth_group_data = fetch_data(WEALTH_GROUP_URL)
68+
69+
clean_livelihood_data = prepare_livelihood_data(livelihood_data)
70+
clean_wealth_group_data = prepare_wealth_group_data(wealth_group_data)

0 commit comments

Comments
 (0)