diff --git a/dashboard/data/bmg_plate.py b/dashboard/data/bmg_plate.py index 82662408..e0e03216 100644 --- a/dashboard/data/bmg_plate.py +++ b/dashboard/data/bmg_plate.py @@ -5,12 +5,6 @@ from enum import Enum, auto -class Mode(Enum): - ACTIVATION = auto() - INHIBITION = auto() - ALL = auto() - - PlateSummary = namedtuple( "PlateSummary", [ @@ -194,7 +188,7 @@ def parse_bmg_files(files: tuple[str, io.StringIO]) -> tuple[pd.DataFrame, np.nd def calculate_activation_inhibition_zscore( values: np.ndarray, stats: dict, - mode: Mode = Mode.ALL, + mode: str, without_pos: bool = False, ) -> tuple[np.ndarray]: """ @@ -207,8 +201,7 @@ def calculate_activation_inhibition_zscore( :return: activation, inhibition and z-score values """ activation, inhibition = None, None - if mode == Mode.ACTIVATION or mode == Mode.ALL: - # NOTE: for now `without_pos` is not used + if mode == "activation": if without_pos: activation = (values - stats["mean_neg"]) / (stats["mean_neg"]) * 100 else: @@ -218,10 +211,10 @@ def calculate_activation_inhibition_zscore( * 100 ) - if mode == Mode.INHIBITION or mode == Mode.ALL: + if mode == "inhibition": inhibition = ( - 1 - ((values - stats["mean_pos"])) / (stats["mean_neg"] - stats["mean_pos"]) - ) * 100 + (values - stats["mean_neg"]) / (stats["mean_pos"] - stats["mean_neg"]) * 100 + ) z_score = (values - stats["mean_cmpd"]) / stats["std_cmpd"] @@ -229,14 +222,17 @@ def calculate_activation_inhibition_zscore( def get_activation_inhibition_zscore_dict( - df_stats: pd.DataFrame, plate_values: np.ndarray, modes: dict[Mode] + df_stats: pd.DataFrame, + plate_values: np.ndarray, + mode: str, + without_pos: bool = False, ) -> dict[str, dict[str, float]]: """ Calculates activation and inhibition for each compound in the plates. :param df_stats: dataframe with statistics for each plate :param plate_values: array with values in the plate - :param mode: list of modes to calculate activation and inhibition + :param mode: mode to calculate activation and inhibition :return: dictionary with activation and inhibition values for each compound in the plate """ stats = {} @@ -251,11 +247,8 @@ def get_activation_inhibition_zscore_dict( act_inh_dict = {} for (_, row_stats), v in zip(df_stats.iterrows(), plate_values): - mode = ( - modes[row_stats["barcode"]] if row_stats["barcode"] in modes else Mode.ALL - ) activation, inhibition, z_score = calculate_activation_inhibition_zscore( - v[0], stats, mode=mode + v[0], stats, mode, without_pos ) act_inh_dict[row_stats["barcode"]] = { "activation": activation, diff --git a/dashboard/data/combine.py b/dashboard/data/combine.py index bbd7be75..610f6abb 100644 --- a/dashboard/data/combine.py +++ b/dashboard/data/combine.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd -from dashboard.data.bmg_plate import Mode, get_activation_inhibition_zscore_dict +from dashboard.data.bmg_plate import get_activation_inhibition_zscore_dict def values_array_to_column( @@ -93,7 +93,8 @@ def combine_bmg_echo_data( echo_df: pd.DataFrame, df_stats: pd.DataFrame, plate_values: np.ndarray, - modes: dict[Mode] = None, + mode: str, + without_pos: bool = False, ) -> pd.DataFrame: """ Combine Echo data with activation and inhibition values. @@ -101,15 +102,15 @@ def combine_bmg_echo_data( :param echo_df: dataframe with Echo data :param df_stats: dataframe containing statistics for each plate :param plate_values: numpy array with activation and inhibition values - shape: (#plates, 2, 16, 24) - :param modes: dictionary with modes for each plate + :param mode: mode to calculate activation and inhibition :return: dataframe with Echo data and activation and inhibition values """ PLATE = "Destination Plate Barcode" WELL = "Destination Well" - if modes is None: - modes = dict() - act_inh_dict = get_activation_inhibition_zscore_dict(df_stats, plate_values, modes) + act_inh_dict = get_activation_inhibition_zscore_dict( + df_stats, plate_values, mode, without_pos + ) dfs = [] for barcode, values_dict in act_inh_dict.items(): activation_inhibition_df = get_activation_inhibition_zscore_df( @@ -150,7 +151,7 @@ def split_compounds_controls(df: pd.DataFrame) -> tuple[pd.DataFrame]: def aggregate_well_plate_stats( - df: pd.DataFrame, assign_x_coords: bool = False + df: pd.DataFrame, key: str, assign_x_coords: bool = False ) -> tuple[pd.DataFrame]: """ Aggregates the statistics (mean and std) per plate needed for the plots. @@ -163,25 +164,19 @@ def aggregate_well_plate_stats( """ PLATE = "Destination Plate Barcode" - ACTIVATION = "% ACTIVATION" - INHIBITION = "% INHIBITION" Z_SCORE = "Z-SCORE" stats_df = ( - df.groupby(PLATE)[[ACTIVATION, INHIBITION, Z_SCORE]] + df.groupby(PLATE)[[key, Z_SCORE]] .agg(["mean", "std", "min", "max"]) .reset_index() ) stats_df.columns = [ PLATE, - f"{ACTIVATION}_mean", - f"{ACTIVATION}_std", - f"{ACTIVATION}_min", - f"{ACTIVATION}_max", - f"{INHIBITION}_mean", - f"{INHIBITION}_std", - f"{INHIBITION}_min", - f"{INHIBITION}_max", + f"{key}_mean", + f"{key}_std", + f"{key}_min", + f"{key}_max", f"{Z_SCORE}_mean", f"{Z_SCORE}_std", f"{Z_SCORE}_min", @@ -189,7 +184,7 @@ def aggregate_well_plate_stats( ] if assign_x_coords: - for col in [ACTIVATION, INHIBITION, Z_SCORE]: + for col in [key, Z_SCORE]: stats_df = stats_df.sort_values(by=f"{col}_mean") # sort by mean stats_df[f"{col}_x"] = range(len(stats_df)) # get the x coordinates diff --git a/dashboard/pages/components.py b/dashboard/pages/components.py index 640f9a89..9d534c1e 100644 --- a/dashboard/pages/components.py +++ b/dashboard/pages/components.py @@ -22,6 +22,7 @@ dcc.Store(id="z-slider-value", storage_type="local"), dcc.Store(id="report-data-correlation-plots", storage_type="local"), dcc.Store(id="report-data-hit-validation-input", storage_type="local"), + dcc.Store(id="activation-inhibition-screening-options", storage_type="local"), ], ) diff --git a/dashboard/pages/screening/callbacks.py b/dashboard/pages/screening/callbacks.py index 15e837d3..878e5558 100644 --- a/dashboard/pages/screening/callbacks.py +++ b/dashboard/pages/screening/callbacks.py @@ -1,6 +1,7 @@ import base64 import functools import io +import json import uuid from datetime import datetime import json @@ -25,7 +26,6 @@ from dashboard.data.combine import ( aggregate_well_plate_stats, combine_bmg_echo_data, - reorder_bmg_echo_columns, split_compounds_controls, ) from dashboard.data.file_preprocessing.echo_files_parser import EchoFilesParser @@ -41,6 +41,10 @@ plot_z_per_plate, visualize_multiple_plates, ) +from dashboard.visualization.text_tables import ( + make_filter_radio_options, + make_summary_stage_datatable, +) # === STAGE 1 === @@ -288,12 +292,37 @@ def upload_echo_data( ) +def on_additional_options_change( + key: str, + formula: str, +) -> dict[str, str]: + """ + Update the additional screening options dictionary + + :param screening_feature: screening feature + :param formula: formula + :return: updated dictionary + """ + disabled = key != "activation" + options_dict = {} + options_dict["key"] = key + options_dict["feature_column"] = "% " + key.upper() + options_dict["without_pos"] = formula + return options_dict, disabled + + # === STAGE 5 === def on_summary_entry( - current_stage: int, stored_uuid: str, z_slider: float, file_storage: FileStorage -) -> tuple[pd.DataFrame, go.Figure, go.Figure, go.Figure]: + current_stage: int, + stored_uuid: str, + z_slider: float, + screening_options: dict, + file_storage: FileStorage, +) -> tuple[ + pd.DataFrame, go.Figure, go.Figure, float, float, float, float, str, html.Div +]: """ Callback for the stage 5 entry Loads the data from storage and prepares visualizations @@ -320,37 +349,41 @@ def on_summary_entry( bmg_df, bmg_vals, z_slider["z_slider_value"] ) - echo_bmg_combined = combine_bmg_echo_data(echo_df, filtered_df, filtered_vals) - drop_duplicates = ( - True # TODO: inform the user about it/allow for deciding what to do + echo_bmg_combined = combine_bmg_echo_data( + echo_df, + filtered_df, + filtered_vals, + screening_options["key"], + screening_options["without_pos"], ) - - if drop_duplicates: - echo_bmg_combined = echo_bmg_combined.drop_duplicates() + echo_bmg_combined = echo_bmg_combined.drop_duplicates() compounds_df, control_pos_df, control_neg_df = split_compounds_controls( echo_bmg_combined ) compounds_df = compounds_df.dropna() - - activation_min = round(compounds_df["% ACTIVATION"].min()) - activation_max = round(compounds_df["% ACTIVATION"].max()) - inhibition_min = round(compounds_df["% INHIBITION"].min()) - inhibition_max = round(compounds_df["% INHIBITION"].max()) - file_storage.save_file( f"{stored_uuid}_echo_bmg_combined_df.pq", compounds_df.to_parquet() ) - cmpd_plate_stats_df = aggregate_well_plate_stats(compounds_df, assign_x_coords=True) - pos_plate_stats_df = aggregate_well_plate_stats(control_pos_df) - neg_plate_stats_df = aggregate_well_plate_stats(control_neg_df) + cmpd_plate_stats_df = aggregate_well_plate_stats( + compounds_df, screening_options["feature_column"], assign_x_coords=True + ) + pos_plate_stats_df = aggregate_well_plate_stats( + control_pos_df, screening_options["feature_column"] + ) + neg_plate_stats_df = aggregate_well_plate_stats( + control_neg_df, screening_options["feature_column"] + ) plate_stats_dfs = [cmpd_plate_stats_df, pos_plate_stats_df, neg_plate_stats_df] file_storage.save_file( f"{stored_uuid}_plate_stats_df.pq", cmpd_plate_stats_df.to_parquet() ) + feature_min = round(compounds_df[screening_options["feature_column"]].min()) + feature_max = round(compounds_df[screening_options["feature_column"]].max()) + fig_z_score = plot_activation_inhibition_zscore( compounds_df, plate_stats_dfs, @@ -358,51 +391,36 @@ def on_summary_entry( (-3, 3), # z-score min and max ) - fig_activation = plot_activation_inhibition_zscore( + fig_feature = plot_activation_inhibition_zscore( compounds_df, plate_stats_dfs, - "% ACTIVATION", - (activation_min, activation_max), + screening_options["feature_column"], + (feature_min, feature_max), ) - fig_inhibition = plot_activation_inhibition_zscore( - compounds_df, - plate_stats_dfs, - "% INHIBITION", - (inhibition_min, inhibition_max), + compounds_url_df = eos_to_ecbd_link(compounds_df) + summary_stage_datatable = make_summary_stage_datatable( + compounds_url_df, screening_options["feature_column"] ) - compounds_url_df = eos_to_ecbd_link(compounds_df) + radio_options = make_filter_radio_options(screening_options["key"]) report_data = { "fig_z_score": fig_z_score.to_html(full_html=False, include_plotlyjs="cdn"), - "fig_activation": fig_activation.to_html( - full_html=False, include_plotlyjs="cdn" - ), - "fig_inhibition": fig_inhibition.to_html( - full_html=False, include_plotlyjs="cdn" - ), + "fig_feature": fig_feature.to_html(full_html=False, include_plotlyjs="cdn"), } return ( - compounds_url_df.to_dict("records"), + summary_stage_datatable, fig_z_score, - fig_activation, - fig_inhibition, + fig_feature, -3, # z_score_min, 3, # z_score_max, - activation_min, - activation_max, - inhibition_min, - inhibition_max, - # NOTE: this will be cleared in the next PR (ACT/INH into one) - False, - False, - False, - False, - False, - False, + feature_min, + feature_max, f"number of compounds: {len(compounds_df)}", + f"{screening_options['feature_column']} range:", + radio_options, report_data, ) @@ -411,10 +429,8 @@ def on_filter_radio_or_range_update( key: str, z_score_min: float, z_score_max: float, - activation_min: float, - activation_max: float, - inhibition_min: float, - inhibition_max: float, + feature_min: float, + feature_max: float, ) -> dict: """ Callback for the filter radio button update @@ -422,10 +438,8 @@ def on_filter_radio_or_range_update( :param key: key to filter by :param z_score_min: min z-score value :param z_score_max: max z-score value - :param activation_min: min activation value - :param activation_max: max activation value - :param inhibition_min: min inhibition value - :param inhibition_max: max inhibition value + :param feature_min: min feature value + :param feature_max: max feature value :return: dictionary storing the ranges of interest """ @@ -434,13 +448,9 @@ def on_filter_radio_or_range_update( if key == "z_score": report_data_csv["key_min"] = z_score_min report_data_csv["key_max"] = z_score_max - elif key == "activation": - report_data_csv["key_min"] = activation_min - report_data_csv["key_max"] = activation_max - elif key == "inhibition": - report_data_csv["key_min"] = inhibition_min - report_data_csv["key_max"] = inhibition_max - + elif key == "activation" or key == "inhibition": + report_data_csv["key_min"] = feature_min + report_data_csv["key_max"] = feature_max return report_data_csv @@ -449,8 +459,8 @@ def on_range_update( max_value: float, figure: go.Figure, stored_uuid: str, + key: dict, file_storage: FileStorage, - key: str, ) -> go.Figure: """ Callback for the z-score range update button @@ -464,8 +474,10 @@ def on_range_update( :param key: key to filter by :return: updated figure """ + if key != "Z-SCORE": + key = key["feature_column"] - if max_value is None or max_value is None or max_value < min_value: + if min_value is None or max_value is None or max_value < min_value: return figure new_figure = go.Figure(figure) @@ -509,6 +521,7 @@ def on_range_update( x=outside_range_df[f"{key}_x"], y=outside_range_df[key], customdata=np.stack((outside_range_df[PLATE], outside_range_df[WELL]), axis=-1), + text=outside_range_df["EOS"], selector=dict(name="COMPOUNDS OUTSIDE"), ) @@ -591,7 +604,6 @@ def on_report_generate_button_click( report_data_second_stage: dict, report_data_third_stage: dict, report_data_screening_summary_plots: dict, - file_storage: FileStorage, ): filename = f"screening_report_{datetime.now().strftime('%Y-%m-%d')}.html" report_data_second_stage.update(report_data_third_stage) @@ -613,7 +625,6 @@ def on_json_generate_button_click( report_data_second_stage: dict, report_data_third_stage: dict, report_data_csv: dict, - file_storage: FileStorage, ): filename = f"screening_settings_{datetime.now().strftime('%Y-%m-%d')}.json" process_settings = read_stages_stats( @@ -682,29 +693,30 @@ def register_callbacks(elements, file_storage): )(functools.partial(upload_echo_data, file_storage=file_storage)) callback( - Output("echo-bmg-combined", "data"), + Output("activation-inhibition-screening-options", "data"), + Output("activation-formula-dropdown", "disabled"), + Input("screening-feature-dropdown", "value"), + Input("activation-formula-dropdown", "value"), + )(on_additional_options_change) + + callback( + Output("compounds-data-table", "children"), Output("z-score-plot", "figure"), - Output("activation-plot", "figure"), - Output("inhibition-plot", "figure"), + Output("feature-plot", "figure"), Output("z-score-min-input", "value"), Output("z-score-max-input", "value"), - Output("activation-min-input", "value"), - Output("activation-max-input", "value"), - Output("inhibition-min-input", "value"), - Output("inhibition-max-input", "value"), - Output("z-score-min-input", "disabled"), - Output("z-score-max-input", "disabled"), - Output("activation-min-input", "disabled"), - Output("activation-max-input", "disabled"), - Output("inhibition-min-input", "disabled"), - Output("inhibition-max-input", "disabled"), + Output("feature-min-input", "value"), + Output("feature-max-input", "value"), Output("compounds-data-subtitle", "children"), + Output("tab-feature-header", "children"), + Output("filter-radio", "options"), Output("report-data-screening-summary-plots", "data"), Input(elements["STAGES_STORE"], "data"), State("user-uuid", "data"), State("z-slider-value", "data"), + State("activation-inhibition-screening-options", "data"), )(functools.partial(on_summary_entry, file_storage=file_storage)) - + # Z-SCORE callback( Output("z-score-plot", "figure", allow_duplicate=True), Input("z-score-min-input", "value"), @@ -712,33 +724,25 @@ def register_callbacks(elements, file_storage): State("z-score-plot", "figure"), State("user-uuid", "data"), prevent_initial_call=True, - )(functools.partial(on_range_update, file_storage=file_storage, key="Z-SCORE")) - callback( - Output("activation-plot", "figure", allow_duplicate=True), - Input("activation-min-input", "value"), - Input("activation-max-input", "value"), - State("activation-plot", "figure"), - State("user-uuid", "data"), - prevent_initial_call=True, - )(functools.partial(on_range_update, file_storage=file_storage, key="% ACTIVATION")) + )(functools.partial(on_range_update, key="Z-SCORE", file_storage=file_storage)) + # ACTIVATION/INHIBITION callback( - Output("inhibition-plot", "figure", allow_duplicate=True), - Input("inhibition-min-input", "value"), - Input("inhibition-max-input", "value"), - State("inhibition-plot", "figure"), + Output("feature-plot", "figure", allow_duplicate=True), + Input("feature-min-input", "value"), + Input("feature-max-input", "value"), + State("feature-plot", "figure"), State("user-uuid", "data"), + State("activation-inhibition-screening-options", "data"), prevent_initial_call=True, - )(functools.partial(on_range_update, file_storage=file_storage, key="% INHIBITION")) + )(functools.partial(on_range_update, file_storage=file_storage)) callback( Output("report-data-csv", "data"), Input("filter-radio", "value"), Input("z-score-min-input", "value"), Input("z-score-max-input", "value"), - Input("activation-min-input", "value"), - Input("activation-max-input", "value"), - Input("inhibition-min-input", "value"), - Input("inhibition-max-input", "value"), - )(functools.partial(on_filter_radio_or_range_update)) + Input("feature-min-input", "value"), + Input("feature-max-input", "value"), + )(on_filter_radio_or_range_update) callback( Output("download-echo-bmg-combined", "data"), Input("save-results-button", "n_clicks"), @@ -760,7 +764,7 @@ def register_callbacks(elements, file_storage): State("report-data-third-stage", "data"), State("report-data-screening-summary-plots", "data"), prevent_initial_call=True, - )(functools.partial(on_report_generate_button_click, file_storage=file_storage)) + )(on_report_generate_button_click) callback( Output("download-json-settings-screening", "data"), Input("generate-json-button", "n_clicks"), @@ -768,4 +772,4 @@ def register_callbacks(elements, file_storage): State("report-data-third-stage", "data"), State("report-data-csv", "data"), prevent_initial_call=True, - )(functools.partial(on_json_generate_button_click, file_storage=file_storage)) + )(on_json_generate_button_click) diff --git a/dashboard/pages/screening/report/report.html b/dashboard/pages/screening/report/report.html index c2edc2e1..817dfa76 100644 --- a/dashboard/pages/screening/report/report.html +++ b/dashboard/pages/screening/report/report.html @@ -75,10 +75,7 @@

{{fig_z_score|safe}}

- {{fig_activation|safe}} -

-

- {{fig_inhibition|safe}} + {{fig_feature|safe}}

diff --git a/dashboard/pages/screening/stages/s4_echo_input.py b/dashboard/pages/screening/stages/s4_echo_input.py index 4da4cd4b..f783f7b1 100644 --- a/dashboard/pages/screening/stages/s4_echo_input.py +++ b/dashboard/pages/screening/stages/s4_echo_input.py @@ -1,4 +1,4 @@ -from dash import html, dcc +from dash import dcc, html ECHO_DESC = """ ECHO files in ".csv" format should have [DETAILS] and (if there are exceptions in the file) [EXCEPTIONS] tags @@ -60,6 +60,82 @@ ], className="grid-2-1", ), + html.Div( + children=[ + html.Div( + children=[ + html.Div( + children=[ + html.H5("Additional options "), + html.Div( + children=[ + html.Div( + className="row", + children=[ + html.Div( + [ + html.Span("Screening feature:"), + html.Div( + children=[ + dcc.Dropdown( + options=[ + { + "label": "% ACTIVATION", + "value": "activation", + }, + { + "label": "% INHIBITION", + "value": "inhibition", + }, + ], + value="activation", + id="screening-feature-dropdown", + clearable=False, + ), + ], + ), + ], + className="col", + ), + html.Div( + [ + html.Span( + "Activation formula:" + ), + html.Div( + children=[ + dcc.Dropdown( + options=[ + { + "label": "((x - μn)/(μp - μn)) * 100%", + "value": False, + }, + { + "label": "(x - μn)/μn * 100%", + "value": True, + }, + ], + value=False, + id="activation-formula-dropdown", + clearable=False, + ), + ], + ), + ], + className="col", + ), + ], + ), + ], + ), + ], + ), + ], + ), + html.Div(), + ], + className="grid-2-1", + ), html.Div( id="echo-filenames", ), diff --git a/dashboard/pages/screening/stages/s5_summary.py b/dashboard/pages/screening/stages/s5_summary.py index 258fe5e1..84219be3 100644 --- a/dashboard/pages/screening/stages/s5_summary.py +++ b/dashboard/pages/screening/stages/s5_summary.py @@ -2,280 +2,114 @@ from dash import dash_table, dcc, html from dash.dash_table.Format import Format, Scheme -PRECISION = 5 -ACT_INH_ZSCORE_DATATABLE = dash_table.DataTable( - id="echo-bmg-combined", - columns=[ - dict(id="EOS", name="ID", type="text", presentation="markdown"), - dict(id="Destination Plate Barcode", name="Plate Barcode"), - dict(id="Destination Well", name="Well"), - dict( - id="% ACTIVATION", - name=" % ACTIVATION", - type="numeric", - format=Format(precision=PRECISION, scheme=Scheme.fixed), - ), - dict( - id="% INHIBITION", - name=" % INHIBITION", - type="numeric", - format=Format(precision=PRECISION, scheme=Scheme.fixed), - ), - dict( - id="Z-SCORE", - name=" Z-SCORE", - type="numeric", - format=Format(precision=PRECISION, scheme=Scheme.fixed), - ), - ], - style_table={"overflowX": "auto", "overflowY": "auto"}, - style_data={ - "padding-left": "10px", - "padding-right": "10px", - "width": "70px", - "autosize": {"type": "fit", "resize": True}, - "overflow": "hidden", - }, - style_cell={ - "font-family": "sans-serif", - "font-size": "12px", - }, - style_header={ - "backgroundColor": "rgb(230, 230, 230)", - "fontWeight": "bold", - }, - style_data_conditional=[ - { - "if": {"row_index": "odd"}, - "backgroundColor": "rgb(248, 248, 248)", - }, - ], - filter_action="native", - filter_options={"case": "insensitive"}, - sort_action="native", - column_selectable=False, - page_size=15, -) - - -ACT_INH_ZSCORE_TABS = dcc.Tabs( - id="summary-tabs", +SUMMARY_STAGE = html.Div( + id="summary_stage", + className="container", children=[ - dcc.Tab( - label="Z-Score", + html.Div( + className="mb-5 mt-4", children=[ - html.Div( - className="my-4", - children=[ - html.H5("Z-Score range:"), - html.Div( - className="row mb-3", - children=[ - html.Div( - className="col-md-6 mt-1", - children=[ - html.Div( - [ - html.Div( - className="row", - children=[ - html.Div( - className="col", - children=[ - html.Label( - "minimum value:" - ), - dcc.Input( - placeholder="min value", - type="number", - id="z-score-min-input", - className="stats-input", - disabled=True, - ), - ], - ), - html.Div( - className="col", - children=[ - html.Label( - "maximum value:" - ), - dcc.Input( - placeholder="max value", - type="number", - id="z-score-max-input", - className="stats-input", - disabled=True, - ), - ], - ), - ], - ) - ], - ) - ], - ), - ], - ), - html.Div( - children=[ - dcc.Loading( - id="loading-z-score-plot", - children=[ - dcc.Graph( - id="z-score-plot", - figure={}, - ), - ], - type="circle", - ), - ], - ), - ], + html.H5("Filter results:"), + html.H6( + "The compounds in the csv report will be ones outside the range of the selected filter." + ), + dcc.RadioItems( + [], + value="no_filter", + style={"display": "flex"}, + id="filter-radio", ), ], ), - dcc.Tab( - label="Activation", + dcc.Tabs( + id="summary-tabs", children=[ - html.Div( - className="my-4", + dcc.Tab( + label="Z-Score", children=[ - html.H5("Activation range:"), html.Div( - className="row mb-3", + className="my-4", children=[ + html.H5("Z-Score range:"), html.Div( - className="col-md-6 mt-1", + className="row mb-3", children=[ html.Div( - [ - html.Div( - className="row", - children=[ - html.Div( - className="col", - children=[ - html.Label( - "minimum value:" - ), - dcc.Input( - placeholder="min value", - type="number", - id="activation-min-input", - className="stats-input", - disabled=True, - ), - ], - ), - html.Div( - className="col", - children=[ - html.Label( - "maximum value:" - ), - dcc.Input( - placeholder="max value", - type="number", - id="activation-max-input", - className="stats-input", - disabled=True, - ), - ], - ), - ], - ) + className="col mt-1", + children=[ + html.Label("minimum value:"), + dcc.Input( + placeholder="min value", + type="number", + id="z-score-min-input", + className="stats-input", + ), + ], + ), + html.Div( + className="col mt-1", + children=[ + html.Label("maximum value:"), + dcc.Input( + placeholder="max value", + type="number", + id="z-score-max-input", + className="stats-input", + ), ], - ) - ], - ), - ], - ), - html.Div( - children=[ - dcc.Loading( - id="loading-activation-plot", - children=[ - dcc.Graph( - id="activation-plot", - figure={}, ), ], - type="circle", + ), + dcc.Graph( + id="z-score-plot", + figure={}, ), ], ), ], ), - ], - ), - dcc.Tab( - label="Inhibition", - children=[ - html.Div( - className="my-4", + dcc.Tab( + label="Activation/Inhibition", children=[ - html.H5("Inhibition range:"), html.Div( - className="row mb-3", + className="my-4", children=[ + html.H5( + "Activation/Inhibition range:", + id="tab-feature-header", + ), html.Div( - className="col-md-6 mt-1", + className="row mb-3", children=[ html.Div( - [ - html.Div( - className="row", - children=[ - html.Div( - className="col", - children=[ - html.Label( - "minimum value:" - ), - dcc.Input( - placeholder="min value", - type="number", - id="inhibition-min-input", - className="stats-input", - disabled=True, - ), - ], - ), - html.Div( - className="col", - children=[ - html.Label( - "maximum value:" - ), - dcc.Input( - placeholder="max value", - type="number", - id="inhibition-max-input", - className="stats-input", - disabled=True, - ), - ], - ), - ], - ) + className="col mt-1", + children=[ + html.Label("minimum value:"), + dcc.Input( + placeholder="min value", + type="number", + id="feature-min-input", + className="stats-input", + ), + ], + ), + html.Div( + className="col mt-1", + children=[ + html.Label("maximum value:"), + dcc.Input( + placeholder="max value", + type="number", + id="feature-max-input", + className="stats-input", + ), ], - ) - ], - ), - ], - ), - html.Div( - children=[ - dcc.Loading( - id="loading-inhibition-plot", - children=[ - dcc.Graph( - id="inhibition-plot", - figure={}, ), ], - type="circle", + ), + dcc.Graph( + id="feature-plot", + figure={}, ), ], ), @@ -283,49 +117,6 @@ ), ], ), - ], -) - - -radio_values = ["Retain all", "Z-Score", "Activation", "Inhibition"] -radio_codes = ["no_filter", "z_score", "activation", "inhibition"] -radio_options = [] - -for i, j in zip(radio_values, radio_codes): - radio_options.append( - { - "label": html.Div( - i, - style={ - "display": "inline", - "padding-left": "0.5rem", - "padding-right": "2rem", - }, - ), - "value": j, - } - ) - -SUMMARY_STAGE = html.Div( - id="summary_stage", - className="container", - children=[ - html.Div( - className="mb-5 mt-4", - children=[ - html.H5("Filter results:"), - html.H6( - "The compounds in the csv report will be ones outside the range of the selected filter." - ), - dcc.RadioItems( - radio_options, - "no_filter", - style={"display": "flex"}, - id="filter-radio", - ), - ], - ), - ACT_INH_ZSCORE_TABS, html.Div( className="my-4", children=[ @@ -350,7 +141,8 @@ ), html.Div( className="overflow-auto mx-2 border border-3 rounded shadow bg-body-tertiary", - children=[ACT_INH_ZSCORE_DATATABLE], + children=[], + id="compounds-data-table", ), ], ), diff --git a/dashboard/visualization/text_tables.py b/dashboard/visualization/text_tables.py index ed18f536..4e333221 100644 --- a/dashboard/visualization/text_tables.py +++ b/dashboard/visualization/text_tables.py @@ -3,6 +3,102 @@ from dash.dash_table.Format import Format, Scheme from sklearn.decomposition import PCA +PRECISION = 5 + + +def make_filter_radio_options(key: str): + """ + Creates the options for the filter radio buttons. + + :param key: key for the filter + :return: list of dictionaries with the options for the filter radio buttons + """ + radio_values = ["No filter (retain all)", "Z-Score", "Activation", "Inhibition"] + radio_codes = ["no_filter", "z_score", "activation", "inhibition"] + radio_codes_to_be_created = ["no_filter", "z_score", key] + radio_options = [] + + for i, j in zip(radio_values, radio_codes): + if j in radio_codes_to_be_created: + radio_options.append( + { + "label": html.Div( + i, + style={ + "display": "inline", + "padding-left": "0.5rem", + "padding-right": "2rem", + }, + ), + "value": j, + } + ) + + return radio_options + + +def make_summary_stage_datatable(df: pd.DataFrame, feature: str): + """ + Creates the datatable for the summary stage. + + :param df: dataframe with the data + :param feature: feature to be displayed (% ACTIVATION,% INHIBITION) + :return: datatable with the data + """ + data = df.to_dict("records") + feature_dict = dict( + id=feature, + name=feature, + type="numeric", + format=Format(precision=PRECISION, scheme=Scheme.fixed), + ) + + ACT_INH_DATATABLE = dash_table.DataTable( + id="compounds-data-table", + data=data, + columns=[ + dict(id="EOS", name="ID", type="text", presentation="markdown"), + dict(id="Destination Plate Barcode", name="Plate Barcode"), + dict(id="Destination Well", name="Well"), + feature_dict, + dict( + id="Z-SCORE", + name=" Z-SCORE", + type="numeric", + format=Format(precision=PRECISION, scheme=Scheme.fixed), + ), + ], + style_table={"overflowX": "auto", "overflowY": "auto"}, + style_data={ + "padding-left": "10px", + "padding-right": "10px", + "width": "70px", + "autosize": {"type": "fit", "resize": True}, + "overflow": "hidden", + }, + style_cell={ + "font-family": "sans-serif", + "font-size": "12px", + }, + style_header={ + "backgroundColor": "rgb(230, 230, 230)", + "fontWeight": "bold", + }, + style_data_conditional=[ + { + "if": {"row_index": "odd"}, + "backgroundColor": "rgb(248, 248, 248)", + }, + ], + filter_action="native", + filter_options={"case": "insensitive"}, + sort_action="native", + column_selectable=False, + page_size=15, + ) + + return ACT_INH_DATATABLE + def table_from_df(df: pd.DataFrame, table_id: str) -> html.Div: """ diff --git a/tests/bmg_plate_test.py b/tests/bmg_plate_test.py index 61a942d1..89e3bfae 100644 --- a/tests/bmg_plate_test.py +++ b/tests/bmg_plate_test.py @@ -4,7 +4,6 @@ import pandas as pd from dashboard.data.bmg_plate import ( - Mode, calculate_activation_inhibition_zscore, filter_low_quality_plates, get_activation_inhibition_zscore_dict, @@ -50,19 +49,19 @@ def test_calculate_activation_inhibition_zscore(stats_for_all): values = np.array([5, 3, 3]) activation, inhibition, z_score = calculate_activation_inhibition_zscore( - values, stats_for_all + values, stats_for_all, "activation", False ) assert ( round(activation[2], 2) == -199.25 - and round(inhibition[1], 2) == -199.25 and round(z_score[0], 2) == -82.92 + and inhibition is None ) def test_get_activation_inhibition_zscore_dict(df_stats): values = np.full((2, 2, 16, 24), 2) values[1] = 1.5 - z_dict = get_activation_inhibition_zscore_dict(df_stats, values, modes=dict()) + z_dict = get_activation_inhibition_zscore_dict(df_stats, values, "activation") assert np.array_equal(z_dict["1234"]["z_score"], np.full((16, 24), 1.0)) diff --git a/tests/combine_test.py b/tests/combine_test.py index a988d143..228e76c3 100644 --- a/tests/combine_test.py +++ b/tests/combine_test.py @@ -6,7 +6,6 @@ split_compounds_controls, combine_bmg_echo_data, ) -from dashboard.data.bmg_plate import Mode def test_values_array_to_column(): @@ -69,8 +68,7 @@ def test_combine_bmg_echo_data(df_stats): r"0(?!$)", "", regex=True ) plate_values = np.random.rand(1, 2, 16, 24) - modes = {"1234": Mode.ACTIVATION} - combined_df = combine_bmg_echo_data(echo_df, df_stats, plate_values, modes) + combined_df = combine_bmg_echo_data(echo_df, df_stats, plate_values, "activation") assert len(combined_df) == 384 assert set(combined_df.columns) == set(echo_data.keys()) | {