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

[ENH] Add help text for built-in datatable filtering syntax and show column datatypes #128

Merged
merged 7 commits into from
Jan 17, 2024
68 changes: 63 additions & 5 deletions digest/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,40 @@ def display_dataset_metadata(parsed_data):
)


@app.callback(
Output("filtering-syntax-help", "style"),
Input("memory-overview", "data"),
prevent_initial_call=True,
)
def display_filtering_syntax_help(parsed_data):
"""When successfully uploaded data changes, show collapsible help text for datatable filtering syntax."""
if parsed_data is None:
return {"display": "none"}
return {"display": "block"}


@app.callback(
[
Output("filtering-syntax-help-collapse", "is_open"),
Output("filtering-syntax-help-icon", "className"),
],
Input("filtering-syntax-help-button", "n_clicks"),
[
State("filtering-syntax-help-collapse", "is_open"),
State("filtering-syntax-help-icon", "className"),
],
prevent_initial_call=True,
)
def toggle_filtering_syntax_collapse_content(n_clicks, is_open, class_name):
"""When filtering syntax help button is clicked, toggle visibility of the help text."""
if n_clicks:
if class_name == "bi bi-caret-right-fill me-1":
return not is_open, "bi bi-caret-down-fill me-1"
return not is_open, "bi bi-caret-right-fill me-1"
alyssadai marked this conversation as resolved.
Show resolved Hide resolved

return is_open, class_name


@app.callback(
[
Output("session-dropdown", "options"),
Expand Down Expand Up @@ -251,7 +285,13 @@ def update_outputs(
status_values=pipeline_selected_filters,
)
tbl_columns = [
{"name": i, "id": i, "hideable": True} for i in data.columns
{
"name": i,
"id": i,
"hideable": True,
"type": util.type_column_for_dashtable(data[i]),
}
for i in data.columns
]
tbl_data = data.to_dict("records")

Expand Down Expand Up @@ -447,34 +487,52 @@ def plot_phenotypic_column(
@app.callback(
[
Output("column-summary-title", "children"),
Output("column-data-type", "children"),
Output("column-summary", "children"),
Output("column-summary-card", "style"),
],
[
Input("phenotypic-column-plotting-dropdown", "value"),
Input("interactive-datatable", "derived_virtual_data"),
],
State("memory-overview", "data"),
[
State("memory-overview", "data"),
State("interactive-datatable", "columns"),
],
prevent_initial_call=True,
)
def generate_column_summary(
selected_column: str, virtual_data: list, parsed_data: dict
selected_column: str,
virtual_data: list,
parsed_data: dict,
datatable_columns: list,
):
"""When a column is selected from the dropdown, generate summary stats of the column values."""
if selected_column is None or parsed_data.get("type") != "phenotypic":
return None, None, {"display": "none"}
return None, None, None, {"display": "none"}

column_datatype = next(
(
column.get("type", None)
for column in datatable_columns
if column["name"] == selected_column
),
None,
)

# If no data is visible in the datatable (i.e., zero matches), return an informative message
if not virtual_data:
return (
selected_column,
column_datatype,
html.I("No matching records available to compute value summary"),
{"display": "block"},
)

column_data = pd.DataFrame.from_dict(virtual_data)[selected_column]
return (
selected_column,
column_datatype,
util.generate_column_summary_str(column_data),
{"display": "block"},
)
Expand All @@ -494,4 +552,4 @@ def display_session_switch(selected_column: str):


if __name__ == "__main__":
app.run_server(debug=True)
app.run(debug=True)
94 changes: 86 additions & 8 deletions digest/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,16 @@ def status_legend_card():
id="title-tooltip-target",
),
dbc.Tooltip(
dcc.Markdown(
"These are the recommended status definitions for processing progress. "
"For more details, see the [schema for an imaging digest file](https://github.com/neurobagel/digest/blob/main/schemas/bagel_schema.json)."
html.P(
[
"These are the recommended status definitions for processing progress. For more details, see the ",
html.A(
"schema for an imaging digest file",
href="https://github.com/neurobagel/digest/blob/main/schemas/bagel_schema.json",
target="_blank",
),
],
className="mb-0",
),
autohide=False,
target="title-tooltip-target",
Expand All @@ -247,6 +254,63 @@ def status_legend_card():
)


def filtering_syntax_help_collapse():
"""Generates the collapse element that displays syntax help for built-in datatable filtering."""
return html.Div(
[
dbc.Button(
[
html.I(
id="filtering-syntax-help-icon",
className="bi bi-caret-right-fill me-1",
),
"Built-in datatable filtering syntax",
],
color="link",
id="filtering-syntax-help-button",
n_clicks=0,
className="ps-0",
),
dbc.Collapse(
dbc.Card(
html.P(
[
dcc.Markdown(
"To filter column values in the table below, supported operators include: `contains` (default), `=`, `>`, `<`, `>=`, `<=`, `!=`. To filter a column for missing (empty) values, use `is blank`.",
alyssadai marked this conversation as resolved.
Show resolved Hide resolved
style={
"white-space": "pre",
"display": "inline-flex",
},
# NOTE: dcc.Markdown actually has problems rendering custom padding/margin (https://community.plotly.com/t/dcc-markdown-style-margin-adjustment/15208) and by default always has bottom padding
# As a result, the below setting actually doesn't anything (but is left here in case dcc.Markdown is fixed in the future)
alyssadai marked this conversation as resolved.
Show resolved Hide resolved
className="mb-0",
),
html.P(
[
"For detailed info on the filtering syntax available, see ",
html.A(
children="here.",
href="https://dash.plotly.com/datatable/filtering",
target="_blank",
),
" To filter based on multiple sessions simultaneously, use the advanced filtering options below.",
],
className="mb-0",
),
],
className="mb-0",
),
body=True,
),
id="filtering-syntax-help-collapse",
is_open=False,
),
],
id="filtering-syntax-help",
style={"display": "none"},
)


def overview_table():
"""Generates overview table for the tasks in the tabular data (imaging or phenotypic)."""
return dash_table.DataTable(
Expand Down Expand Up @@ -285,9 +349,14 @@ def advanced_filter_form_title():
id="tooltip-question-target",
),
dbc.Tooltip(
dcc.Markdown(
"Filter based on multiple sessions simultaneously. "
"Note that any data filters selected here will always be applied *before* any column filters specified directly in the data table."
html.P(
[
"Filter based on multiple sessions simultaneously. "
"Note that any data filters selected here will always be applied ",
html.I("before"),
" any column filters specified directly in the data table.",
],
className="mb-0",
),
target="tooltip-question-target",
),
Expand Down Expand Up @@ -389,9 +458,17 @@ def column_summary_card():
className="card-title",
),
html.P(
id="column-summary",
style={"whiteSpace": "pre-wrap"}, # preserve newlines
[
"column data type: ",
html.Span(id="column-data-type"),
html.P(),
html.P(
id="column-summary",
className="mb-0",
),
],
className="card-text",
style={"whiteSpace": "pre-wrap"},
),
],
),
Expand Down Expand Up @@ -440,6 +517,7 @@ def construct_layout():
),
]
),
filtering_syntax_help_collapse(),
overview_table(),
],
style={"margin-top": "10px", "margin-bottom": "10px"},
Expand Down
17 changes: 17 additions & 0 deletions digest/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ def reset_column_dtypes(data: pd.DataFrame) -> pd.DataFrame:
return data_retyped


def type_column_for_dashtable(df_column: pd.Series) -> str:
"""
Determines the appropriate type for a given column for use in a dash datatable, using Pandas and the column dtype.
This is needed because dash datatable does not automatically infer column data types, and will treat all columns as 'text' for filtering purposes by default
(the actual default column type is 'any' if not defined manually).

See also https://dash.plotly.com/datatable/filtering.

# TODO:
# - This is pretty simplistic and mainly to enable easier selection of filtering UI syntax - we might be able to remove this after switch to AG Grid
# - If needed, in future could support explicitly setting 'datetime' type as well, by applying pd.to_datetime() and catching any errors
"""
if np.issubdtype(df_column.dtype, np.number):
return "numeric"
return "text"


def construct_legend_str(status_desc: dict) -> str:
"""From a dictionary, constructs a legend-style string with multiple lines in the format of key: value."""
return "\n".join(
Expand Down