diff --git a/proc_dash/app.py b/proc_dash/app.py index e402f7f..9760c50 100644 --- a/proc_dash/app.py +++ b/proc_dash/app.py @@ -298,16 +298,17 @@ def update_matching_rows(columns, virtual_data): Output("interactive-datatable", "filter_query"), Output("session-dropdown", "value"), Output("phenotypic-column-plotting-dropdown", "value"), + Output("session-toggle-switch", "value"), ], Input("memory-filename", "data"), prevent_initial_call=True, ) def reset_selections(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 file contents change (i.e., selected new CSV for upload), reset displayed file name and selection values related to data filtering or plotting. + Reset will occur regardless of whether there is an issue processing the selected file. """ - return f"Input file: {filename}", "", "", None + return f"Input file: {filename}", "", "", None, False @app.callback( @@ -413,12 +414,16 @@ def display_phenotypic_column_dropdown(parsed_data): [ Input("phenotypic-column-plotting-dropdown", "value"), Input("interactive-datatable", "derived_virtual_data"), + Input("session-toggle-switch", "value"), ], State("memory-overview", "data"), prevent_initial_call=True, ) def plot_phenotypic_column( - selected_column: str, virtual_data: list, parsed_data: dict + selected_column: str, + virtual_data: list, + session_switch_value: bool, + parsed_data: dict, ): """When a column is selected from the dropdown, generate a histogram of the column values.""" if selected_column is None or parsed_data.get("type") != "phenotypic": @@ -433,8 +438,13 @@ def plot_phenotypic_column( else: data_to_plot = virtual_data + if session_switch_value: + color = "session" + else: + color = None + return plot.plot_phenotypic_column_histogram( - pd.DataFrame.from_dict(data_to_plot), selected_column + pd.DataFrame.from_dict(data_to_plot), selected_column, color ), {"display": "block"} @@ -474,5 +484,18 @@ def generate_column_summary( ) +@app.callback( + Output("session-toggle-switch", "style"), + Input("phenotypic-column-plotting-dropdown", "value"), + prevent_initial_call=True, +) +def display_session_switch(selected_column: str): + """When a column is selected from the dropdown, display switch to enable/disable stratifying the plot by session.""" + if selected_column is None: + return {"display": "none"} + + return {"display": "block"} + + if __name__ == "__main__": app.run_server(debug=True) diff --git a/proc_dash/layout.py b/proc_dash/layout.py index bbb2e86..16de789 100644 --- a/proc_dash/layout.py +++ b/proc_dash/layout.py @@ -358,6 +358,16 @@ def column_summary_card(): ) +def session_toggle_switch(): + """Generates a switch that toggles whether the column plot is stratified by session.""" + return dbc.Switch( + id="session-toggle-switch", + label="Stratify plot by session", + value=False, + style={"display": "none"}, + ) + + def construct_layout(): """Generates the overall dashboard layout.""" return html.Div( @@ -447,7 +457,15 @@ def construct_layout(): ), width=8, ), - dbc.Col(column_summary_card()), + dbc.Col( + dbc.Stack( + [ + column_summary_card(), + session_toggle_switch(), + ], + gap=3, + ), + ), ], align="center", ), diff --git a/proc_dash/plotting.py b/proc_dash/plotting.py index f250051..52d87e0 100644 --- a/proc_dash/plotting.py +++ b/proc_dash/plotting.py @@ -1,6 +1,7 @@ from itertools import product from textwrap import wrap +import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go @@ -13,7 +14,7 @@ "FAIL": CMAP[9], "UNAVAILABLE": CMAP[10], } -HISTO_COLOR = CMAP[0] +CMAP_PHENO = px.colors.qualitative.Vivid # TODO: could use util.PIPE_COMPLETE_STATUS_SHORT_DESC to define below variable instead PIPELINE_STATUS_ORDER = ["SUCCESS", "FAIL", "UNAVAILABLE"] @@ -133,15 +134,37 @@ def populate_empty_records_pipeline_status_plot( def plot_phenotypic_column_histogram( - data: pd.DataFrame, column: str + data: pd.DataFrame, column: str, color: str = None ) -> go.Figure: - """Returns a histogram of the values of the given column across records in the active datatable.""" + """ + Returns a histogram of the values of the given column across records in the active datatable. + If the column data are continuous, a box plot of the distribution is also plotted as a subplot. + """ + axis_title_gap = 8 # reduce gap between axis title and axis tick labels title_fsize = 18 + if np.issubdtype(data[column].dtype, np.number): + # NOTE: The default box plot on-hover labels mean/q1/q3 etc. are a bit verbose, but there's no way to customize how they are displayed yet + # (See https://github.com/plotly/plotly.js/pull/3685) + marginal = "box" + else: + marginal = None + fig = px.histogram( wrap_df_column_values(df=data, column=column, width=30), x=column, - color_discrete_sequence=[HISTO_COLOR], - text_auto=True, + color=color, + color_discrete_sequence=CMAP_PHENO, + marginal=marginal, + ) + # Customize box plot appearance and on-hover labels for data points (display participant_id as well as the column value (x)) + fig.update_traces( + boxmean=True, + notched=False, + jitter=1, + customdata=data["participant_id"], + meta=column, + hovertemplate="participant_id: %{customdata}
%{meta}=%{x}", + selector={"type": "box"}, ) fig.update_layout( margin=LAYOUTS["margin"], @@ -151,5 +174,13 @@ def plot_phenotypic_column_histogram( **LAYOUTS["title"], }, bargap=0.1, + barmode="group", + boxgap=0.1, + # Reduce gap between legend and plot area + # (https://plotly.com/python-api-reference/generated/plotly.graph_objects.Layout.html#plotly.graph_objects.layout.Legend.x) + legend={"x": 1.01}, ) + fig.update_xaxes(title_standoff=axis_title_gap) + fig.update_yaxes(title_standoff=axis_title_gap) + return fig