Skip to content

Commit

Permalink
Merge pull request #27 from neurobagel/feat-17/add-pipe-status-graph
Browse files Browse the repository at this point in the history
Add graph components for plotting pipeline completion status counts of dataset
  • Loading branch information
alyssadai authored Apr 17, 2023
2 parents 7e3a799 + 846b0c4 commit 89cd6de
Show file tree
Hide file tree
Showing 3 changed files with 318 additions and 85 deletions.
255 changes: 196 additions & 59 deletions proc_dash/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,31 @@
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

import proc_dash.plotting as plot
import proc_dash.utility as util
from dash import Dash, ctx, dash_table, dcc, html

app = Dash(
__name__,
external_stylesheets=["https://codepen.io/chriddyp/pen/bWLwgP.css"],
)
EMPTY_FIGURE_PROPS = {"data": [], "layout": {}, "frames": []}

app = Dash(__name__, external_stylesheets=[dbc.themes.FLATLY])


app.layout = html.Div(
children=[
html.H2(children="Neuroimaging Derivatives Status Dashboard"),
dcc.Store(id="memory"),
dcc.Upload(
id="upload-data",
children=html.Button("Drag and Drop or Select .csv File"),
children=dbc.Button(
"Drag and Drop or Select .csv File", color="secondary"
), # TODO: Constrain click responsive area of button
style={"margin-top": "10px", "margin-bottom": "10px"},
multiple=False,
),
html.Div(
id="output-data-upload",
children=[
html.H6(id="input-filename"),
html.H4(id="input-filename"),
html.Div(
children=[
html.Div(id="total-participants"),
Expand All @@ -49,92 +52,176 @@
page_size=50,
fixed_rows={"headers": True},
style_table={"height": "300px", "overflowY": "auto"},
), # TODO: Treat all columns as strings to standardize filtering syntax?
style_cell={
"fontSize": 13 # accounts for font size inflation by dbc theme
},
),
# NOTE: Could cast columns to strings for the datatable to standardize filtering syntax,
# but this results in undesirable effects (e.g., if there is session 1 and session 11,
# a query for "1" would return both)
],
style={"margin-top": "10px", "margin-bottom": "10px"},
),
dbc.Card(
dbc.Row(
[
# TODO: Put label and dropdown in same row
html.Div(
[
dbc.Label("Filter by multiple sessions:"),
dcc.Dropdown(
id="session-dropdown",
options=[],
multi=True,
placeholder="Select one or more available sessions to filter by",
# TODO: Can set `disabled=True` here to prevent any user interaction before file is uploaded
),
]
dbc.Col(
dbc.Form(
[
# TODO: Put label and dropdown in same row
html.Div(
[
dbc.Label(
"Filter by multiple sessions:",
html_for="session-dropdown",
className="mb-0",
),
dcc.Dropdown(
id="session-dropdown",
options=[],
multi=True,
placeholder="Select one or more available sessions to filter by",
# TODO: Can set `disabled=True` here to prevent any user interaction before file is uploaded
),
],
className="mb-2", # Add margin to keep dropdowns spaced apart
),
html.Div(
[
dbc.Label(
"Selection operator:",
html_for="select-operator",
className="mb-0",
),
dcc.Dropdown(
id="select-operator",
options=[
{
"label": "AND",
"value": "AND",
"title": "Show only participants with all selected sessions.",
},
{
"label": "OR",
"value": "OR",
"title": "Show participants with any of the selected sessions.",
},
],
value="AND",
clearable=False,
# TODO: Can set `disabled=True` here to prevent any user interaction before file is uploaded
),
],
className="mb-2",
),
],
)
),
html.Div(
[
dbc.Label("Selection operator:"),
dcc.Dropdown(
id="select-operator",
options=[
{
"label": "AND",
"value": "AND",
"title": "Show only participants with all selected sessions.",
},
{
"label": "OR",
"value": "OR",
"title": "Show participants with any of the selected sessions.",
},
],
value="AND",
clearable=False,
# TODO: Can set `disabled=True` here to prevent any user interaction before file is uploaded
dbc.Col(
dbc.Card(
dbc.CardBody(
[
html.H5(
"Legend: Processing status",
className="card-title",
),
html.P(
children=util.construct_legend_str(
util.PIPE_COMPLETE_STATUS_SHORT_DESC
),
style={
"whiteSpace": "pre" # preserve newlines
},
className="card-text",
),
]
),
]
)
),
]
),
]
dbc.Row(
[
# NOTE: Legend displayed for both graphs so that user can toggle visibility of status data
dbc.Col(
dcc.Graph(
id="fig-pipeline-status", style={"display": "none"}
)
),
dbc.Col(
dcc.Graph(
id="fig-pipeline-status-all-ses",
style={"display": "none"},
)
),
],
),
],
style={"padding": "10px 10px 10px 10px"},
)


@app.callback(
[
Output("interactive-datatable", "columns"),
Output("interactive-datatable", "data"),
Output("memory", "data"),
Output("total-participants", "children"),
Output("session-dropdown", "options"),
],
[
Input("upload-data", "contents"),
State("upload-data", "filename"),
Input("session-dropdown", "value"),
Input("select-operator", "value"),
],
)
def update_outputs(contents, filename, session_values, operator_value):
def process_bagel(contents, filename):
"""
From the contents of a correctly-formatted uploaded .csv file, parse and store the pipeline overview
data as a dataframe and update the session dropdown options and displayed total participants count.
Returns any errors encountered during input file processing as a user-friendly message.
"""
if contents is None:
return None, None, "Upload a CSV file to begin.", []

data, total_subjects, sessions, upload_error = util.parse_csv_contents(
contents=contents, filename=filename
)
return None, "Upload a CSV file to begin.", []
try:
data, total_subjects, sessions, upload_error = util.parse_csv_contents(
contents=contents, filename=filename
)
except Exception:
upload_error = "Something went wrong while processing this file."

if upload_error is not None:
return None, None, f"Error: {upload_error} Please try again.", []
return None, f"Error: {upload_error} Please try again.", []

report_total_subjects = f"Total number of participants: {total_subjects}"
session_opts = [{"label": ses, "value": ses} for ses in sessions]

return data.to_dict("records"), report_total_subjects, session_opts


@app.callback(
[
Output("interactive-datatable", "columns"),
Output("interactive-datatable", "data"),
],
[
Input("memory", "data"),
Input("session-dropdown", "value"),
Input("select-operator", "value"),
],
)
def update_outputs(parsed_data, session_values, operator_value):
if parsed_data is None:
return None, None

data = pd.DataFrame.from_dict(parsed_data)

if session_values:
data = util.filter_by_sessions(
data=data,
session_values=session_values,
operator_value=operator_value,
)

tbl_columns = [{"name": i, "id": i} for i in data.columns]
tbl_data = data.to_dict("records")
tbl_total_subjects = f"Total number of participants: {total_subjects}"
session_opts = [{"label": ses, "value": ses} for ses in sessions]

return tbl_columns, tbl_data, tbl_total_subjects, session_opts
return tbl_columns, tbl_data


@app.callback(
Expand Down Expand Up @@ -171,13 +258,63 @@ def update_matching_participants(columns, virtual_data):
State("upload-data", "filename"),
prevent_initial_call=True,
)
def reset_table(contents, filename):
"""If file contents change (i.e., new CSV uploaded), reset file name and filter selection values."""
def reset_selections(contents, filename):
"""
If file contents change (i.e., selected new CSV for upload), reset displayed file name and dropdown filter
selection values. Reset will occur regardless of whether there is an issue processing the selected file.
"""
if ctx.triggered_id == "upload-data":
return f"Input file: {filename}", "", ""

raise PreventUpdate


@app.callback(
[
Output("fig-pipeline-status-all-ses", "figure"),
Output("fig-pipeline-status-all-ses", "style"),
],
Input("memory", "data"),
prevent_initial_call=True,
)
def generate_overview_status_fig_for_participants(parsed_data):
"""
If new dataset uploaded, generate stacked bar plot of pipeline_complete statuses per session,
grouped by pipeline. Provides overview of the number of participants with each status in a given session,
per processing pipeline.
"""
if parsed_data is None:
return EMPTY_FIGURE_PROPS, {"display": "none"}

return plot.plot_pipeline_status_by_participants(
pd.DataFrame.from_dict(parsed_data)
), {"display": "block"}


@app.callback(
[
Output("fig-pipeline-status", "figure"),
Output("fig-pipeline-status", "style"),
],
Input(
"interactive-datatable", "data"
), # Input not triggered by datatable frontend filtering
prevent_initial_call=True,
)
def update_overview_status_fig_for_records(data):
"""
When visible data in the overview datatable is updated (excluding built-in frontend datatable filtering
but including component filtering for multiple sessions), generate stacked bar plot of pipeline_complete
statuses aggregated by pipeline. Counts of statuses in plot thus correspond to unique records (unique
participant-session combinations).
"""
if data is not None:
return plot.plot_pipeline_status_by_records(
pd.DataFrame.from_dict(data)
), {"display": "block"}

return EMPTY_FIGURE_PROPS, {"display": "none"}


if __name__ == "__main__":
app.run_server(debug=True)
Loading

0 comments on commit 89cd6de

Please sign in to comment.