RobinChu commited on
Commit
e42f219
·
1 Parent(s): 7536e13

initial commit

Browse files
.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: Test1
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"