Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map! #416

Merged
merged 4 commits into from
Dec 5, 2024
Merged

Map! #416

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions project/constants/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .albuminuria_stage import *
from .colors import *
from .csv_headings import *
from .closed_loop_types import *
from .diabetes_types import *
Expand Down
54 changes: 54 additions & 0 deletions project/constants/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
RCPCH_LIGHTEST_GREY = "#F3F3F3"
RCPCH_DARK_BLUE = "#0D0D58"
RCPCH_STRONG_BLUE = "#3366CC"
RCPCH_STRONG_BLUE_LIGHT_TINT1 = "#668CD9"
RCPCH_STRONG_BLUE_LIGHT_TINT2 = "#99B3E6"
RCPCH_STRONG_BLUE_LIGHT_TINT3 = "#CCD9F2"
RCPCH_STRONG_BLUE_DARK_TINT = "#405A97"
RCPCH_LIGHT_BLUE = "#11A7F2"
RCPCH_LIGHT_BLUE_TINT1 = "#4DBDF5"
RCPCH_LIGHT_BLUE_TINT2 = "#88D3F9"
RCPCH_LIGHT_BLUE_TINT3 = "#CFE9FC"
RCPCH_LIGHT_BLUE_DARK_TINT = "#0082BC"
RCPCH_PINK = "#E00087"
RCPCH_PINK_LIGHT_TINT1 = "#E840A5"
RCPCH_PINK_LIGHT_TINT2 = "#EF80C3"
RCPCH_PINK_LIGHT_TINT3 = "#F7BFE1"
RCPCH_PINK_DARK_TINT = "#AB1368"
RCPCH_WHITE = "#FFFFFF"
RCPCH_LIGHT_GREY = "#D9D9D9"
RCPCH_MID_GREY = "#B3B3B3"
RCPCH_DARK_GREY = "#808080"
RCPCH_CHARCOAL = "#4D4D4D"
RCPCH_CHARCOAL_DARK = "#191919"
RCPCH_BLACK = "#000000"
RCPCH_RED = "#E60700"
RCPCH_RED_LIGHT_TINT1 = "#EC4540"
RCPCH_RED_LIGHT_TINT2 = "#F38380"
RCPCH_RED_LIGHT_TINT3 = "#F9C1BF"
RCPCH_RED_DARK_TINT = "#B11D23"
RCPCH_ORANGE = "#FF8000"
RCPCH_ORANGE_LIGHT_TINT1 = "#FFA040"
RCPCH_ORANGE_LIGHT_TINT2 = "#FFC080"
RCPCH_ORANGE_LIGHT_TINT3 = "#FFDFBF"
RCPCH_ORANGE_DARK_TINT = "#BF6914"
RCPCH_YELLOW = "#FFD200"
RCPCH_YELLOW_LIGHT_TINT1 = "#FFDD40"
RCPCH_YELLOW_LIGHT_TINT2 = "#FFE980"
RCPCH_YELLOW_LIGHT_TINT3 = "#FFF4BF"
RCPCH_YELLOW_DARK_TINT = "#C5A000"
RCPCH_STRONG_GREEN = "#66CC33"
RCPCH_STRONG_GREEN_LIGHT_TINT1 = "#8CD966"
RCPCH_STRONG_GREEN_LIGHT_TINT2 = "#B3E699"
RCPCH_STRONG_GREEN_LIGHT_TINT3 = "#D9F2CC"
RCPCH_STRONG_GREEN_DARK_TINT = "#53861B"
RCPCH_AQUA_GREEN = "#00BDAA"
RCPCH_AQUA_GREEN_LIGHT_TINT1 = "#40ECBF"
RCPCH_AQUA_GREEN_LIGHT_TINT2 = "#80DED4"
RCPCH_AQUA_GREEN_LIGHT_TINT3 = "#BFEEEA"
RCPCH_AQUA_GREEN_DARK_TINT = "#2E888D"
RCPCH_PURPLE = "#7159AA"
RCPCH_PURPLE_LIGHT_TINT1 = "#AE4CBF"
RCPCH_PURPLE_LIGHT_TINT2 = "#C987D4"
RCPCH_PURPLE_LIGHT_TINT3 = "#E4C3EA"
RCPCH_PURPLE_DARK_TINT = "#66296C"
1 change: 1 addition & 0 deletions project/npda/general_functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .email import *
from .group_for_group import *
from .index_multiple_deprivation import *
from .map import *
from .nhs_ods_requests import *
from .organisations_adapter import *
from .quarter_for_date import *
Expand Down
227 changes: 227 additions & 0 deletions project/npda/general_functions/map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# python imports

# django imports
from django.conf import settings
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from django.db.models import Q
from django.apps import apps

# third-party imports
import pandas as pd
import plotly.io as pio
import plotly.graph_objects as go

# RCPCH imports
from project.constants import RCPCH_LIGHT_BLUE, RCPCH_PINK, RCPCH_DARK_BLUE

from project.npda.general_functions.validate_postcode import location_for_postcode


"""
Functions to return scatter plot of children by postcode
"""


def get_children_by_pdu_audit_year(
audit_year, paediatric_diabetes_unit, paediatric_diabetes_unit_lead_organisation
):
"""
Returns a list of children by postcode for a given audit year and paediatric diabetes unit
"""
Patient = apps.get_model("npda", "Patient")
Submission = apps.get_model("npda", "Submission")

submission = Submission.objects.filter(
audit_year=audit_year, paediatric_diabetes_unit=paediatric_diabetes_unit
).first()

if submission is None:
return Patient.objects.none()

patients = submission.patients.all()

print(patients)

if patients:
filtered_patients = patients.filter(
~Q(postcode__isnull=True)
| ~Q(postcode__exact=""), # Exclude patients with no postcode
submission=submission, # Filter by submission
)

for patient in filtered_patients:
if patient.postcode and not any(
patient.location_wgs84 is None, patient.location_bng is None
):
# add the location data to the queryset - note these fields do not exist in the model
lon, lat, location_wgs84, location_bng = location_for_postcode(
patient.postcode
)
patient.location_wgs84 = location_wgs84
patient.location_bng = location_bng

filtered_patients = filtered_patients.annotate(
distance_from_lead_organisation=Distance(
"location_wgs84",
Point(
paediatric_diabetes_unit_lead_organisation.longitude,
paediatric_diabetes_unit_lead_organisation.latitude,
srid=4326,
),
)
).values(
"pk",
"location_bng",
"location_wgs84",
"distance_from_lead_organisation",
)
else:
return Patient.objects.none()


# RCPCH imports


def generate_distance_from_organisation_scatterplot_figure(
geo_df: pd.DataFrame, pdu_lead_organisation
):
"""
Returns a plottable map with Cases overlayed as dots with tooltips on hover
"""

fig = go.Figure(
go.Scattermapbox(
lat=geo_df["latitude"] if not geo_df.empty else [],
lon=geo_df["longitude"] if not geo_df.empty else [],
hovertext=geo_df["site__organisation__name"] if not geo_df.empty else None,
mode="markers",
marker=go.scattermapbox.Marker(
size=9,
color=RCPCH_PINK,
),
customdata=geo_df[["pk", "distance_mi", "distance_km"]],
)
)

fig.update_layout(
mapbox_style="carto-positron",
mapbox_zoom=10,
mapbox_center=dict(
lat=pdu_lead_organisation["latitude"],
lon=pdu_lead_organisation["longitude"],
),
height=590,
mapbox_accesstoken=settings.MAPBOX_API_KEY,
showlegend=False,
)

# Update the hover template
fig.update_traces(
hovertemplate="<b>%{hovertext}</b><br>Epilepsy12 ID: %{customdata[0]}<br>Distance to Lead Centre: %{customdata[1]:.2f} mi (%{customdata[2]:.2f} km)<extra></extra>"
)

# Add a scatterplot point for the organization
fig.add_trace(
go.Scattermapbox(
lat=[pdu_lead_organisation["latitude"]],
lon=[pdu_lead_organisation["longitude"]],
mode="markers",
marker=go.scattermapbox.Marker(
size=12,
color=RCPCH_DARK_BLUE, # Set the color of the point
),
text=[pdu_lead_organisation["name"]], # Set the hover text for the point
hovertemplate="%{text}<extra></extra>", # Custom hovertemplate just for the lead organisation
showlegend=False,
)
)

fig.update_layout(
margin={"r": 0, "t": 0, "l": 0, "b": 0},
font=dict(family="Montserrat-Regular", color="#FFFFFF"),
hoverlabel=dict(
bgcolor=RCPCH_LIGHT_BLUE,
font_size=12,
font=dict(color="white", family="Montserrat-Regular"),
bordercolor=RCPCH_LIGHT_BLUE,
),
mapbox=dict(
style="carto-positron",
zoom=10,
center=dict(
lat=pdu_lead_organisation["latitude"],
lon=pdu_lead_organisation["longitude"],
),
),
mapbox_layers=[
{
"below": "traces",
"sourcetype": "raster",
"sourceattribution": "Source: Office for National Statistics licensed under the Open Government Licence v.3.0",
"source": [
"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
],
}
],
),

# Convert the Plotly figure to JSON
return pio.to_json(fig)


def generate_dataframe_and_aggregated_distance_data_from_cases(filtered_cases):
"""
Returns a dataframe of all Cases, location data and distances with aggregated results
Returns it as a tuple of two dataframes, one for the cases and the distances from the lead organisation, the other for the aggregated distances (max, mean, median, std)
"""
geo_df = pd.DataFrame(list(filtered_cases))
# Ensure location is a tuple of (easting, northing)

if not geo_df.empty:
if "location_wgs84" in geo_df.columns:
geo_df["longitude"] = geo_df["location_wgs84"].apply(lambda loc: loc.x)
geo_df["latitude"] = geo_df["location_wgs84"].apply(lambda loc: loc.y)
geo_df["distance_km"] = geo_df["distance_from_lead_organisation"].apply(
lambda d: d.km
)
geo_df["distance_mi"] = geo_df["distance_from_lead_organisation"].apply(
lambda d: d.mi
)

max_distance_travelled_km = geo_df["distance_km"].min()
mean_distance_travelled_km = geo_df["distance_km"].mean()
median_distance_travelled_km = geo_df["distance_km"].median()
std_distance_travelled_km = geo_df["distance_km"].std()

max_distance_travelled_mi = geo_df["distance_mi"].min()
mean_distance_travelled_mi = geo_df["distance_mi"].mean()
median_distance_travelled_mi = geo_df["distance_mi"].median()
std_distance_travelled_mi = geo_df["distance_mi"].std()

return {
"max_distance_travelled_km": f"{max_distance_travelled_km:.2f}",
"mean_distance_travelled_km": f"{mean_distance_travelled_km:.2f}",
"median_distance_travelled_km": f"{median_distance_travelled_km:.2f}",
"std_distance_travelled_km": f"{std_distance_travelled_km:.2f}",
"max_distance_travelled_mi": f"{max_distance_travelled_mi:.2f}",
"mean_distance_travelled_mi": f"{mean_distance_travelled_mi:.2f}",
"median_distance_travelled_mi": f"{median_distance_travelled_mi:.2f}",
"std_distance_travelled_mi": f"{std_distance_travelled_mi:.2f}",
}, geo_df
else:
geo_df["pk"] = None
geo_df["longitude"] = None
geo_df["latitude"] = None
geo_df["distance_km"] = 0
geo_df["distance_mi"] = 0
return {
"max_distance_travelled_km": f"~",
"mean_distance_travelled_km": f"~",
"median_distance_travelled_km": f"~",
"std_distance_travelled_km": f"~",
"max_distance_travelled_mi": f"~",
"mean_distance_travelled_mi": f"~",
"median_distance_travelled_mi": f"~",
"std_distance_travelled_mi": f"~",
}, geo_df
31 changes: 27 additions & 4 deletions project/npda/general_functions/rcpch_nhs_organisations.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,34 @@ def get_all_pz_codes_with_their_trust_and_primary_organisation() -> (
List[Tuple[str, str]]: A list of tuples containing the ODS code and name of NHS organisations.
"""

request_url = f"{settings.RCPCH_NHS_ORGANISATIONS_API_URL}/paediatric_diabetes_units/parent/"
request_url = (
f"{settings.RCPCH_NHS_ORGANISATIONS_API_URL}/paediatric_diabetes_units/parent/"
)

headers = {
"Ocp-Apim-Subscription-Key": settings.RCPCH_NHS_ORGANISATIONS_API_KEY
}
headers = {"Ocp-Apim-Subscription-Key": settings.RCPCH_NHS_ORGANISATIONS_API_KEY}

response = requests.get(request_url, headers=headers, timeout=10)
response.raise_for_status()

return response.json()


def fetch_organisation_by_ods_code(ods_code: str) -> Dict[str, Any]:
"""
This function returns an NHS organisation from the RCPCH dataset that is affiliated with a paediatric diabetes unit.

Args:
ods_code (str): The ODS code of the NHS organisation.

Returns:
Dict[str, Any]: A dictionary containing the details of the NHS organisation.
"""

request_url = (
f"{settings.RCPCH_NHS_ORGANISATIONS_API_URL}/organisations/{ods_code}/"
)

headers = {"Ocp-Apim-Subscription-Key": settings.RCPCH_NHS_ORGANISATIONS_API_KEY}

response = requests.get(request_url, headers=headers, timeout=10)
response.raise_for_status()
Expand Down
Loading
Loading