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

editable table #7

Merged
merged 4 commits into from
Nov 6, 2024
Merged
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
2 changes: 1 addition & 1 deletion PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,4 @@ notebook or Python interface.

Figma: https://www.figma.com/design/bpaumciR1DPhGejjCXER86/ISARIC-ARCmapper

![ARCmapper UX mockup](images/arcmapper-mockup.png)
![ARCmapper UX mockup](images/arcmapper-mockup.png)
243 changes: 86 additions & 157 deletions src/arcmapper/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
from dash import dcc, html, ctx, callback, dash_table, Input, Output, State
import dash_bootstrap_components as dbc


from .components import select
from .components import arc_form, upload_form
from .util import read_upload_data
from .dictionary import read_data_dictionary
from .strategies import map as map_data_dictionary_to_arc
Expand All @@ -15,6 +14,10 @@
app = dash.Dash("arcmapper", external_stylesheets=[dbc.themes.BOOTSTRAP])
app.title = "ARCMapper"

PAGE_SIZE = 25
OK = "✅"
HIGHLIGHT_COLOR = "bisque"

navbar = dbc.Navbar(
dbc.Container(
[
Expand Down Expand Up @@ -43,159 +46,23 @@
dark=True,
)

upload_form = dbc.Container(
output_table = dbc.Container(
html.Div(
dbc.Form(
[
dbc.Row(
[
html.P(
[
html.Strong("SOURCE: "),
"ARCmapper supports data dictionaries in CSV, XLSX, JSON schema, or you can upload sample data.",
]
)
]
),
dbc.Row(
dbc.Col(
dbc.Switch(
id="upload-is-sample-data",
label="Uploaded file is sample data, not a data dictionary. Data will be sent to server, only use on local deployments",
disabled=True,
)
)
),
dbc.Row(
[
dbc.Label("Responses column", width="auto"),
dbc.Col(
dbc.Input(
id="upload-col-responses",
type="text",
value="Choices, Calculations, OR Slider Labels",
),
className="me-3",
),
dbc.Label("Description column", width="auto"),
dbc.Col(
dbc.Input(
id="upload-col-description",
type="text",
placeholder="Defaults to longest column",
value="Field Label",
),
className="me-3",
),
dbc.Col(
dcc.Upload(
id="upload-input-file",
children=html.Div(
"Drag and drop or select file",
style={
"border": "1px dashed silver",
"padding": "0.3em",
},
),
),
className="me-3",
),
dbc.Col(
dbc.Button(
"Upload", id="upload-btn", color="primary", n_clicks=0
)
),
dcc.Store(id="upload-data-dictionary"),
],
className="g-2",
),
dbc.Row(id="upload-status"),
]
),
style={
"border": "1px solid silver",
"border-radius": "0.4em",
"padding": "1em",
},
),
style={"margin-top": "1em"},
dbc.Row(id="output"),
style={"padding": "0.5em", "border": "1px solid silver", "borderRadius": "5px"},
)
)

arc_form = dbc.Container(
html.Div(
dbc.Form(
final_mapping_form = dbc.Container(
dbc.Row(
dbc.Col(
[
dbc.Row(
html.P(
[
html.Strong("TARGET: "),
"Choose the target ARC version and select method and method parameters",
]
)
),
dbc.Row(
[
dbc.Label("Target ARC version", width="auto"),
dbc.Col(
select("arc-version", ["1.0.0", "1.0.1"]),
className="me-3",
),
dbc.Label("Mapping method", width="auto"),
dbc.Col(
dbc.Select(
id="arc-mapping-method",
options=[
{"label": "TF-IDF", "value": "tf-idf"},
{
"label": "Sentence Transformers",
"value": "sbert",
},
],
value="tf-idf",
),
className="me-3",
),
dbc.Label("Number of matches", width="auto"),
dbc.Col(
dbc.Input(
id="arc-num-matches",
type="number",
min=2,
max=10,
step=1,
value=3,
),
),
dbc.Label("Threshold", width="auto"),
dbc.Col(
dbc.Input(
id="arc-threshold",
type="number",
min=0.1,
max=1,
step=0.1,
value=0.3,
),
),
dbc.Col(dbc.Button("Map to ARC", id="map-btn"), width="auto"),
],
className="g-2",
dcc.Download(id="download-mapping"),
dbc.Button(
"Download mapping", id="download-btn", style={"marginTop": "1em"}
),
]
),
style={
"border": "1px solid silver",
"border-radius": "0.4em",
"padding": "1em",
},
),
style={"margin-top": "1em"},
)

output_table = dbc.Container(
html.Div(
dbc.Row(id="output"),
style={"padding": "0.5em", "border": "1px solid silver", "borderRadius": "5px"},
)
)
)

Expand All @@ -217,9 +84,10 @@ def upload_data_dictionary(
col_responses,
col_description,
):
ok = dbc.Alert("Upload successful", color="success")
ok = dbc.Alert("Upload successful", color="success", style={"marginTop": "1em"})

def err(msg): return dbc.Alert(msg, color="danger")
def err(msg):
return dbc.Alert(msg, color="danger", style={"marginTop": "1em"})

if ctx.triggered_id == "upload-btn" and upload_contents is not None:
try:
Expand All @@ -231,7 +99,12 @@ def err(msg): return dbc.Alert(msg, color="danger")
return {}, err("Description column not found")
if col_responses not in df.columns:
return {}, err("Responses column not found")
data = read_data_dictionary(df, description_field=col_description, response_field=col_responses, response_func="redcap")
data = read_data_dictionary(
df,
description_field=col_description,
response_field=col_responses,
response_func="redcap",
)
return data.to_json(), ok

except Exception as e:
Expand All @@ -253,23 +126,79 @@ def invoke_map_arc(data, _, version, method, num_matches):
if ctx.triggered_id == "map-btn":
arc = read_arc_schema(version)
dictionary = pd.read_json(data)

mapped_data = map_data_dictionary_to_arc(method, dictionary, arc, num_matches)
data = mapped_data.to_dict("records")
for i, row in enumerate(data):
row["id"] = i
return (
dash_table.DataTable(
id="mapping",
data=mapped_data.to_dict("records"),
columns=[{"name": i, "id": i} for i in mapped_data.columns],
data=data,
columns=[
{"name": i, "id": i, "editable": i != "status"}
for i in mapped_data.columns
],
editable=True,
style_data={
"whiteSpace": "normal",
"height": "auto",
"fontSize": "90%",
},
style_table={"overflowX": "auto"},
page_size=25,
page_size=PAGE_SIZE,
),
)
return html.Span("No data to see here")
else:
return html.Span("No data to see here")


@callback(
Output("mapping", "data"),
Output("mapping", "style_data_conditional"),
Output("mapping", "active_cell"),
Input("mapping", "data"),
Input("mapping", "active_cell"),
prevent_initial_call=True,
)
def handle_status(data, active_cell):
if active_cell and active_cell.get("column_id") == "status":
i = active_cell.get("row_id")
row = data[i]
row["status"] = OK if row["status"] == "-" else "-"
else:
raise dash.exceptions.PreventUpdate
highlighted_rows = [i for i in range(len(data)) if data[i]["status"] == OK]
return (
data, # mapping data
[
{
"if": {
"filter_query": " || ".join(
f"{{id}} = {k}" for k in highlighted_rows
)
},
"backgroundColor": HIGHLIGHT_COLOR,
}
], # style_data_conditional
False, # unsets active cell, allowing the cell to be clicked immediately again
)


@callback(
Output("download-mapping", "data"),
Input("download-btn", "n_clicks"),
State("mapping", "data"),
prevent_initial_call=True,
)
def handle_download(_, data):
if ctx.triggered_id == "download-btn":
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


app.layout = html.Div([navbar, upload_form, arc_form, output_table])
app.layout = html.Div([navbar, upload_form, arc_form, output_table, final_mapping_form])
server = app.server
1 change: 0 additions & 1 deletion src/arcmapper/arc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ def arc_schema_url(arc_version: str) -> str:
return f"https://github.com/ISARICResearch/DataPlatform/raw/refs/heads/main/ARCH/ARCH{arc_version}/ARCH.csv"



def read_arc_schema(
arc_version_or_file: str, preset: str | None = None
) -> pd.DataFrame:
Expand Down
Loading