initial commit
Browse files- .gitignore +4 -0
- README.md +5 -4
- app.py +230 -0
- config.yml +14 -0
- dockerfile +129 -0
- files/analyzed_results_compilation.csv +0 -0
- files/analyzed_results_compilation_wCDAResults.csv +0 -0
- files/file_qc_results.csv +0 -0
- files/refactored_results_compilation.csv +0 -0
- files/site_ins_qc_results.csv +43 -0
- nist_cda_dashboard/__init__.py +0 -0
- nist_cda_dashboard/__pycache__/__init__.cpython-312.pyc +0 -0
- nist_cda_dashboard/__pycache__/component.cpython-312.pyc +0 -0
- nist_cda_dashboard/__pycache__/utils.cpython-312.pyc +0 -0
- nist_cda_dashboard/__pycache__/visualization.cpython-312.pyc +0 -0
- nist_cda_dashboard/component.py +517 -0
- nist_cda_dashboard/utils.py +21 -0
- nist_cda_dashboard/visualization.py +743 -0
- poetry.lock +0 -0
- pyproject.toml +31 -0
- requirements.txt +61 -0
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
files/*
|
| 2 |
+
!files/*.csv
|
| 3 |
+
.venv/
|
| 4 |
+
app_st.py
|
README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom: red
|
| 5 |
-
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: NIST CDA Dashboard
|
| 3 |
+
emoji: 📑
|
| 4 |
+
# colorFrom: red
|
| 5 |
+
# colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
app_port: 7860
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml
|
| 2 |
+
import gradio as gr
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from functools import partial
|
| 5 |
+
from nist_cda_dashboard.component import ComponentHelper
|
| 6 |
+
from nist_cda_dashboard.utils import DownloadHelper
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
with open("./config.yml", 'r') as file:
|
| 10 |
+
config = yaml.safe_load(file)
|
| 11 |
+
|
| 12 |
+
component_helper = ComponentHelper(file_qc_results=pd.read_csv(config["file_path"]["file_qc_results"]),
|
| 13 |
+
dataset_qc_results=pd.read_csv(
|
| 14 |
+
config["file_path"]["dataset_qc_results"]),
|
| 15 |
+
analyzed_gating_results=pd.read_csv(
|
| 16 |
+
config["file_path"]["analyzed_gating_results"]),
|
| 17 |
+
analyzed_gating_results_wCDAResults=pd.read_csv(
|
| 18 |
+
config["file_path"]["analyzed_gating_results_wCDAResults"]),
|
| 19 |
+
config=config)
|
| 20 |
+
|
| 21 |
+
custom_css = """
|
| 22 |
+
#plot_wScrollBar {
|
| 23 |
+
overflow-x: auto !important;
|
| 24 |
+
text-align: center !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
#plot_wScrollBar > .js-plotly-plot,
|
| 28 |
+
#plot_wScrollBar > .plotly,
|
| 29 |
+
#plot_wScrollBar > div:first-child {
|
| 30 |
+
display: inline-block !important;
|
| 31 |
+
text-align: left !important;
|
| 32 |
+
}
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
with gr.Blocks(css=custom_css) as demo:
|
| 36 |
+
with gr.Sidebar(position="left", width=700):
|
| 37 |
+
gr.Markdown("# STEP 1: Filter Dataset")
|
| 38 |
+
clear_all_models_button, select_all_models_button, model_filter, \
|
| 39 |
+
clear_all_datasets_button, select_all_datasets_button, dataset_filter, \
|
| 40 |
+
main_apply_filters_button = component_helper.creator.main_filter_components()
|
| 41 |
+
|
| 42 |
+
clear_all_models_button.click(fn=partial(component_helper.updater.clean_all_choices),
|
| 43 |
+
outputs=model_filter)
|
| 44 |
+
select_all_models_button.click(fn=partial(component_helper.updater.select_all_choices, "model_filter"),
|
| 45 |
+
outputs=model_filter)
|
| 46 |
+
|
| 47 |
+
clear_all_datasets_button.click(fn=partial(component_helper.updater.clean_all_choices),
|
| 48 |
+
outputs=dataset_filter)
|
| 49 |
+
select_all_datasets_button.click(fn=partial(component_helper.updater.select_all_choices, "dataset_filter"),
|
| 50 |
+
outputs=dataset_filter)
|
| 51 |
+
|
| 52 |
+
model_filter.change(fn=component_helper.updater.update_dataset_filter,
|
| 53 |
+
inputs=[model_filter,
|
| 54 |
+
dataset_filter],
|
| 55 |
+
outputs=dataset_filter)
|
| 56 |
+
|
| 57 |
+
gr.Markdown("# STEP 2: Choose QC or Analysis Tab")
|
| 58 |
+
with gr.Tab(label="Quality Check"):
|
| 59 |
+
gr.Markdown("# STEP 3: Filter File")
|
| 60 |
+
clear_all_sop_exps_qc_tab_button, select_all_sop_exps_qc_tab_button, sop_exp_qc_tab_filter, \
|
| 61 |
+
clear_all_materials_button, select_all_materials_button, material_filter, \
|
| 62 |
+
clear_all_issues_button, select_all_issues_button, issue_filter, \
|
| 63 |
+
qc_tab_apply_filters_button = component_helper.creator.qc_tab_filter_components()
|
| 64 |
+
|
| 65 |
+
clear_all_sop_exps_qc_tab_button.click(fn=partial(component_helper.updater.clean_all_choices),
|
| 66 |
+
outputs=sop_exp_qc_tab_filter)
|
| 67 |
+
select_all_sop_exps_qc_tab_button.click(fn=partial(component_helper.updater.select_all_choices, "sop_exp_qc_tab_filter"),
|
| 68 |
+
outputs=sop_exp_qc_tab_filter)
|
| 69 |
+
|
| 70 |
+
clear_all_materials_button.click(fn=partial(component_helper.updater.clean_all_choices),
|
| 71 |
+
outputs=material_filter)
|
| 72 |
+
select_all_materials_button.click(fn=partial(component_helper.updater.select_all_choices, "material_filter"),
|
| 73 |
+
outputs=material_filter)
|
| 74 |
+
|
| 75 |
+
clear_all_issues_button.click(fn=partial(component_helper.updater.clean_all_choices),
|
| 76 |
+
outputs=issue_filter)
|
| 77 |
+
select_all_issues_button.click(fn=partial(component_helper.updater.select_all_choices, "issue_filter"),
|
| 78 |
+
outputs=issue_filter)
|
| 79 |
+
|
| 80 |
+
sop_exp_qc_tab_filter.change(fn=component_helper.updater.update_material_filter,
|
| 81 |
+
inputs=[sop_exp_qc_tab_filter,
|
| 82 |
+
material_filter],
|
| 83 |
+
outputs=material_filter)
|
| 84 |
+
|
| 85 |
+
gr.Markdown("<br><br>")
|
| 86 |
+
gr.Markdown("# STEP 4: Results")
|
| 87 |
+
qc_tab_dataset_qc_status_filter = component_helper.creator.qc_tab_dataset_qc_status_filter_component()
|
| 88 |
+
|
| 89 |
+
with gr.Row():
|
| 90 |
+
gr.Markdown("## File QC Results Table")
|
| 91 |
+
download_file_qc_button = gr.Button(
|
| 92 |
+
"Download File QC Results")
|
| 93 |
+
|
| 94 |
+
file_qc_status_filter, file_qc_table, file_qc_table_no_file_msg = component_helper.creator.file_qc_result_components()
|
| 95 |
+
|
| 96 |
+
download_file_qc_button_hidden = gr.DownloadButton(
|
| 97 |
+
visible=False, elem_id="download_file_qc_button_hidden")
|
| 98 |
+
download_file_qc_button.click(fn=(lambda df: DownloadHelper.df2csv(df, "file_qc_table")),
|
| 99 |
+
inputs=[file_qc_table],
|
| 100 |
+
outputs=[download_file_qc_button_hidden]). \
|
| 101 |
+
then(fn=None,
|
| 102 |
+
inputs=None,
|
| 103 |
+
outputs=None,
|
| 104 |
+
js="() => document.querySelector('#download_file_qc_button_hidden').click()")
|
| 105 |
+
|
| 106 |
+
gr.Markdown("<br><br>")
|
| 107 |
+
with gr.Row():
|
| 108 |
+
gr.Markdown("## Dataset QC Results Table")
|
| 109 |
+
download_dataset_qc_button = gr.Button(
|
| 110 |
+
"Download Dataset QC Results")
|
| 111 |
+
dataset_qc_table, dataset_qc_table_no_dataset_msg = component_helper.creator.dataset_qc_result_components()
|
| 112 |
+
|
| 113 |
+
download_dataset_qc_button_hidden = gr.DownloadButton(
|
| 114 |
+
visible=False, elem_id="download_dataset_qc_button_hidden")
|
| 115 |
+
download_dataset_qc_button.click(fn=(lambda df: DownloadHelper.df2csv(df, "dataset_qc_table")),
|
| 116 |
+
inputs=[dataset_qc_table],
|
| 117 |
+
outputs=[download_dataset_qc_button_hidden]). \
|
| 118 |
+
then(fn=None,
|
| 119 |
+
inputs=None,
|
| 120 |
+
outputs=None,
|
| 121 |
+
js="() => document.querySelector('#download_dataset_qc_button_hidden').click()")
|
| 122 |
+
|
| 123 |
+
gr.Markdown("<br><br>")
|
| 124 |
+
with gr.Row():
|
| 125 |
+
gr.Markdown("## QC Results Visualization")
|
| 126 |
+
download_qc_fig_button = gr.Button(
|
| 127 |
+
"Download QC Visualization Figure")
|
| 128 |
+
qc_fig, qc_fig_no_file_msg = component_helper.creator.qc_result_visual_components()
|
| 129 |
+
|
| 130 |
+
download_qc_fig_button_hidden = gr.DownloadButton(
|
| 131 |
+
visible=False, elem_id="download_qc_fig_button_hidden")
|
| 132 |
+
download_qc_fig_button.click(fn=(lambda fig: DownloadHelper.fig2png(fig, "qc_visual_fig")),
|
| 133 |
+
inputs=[qc_fig],
|
| 134 |
+
outputs=[download_qc_fig_button_hidden]). \
|
| 135 |
+
then(fn=None,
|
| 136 |
+
inputs=None,
|
| 137 |
+
outputs=None,
|
| 138 |
+
js="() => document.querySelector('#download_qc_fig_button_hidden').click()")
|
| 139 |
+
|
| 140 |
+
gr.on(fn=component_helper.updater.update_file_qc_table,
|
| 141 |
+
triggers=[main_apply_filters_button.click, qc_tab_apply_filters_button.click,
|
| 142 |
+
qc_tab_dataset_qc_status_filter.change, file_qc_status_filter.change],
|
| 143 |
+
inputs=[dataset_filter, sop_exp_qc_tab_filter, material_filter,
|
| 144 |
+
issue_filter, qc_tab_dataset_qc_status_filter, file_qc_status_filter],
|
| 145 |
+
outputs=[file_qc_table, file_qc_table_no_file_msg,
|
| 146 |
+
dataset_qc_table, dataset_qc_table_no_dataset_msg])
|
| 147 |
+
|
| 148 |
+
gr.on(fn=component_helper.updater.update_qc_fig,
|
| 149 |
+
triggers=[main_apply_filters_button.click, qc_tab_apply_filters_button.click,
|
| 150 |
+
qc_tab_dataset_qc_status_filter.change],
|
| 151 |
+
inputs=[dataset_filter, sop_exp_qc_tab_filter, material_filter,
|
| 152 |
+
issue_filter, qc_tab_dataset_qc_status_filter],
|
| 153 |
+
outputs=[qc_fig, qc_fig_no_file_msg])
|
| 154 |
+
|
| 155 |
+
with gr.Tab(label="Gating Result Analysis"):
|
| 156 |
+
gr.Markdown("# STEP 3: Filter File")
|
| 157 |
+
sample_filter, sop_exp_analysis_tab_filter, compensation_control_filter, \
|
| 158 |
+
gating_control_filter, pop_pheno_parent_filter, clear_gating_tab_filters_button, analysis_tab_apply_filters_button = component_helper.creator.analysis_tab_filter_components()
|
| 159 |
+
|
| 160 |
+
gr.Markdown("<br><br>")
|
| 161 |
+
gr.Markdown("# STEP 4: Results")
|
| 162 |
+
gr.Markdown("## Analysis Visualization")
|
| 163 |
+
analysis_tab_dataset_qc_status_filter, analyzed_result_filter = component_helper.creator.analyzed_result_filter_component()
|
| 164 |
+
|
| 165 |
+
with gr.Tab(label="Single Result Barplot"):
|
| 166 |
+
download_barplot_fig_button = gr.Button("Download Barplot Figure")
|
| 167 |
+
analysis_barplot_fig, barplot_not_reportable_msg = component_helper.creator.analysis_barplot_components()
|
| 168 |
+
|
| 169 |
+
download_barplot_fig_button_hidden = gr.DownloadButton(
|
| 170 |
+
visible=False, elem_id="download_barplot_fig_button_hidden")
|
| 171 |
+
download_barplot_fig_button.click(fn=(lambda fig: DownloadHelper.fig2png(fig, "analysis_barplot_fig")),
|
| 172 |
+
inputs=[analysis_barplot_fig],
|
| 173 |
+
outputs=[download_barplot_fig_button_hidden]). \
|
| 174 |
+
then(fn=None,
|
| 175 |
+
inputs=None,
|
| 176 |
+
outputs=None,
|
| 177 |
+
js="() => document.querySelector('#download_barplot_fig_button_hidden').click()")
|
| 178 |
+
|
| 179 |
+
with gr.Tab(label="Multiple Results Heatmap"):
|
| 180 |
+
download_heatmap_fig_button = gr.Button(
|
| 181 |
+
"Download Heatmap Figure")
|
| 182 |
+
compared_protocol_filter, include_CDA_results_checkbox, analysis_heatmap_exp_info_table, analysis_heatmap_exp_comparison_table, \
|
| 183 |
+
analysis_heatmap_fig, heatmap_not_reportable_msg = component_helper.creator.analysis_heatmap_components()
|
| 184 |
+
|
| 185 |
+
download_heatmap_fig_button_hidden = gr.DownloadButton(
|
| 186 |
+
visible=False, elem_id="download_heatmap_fig_button_hidden")
|
| 187 |
+
download_heatmap_fig_button.click(fn=(lambda fig: DownloadHelper.fig2png(fig, "analysis_heatmap_fig")),
|
| 188 |
+
inputs=[analysis_heatmap_fig],
|
| 189 |
+
outputs=[download_heatmap_fig_button_hidden]). \
|
| 190 |
+
then(fn=None,
|
| 191 |
+
inputs=None,
|
| 192 |
+
outputs=None,
|
| 193 |
+
js="() => document.querySelector('#download_heatmap_fig_button_hidden').click()")
|
| 194 |
+
|
| 195 |
+
gr.on(fn=component_helper.updater.update_barplot_fig,
|
| 196 |
+
triggers=[main_apply_filters_button.click, analysis_tab_apply_filters_button.click,
|
| 197 |
+
analysis_tab_dataset_qc_status_filter.change, analyzed_result_filter.change],
|
| 198 |
+
inputs=[dataset_filter, analysis_tab_dataset_qc_status_filter,
|
| 199 |
+
sample_filter, sop_exp_analysis_tab_filter, compensation_control_filter,
|
| 200 |
+
gating_control_filter, pop_pheno_parent_filter, analyzed_result_filter],
|
| 201 |
+
outputs=[analysis_barplot_fig, barplot_not_reportable_msg])
|
| 202 |
+
|
| 203 |
+
gr.on(fn=component_helper.updater.update_heatmap_fig,
|
| 204 |
+
triggers=[main_apply_filters_button.click, analysis_tab_apply_filters_button.click,
|
| 205 |
+
analysis_tab_dataset_qc_status_filter.change, analyzed_result_filter.change,
|
| 206 |
+
compared_protocol_filter.change, include_CDA_results_checkbox.change],
|
| 207 |
+
inputs=[dataset_filter, analysis_tab_dataset_qc_status_filter,
|
| 208 |
+
sample_filter, sop_exp_analysis_tab_filter, compensation_control_filter,
|
| 209 |
+
gating_control_filter, pop_pheno_parent_filter,
|
| 210 |
+
analyzed_result_filter, compared_protocol_filter, include_CDA_results_checkbox],
|
| 211 |
+
outputs=[analysis_heatmap_exp_info_table, analysis_heatmap_exp_comparison_table,
|
| 212 |
+
analysis_heatmap_fig, heatmap_not_reportable_msg])
|
| 213 |
+
|
| 214 |
+
gr.on(fn=component_helper.updater.update_analysis_tab_filters,
|
| 215 |
+
triggers=[sample_filter.change,
|
| 216 |
+
sop_exp_analysis_tab_filter.change, compensation_control_filter.change,
|
| 217 |
+
gating_control_filter.change, pop_pheno_parent_filter.change],
|
| 218 |
+
inputs=[sample_filter,
|
| 219 |
+
sop_exp_analysis_tab_filter, compensation_control_filter,
|
| 220 |
+
gating_control_filter, pop_pheno_parent_filter],
|
| 221 |
+
outputs=[sample_filter,
|
| 222 |
+
sop_exp_analysis_tab_filter, compensation_control_filter,
|
| 223 |
+
gating_control_filter, pop_pheno_parent_filter])
|
| 224 |
+
|
| 225 |
+
clear_gating_tab_filters_button.click(fn=component_helper.updater.clear_analysis_tab_filters,
|
| 226 |
+
outputs=[sample_filter,
|
| 227 |
+
sop_exp_analysis_tab_filter, compensation_control_filter,
|
| 228 |
+
gating_control_filter, pop_pheno_parent_filter])
|
| 229 |
+
|
| 230 |
+
demo.launch(inbrowser=True)
|
config.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
file_path:
|
| 2 |
+
file_qc_results: './files/file_qc_results.csv'
|
| 3 |
+
dataset_qc_results: './files/site_ins_qc_results.csv'
|
| 4 |
+
analyzed_gating_results: './files/analyzed_results_compilation.csv'
|
| 5 |
+
analyzed_gating_results_wCDAResults: './files/analyzed_results_compilation_wCDAResults.csv'
|
| 6 |
+
|
| 7 |
+
qc_results_tab:
|
| 8 |
+
file_infos: ['Dataset', 'Site (anonymized)', 'Instrument model', 'File', 'SOP-Exp', 'Material', 'Repeat times', 'QC status']
|
| 9 |
+
issues: ['Missing file', 'Insufficient event', 'Missing time', 'Missing FSCA/SSCA', 'Missing FSCH/SSCH', 'Missing fluorescence', 'Voltage flipped']
|
| 10 |
+
file_sets: ['Rainbow bead and FC beads (SOP1-e1)', 'All cryoPBMCs (SOP3-e1, except FMO)']
|
| 11 |
+
|
| 12 |
+
gating_results_tab:
|
| 13 |
+
exp_infos: ['Dataset', 'Site (anonymized)', 'Instrument model', 'Result ID', 'Sample', 'SOP-Exp', 'Repetition', 'Compensation control', 'Gating control', 'Population', 'Phenotype', 'Parent gate']
|
| 14 |
+
results: ['Cell population (%)', 'Abs. cell count (volume)', 'Abs. cell count (TruCount)', 'MedFI', 'rSD', 'ERF']
|
dockerfile
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ====================
|
| 2 |
+
# 基礎映像檔設定
|
| 3 |
+
# ====================
|
| 4 |
+
# 使用輕量版 Python(固定具體版本避免意外更新)
|
| 5 |
+
FROM python:3.12.7-slim
|
| 6 |
+
|
| 7 |
+
# 安裝系統相依套件
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
build-essential \
|
| 10 |
+
libglib2.0-0 \
|
| 11 |
+
libgl1-mesa-glx \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# ====================
|
| 15 |
+
# 環境變數設定
|
| 16 |
+
# ====================
|
| 17 |
+
# 設定 Python 環境變數以提升安全性和效能
|
| 18 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 19 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 20 |
+
PIP_NO_CACHE_DIR=1 \
|
| 21 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
| 22 |
+
# PYTHONUNBUFFERED=1: 確保 Python 輸出立即顯示
|
| 23 |
+
# PYTHONDONTWRITEBYTECODE=1: 防止生成 .pyc 檔案,減少映像檔大小
|
| 24 |
+
# PIP_NO_CACHE_DIR=1: 禁用 pip 快取,減少映像檔大小
|
| 25 |
+
# PIP_DISABLE_PIP_VERSION_CHECK=1: 禁用 pip 版本檢查,加速安裝
|
| 26 |
+
|
| 27 |
+
# ====================
|
| 28 |
+
# 使用者和群組設定
|
| 29 |
+
# ====================
|
| 30 |
+
# 建立專用的應用程式使用者和群組(早期建立以確保安全)
|
| 31 |
+
RUN groupadd -r appgroup && \
|
| 32 |
+
useradd -r -g appgroup -u 1001 -d /app -s /sbin/nologin appuser
|
| 33 |
+
# -r: 建立系統使用者/群組
|
| 34 |
+
# -u 1001: 指定 UID,避免與 host 使用者衝突
|
| 35 |
+
# -d /app: 設定家目錄
|
| 36 |
+
# -s /sbin/nologin: 禁止 shell 登入,增強安全性
|
| 37 |
+
|
| 38 |
+
# ====================
|
| 39 |
+
# 系統套件安裝
|
| 40 |
+
# ====================
|
| 41 |
+
# 安裝必要的系統相依套件並立即清理,減少映像檔大小和攻擊面
|
| 42 |
+
RUN apt-get update && \
|
| 43 |
+
apt-get install -y --no-install-recommends \
|
| 44 |
+
build-essential \
|
| 45 |
+
libglib2.0-0 \
|
| 46 |
+
libgl1-mesa-glx && \
|
| 47 |
+
apt-get clean && \
|
| 48 |
+
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
| 49 |
+
# --no-install-recommends: 只安裝必要套件,不安裝建議套件
|
| 50 |
+
# 立即清理 apt 快取和臨時檔案
|
| 51 |
+
|
| 52 |
+
# ====================
|
| 53 |
+
# 工作目錄和權限設定
|
| 54 |
+
# ====================
|
| 55 |
+
# 設定應用程式工作目錄
|
| 56 |
+
WORKDIR /app
|
| 57 |
+
|
| 58 |
+
# 建立必要目錄並設定適當權限(避免使用危險的 777 權限)
|
| 59 |
+
# RUN mkdir -p /app/tmp /app/logs /app/data && \
|
| 60 |
+
# chown -R appuser:appgroup /app && \
|
| 61 |
+
# chmod 777 /app && \
|
| 62 |
+
# chmod 777 /app/tmp /app/logs /app/data
|
| 63 |
+
# 755: 擁有者可讀寫執行,群組和其他人可讀執行
|
| 64 |
+
# 750: 擁有者可讀寫執行,群組可讀執行,其他人無權限
|
| 65 |
+
|
| 66 |
+
# ====================
|
| 67 |
+
# Python 依賴安裝
|
| 68 |
+
# ====================
|
| 69 |
+
# 先複製 requirements.txt(利用 Docker 層快取優化)
|
| 70 |
+
COPY --chown=appuser:appgroup requirements.txt .
|
| 71 |
+
# --chown: 複製時直接設定擁有者,避免額外的 chown 指令
|
| 72 |
+
|
| 73 |
+
# 升級 pip 並安裝 Python 套件
|
| 74 |
+
RUN pip install --upgrade pip && \
|
| 75 |
+
pip install --no-cache-dir -r requirements.txt
|
| 76 |
+
# --no-cache-dir: 不使用快取,減少映像檔大小
|
| 77 |
+
|
| 78 |
+
# ====================
|
| 79 |
+
# 應用程式代碼複製
|
| 80 |
+
# ====================
|
| 81 |
+
# 複製應用程式代碼並設定擁有者
|
| 82 |
+
COPY --chown=appuser:appgroup . .
|
| 83 |
+
|
| 84 |
+
# ====================
|
| 85 |
+
# 清理和優化
|
| 86 |
+
# ====================
|
| 87 |
+
# 移除不必要的 Python 快取檔案,進一步減少映像檔大小
|
| 88 |
+
RUN find /app -name "*.pyc" -delete && \
|
| 89 |
+
find /app -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
| 90 |
+
# 刪除所有 .pyc 檔案和 __pycache__ 目錄
|
| 91 |
+
# 2>/dev/null || true: 忽略錯誤訊息
|
| 92 |
+
|
| 93 |
+
# ====================
|
| 94 |
+
# 使用者切換
|
| 95 |
+
# ====================
|
| 96 |
+
# 切換到非特權使用者執行應用程式(重要安全措施)
|
| 97 |
+
USER appuser
|
| 98 |
+
|
| 99 |
+
# ====================
|
| 100 |
+
# 網路設定
|
| 101 |
+
# ====================
|
| 102 |
+
# 暴露應用程式端口
|
| 103 |
+
EXPOSE 7860
|
| 104 |
+
|
| 105 |
+
# ====================
|
| 106 |
+
# 健康檢查
|
| 107 |
+
# ====================
|
| 108 |
+
# 設定容器健康檢查,確保應用程式正常運作
|
| 109 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 110 |
+
CMD curl -f http://localhost:7860/_stcore/health || exit 1
|
| 111 |
+
# --interval: 檢查間隔
|
| 112 |
+
# --timeout: 檢查超時時間
|
| 113 |
+
# --start-period: 啟動寬限期
|
| 114 |
+
# --retries: 重試次數
|
| 115 |
+
|
| 116 |
+
# ====================
|
| 117 |
+
# 應用程式啟動
|
| 118 |
+
# ====================
|
| 119 |
+
# 啟動 Streamlit 應用程式(啟用安全功能)
|
| 120 |
+
CMD ["gradio", "app.py", \
|
| 121 |
+
# "--server.port=7860", \
|
| 122 |
+
# "--server.address=0.0.0.0", \
|
| 123 |
+
# "--server.enableXsrfProtection=false", \
|
| 124 |
+
# "--server.enableCORS=false", \
|
| 125 |
+
# "--server.headless=true"
|
| 126 |
+
]
|
| 127 |
+
# enableXsrfProtection=true: 啟用 CSRF 保護
|
| 128 |
+
# enableCORS=false: 禁用跨域請求,增強安全性
|
| 129 |
+
# headless=true: 無頭模式,適合容器環境
|
files/analyzed_results_compilation.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
files/analyzed_results_compilation_wCDAResults.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
files/file_qc_results.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
files/refactored_results_compilation.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
files/site_ins_qc_results.csv
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Dataset,Dataset code,Site (anonymized),Instrument model,QC status,Rainbow bead and FC beads (SOP1-e1),"All cryoPBMCs (SOP3-e1, except FMO)"
|
| 2 |
+
Site01-Quanteon,1,Site01,Quanteon,Pass QC,Completed,Completed
|
| 3 |
+
Site02-Penteon,2,Site02,Penteon,Fail QC,Completed,Incomplete
|
| 4 |
+
Site03-Aurora,3,Site03,Aurora,Pass QC,Completed,Completed
|
| 5 |
+
Site03-SymphonyA3,4,Site03,SymphonyA3,Pass QC,Completed,Completed
|
| 6 |
+
Site03-SymphonyA5,5,Site03,SymphonyA5,Pass QC,Completed,Completed
|
| 7 |
+
Site04-Aurora,6,Site04,Aurora,Pass QC,Completed,Completed
|
| 8 |
+
Site05-Cytoflex,7,Site05,Cytoflex,Fail QC,Completed,Incomplete
|
| 9 |
+
Site05-CytoflexLX,8,Site05,CytoflexLX,Fail QC,Completed,Incomplete
|
| 10 |
+
Site05-CytoflexLXNUV,9,Site05,CytoflexLXNUV,Fail QC,Completed,Incomplete
|
| 11 |
+
Site06-Lyric-1,10,Site06,Lyric,Fail QC,Completed,Incomplete
|
| 12 |
+
Site06-Lyric-2,11,Site06,Lyric,Pass QC,Completed,Completed
|
| 13 |
+
Site07-CantoSORP-1,12,Site07,CantoSORP,Pass QC,Completed,Completed
|
| 14 |
+
Site07-CantoSORP-2,13,Site07,CantoSORP,Fail QC,Completed,Incomplete
|
| 15 |
+
Site08-CantoSORP,14,Site08,CantoSORP,Pass QC,Completed,Completed
|
| 16 |
+
Site08-MQA10-1,15,Site08,MQA10,Pass QC,Completed,Completed
|
| 17 |
+
Site08-MQA10-2,16,Site08,MQA10,Pass QC,Completed,Completed
|
| 18 |
+
Site09-CellStream,17,Site09,CellStream,Fail QC,Completed,Incomplete
|
| 19 |
+
Site09-CytoflexS,18,Site09,CytoflexS,Fail QC,Completed,Incomplete
|
| 20 |
+
Site10-AriaIII,19,Site10,AriaIII,Pass QC,Completed,Completed
|
| 21 |
+
Site10-Canto10,20,Site10,Canto10,Pass QC,Completed,Completed
|
| 22 |
+
Site10-CantoII,21,Site10,CantoII,Pass QC,Completed,Completed
|
| 23 |
+
Site10-Fortessa,22,Site10,Fortessa,Pass QC,Completed,Completed
|
| 24 |
+
Site11-BRZE5,23,Site11,BRZE5,Pass QC,Completed,Completed
|
| 25 |
+
Site11-CantoII,24,Site11,CantoII,Pass QC,Completed,Completed
|
| 26 |
+
Site11-Lyric,25,Site11,Lyric,Pass QC,Completed,Completed
|
| 27 |
+
Site12-CytoflexS,26,Site12,CytoflexS,Pass QC,Completed,Completed
|
| 28 |
+
Site13-CellStream-1,27,Site13,CellStream,Pass QC,Completed,Completed
|
| 29 |
+
Site13-CellStream-2,28,Site13,CellStream,Pass QC,Completed,Completed
|
| 30 |
+
Site13-ImageStreamX,29,Site13,ImageStreamX,Pass QC,Completed,Completed
|
| 31 |
+
Site14-Aurora,30,Site14,Aurora,Pass QC,Completed,Completed
|
| 32 |
+
Site15-AriaIII,31,Site15,AriaIII,Pass QC,Completed,Completed
|
| 33 |
+
Site15-CantoII-1,32,Site15,CantoII,Pass QC,Completed,Completed
|
| 34 |
+
Site15-CantoII-2,33,Site15,CantoII,Pass QC,Completed,Completed
|
| 35 |
+
Site16-AttuneNXT,34,Site16,AttuneNXT,Pass QC,Completed,Completed
|
| 36 |
+
Site16-CytoflexLX,35,Site16,CytoflexLX,Pass QC,Completed,Completed
|
| 37 |
+
Site17-Aurora-1,36,Site17,Aurora,Pass QC,Completed,Completed
|
| 38 |
+
Site17-Aurora-2,37,Site17,Aurora,Pass QC,Completed,Completed
|
| 39 |
+
Site18-CytoflexS,38,Site18,CytoflexS,Fail QC,Incomplete,Incomplete
|
| 40 |
+
Site18-NorthernL,39,Site18,NorthernL,Fail QC,Incomplete,Incomplete
|
| 41 |
+
Site19-Fusion-1,40,Site19,Fusion,Pass QC,Completed,Completed
|
| 42 |
+
Site19-Fusion-2,41,Site19,Fusion,Pass QC,Completed,Completed
|
| 43 |
+
Site20-Fortessa,42,Site20,Fortessa,Pass QC,Completed,Completed
|
nist_cda_dashboard/__init__.py
ADDED
|
File without changes
|
nist_cda_dashboard/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (161 Bytes). View file
|
|
|
nist_cda_dashboard/__pycache__/component.cpython-312.pyc
ADDED
|
Binary file (34.9 kB). View file
|
|
|
nist_cda_dashboard/__pycache__/utils.cpython-312.pyc
ADDED
|
Binary file (1.91 kB). View file
|
|
|
nist_cda_dashboard/__pycache__/visualization.cpython-312.pyc
ADDED
|
Binary file (32.4 kB). View file
|
|
|
nist_cda_dashboard/component.py
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import gradio as gr
|
| 3 |
+
from .visualization import QCVisualizer, AnalysisVisualizer
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ComponentHelper:
|
| 7 |
+
def __init__(self,
|
| 8 |
+
file_qc_results: pd.DataFrame,
|
| 9 |
+
dataset_qc_results: pd.DataFrame,
|
| 10 |
+
analyzed_gating_results: pd.DataFrame,
|
| 11 |
+
analyzed_gating_results_wCDAResults: pd.DataFrame,
|
| 12 |
+
config: dict):
|
| 13 |
+
self.file_qc_results = file_qc_results
|
| 14 |
+
self.dataset_qc_results = dataset_qc_results
|
| 15 |
+
self.analyzed_gating_results = analyzed_gating_results
|
| 16 |
+
self.analyzed_gating_results_wCDAResults = analyzed_gating_results_wCDAResults
|
| 17 |
+
self.config = config
|
| 18 |
+
|
| 19 |
+
self.choices = {"dataset_filter": list(file_qc_results["Dataset"].unique()),
|
| 20 |
+
"model_filter": sorted(list(file_qc_results["Instrument model"].unique())),
|
| 21 |
+
"sop_exp_qc_tab_filter": list(file_qc_results["SOP-Exp"].unique()),
|
| 22 |
+
"material_filter": list((file_qc_results["SOP-Exp"] + " " + file_qc_results["Material"]).unique()),
|
| 23 |
+
"issue_filter": config["qc_results_tab"]["issues"],
|
| 24 |
+
"sample_filter": list(analyzed_gating_results["Sample"].unique()),
|
| 25 |
+
"sop_exp_analysis_tab_filter": list(analyzed_gating_results["SOP-Exp"].unique()),
|
| 26 |
+
"compensation_control_filter": list(analyzed_gating_results["Compensation control"].unique()),
|
| 27 |
+
"gating_control_filter": list(analyzed_gating_results["Gating control"].unique()),
|
| 28 |
+
"pop_pheno_parent_filter": list(analyzed_gating_results["Population; Phenotype (Parent gate)"].unique()),
|
| 29 |
+
"analyzed_result_filter": config["gating_results_tab"]["results"],
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
self.creator = self._Creator(self)
|
| 33 |
+
self.updater = self._Updater(self)
|
| 34 |
+
|
| 35 |
+
class _Creator:
|
| 36 |
+
def __init__(self,
|
| 37 |
+
helper_instance):
|
| 38 |
+
self.helper = helper_instance
|
| 39 |
+
self.initialization_values = {}
|
| 40 |
+
|
| 41 |
+
def main_filter_components(self):
|
| 42 |
+
with gr.Row():
|
| 43 |
+
gr.Markdown("Instrument model")
|
| 44 |
+
clear_all_models_button = gr.Button("Clear All", size="sm")
|
| 45 |
+
select_all_models_button = gr.Button("Select All", size="sm")
|
| 46 |
+
model_filter = gr.Dropdown(label='', multiselect=True,
|
| 47 |
+
choices=self.helper.choices["model_filter"],
|
| 48 |
+
value=self.helper.choices["model_filter"])
|
| 49 |
+
self.initialization_values["model_filter"] = self.helper.choices["model_filter"]
|
| 50 |
+
|
| 51 |
+
with gr.Row():
|
| 52 |
+
gr.Markdown("Dataset")
|
| 53 |
+
clear_all_datasets_button = gr.Button("Clear All", size="sm")
|
| 54 |
+
select_all_datasets_button = gr.Button(
|
| 55 |
+
"Select All", size="sm")
|
| 56 |
+
dataset_filter = gr.Dropdown(label='', multiselect=True,
|
| 57 |
+
choices=self.helper.choices["dataset_filter"],
|
| 58 |
+
value=self.helper.choices["dataset_filter"])
|
| 59 |
+
self.initialization_values["dataset_filter"] = self.helper.choices["dataset_filter"]
|
| 60 |
+
|
| 61 |
+
main_apply_filters_button = gr.Button(value="Apply filters")
|
| 62 |
+
|
| 63 |
+
return clear_all_models_button, select_all_models_button, model_filter, \
|
| 64 |
+
clear_all_datasets_button, select_all_datasets_button, dataset_filter, \
|
| 65 |
+
main_apply_filters_button
|
| 66 |
+
|
| 67 |
+
def qc_tab_filter_components(self):
|
| 68 |
+
with gr.Row():
|
| 69 |
+
gr.Markdown("SOP-Exp")
|
| 70 |
+
clear_all_sop_exps_qc_tab_button = gr.Button(
|
| 71 |
+
"Clear All", size="sm")
|
| 72 |
+
select_all_sop_exps_qc_tab_button = gr.Button(
|
| 73 |
+
"Select All", size="sm")
|
| 74 |
+
sop_exp_qc_tab_filter = gr.Dropdown(label='', multiselect=True,
|
| 75 |
+
choices=self.helper.choices["sop_exp_qc_tab_filter"],
|
| 76 |
+
value=self.helper.choices["sop_exp_qc_tab_filter"])
|
| 77 |
+
self.initialization_values["sop_exp_qc_tab_filter"] = self.helper.choices["sop_exp_qc_tab_filter"]
|
| 78 |
+
|
| 79 |
+
with gr.Row():
|
| 80 |
+
gr.Markdown("Material")
|
| 81 |
+
clear_all_materials_button = gr.Button("Clear All", size="sm")
|
| 82 |
+
select_all_materials_button = gr.Button(
|
| 83 |
+
"Select All", size="sm")
|
| 84 |
+
material_filter = gr.Dropdown(label='', multiselect=True,
|
| 85 |
+
choices=self.helper.choices["material_filter"],
|
| 86 |
+
value=self.helper.choices["material_filter"])
|
| 87 |
+
self.initialization_values["material_filter"] = self.helper.choices["material_filter"]
|
| 88 |
+
|
| 89 |
+
with gr.Row():
|
| 90 |
+
gr.Markdown("Issues")
|
| 91 |
+
clear_all_issues_button = gr.Button("Clear All", size="sm")
|
| 92 |
+
select_all_issues_button = gr.Button("Select All", size="sm")
|
| 93 |
+
issue_filter = gr.Dropdown(label='', multiselect=True,
|
| 94 |
+
choices=self.helper.choices["issue_filter"],
|
| 95 |
+
value=self.helper.choices["issue_filter"])
|
| 96 |
+
self.initialization_values["issue_filter"] = self.helper.choices["issue_filter"]
|
| 97 |
+
|
| 98 |
+
qc_tab_apply_filters_button = gr.Button(value="Apply filters")
|
| 99 |
+
|
| 100 |
+
return clear_all_sop_exps_qc_tab_button, select_all_sop_exps_qc_tab_button, sop_exp_qc_tab_filter, \
|
| 101 |
+
clear_all_materials_button, select_all_materials_button, material_filter, \
|
| 102 |
+
clear_all_issues_button, select_all_issues_button, issue_filter, \
|
| 103 |
+
qc_tab_apply_filters_button
|
| 104 |
+
|
| 105 |
+
def qc_tab_dataset_qc_status_filter_component(self):
|
| 106 |
+
gr.Markdown("Dataset QC status")
|
| 107 |
+
qc_tab_dataset_qc_status_filter = gr.CheckboxGroup(label='',
|
| 108 |
+
choices=[
|
| 109 |
+
"Pass QC", "Fail QC"],
|
| 110 |
+
value=["Pass QC", "Fail QC"])
|
| 111 |
+
self.initialization_values["qc_tab_dataset_qc_status_filter"] = [
|
| 112 |
+
"Pass QC", "Fail QC"]
|
| 113 |
+
|
| 114 |
+
return qc_tab_dataset_qc_status_filter
|
| 115 |
+
|
| 116 |
+
def file_qc_result_components(self):
|
| 117 |
+
gr.Markdown("File QC status")
|
| 118 |
+
file_qc_status_filter = gr.CheckboxGroup(label='',
|
| 119 |
+
choices=[
|
| 120 |
+
"Pass QC", "Fail QC"],
|
| 121 |
+
value=["Fail QC"])
|
| 122 |
+
self.initialization_values["file_qc_status_filter"] = ["Fail QC"]
|
| 123 |
+
|
| 124 |
+
updater = self.helper.updater
|
| 125 |
+
updater.update_file_qc_table(self.initialization_values["dataset_filter"],
|
| 126 |
+
self.initialization_values["sop_exp_qc_tab_filter"],
|
| 127 |
+
self.initialization_values["material_filter"],
|
| 128 |
+
self.initialization_values["issue_filter"],
|
| 129 |
+
self.initialization_values["qc_tab_dataset_qc_status_filter"],
|
| 130 |
+
self.initialization_values["file_qc_status_filter"])
|
| 131 |
+
file_qc_table = gr.Dataframe(
|
| 132 |
+
type="pandas",
|
| 133 |
+
show_copy_button=True, show_row_numbers=True,
|
| 134 |
+
value=updater.filtered_file_qc_results)
|
| 135 |
+
|
| 136 |
+
file_qc_table_no_file_msg = gr.Markdown(
|
| 137 |
+
value="All files are filtered", visible=False)
|
| 138 |
+
|
| 139 |
+
return file_qc_status_filter, file_qc_table, file_qc_table_no_file_msg
|
| 140 |
+
|
| 141 |
+
def dataset_qc_result_components(self):
|
| 142 |
+
updater = self.helper.updater
|
| 143 |
+
updater.update_file_qc_table(self.initialization_values["dataset_filter"],
|
| 144 |
+
self.initialization_values["sop_exp_qc_tab_filter"],
|
| 145 |
+
self.initialization_values["material_filter"],
|
| 146 |
+
self.initialization_values["issue_filter"],
|
| 147 |
+
self.initialization_values["qc_tab_dataset_qc_status_filter"],
|
| 148 |
+
self.initialization_values["file_qc_status_filter"])
|
| 149 |
+
dataset_qc_table = gr.Dataframe(
|
| 150 |
+
type="pandas",
|
| 151 |
+
show_copy_button=True, show_row_numbers=True,
|
| 152 |
+
value=updater.filtered_dataset_qc_results)
|
| 153 |
+
|
| 154 |
+
dataset_qc_table_no_dataset_msg = gr.Markdown(
|
| 155 |
+
value="All datasets are filtered", visible=False)
|
| 156 |
+
|
| 157 |
+
return dataset_qc_table, dataset_qc_table_no_dataset_msg
|
| 158 |
+
|
| 159 |
+
def qc_result_visual_components(self):
|
| 160 |
+
updater = self.helper.updater
|
| 161 |
+
updater.update_qc_fig(self.initialization_values["dataset_filter"],
|
| 162 |
+
self.initialization_values["sop_exp_qc_tab_filter"],
|
| 163 |
+
self.initialization_values["material_filter"],
|
| 164 |
+
self.initialization_values["issue_filter"],
|
| 165 |
+
self.initialization_values["qc_tab_dataset_qc_status_filter"])
|
| 166 |
+
qc_fig = gr.Plot(elem_id="plot_wScrollBar",
|
| 167 |
+
value=updater.qc_visual)
|
| 168 |
+
qc_fig_no_file_msg = gr.Markdown(
|
| 169 |
+
value="All files are filtered", visible=False)
|
| 170 |
+
|
| 171 |
+
return qc_fig, qc_fig_no_file_msg
|
| 172 |
+
|
| 173 |
+
def analysis_tab_filter_components(self):
|
| 174 |
+
gr.Markdown("Sample")
|
| 175 |
+
sample_filter = gr.Dropdown(label='',
|
| 176 |
+
choices=self.helper.choices["sample_filter"],
|
| 177 |
+
value=self.helper.choices["sample_filter"][0])
|
| 178 |
+
self.initialization_values["sample_filter"] = self.helper.choices["sample_filter"][0]
|
| 179 |
+
|
| 180 |
+
gr.Markdown("SOP-Exp")
|
| 181 |
+
sop_exp_analysis_tab_filter = gr.Dropdown(label='',
|
| 182 |
+
choices=self.helper.choices["sop_exp_analysis_tab_filter"],
|
| 183 |
+
value=self.helper.choices["sop_exp_analysis_tab_filter"][0])
|
| 184 |
+
self.initialization_values["sop_exp_analysis_tab_filter"] = self.helper.choices["sop_exp_analysis_tab_filter"][0]
|
| 185 |
+
|
| 186 |
+
gr.Markdown("Compensation control")
|
| 187 |
+
compensation_control_filter = gr.Dropdown(label='',
|
| 188 |
+
choices=self.helper.choices["compensation_control_filter"],
|
| 189 |
+
value=self.helper.choices["compensation_control_filter"][0])
|
| 190 |
+
self.initialization_values["compensation_control_filter"] = self.helper.choices["compensation_control_filter"][0]
|
| 191 |
+
|
| 192 |
+
gr.Markdown("Gating control")
|
| 193 |
+
gating_control_filter = gr.Dropdown(label='',
|
| 194 |
+
choices=self.helper.choices["gating_control_filter"],
|
| 195 |
+
value=self.helper.choices["gating_control_filter"][0])
|
| 196 |
+
self.initialization_values["gating_control_filter"] = self.helper.choices["gating_control_filter"][0]
|
| 197 |
+
|
| 198 |
+
gr.Markdown("Population; Phenotype (Parent gate)")
|
| 199 |
+
pop_pheno_parent_filter = gr.Dropdown(label='',
|
| 200 |
+
choices=self.helper.choices["pop_pheno_parent_filter"],
|
| 201 |
+
value=self.helper.choices["pop_pheno_parent_filter"][3])
|
| 202 |
+
self.initialization_values["pop_pheno_parent_filter"] = self.helper.choices["pop_pheno_parent_filter"][3]
|
| 203 |
+
|
| 204 |
+
clear_gating_tab_filters_button = gr.Button(
|
| 205 |
+
value="Clear selections")
|
| 206 |
+
analysis_tab_apply_filters_button = gr.Button(
|
| 207 |
+
value="Apply filters")
|
| 208 |
+
|
| 209 |
+
return sample_filter, sop_exp_analysis_tab_filter, compensation_control_filter, \
|
| 210 |
+
gating_control_filter, pop_pheno_parent_filter, clear_gating_tab_filters_button, analysis_tab_apply_filters_button
|
| 211 |
+
|
| 212 |
+
def analyzed_result_filter_component(self):
|
| 213 |
+
gr.Markdown("Dataset QC status")
|
| 214 |
+
analysis_tab_dataset_qc_status_filter = gr.CheckboxGroup(label='',
|
| 215 |
+
choices=[
|
| 216 |
+
"Pass QC", "Fail QC"],
|
| 217 |
+
value=["Pass QC"])
|
| 218 |
+
self.initialization_values["analysis_tab_dataset_qc_status_filter"] = [
|
| 219 |
+
"Pass QC"]
|
| 220 |
+
|
| 221 |
+
gr.Markdown("Analyzed result")
|
| 222 |
+
analyzed_result_filter = gr.Dropdown(label='',
|
| 223 |
+
multiselect=True,
|
| 224 |
+
choices=self.helper.config["gating_results_tab"]["results"],
|
| 225 |
+
value=[self.helper.config["gating_results_tab"]["results"][0]])
|
| 226 |
+
self.initialization_values["analyzed_result_filter"] = [
|
| 227 |
+
self.helper.config["gating_results_tab"]["results"][0]]
|
| 228 |
+
|
| 229 |
+
return analysis_tab_dataset_qc_status_filter, analyzed_result_filter
|
| 230 |
+
|
| 231 |
+
def analysis_barplot_components(self):
|
| 232 |
+
updater = self.helper.updater
|
| 233 |
+
updater.update_barplot_fig(self.initialization_values["dataset_filter"],
|
| 234 |
+
self.initialization_values["analysis_tab_dataset_qc_status_filter"],
|
| 235 |
+
self.initialization_values["sample_filter"],
|
| 236 |
+
self.initialization_values["sop_exp_analysis_tab_filter"],
|
| 237 |
+
self.initialization_values["compensation_control_filter"],
|
| 238 |
+
self.initialization_values["gating_control_filter"],
|
| 239 |
+
self.initialization_values["pop_pheno_parent_filter"],
|
| 240 |
+
self.initialization_values["analyzed_result_filter"])
|
| 241 |
+
analysis_barplot_fig = gr.Plot(elem_id="plot_wScrollBar",
|
| 242 |
+
value=updater.barplot)
|
| 243 |
+
barplot_not_reportable_msg = gr.Markdown(
|
| 244 |
+
value="The assigned result type of the experiment is not reportable.", visible=False)
|
| 245 |
+
|
| 246 |
+
return analysis_barplot_fig, barplot_not_reportable_msg
|
| 247 |
+
|
| 248 |
+
def analysis_heatmap_components(self):
|
| 249 |
+
gr.Markdown("Protocol for multi-exps comparison")
|
| 250 |
+
compared_protocol_filter = gr.Radio(label='',
|
| 251 |
+
choices=[
|
| 252 |
+
"Compensation control", "Population; Phenotype (Parent gate)"],
|
| 253 |
+
value="Compensation control")
|
| 254 |
+
|
| 255 |
+
include_CDA_results_checkbox = gr.Checkbox(label="Include available CDA results",
|
| 256 |
+
value=False)
|
| 257 |
+
|
| 258 |
+
updater = self.helper.updater
|
| 259 |
+
updater.update_heatmap_fig(self.initialization_values["dataset_filter"],
|
| 260 |
+
self.initialization_values["analysis_tab_dataset_qc_status_filter"],
|
| 261 |
+
self.initialization_values["sample_filter"],
|
| 262 |
+
self.initialization_values["sop_exp_analysis_tab_filter"],
|
| 263 |
+
self.initialization_values["compensation_control_filter"],
|
| 264 |
+
self.initialization_values["gating_control_filter"],
|
| 265 |
+
self.initialization_values["pop_pheno_parent_filter"],
|
| 266 |
+
self.initialization_values["analyzed_result_filter"],
|
| 267 |
+
"Compensation control",
|
| 268 |
+
False)
|
| 269 |
+
with gr.Row():
|
| 270 |
+
analysis_heatmap_exp_info_table = gr.Dataframe(
|
| 271 |
+
show_copy_button=True, value=updater.exp_info_table)
|
| 272 |
+
analysis_heatmap_exp_comparison_table = gr.Dataframe(
|
| 273 |
+
show_copy_button=True, value=updater.exp_comparison_table)
|
| 274 |
+
analysis_heatmap_fig = gr.Plot(elem_id="plot_wScrollBar",
|
| 275 |
+
value=updater.heatmap)
|
| 276 |
+
heatmap_not_reportable_msg = gr.Markdown(
|
| 277 |
+
value="The assigned result type of all compared experiments are not reportable.", visible=False)
|
| 278 |
+
|
| 279 |
+
return compared_protocol_filter, include_CDA_results_checkbox, analysis_heatmap_exp_info_table, analysis_heatmap_exp_comparison_table, \
|
| 280 |
+
analysis_heatmap_fig, heatmap_not_reportable_msg
|
| 281 |
+
|
| 282 |
+
class _Updater:
|
| 283 |
+
def __init__(self,
|
| 284 |
+
helper_instance):
|
| 285 |
+
self.helper = helper_instance
|
| 286 |
+
|
| 287 |
+
def select_all_choices(self, filter_name: str):
|
| 288 |
+
return gr.update(value=self.helper.choices[filter_name])
|
| 289 |
+
|
| 290 |
+
def clean_all_choices(self):
|
| 291 |
+
return gr.update(value=[])
|
| 292 |
+
|
| 293 |
+
def update_dataset_filter(self, selected_models: list[str], selected_datasets: list[str]):
|
| 294 |
+
model_dataset_mapping = self.helper.file_qc_results.groupby("Instrument model")["Dataset"].apply(
|
| 295 |
+
lambda x: sorted(x.unique().tolist())).to_dict()
|
| 296 |
+
updated_dataset_choices = set()
|
| 297 |
+
if selected_models:
|
| 298 |
+
for selected_model in selected_models:
|
| 299 |
+
if selected_model in model_dataset_mapping.keys():
|
| 300 |
+
updated_dataset_choices.update(
|
| 301 |
+
model_dataset_mapping[selected_model])
|
| 302 |
+
updated_dataset_choices = sorted(
|
| 303 |
+
list(updated_dataset_choices))
|
| 304 |
+
self.helper.choices["dataset_filter"] = updated_dataset_choices
|
| 305 |
+
updated_dataset_values = [
|
| 306 |
+
item for item in selected_datasets if item in updated_dataset_choices]
|
| 307 |
+
return gr.update(choices=updated_dataset_choices,
|
| 308 |
+
value=updated_dataset_values)
|
| 309 |
+
|
| 310 |
+
def update_material_filter(self, selected_sop_exps: list[str], selected_materials: list[str]):
|
| 311 |
+
df = self.helper.file_qc_results.copy()
|
| 312 |
+
df["Material"] = df["SOP-Exp"] + " " + df["Material"]
|
| 313 |
+
sop_exp_material_mapping = df.groupby("SOP-Exp")["Material"].apply(
|
| 314 |
+
lambda x: sorted(x.unique().tolist())).to_dict()
|
| 315 |
+
updated_material_choices = set()
|
| 316 |
+
if selected_sop_exps:
|
| 317 |
+
for selected_sop_exp in selected_sop_exps:
|
| 318 |
+
if selected_sop_exp in sop_exp_material_mapping.keys():
|
| 319 |
+
updated_material_choices.update(
|
| 320 |
+
sop_exp_material_mapping[selected_sop_exp])
|
| 321 |
+
updated_material_choices = sorted(
|
| 322 |
+
list(updated_material_choices))
|
| 323 |
+
self.helper.choices["material_filter"] = updated_material_choices
|
| 324 |
+
updated_material_values = [
|
| 325 |
+
item for item in selected_materials if item in updated_material_choices]
|
| 326 |
+
return gr.update(choices=updated_material_choices,
|
| 327 |
+
value=updated_material_values)
|
| 328 |
+
|
| 329 |
+
def _filter_dataset_qc_result(self, selected_datasets, selected_dataset_qc_status):
|
| 330 |
+
return self.helper.dataset_qc_results[(self.helper.dataset_qc_results["Dataset"].isin(selected_datasets)) &
|
| 331 |
+
(self.helper.dataset_qc_results["QC status"].isin(selected_dataset_qc_status))]
|
| 332 |
+
|
| 333 |
+
def update_file_qc_table(self,
|
| 334 |
+
selected_datasets: list[str], selected_sop_exps: list[str], selected_materials: list[str],
|
| 335 |
+
selected_qc_issues: list[str], selected_dataset_qc_status: list[str], selected_file_qc_status: list[str]):
|
| 336 |
+
self.filtered_dataset_qc_results = self._filter_dataset_qc_result(
|
| 337 |
+
selected_datasets, selected_dataset_qc_status)
|
| 338 |
+
selected_materials = [material.split(
|
| 339 |
+
" ")[1] for material in selected_materials]
|
| 340 |
+
self.filtered_file_qc_results = self.helper.file_qc_results.loc[(self.helper.file_qc_results["Dataset"].isin(self.filtered_dataset_qc_results["Dataset"])) &
|
| 341 |
+
(self.helper.file_qc_results["SOP-Exp"].isin(selected_sop_exps)) &
|
| 342 |
+
(self.helper.file_qc_results["Material"].isin(selected_materials)) &
|
| 343 |
+
(self.helper.file_qc_results["QC status"].isin(
|
| 344 |
+
selected_file_qc_status)),
|
| 345 |
+
self.helper.config["qc_results_tab"]["file_infos"]+selected_qc_issues]
|
| 346 |
+
|
| 347 |
+
if len(self.filtered_file_qc_results) == 0:
|
| 348 |
+
file_qc_table_update = {"visible": False}
|
| 349 |
+
file_qc_table_no_file_msg_update = {"visible": True}
|
| 350 |
+
else:
|
| 351 |
+
file_qc_table_update = {
|
| 352 |
+
"value": {"data": self.filtered_file_qc_results.values.tolist(),
|
| 353 |
+
"headers": self.filtered_file_qc_results.columns.to_list()},
|
| 354 |
+
"visible": True}
|
| 355 |
+
file_qc_table_no_file_msg_update = {"visible": False}
|
| 356 |
+
if len(self.filtered_dataset_qc_results) == 0:
|
| 357 |
+
dataset_qc_table_update = {"visible": False}
|
| 358 |
+
dataset_qc_table_no_dataset_msg_update = {
|
| 359 |
+
"visible": True}
|
| 360 |
+
else:
|
| 361 |
+
dataset_qc_table_update = {
|
| 362 |
+
"value": {"data": self.filtered_dataset_qc_results.values.tolist(),
|
| 363 |
+
"headers": self.filtered_dataset_qc_results.columns.to_list()},
|
| 364 |
+
"visible": True}
|
| 365 |
+
dataset_qc_table_no_dataset_msg_update = {
|
| 366 |
+
"visible": False}
|
| 367 |
+
return [gr.update(**file_qc_table_update),
|
| 368 |
+
gr.update(**file_qc_table_no_file_msg_update),
|
| 369 |
+
gr.update(**dataset_qc_table_update),
|
| 370 |
+
gr.update(**dataset_qc_table_no_dataset_msg_update)]
|
| 371 |
+
|
| 372 |
+
def update_qc_fig(self,
|
| 373 |
+
selected_datasets: list[str], selected_sop_exps: list[str], selected_materials: list[str],
|
| 374 |
+
selected_qc_issues: list[str], selected_dataset_qc_status: list[str]):
|
| 375 |
+
filtered_dataset_qc_results = self.filtered_dataset_qc_results = self._filter_dataset_qc_result(
|
| 376 |
+
selected_datasets, selected_dataset_qc_status)
|
| 377 |
+
selected_materials = [material.split(
|
| 378 |
+
" ")[1] for material in selected_materials]
|
| 379 |
+
filtered_file_qc_results = self.helper.file_qc_results.loc[(self.helper.file_qc_results["Dataset"].isin(filtered_dataset_qc_results["Dataset"])) &
|
| 380 |
+
(self.helper.file_qc_results["SOP-Exp"].isin(selected_sop_exps)) &
|
| 381 |
+
(self.helper.file_qc_results["Material"].isin(
|
| 382 |
+
selected_materials)),
|
| 383 |
+
self.helper.config["qc_results_tab"]["file_infos"]+selected_qc_issues]
|
| 384 |
+
if len(filtered_file_qc_results) == 0:
|
| 385 |
+
return [gr.update(visible=False), gr.update(visible=True)]
|
| 386 |
+
else:
|
| 387 |
+
self.qc_visual = QCVisualizer.visualize(filtered_dataset_qc_results,
|
| 388 |
+
self.helper.config["qc_results_tab"]["file_sets"],
|
| 389 |
+
filtered_file_qc_results,
|
| 390 |
+
selected_qc_issues)
|
| 391 |
+
return [gr.update(value=self.qc_visual,
|
| 392 |
+
visible=True),
|
| 393 |
+
gr.update(visible=False)]
|
| 394 |
+
|
| 395 |
+
def update_barplot_fig(self,
|
| 396 |
+
selected_datasets: list[str], selected_dataset_qc_status: list[str],
|
| 397 |
+
selected_sample, selected_sop_exp, selected_comp,
|
| 398 |
+
selected_fmo, selected_pop_pheno_parent, selected_results: list[str]):
|
| 399 |
+
if any([s is None for s in [selected_sample, selected_sop_exp, selected_comp, selected_fmo, selected_pop_pheno_parent]]):
|
| 400 |
+
return [gr.update(visible=False), gr.update(visible=False)]
|
| 401 |
+
|
| 402 |
+
filtered_dataset_qc_results = self.filtered_dataset_qc_results = self._filter_dataset_qc_result(
|
| 403 |
+
selected_datasets, selected_dataset_qc_status)
|
| 404 |
+
filtered_analyzed_gating_results = self.helper.analyzed_gating_results.loc[(self.helper.analyzed_gating_results["Dataset"].isin(filtered_dataset_qc_results["Dataset"])) &
|
| 405 |
+
(self.helper.analyzed_gating_results["Sample"] == selected_sample) &
|
| 406 |
+
(self.helper.analyzed_gating_results["SOP-Exp"] == selected_sop_exp) &
|
| 407 |
+
(self.helper.analyzed_gating_results["Compensation control"] == selected_comp) &
|
| 408 |
+
(self.helper.analyzed_gating_results["Gating control"] == selected_fmo) &
|
| 409 |
+
(self.helper.analyzed_gating_results["Population; Phenotype (Parent gate)"] == selected_pop_pheno_parent),
|
| 410 |
+
self.helper.config["gating_results_tab"]["exp_infos"]+[c for c in self.helper.analyzed_gating_results.columns if any(selected_result in c for selected_result in selected_results)]]
|
| 411 |
+
|
| 412 |
+
# if (filtered_analyzed_gating_results[[f"{selected_result}_mean", f"{selected_result}_std"]] == "Not reportable").all(axis=None):
|
| 413 |
+
# return [gr.update(visible=False), gr.update(visible=True)]
|
| 414 |
+
|
| 415 |
+
self.barplot = AnalysisVisualizer.visualize_barplot(filtered_analyzed_gating_results,
|
| 416 |
+
selected_results)
|
| 417 |
+
return [gr.update(value=self.barplot,
|
| 418 |
+
visible=True),
|
| 419 |
+
gr.update(visible=False)]
|
| 420 |
+
|
| 421 |
+
def update_heatmap_fig(self,
|
| 422 |
+
selected_datasets: list[str], selected_dataset_qc_status: list[str],
|
| 423 |
+
selected_sample, selected_sop_exp, selected_comp,
|
| 424 |
+
selected_fmo, selected_pop_pheno_parent, selected_results: list[str], selected_comparison,
|
| 425 |
+
include_CDA):
|
| 426 |
+
if include_CDA:
|
| 427 |
+
analyzed_results = self.helper.analyzed_gating_results_wCDAResults
|
| 428 |
+
else:
|
| 429 |
+
analyzed_results = self.helper.analyzed_gating_results
|
| 430 |
+
|
| 431 |
+
if any([s is None for s in [selected_sample, selected_sop_exp, selected_comp, selected_fmo, selected_pop_pheno_parent]]):
|
| 432 |
+
return [gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)]
|
| 433 |
+
masks = {"Sample": analyzed_results["Sample"] == selected_sample,
|
| 434 |
+
"SOP-Exp": analyzed_results["SOP-Exp"] == selected_sop_exp,
|
| 435 |
+
"Compensation control": analyzed_results["Compensation control"] == selected_comp,
|
| 436 |
+
"Gating control": analyzed_results["Gating control"] == selected_fmo,
|
| 437 |
+
"Population; Phenotype (Parent gate)": analyzed_results["Population; Phenotype (Parent gate)"] == selected_pop_pheno_parent}
|
| 438 |
+
filtering_mask = pd.DataFrame(pd.concat([mask for mask_name, mask in masks.items(
|
| 439 |
+
) if mask_name != selected_comparison], axis=1)).all(axis=1)
|
| 440 |
+
|
| 441 |
+
filtered_dataset_qc_results = self.filtered_dataset_qc_results = self._filter_dataset_qc_result(
|
| 442 |
+
selected_datasets, selected_dataset_qc_status)
|
| 443 |
+
filtered_analyzed_gating_results = analyzed_results.loc[analyzed_results["Dataset"].isin(filtered_dataset_qc_results["Dataset"]) &
|
| 444 |
+
filtering_mask,
|
| 445 |
+
self.helper.config["gating_results_tab"]["exp_infos"]+[c for c in analyzed_results.columns if any(selected_result in c for selected_result in selected_results)]]
|
| 446 |
+
|
| 447 |
+
# if (filtered_analyzed_gating_results[[f"{selected_result}_mean", f"{selected_result}_std"]] == "Not reportable").all(axis=None):
|
| 448 |
+
# return [gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)]
|
| 449 |
+
|
| 450 |
+
self.exp_info_table = filtered_analyzed_gating_results[[
|
| 451 |
+
e for e in self.helper.config["gating_results_tab"]["exp_infos"] if e not in ["Dataset", "Site (anonymized)", "Instrument model", "Result ID"]]]
|
| 452 |
+
|
| 453 |
+
if selected_comparison == "Population; Phenotype (Parent gate)":
|
| 454 |
+
shown_exp_comparison = [
|
| 455 |
+
"Population", "Phenotype", "Parent gate"]
|
| 456 |
+
else:
|
| 457 |
+
shown_exp_comparison = [selected_comparison]
|
| 458 |
+
|
| 459 |
+
self.exp_info_table = pd.DataFrame(self.exp_info_table).drop(
|
| 460 |
+
columns=shown_exp_comparison)
|
| 461 |
+
self.exp_info_table = self.exp_info_table.value_counts().reset_index().drop(
|
| 462 |
+
columns="count").T.reset_index().set_axis(["Protocol", "Content"], axis=1)
|
| 463 |
+
|
| 464 |
+
self.exp_comparison_table = filtered_analyzed_gating_results[["Result ID"]+shown_exp_comparison].value_counts(
|
| 465 |
+
).reset_index().drop(columns="count")
|
| 466 |
+
|
| 467 |
+
if selected_comparison == "Population; Phenotype (Parent gate)":
|
| 468 |
+
filtered_analyzed_gating_results["Result ID"] = filtered_analyzed_gating_results["Result ID"].astype(str) + \
|
| 469 |
+
" (" + filtered_analyzed_gating_results["Population"] + ")"
|
| 470 |
+
else:
|
| 471 |
+
filtered_analyzed_gating_results["Result ID"] = filtered_analyzed_gating_results["Result ID"].astype(str) + \
|
| 472 |
+
" (" + filtered_analyzed_gating_results[selected_comparison] + ")"
|
| 473 |
+
|
| 474 |
+
self.heatmap = AnalysisVisualizer.visualize_heatmap(filtered_analyzed_gating_results,
|
| 475 |
+
selected_results)
|
| 476 |
+
|
| 477 |
+
return [gr.update(value=self.exp_info_table, visible=True),
|
| 478 |
+
gr.update(value=self.exp_comparison_table, visible=True),
|
| 479 |
+
gr.update(value=self.heatmap,
|
| 480 |
+
visible=True),
|
| 481 |
+
gr.update(visible=False)]
|
| 482 |
+
|
| 483 |
+
def update_analysis_tab_filters(self, selected_sample, selected_sop_exp, selected_comp, selected_fmo, selected_pop_pheno_parent):
|
| 484 |
+
masks = {}
|
| 485 |
+
col_selection_mapping = dict(zip(["Sample", "SOP-Exp", "Compensation control", "Gating control", "Population; Phenotype (Parent gate)"],
|
| 486 |
+
[selected_sample, selected_sop_exp, selected_comp, selected_fmo, selected_pop_pheno_parent]))
|
| 487 |
+
for col, selection in col_selection_mapping.items():
|
| 488 |
+
if selection is not None:
|
| 489 |
+
masks[col] = self.helper.analyzed_gating_results[col] == selection
|
| 490 |
+
if len(masks) == 0:
|
| 491 |
+
return self.clear_analysis_tab_filters()
|
| 492 |
+
|
| 493 |
+
filtering_mask = pd.DataFrame(
|
| 494 |
+
pd.concat([mask for mask in masks.values()], axis=1)).all(axis=1)
|
| 495 |
+
filtered_analyzed_gating_results = self.helper.analyzed_gating_results[
|
| 496 |
+
filtering_mask]
|
| 497 |
+
|
| 498 |
+
updated_choices = {}
|
| 499 |
+
updated_values = {}
|
| 500 |
+
for col in ["Sample", "SOP-Exp", "Compensation control", "Gating control", "Population; Phenotype (Parent gate)"]:
|
| 501 |
+
updated_choices[col] = sorted(
|
| 502 |
+
list(filtered_analyzed_gating_results[col].unique()))
|
| 503 |
+
updated_values[col] = (
|
| 504 |
+
col_selection_mapping[col] if col_selection_mapping[col] in updated_choices[col] else None)
|
| 505 |
+
|
| 506 |
+
return [gr.update(choices=updated_choices[col], value=updated_values[col])
|
| 507 |
+
for col in ["Sample", "SOP-Exp", "Compensation control", "Gating control", "Population; Phenotype (Parent gate)"]]
|
| 508 |
+
|
| 509 |
+
def clear_analysis_tab_filters(self):
|
| 510 |
+
return [gr.update(choices=self.helper.choices["sample_filter"], value=None),
|
| 511 |
+
gr.update(
|
| 512 |
+
choices=self.helper.choices["sop_exp_analysis_tab_filter"], value=None),
|
| 513 |
+
gr.update(
|
| 514 |
+
choices=self.helper.choices["compensation_control_filter"], value=None),
|
| 515 |
+
gr.update(
|
| 516 |
+
choices=self.helper.choices["gating_control_filter"], value=None),
|
| 517 |
+
gr.update(choices=self.helper.choices["pop_pheno_parent_filter"], value=None)]
|
nist_cda_dashboard/utils.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tempfile
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import gradio as gr
|
| 4 |
+
import json
|
| 5 |
+
import plotly.io as pio
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class DownloadHelper:
|
| 9 |
+
@staticmethod
|
| 10 |
+
def df2csv(df: pd.DataFrame, prefix: str):
|
| 11 |
+
with tempfile.NamedTemporaryFile(mode="w", delete=False, prefix=f"{prefix}_",
|
| 12 |
+
suffix=".csv", encoding="utf-8") as tmpfile:
|
| 13 |
+
df.to_csv(tmpfile.name, index=False)
|
| 14 |
+
return tmpfile.name
|
| 15 |
+
|
| 16 |
+
@staticmethod
|
| 17 |
+
def fig2png(fig: gr.components.plot.PlotData, prefix: str):
|
| 18 |
+
with tempfile.NamedTemporaryFile(mode="wb", delete=False, prefix=f"{prefix}_",
|
| 19 |
+
suffix=".png") as tmpfile:
|
| 20 |
+
pio.write_image(json.loads(fig.plot), tmpfile.name, format="png")
|
| 21 |
+
return tmpfile.name
|
nist_cda_dashboard/visualization.py
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import plotly.graph_objects as go
|
| 5 |
+
from plotly.express import colors
|
| 6 |
+
from plotly.subplots import make_subplots
|
| 7 |
+
import base64
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class _CustomizedMarker:
|
| 11 |
+
def regular_polygon_coords(self, corners: int) -> list[np.array]:
|
| 12 |
+
if corners < 3:
|
| 13 |
+
raise ValueError("A polygon must have at least 3 corners.")
|
| 14 |
+
|
| 15 |
+
radius = 0.4
|
| 16 |
+
angle_step = 2 * math.pi / corners
|
| 17 |
+
polygon_coordinates = []
|
| 18 |
+
for i in range(corners):
|
| 19 |
+
angle = i * angle_step
|
| 20 |
+
x = radius * math.sin(angle)
|
| 21 |
+
y = radius * math.cos(angle)
|
| 22 |
+
polygon_coordinates.append(np.array([x, y]))
|
| 23 |
+
polygon_coordinates.append(polygon_coordinates[0])
|
| 24 |
+
return polygon_coordinates
|
| 25 |
+
|
| 26 |
+
def clock_marker_coords(self, corners: int, target_corner: int) -> list[np.array]:
|
| 27 |
+
if target_corner > corners or target_corner <= 0:
|
| 28 |
+
raise ValueError("Target corner outside available value range.")
|
| 29 |
+
|
| 30 |
+
target_corner -= 1
|
| 31 |
+
polygon_coords = self.regular_polygon_coords(corners)
|
| 32 |
+
corner_coord = polygon_coords[target_corner]
|
| 33 |
+
left_coord = (
|
| 34 |
+
corner_coord + polygon_coords[(target_corner+1) % corners]) / 2
|
| 35 |
+
right_coord = (
|
| 36 |
+
corner_coord + polygon_coords[(target_corner-1) % corners]) / 2
|
| 37 |
+
center_coord = np.array([0, 0])
|
| 38 |
+
|
| 39 |
+
return [corner_coord, right_coord, center_coord, left_coord, corner_coord]
|
| 40 |
+
|
| 41 |
+
def marker_to_scatter_line_coords(self, clock_marker_coords, x_coords, y_coords):
|
| 42 |
+
scatter_marker_x_coords = []
|
| 43 |
+
scatter_marker_y_coords = []
|
| 44 |
+
for x_coord, y_coord in zip(x_coords, y_coords):
|
| 45 |
+
scatter_marker_x_coords.extend([marker_coord[0] + x_coord
|
| 46 |
+
for marker_coord in clock_marker_coords])
|
| 47 |
+
scatter_marker_x_coords.append(None)
|
| 48 |
+
scatter_marker_y_coords.extend([marker_coord[1] + y_coord
|
| 49 |
+
for marker_coord in clock_marker_coords])
|
| 50 |
+
scatter_marker_y_coords.append(None)
|
| 51 |
+
return scatter_marker_x_coords, scatter_marker_y_coords
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class QCVisualizer:
|
| 55 |
+
@staticmethod
|
| 56 |
+
def visualize(site_ins_qc_results: pd.DataFrame,
|
| 57 |
+
file_sets: list[str],
|
| 58 |
+
file_qc_results: pd.DataFrame,
|
| 59 |
+
qc_issues: list[str]
|
| 60 |
+
) -> go.Figure:
|
| 61 |
+
|
| 62 |
+
site_ins_coord_mapping = dict(
|
| 63 |
+
zip(list(file_qc_results["Dataset"].unique()), list(range(len(file_qc_results["Dataset"].unique())))))
|
| 64 |
+
file_coord_mapping = dict(
|
| 65 |
+
zip(file_qc_results["File"].unique(), list(range(len(file_qc_results["File"].unique())))))
|
| 66 |
+
|
| 67 |
+
fig = make_subplots(rows=1, cols=2,
|
| 68 |
+
shared_yaxes=True)
|
| 69 |
+
|
| 70 |
+
# Figure 1. site-ins QC results
|
| 71 |
+
site_ins_plot_table = pd.melt(site_ins_qc_results,
|
| 72 |
+
id_vars=[
|
| 73 |
+
"Dataset", "Site (anonymized)", "Instrument model"],
|
| 74 |
+
value_vars=file_sets,
|
| 75 |
+
var_name="File set",
|
| 76 |
+
value_name="status")
|
| 77 |
+
status_marker_mapping = {"Completed": "circle", "Incomplete": "x"}
|
| 78 |
+
|
| 79 |
+
default_colors = colors.qualitative.Set1
|
| 80 |
+
status_marker_color_mapping = {
|
| 81 |
+
"Completed": default_colors[1], "Incomplete": default_colors[0]}
|
| 82 |
+
default_colors = [default_colors[i]
|
| 83 |
+
for i in range(len(default_colors)) if i not in [0, 1]]
|
| 84 |
+
for status in ["Completed", "Incomplete"]:
|
| 85 |
+
sub_site_ins_plot_table = site_ins_plot_table[site_ins_plot_table["status"] == status]
|
| 86 |
+
sub_site_ins_plot_table.loc[:, "Dataset"] = sub_site_ins_plot_table["Dataset"].map(
|
| 87 |
+
site_ins_coord_mapping)
|
| 88 |
+
if status == "Incomplete":
|
| 89 |
+
for y in sub_site_ins_plot_table["Dataset"].unique():
|
| 90 |
+
fig.add_shape(
|
| 91 |
+
type="line",
|
| 92 |
+
y0=y, y1=y,
|
| 93 |
+
x0=-0.5, x1=1.5,
|
| 94 |
+
line=dict(color="red", width=1),
|
| 95 |
+
layer="between",
|
| 96 |
+
row=1, col=1
|
| 97 |
+
)
|
| 98 |
+
fig.add_shape(
|
| 99 |
+
type="line",
|
| 100 |
+
y0=y, y1=y,
|
| 101 |
+
x0=-2, x1=(len(file_qc_results["File"].unique())-1)+2,
|
| 102 |
+
line=dict(color="red", width=1),
|
| 103 |
+
layer="between",
|
| 104 |
+
row=1, col=2
|
| 105 |
+
)
|
| 106 |
+
if len(sub_site_ins_plot_table) == 0:
|
| 107 |
+
fig.add_trace(go.Scatter(x=[None],
|
| 108 |
+
y=[None],
|
| 109 |
+
mode="markers",
|
| 110 |
+
marker=dict(symbol=status_marker_mapping[status],
|
| 111 |
+
color=status_marker_color_mapping[status],
|
| 112 |
+
size=12,
|
| 113 |
+
),
|
| 114 |
+
name=f"File set {status.lower()}",
|
| 115 |
+
visible="legendonly"
|
| 116 |
+
),
|
| 117 |
+
row=1, col=1)
|
| 118 |
+
else:
|
| 119 |
+
hover_info = []
|
| 120 |
+
for _, row in sub_site_ins_plot_table.iterrows():
|
| 121 |
+
hover_info.append(
|
| 122 |
+
[row["Site (anonymized)"], row["Instrument model"], row["status"]])
|
| 123 |
+
fig.add_trace(go.Scatter(x=sub_site_ins_plot_table["File set"],
|
| 124 |
+
y=sub_site_ins_plot_table["Dataset"],
|
| 125 |
+
mode="markers",
|
| 126 |
+
marker=dict(symbol=status_marker_mapping[status],
|
| 127 |
+
color=status_marker_color_mapping[status],
|
| 128 |
+
size=12,
|
| 129 |
+
),
|
| 130 |
+
name=f"File set {status.lower()}",
|
| 131 |
+
customdata=hover_info,
|
| 132 |
+
hovertemplate=("Dataset: %{y}<br>" +
|
| 133 |
+
"Site (anonymized): %{customdata[0]}<br>" +
|
| 134 |
+
"Instrument model: %{customdata[1]}<br>" +
|
| 135 |
+
"File set: %{x}<br>" +
|
| 136 |
+
"Status: %{customdata[2]}" +
|
| 137 |
+
"<extra></extra>"
|
| 138 |
+
)
|
| 139 |
+
),
|
| 140 |
+
row=1, col=1)
|
| 141 |
+
fig.update_xaxes(title_text="File set",
|
| 142 |
+
range=[
|
| 143 |
+
0-0.5, (len(site_ins_plot_table["File set"].unique())-1)+0.5],
|
| 144 |
+
tickangle=90,
|
| 145 |
+
gridcolor="lightgray",
|
| 146 |
+
zeroline=False,
|
| 147 |
+
showline=False,
|
| 148 |
+
row=1, col=1)
|
| 149 |
+
|
| 150 |
+
# Figure 2. file QC results
|
| 151 |
+
file_plot_table = file_qc_results[file_qc_results[qc_issues].any(
|
| 152 |
+
axis=1)]
|
| 153 |
+
clock_marker_index = 1
|
| 154 |
+
hover_information = pd.DataFrame(
|
| 155 |
+
columns=["x", "y", "Dataset", "Site (anonymized)", "Instrument model", "File", "Issues"])
|
| 156 |
+
for issue_index, qc_issue in enumerate(qc_issues):
|
| 157 |
+
sub_file_plot_table = file_plot_table[file_plot_table[qc_issue]]
|
| 158 |
+
if len(sub_file_plot_table) == 0:
|
| 159 |
+
fig.add_trace(go.Scatter(x=[None],
|
| 160 |
+
y=[None],
|
| 161 |
+
mode="lines",
|
| 162 |
+
fill="toself",
|
| 163 |
+
fillcolor=default_colors[issue_index],
|
| 164 |
+
line=dict(color="black", width=0.5),
|
| 165 |
+
name=qc_issue,
|
| 166 |
+
visible="legendonly"
|
| 167 |
+
),
|
| 168 |
+
row=1, col=2)
|
| 169 |
+
if qc_issue != "Missing file":
|
| 170 |
+
clock_marker_index += 1
|
| 171 |
+
else:
|
| 172 |
+
x_coords = [file_coord_mapping[file_code]
|
| 173 |
+
for file_code in sub_file_plot_table["File"]]
|
| 174 |
+
y_coords = [site_ins_coord_mapping[site_ins_code]
|
| 175 |
+
for site_ins_code in sub_file_plot_table["Dataset"]]
|
| 176 |
+
hover_information = pd.concat(
|
| 177 |
+
[hover_information, pd.DataFrame({"x": x_coords,
|
| 178 |
+
"y": y_coords,
|
| 179 |
+
"Dataset": sub_file_plot_table["Dataset"],
|
| 180 |
+
"Site (anonymized)": sub_file_plot_table["Site (anonymized)"],
|
| 181 |
+
"Instrument model": sub_file_plot_table["Instrument model"],
|
| 182 |
+
"File": sub_file_plot_table["File"],
|
| 183 |
+
"Issues": qc_issue})])
|
| 184 |
+
if qc_issue == "Missing file":
|
| 185 |
+
marker_coords = _CustomizedMarker().regular_polygon_coords(len(qc_issues)-1)
|
| 186 |
+
else:
|
| 187 |
+
marker_coords = _CustomizedMarker().clock_marker_coords(
|
| 188 |
+
len(qc_issues)-1, clock_marker_index)
|
| 189 |
+
clock_marker_index += 1
|
| 190 |
+
issue_marker_x_coords, issue_marker_y_coords = \
|
| 191 |
+
_CustomizedMarker().marker_to_scatter_line_coords(marker_coords,
|
| 192 |
+
x_coords,
|
| 193 |
+
y_coords)
|
| 194 |
+
fig.add_trace(go.Scatter(x=issue_marker_x_coords,
|
| 195 |
+
y=issue_marker_y_coords,
|
| 196 |
+
mode="lines",
|
| 197 |
+
fill="toself",
|
| 198 |
+
fillcolor=default_colors[issue_index],
|
| 199 |
+
line=dict(color="black", width=0.5),
|
| 200 |
+
name=qc_issue,
|
| 201 |
+
hoverinfo="skip"
|
| 202 |
+
),
|
| 203 |
+
row=1, col=2)
|
| 204 |
+
|
| 205 |
+
qc_issues_woMissingFile = [
|
| 206 |
+
issue for issue in qc_issues if issue != "Missing file"]
|
| 207 |
+
for issue_index, qc_issue in enumerate(qc_issues_woMissingFile):
|
| 208 |
+
sub_file_plot_table = file_plot_table[(~file_plot_table[qc_issue]) & (
|
| 209 |
+
file_plot_table[[issue for issue in qc_issues_woMissingFile if issue != qc_issue]].any(axis=1))]
|
| 210 |
+
x_coords = [file_coord_mapping[file_code]
|
| 211 |
+
for file_code in sub_file_plot_table["File"]]
|
| 212 |
+
y_coords = [site_ins_coord_mapping[site_ins_code]
|
| 213 |
+
for site_ins_code in sub_file_plot_table["Dataset"]]
|
| 214 |
+
marker_coords = _CustomizedMarker().clock_marker_coords(
|
| 215 |
+
len(qc_issues)-1, issue_index+1)
|
| 216 |
+
issue_marker_x_coords, issue_marker_y_coords = \
|
| 217 |
+
_CustomizedMarker().marker_to_scatter_line_coords(marker_coords,
|
| 218 |
+
x_coords,
|
| 219 |
+
y_coords)
|
| 220 |
+
fig.add_trace(go.Scatter(x=issue_marker_x_coords,
|
| 221 |
+
y=issue_marker_y_coords,
|
| 222 |
+
mode="lines",
|
| 223 |
+
fill="toself",
|
| 224 |
+
fillcolor="rgba(0,0,0,0)",
|
| 225 |
+
line=dict(color="black", width=0.5),
|
| 226 |
+
showlegend=False,
|
| 227 |
+
hoverinfo="skip"
|
| 228 |
+
),
|
| 229 |
+
row=1, col=2)
|
| 230 |
+
pass
|
| 231 |
+
|
| 232 |
+
hover_information = hover_information.groupby(["x", "y", "Dataset", "Site (anonymized)", "Instrument model", "File"], dropna=False)[
|
| 233 |
+
"Issues"].apply(lambda x: ", ".join(x.astype(str))).reset_index()
|
| 234 |
+
|
| 235 |
+
marker_coords = _CustomizedMarker().regular_polygon_coords(len(qc_issues)-1)
|
| 236 |
+
hover_marker_x_coords = []
|
| 237 |
+
hover_marker_y_coords = []
|
| 238 |
+
hover_marker_customdata = []
|
| 239 |
+
for row_index, row in hover_information.iterrows():
|
| 240 |
+
hover_marker_x_coords.extend([marker_coord[0] + row["x"]
|
| 241 |
+
for marker_coord in marker_coords])
|
| 242 |
+
hover_marker_x_coords.append(None)
|
| 243 |
+
hover_marker_y_coords.extend([marker_coord[1] + row["y"]
|
| 244 |
+
for marker_coord in marker_coords])
|
| 245 |
+
hover_marker_y_coords.append(None)
|
| 246 |
+
for marker_coord in marker_coords:
|
| 247 |
+
hover_marker_customdata.append(
|
| 248 |
+
[row["Dataset"], row["Site (anonymized)"], row["Instrument model"], row["File"], row["Issues"]])
|
| 249 |
+
hover_marker_customdata.append(None)
|
| 250 |
+
fig.add_trace(go.Scatter(x=hover_marker_x_coords,
|
| 251 |
+
y=hover_marker_y_coords,
|
| 252 |
+
showlegend=False,
|
| 253 |
+
mode="none",
|
| 254 |
+
customdata=hover_marker_customdata,
|
| 255 |
+
hovertemplate=("Dataset: %{customdata[0]}<br>" +
|
| 256 |
+
"Site (anonymized): %{customdata[1]}<br>" +
|
| 257 |
+
"Instrument model: %{customdata[2]}<br>" +
|
| 258 |
+
"File: %{customdata[3]}<br>" +
|
| 259 |
+
"Issues: %{customdata[4]}" +
|
| 260 |
+
"<extra></extra>"
|
| 261 |
+
)
|
| 262 |
+
),
|
| 263 |
+
row=1, col=2)
|
| 264 |
+
|
| 265 |
+
fig.update_xaxes(title_text="File",
|
| 266 |
+
range=[
|
| 267 |
+
0-2, (len(file_qc_results["File"].unique())-1)+2],
|
| 268 |
+
tickvals=list(file_coord_mapping.values()),
|
| 269 |
+
ticktext=list(file_coord_mapping.keys()),
|
| 270 |
+
tickangle=90,
|
| 271 |
+
gridcolor="lightgray",
|
| 272 |
+
zeroline=False,
|
| 273 |
+
showline=False,
|
| 274 |
+
row=1, col=2)
|
| 275 |
+
|
| 276 |
+
# figure 3. legend
|
| 277 |
+
legend_fig = go.Figure()
|
| 278 |
+
legend_list = ["Completed", "Incomplete"] + qc_issues
|
| 279 |
+
for status_index, status in enumerate(["Completed", "Incomplete"]):
|
| 280 |
+
legend_fig.add_trace(go.Scatter(x=[0], y=[status_index],
|
| 281 |
+
marker=dict(symbol=status_marker_mapping[status],
|
| 282 |
+
color=status_marker_color_mapping[status],
|
| 283 |
+
size=12,
|
| 284 |
+
),
|
| 285 |
+
hoverinfo="skip"))
|
| 286 |
+
legend_fig.add_annotation(x=0, y=status_index,
|
| 287 |
+
text=status,
|
| 288 |
+
xanchor="left",
|
| 289 |
+
yanchor="middle",
|
| 290 |
+
showarrow=False,
|
| 291 |
+
xshift=15)
|
| 292 |
+
clock_marker_count = 1
|
| 293 |
+
indexes = [i for i in range(len(legend_list)) if legend_list[i] not in [
|
| 294 |
+
"Completed", "Incomplete", "Missing file"]]
|
| 295 |
+
for issue_index, qc_issue in enumerate(qc_issues):
|
| 296 |
+
if qc_issue == "Missing file":
|
| 297 |
+
marker_coords = _CustomizedMarker().regular_polygon_coords(len(qc_issues)-1)
|
| 298 |
+
else:
|
| 299 |
+
marker_coords = _CustomizedMarker().clock_marker_coords(len(qc_issues)-1,
|
| 300 |
+
clock_marker_count)
|
| 301 |
+
clock_marker_count += 1
|
| 302 |
+
|
| 303 |
+
x, y = _CustomizedMarker().marker_to_scatter_line_coords(marker_coords,
|
| 304 |
+
[0], [legend_list.index(qc_issue)])
|
| 305 |
+
legend_fig.add_trace(go.Scatter(x=x, y=y,
|
| 306 |
+
mode="lines",
|
| 307 |
+
fill="toself",
|
| 308 |
+
fillcolor=default_colors[issue_index],
|
| 309 |
+
line=dict(
|
| 310 |
+
color="black", width=0.5),
|
| 311 |
+
hoverinfo="skip"))
|
| 312 |
+
if qc_issue != "Miising file":
|
| 313 |
+
x, y = _CustomizedMarker().marker_to_scatter_line_coords(marker_coords,
|
| 314 |
+
[0] *
|
| 315 |
+
(len(
|
| 316 |
+
indexes)-1),
|
| 317 |
+
[i for i in indexes
|
| 318 |
+
if i != issue_index+2])
|
| 319 |
+
legend_fig.add_trace(go.Scatter(x=x, y=y,
|
| 320 |
+
mode="lines",
|
| 321 |
+
fill="toself",
|
| 322 |
+
fillcolor="rgba(0,0,0,0)",
|
| 323 |
+
line=dict(
|
| 324 |
+
color="black", width=0.5),
|
| 325 |
+
hoverinfo="skip"))
|
| 326 |
+
legend_fig.add_annotation(x=0, y=issue_index+2,
|
| 327 |
+
text=qc_issue,
|
| 328 |
+
xanchor="left",
|
| 329 |
+
yanchor="middle",
|
| 330 |
+
showarrow=False,
|
| 331 |
+
xshift=15)
|
| 332 |
+
legend_fig.update_xaxes(visible=False,
|
| 333 |
+
range=[0-1, 0+7])
|
| 334 |
+
legend_fig.update_yaxes(visible=False,
|
| 335 |
+
autorange="reversed")
|
| 336 |
+
legend_fig_aspect = [25*8, 25*(2+len(qc_issues)+2)]
|
| 337 |
+
legend_fig.update_layout(width=legend_fig_aspect[0],
|
| 338 |
+
height=legend_fig_aspect[1],
|
| 339 |
+
title="Marker Legend",
|
| 340 |
+
margin=dict(l=0, r=0, t=50, b=0),
|
| 341 |
+
showlegend=False,
|
| 342 |
+
plot_bgcolor="white")
|
| 343 |
+
|
| 344 |
+
image_bytes = legend_fig.to_image(format="png", scale=2)
|
| 345 |
+
base64_image_string = base64.b64encode(image_bytes).decode("utf-8")
|
| 346 |
+
image_data_uri = f"data:image/png;base64,{base64_image_string}"
|
| 347 |
+
|
| 348 |
+
file_counts = len(file_qc_results["File"].unique())
|
| 349 |
+
|
| 350 |
+
fig.update_yaxes(title_text="Dataset",
|
| 351 |
+
showticklabels=True,
|
| 352 |
+
range=[0-2, (len(site_ins_coord_mapping)-1)+2],
|
| 353 |
+
tickvals=list(site_ins_coord_mapping.values()),
|
| 354 |
+
ticktext=list(site_ins_coord_mapping.keys()),
|
| 355 |
+
gridcolor="lightgray",
|
| 356 |
+
zeroline=False,
|
| 357 |
+
showline=False)
|
| 358 |
+
|
| 359 |
+
def _normalize(values: list, value_range: list):
|
| 360 |
+
return [(value-min(value_range))/(max(value_range)-min(value_range)) for value in values]
|
| 361 |
+
|
| 362 |
+
margin_left = 200
|
| 363 |
+
subplot1_width = 85
|
| 364 |
+
space12 = 200
|
| 365 |
+
subplot2_width = 25*(file_counts+4)
|
| 366 |
+
margin_right = 250
|
| 367 |
+
|
| 368 |
+
center_plots_width = subplot1_width + space12 + subplot2_width
|
| 369 |
+
figure_width = margin_left + center_plots_width + margin_right
|
| 370 |
+
|
| 371 |
+
subplot1_x_domain = _normalize([0,
|
| 372 |
+
subplot1_width],
|
| 373 |
+
[0, center_plots_width])
|
| 374 |
+
subplot2_x_domain = _normalize([subplot1_width + space12,
|
| 375 |
+
subplot1_width + space12 + subplot2_width],
|
| 376 |
+
[0, center_plots_width])
|
| 377 |
+
|
| 378 |
+
margin_top = 30
|
| 379 |
+
center_plots_height = subplot12_height = 25 * \
|
| 380 |
+
(len(site_ins_coord_mapping)+4)
|
| 381 |
+
margin_bottom = 300
|
| 382 |
+
figure_height = margin_top + subplot12_height + margin_bottom
|
| 383 |
+
|
| 384 |
+
legend_image_x = (center_plots_width + 20) / center_plots_width
|
| 385 |
+
legend_image_sizex = legend_fig_aspect[0]*1.2
|
| 386 |
+
legend_image_sizey = legend_image_sizex*(legend_fig_aspect[1]/legend_fig_aspect[0])
|
| 387 |
+
fig.add_layout_image(dict(source=image_data_uri,
|
| 388 |
+
xref="paper", yref="paper",
|
| 389 |
+
x=legend_image_x, y=0.98,
|
| 390 |
+
sizex=legend_image_sizex/center_plots_width,
|
| 391 |
+
sizey=legend_image_sizey/center_plots_height,
|
| 392 |
+
sizing="contain",
|
| 393 |
+
xanchor="left", yanchor="top",
|
| 394 |
+
layer="below"
|
| 395 |
+
))
|
| 396 |
+
|
| 397 |
+
fig.add_annotation(x=np.mean(subplot1_x_domain), y=1, yshift=5,
|
| 398 |
+
xref="paper", yref="paper",
|
| 399 |
+
text="Dataset QC",
|
| 400 |
+
showarrow=False,
|
| 401 |
+
xanchor="center",
|
| 402 |
+
yanchor="bottom",
|
| 403 |
+
font=dict(size=16))
|
| 404 |
+
fig.add_annotation(x=np.mean(subplot2_x_domain), y=1, yshift=5,
|
| 405 |
+
xref="paper", yref="paper",
|
| 406 |
+
text="File QC",
|
| 407 |
+
showarrow=False,
|
| 408 |
+
xanchor="center",
|
| 409 |
+
yanchor="bottom",
|
| 410 |
+
font=dict(size=16))
|
| 411 |
+
|
| 412 |
+
fig.update_layout(height=figure_height, width=figure_width,
|
| 413 |
+
xaxis=dict(domain=subplot1_x_domain),
|
| 414 |
+
xaxis2=dict(domain=subplot2_x_domain),
|
| 415 |
+
yaxis=dict(automargin=False),
|
| 416 |
+
margin=dict(
|
| 417 |
+
t=margin_top, b=margin_bottom, l=margin_left, r=margin_right),
|
| 418 |
+
autosize=False,
|
| 419 |
+
showlegend=False,
|
| 420 |
+
hoverlabel=dict(bgcolor="white",
|
| 421 |
+
font_color="black",
|
| 422 |
+
align="left"),
|
| 423 |
+
plot_bgcolor="white"
|
| 424 |
+
)
|
| 425 |
+
|
| 426 |
+
return fig
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
class AnalysisVisualizer:
|
| 430 |
+
def visualize_barplot(analyzed_gating_results: pd.DataFrame,
|
| 431 |
+
visualized_results: list[str]
|
| 432 |
+
) -> go.Figure:
|
| 433 |
+
fig = make_subplots(rows=1, cols=len(visualized_results),
|
| 434 |
+
horizontal_spacing=0.4/len(visualized_results),
|
| 435 |
+
subplot_titles=visualized_results
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
dataset_counts = []
|
| 439 |
+
for result_index, visualized_result in enumerate(visualized_results):
|
| 440 |
+
result_index += 1
|
| 441 |
+
|
| 442 |
+
df = analyzed_gating_results.copy()
|
| 443 |
+
|
| 444 |
+
df[f"{visualized_result}_count"] = df[f"{visualized_result}_count"].astype(
|
| 445 |
+
int)
|
| 446 |
+
df["Dataset_wCount"] = df["Dataset"].astype(
|
| 447 |
+
str) + " (" + df[f"{visualized_result}_count"].astype(str) + ")"
|
| 448 |
+
dataset_wCount = df["Dataset_wCount"].to_list()
|
| 449 |
+
dataset_wCount_mapping = dict(
|
| 450 |
+
zip(dataset_wCount[::-1], list(range(len(dataset_wCount)))))
|
| 451 |
+
df["Dataset_wCount_index"] = df["Dataset_wCount"].map(
|
| 452 |
+
dataset_wCount_mapping)
|
| 453 |
+
|
| 454 |
+
if (df[[f"{visualized_result}_mean", f"{visualized_result}_std"]] == "Not reportable").all(axis=None):
|
| 455 |
+
fig.add_annotation(x=0, y=np.percentile(list(dataset_wCount_mapping.values()), 50),
|
| 456 |
+
text="Not reportable",
|
| 457 |
+
xanchor="center",
|
| 458 |
+
yanchor="middle",
|
| 459 |
+
font=dict(size=24),
|
| 460 |
+
showarrow=False,
|
| 461 |
+
xshift=0,
|
| 462 |
+
row=1, col=result_index)
|
| 463 |
+
fig.update_xaxes(range=[-1, 1],
|
| 464 |
+
showticklabels=False,
|
| 465 |
+
row=1, col=result_index)
|
| 466 |
+
else:
|
| 467 |
+
df = df[df[f"{visualized_result}_count"]
|
| 468 |
+
!= 0].reset_index(drop=True)
|
| 469 |
+
|
| 470 |
+
df[f"{visualized_result}_std"] = df[f"{visualized_result}_std"].replace({
|
| 471 |
+
"Only one data": None})
|
| 472 |
+
|
| 473 |
+
for analysis in ["mean", "std"]:
|
| 474 |
+
df[f"{visualized_result}_{analysis}"] = df[f"{visualized_result}_{analysis}"].astype(
|
| 475 |
+
float).round(2)
|
| 476 |
+
|
| 477 |
+
statistic = {}
|
| 478 |
+
statistic["Q1"] = df[f"{visualized_result}_mean"].quantile(
|
| 479 |
+
0.25)
|
| 480 |
+
statistic["Q2"] = df[f"{visualized_result}_mean"].quantile(0.5)
|
| 481 |
+
statistic["Q3"] = df[f"{visualized_result}_mean"].quantile(
|
| 482 |
+
0.75)
|
| 483 |
+
statistic["IQR"] = statistic["Q3"] - statistic["Q1"]
|
| 484 |
+
statistic["Lower fence"] = statistic["Q1"] - \
|
| 485 |
+
1.5*statistic["IQR"]
|
| 486 |
+
statistic["Upper fence"] = statistic["Q3"] + \
|
| 487 |
+
1.5*statistic["IQR"]
|
| 488 |
+
statistic["Extreme lower fence"] = statistic["Q1"] - \
|
| 489 |
+
3*statistic["IQR"]
|
| 490 |
+
statistic["Extreme upper fence"] = statistic["Q3"] + \
|
| 491 |
+
3*statistic["IQR"]
|
| 492 |
+
for index in df.index.to_list():
|
| 493 |
+
if df.loc[index, f"{visualized_result}_mean"] >= statistic["Lower fence"] and \
|
| 494 |
+
df.loc[index, f"{visualized_result}_mean"] <= statistic["Upper fence"]:
|
| 495 |
+
df.loc[index, "distribution"] = "Normal"
|
| 496 |
+
df.loc[index, "bar_color"] = "#636EFA"
|
| 497 |
+
elif df.loc[index, f"{visualized_result}_mean"] >= statistic["Extreme lower fence"] and \
|
| 498 |
+
df.loc[index, f"{visualized_result}_mean"] <= statistic["Extreme upper fence"]:
|
| 499 |
+
df.loc[index, "distribution"] = "Outlier"
|
| 500 |
+
df.loc[index, "bar_color"] = "#FFA15A"
|
| 501 |
+
else:
|
| 502 |
+
df.loc[index, "distribution"] = "Extreme outlier"
|
| 503 |
+
df.loc[index, "bar_color"] = "#EF553B"
|
| 504 |
+
|
| 505 |
+
if visualized_result == "Cell population (%)":
|
| 506 |
+
xmin = 0
|
| 507 |
+
xmax = 100
|
| 508 |
+
else:
|
| 509 |
+
xmin = df[f"{visualized_result}_mean"].min()
|
| 510 |
+
xmax = df[f"{visualized_result}_mean"].max()
|
| 511 |
+
xrange = [xmin-0.02*(xmax-xmin), xmax+0.02*(xmax-xmin)]
|
| 512 |
+
|
| 513 |
+
for distribution in ["Normal", "Outlier", "Extreme outlier"]:
|
| 514 |
+
sub_df = df[
|
| 515 |
+
df["distribution"] == distribution]
|
| 516 |
+
hover_info = [[row["Dataset"], row["Site (anonymized)"], row["Instrument model"],
|
| 517 |
+
row[f"{visualized_result}_count"], row[f"{visualized_result}_mean"], row[f"{visualized_result}_std"],
|
| 518 |
+
row["distribution"]]
|
| 519 |
+
for row_index, row in sub_df.iterrows()]
|
| 520 |
+
fig.add_trace(go.Bar(x=sub_df[f"{visualized_result}_mean"],
|
| 521 |
+
y=sub_df["Dataset_wCount_index"],
|
| 522 |
+
base=[xrange[0]] * len(sub_df),
|
| 523 |
+
orientation="h",
|
| 524 |
+
error_x=dict(type="data",
|
| 525 |
+
array=sub_df[f"{visualized_result}_std"],
|
| 526 |
+
visible=True),
|
| 527 |
+
marker_color=sub_df["bar_color"],
|
| 528 |
+
width=0.5,
|
| 529 |
+
name=distribution,
|
| 530 |
+
customdata=hover_info,
|
| 531 |
+
hovertemplate=("Dataset: %{customdata[0]}<br>" +
|
| 532 |
+
"Site (anonymized): %{customdata[1]}<br>" +
|
| 533 |
+
"Instrument model: %{customdata[2]}<br>" +
|
| 534 |
+
"Data count: %{customdata[3]}<br>" +
|
| 535 |
+
"Mean (bar): %{customdata[4]}<br>" +
|
| 536 |
+
"STD (error bar): %{customdata[5]}<br>" +
|
| 537 |
+
"Distribution: %{customdata[6]}" +
|
| 538 |
+
"<extra></extra>"
|
| 539 |
+
)),
|
| 540 |
+
row=1, col=result_index)
|
| 541 |
+
|
| 542 |
+
for statistic_key, statistic_value in statistic.items():
|
| 543 |
+
if statistic_key != "IQR" and (statistic_value >= xrange[0] and statistic_value <= xrange[1]):
|
| 544 |
+
if statistic_key in ["Q1", "Q2", "Q3"]:
|
| 545 |
+
line_color = "blue"
|
| 546 |
+
elif statistic_key in ["Lower fence", "Upper fence"]:
|
| 547 |
+
line_color = "orange"
|
| 548 |
+
elif statistic_key in ["Extreme lower fence", "Extreme upper fence"]:
|
| 549 |
+
line_color = "red"
|
| 550 |
+
fig.add_trace(go.Scatter(x=[statistic_value]*(len(dataset_wCount_mapping)+2),
|
| 551 |
+
y=[min(list(dataset_wCount_mapping.values()))-0.5] +
|
| 552 |
+
list(dataset_wCount_mapping.values()) +
|
| 553 |
+
[max(
|
| 554 |
+
list(dataset_wCount_mapping.values()))+0.5],
|
| 555 |
+
mode="lines",
|
| 556 |
+
line=dict(
|
| 557 |
+
color=line_color, width=2),
|
| 558 |
+
showlegend=False,
|
| 559 |
+
hoverinfo="text",
|
| 560 |
+
hovertext=f"{statistic_key} ({statistic_value})"
|
| 561 |
+
),
|
| 562 |
+
row=1, col=result_index
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
fig.update_xaxes(title_text=visualized_result,
|
| 566 |
+
range=xrange,
|
| 567 |
+
automargin=False,
|
| 568 |
+
row=1, col=result_index)
|
| 569 |
+
|
| 570 |
+
fig.update_yaxes(title_text="Dataset (Result counts)",
|
| 571 |
+
range=[min(list(dataset_wCount_mapping.values()))-1,
|
| 572 |
+
max(list(dataset_wCount_mapping.values()))+1],
|
| 573 |
+
tickvals=list(
|
| 574 |
+
dataset_wCount_mapping.values()),
|
| 575 |
+
ticktext=list(dataset_wCount_mapping.keys()),
|
| 576 |
+
autorange=False,
|
| 577 |
+
automargin=False,
|
| 578 |
+
row=1, col=result_index)
|
| 579 |
+
|
| 580 |
+
dataset_counts.append(len(dataset_wCount_mapping))
|
| 581 |
+
|
| 582 |
+
margin_top = 30
|
| 583 |
+
margin_bottom = 50
|
| 584 |
+
margin_left = 200
|
| 585 |
+
plot_height = 40 * max(dataset_counts)
|
| 586 |
+
fig.update_layout(height=max([margin_top + plot_height + margin_bottom,
|
| 587 |
+
250]),
|
| 588 |
+
width=800*len(visualized_results),
|
| 589 |
+
margin=dict(t=margin_top, b=margin_bottom, l=margin_left))
|
| 590 |
+
|
| 591 |
+
return fig
|
| 592 |
+
|
| 593 |
+
def visualize_heatmap(analyzed_gating_results: pd.DataFrame,
|
| 594 |
+
visualized_results: list[str]
|
| 595 |
+
) -> go.Figure:
|
| 596 |
+
|
| 597 |
+
space_between_ratio = 0.4/(len(visualized_results))
|
| 598 |
+
fig = make_subplots(rows=1, cols=len(visualized_results),
|
| 599 |
+
subplot_titles=visualized_results,
|
| 600 |
+
horizontal_spacing=space_between_ratio,
|
| 601 |
+
shared_xaxes=True, shared_yaxes=True
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
for result_index, visualized_result in enumerate(visualized_results):
|
| 605 |
+
result_index += 1
|
| 606 |
+
|
| 607 |
+
std_table = analyzed_gating_results.pivot(
|
| 608 |
+
index=["Dataset", "Site (anonymized)", "Instrument model"],
|
| 609 |
+
columns="Result ID", values=f"{visualized_result}_std")
|
| 610 |
+
|
| 611 |
+
count_table = analyzed_gating_results.pivot(
|
| 612 |
+
index=["Dataset", "Site (anonymized)", "Instrument model"],
|
| 613 |
+
columns="Result ID", values=f"{visualized_result}_count")
|
| 614 |
+
|
| 615 |
+
std_table = std_table[sorted(
|
| 616 |
+
std_table.columns.to_list(), key=lambda s: int(s.split(" ")[0]))]
|
| 617 |
+
count_table = count_table[sorted(
|
| 618 |
+
count_table.columns.to_list(), key=lambda s: int(s.split(" ")[0]))]
|
| 619 |
+
|
| 620 |
+
numeric_values = pd.to_numeric(
|
| 621 |
+
std_table.values.flatten(), errors="coerce")
|
| 622 |
+
numeric_values = numeric_values[~np.isnan(numeric_values)]
|
| 623 |
+
|
| 624 |
+
def scientific_anno(x):
|
| 625 |
+
try:
|
| 626 |
+
x = float(x)
|
| 627 |
+
if x >= 1000:
|
| 628 |
+
return f"{x:.1e}"
|
| 629 |
+
else:
|
| 630 |
+
return f"{round(x, 2)}"
|
| 631 |
+
except:
|
| 632 |
+
return x
|
| 633 |
+
|
| 634 |
+
annotation_table = pd.DataFrame()
|
| 635 |
+
for col in std_table.columns:
|
| 636 |
+
annotation_table[col] = std_table[col].apply(
|
| 637 |
+
scientific_anno)
|
| 638 |
+
annotation_table = annotation_table.replace({"Not reportable": "Not<br>reportable",
|
| 639 |
+
"Only one data": "Only<br>one data"})
|
| 640 |
+
|
| 641 |
+
hover_info = [[[row["Site (anonymized)"], row["Instrument model"], count_table.iloc[row_index, col_index]]
|
| 642 |
+
for col_index, col_key in enumerate(std_table.columns.to_list())]
|
| 643 |
+
for row_index, row in std_table.reset_index().iterrows()]
|
| 644 |
+
subplot_ratio = (
|
| 645 |
+
1 - space_between_ratio * (len(visualized_results)-1))/len(visualized_results)
|
| 646 |
+
|
| 647 |
+
if len(numeric_values) > 0:
|
| 648 |
+
fig.add_trace(go.Heatmap(z=std_table.values,
|
| 649 |
+
text=annotation_table.values,
|
| 650 |
+
texttemplate="%{text}",
|
| 651 |
+
x=std_table.columns,
|
| 652 |
+
y=std_table.reset_index()["Dataset"],
|
| 653 |
+
xgap=2, ygap=2,
|
| 654 |
+
colorbar_x=(
|
| 655 |
+
(subplot_ratio+space_between_ratio)*result_index - space_between_ratio*(8/9)),
|
| 656 |
+
colorscale="Magma",
|
| 657 |
+
zauto=False,
|
| 658 |
+
zmin=np.percentile(numeric_values, 2),
|
| 659 |
+
zmax=np.percentile(
|
| 660 |
+
numeric_values, 98),
|
| 661 |
+
customdata=hover_info,
|
| 662 |
+
hovertemplate=("Dataset: %{y}<br>" +
|
| 663 |
+
"Site (anonymized): %{customdata[0]}<br>" +
|
| 664 |
+
"Instrument model: %{customdata[1]}<br>" +
|
| 665 |
+
"Result ID: %{x}<br>" +
|
| 666 |
+
"Data count: %{customdata[2]}<br>" +
|
| 667 |
+
"STD value: %{z}" +
|
| 668 |
+
"<extra></extra>"
|
| 669 |
+
)
|
| 670 |
+
),
|
| 671 |
+
row=1, col=result_index)
|
| 672 |
+
else:
|
| 673 |
+
fig.add_trace(go.Heatmap(z=pd.DataFrame(0.5, index=std_table.index, columns=std_table.columns),
|
| 674 |
+
text=annotation_table.values,
|
| 675 |
+
texttemplate="%{text}",
|
| 676 |
+
x=std_table.columns,
|
| 677 |
+
y=std_table.reset_index()["Dataset"],
|
| 678 |
+
xgap=2, ygap=2,
|
| 679 |
+
showscale=False,
|
| 680 |
+
colorscale=[
|
| 681 |
+
[0, "blue"], [0.5, "rgba(0,0,0,0)"], [1, "red"]],
|
| 682 |
+
zauto=False, zmin=0, zmax=1,
|
| 683 |
+
customdata=hover_info,
|
| 684 |
+
hovertemplate=("Dataset: %{y}<br>" +
|
| 685 |
+
"Site (anonymized): %{customdata[0]}<br>" +
|
| 686 |
+
"Instrument model: %{customdata[1]}<br>" +
|
| 687 |
+
"Result ID: %{x}<br>" +
|
| 688 |
+
"Data count: %{customdata[2]}<br>" +
|
| 689 |
+
"STD value: %{z}" +
|
| 690 |
+
"<extra></extra>"
|
| 691 |
+
)
|
| 692 |
+
),
|
| 693 |
+
row=1, col=result_index)
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
fig.update_xaxes(title_text="Result ID",
|
| 697 |
+
automargin=False,
|
| 698 |
+
showticklabels=True,
|
| 699 |
+
type="category",
|
| 700 |
+
tickmode="array",
|
| 701 |
+
tickvals=std_table.columns.to_list(),
|
| 702 |
+
side="top"
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
for col_index in range(len(visualized_results)):
|
| 706 |
+
col_index += 1
|
| 707 |
+
fig.layout[f"xaxis{len(visualized_results) + col_index}"] = {
|
| 708 |
+
"title_text": None,
|
| 709 |
+
"automargin": False,
|
| 710 |
+
"showticklabels": True,
|
| 711 |
+
"type": "category",
|
| 712 |
+
"tickmode": "array",
|
| 713 |
+
"tickvals": std_table.columns.to_list(),
|
| 714 |
+
"mirror": "allticks",
|
| 715 |
+
"overlaying": f"x{col_index}",
|
| 716 |
+
"anchor": f"y{col_index}",
|
| 717 |
+
"side": "top"
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
fig.update_yaxes(title_text="Dataset",
|
| 721 |
+
autorange="reversed",
|
| 722 |
+
automargin=False,
|
| 723 |
+
showticklabels=True,
|
| 724 |
+
type="category",
|
| 725 |
+
tickmode="array",
|
| 726 |
+
tickvals=std_table.reset_index()["Dataset"].to_list())
|
| 727 |
+
|
| 728 |
+
margin_top = 200
|
| 729 |
+
margin_bottom = 30
|
| 730 |
+
margin_left = 200
|
| 731 |
+
margin_right = 30
|
| 732 |
+
plot_height = 50 * len(std_table.reset_index()["Dataset"].to_list())
|
| 733 |
+
plot_width = 900*len(visualized_results)
|
| 734 |
+
fig.update_layout(height=max([margin_top + plot_height + margin_bottom,
|
| 735 |
+
200]),
|
| 736 |
+
width=plot_width,
|
| 737 |
+
margin=dict(t=margin_top, b=margin_bottom, l=margin_left, r=margin_right))
|
| 738 |
+
|
| 739 |
+
for annotation in fig.layout.annotations:
|
| 740 |
+
annotation.xshift = -400
|
| 741 |
+
annotation.yshift = 5
|
| 742 |
+
|
| 743 |
+
return fig
|
poetry.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pyproject.toml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project] # This PEP 621 section is good for metadata, but Poetry will use its own for active dependency management.
|
| 2 |
+
name = "nist-cda-dashboard"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = ""
|
| 5 |
+
authors = [
|
| 6 |
+
{name = "robinchu", email = "robin.chu@aheadmedicine.com"}
|
| 7 |
+
]
|
| 8 |
+
readme = "README.md"
|
| 9 |
+
requires-python = ">=3.12"
|
| 10 |
+
|
| 11 |
+
[tool.poetry]
|
| 12 |
+
name = "nist-cda-dashboard"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = ""
|
| 15 |
+
authors = ["robinchu <robin.chu@aheadmedicine.com>"] # Poetry's preferred author format
|
| 16 |
+
readme = "README.md"
|
| 17 |
+
|
| 18 |
+
[tool.poetry.dependencies]
|
| 19 |
+
python = ">=3.12"
|
| 20 |
+
rapidfuzz = ">=3.13.0,<4.0.0" # Add these here
|
| 21 |
+
openpyxl = ">=3.1.5,<4.0.0" # Add these here
|
| 22 |
+
gradio = "^5.29.0"
|
| 23 |
+
plotly = "^6.0.1"
|
| 24 |
+
pandas = "^2.2.3"
|
| 25 |
+
kaleido = "0.2.1"
|
| 26 |
+
|
| 27 |
+
[tool.poetry.group.dev.dependencies]
|
| 28 |
+
|
| 29 |
+
[build-system]
|
| 30 |
+
requires = ["poetry-core>=1.0.0"]
|
| 31 |
+
build-backend = "poetry.core.masonry.api"
|
requirements.txt
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
aiofiles==24.1.0 ; python_version >= "3.12"
|
| 2 |
+
annotated-types==0.7.0 ; python_version >= "3.12"
|
| 3 |
+
anyio==4.9.0 ; python_version >= "3.12"
|
| 4 |
+
audioop-lts==0.2.1 ; python_version >= "3.13"
|
| 5 |
+
certifi==2025.4.26 ; python_version >= "3.12"
|
| 6 |
+
charset-normalizer==3.4.2 ; python_version >= "3.12"
|
| 7 |
+
click==8.1.8 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 8 |
+
colorama==0.4.6 ; python_version >= "3.12" and platform_system == "Windows"
|
| 9 |
+
et-xmlfile==2.0.0 ; python_version >= "3.12"
|
| 10 |
+
fastapi==0.115.12 ; python_version >= "3.12"
|
| 11 |
+
ffmpy==0.3.2 ; python_version >= "3.12"
|
| 12 |
+
filelock==3.18.0 ; python_version >= "3.12"
|
| 13 |
+
fsspec==2025.3.2 ; python_version >= "3.12"
|
| 14 |
+
gradio-client==1.10.1 ; python_version >= "3.12"
|
| 15 |
+
gradio==5.29.1 ; python_version >= "3.12"
|
| 16 |
+
groovy==0.1.2 ; python_version >= "3.12"
|
| 17 |
+
h11==0.16.0 ; python_version >= "3.12"
|
| 18 |
+
httpcore==1.0.9 ; python_version >= "3.12"
|
| 19 |
+
httpx==0.28.1 ; python_version >= "3.12"
|
| 20 |
+
huggingface-hub==0.31.2 ; python_version >= "3.12"
|
| 21 |
+
idna==3.10 ; python_version >= "3.12"
|
| 22 |
+
jinja2==3.1.6 ; python_version >= "3.12"
|
| 23 |
+
kaleido==0.2.1 ; python_version >= "3.12"
|
| 24 |
+
markdown-it-py==3.0.0 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 25 |
+
markupsafe==3.0.2 ; python_version >= "3.12"
|
| 26 |
+
mdurl==0.1.2 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 27 |
+
narwhals==1.38.0 ; python_version >= "3.12"
|
| 28 |
+
numpy==2.2.5 ; python_version >= "3.12"
|
| 29 |
+
openpyxl==3.1.5 ; python_version >= "3.12"
|
| 30 |
+
orjson==3.10.18 ; python_version >= "3.12"
|
| 31 |
+
packaging==24.2 ; python_version >= "3.12"
|
| 32 |
+
pandas==2.2.3 ; python_version >= "3.12"
|
| 33 |
+
pillow==11.2.1 ; python_version >= "3.12"
|
| 34 |
+
plotly==6.1.0 ; python_version >= "3.12"
|
| 35 |
+
pydantic-core==2.33.2 ; python_version >= "3.12"
|
| 36 |
+
pydantic==2.11.4 ; python_version >= "3.12"
|
| 37 |
+
pydub==0.25.1 ; python_version >= "3.12"
|
| 38 |
+
pygments==2.19.1 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 39 |
+
python-dateutil==2.9.0.post0 ; python_version >= "3.12"
|
| 40 |
+
python-multipart==0.0.20 ; python_version >= "3.12"
|
| 41 |
+
pytz==2025.2 ; python_version >= "3.12"
|
| 42 |
+
pyyaml==6.0.2 ; python_version >= "3.12"
|
| 43 |
+
rapidfuzz==3.13.0 ; python_version >= "3.12"
|
| 44 |
+
requests==2.32.3 ; python_version >= "3.12"
|
| 45 |
+
rich==14.0.0 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 46 |
+
ruff==0.11.10 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 47 |
+
safehttpx==0.1.6 ; python_version >= "3.12"
|
| 48 |
+
semantic-version==2.10.0 ; python_version >= "3.12"
|
| 49 |
+
shellingham==1.5.4 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 50 |
+
six==1.17.0 ; python_version >= "3.12"
|
| 51 |
+
sniffio==1.3.1 ; python_version >= "3.12"
|
| 52 |
+
starlette==0.46.2 ; python_version >= "3.12"
|
| 53 |
+
tomlkit==0.13.2 ; python_version >= "3.12"
|
| 54 |
+
tqdm==4.67.1 ; python_version >= "3.12"
|
| 55 |
+
typer==0.15.4 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 56 |
+
typing-extensions==4.13.2 ; python_version >= "3.12"
|
| 57 |
+
typing-inspection==0.4.0 ; python_version >= "3.12"
|
| 58 |
+
tzdata==2025.2 ; python_version >= "3.12"
|
| 59 |
+
urllib3==2.4.0 ; python_version >= "3.12"
|
| 60 |
+
uvicorn==0.34.2 ; sys_platform != "emscripten" and python_version >= "3.12"
|
| 61 |
+
websockets==15.0.1 ; python_version >= "3.12"
|