Skip to content

Commit

Permalink
Add UI for FHIR mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
abhidg committed Nov 19, 2024
1 parent ed1e448 commit 43ef899
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 31 deletions.
102 changes: 90 additions & 12 deletions src/arcmapper/app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Dash frontend for the arcmapper library"""

import io

import pandas as pd
import dash
from dash import dcc, html, ctx, callback, dash_table, Input, Output, State
import dash_bootstrap_components as dbc

from .components import arc_form, upload_form
from .fhir import merge, FHIRMapping, FHIR_RESOURCES_ONE_TO_ONE
from .util import read_upload_data
from .dictionary import read_data_dictionary
from .strategies import use_map
Expand All @@ -18,6 +21,9 @@
OK = "✅"
HIGHLIGHT_COLOR = "bisque"


FHIR_MAPPING = FHIRMapping("arc-fhir/ARC_pre_1.0.0_preset_dengue.xlsx")

navbar = dbc.Navbar(
dbc.Container(
[
Expand Down Expand Up @@ -55,14 +61,38 @@

final_mapping_form = dbc.Container(
dbc.Row(
dbc.Col(
[
dcc.Download(id="download-mapping"),
dbc.Button(
"Download mapping", id="download-btn", style={"marginTop": "1em"}
),
]
)
[
dbc.Col(
[
dcc.Download(id="download-intermediate-mapping"),
dbc.Button(
"Load", id="load-intermediate", style={"marginTop": "1em"}
),
dbc.Button(
"Save",
id="save-intermediate",
style={"marginTop": "1em", "marginLeft": "0.6em"},
),
]
),
dbc.Col(
html.Div(
"After finalising the intermediate mapping, "
"download the mapping for FHIRflat conversion →",
style={"marginTop": "0.7em"},
)
),
dbc.Col(
[
dcc.Download(id="download-fhirflat"),
dbc.Button(
"Download FHIRflat mapping",
id="save-fhirflat",
style={"marginTop": "1em"},
),
]
),
]
)
)

Expand Down Expand Up @@ -118,10 +148,19 @@ def err(msg):
Input("map-btn", "n_clicks"),
prevent_initial_call=True,
)
def set_loading(_):
def set_loading_map(_):
return [dbc.Spinner(size="sm"), " Map to ARC"]


@callback(
Output("save-fhirflat", "children"),
Input("save-fhirflat", "n_clicks"),
prevent_initial_call=True,
)
def set_loading_save_fhirflat(_):
return [dbc.Spinner(size="sm"), " Download FHIRflat mapping"]


@callback(
Output("output", "children"),
Output("map-btn", "children", allow_duplicate=True),
Expand Down Expand Up @@ -203,19 +242,58 @@ def handle_status(data, active_cell):


@callback(
Output("download-mapping", "data"),
Input("download-btn", "n_clicks"),
Output("download-intermediate-mapping", "data"),
Input("save-intermediate", "n_clicks"),
State("mapping", "data"),
prevent_initial_call=True,
)
def handle_download(_, data):
if ctx.triggered_id == "download-btn":
if ctx.triggered_id == "save-intermediate":
df = pd.DataFrame(data)
df = df[df.status == OK].drop(columns=["status", "rank"])
return dcc.send_data_frame(df.to_csv, "arcmapper-mapping-file.csv", index=False)
else:
raise dash.exceptions.PreventUpdate


@callback(
Output("download-fhirflat", "data"),
Output("save-fhirflat", "children", allow_duplicate=True),
Input("save-fhirflat", "n_clicks"),
State("mapping", "data"),
prevent_initial_call=True,
)
def handle_download_fhir(_, data):
if ctx.triggered_id == "save-fhirflat":
df = pd.DataFrame(data)
df = df[df.status == OK].drop(columns=["status", "rank"])
dfs_by_resource = merge(df, FHIR_MAPPING)
output = io.BytesIO()
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
non_empty_resources = [
res for res in dfs_by_resource if not dfs_by_resource[res].empty
]
resource_type = [
("one-to-one" if res in FHIR_RESOURCES_ONE_TO_ONE else "one-to-many")
for res in non_empty_resources
]
index = pd.DataFrame(
{"Resources": non_empty_resources, "Resource Type": resource_type}
)
index.to_excel(writer, sheet_name="Resources")
for resource in dfs_by_resource:
if dfs_by_resource[resource].empty:
continue
dfs_by_resource[resource].to_excel(
writer, sheet_name=resource, index=False
)
data = output.getvalue()
return dcc.send_bytes(
data, "fhirflat-mapping.xlsx"
), "Download FHIRflat mapping"
else:
raise dash.exceptions.PreventUpdate


app.layout = html.Div([navbar, upload_form, arc_form, output_table, final_mapping_form])
server = app.server
3 changes: 2 additions & 1 deletion src/arcmapper/fhir.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"Specimen",
]

FHIR_RESOURCES_ONE_TO_ONE = ["Patient", "Encounter"]


class FHIRMapping:
"Loads mapping file from a Excel (XLSX) sheet"
Expand All @@ -46,7 +48,6 @@ def __init__(self, file: str | Path):

def get_resource(self, resource: str) -> pd.DataFrame:
"Gets resource from FHIR mapping Excel sheet"
resource = resource.capitalize() # capitalize first letter
if resource not in self.resources:
raise ValueError(
f"Resource '{resource}' not found, valid resources: {self.resources}"
Expand Down
29 changes: 12 additions & 17 deletions src/arcmapper/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ def match_responses(


def has_valid_response(row) -> bool:
return isinstance(row.raw_response, str) and isinstance(row.arc_response, str)
return (
isinstance(row.raw_response, str) and isinstance(row.arc_response, str)
) or (isinstance(row.raw_response, list) and isinstance(row.arc_response, list))


def infer_response_mapping(
Expand All @@ -161,9 +163,14 @@ def infer_response_mapping(
This is a simplified version of the mapping that takes place in strategies
"""
# data schema for m:
# raw_variable, raw_description, raw_response,
# arc_variable, arc_description, arc_response,
columns = [
"raw_variable",
"raw_description",
"raw_response",
"arc_variable",
"arc_description",
"arc_response",
]
out = []
sbert_model = SentenceTransformer(sbert_model)

Expand Down Expand Up @@ -196,7 +203,6 @@ def infer_response_mapping(
]
)
else:
print("multiselect mode::")
out.extend(
[
(
Expand All @@ -211,7 +217,6 @@ def infer_response_mapping(
if sr.text.lower() not in NULL_RESPONSES
]
)
print(out[-3])
else:
out.append(
(
Expand All @@ -223,17 +228,7 @@ def infer_response_mapping(
None,
)
)
df = pd.DataFrame(
out,
columns=[
"raw_variable",
"raw_description",
"raw_response",
"arc_variable",
"arc_description",
"arc_response",
],
)
df = pd.DataFrame(out, columns=columns)
return df


Expand Down
2 changes: 1 addition & 1 deletion tests/test_fhir.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_fhir_mapping():
"Specimen",
]

encounter = m.get_resource("encounter")
encounter = m.get_resource("Encounter")
assert {"arc_variable", "arc_response"} <= set(encounter.columns)


Expand Down

0 comments on commit 43ef899

Please sign in to comment.