Skip to content

Commit

Permalink
Merge pull request #540 from rcpch/anchit/dashboard/treemap
Browse files Browse the repository at this point in the history
[Dashboard] Adds generalised treemap partial, with ethnicity treemap chart
  • Loading branch information
anchit-chandran authored Jan 31, 2025
2 parents d450b2d + 768a692 commit 3e3a81d
Show file tree
Hide file tree
Showing 9 changed files with 1,308 additions and 37 deletions.
34 changes: 34 additions & 0 deletions project/constants/ethnicities.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from project.constants import colors


ETHNICITIES = (
("N", "African"),
("L", "Any other Asian background"),
Expand All @@ -18,3 +21,34 @@
("D", "Mixed (White and Black Caribbean)"),
("99", "Not known"),
)

# Define top-level ethnicity categories and their colors (RCPCH defined)
ETHNICITY_PARENT_COLOR_MAP = {
"White": colors.RCPCH_LIGHT_BLUE,
"Asian": colors.RCPCH_PINK,
"Black": colors.RCPCH_MID_GREY,
"Mixed": colors.RCPCH_YELLOW,
"Other": colors.RCPCH_DARK_BLUE,
}

# Define ethnicity mapping to parents
ETHNICITY_CHILD_PARENT_MAP = {
"Not known": "Other",
"Any other mixed background": "Mixed",
"African": "Black",
"Pakistani or British Pakistani": "Asian",
"Caribbean": "Black",
"British, Mixed British": "White",
"Any other White background": "White",
"Any other Black background": "Black",
"Mixed (White and Black Caribbean)": "Mixed",
"Irish": "White",
"Any other ethnic group": "Other",
"Chinese": "Asian",
"Any other Asian background": "Asian",
"Mixed (White and Asian)": "Mixed",
"Indian or British Indian": "Asian",
"Not Stated": "Other",
"Mixed (White and Black African)": "Mixed",
"Bangladeshi or British Bangladeshi": "Asian",
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
{% endblock card_title%}
{% block card_body %}
<div
hx-get="{% url 'get_waffle_chart_partial' %}"
hx-vals='{{ charts.pt_ethnicity_value_counts_pct.data }}'
hx-get="{% url 'get_treemap_chart_partial' %}"
hx-vals='{{ charts.pt_ethnicity_tree_map_data }}'
hx-trigger="load" hx-swap="innerHTML"
_="on htmx:afterSwap remove #loading-spinner-ethnicity-card"></div>

Expand Down
1 change: 1 addition & 0 deletions project/npda/templates/dashboard/dashboard_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<div class="my-6">
<h1 class="text-5xl text-center">
PDU Dashboard for {{ pdu_lead_organisation.name }} ({{ pdu_object.pz_code }})

</h1>
<p class="text-center text-gray-500 mt-2">
Calculated at {{ kpi_calculations_object.calculation_datetime }}
Expand Down
7 changes: 7 additions & 0 deletions project/npda/templates/dashboard/treemap_chart_partial.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% if error %}
<div class="alert alert-danger">
<i class="fa-solid fa-person-circle-exclamation"></i> {{ error }}
</div>
{% elif chart_html %}
{{ chart_html|safe }}
{% endif %}
5 changes: 5 additions & 0 deletions project/npda/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@
view=get_hcl_scatter_plot,
name="get_hcl_scatter_plot",
),
path(
"get_treemap_chart_partial",
view=get_treemap_chart_partial,
name="get_treemap_chart_partial",
),
path(
"temp_set_eligible_kpi_7",
view=temp_set_eligible_kpi_7,
Expand Down
18 changes: 12 additions & 6 deletions project/npda/views/dashboard/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from django.contrib import messages
from django.shortcuts import render

from project.constants.diabetes_types import DIABETES_TYPES
from project import constants
from project.npda.general_functions.quarter_for_date import retrieve_quarter_for_date
from project.npda.models.paediatric_diabetes_unit import (
PaediatricDiabetesUnit as PaediatricDiabetesUnitClass,
Expand All @@ -26,6 +26,7 @@
from project.npda.views.decorators import login_and_otp_required
from project.npda.views.dashboard.template_data import *


# LOGGING
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,7 +54,9 @@ def temp_set_eligible_kpi_7(request):

_ = 10
logger.error(f"🔥 Setting {_} patients to be eligible for KPI 7")
to_set_kpi_7_eligible = Patient.objects.filter(diabetes_type=DIABETES_TYPES[0][0])[:_]
to_set_kpi_7_eligible = Patient.objects.filter(
diabetes_type=constants.diabetes_types.DIABETES_TYPES[0][0]
)[:_]
for pt in to_set_kpi_7_eligible:
pt.diagnosis_date = CalculateKPIS().audit_start_date + relativedelta(months=4)
pt.save()
Expand Down Expand Up @@ -215,7 +218,6 @@ def dashboard(request):
)
# Convert to pcts
pt_sex_value_counts_pct = convert_value_counts_dict_to_pct(pt_sex_value_counts)
pt_ethnicity_value_counts_pct = convert_value_counts_dict_to_pct(pt_ethnicity_value_counts)
pt_imd_value_counts_pct = convert_value_counts_dict_to_pct(pt_imd_value_counts)

# Gather other context vars
Expand Down Expand Up @@ -322,9 +324,13 @@ def dashboard(request):
"pt_sex_value_counts_pct": {
"data": json.dumps(pt_sex_value_counts_pct),
},
"pt_ethnicity_value_counts_pct": {
"data": json.dumps(pt_ethnicity_value_counts_pct),
},
"pt_ethnicity_tree_map_data": json.dumps(
{
"data": pt_ethnicity_value_counts,
"parent_color_map": constants.ethnicities.ETHNICITY_PARENT_COLOR_MAP,
"child_parent_map": constants.ethnicities.ETHNICITY_CHILD_PARENT_MAP,
}
),
"pt_imd_value_counts_pct": {
"data": json.dumps(pt_imd_value_counts_pct),
},
Expand Down
185 changes: 157 additions & 28 deletions project/npda/views/dashboard/partials.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render

from project.constants.colors import *
import project.constants.colors as colors
from project.npda.models.paediatric_diabetes_unit import (
PaediatricDiabetesUnit as PaediatricDiabetesUnitClass,
)
Expand Down Expand Up @@ -146,24 +146,24 @@ def get_waffle_chart_partial(request):
# Prepare waffle chart
# TODO: ADD IN A BUNCH OF COLORS HERE. ?COULD SPECIFY COLORS IN GET REQUEST
colours = [
RCPCH_DARK_BLUE,
RCPCH_PINK,
RCPCH_MID_GREY,
RCPCH_CHARCOAL_DARK,
RCPCH_RED,
RCPCH_ORANGE,
RCPCH_YELLOW,
RCPCH_STRONG_GREEN,
RCPCH_AQUA_GREEN,
RCPCH_PURPLE,
RCPCH_PURPLE_LIGHT_TINT2,
RCPCH_PURPLE_DARK_TINT,
RCPCH_RED_LIGHT_TINT3,
RCPCH_ORANGE_LIGHT_TINT3,
RCPCH_STRONG_GREEN_LIGHT_TINT3,
RCPCH_AQUA_GREEN_LIGHT_TINT3,
RCPCH_ORANGE_LIGHT_TINT3,
RCPCH_DARK_GREY,
colors.RCPCH_DARK_BLUE,
colors.RCPCH_PINK,
colors.RCPCH_MID_GREY,
colors.RCPCH_CHARCOAL_DARK,
colors.RCPCH_RED,
colors.RCPCH_ORANGE,
colors.RCPCH_YELLOW,
colors.RCPCH_STRONG_GREEN,
colors.RCPCH_AQUA_GREEN,
colors.RCPCH_PURPLE,
colors.RCPCH_PURPLE_LIGHT_TINT2,
colors.RCPCH_PURPLE_DARK_TINT,
colors.RCPCH_RED_LIGHT_TINT3,
colors.RCPCH_ORANGE_LIGHT_TINT3,
colors.RCPCH_STRONG_GREEN_LIGHT_TINT3,
colors.RCPCH_AQUA_GREEN_LIGHT_TINT3,
colors.RCPCH_ORANGE_LIGHT_TINT3,
colors.RCPCH_DARK_GREY,
][: len(data)]

# Create Plotly waffle chart
Expand Down Expand Up @@ -380,7 +380,7 @@ def get_progress_bar_chart_partial(
x=[100] * len(values),
y=labels,
orientation="h",
marker=dict(color=RCPCH_LIGHT_GREY),
marker=dict(color=colors.RCPCH_LIGHT_GREY),
showlegend=False,
hoverinfo="none",
name="Background",
Expand All @@ -396,7 +396,7 @@ def get_progress_bar_chart_partial(
showarrow=False,
xanchor="left",
yanchor="bottom",
font=dict(size=14, color=RCPCH_DARK_BLUE),
font=dict(size=14, color=colors.RCPCH_DARK_BLUE),
align="left",
yshift=27, # Shift the text upwards for readability
)
Expand All @@ -407,7 +407,7 @@ def get_progress_bar_chart_partial(
x=percentages,
y=labels,
orientation="h",
marker=dict(color=RCPCH_DARK_BLUE),
marker=dict(color=colors.RCPCH_DARK_BLUE),
text=[f"{pct}%" for pct in percentages],
textposition=["inside" if pct > 5 else "outside" for pct in percentages],
insidetextanchor="end",
Expand Down Expand Up @@ -500,7 +500,7 @@ def get_simple_bar_chart_pcts_partial(request):
# so add the '#' if it's not there
bar_color = f"#{bar_color}" if bar_color[0] != "#" else bar_color
else:
bar_color = RCPCH_DARK_BLUE
bar_color = colors.RCPCH_DARK_BLUE

# NOTE: don't need to handle empty data as the template handles this
data_raw = json.loads(request.GET.get("data"))
Expand Down Expand Up @@ -606,9 +606,9 @@ def get_hcl_scatter_plot(request):
percentages = [data[q]["pct"] for q in data]
passed = [data[q]["total_passed"] for q in data]
eligible = [data[q]["total_eligible"] for q in data]
colors = [RCPCH_LIGHT_BLUE for _ in data]
all_colors = [colors.RCPCH_LIGHT_BLUE for _ in data]
# highlight the last quarter
colors[-1] = RCPCH_PINK
all_colors[-1] = colors.RCPCH_PINK

# Create scatter plot
fig = go.Figure()
Expand All @@ -620,10 +620,10 @@ def get_hcl_scatter_plot(request):
mode="lines+markers",
marker=dict(
size=12,
color=colors,
color=all_colors,
symbol="square",
),
line=dict(color=RCPCH_LIGHT_BLUE),
line=dict(color=colors.RCPCH_LIGHT_BLUE),
hovertemplate="<b>%{x}</b>:Eligible passed: %{customdata[0]} / %{customdata[1]} (%{y:.1f}%)<extra></extra>",
customdata=list(zip(passed, eligible)),
)
Expand All @@ -644,7 +644,7 @@ def get_hcl_scatter_plot(request):
y=percentages[-1],
text=f"{percentages[-1]}%",
showarrow=False,
font=dict(color=RCPCH_PINK, size=12),
font=dict(color=colors.RCPCH_PINK, size=12),
yshift=yshift,
)

Expand Down Expand Up @@ -676,3 +676,132 @@ def get_hcl_scatter_plot(request):
"dashboard/hcl_scatter_plot_partial.html",
{"error": "Something went wrong!"},
)


def get_treemap_chart_partial(request):
"""
Expects in request.GET:
{
# A map where keys are all parents, values are their colors (children assigned same color)
parent_color_map : {
"Other": colors.RCPCH_DARK_BLUE,
...
},
# Child to parent map
child_parent_map : {
"Not known": "Other",
"Any other ethnic group": "Other",
...
}
# A data dict with keys being child names, values being ABSOLUTE counts in whole group
data : {
"Not known" : 3,
...
}
}
"""
try:

if not request.htmx:
return HttpResponseBadRequest("This view is only accessible via HTMX")

# Fetch data from query parameters
client_errors = []
if not (data := json.loads(request.GET.get("data"))):
client_errors.append("No data key provided in request.GET")
if not (parent_color_map := json.loads(request.GET.get("parent_color_map"))):
client_errors.append("No parent_color_map key provided in request.GET")
if not (child_parent_map := json.loads(request.GET.get("child_parent_map"))):
client_errors.append("No child_parent_map key provided in request.GET")

# Validate keys and vals
for child in data:
if child not in child_parent_map:
client_errors.append(f"{child} not found in child_parent_map")

parents_in_map = set(child_parent_map.values())
for parent in parent_color_map:
if parent not in parents_in_map:
client_errors.append(
f"{parent} from parent_color_map not found in child_parent_map"
)

if len(client_errors) > 0:
logger.error(f"Treemap partial bad client request with errors: {client_errors}")
return HttpResponseBadRequest(client_errors)

# Extract lists
children = list(data.keys())
percentages = list(data.values())
parents = [child_parent_map[child] for child in children] # Assign parents

# Ensure unique parent labels in the treemap
parent_labels = list(set(parents))
# Parent values are the sum of their children
parent_values = []
for parent in parent_labels:
parent_values.append(
sum([percentages[i] for i in range(len(children)) if parents[i] == parent])
)

# Define all labels (parents first, then children)
all_labels = parent_labels + children
all_parents = ["ALL"] * len(parent_labels) + parents

# Define values (parents first, then children)
all_values = parent_values + percentages

# Assign the same color to subcategories as their parent
all_colors = {p: parent_color_map[p] for p in parent_labels} # Assign parent colors
all_colors.update(
# Apply same color to children
{child: parent_color_map[child_parent_map[child]] for child in children}
)

# Create Treemap
fig = go.Figure(
go.Treemap(
labels=all_labels, # Labels including parents
parents=all_parents, # Hierarchical structure
values=all_values, # Sizes
textinfo="label+percent parent", # Show labels and percentages
marker=dict(
# Apply parent colors to subcategories
colors=[all_colors[label] for label in all_labels]
),
hovertemplate=(
"<b>%{label}</b><br>" "N=%{value} (%{percentRoot:.0%})<br><extra></extra>"
),
)
)

fig.update_layout(
margin=dict(l=0, r=0, t=0, b=0),
)

# Convert Plotly figure to HTML
chart_html = fig.to_html(
full_html=False,
include_plotlyjs=False,
config={
"displayModeBar": False,
},
default_height=DEFAULT_CHART_HTML_HEIGHT,
)

return render(
request,
"dashboard/treemap_chart_partial.html",
{"chart_html": chart_html},
)

except Exception as e:
logger.error("Error generating treemap chart", exc_info=True)
return render(
request,
"dashboard/treemap_chart_partial.html",
{"error": "Something went wrong!"},
)
Loading

0 comments on commit 3e3a81d

Please sign in to comment.