From 1b37039d94a17a7d97bb31f744a6af05a113877d Mon Sep 17 00:00:00 2001 From: root Date: Sun, 6 Jul 2025 10:40:11 -0700 Subject: [PATCH 01/49] Add README with Windows and Ubuntu installation guide --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bedf219..34483f2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,47 @@ -# SPAC_Shiny -The Shiny Interactive Realtime Dashboard for SPAC +# 🧬 SPAC_Shiny – Windows Installation for Modularize Branch + +## 📁 Branch + +This setup guide is tailored for the `modularize` branch. + +--- + +## Windows Installation using Ubuntu + +### Prerequisites + +- [Git for Windows](https://git-scm.com/download/win) +- [Miniconda](https://docs.conda.io/en/latest/miniconda.html) +- [Visual Studio Code](https://code.visualstudio.com/) (optional) +- [Ubuntu] (wsl --install) + +### Steps + +1. **Access Ubuntu** + In terminal + ```bash + wsl --install + +2. **Clone the repository** + ```bash + git clone --branch modularize https://github.com//SPAC_Shiny.git + cd SPAC_Shiny + +3. **Create and activate environment** + ```bash + conda create -n spac_env_3119 python=3.11 + conda activate spac_env_3119 + +4. **Install dependencies** + pip install --upgrade pip wheel + pip install scikit-learn==1.3.2 --only-binary :all: + pip install llvmlite==0.41.1 --only-binary :all: + pip install shiny scanpy + pip install --no-deps -r requirements.txt + + +5. **Launch the app** + python.app.py + Type http://127.0.0.1:8000 to access app on local web browser + + From 5901c975706232b87f65ee8d10edfca3d6f3e833 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 6 Jul 2025 11:02:22 -0700 Subject: [PATCH 02/49] Update README with additional setup details --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 34483f2..18d2d72 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# 🧬 SPAC_Shiny – Windows Installation for Modularize Branch +# SPAC_Shiny – Windows Installation for Modularize Branch -## 📁 Branch +## Branch This setup guide is tailored for the `modularize` branch. @@ -33,6 +33,7 @@ This setup guide is tailored for the `modularize` branch. conda activate spac_env_3119 4. **Install dependencies** + ```bash pip install --upgrade pip wheel pip install scikit-learn==1.3.2 --only-binary :all: pip install llvmlite==0.41.1 --only-binary :all: @@ -41,7 +42,6 @@ This setup guide is tailored for the `modularize` branch. 5. **Launch the app** + ```bash python.app.py - Type http://127.0.0.1:8000 to access app on local web browser - - + http://127.0.0.1:8000 From 106804a0b63ec06f6f7b115b4998510e928d97a6 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 6 Jul 2025 11:08:27 -0700 Subject: [PATCH 03/49] Update README with more additional setup details --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 18d2d72..751a5bf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # SPAC_Shiny – Windows Installation for Modularize Branch + ## Branch -This setup guide is tailored for the `modularize` branch. +This setup guide is for the `modularize` branch. --- @@ -13,7 +14,7 @@ This setup guide is tailored for the `modularize` branch. - [Git for Windows](https://git-scm.com/download/win) - [Miniconda](https://docs.conda.io/en/latest/miniconda.html) - [Visual Studio Code](https://code.visualstudio.com/) (optional) -- [Ubuntu] (wsl --install) +- [wsl --install](https://learn.microsoft.com/en-us/windows/wsl/install) ### Steps @@ -43,5 +44,7 @@ This setup guide is tailored for the `modularize` branch. 5. **Launch the app** ```bash - python.app.py - http://127.0.0.1:8000 + python app.py + + Visit http://127.0.0.1:8000 in order to fully access the local web server + From 8ea7a233d8eb80b57bc9730f71dbad7e46d551be Mon Sep 17 00:00:00 2001 From: root Date: Sun, 6 Jul 2025 11:09:52 -0700 Subject: [PATCH 04/49] Update README with more additional setup details --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 751a5bf..29dae71 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This setup guide is for the `modularize` branch. - [Git for Windows](https://git-scm.com/download/win) - [Miniconda](https://docs.conda.io/en/latest/miniconda.html) - [Visual Studio Code](https://code.visualstudio.com/) (optional) -- [wsl --install](https://learn.microsoft.com/en-us/windows/wsl/install) +- [Ubuntu](https://ubuntu.com/download) ### Steps From f59a8b5b6d239e1abef547df2164d171931a712e Mon Sep 17 00:00:00 2001 From: root Date: Sun, 6 Jul 2025 19:56:09 -0700 Subject: [PATCH 05/49] Update README with additional setup details --- README.md | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 29dae71..791c08d 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,58 @@ -# SPAC_Shiny – Windows Installation for Modularize Branch +# SPAC Shiny: Windows Installation Guide - -## Branch +The SPAC Shiny App is a web-based dashboard that allows researchers to analyze single-cell data. This dahboard contains a user friendly interface with a customizable graphs, along with a modular pipeline for examining images. Use this installation guide for WSL environments to setup the SPAC Shiny dashboard. This setup guide is for the `modularize` branch. ---- ## Windows Installation using Ubuntu ### Prerequisites +Git for Windows, Miniconda, Vscode, and Ubuntu are all used to setup and launch this app. - [Git for Windows](https://git-scm.com/download/win) -- [Miniconda](https://docs.conda.io/en/latest/miniconda.html) -- [Visual Studio Code](https://code.visualstudio.com/) (optional) +- [Miniconda (Python 3.11+)](https://docs.conda.io/en/latest/miniconda.html) - [Ubuntu](https://ubuntu.com/download) +- [Visual Studio Code](https://code.visualstudio.com/) (optional) ### Steps 1. **Access Ubuntu** - In terminal + + In the terminal, open Ubuntu usin this command. ```bash wsl --install - 2. **Clone the repository** + + After installing Git for Windows, clone the modularize branch to ensure the latest version of the app is available. ```bash - git clone --branch modularize https://github.com//SPAC_Shiny.git + git clone --branch modularize https://github.com/FNLCR-DMAP/SPAC_Shiny.git cd SPAC_Shiny - 3. **Create and activate environment** + + Using miniconda, create a virutal environment called spac_env_3119 using python version 3.11.9. Use the next command to activate the environment. ```bash - conda create -n spac_env_3119 python=3.11 + conda create -n spac_env_3119 python=3.11.9 conda activate spac_env_3119 - 4. **Install dependencies** + + Use the upgrade command to ensure you are using the latest version of pip. Then install scikit learn and llvmlite with the command. This forces pip to use precompiled binary wheels instead of building from source — which avoids messy compiler errors, especially on Windows or WSL setups. + ```bash pip install --upgrade pip wheel pip install scikit-learn==1.3.2 --only-binary :all: pip install llvmlite==0.41.1 --only-binary :all: pip install shiny scanpy pip install --no-deps -r requirements.txt - - 5. **Launch the app** + + Launch the app using python app.py. ```bash python app.py + ``` - Visit http://127.0.0.1:8000 in order to fully access the local web server - + Visit http://127.0.0.1:8000 in order to fully access the local web server. If the local server is not working, try + ```bash + shiny run --reload app.py + ``` + This starts the Shiny server using the CLI tool. From adc67754d51d1ed7d31ee98d1d2213955ab61a75 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Jul 2025 22:57:44 -0700 Subject: [PATCH 06/49] =?UTF-8?q?=E2=9C=A8=20Abbreviation=20toggle,=20font?= =?UTF-8?q?=20styling,=20and=20heatmap=20controls=20implemented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/feat_vs_anno_server.py | 79 ++++++++++++++++++++++++++--------- ui/feat_vs_anno_ui.py | 32 ++++++++++++++ 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/server/feat_vs_anno_server.py b/server/feat_vs_anno_server.py index b559023..132be6b 100644 --- a/server/feat_vs_anno_server.py +++ b/server/feat_vs_anno_server.py @@ -5,6 +5,8 @@ import spac.visualization + + def feat_vs_anno_server(input, output, session, shared): def on_layer_check(): return input.hm1_layer() if input.hm1_layer() != "Original" else None @@ -34,38 +36,75 @@ def spac_Heatmap(): layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) + if adata is not None: vmin = input.min_select() - vmax = input.max_select() - cmap = input.hm1_cmap() # Get the selected color map from the dropdown - kwargs = {"vmin": vmin,"vmax": vmax,} + vmax = input.max_select() + cmap = input.hm1_cmap() + fontsize = input.axis_label_fontsize() + kwargs = {"vmin": vmin, "vmax": vmax} cluster_annotations, cluster_features = on_dendro_check() - df, fig, ax = spac.visualization.hierarchical_heatmap( - adata, - annotation=input.hm1_anno(), - layer=on_layer_check(), - z_score=None, - cluster_annotations=cluster_annotations, - cluster_feature=cluster_features, - **kwargs - ) - # Only update if a non-default color map is selected - if cmap != "viridis": + try: + df, fig, ax = spac.visualization.hierarchical_heatmap( + adata, + annotation=input.hm1_anno(), + layer=on_layer_check(), + z_score=None, + cluster_annotations=cluster_annotations, + cluster_feature=cluster_features, + **kwargs + ) + except Exception as e: + ("Heatmap generation failed:", e) + return None + + if fig is None or not hasattr(fig, "ax_heatmap"): + print("Invalid figure structure.") + return None + + if cmap != "viridis": fig.ax_heatmap.collections[0].set_cmap(cmap) shared['df_heatmap'].set(df) - - # Rotate x-axis labels + + # 🔄 Rotate and style axis labels fig.ax_heatmap.set_xticklabels( fig.ax_heatmap.get_xticklabels(), - rotation=input.hm_x_label_rotation(), # degrees + rotation=input.hm_x_label_rotation(), horizontalalignment='right' ) - # fig is a seaborn.matrix.ClusterGrid - fig.fig.subplots_adjust(bottom=0.4) - fig.fig.subplots_adjust(left=0.1) + fig.ax_heatmap.set_yticklabels( + fig.ax_heatmap.get_yticklabels(), + rotation=input.hm_y_label_rotation(), + verticalalignment='center' + ) + + # Abbreviate tick labels + def abbreviate_labels(labels, limit): + return [label.get_text()[:limit] if label.get_text() else "" for label in labels] + if input.enable_abbreviation(): + limit = input.label_char_limit() + + abbreviated_xticks = abbreviate_labels(fig.ax_heatmap.get_xticklabels(), limit) + fig.ax_heatmap.set_xticklabels(abbreviated_xticks, rotation=input.hm_x_label_rotation()) + + abbreviated_yticks = abbreviate_labels(fig.ax_heatmap.get_yticklabels(), limit) + fig.ax_heatmap.set_yticklabels(abbreviated_yticks, rotation=input.hm_y_label_rotation()) + + # Apply font styling after label rotation + for label in fig.ax_heatmap.get_xticklabels(): + label.set_fontsize(fontsize) + label.set_fontfamily("DejaVu Sans") + + for label in fig.ax_heatmap.get_yticklabels(): + label.set_fontsize(fontsize) + label.set_fontfamily("DejaVu Sans") + + # Layout adjustments + fig.fig.subplots_adjust(bottom=0.4, left=0.1) + return fig return None diff --git a/ui/feat_vs_anno_ui.py b/ui/feat_vs_anno_ui.py index e57aa27..93f98d4 100644 --- a/ui/feat_vs_anno_ui.py +++ b/ui/feat_vs_anno_ui.py @@ -36,11 +36,43 @@ def feat_vs_anno_ui(): max=90, value=25 ), + ui.input_slider( + "hm_y_label_rotation", + "Rotate Y Axis Labels", + min=0, + max=90, + value=25 + ), + + ui.input_slider( + "axis_label_fontsize", + "Axis Label Font Size", + min=3, + max=24, + value=10 + ), + ui.input_checkbox( "dendogram", "Include Dendrogram", False ), + + ui.input_checkbox( + "enable_abbreviation", + "Abbreviate Axis Labels", + value=False + ), + + ui.input_slider( + "label_char_limit", + "Max Characters per Label", + min=2, + max=20, + value=6 + ), + + ui.div(id="main-hm1_check"), ui.div(id="main-hm2_check"), ui.div(id="main-min_num"), From d574b0a49daf5739a94622ebd7170adc81301562 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 20 Jul 2025 22:59:14 -0700 Subject: [PATCH 07/49] =?UTF-8?q?=F0=9F=94=A7=20Updated=20issues=20relatin?= =?UTF-8?q?g=20to=20overlapping=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ __pycache__/app.cpython-39.pyc | Bin 40030 -> 1824 bytes 2 files changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/__pycache__/app.cpython-39.pyc b/__pycache__/app.cpython-39.pyc index 48b9ace1b5955f893f435bb2d1efe40a448765fb..bde9e928e90bdf309a90e565f5f0c82b9a0de99f 100644 GIT binary patch literal 1824 zcmbtVON%5$5UzLEqhHhA^WK@&*+p$xW^_;xS;S$)lPto(A_-FDbY)K$TUk|RRy8}) zb83HvhaGQT{W*2@pn_K~f@nl`HFlw%tRlXOFETR7h>XmlosO@;^V7xM?1w= z4)2n#nqwwC-Y0#&MYi~W4ET@?`G}18HreJoWQXsPUA{;5_&(X^2jqYsl0!AmN*?ed zas;|fj@c1AUh3o_GhV$0@;bAY8nZzhkQRsw;(_=e0Z1F91JYe;_rbkRP9i62eWQ^c z)7c5@Ee-N0a<8+!x#%|>tJ*3wM{Kkt-m&O z*E!UD*vMmTY~-?@xz4BNfy${SgU!5Z+wMPdTSr>wx8h^H@{){3Atw6uAY${7y`&sU zaGJ-H7YUn~xP$fM!q*151d+c26xu62((m;<6Scws&ANd`{!YKM3u9p}tjPS>+ zbvIe0K{H#=J{=E<^M$GvakFc=(mc@kN(~Zv%S3H==Ty|bC3G}anD}x=bN2VVFr}PH z;;RrVy1j7Qz5wh%iFf zM%Y2vMc6~wM>s$@1Q@%Ot?q%!$;txmgQ_K&lyDP>WAu3l0PDr;;sv{vl>=oIEUHXk z-)rIx82cSBebYDG zT|V7(fM{4OIt$r$RUgEd)F$!b8Bnf`A`_CfW!c zgf0U95LD3rQ#Z0*7I7j^Rb_IKO|WR`jd%dTD{mSnEQE`2ruL`ub9%$VX;Q{f)l2DI zvLc*N5rqZ4t{kWk-hU>1RXsQ0O^oI~?0kRd;&1C-s^qPZ=06x8kYVa?{CJR IR^Pq+3zUBSqW}N^ literal 40030 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 From 59c26d5f7950cef2967e463cd274df239d2d8e88 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 23 Jul 2025 23:26:09 -0700 Subject: [PATCH 08/49] Update feat_vs_anno_server.py with better readibality --- server/feat_vs_anno_server.py | 187 +++++++++++++++++++--------------- 1 file changed, 103 insertions(+), 84 deletions(-) diff --git a/server/feat_vs_anno_server.py b/server/feat_vs_anno_server.py index 132be6b..0cba34f 100644 --- a/server/feat_vs_anno_server.py +++ b/server/feat_vs_anno_server.py @@ -5,9 +5,19 @@ import spac.visualization +def feat_vs_anno_server(input, output, session, shared): + rendering_state = reactive.Value(False) + @reactive.effect + @reactive.event(input.go_hm1) + def handle_render_start(): + rendering_state.set(True) + + @reactive.effect + @reactive.event(input.cancel_hm1) + def handle_cancel_click(): + rendering_state.set(False) -def feat_vs_anno_server(input, output, session, shared): def on_layer_check(): return input.hm1_layer() if input.hm1_layer() != "Original" else None @@ -20,8 +30,8 @@ def on_dendro_check(): return (None, None) to indicate that no dendrogram is available. ''' return ( - (input.h2_anno_dendro(), input.h2_feat_dendro()) - if input.dendogram() + (input.h2_anno_dendro(), input.h2_feat_dendro()) + if input.dendogram() else (None, None) ) @@ -30,85 +40,78 @@ def on_dendro_check(): @reactive.event(input.go_hm1, ignore_none=True) def spac_Heatmap(): adata = ad.AnnData( - X=shared['X_data'].get(), - obs=pd.DataFrame(shared['obs_data'].get()), - var=pd.DataFrame(shared['var_data'].get()), - layers=shared['layers_data'].get(), + X=shared['X_data'].get(), + obs=pd.DataFrame(shared['obs_data'].get()), + var=pd.DataFrame(shared['var_data'].get()), + layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) - - if adata is not None: - vmin = input.min_select() - vmax = input.max_select() - cmap = input.hm1_cmap() - fontsize = input.axis_label_fontsize() - - kwargs = {"vmin": vmin, "vmax": vmax} - cluster_annotations, cluster_features = on_dendro_check() - - try: - df, fig, ax = spac.visualization.hierarchical_heatmap( - adata, - annotation=input.hm1_anno(), - layer=on_layer_check(), - z_score=None, - cluster_annotations=cluster_annotations, - cluster_feature=cluster_features, - **kwargs - ) - except Exception as e: - ("Heatmap generation failed:", e) - return None - - if fig is None or not hasattr(fig, "ax_heatmap"): - print("Invalid figure structure.") - return None - - if cmap != "viridis": - fig.ax_heatmap.collections[0].set_cmap(cmap) - - shared['df_heatmap'].set(df) - - # 🔄 Rotate and style axis labels - fig.ax_heatmap.set_xticklabels( - fig.ax_heatmap.get_xticklabels(), - rotation=input.hm_x_label_rotation(), - horizontalalignment='right' - ) - fig.ax_heatmap.set_yticklabels( - fig.ax_heatmap.get_yticklabels(), - rotation=input.hm_y_label_rotation(), - verticalalignment='center' + if adata is None: + return None + + vmin = input.min_select() + vmax = input.max_select() + cmap = input.hm1_cmap() + fontsize = input.axis_label_fontsize() + kwargs = {"vmin": vmin, "vmax": vmax} + cluster_annotations, cluster_features = on_dendro_check() + + try: + df, fig, ax = spac.visualization.hierarchical_heatmap( + adata, + annotation=input.hm1_anno(), + layer=on_layer_check(), + z_score=None, + cluster_annotations=cluster_annotations, + cluster_feature=cluster_features, + **kwargs ) + except Exception as e: + print("Heatmap generation failed:", e) + return None - # Abbreviate tick labels - def abbreviate_labels(labels, limit): - return [label.get_text()[:limit] if label.get_text() else "" for label in labels] - if input.enable_abbreviation(): - limit = input.label_char_limit() + if fig is None or not hasattr(fig, "ax_heatmap"): + print("Invalid figure structure.") + return None - abbreviated_xticks = abbreviate_labels(fig.ax_heatmap.get_xticklabels(), limit) - fig.ax_heatmap.set_xticklabels(abbreviated_xticks, rotation=input.hm_x_label_rotation()) + if cmap != "viridis": + fig.ax_heatmap.collections[0].set_cmap(cmap) - abbreviated_yticks = abbreviate_labels(fig.ax_heatmap.get_yticklabels(), limit) - fig.ax_heatmap.set_yticklabels(abbreviated_yticks, rotation=input.hm_y_label_rotation()) + shared['df_heatmap'].set(df) - # Apply font styling after label rotation - for label in fig.ax_heatmap.get_xticklabels(): - label.set_fontsize(fontsize) - label.set_fontfamily("DejaVu Sans") - - for label in fig.ax_heatmap.get_yticklabels(): - label.set_fontsize(fontsize) - label.set_fontfamily("DejaVu Sans") - - # Layout adjustments - fig.fig.subplots_adjust(bottom=0.4, left=0.1) - - return fig + #Rotate X and Y axis labels + fig.ax_heatmap.set_xticklabels( + fig.ax_heatmap.get_xticklabels(), + rotation=input.hm_x_label_rotation(), + horizontalalignment='right' + ) + fig.ax_heatmap.set_yticklabels( + fig.ax_heatmap.get_yticklabels(), + rotation=input.hm_y_label_rotation(), + verticalalignment='center' + ) + + # Abbreviate labels if enabled + def abbreviate_labels(labels, limit): + return [label.get_text()[:limit] if label.get_text() else "" for label in labels] + + if input.enable_abbreviation(): + limit = input.label_char_limit() + abbreviated_xticks = abbreviate_labels(fig.ax_heatmap.get_xticklabels(), limit) + fig.ax_heatmap.set_xticklabels(abbreviated_xticks, rotation=input.hm_x_label_rotation()) + abbreviated_yticks = abbreviate_labels(fig.ax_heatmap.get_yticklabels(), limit) + fig.ax_heatmap.set_yticklabels(abbreviated_yticks, rotation=input.hm_y_label_rotation()) + + for label in fig.ax_heatmap.get_xticklabels(): + label.set_fontsize(fontsize) + label.set_fontfamily("DejaVu Sans") + for label in fig.ax_heatmap.get_yticklabels(): + label.set_fontsize(fontsize) + label.set_fontfamily("DejaVu Sans") + + fig.fig.subplots_adjust(bottom=0.4, left=0.1) + return fig - return None - heatmap_ui_initialized = reactive.Value(False) @reactive.effect @@ -117,27 +120,26 @@ def heatmap_reactivity(): ui_initialized = heatmap_ui_initialized.get() if btn and not ui_initialized: - annotation_check = ui.input_checkbox("h2_anno_dendro", "Annotation Cluster", value=False) + # Insert feature cluster first + feat_check = ui.input_checkbox("h2_feat_dendro", "Feature Cluster", value=False) ui.insert_ui( - ui.div({"id": "inserted-check"}, annotation_check), - selector="#main-hm1_check", + ui.div({"id": "inserted-check1"}, feat_check), + selector="#main-hm2_check", where="beforeEnd", ) - - feat_check = ui.input_checkbox("h2_feat_dendro", "Feature Cluster", value=False) + # Insert annotation cluster below + annotation_check = ui.input_checkbox("h2_anno_dendro", "Annotation Cluster", value=False) ui.insert_ui( - ui.div({"id": "inserted-check1"}, feat_check), + ui.div({"id": "inserted-check"}, annotation_check), selector="#main-hm2_check", where="beforeEnd", ) heatmap_ui_initialized.set(True) - elif not btn and ui_initialized: ui.remove_ui("#inserted-check") ui.remove_ui("#inserted-check1") heatmap_ui_initialized.set(False) - @render.download(filename="heatmap_data.csv") def download_df(): df = shared['df_heatmap'].get() @@ -147,7 +149,6 @@ def download_df(): return csv_bytes, "text/csv" return None - @render.ui @reactive.event(input.go_hm1, ignore_none=True) def download_button_ui(): @@ -155,7 +156,6 @@ def download_button_ui(): return ui.download_button("download_df", "Download Data", class_="btn-warning") return None - @reactive.effect @reactive.event(input.hm1_layer) def update_min_max(): @@ -202,3 +202,22 @@ def update_min_max(): selector="#main-max_num", where="beforeEnd", ) + + @reactive.effect + @reactive.event(input.enable_abbreviation) + def toggle_label_char_limit_slider(): + if input.enable_abbreviation(): + slider = ui.input_slider( + "label_char_limit", + "Max Characters per Label", + min=2, + max=20, + value=6 + ) + ui.insert_ui( + ui.div({"id": "inserted-label-char-limit"}, slider), + selector="#main-hm1_check", # Or another appropriate container + where="beforeEnd", + ) + else: + ui.remove_ui("#inserted-label-char-limit") From c7ea3ea843b8788b4920db1c99f8d2e9e2df124a Mon Sep 17 00:00:00 2001 From: root Date: Wed, 23 Jul 2025 23:33:44 -0700 Subject: [PATCH 09/49] Update feat_vs_anno_server.py with better readability for the labels --- ui/feat_vs_anno_ui.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/ui/feat_vs_anno_ui.py b/ui/feat_vs_anno_ui.py index 93f98d4..8984f9e 100644 --- a/ui/feat_vs_anno_ui.py +++ b/ui/feat_vs_anno_ui.py @@ -52,11 +52,6 @@ def feat_vs_anno_ui(): value=10 ), - ui.input_checkbox( - "dendogram", - "Include Dendrogram", - False - ), ui.input_checkbox( "enable_abbreviation", @@ -64,24 +59,27 @@ def feat_vs_anno_ui(): value=False ), - ui.input_slider( - "label_char_limit", - "Max Characters per Label", - min=2, - max=20, - value=6 - ), - + ui.div(id="main-hm1_check"), - ui.div(id="main-hm1_check"), + ui.input_checkbox( + "dendogram", + "Include Dendrogram", + False + ), + ui.div(id="main-hm2_check"), ui.div(id="main-min_num"), ui.div(id="main-max_num"), + # Grouped buttons with spacing + ui.input_action_button( - "go_hm1", - "Render Plot", + "go_hm1", + "Render Plot", class_="btn-success" ), + + + ui.div( {"style": "padding-top: 20px;"}, ui.output_ui("download_button_ui") From 75731ec07a796f7c5d40f3c192b796269d411605 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 31 Jul 2025 23:16:06 -0700 Subject: [PATCH 10/49] Integrated datashader library into heatmaps for scatterplots and environment config --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc From 90c4513848a2379d92846642297220f690cbbdb6 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 31 Jul 2025 23:18:41 -0700 Subject: [PATCH 11/49] Full integration: Datashader updates, utils, and heatmap UI --- environment.yml | 4 +++- server/scatterplot_server.py | 39 ++++++++++++++++++++++++------------ ui/scatterplot_ui.py | 5 +++++ utils/datashader_utils.py | 16 +++++++++++++++ 4 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 utils/datashader_utils.py diff --git a/environment.yml b/environment.yml index 518cf63..c0b6c16 100644 --- a/environment.yml +++ b/environment.yml @@ -5,7 +5,7 @@ channels: - leej3 - https://fnlcr-dmap.github.io/scimap/ dependencies: - - python=3.9.13 + - python=3.10 - numpy=1.26.4 - pandas=1.5.3 - anndata=0.10.9 @@ -18,8 +18,10 @@ dependencies: - squidpy=1.2.2 - scikit-image=0.19.3 - scipy=1.10.1 + - colorcet - pip - pip: + - git+https://github.com/holoviz/datashader.git - ipykernel==6.29.5 - ipython==8.18.0 - ipython-genutils==0.2.0 diff --git a/server/scatterplot_server.py b/server/scatterplot_server.py index cf2d9c7..479775e 100644 --- a/server/scatterplot_server.py +++ b/server/scatterplot_server.py @@ -2,6 +2,8 @@ import anndata as ad import pandas as pd import spac.visualization +from utils.datashader_utils import scatter_heatmap +import matplotlib.pyplot as plt def scatterplot_server(input, output, session, shared): @@ -120,22 +122,33 @@ def spac_Scatter(): x = get_scatterplot_coordinates_x() y = get_scatterplot_coordinates_y() color_enabled = input.scatter_color_check() + heatmap_mode = input.scatter_heatmap_mode() x_label = input.scatter_x() y_label = input.scatter_y() title = f"Scatterplot: {x_label} vs {y_label}" - if color_enabled: - fig, ax = spac.visualization.visualize_2D_scatter( - x, y, labels=get_color_values() - ) - for a in fig.axes: - if hasattr(a, "get_ylabel") and a != ax: - a.set_ylabel(f"Colored by: {input.scatter_color()}") + if heatmap_mode: + color = get_color_values() if color_enabled else None + img = scatter_heatmap(x, y, color) + fig, ax = plt.subplots(figsize=(8, 6)) + ax.imshow(img, aspect='auto') + ax.set_title(title, fontsize=14) + ax.set_xlabel(x_label) + ax.set_ylabel(y_label) + ax.axis('on') # Show axes + return fig else: - fig, ax = spac.visualization.visualize_2D_scatter(x, y) - - ax.set_title(title, fontsize=14) - ax.set_xlabel(x_label) - ax.set_ylabel(y_label) + if color_enabled: + fig, ax = spac.visualization.visualize_2D_scatter( + x, y, labels=get_color_values() + ) + for a in fig.axes: + if hasattr(a, "get_ylabel") and a != ax: + a.set_ylabel(f"Colored by: {input.scatter_color()}") + else: + fig, ax = spac.visualization.visualize_2D_scatter(x, y) - return ax + ax.set_title(title, fontsize=14) + ax.set_xlabel(x_label) + ax.set_ylabel(y_label) + return ax diff --git a/ui/scatterplot_ui.py b/ui/scatterplot_ui.py index a552a63..6c97823 100644 --- a/ui/scatterplot_ui.py +++ b/ui/scatterplot_ui.py @@ -32,6 +32,11 @@ def scatterplot_ui(): "Color by Feature", value=False ), + ui.input_checkbox( + "scatter_heatmap_mode", + "Show as Heatmap", + value=False + ), ui.div(id="main-scatter_dropdown"), ui.input_action_button( "go_scatter", diff --git a/utils/datashader_utils.py b/utils/datashader_utils.py new file mode 100644 index 0000000..f232b18 --- /dev/null +++ b/utils/datashader_utils.py @@ -0,0 +1,16 @@ +import pandas as pd +import datashader as ds +import datashader.transfer_functions as tf +from colorcet import fire + +def scatter_heatmap(x, y, color=None, width=800, height=600): + df = pd.DataFrame({'x': x, 'y': y}) + cvs = ds.Canvas(plot_width=width, plot_height=height) + if color is not None: + df['color'] = color + agg = cvs.points(df, 'x', 'y', ds.mean('color')) + img = tf.shade(agg, cmap=fire, how='eq_hist') + else: + agg = cvs.points(df, 'x', 'y', ds.count()) + img = tf.shade(agg, cmap=fire) + return img.to_pil() \ No newline at end of file From 33aba4b6df5715f4d32f175e17657548997d954a Mon Sep 17 00:00:00 2001 From: root Date: Thu, 31 Jul 2025 23:21:06 -0700 Subject: [PATCH 12/49] Update to python version to 3.9.13 --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index c0b6c16..24fb6d3 100644 --- a/environment.yml +++ b/environment.yml @@ -5,7 +5,7 @@ channels: - leej3 - https://fnlcr-dmap.github.io/scimap/ dependencies: - - python=3.10 + - python=3.9.13 - numpy=1.26.4 - pandas=1.5.3 - anndata=0.10.9 From d387d64e36355d195b9acf007a4df116509e9a4c Mon Sep 17 00:00:00 2001 From: root Date: Thu, 31 Jul 2025 23:25:26 -0700 Subject: [PATCH 13/49] Moving colorcet library under pip for better organization --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 24fb6d3..71fc642 100644 --- a/environment.yml +++ b/environment.yml @@ -18,9 +18,9 @@ dependencies: - squidpy=1.2.2 - scikit-image=0.19.3 - scipy=1.10.1 - - colorcet - pip - pip: + - colorcet - git+https://github.com/holoviz/datashader.git - ipykernel==6.29.5 - ipython==8.18.0 From 5a1be360002965f1dd39bbbd598a5d253755442b Mon Sep 17 00:00:00 2001 From: Rohit Kallakuri Date: Wed, 6 Aug 2025 17:00:25 -0700 Subject: [PATCH 14/49] Update Quickstart_MacArm64.md with improved setup instructions --- tutorials/Quickstart_MacArm64.md | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tutorials/Quickstart_MacArm64.md diff --git a/tutorials/Quickstart_MacArm64.md b/tutorials/Quickstart_MacArm64.md new file mode 100644 index 0000000..7c77b33 --- /dev/null +++ b/tutorials/Quickstart_MacArm64.md @@ -0,0 +1,86 @@ +## Pre-requisites + +- [x] `miniconda` for Macbook M1/M2/M3 (`arm64`) +- [x] `homebrew`, `git`, `XCode` +- [x] Github account and authentication tokens +- [x] VS Code + +## Steps + +1. Open a Terminal application and `git clone` this repository inside a folder, e.g., `~/summer2025/`: + + ```sh + mkdir -p ~/summer2025; + git clone [https://github.com/Summer2025-SPAC/SPAC_Shiny](https://github.com/Summer2025-SPAC/SPAC_Shiny); + cd ~/summer2025/SPAC_Shiny + ``` + +2. `git checkout` your development branch, e.g., `gh_iss2_rk`: + + ```sh + git checkout branch_name; + ``` + +3. Open the folder in VS Code: + + ```sh + code ./ + ``` + +4. Inside the VS Code `terminal`, create a `conda` environment using the `environment.yml` file. This file specifies all necessary packages and ensures compatibility. + + + Ensure you are in the SPAC_Shiny directory + + ```sh + cd ~/summer2025/SPAC_Shiny; + ``` + + Make edits these edits to the `environment.yml` file: + + Under dependencies, add these lines: + + ```sh + - tables>=3.8.0 + - c-blosc2 + - libtiff + ``` + + Under pip, remove this line: + + ```sh + - tables==3.8.0 + ``` + + Installing tables and c-blosc2 with conda guarantees a smooth setup on MacOS by using pre-built, compatible binaries, while avoiding build errors that occur with pip installations. + + By adding libtiff directly to your conda dependencies, you instruct conda to download this library and place it correctly within your shiny environment's path. This ensures that when Pillow is imported, its dependencies are available and properly linked, resolving the error. + + # Create the environment from environment.yml + # This will install Python 3.9 and all specified packages. + conda env create -f environment.yml + + # Activate the newly created environment using the environment name + + ```sh + conda activate shiny + ``` + +5. Assuming all installation works, run the shiny app in the terminal. + + ```sh + shiny run app.py + ``` + +6. If you encounter ImportErrors with libtiff, carry out these commands: + + ```sh + conda activate shiny + pip uninstall pillow + conda install -c conda-forge --force-reinstall pillow libtiff + ``` + Then run the shiny app again in the terminal: + + ```sh + shiny run app.py + ```git rm -r --cached __pycache__/ \ No newline at end of file From ec7b22c6b07ed72f76c29459fe850942e5536841 Mon Sep 17 00:00:00 2001 From: Rohit Kallakuri Date: Thu, 7 Aug 2025 13:07:01 -0700 Subject: [PATCH 15/49] Fix typo in Quickstart_MacArm64.md --- tutorials/Quickstart_MacArm64.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/Quickstart_MacArm64.md b/tutorials/Quickstart_MacArm64.md index 7c77b33..ba95dbf 100644 --- a/tutorials/Quickstart_MacArm64.md +++ b/tutorials/Quickstart_MacArm64.md @@ -83,4 +83,4 @@ ```sh shiny run app.py - ```git rm -r --cached __pycache__/ \ No newline at end of file + ``` \ No newline at end of file From 8834bb41ce3e01f230b796bded14838a6c8c4712 Mon Sep 17 00:00:00 2001 From: Rohit Kallakuri Date: Thu, 7 Aug 2025 14:40:01 -0700 Subject: [PATCH 16/49] feat: Add list of supported file types under the file upload button --- ui/data_input_ui.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/data_input_ui.py b/ui/data_input_ui.py index 44e112f..8c80aab 100644 --- a/ui/data_input_ui.py +++ b/ui/data_input_ui.py @@ -41,6 +41,11 @@ def data_input_ui(): "input_file", "Choose a file to upload:", multiple=False, width="100%" + ), + # ADDED: Helper text to show supported file types + ui.p( + "Supported types: .pickle, .h5ad", + style="font-size: 0.9em; font-style: italic; color: #6c757d;" ) ), ui.row( @@ -178,4 +183,4 @@ def data_input_ui(): ) ) ) - ) + ) \ No newline at end of file From 47aefca9bf8888e5a2b6e06a5518908bbc9a60be Mon Sep 17 00:00:00 2001 From: Rohit Kallakuri Date: Thu, 7 Aug 2025 14:44:16 -0700 Subject: [PATCH 17/49] Removed mac quickstart file --- tutorials/Quickstart_MacArm64.md | 86 -------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 tutorials/Quickstart_MacArm64.md diff --git a/tutorials/Quickstart_MacArm64.md b/tutorials/Quickstart_MacArm64.md deleted file mode 100644 index 7c77b33..0000000 --- a/tutorials/Quickstart_MacArm64.md +++ /dev/null @@ -1,86 +0,0 @@ -## Pre-requisites - -- [x] `miniconda` for Macbook M1/M2/M3 (`arm64`) -- [x] `homebrew`, `git`, `XCode` -- [x] Github account and authentication tokens -- [x] VS Code - -## Steps - -1. Open a Terminal application and `git clone` this repository inside a folder, e.g., `~/summer2025/`: - - ```sh - mkdir -p ~/summer2025; - git clone [https://github.com/Summer2025-SPAC/SPAC_Shiny](https://github.com/Summer2025-SPAC/SPAC_Shiny); - cd ~/summer2025/SPAC_Shiny - ``` - -2. `git checkout` your development branch, e.g., `gh_iss2_rk`: - - ```sh - git checkout branch_name; - ``` - -3. Open the folder in VS Code: - - ```sh - code ./ - ``` - -4. Inside the VS Code `terminal`, create a `conda` environment using the `environment.yml` file. This file specifies all necessary packages and ensures compatibility. - - - Ensure you are in the SPAC_Shiny directory - - ```sh - cd ~/summer2025/SPAC_Shiny; - ``` - - Make edits these edits to the `environment.yml` file: - - Under dependencies, add these lines: - - ```sh - - tables>=3.8.0 - - c-blosc2 - - libtiff - ``` - - Under pip, remove this line: - - ```sh - - tables==3.8.0 - ``` - - Installing tables and c-blosc2 with conda guarantees a smooth setup on MacOS by using pre-built, compatible binaries, while avoiding build errors that occur with pip installations. - - By adding libtiff directly to your conda dependencies, you instruct conda to download this library and place it correctly within your shiny environment's path. This ensures that when Pillow is imported, its dependencies are available and properly linked, resolving the error. - - # Create the environment from environment.yml - # This will install Python 3.9 and all specified packages. - conda env create -f environment.yml - - # Activate the newly created environment using the environment name - - ```sh - conda activate shiny - ``` - -5. Assuming all installation works, run the shiny app in the terminal. - - ```sh - shiny run app.py - ``` - -6. If you encounter ImportErrors with libtiff, carry out these commands: - - ```sh - conda activate shiny - pip uninstall pillow - conda install -c conda-forge --force-reinstall pillow libtiff - ``` - Then run the shiny app again in the terminal: - - ```sh - shiny run app.py - ```git rm -r --cached __pycache__/ \ No newline at end of file From b061f423ece7c324a75902a6ba1265afc92c988e Mon Sep 17 00:00:00 2001 From: Rohit Kallakuri Date: Sun, 31 Aug 2025 23:18:31 -0700 Subject: [PATCH 18/49] feat: Add font size sliders to plot tabs --- __pycache__/app.cpython-39.pyc | Bin 1824 -> 1840 bytes server/anno_vs_anno_server.py | 40 ++++++++++++++----- server/annotations_server.py | 62 +++++++++++++++++------------ server/boxplot_server.py | 60 ++++++++++++++++------------ server/nearest_neighbor_server.py | 61 ++++++++++++++++++---------- server/scatterplot_server.py | 20 +++++++--- server/spatial_server.py | 64 +++++++++++++++++------------- server/umap_server.py | 37 ++++++++++++----- ui/anno_vs_anno_ui.py | 46 ++++++++++++++------- ui/annotations_ui.py | 10 ++++- ui/boxplot_ui.py | 10 ++++- ui/nearest_neighbor_ui.py | 12 +++++- ui/scatterplot_ui.py | 11 ++++- ui/spatial_ui.py | 11 ++++- ui/umap_ui.py | 19 ++++++++- 15 files changed, 315 insertions(+), 148 deletions(-) diff --git a/__pycache__/app.cpython-39.pyc b/__pycache__/app.cpython-39.pyc index bde9e928e90bdf309a90e565f5f0c82b9a0de99f..fe11570716e19bac0b8387619f1a3df65a3ffc91 100644 GIT binary patch delta 70 zcmZ3$w}Fp4k(ZZ?0SJUQ7H;IOVAV0w4=qkDD%LN`&&VvvPRz+k%q}g;)DI}i&q_@$ YDb^1TaCD9j&dAKG)HU5apS6(@0Nhy?lK=n! delta 54 zcmdnMw}6j3k(ZZ?0SKNu)^6mkV3kqQFUrp^(JwB|%}p&bGB7gL4-Rm2jt|br%&XkI Ip0$w?0GBurMgRZ+ diff --git a/server/anno_vs_anno_server.py b/server/anno_vs_anno_server.py index 90a96a1..12025d8 100644 --- a/server/anno_vs_anno_server.py +++ b/server/anno_vs_anno_server.py @@ -11,17 +11,26 @@ def anno_vs_anno_server(input, output, session, shared): @reactive.event(input.go_sk1, ignore_none=True) def spac_Sankey(): adata = ad.AnnData( - X=shared['X_data'].get(), - obs=pd.DataFrame(shared['obs_data'].get()), - layers=shared['layers_data'].get(), + X=shared['X_data'].get(), + obs=pd.DataFrame(shared['obs_data'].get()), + layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) if adata is not None: fig = spac.visualization.sankey_plot( - adata, - source_annotation=input.sk1_anno1(), + adata, + source_annotation=input.sk1_anno1(), target_annotation=input.sk1_anno2() ) + + font_size = input.sankey_font_size() + + # Modified... + # Applying font size directly to the Sankey trace for node and + # label text, as the global layout font can sometimes be ignored. + fig.update_layout(font=dict(size=font_size)) + fig.update_traces(textfont=dict(size=font_size), selector=dict(type='sankey')) + return fig return None @@ -30,16 +39,29 @@ def spac_Sankey(): @reactive.event(input.go_rhm1, ignore_none=True) def spac_Relational(): adata = ad.AnnData( - X=shared['X_data'].get(), + X=shared['X_data'].get(), obs=pd.DataFrame(shared['obs_data'].get()) ) if adata is not None: result = spac.visualization.relational_heatmap( - adata, - source_annotation=input.rhm_anno1(), + adata, + source_annotation=input.rhm_anno1(), target_annotation=input.rhm_anno2() ) shared['df_relational'].set(result['data']) + + font_size = input.heatmap_font_size() + + # Modified... + # Applying font size directly to the heatmap axes and color bar, + # as these elements often have their own font settings. + result['figure'].update_layout( + font=dict(size=font_size), + xaxis=dict(tickfont=dict(size=font_size)), + yaxis=dict(tickfont=dict(size=font_size)) + ) + result['figure'].update_coloraxes(colorbar_tickfont_size=font_size) + return result['figure'] return None @@ -57,4 +79,4 @@ def download_df_1(): def download_button_ui_1(): if shared['df_relational'].get() is not None: return ui.download_button("download_df_1", "Download Data", class_="btn-warning") - return None + return None \ No newline at end of file diff --git a/server/annotations_server.py b/server/annotations_server.py index 04242a0..8296da1 100644 --- a/server/annotations_server.py +++ b/server/annotations_server.py @@ -1,6 +1,8 @@ from shiny import ui, render, reactive import numpy as np import spac.visualization +# Added... +import matplotlib.pyplot as plt def annotations_server(input, output, session, shared): @output @@ -11,30 +13,38 @@ def spac_Histogram_2(): if adata is None: return None + # Added... + # Note: This assumes your UI file has a slider with the id 'annotations_font_size'. + # Please ensure this ID matches the one in your annotations_ui.py file. + font_size = input.annotations_font_size() + plt.rcParams.update({'font.size': font_size}) + rotation = input.anno_slider() + # 1) If "Group By" is UNCHECKED, show a simple annotation histogram if not input.h2_group_by_check(): fig, ax, df = spac.visualization.histogram( adata, annotation=input.h2_anno() ).values() - shared['df_histogram2'].set(df) - ax.tick_params(axis='x', rotation=input.anno_slider(), labelsize=10) + shared['df_histogram2'].set(df) + # Modified... + ax.tick_params(axis='x', rotation=rotation, labelsize=font_size) return fig - # 2) If "Group By" is CHECKED, we must always supply a + # 2) If "Group By" is CHECKED, we must always supply a # valid multiple parameter else: - # If user also checked "Plot Together", use their selected + # If user also checked "Plot Together", use their selected # stack type if input.h2_together_check(): # e.g. 'stack', 'dodge', etc. - multiple_param = input.h2_together_drop() + multiple_param = input.h2_together_drop() together_flag = True else: # If grouping by but not "plot together", pick a default layout # or 'dodge' or any valid string - multiple_param = "layer" + multiple_param = "layer" together_flag = False fig, ax, df = spac.visualization.histogram( @@ -44,13 +54,15 @@ def spac_Histogram_2(): together=together_flag, multiple=multiple_param ).values() - shared['df_histogram2'].set(df) + shared['df_histogram2'].set(df) axes = ax if isinstance(ax, (list, np.ndarray)) else [ax] - for ax in axes: - ax.tick_params( - axis='x', - rotation=input.anno_slider(), - labelsize=10 + # Modified... (renamed loop variable to avoid shadowing) + for current_ax in axes: + # Modified... + current_ax.tick_params( + axis='x', + rotation=rotation, + labelsize=font_size ) return fig return None @@ -61,8 +73,8 @@ def spac_Histogram_2(): def download_histogram_button_ui(): if shared['df_histogram2'].get() is not None: return ui.download_button( - "download_histogram2_df", - "Download Data", + "download_histogram2_df", + "Download Data", class_="btn-warning" ) return None @@ -70,7 +82,7 @@ def download_histogram_button_ui(): @render.download(filename="annotation_histogram_data.csv") def download_histogram2_df(): - df = shared['df_histogram2'].get() + df = shared['df_human_histogram2'].get() if df is not None: csv_string = df.to_csv(index=False) csv_bytes = csv_string.encode("utf-8") @@ -86,8 +98,8 @@ def histogram_reactivity_2(): if btn and not ui_initialized: dropdown = ui.input_select( - "h2_anno_1", - "Select an Annotation", + "h2_anno_1", + "Select an Annotation", choices=shared['obs_names'].get() ) ui.insert_ui( @@ -97,8 +109,8 @@ def histogram_reactivity_2(): ) together_check = ui.input_checkbox( - "h2_together_check", - "Plot Together", + "h2_together_check", + "Plot Together", value=True ) ui.insert_ui( @@ -120,18 +132,18 @@ def histogram_reactivity_2(): def update_stack_type_dropdown(): if input.h2_together_check(): dropdown_together = ui.input_select( - "h2_together_drop", - "Select Stack Type", - choices=['stack', 'layer', 'dodge', 'fill'], + "h2_together_drop", + "Select Stack Type", + choices=['stack', 'layer', 'dodge', 'fill'], selected='stack' ) ui.insert_ui( ui.div({ - "id": "inserted-dropdown_together-1"}, + "id": "inserted-dropdown_together-1"}, dropdown_together ), selector="#main-h2_together_drop", where="beforeEnd" - ) + ) else: - ui.remove_ui("#inserted-dropdown_together-1") + ui.remove_ui("#inserted-dropdown_together-1") \ No newline at end of file diff --git a/server/boxplot_server.py b/server/boxplot_server.py index c381492..96d324e 100644 --- a/server/boxplot_server.py +++ b/server/boxplot_server.py @@ -3,6 +3,8 @@ import anndata as ad import pandas as pd import spac.visualization +# Added... +import matplotlib.pyplot as plt def boxplot_server(input, output, session, shared): @@ -32,34 +34,38 @@ def spac_Boxplot(): if not input.bp_output_type(): return None - else: + else: adata = ad.AnnData( - X=shared['X_data'].get(), - obs=pd.DataFrame(shared['obs_data'].get()), - var=pd.DataFrame(shared['var_data'].get()), - layers=shared['layers_data'].get(), + X=shared['X_data'].get(), + obs=pd.DataFrame(shared['obs_data'].get()), + var=pd.DataFrame(shared['var_data'].get()), + layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) + # Added... + font_size = input.bp_font_size() # Proceed only if adata is valid if adata is not None and adata.var is not None: fig, df = spac.visualization.boxplot_interactive( - adata, - annotation=on_anno_check(), - layer=on_layer_check(), + adata, + annotation=on_anno_check(), + layer=on_layer_check(), features=list(input.bp_features()), showfliers=on_outlier_check(), log_scale=input.bp_log_scale(), orient=on_orient_check(), - figure_height=3, - figure_width=4.8, + figure_height=3, + figure_width=4.8, figure_type="interactive" ).values() # Return the interactive Plotly figure object shared['df_boxplot'].set(df) + # Added... + fig.update_layout(font=dict(size=font_size)) print(type(fig)) return fig @@ -81,8 +87,8 @@ def download_boxplot(): def download_button_ui1(): if shared['df_boxplot'].get() is not None: return ui.download_button( - "download_boxplot", - "Download Data", + "download_boxplot", + "Download Data", class_="btn-warning" ) return None @@ -101,33 +107,35 @@ def boxplot_static(): if input.bp_output_type(): return None - else: + else: adata = ad.AnnData( - X=shared['X_data'].get(), - obs=pd.DataFrame(shared['obs_data'].get()), - var=pd.DataFrame(shared['var_data'].get()), - layers=shared['layers_data'].get(), + X=shared['X_data'].get(), + obs=pd.DataFrame(shared['obs_data'].get()), + var=pd.DataFrame(shared['var_data'].get()), + layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) + # Added... + font_size = input.bp_font_size() # Proceed only if adata is valid if adata is not None and adata.var is not None: - + fig, df = spac.visualization.boxplot_interactive( - adata, - annotation=on_anno_check(), - layer=on_layer_check(), + adata, + annotation=on_anno_check(), + layer=on_layer_check(), features=list(input.bp_features()), showfliers=on_outlier_check(), log_scale=input.bp_log_scale(), orient=on_orient_check(), - figure_height=3, - figure_width=4.8, + figure_height=3, + figure_width=4.8, figure_type="static" ).values() - + # Added... + fig.update_layout(font=dict(size=font_size)) return fig - return None - + return None \ No newline at end of file diff --git a/server/nearest_neighbor_server.py b/server/nearest_neighbor_server.py index 58fa26b..a11a46d 100644 --- a/server/nearest_neighbor_server.py +++ b/server/nearest_neighbor_server.py @@ -1,7 +1,9 @@ from shiny import ui, render, reactive import spac.spatial_analysis import spac.visualization - +import matplotlib.pyplot as plt +# Added: Import seaborn to control styling directly +import seaborn as sns def nearest_neighbor_server(input, output, session, shared): @output @@ -11,38 +13,55 @@ def spac_nearest_neighbor(): adata = shared['adata_main'].get() annotation = input.nn_anno() label = str(input.nn_anno_label()) + if input.nn_plot_style() == 'numeric': plot_type = input.nn_plot_type_n() else: plot_type = input.nn_plot_type_d() + if input.nn_stratify(): stratify_by = input.nn_strat_select() else: stratify_by = None + if annotation in adata.obs.columns: adata.obs[annotation] = adata.obs[annotation].astype(str) - spac.spatial_analysis.calculate_nearest_neighbor( - adata, - annotation, - spatial_associated_table=input.nn_spatial(), - imageid=stratify_by, - label='spatial_distance', - verbose=True - ) - if adata is not None: - out = spac.visualization.visualize_nearest_neighbor( - adata=adata, - annotation=annotation, - distance_from=label, - method=input.nn_plot_style(), - log=input.nn_log(), - facet_plot=True, - plot_type=plot_type, - stratify_by=stratify_by + font_size = input.nn_font_size() + + # Modified: Use seaborn's context manager to apply font size + # This is more effective for plots built with seaborn. + with sns.plotting_context(rc={"font.size": font_size, + "axes.labelsize": font_size, + "xtick.labelsize": font_size, + "ytick.labelsize": font_size, + "legend.fontsize": font_size, + "axes.titlesize": font_size * 1.2}): + + spac.spatial_analysis.calculate_nearest_neighbor( + adata, + annotation, + spatial_associated_table=input.nn_spatial(), + imageid=stratify_by, + label='spatial_distance', + verbose=True ) - shared['df_nn'].set(out['data']) - return out['fig'] + + if adata is not None: + out = spac.visualization.visualize_nearest_neighbor( + adata=adata, + annotation=annotation, + distance_from=label, + method=input.nn_plot_style(), + log=input.nn_log(), + facet_plot=True, + plot_type=plot_type, + stratify_by=stratify_by + ) + shared['df_nn'].set(out['data']) + # The figure is created within the 'with' block, + # so it will have the correct font size. + return out['fig'] @render.download(filename="nearest_neighbor_data.csv") def download_df_nn(): diff --git a/server/scatterplot_server.py b/server/scatterplot_server.py index cf2d9c7..0537392 100644 --- a/server/scatterplot_server.py +++ b/server/scatterplot_server.py @@ -2,6 +2,8 @@ import anndata as ad import pandas as pd import spac.visualization +# Added... +import matplotlib.pyplot as plt def scatterplot_server(input, output, session, shared): @@ -83,8 +85,8 @@ def scatter_reactivity(): if btn and not scatter_ui_initialized.get(): # Insert the color selection dropdown if not already initialized dropdown = ui.input_select( - "scatter_color", - "Select Feature", + "scatter_color", + "Select Feature", choices=shared['var_names'].get() ) ui.insert_ui( @@ -104,14 +106,14 @@ def get_color_values(): if selected_feature is None: return None adata = ad.AnnData( - X=shared['X_data'].get(), + X=shared['X_data'].get(), var=pd.DataFrame(shared['var_data'].get()) ) if selected_feature in adata.var_names: column_index = adata.var_names.get_loc(selected_feature) color_values = adata.X[:, column_index] return color_values - return None + return None @output @render.plot @@ -123,6 +125,11 @@ def spac_Scatter(): x_label = input.scatter_x() y_label = input.scatter_y() title = f"Scatterplot: {x_label} vs {y_label}" + # Added... + font_size = input.scatter_font_size() + + # Added... + plt.rcParams.update({'font.size': font_size}) if color_enabled: fig, ax = spac.visualization.visualize_2D_scatter( @@ -134,8 +141,9 @@ def spac_Scatter(): else: fig, ax = spac.visualization.visualize_2D_scatter(x, y) - ax.set_title(title, fontsize=14) + # Modified... + ax.set_title(title, fontsize=font_size + 2) ax.set_xlabel(x_label) ax.set_ylabel(y_label) - return ax + return ax \ No newline at end of file diff --git a/server/spatial_server.py b/server/spatial_server.py index 7ab1731..c33315a 100644 --- a/server/spatial_server.py +++ b/server/spatial_server.py @@ -3,6 +3,8 @@ import anndata as ad import pandas as pd import spac.visualization +# Added... +import matplotlib.pyplot as plt def spatial_server(input, output, session, shared): @@ -15,8 +17,8 @@ def slide_reactivity(): if btn and not ui_initialized: dropdown_slide = ui.input_select( - "slide_select_drop", - "Select the Slide Annotation", + "slide_select_drop", + "Select the Slide Annotation", choices=shared['obs_names'].get()) ui.insert_ui( ui.div({"id": "inserted-slide_dropdown"}, dropdown_slide), @@ -25,8 +27,8 @@ def slide_reactivity(): ) dropdown_label = ui.input_select( - "slide_select_label", - "Select a Slide", + "slide_select_label", + "Select a Slide", choices=[] ) ui.insert_ui( @@ -58,8 +60,8 @@ def region_reactivity(): if btn and not ui_initialized: dropdown_region = ui.input_select( - "region_select_drop", - "Select the Region Annotation", + "region_select_drop", + "Select the Region Annotation", choices=shared['obs_names'].get()) ui.insert_ui( ui.div({"id": "inserted-region_dropdown"}, dropdown_region), @@ -68,13 +70,13 @@ def region_reactivity(): ) dropdown_label = ui.input_select( - "region_label_select", + "region_label_select", "Select a Region", choices=[] ) ui.insert_ui( ui.div( - {"id": "inserted-region_label_select_dropdown"}, + {"id": "inserted-region_label_select_dropdown"}, dropdown_label ), selector="#main-region_label_select_dropdown", @@ -100,15 +102,21 @@ def update_region_select_drop(): @reactive.event(input.go_sp1, ignore_none=True) def spac_Spatial(): adata = ad.AnnData( - X=shared['X_data'].get(), - var=pd.DataFrame(shared['var_data'].get()), - obsm=shared['obsm_data'].get(), - obs=shared['obs_data'].get(), - dtype=shared['X_data'].get().dtype, + X=shared['X_data'].get(), + var=pd.DataFrame(shared['var_data'].get()), + obsm=shared['obsm_data'].get(), + obs=shared['obs_data'].get(), + dtype=shared['X_data'].get().dtype, layers=shared['layers_data'].get() ) slide_check = input.slide_select_check() region_check = input.region_select_check() + # Added... + font_size = input.spatial_font_size() + + # Added... + plt.rcParams.update({'font.size': font_size}) + if adata is not None: if slide_check is False and region_check is False: adata_subset = adata @@ -139,7 +147,7 @@ def spac_Spatial(): if "spatial_feat" not in input or input.spatial_feat() is None: return None layer = ( - None if input.spatial_layer() == "Original" + None if input.spatial_layer() == "Original" else input.spatial_layer() ) out = spac.visualization.interactive_spatial_plot( @@ -163,25 +171,27 @@ def spac_Spatial(): else: return None out[0]['image_object'].update_xaxes( - showticklabels=True, - ticks="outside", - tickwidth=2, + showticklabels=True, + ticks="outside", + tickwidth=2, ticklen=10 ) out[0]['image_object'].update_yaxes( - showticklabels=True, - ticks="outside", - tickwidth=2, + showticklabels=True, + ticks="outside", + tickwidth=2, ticklen=10 ) + # Added... + out[0]['image_object'].update_layout(font=dict(size=font_size)) return out[0]['image_object'] return None - #Track UI State + #Track UI State spatial_annotation_initialized = reactive.Value(False) spatial_feature_initialized = reactive.Value(False) - + @reactive.effect def spatial_reactivity(): flipper = shared['data_loaded'].get() @@ -211,8 +221,8 @@ def spatial_reactivity(): elif btn == "Feature": if not spatial_feature_initialized.get(): dropdown = ui.input_select( - "spatial_feat", - "Select a Feature", + "spatial_feat", + "Select a Feature", choices=shared['var_names'].get() ) ui.insert_ui( @@ -223,8 +233,8 @@ def spatial_reactivity(): where="beforeEnd" ) table_select = ui.input_select( - "spatial_layer", - "Select a Table", + "spatial_layer", + "Select a Table", choices=shared['layers_names'].get() + ["Original"], selected="Original" ) @@ -237,4 +247,4 @@ def spatial_reactivity(): if spatial_annotation_initialized.get(): ui.remove_ui("#inserted-spatial_dropdown_anno") - spatial_annotation_initialized.set(False) + spatial_annotation_initialized.set(False) \ No newline at end of file diff --git a/server/umap_server.py b/server/umap_server.py index f0b1ff4..ab57461 100644 --- a/server/umap_server.py +++ b/server/umap_server.py @@ -2,7 +2,8 @@ import anndata as ad import pandas as pd import spac.visualization - +# Added... +import matplotlib.pyplot as plt def umap_server(input, output, session, shared): @output @@ -23,21 +24,28 @@ def spac_UMAP(): method = input.plottype() point_size = input.umap_slider_1() + # Modified: This line correctly reads the font size value + font_size = input.umap_font_size_1() mode = input.umap_rb() + # Added: This sets the font size for the entire plot + plt.rcParams.update({'font.size': font_size}) + if mode == "Feature": feature = input.umap_rb_feat() layer = None if input.umap_layer() == "Original" else input.umap_layer() fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, feature=feature, layer=layer, point_size=point_size ) - ax.set_title(f"{method.upper()}: {feature}", fontsize=14) + # Modified: Let matplotlib handle relative font sizes for consistency + ax.set_title(f"{method.upper()}: {feature}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") for extra_ax in fig.axes: if hasattr(extra_ax, "get_ylabel") and extra_ax != ax: - extra_ax.set_ylabel(f"Colored by: {feature.upper()}", fontsize=12) + # Modified: Let matplotlib handle relative font sizes + extra_ax.set_ylabel(f"Colored by: {feature.upper()}") return fig @@ -46,7 +54,8 @@ def spac_UMAP(): fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, annotation=annotation, point_size=point_size ) - ax.set_title(f"{method.upper()}: {annotation}", fontsize=14) + # Modified: Let matplotlib handle relative font sizes + ax.set_title(f"{method.upper()}: {annotation}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") @@ -149,20 +158,29 @@ def spac_UMAP2(): method = input.plottype2() point_size = input.umap_slider_2() mode = input.umap_rb2() - + + # Added: This was the line causing the error. It reads the font + # size from the second slider. + font_size = input.umap_font_size_2() + + # Added: This sets the font size for the entire plot + plt.rcParams.update({'font.size': font_size}) + if mode == "Feature": feature = input.umap_rb_feat2() layer = None if input.umap_layer2() == "Original" else input.umap_layer2() fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, feature=feature, layer=layer, point_size=point_size ) - ax.set_title(f"{method.upper()}: {feature}", fontsize=14) + # Modified: Let matplotlib handle relative font sizes + ax.set_title(f"{method.upper()}: {feature}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") for extra_ax in fig.axes: if hasattr(extra_ax, "get_ylabel") and extra_ax != ax: - extra_ax.set_ylabel(f"Colored by: {feature}", fontsize=12) + # Modified: Let matplotlib handle relative font sizes + extra_ax.set_ylabel(f"Colored by: {feature.upper()}") return fig @@ -171,7 +189,8 @@ def spac_UMAP2(): fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, annotation=annotation, point_size=point_size ) - ax.set_title(f"{method.upper()}: {annotation}", fontsize=14) + # Modified: Let matplotlib handle relative font sizes + ax.set_title(f"{method.upper()}: {annotation}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") @@ -241,4 +260,4 @@ def umap_reactivity2(): if umap2_feature_initialized.get(): ui.remove_ui("#inserted-rbdropdown_feat2") ui.remove_ui("#inserted-umap_table2") - umap2_feature_initialized.set(False) + umap2_feature_initialized.set(False) \ No newline at end of file diff --git a/ui/anno_vs_anno_ui.py b/ui/anno_vs_anno_ui.py index c7f4286..9258fc5 100644 --- a/ui/anno_vs_anno_ui.py +++ b/ui/anno_vs_anno_ui.py @@ -11,18 +11,26 @@ def anno_vs_anno_ui(): ui.column( 2, ui.input_select( - "sk1_anno1", - "Select Source Annotation", + "sk1_anno1", + "Select Source Annotation", choices=[] ), ui.input_select( - "sk1_anno2", - "Select Target Annotation", + "sk1_anno2", + "Select Target Annotation", choices=[] ), + # Added... + ui.input_slider( + "sankey_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( - "go_sk1", - "Render Plot", + "go_sk1", + "Render Plot", class_="btn-success" ) ), @@ -43,20 +51,28 @@ def anno_vs_anno_ui(): ui.column( 2, ui.input_select( - "rhm_anno1", - "Select Source Annotation", - choices=[], + "rhm_anno1", + "Select Source Annotation", + choices=[], selected=[] ), ui.input_select( - "rhm_anno2", - "Select Target Annotation", - choices=[], + "rhm_anno2", + "Select Target Annotation", + choices=[], selected=[] ), + # Added... + ui.input_slider( + "heatmap_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( - "go_rhm1", - "Render Plot", + "go_rhm1", + "Render Plot", class_="btn-success" ), ui.div( @@ -74,4 +90,4 @@ def anno_vs_anno_ui(): ) ) ) - ) + ) \ No newline at end of file diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index c01a39b..794fd3d 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -40,6 +40,14 @@ def annotations_ui(): {"style": "padding-top: 20px;"}, ui.output_ui("download_histogram_button_ui") ), + # ADDED: Font size slider for the plot + ui.input_slider( + "annotations_font_size", + "Axis Label Font Size", + min=3, + max=24, + value=10 + ) ), ui.column( 10, @@ -55,4 +63,4 @@ def annotations_ui(): ) ) ) - ) + ) \ No newline at end of file diff --git a/ui/boxplot_ui.py b/ui/boxplot_ui.py index 82fdee3..8096de4 100644 --- a/ui/boxplot_ui.py +++ b/ui/boxplot_ui.py @@ -54,6 +54,14 @@ def boxplot_ui(): "Enable Interactive Plot", True ), + # Added... + ui.input_slider( + "bp_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_bp", "Render Plot", @@ -92,4 +100,4 @@ def boxplot_ui(): ), ) ), - ) + ) \ No newline at end of file diff --git a/ui/nearest_neighbor_ui.py b/ui/nearest_neighbor_ui.py index c6b5e25..6d0751e 100644 --- a/ui/nearest_neighbor_ui.py +++ b/ui/nearest_neighbor_ui.py @@ -71,10 +71,18 @@ def nearest_neighbor_ui(): # "Apply Facet Plots", # value=False # ), + # Added... + ui.input_slider( + "nn_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_nn", "Render Plot", - class_="btn-success" + class_="btn_success" ), ui.div( {"style": "padding-top: 20px;"}, @@ -92,4 +100,4 @@ def nearest_neighbor_ui(): ), ), ) - ) + ) \ No newline at end of file diff --git a/ui/scatterplot_ui.py b/ui/scatterplot_ui.py index a552a63..a62f153 100644 --- a/ui/scatterplot_ui.py +++ b/ui/scatterplot_ui.py @@ -33,6 +33,14 @@ def scatterplot_ui(): value=False ), ui.div(id="main-scatter_dropdown"), + # Added... + ui.input_slider( + "scatter_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_scatter", "Render Plot", @@ -50,5 +58,4 @@ def scatterplot_ui(): ) ) ) - ) - + ) \ No newline at end of file diff --git a/ui/spatial_ui.py b/ui/spatial_ui.py index 5d40093..eadcf96 100644 --- a/ui/spatial_ui.py +++ b/ui/spatial_ui.py @@ -30,6 +30,14 @@ def spatial_ui(): max=10, value=3 ), + # Added... + ui.input_slider( + "spatial_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_checkbox( "slide_select_check", "Stratify by Slide", @@ -61,5 +69,4 @@ def spatial_ui(): ) ) ) - ) - + ) \ No newline at end of file diff --git a/ui/umap_ui.py b/ui/umap_ui.py index a14b220..3649b4f 100644 --- a/ui/umap_ui.py +++ b/ui/umap_ui.py @@ -29,6 +29,14 @@ def umap_ui(): max=10, value=3 ), + # Added... + ui.input_slider( + "umap_font_size_1", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_umap1", "Render Plot", @@ -62,6 +70,14 @@ def umap_ui(): max=10, value=3 ), + # Added missing font size slider for the second plot + ui.input_slider( + "umap_font_size_2", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_umap2", "Render Plot", @@ -76,5 +92,4 @@ def umap_ui(): ) ) ) - ) - + ) \ No newline at end of file From bc036452eb26a63e85cb8cf04bfa14c7db9cd9f6 Mon Sep 17 00:00:00 2001 From: Boqiang Date: Thu, 2 Oct 2025 17:17:20 -0400 Subject: [PATCH 19/49] add boqiang as a contributer --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e80a4ce..0b01ebc 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ This project was a success thanks to the invaluable collaboration and support fr * Suriya Selvarajan * Qianyue Wang * Andree Kolliegbo + * Boqiang Zhang * **Teaching Assistants (TAs) from Purdue's Data Mine:** * Alex Liu * Omar Eldaghar From 88e2891efc0195b3389714d901721ac7d42171bf Mon Sep 17 00:00:00 2001 From: risingmin Date: Thu, 2 Oct 2025 17:17:45 -0400 Subject: [PATCH 20/49] sungmin --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e80a4ce..cd3bd9e 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ This project was a success thanks to the invaluable collaboration and support fr * Suriya Selvarajan * Qianyue Wang * Andree Kolliegbo + * Sungmin Lee * **Teaching Assistants (TAs) from Purdue's Data Mine:** * Alex Liu * Omar Eldaghar From 93acb1ff8c99f8c7e71d0ec0eb0ce940e3c9a539 Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Thu, 2 Oct 2025 17:25:51 -0400 Subject: [PATCH 21/49] Testing --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cd3bd9e..999e7c8 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ This project was a success thanks to the invaluable collaboration and support fr * Qianyue Wang * Andree Kolliegbo * Sungmin Lee + * Saran Nagubandi * **Teaching Assistants (TAs) from Purdue's Data Mine:** * Alex Liu * Omar Eldaghar From 28b5c4175bafc2c996bac6f37854348cc4332262 Mon Sep 17 00:00:00 2001 From: nlee3105 Date: Thu, 2 Oct 2025 17:29:44 -0400 Subject: [PATCH 22/49] Noah Lee --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 999e7c8..a56a938 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ This project was a success thanks to the invaluable collaboration and support fr * Andree Kolliegbo * Sungmin Lee * Saran Nagubandi + * Noah Lee * **Teaching Assistants (TAs) from Purdue's Data Mine:** * Alex Liu * Omar Eldaghar From 83ac232b1a236782df16faca43bc3377c95ae45d Mon Sep 17 00:00:00 2001 From: heaven-05 Date: Tue, 7 Oct 2025 15:39:53 -0400 Subject: [PATCH 23/49] add Heaven --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e523c31..d963967 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ This project was a success thanks to the invaluable collaboration and support fr * Saran Nagubandi * Noah Lee * Boqiang Zhang + * Heaven Golladay-Watkins * **Teaching Assistants (TAs) from Purdue's Data Mine:** * Alex Liu * Omar Eldaghar From ef8377f7fd0a8ad36062e1e3203329fac8e1a0fe Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Tue, 7 Oct 2025 23:13:39 -0400 Subject: [PATCH 24/49] add gitignore, rm __pycache__ --- .gitignore | 1 + __pycache__/app.cpython-39.pyc | Bin 40030 -> 0 bytes 2 files changed, 1 insertion(+) create mode 100644 .gitignore delete mode 100644 __pycache__/app.cpython-39.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/__pycache__/app.cpython-39.pyc b/__pycache__/app.cpython-39.pyc deleted file mode 100644 index 48b9ace1b5955f893f435bb2d1efe40a448765fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40030 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 From f989aa40d586f37a78d637b82ffd478065c480af Mon Sep 17 00:00:00 2001 From: Boqiang Date: Wed, 8 Oct 2025 10:53:13 -0400 Subject: [PATCH 25/49] update gitignore to include .ipynb_checkpoints/ --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ba0430d..cc715cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__/ \ No newline at end of file +__pycache__/ +.ipynb_checkpoints/ \ No newline at end of file From 57f3da7f70fbb56b75400d60ea398bed07f3d247 Mon Sep 17 00:00:00 2001 From: Arjun Chhabra Date: Thu, 16 Oct 2025 01:17:33 -0400 Subject: [PATCH 26/49] update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d963967..f5d3a1a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ This project was a success thanks to the invaluable collaboration and support fr * Noah Lee * Boqiang Zhang * Heaven Golladay-Watkins + * Arjun Chhabra * **Teaching Assistants (TAs) from Purdue's Data Mine:** * Alex Liu * Omar Eldaghar From 885e06aa5dec6fb997834f0052a3feb607d54575 Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Thu, 23 Oct 2025 15:23:40 -0400 Subject: [PATCH 27/49] fix(docs): replace the spac channel to solve import error --- environment.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 64daffc..fb6d3ef 100644 --- a/environment.yml +++ b/environment.yml @@ -61,4 +61,6 @@ dependencies: - zipp==3.19.2 - tifffile - sag-py-execution-time-decorator @ git+https://github.com/SamhammerAG/sag_py_execution_time_decorator.git@976c9683d561aadc0166495117c42cc1762633d7 - - spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@76f381316acd57ff9fbe3e5bea4e37b0a9ae3dd9#egg=spac + # - spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@76f381316acd57ff9fbe3e5bea4e37b0a9ae3dd9#egg=spac + # SPAC package pinned to specific commit for template compatibility (dev branch as of 2025-10-15) + - spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@49a6479d3083070064a6218f34125b5e86ad8e46#egg=spac From 5d75f2d649c4db802ff72e50aec2c250fc1a4116 Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Thu, 23 Oct 2025 15:27:27 -0400 Subject: [PATCH 28/49] fix(docker): change apt-get source from http:// to https:// to make it more resilient --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5a9787d..46290ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,9 @@ FROM python:3.9.13-slim WORKDIR /app # Install system dependencies needed for scientific packages -RUN apt-get update && apt-get install -y \ +RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list \ + && apt-get update && apt-get install -y \ +# RUN apt-get update && apt-get install -y \ gcc \ g++ \ git \ From d5751a0441dcc519d141c1bea82705e4b807b5cd Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Thu, 23 Oct 2025 16:29:43 -0400 Subject: [PATCH 29/49] PR 59: Merge George to Purdue --- .gitignore | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/.gitignore b/.gitignore index e69de29..690ce21 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,70 @@ +__pycache__/ +.ipynb_checkpoints/ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Deployment configurations +rsconnect-python/ +*.json + +# Logs +*.log + +# Temporary files +*.tmp +*.temp + +# Submodules/external repositories +SCSAWorkflow/ + +# Security documentation (internal use only) +SECURITY_DOCUMENTATION.md +SECURITY_IMPLEMENTATION.md +SECURITY_FIXES.md From ff6495160b3d5850843438c53768ab23ebd2580a Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Thu, 23 Oct 2025 19:13:53 -0400 Subject: [PATCH 30/49] Font Slider Testing --- server/annotations_server.py | 25 ++++++++++++++++--------- ui/annotations_ui.py | 10 +++++++++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/server/annotations_server.py b/server/annotations_server.py index 04242a0..7e19d56 100644 --- a/server/annotations_server.py +++ b/server/annotations_server.py @@ -1,7 +1,7 @@ from shiny import ui, render, reactive import numpy as np import spac.visualization - +import matplotlib.pyplot as plt def annotations_server(input, output, session, shared): @output @render.plot @@ -10,6 +10,12 @@ def spac_Histogram_2(): adata = shared['adata_main'].get() if adata is None: return None + # Added... + # Note: This assumes your UI file has a slider with the id 'annotations_font_size'. + # Please ensure this ID matches the one in your annotations_ui.py file. + font_size = input.annotations_font_size() + plt.rcParams.update({'font.size': font_size}) + rotation = input.anno_slider() # 1) If "Group By" is UNCHECKED, show a simple annotation histogram if not input.h2_group_by_check(): @@ -18,7 +24,7 @@ def spac_Histogram_2(): annotation=input.h2_anno() ).values() shared['df_histogram2'].set(df) - ax.tick_params(axis='x', rotation=input.anno_slider(), labelsize=10) + ax.tick_params(axis='x', rotation=rotation, labelsize=font_size) return fig # 2) If "Group By" is CHECKED, we must always supply a @@ -46,12 +52,13 @@ def spac_Histogram_2(): ).values() shared['df_histogram2'].set(df) axes = ax if isinstance(ax, (list, np.ndarray)) else [ax] - for ax in axes: - ax.tick_params( - axis='x', - rotation=input.anno_slider(), - labelsize=10 - ) + # Modified... (renamed loop variable to avoid shadowing) + for current_ax in axes: + # Modified... + current_ax.tick_params( + axis='x', + rotation=rotation, + labelsize=font_size return fig return None @@ -70,7 +77,7 @@ def download_histogram_button_ui(): @render.download(filename="annotation_histogram_data.csv") def download_histogram2_df(): - df = shared['df_histogram2'].get() + df = shared['df_human_histogram2'].get() if df is not None: csv_string = df.to_csv(index=False) csv_bytes = csv_string.encode("utf-8") diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index 4767809..de263b3 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -28,7 +28,7 @@ def annotations_ui(): ui.div(id="main-h2_together_drop"), accessible_slider( "anno_slider", - "Rotate X-axis Labels (degrees)", + "Rotate X-axis Labels (degrees up to 90)", min_val=0, max_val=90, value=0, @@ -43,6 +43,14 @@ def annotations_ui(): {"style": "padding-top: 20px;"}, ui.output_ui("download_histogram_button_ui") ), + # ADDED: Font size slider for the plot + ui.input_slider( + "annotations_font_size", + "Axis Label Font Size", + min=3, + max=24, + value=10 + ) ), ui.column( 10, From da909d4fe6bd8b621d9143cfcfe7b8f0f4e47073 Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Thu, 23 Oct 2025 19:30:16 -0400 Subject: [PATCH 31/49] Undo Changes --- server/annotations_server.py | 25 ++++++++----------------- ui/annotations_ui.py | 10 +--------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/server/annotations_server.py b/server/annotations_server.py index 7e19d56..f16b4e4 100644 --- a/server/annotations_server.py +++ b/server/annotations_server.py @@ -1,7 +1,6 @@ from shiny import ui, render, reactive import numpy as np import spac.visualization -import matplotlib.pyplot as plt def annotations_server(input, output, session, shared): @output @render.plot @@ -10,13 +9,6 @@ def spac_Histogram_2(): adata = shared['adata_main'].get() if adata is None: return None - # Added... - # Note: This assumes your UI file has a slider with the id 'annotations_font_size'. - # Please ensure this ID matches the one in your annotations_ui.py file. - font_size = input.annotations_font_size() - plt.rcParams.update({'font.size': font_size}) - rotation = input.anno_slider() - # 1) If "Group By" is UNCHECKED, show a simple annotation histogram if not input.h2_group_by_check(): fig, ax, df = spac.visualization.histogram( @@ -24,7 +16,7 @@ def spac_Histogram_2(): annotation=input.h2_anno() ).values() shared['df_histogram2'].set(df) - ax.tick_params(axis='x', rotation=rotation, labelsize=font_size) + ax.tick_params(axis='x', rotation=input.anno_slider(), labelsize=10) return fig # 2) If "Group By" is CHECKED, we must always supply a @@ -52,13 +44,12 @@ def spac_Histogram_2(): ).values() shared['df_histogram2'].set(df) axes = ax if isinstance(ax, (list, np.ndarray)) else [ax] - # Modified... (renamed loop variable to avoid shadowing) - for current_ax in axes: - # Modified... - current_ax.tick_params( - axis='x', - rotation=rotation, - labelsize=font_size + for ax in axes: + ax.tick_params( + axis='x', + rotation=input.anno_slider(), + labelsize=10 + ) return fig return None @@ -77,7 +68,7 @@ def download_histogram_button_ui(): @render.download(filename="annotation_histogram_data.csv") def download_histogram2_df(): - df = shared['df_human_histogram2'].get() + df = shared['df_histogram2'].get() if df is not None: csv_string = df.to_csv(index=False) csv_bytes = csv_string.encode("utf-8") diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index de263b3..4767809 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -28,7 +28,7 @@ def annotations_ui(): ui.div(id="main-h2_together_drop"), accessible_slider( "anno_slider", - "Rotate X-axis Labels (degrees up to 90)", + "Rotate X-axis Labels (degrees)", min_val=0, max_val=90, value=0, @@ -43,14 +43,6 @@ def annotations_ui(): {"style": "padding-top: 20px;"}, ui.output_ui("download_histogram_button_ui") ), - # ADDED: Font size slider for the plot - ui.input_slider( - "annotations_font_size", - "Axis Label Font Size", - min=3, - max=24, - value=10 - ) ), ui.column( 10, From d1ba7faed161c05b9836fa892e36c1fcc00105db Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Thu, 23 Oct 2025 19:55:39 -0400 Subject: [PATCH 32/49] Font Slider for Annotations --- server/annotations_server.py | 23 ++++++++++++++++------- ui/annotations_ui.py | 8 ++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/server/annotations_server.py b/server/annotations_server.py index f16b4e4..e174175 100644 --- a/server/annotations_server.py +++ b/server/annotations_server.py @@ -1,6 +1,7 @@ from shiny import ui, render, reactive import numpy as np import spac.visualization +import matplotlib.pyplot as plt def annotations_server(input, output, session, shared): @output @render.plot @@ -9,6 +10,12 @@ def spac_Histogram_2(): adata = shared['adata_main'].get() if adata is None: return None + # Added... + # Note: This assumes your UI file has a slider with the id 'annotations_font_size'. + # Please ensure this ID matches the one in your annotations_ui.py file. + font_size = input.annotations_font_size() + plt.rcParams.update({'font.size': font_size}) + rotation = input.anno_slider() # 1) If "Group By" is UNCHECKED, show a simple annotation histogram if not input.h2_group_by_check(): fig, ax, df = spac.visualization.histogram( @@ -16,7 +23,7 @@ def spac_Histogram_2(): annotation=input.h2_anno() ).values() shared['df_histogram2'].set(df) - ax.tick_params(axis='x', rotation=input.anno_slider(), labelsize=10) + ax.tick_params(axis='x', rotation=rotation, labelsize=font_size) return fig # 2) If "Group By" is CHECKED, we must always supply a @@ -44,11 +51,13 @@ def spac_Histogram_2(): ).values() shared['df_histogram2'].set(df) axes = ax if isinstance(ax, (list, np.ndarray)) else [ax] - for ax in axes: - ax.tick_params( - axis='x', - rotation=input.anno_slider(), - labelsize=10 + # Modified... (renamed loop variable to avoid shadowing) + for current_ax in axes: + # Modified... + current_ax.tick_params( + axis='x', + rotation=rotation, + labelsize=font_size ) return fig return None @@ -68,7 +77,7 @@ def download_histogram_button_ui(): @render.download(filename="annotation_histogram_data.csv") def download_histogram2_df(): - df = shared['df_histogram2'].get() + df = shared['df_human_histogram2'].get() if df is not None: csv_string = df.to_csv(index=False) csv_bytes = csv_string.encode("utf-8") diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index 4767809..fa4c284 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -43,6 +43,14 @@ def annotations_ui(): {"style": "padding-top: 20px;"}, ui.output_ui("download_histogram_button_ui") ), + ui.div(style="height: 20px;"), + ui.input_slider( + "annotations_font_size", + "Axis Label Font Size", + min=3, + max=24, + value=10 + ) ), ui.column( 10, From 8b4322d1899359f5d07daeacc6eb89026f5d3116 Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Thu, 23 Oct 2025 20:29:29 -0400 Subject: [PATCH 33/49] Boxplot Font Slider --- server/boxplot_server.py | 13 +++++++++---- ui/annotations_ui.py | 4 ++-- ui/boxplot_ui.py | 8 ++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/server/boxplot_server.py b/server/boxplot_server.py index c381492..747ac86 100644 --- a/server/boxplot_server.py +++ b/server/boxplot_server.py @@ -3,7 +3,7 @@ import anndata as ad import pandas as pd import spac.visualization - +import matplotlib.pyplot as plt def boxplot_server(input, output, session, shared): # Helper functions for reusability @@ -41,7 +41,8 @@ def spac_Boxplot(): layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) - + # Added... + font_size = input.bp_font_size() # Proceed only if adata is valid if adata is not None and adata.var is not None: @@ -60,6 +61,8 @@ def spac_Boxplot(): # Return the interactive Plotly figure object shared['df_boxplot'].set(df) + # Added... + fig.update_layout(font=dict(size=font_size)) print(type(fig)) return fig @@ -110,7 +113,8 @@ def boxplot_static(): layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) - + # Added... + font_size = input.bp_font_size() # Proceed only if adata is valid if adata is not None and adata.var is not None: @@ -126,7 +130,8 @@ def boxplot_static(): figure_width=4.8, figure_type="static" ).values() - + # Added... + fig.update_layout(font=dict(size=font_size)) return fig return None diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index fa4c284..652e12c 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -7,7 +7,7 @@ def annotations_ui(): return ui.nav_panel( "Annotations", ui.card( - {"style": "width:100%;"}, + {"style": "width:125%;"}, ui.column( 12, ui.row( @@ -50,7 +50,7 @@ def annotations_ui(): min=3, max=24, value=10 - ) + ), ), ui.column( 10, diff --git a/ui/boxplot_ui.py b/ui/boxplot_ui.py index 82fdee3..25757ee 100644 --- a/ui/boxplot_ui.py +++ b/ui/boxplot_ui.py @@ -54,6 +54,14 @@ def boxplot_ui(): "Enable Interactive Plot", True ), + # Added... + ui.input_slider( + "bp_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_bp", "Render Plot", From 05adb49d0dfa19e199e1bee86d02cb8f39afa038 Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 24 Oct 2025 17:06:29 -0400 Subject: [PATCH 34/49] Revert "Feat vs anno labels" --- .gitignore | 2 +- server/feat_vs_anno_server.py | 148 +++++++++++----------------------- ui/feat_vs_anno_ui.py | 36 +-------- 3 files changed, 49 insertions(+), 137 deletions(-) diff --git a/.gitignore b/.gitignore index 85465f8..690ce21 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ +__pycache__/ .ipynb_checkpoints/ - # Python __pycache__/ *.py[cod] diff --git a/server/feat_vs_anno_server.py b/server/feat_vs_anno_server.py index 0cba34f..b559023 100644 --- a/server/feat_vs_anno_server.py +++ b/server/feat_vs_anno_server.py @@ -6,18 +6,6 @@ def feat_vs_anno_server(input, output, session, shared): - rendering_state = reactive.Value(False) - - @reactive.effect - @reactive.event(input.go_hm1) - def handle_render_start(): - rendering_state.set(True) - - @reactive.effect - @reactive.event(input.cancel_hm1) - def handle_cancel_click(): - rendering_state.set(False) - def on_layer_check(): return input.hm1_layer() if input.hm1_layer() != "Original" else None @@ -30,8 +18,8 @@ def on_dendro_check(): return (None, None) to indicate that no dendrogram is available. ''' return ( - (input.h2_anno_dendro(), input.h2_feat_dendro()) - if input.dendogram() + (input.h2_anno_dendro(), input.h2_feat_dendro()) + if input.dendogram() else (None, None) ) @@ -40,78 +28,48 @@ def on_dendro_check(): @reactive.event(input.go_hm1, ignore_none=True) def spac_Heatmap(): adata = ad.AnnData( - X=shared['X_data'].get(), - obs=pd.DataFrame(shared['obs_data'].get()), - var=pd.DataFrame(shared['var_data'].get()), - layers=shared['layers_data'].get(), + X=shared['X_data'].get(), + obs=pd.DataFrame(shared['obs_data'].get()), + var=pd.DataFrame(shared['var_data'].get()), + layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) - if adata is None: - return None + if adata is not None: + vmin = input.min_select() + vmax = input.max_select() + cmap = input.hm1_cmap() # Get the selected color map from the dropdown + kwargs = {"vmin": vmin,"vmax": vmax,} - vmin = input.min_select() - vmax = input.max_select() - cmap = input.hm1_cmap() - fontsize = input.axis_label_fontsize() - kwargs = {"vmin": vmin, "vmax": vmax} - cluster_annotations, cluster_features = on_dendro_check() - - try: + cluster_annotations, cluster_features = on_dendro_check() df, fig, ax = spac.visualization.hierarchical_heatmap( - adata, - annotation=input.hm1_anno(), - layer=on_layer_check(), - z_score=None, + adata, + annotation=input.hm1_anno(), + layer=on_layer_check(), + z_score=None, cluster_annotations=cluster_annotations, - cluster_feature=cluster_features, + cluster_feature=cluster_features, **kwargs ) - except Exception as e: - print("Heatmap generation failed:", e) - return None - - if fig is None or not hasattr(fig, "ax_heatmap"): - print("Invalid figure structure.") - return None - if cmap != "viridis": - fig.ax_heatmap.collections[0].set_cmap(cmap) - - shared['df_heatmap'].set(df) - - #Rotate X and Y axis labels - fig.ax_heatmap.set_xticklabels( - fig.ax_heatmap.get_xticklabels(), - rotation=input.hm_x_label_rotation(), - horizontalalignment='right' - ) - fig.ax_heatmap.set_yticklabels( - fig.ax_heatmap.get_yticklabels(), - rotation=input.hm_y_label_rotation(), - verticalalignment='center' - ) - - # Abbreviate labels if enabled - def abbreviate_labels(labels, limit): - return [label.get_text()[:limit] if label.get_text() else "" for label in labels] - - if input.enable_abbreviation(): - limit = input.label_char_limit() - abbreviated_xticks = abbreviate_labels(fig.ax_heatmap.get_xticklabels(), limit) - fig.ax_heatmap.set_xticklabels(abbreviated_xticks, rotation=input.hm_x_label_rotation()) - abbreviated_yticks = abbreviate_labels(fig.ax_heatmap.get_yticklabels(), limit) - fig.ax_heatmap.set_yticklabels(abbreviated_yticks, rotation=input.hm_y_label_rotation()) - - for label in fig.ax_heatmap.get_xticklabels(): - label.set_fontsize(fontsize) - label.set_fontfamily("DejaVu Sans") - for label in fig.ax_heatmap.get_yticklabels(): - label.set_fontsize(fontsize) - label.set_fontfamily("DejaVu Sans") - - fig.fig.subplots_adjust(bottom=0.4, left=0.1) - return fig + # Only update if a non-default color map is selected + if cmap != "viridis": + fig.ax_heatmap.collections[0].set_cmap(cmap) + + shared['df_heatmap'].set(df) + + # Rotate x-axis labels + fig.ax_heatmap.set_xticklabels( + fig.ax_heatmap.get_xticklabels(), + rotation=input.hm_x_label_rotation(), # degrees + horizontalalignment='right' + ) + # fig is a seaborn.matrix.ClusterGrid + fig.fig.subplots_adjust(bottom=0.4) + fig.fig.subplots_adjust(left=0.1) + return fig + return None + heatmap_ui_initialized = reactive.Value(False) @reactive.effect @@ -120,26 +78,27 @@ def heatmap_reactivity(): ui_initialized = heatmap_ui_initialized.get() if btn and not ui_initialized: - # Insert feature cluster first - feat_check = ui.input_checkbox("h2_feat_dendro", "Feature Cluster", value=False) + annotation_check = ui.input_checkbox("h2_anno_dendro", "Annotation Cluster", value=False) ui.insert_ui( - ui.div({"id": "inserted-check1"}, feat_check), - selector="#main-hm2_check", + ui.div({"id": "inserted-check"}, annotation_check), + selector="#main-hm1_check", where="beforeEnd", ) - # Insert annotation cluster below - annotation_check = ui.input_checkbox("h2_anno_dendro", "Annotation Cluster", value=False) + + feat_check = ui.input_checkbox("h2_feat_dendro", "Feature Cluster", value=False) ui.insert_ui( - ui.div({"id": "inserted-check"}, annotation_check), + ui.div({"id": "inserted-check1"}, feat_check), selector="#main-hm2_check", where="beforeEnd", ) heatmap_ui_initialized.set(True) + elif not btn and ui_initialized: ui.remove_ui("#inserted-check") ui.remove_ui("#inserted-check1") heatmap_ui_initialized.set(False) + @render.download(filename="heatmap_data.csv") def download_df(): df = shared['df_heatmap'].get() @@ -149,6 +108,7 @@ def download_df(): return csv_bytes, "text/csv" return None + @render.ui @reactive.event(input.go_hm1, ignore_none=True) def download_button_ui(): @@ -156,6 +116,7 @@ def download_button_ui(): return ui.download_button("download_df", "Download Data", class_="btn-warning") return None + @reactive.effect @reactive.event(input.hm1_layer) def update_min_max(): @@ -202,22 +163,3 @@ def update_min_max(): selector="#main-max_num", where="beforeEnd", ) - - @reactive.effect - @reactive.event(input.enable_abbreviation) - def toggle_label_char_limit_slider(): - if input.enable_abbreviation(): - slider = ui.input_slider( - "label_char_limit", - "Max Characters per Label", - min=2, - max=20, - value=6 - ) - ui.insert_ui( - ui.div({"id": "inserted-label-char-limit"}, slider), - selector="#main-hm1_check", # Or another appropriate container - where="beforeEnd", - ) - else: - ui.remove_ui("#inserted-label-char-limit") diff --git a/ui/feat_vs_anno_ui.py b/ui/feat_vs_anno_ui.py index c6c2d84..3c64a66 100644 --- a/ui/feat_vs_anno_ui.py +++ b/ui/feat_vs_anno_ui.py @@ -38,50 +38,20 @@ def feat_vs_anno_ui(): value=50, step=1 ), - ui.input_slider( - "hm_y_label_rotation", - "Rotate Y Axis Labels", - min=0, - max=90, - value=25 - ), - - ui.input_slider( - "axis_label_fontsize", - "Axis Label Font Size", - min=3, - max=24, - value=10 - ), - - - ui.input_checkbox( - "enable_abbreviation", - "Abbreviate Axis Labels", - value=False - ), - - ui.div(id="main-hm1_check"), - ui.input_checkbox( "dendogram", "Include Dendrogram", False ), - + ui.div(id="main-hm1_check"), ui.div(id="main-hm2_check"), ui.div(id="main-min_num"), ui.div(id="main-max_num"), - # Grouped buttons with spacing - ui.input_action_button( - "go_hm1", - "Render Plot", + "go_hm1", + "Render Plot", class_="btn-success" ), - - - ui.div( {"style": "padding-top: 20px;"}, ui.output_ui("download_button_ui") From d38ad2ca49a0fbe1db0e53047485c3544133d901 Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 24 Oct 2025 17:22:41 -0400 Subject: [PATCH 35/49] Revert "Revert "Feat vs anno labels"" --- .gitignore | 2 +- server/feat_vs_anno_server.py | 148 +++++++++++++++++++++++----------- ui/feat_vs_anno_ui.py | 36 ++++++++- 3 files changed, 137 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 690ce21..85465f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -__pycache__/ .ipynb_checkpoints/ + # Python __pycache__/ *.py[cod] diff --git a/server/feat_vs_anno_server.py b/server/feat_vs_anno_server.py index b559023..0cba34f 100644 --- a/server/feat_vs_anno_server.py +++ b/server/feat_vs_anno_server.py @@ -6,6 +6,18 @@ def feat_vs_anno_server(input, output, session, shared): + rendering_state = reactive.Value(False) + + @reactive.effect + @reactive.event(input.go_hm1) + def handle_render_start(): + rendering_state.set(True) + + @reactive.effect + @reactive.event(input.cancel_hm1) + def handle_cancel_click(): + rendering_state.set(False) + def on_layer_check(): return input.hm1_layer() if input.hm1_layer() != "Original" else None @@ -18,8 +30,8 @@ def on_dendro_check(): return (None, None) to indicate that no dendrogram is available. ''' return ( - (input.h2_anno_dendro(), input.h2_feat_dendro()) - if input.dendogram() + (input.h2_anno_dendro(), input.h2_feat_dendro()) + if input.dendogram() else (None, None) ) @@ -28,48 +40,78 @@ def on_dendro_check(): @reactive.event(input.go_hm1, ignore_none=True) def spac_Heatmap(): adata = ad.AnnData( - X=shared['X_data'].get(), - obs=pd.DataFrame(shared['obs_data'].get()), - var=pd.DataFrame(shared['var_data'].get()), - layers=shared['layers_data'].get(), + X=shared['X_data'].get(), + obs=pd.DataFrame(shared['obs_data'].get()), + var=pd.DataFrame(shared['var_data'].get()), + layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) - if adata is not None: - vmin = input.min_select() - vmax = input.max_select() - cmap = input.hm1_cmap() # Get the selected color map from the dropdown - kwargs = {"vmin": vmin,"vmax": vmax,} + if adata is None: + return None - cluster_annotations, cluster_features = on_dendro_check() + vmin = input.min_select() + vmax = input.max_select() + cmap = input.hm1_cmap() + fontsize = input.axis_label_fontsize() + kwargs = {"vmin": vmin, "vmax": vmax} + cluster_annotations, cluster_features = on_dendro_check() + + try: df, fig, ax = spac.visualization.hierarchical_heatmap( - adata, - annotation=input.hm1_anno(), - layer=on_layer_check(), - z_score=None, + adata, + annotation=input.hm1_anno(), + layer=on_layer_check(), + z_score=None, cluster_annotations=cluster_annotations, - cluster_feature=cluster_features, + cluster_feature=cluster_features, **kwargs ) + except Exception as e: + print("Heatmap generation failed:", e) + return None - # Only update if a non-default color map is selected - if cmap != "viridis": - fig.ax_heatmap.collections[0].set_cmap(cmap) - - shared['df_heatmap'].set(df) - - # Rotate x-axis labels - fig.ax_heatmap.set_xticklabels( - fig.ax_heatmap.get_xticklabels(), - rotation=input.hm_x_label_rotation(), # degrees - horizontalalignment='right' - ) - # fig is a seaborn.matrix.ClusterGrid - fig.fig.subplots_adjust(bottom=0.4) - fig.fig.subplots_adjust(left=0.1) - return fig + if fig is None or not hasattr(fig, "ax_heatmap"): + print("Invalid figure structure.") + return None + + if cmap != "viridis": + fig.ax_heatmap.collections[0].set_cmap(cmap) + + shared['df_heatmap'].set(df) + + #Rotate X and Y axis labels + fig.ax_heatmap.set_xticklabels( + fig.ax_heatmap.get_xticklabels(), + rotation=input.hm_x_label_rotation(), + horizontalalignment='right' + ) + fig.ax_heatmap.set_yticklabels( + fig.ax_heatmap.get_yticklabels(), + rotation=input.hm_y_label_rotation(), + verticalalignment='center' + ) + + # Abbreviate labels if enabled + def abbreviate_labels(labels, limit): + return [label.get_text()[:limit] if label.get_text() else "" for label in labels] + + if input.enable_abbreviation(): + limit = input.label_char_limit() + abbreviated_xticks = abbreviate_labels(fig.ax_heatmap.get_xticklabels(), limit) + fig.ax_heatmap.set_xticklabels(abbreviated_xticks, rotation=input.hm_x_label_rotation()) + abbreviated_yticks = abbreviate_labels(fig.ax_heatmap.get_yticklabels(), limit) + fig.ax_heatmap.set_yticklabels(abbreviated_yticks, rotation=input.hm_y_label_rotation()) + + for label in fig.ax_heatmap.get_xticklabels(): + label.set_fontsize(fontsize) + label.set_fontfamily("DejaVu Sans") + for label in fig.ax_heatmap.get_yticklabels(): + label.set_fontsize(fontsize) + label.set_fontfamily("DejaVu Sans") + + fig.fig.subplots_adjust(bottom=0.4, left=0.1) + return fig - return None - heatmap_ui_initialized = reactive.Value(False) @reactive.effect @@ -78,27 +120,26 @@ def heatmap_reactivity(): ui_initialized = heatmap_ui_initialized.get() if btn and not ui_initialized: - annotation_check = ui.input_checkbox("h2_anno_dendro", "Annotation Cluster", value=False) + # Insert feature cluster first + feat_check = ui.input_checkbox("h2_feat_dendro", "Feature Cluster", value=False) ui.insert_ui( - ui.div({"id": "inserted-check"}, annotation_check), - selector="#main-hm1_check", + ui.div({"id": "inserted-check1"}, feat_check), + selector="#main-hm2_check", where="beforeEnd", ) - - feat_check = ui.input_checkbox("h2_feat_dendro", "Feature Cluster", value=False) + # Insert annotation cluster below + annotation_check = ui.input_checkbox("h2_anno_dendro", "Annotation Cluster", value=False) ui.insert_ui( - ui.div({"id": "inserted-check1"}, feat_check), + ui.div({"id": "inserted-check"}, annotation_check), selector="#main-hm2_check", where="beforeEnd", ) heatmap_ui_initialized.set(True) - elif not btn and ui_initialized: ui.remove_ui("#inserted-check") ui.remove_ui("#inserted-check1") heatmap_ui_initialized.set(False) - @render.download(filename="heatmap_data.csv") def download_df(): df = shared['df_heatmap'].get() @@ -108,7 +149,6 @@ def download_df(): return csv_bytes, "text/csv" return None - @render.ui @reactive.event(input.go_hm1, ignore_none=True) def download_button_ui(): @@ -116,7 +156,6 @@ def download_button_ui(): return ui.download_button("download_df", "Download Data", class_="btn-warning") return None - @reactive.effect @reactive.event(input.hm1_layer) def update_min_max(): @@ -163,3 +202,22 @@ def update_min_max(): selector="#main-max_num", where="beforeEnd", ) + + @reactive.effect + @reactive.event(input.enable_abbreviation) + def toggle_label_char_limit_slider(): + if input.enable_abbreviation(): + slider = ui.input_slider( + "label_char_limit", + "Max Characters per Label", + min=2, + max=20, + value=6 + ) + ui.insert_ui( + ui.div({"id": "inserted-label-char-limit"}, slider), + selector="#main-hm1_check", # Or another appropriate container + where="beforeEnd", + ) + else: + ui.remove_ui("#inserted-label-char-limit") diff --git a/ui/feat_vs_anno_ui.py b/ui/feat_vs_anno_ui.py index 3c64a66..c6c2d84 100644 --- a/ui/feat_vs_anno_ui.py +++ b/ui/feat_vs_anno_ui.py @@ -38,20 +38,50 @@ def feat_vs_anno_ui(): value=50, step=1 ), + ui.input_slider( + "hm_y_label_rotation", + "Rotate Y Axis Labels", + min=0, + max=90, + value=25 + ), + + ui.input_slider( + "axis_label_fontsize", + "Axis Label Font Size", + min=3, + max=24, + value=10 + ), + + + ui.input_checkbox( + "enable_abbreviation", + "Abbreviate Axis Labels", + value=False + ), + + ui.div(id="main-hm1_check"), + ui.input_checkbox( "dendogram", "Include Dendrogram", False ), - ui.div(id="main-hm1_check"), + ui.div(id="main-hm2_check"), ui.div(id="main-min_num"), ui.div(id="main-max_num"), + # Grouped buttons with spacing + ui.input_action_button( - "go_hm1", - "Render Plot", + "go_hm1", + "Render Plot", class_="btn-success" ), + + + ui.div( {"style": "padding-top: 20px;"}, ui.output_ui("download_button_ui") From 23347308e05d2861328f7c2171ad830ec74d53dc Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Tue, 28 Oct 2025 16:01:34 -0400 Subject: [PATCH 36/49] Moved Annotation UI for Render Plot --- ui/annotations_ui.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index 652e12c..c4b9d8e 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -34,6 +34,14 @@ def annotations_ui(): value=0, step=1 ), + ui.div(style="height: 20px;"), + ui.input_slider( + "annotations_font_size", + "Axis Label Font Size", + min=3, + max=24, + value=10 + ), ui.input_action_button( "go_h2", "Render Plot", @@ -43,14 +51,6 @@ def annotations_ui(): {"style": "padding-top: 20px;"}, ui.output_ui("download_histogram_button_ui") ), - ui.div(style="height: 20px;"), - ui.input_slider( - "annotations_font_size", - "Axis Label Font Size", - min=3, - max=24, - value=10 - ), ), ui.column( 10, From 62a14961c0fdff8d4cf956fa43f93f4a30bd211d Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Tue, 28 Oct 2025 16:58:15 -0400 Subject: [PATCH 37/49] Features Font Slider --- server/features_server.py | 5 +++-- ui/features_ui.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/server/features_server.py b/server/features_server.py index b2207c3..845a30d 100644 --- a/server/features_server.py +++ b/server/features_server.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd import spac.visualization - +import matplotlib.pyplot as plt def features_server(input, output, session, shared): def on_layer_check(): @@ -26,6 +26,7 @@ def spac_Histogram_1(): return None feature = input.h1_feat() + font_size = input.features_font_size() rotation = input.feat_slider() btn_log_x = input.h1_log_x() btn_log_y = input.h1_log_y() @@ -49,7 +50,7 @@ def spac_Histogram_1(): axes = ax if isinstance(ax, (list, np.ndarray)) else [ax] for a in axes: - a.tick_params(axis='x', rotation=rotation, labelsize=10) + a.tick_params(axis='x', rotation=rotation, labelsize=font_size) shared['df_histogram1'].set(df) return fig1 diff --git a/ui/features_ui.py b/ui/features_ui.py index 5ade9bb..c895ff1 100644 --- a/ui/features_ui.py +++ b/ui/features_ui.py @@ -50,6 +50,14 @@ def features_ui(): value=0, step=1 ), + ui.div(style="height: 20px;"), + ui.input_slider( + "features_font_size", + "Axis Label Font Size", + min=3, + max=24, + value=10 + ), ui.input_action_button( "go_h1", "Render Plot", From 1996175ae6b99182cbd35282ae4135f2cfceba1d Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Wed, 29 Oct 2025 19:56:06 -0400 Subject: [PATCH 38/49] fix(docker): fix 'Hash Sum Mismatch' bug on mac --- Dockerfile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index f1e8f1c..bf781e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,16 @@ FROM python:3.9.19-slim-bookworm # Set working directory WORKDIR /app +# Issue #12: Fix 'Hash Sum Mismatch' bug for mac device +RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \ + echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ + echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom + +# This is a previous method to fix this bug. It no longer works for 3.9.19-slim-bookworm +# RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list + # Install system dependencies needed for scientific packages -RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list \ - && apt-get update && apt-get install -y \ -# RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y \ gcc \ g++ \ git \ From 3b014f6a7931d136b512af25b115f896d77cbe7c Mon Sep 17 00:00:00 2001 From: arjunchhabra1 Date: Thu, 30 Oct 2025 06:19:57 -0400 Subject: [PATCH 39/49] Added font size sliders for Sankey + Heatmap (Anno vs Anno) --- server/anno_vs_anno_server.py | 22 ++++++ ui/anno_vs_anno_ui.py | 124 ++++++++++++++++++---------------- 2 files changed, 89 insertions(+), 57 deletions(-) diff --git a/server/anno_vs_anno_server.py b/server/anno_vs_anno_server.py index 90a96a1..e15faca 100644 --- a/server/anno_vs_anno_server.py +++ b/server/anno_vs_anno_server.py @@ -22,6 +22,15 @@ def spac_Sankey(): source_annotation=input.sk1_anno1(), target_annotation=input.sk1_anno2() ) + + font_size = input.sankey_font_size() + + # Modified... + # Applying font size directly to the Sankey trace for node and + # label text, as the global layout font can sometimes be ignored. + fig.update_layout(font=dict(size=font_size)) + fig.update_traces(textfont=dict(size=font_size), selector=dict(type='sankey')) + return fig return None @@ -40,6 +49,19 @@ def spac_Relational(): target_annotation=input.rhm_anno2() ) shared['df_relational'].set(result['data']) + + font_size = input.heatmap_font_size() + + # Modified... + # Applying font size directly to the heatmap axes and color bar, + # as these elements often have their own font settings. + result['figure'].update_layout( + font=dict(size=font_size), + xaxis=dict(tickfont=dict(size=font_size)), + yaxis=dict(tickfont=dict(size=font_size)) + ) + result['figure'].update_coloraxes(colorbar_tickfont_size=font_size) + return result['figure'] return None diff --git a/ui/anno_vs_anno_ui.py b/ui/anno_vs_anno_ui.py index fbbf96e..9e6163e 100644 --- a/ui/anno_vs_anno_ui.py +++ b/ui/anno_vs_anno_ui.py @@ -77,33 +77,38 @@ def anno_vs_anno_ui(): "font-size: 0.9em;" ) ), - ui.column( - 12, - ui.row( - ui.column( - 2, - ui.input_select( - "sk1_anno1", - "Select Source Annotation", - choices=[] - ), - ui.input_select( - "sk1_anno2", - "Select Target Annotation", - choices=[] - ), - ui.input_action_button( - "go_sk1", - "Generate Sankey Plot", - class_="btn-success" - ) + ui.row( + ui.column( + 2, + ui.input_select( + "sk1_anno1", + "Select Source Annotation", + choices=[] ), - ui.column( - 10, - ui.div( - output_widget("spac_Sankey"), - style="width:100%; height:80vh;" - ) + ui.input_select( + "sk1_anno2", + "Select Target Annotation", + choices=[] + ), + # Added... + ui.input_slider( + "sankey_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), + ui.input_action_button( + "go_sk1", + "Generate Sankey Plot", + class_="btn-success" + ) + ), + ui.column( + 10, + ui.div( + output_widget("spac_Sankey"), + style="width:100%; height:80vh;" ) ) ) @@ -123,39 +128,44 @@ def anno_vs_anno_ui(): "font-size: 0.9em;" ) ), - ui.column( - 12, - ui.row( - ui.column( - 2, - ui.input_select( - "rhm_anno1", - "Select Source Annotation", - choices=[], - selected=[] - ), - ui.input_select( - "rhm_anno2", - "Select Target Annotation", - choices=[], - selected=[] - ), - ui.input_action_button( - "go_rhm1", - "Generate Heatmap", - class_="btn-success" + ui.row( + ui.column( + 2, + ui.input_select( + "rhm_anno1", + "Select Source Annotation", + choices=[], + selected=[] + ), + ui.input_select( + "rhm_anno2", + "Select Target Annotation", + choices=[], + selected=[] + ), + # Added... + ui.input_slider( + "heatmap_font_size", + "Font Size", + min=5, + max=30, + value=12 ), - ui.div( - {"style": "padding-top: 20px;"}, - ui.output_ui("download_button_ui_1") - ) + ui.input_action_button( + "go_rhm1", + "Generate Heatmap", + class_="btn-success" ), - ui.column( - 10, - ui.div( - output_widget("spac_Relational"), - style="width:100%; height:80vh;" - ) + ui.div( + {"style": "padding-top: 20px;"}, + ui.output_ui("download_button_ui_1") + ) + ), + ui.column( + 10, + ui.div( + output_widget("spac_Relational"), + style="width:100%; height:80vh;" ) ) ) From 3b09cf139e4f18025dc2266ef14c229222bdb2f9 Mon Sep 17 00:00:00 2001 From: nlee3105 Date: Thu, 30 Oct 2025 15:43:45 -0400 Subject: [PATCH 40/49] added font sliders to scatterplot and nearest_neighbor --- server/nearest_neighbor_server.py | 40 ++++++++++++++----------------- server/scatterplot_server.py | 40 ++++++++++++++++++------------- ui/nearest_neighbor_ui.py | 36 +++++++++++++--------------- ui/scatterplot_ui.py | 7 ++++++ 4 files changed, 65 insertions(+), 58 deletions(-) diff --git a/server/nearest_neighbor_server.py b/server/nearest_neighbor_server.py index e76fc45..a0fc0ca 100644 --- a/server/nearest_neighbor_server.py +++ b/server/nearest_neighbor_server.py @@ -6,7 +6,7 @@ """ from shiny import ui, render, reactive - +import seaborn as sns def nearest_neighbor_server(input, output, session, shared): """ @@ -28,7 +28,6 @@ def nearest_neighbor_server(input, output, session, shared): def get_adata(): """Get the main AnnData object from shared state.""" return shared['adata_main'].get() - @reactive.calc def process_target_labels(): """ @@ -65,11 +64,6 @@ def get_color_mapping(): color_mapping = input.nn_color_mapping() return None if color_mapping == "None" else color_mapping - @reactive.calc - def get_font_size(): - """Process font size, returning None if using default.""" - font_size = input.nn_x_title_fontsize() - return font_size if font_size != 12 else None @output @render.plot @@ -164,26 +158,28 @@ def nn_visualization_plot(): "Facet_Plot": input.nn_facet_plot(), "X_Axis_Label_Rotation": input.nn_x_axis_rotation(), "Shared_X_Axis_Title_": input.nn_shared_x_title(), - "X_Axis_Title_Font_Size": (input.nn_x_title_fontsize() - if input.nn_x_title_fontsize() - else "None"), "Defined_Color_Mapping": get_color_mapping() or "None", "Figure_Width": input.nn_figure_width(), "Figure_Height": input.nn_figure_height(), "Figure_DPI": input.nn_figure_dpi(), - "Font_Size": input.nn_font_size() } - - try: - # Call run_from_json with virtual path - figs, df_data = run_from_json( - json_path=params, - save_results=False, # Return figures directly - show_plot=False - ) - finally: - # Clean up the memory registry - unregister_memory_object(virtual_path) + font_size = input.nn_font_size() + with sns.plotting_context(rc={"font.size": font_size, + "axes.labelsize": font_size, + "xtick.labelsize": font_size, + "ytick.labelsize": font_size, + "legend.fontsize": font_size, + "axes.titlesize": font_size * 1.2}): + + try: + figs, df_data = run_from_json( + json_path=params, + save_results=False, # Return figures directly + show_plot=False + ) + finally: + # Clean up the memory registry + unregister_memory_object(virtual_path) # Store the data for download shared['df_nn'].set(df_data) diff --git a/server/scatterplot_server.py b/server/scatterplot_server.py index cf2d9c7..1639e31 100644 --- a/server/scatterplot_server.py +++ b/server/scatterplot_server.py @@ -2,10 +2,10 @@ import anndata as ad import pandas as pd import spac.visualization - +import seaborn as sns def scatterplot_server(input, output, session, shared): - @reactive.Calc + @reactive.calc def get_scatterplot_names(): obsm_list = shared['obsm_names'].get() var_list = shared['var_names'].get() @@ -123,19 +123,27 @@ def spac_Scatter(): x_label = input.scatter_x() y_label = input.scatter_y() title = f"Scatterplot: {x_label} vs {y_label}" + font_size = input.scatter_font_size() + with sns.plotting_context(rc={"font.size": font_size, + "axes.labelsize": font_size, + "xtick.labelsize": font_size, + "ytick.labelsize": font_size, + "legend.fontsize": font_size, + "axes.titlesize": font_size * 1.2 + }): + + if color_enabled: + fig, ax = spac.visualization.visualize_2D_scatter( + x, y, labels=get_color_values() + ) + for a in fig.axes: + if hasattr(a, "get_ylabel") and a != ax: + a.set_ylabel(f"Colored by: {input.scatter_color()}") + else: + fig, ax = spac.visualization.visualize_2D_scatter(x, y) - if color_enabled: - fig, ax = spac.visualization.visualize_2D_scatter( - x, y, labels=get_color_values() - ) - for a in fig.axes: - if hasattr(a, "get_ylabel") and a != ax: - a.set_ylabel(f"Colored by: {input.scatter_color()}") - else: - fig, ax = spac.visualization.visualize_2D_scatter(x, y) - - ax.set_title(title, fontsize=14) - ax.set_xlabel(x_label) - ax.set_ylabel(y_label) + ax.set_title(title, fontsize=font_size * 1.2) + ax.set_xlabel(x_label, fontsize=font_size) + ax.set_ylabel(y_label, fontsize=font_size) - return ax + return ax diff --git a/ui/nearest_neighbor_ui.py b/ui/nearest_neighbor_ui.py index ce77045..b4409e5 100644 --- a/ui/nearest_neighbor_ui.py +++ b/ui/nearest_neighbor_ui.py @@ -11,7 +11,7 @@ def nearest_neighbor_ui(): """ Create the nearest neighbor visualization UI. - + Returns ------- shiny.ui.NavPanel @@ -66,7 +66,7 @@ def nearest_neighbor_ui(): ui.div( ui.h4("Core Parameters", class_="accessible-heading"), - + # Core functionality parameters ui.input_select( "nn_source_label", @@ -100,9 +100,9 @@ def nearest_neighbor_ui(): ), choices=["None"] ), - + ui.hr(), - + # Plot configuration in expandable section ui.div( ui.input_checkbox( @@ -168,9 +168,9 @@ def nearest_neighbor_ui(): ), ) ), - + ui.hr(), - + # Figure configuration in expandable section ui.div( ui.input_checkbox( @@ -204,17 +204,14 @@ def nearest_neighbor_ui(): ), ) ), + ui.input_slider( + "nn_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.row( - ui.column( - 6, - ui.input_numeric( - "nn_font_size", - "Font Size", - value=11, - min=8, - max=20 - ), - ), ui.column( 6, ui.input_numeric( @@ -229,7 +226,7 @@ def nearest_neighbor_ui(): ), ) ), - + # Axis settings in expandable section ui.div( ui.input_checkbox( @@ -260,7 +257,7 @@ def nearest_neighbor_ui(): ), ) ), - + ui.br(), ui.input_action_button( "go_nn_viz", @@ -294,5 +291,4 @@ def nearest_neighbor_ui(): ), ), ) - ) - + ) \ No newline at end of file diff --git a/ui/scatterplot_ui.py b/ui/scatterplot_ui.py index a552a63..3c77db2 100644 --- a/ui/scatterplot_ui.py +++ b/ui/scatterplot_ui.py @@ -33,6 +33,13 @@ def scatterplot_ui(): value=False ), ui.div(id="main-scatter_dropdown"), + ui.input_slider( + "scatter_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_scatter", "Render Plot", From c6c1709718435c6133e6091f08a29d4bc3ccfbee Mon Sep 17 00:00:00 2001 From: risingmin <48868998+risingmin@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:56:27 -0400 Subject: [PATCH 41/49] Added font sliders to Spatial and UMAP tabs --- server/spatial_server.py | 9 ++++++++- server/umap_server.py | 25 +++++++++++++++++++------ ui/spatial_ui.py | 7 +++++++ ui/umap_ui.py | 14 ++++++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/server/spatial_server.py b/server/spatial_server.py index 7ab1731..d298657 100644 --- a/server/spatial_server.py +++ b/server/spatial_server.py @@ -3,7 +3,7 @@ import anndata as ad import pandas as pd import spac.visualization - +import matplotlib.pyplot as plt def spatial_server(input, output, session, shared): slide_ui_initialized = reactive.Value(False) @@ -109,6 +109,12 @@ def spac_Spatial(): ) slide_check = input.slide_select_check() region_check = input.region_select_check() + # Added... + font_size = input.spatial_font_size() + + # Added... + plt.rcParams.update({'font.size': font_size}) + if adata is not None: if slide_check is False and region_check is False: adata_subset = adata @@ -174,6 +180,7 @@ def spac_Spatial(): tickwidth=2, ticklen=10 ) + out[0]['image_object'].update_layout(font=dict(size=font_size)) return out[0]['image_object'] return None diff --git a/server/umap_server.py b/server/umap_server.py index f0b1ff4..bf95291 100644 --- a/server/umap_server.py +++ b/server/umap_server.py @@ -2,6 +2,7 @@ import anndata as ad import pandas as pd import spac.visualization +import matplotlib.pyplot as plt def umap_server(input, output, session, shared): @@ -23,21 +24,25 @@ def spac_UMAP(): method = input.plottype() point_size = input.umap_slider_1() + font_size = input.umap_font_size_1() mode = input.umap_rb() + # Added: This sets the font size for the entire plot + plt.rcParams.update({'font.size': font_size}) + if mode == "Feature": feature = input.umap_rb_feat() layer = None if input.umap_layer() == "Original" else input.umap_layer() fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, feature=feature, layer=layer, point_size=point_size ) - ax.set_title(f"{method.upper()}: {feature}", fontsize=14) + ax.set_title(f"{method.upper()}: {feature}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") for extra_ax in fig.axes: if hasattr(extra_ax, "get_ylabel") and extra_ax != ax: - extra_ax.set_ylabel(f"Colored by: {feature.upper()}", fontsize=12) + extra_ax.set_ylabel(f"Colored by: {feature.upper()}") return fig @@ -46,7 +51,7 @@ def spac_UMAP(): fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, annotation=annotation, point_size=point_size ) - ax.set_title(f"{method.upper()}: {annotation}", fontsize=14) + ax.set_title(f"{method.upper()}: {annotation}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") @@ -150,19 +155,27 @@ def spac_UMAP2(): point_size = input.umap_slider_2() mode = input.umap_rb2() + # Added: This was the line causing the error. It reads the font + # size from the second slider. + font_size = input.umap_font_size_2() + + # Added: This sets the font size for the entire plot + plt.rcParams.update({'font.size': font_size}) + + if mode == "Feature": feature = input.umap_rb_feat2() layer = None if input.umap_layer2() == "Original" else input.umap_layer2() fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, feature=feature, layer=layer, point_size=point_size ) - ax.set_title(f"{method.upper()}: {feature}", fontsize=14) + ax.set_title(f"{method.upper()}: {feature}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") for extra_ax in fig.axes: if hasattr(extra_ax, "get_ylabel") and extra_ax != ax: - extra_ax.set_ylabel(f"Colored by: {feature}", fontsize=12) + extra_ax.set_ylabel(f"Colored by: {feature}") return fig @@ -171,7 +184,7 @@ def spac_UMAP2(): fig, ax = spac.visualization.dimensionality_reduction_plot( adata, method=method, annotation=annotation, point_size=point_size ) - ax.set_title(f"{method.upper()}: {annotation}", fontsize=14) + ax.set_title(f"{method.upper()}: {annotation}") ax.set_xlabel(f"{method.upper()} 1") ax.set_ylabel(f"{method.upper()} 2") diff --git a/ui/spatial_ui.py b/ui/spatial_ui.py index 27d4e4e..8cdc2cf 100644 --- a/ui/spatial_ui.py +++ b/ui/spatial_ui.py @@ -33,6 +33,13 @@ def spatial_ui(): value=3, step=1 ), + ui.input_slider( + "spatial_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_checkbox( "slide_select_check", "Stratify by Slide", diff --git a/ui/umap_ui.py b/ui/umap_ui.py index f82e64c..e72f2db 100644 --- a/ui/umap_ui.py +++ b/ui/umap_ui.py @@ -32,6 +32,13 @@ def umap_ui(): value=3, step=0.1 ), + ui.input_slider( + "umap_font_size_1", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_umap1", "Render Plot", @@ -66,6 +73,13 @@ def umap_ui(): value=3, step=0.1 ), + ui.input_slider( + "umap_font_size_2", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_umap2", "Render Plot", From 1491c3464a8b613f8a70d6512534502ba16dd1eb Mon Sep 17 00:00:00 2001 From: Saran-Nag Date: Thu, 30 Oct 2025 17:36:43 -0400 Subject: [PATCH 42/49] Heatmap Pin for now --- requirements.txt | 2 ++ server/scatterplot_server.py | 55 ++++++++++++++++++------------------ 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/requirements.txt b/requirements.txt index f931da2..01f6827 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,3 +54,5 @@ shapely==2.0.7 sag-py-execution-time-decorator @ git+https://github.com/SamhammerAG/sag_py_execution_time_decorator.git@976c9683d561aadc0166495117c42cc1762633d7 # SPAC package pinned to specific commit for template compatibility (dev branch as of 2025-10-15) spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@49a6479d3083070064a6218f34125b5e86ad8e46#egg=spac +colorcet +git+https://github.com/holoviz/datashader.git diff --git a/server/scatterplot_server.py b/server/scatterplot_server.py index 4c05742..deb47bd 100644 --- a/server/scatterplot_server.py +++ b/server/scatterplot_server.py @@ -129,36 +129,35 @@ def spac_Scatter(): title = f"Scatterplot: {x_label} vs {y_label}" font_size = input.scatter_font_size() with sns.plotting_context(rc={"font.size": font_size, - "axes.labelsize": font_size, - "xtick.labelsize": font_size, - "ytick.labelsize": font_size, - "legend.fontsize": font_size, + "axes.labelsize": font_size, + "xtick.labelsize": font_size, + "ytick.labelsize": font_size, + "legend.fontsize": font_size, "axes.titlesize": font_size * 1.2 }): - - if heatmap_mode: - color = get_color_values() if color_enabled else None - img = scatter_heatmap(x, y, color) - fig, ax = plt.subplots(figsize=(8, 6)) - ax.imshow(img, aspect='auto') - ax.set_title(title, fontsize=14) - ax.set_xlabel(x_label) - ax.set_ylabel(y_label) - ax.axis('on') # Show axes - return fig - else: - if color_enabled: - fig, ax = spac.visualization.visualize_2D_scatter( - x, y, labels=get_color_values() - ) - for a in fig.axes: - if hasattr(a, "get_ylabel") and a != ax: - a.set_ylabel(f"Colored by: {input.scatter_color()}") + if heatmap_mode: + color = get_color_values() if color_enabled else None + img = scatter_heatmap(x, y, color) + fig, ax = plt.subplots(figsize=(8, 6)) + ax.imshow(img, aspect='auto') + ax.set_title(title, fontsize=14) + ax.set_xlabel(x_label) + ax.set_ylabel(y_label) + ax.axis('on') # Show axes + return fig else: - fig, ax = spac.visualization.visualize_2D_scatter(x, y) - - ax.set_title(title, fontsize=font_size * 1.2) - ax.set_xlabel(x_label, fontsize=font_size) - ax.set_ylabel(y_label, fontsize=font_size) + if color_enabled: + fig, ax = spac.visualization.visualize_2D_scatter( + x, y, labels=get_color_values() + ) + for a in fig.axes: + if hasattr(a, "get_ylabel") and a != ax: + a.set_ylabel(f"Colored by: {input.scatter_color()}") + else: + fig, ax = spac.visualization.visualize_2D_scatter(x, y) + + ax.set_title(title, fontsize=font_size * 1.2) + ax.set_xlabel(x_label, fontsize=font_size) + ax.set_ylabel(y_label, fontsize=font_size) return ax From dc68e72f4cd1e47d0bdb367001c89160ccae0dc8 Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 31 Oct 2025 13:18:14 -0400 Subject: [PATCH 43/49] fix(docker): python layer changed to 3.10-slim-bookworm --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bf781e6..5627a23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,9 @@ # Use official Python image as base -FROM python:3.9.19-slim-bookworm +# FROM python:3.9.19-slim-bookworm +# Issue #15: Fix python version conflicts +# Error message when building docker: +# Package 'datashader' requires a different Python: 3.9.19 not in '>=3.10' +FROM python:3.10-slim-bookworm # Set working directory WORKDIR /app From 199064a0439e068d760b9332a88dba61fa3ae8ab Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 31 Oct 2025 16:47:54 -0400 Subject: [PATCH 44/49] chore(env): remove/archive redundant files for environments --- {tutorials => archived/tutorials}/Quickstart_MacArm64.md | 0 environment.yml | 2 ++ 2 files changed, 2 insertions(+) rename {tutorials => archived/tutorials}/Quickstart_MacArm64.md (100%) diff --git a/tutorials/Quickstart_MacArm64.md b/archived/tutorials/Quickstart_MacArm64.md similarity index 100% rename from tutorials/Quickstart_MacArm64.md rename to archived/tutorials/Quickstart_MacArm64.md diff --git a/environment.yml b/environment.yml index a17f4ab..66ab338 100644 --- a/environment.yml +++ b/environment.yml @@ -1,3 +1,5 @@ +# This file may be outdated. Use requirements.txt to create environment instead. + name: shiny channels: - ohsu-comp-bio From 5c15b9bac83c538ec30c7453623e20467be8db0a Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 31 Oct 2025 17:03:27 -0400 Subject: [PATCH 45/49] chore: fix width issue in anno. tab; create a to-do for scatter_heatmap --- ui/annotations_ui.py | 2 +- utils/datashader_utils.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index 6c42de0..63b9025 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -7,7 +7,7 @@ def annotations_ui(): return ui.nav_panel( "Annotations", ui.card( - {"style": "width:125%;"}, + {"style": "width:100%;"}, ui.column( 12, ui.row( diff --git a/utils/datashader_utils.py b/utils/datashader_utils.py index f232b18..c69fc56 100644 --- a/utils/datashader_utils.py +++ b/utils/datashader_utils.py @@ -3,6 +3,7 @@ import datashader.transfer_functions as tf from colorcet import fire +# TODO: Recheck its logic. def scatter_heatmap(x, y, color=None, width=800, height=600): df = pd.DataFrame({'x': x, 'y': y}) cvs = ds.Canvas(plot_width=width, plot_height=height) From 53e8c5f7a9e0fa2969d4bb8612cefa4ef3c69df3 Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 31 Oct 2025 20:01:51 -0400 Subject: [PATCH 46/49] chore: small fixes and files clean-up for Pull Request #61 small fixes - Update environment.yml to make it consistent with requirement.txt - Update README.md to include TA's name docs clean-up - Update .gitignore to include Jupyter section - Write clearer comments in Dockerfile; remove redundant comments - Remove redundant quickstart guide server files clean-up: anno_vs_anno_server.py, feat_vs_anno_server.py, nearest_neighbor_server.py, scatterplot_server.py ui files clean-up: anno_vs_anno_ui.py, annotations_ui.py, feat_vs_anno_ui.py, features_ui.py, scatterplot_ui.py --- .gitignore | 1 + Dockerfile | 9 +-- README.md | 1 + archived/tutorials/Quickstart_MacArm64.md | 86 ----------------------- environment.yml | 38 +++++++--- server/anno_vs_anno_server.py | 2 +- server/feat_vs_anno_server.py | 10 ++- server/nearest_neighbor_server.py | 10 +++ server/scatterplot_server.py | 5 +- ui/anno_vs_anno_ui.py | 14 ++-- ui/annotations_ui.py | 1 + ui/feat_vs_anno_ui.py | 10 +-- ui/features_ui.py | 1 + ui/scatterplot_ui.py | 1 + 14 files changed, 74 insertions(+), 115 deletions(-) delete mode 100644 archived/tutorials/Quickstart_MacArm64.md diff --git a/.gitignore b/.gitignore index 85465f8..f3d266d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# Jupyter .ipynb_checkpoints/ # Python diff --git a/Dockerfile b/Dockerfile index 5627a23..e04349d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,18 @@ # Use official Python image as base -# FROM python:3.9.19-slim-bookworm -# Issue #15: Fix python version conflicts +# Fix: python version conflicts (Issue #15 in Purdue-2025 fork) # Error message when building docker: # Package 'datashader' requires a different Python: 3.9.19 not in '>=3.10' +# FROM python:3.9.19-slim-bookworm FROM python:3.10-slim-bookworm # Set working directory WORKDIR /app -# Issue #12: Fix 'Hash Sum Mismatch' bug for mac device +# Fix: 'Hash Sum Mismatch' bug for mac device (Issue #12 in Purdue-2025 fork) RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \ echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \ echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom -# This is a previous method to fix this bug. It no longer works for 3.9.19-slim-bookworm -# RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list - # Install system dependencies needed for scientific packages RUN apt-get update && apt-get install -y \ gcc \ diff --git a/README.md b/README.md index f5d3a1a..728b54e 100644 --- a/README.md +++ b/README.md @@ -73,4 +73,5 @@ This project was a success thanks to the invaluable collaboration and support fr * Alex Liu * Omar Eldaghar * Thomas Sheeley + * Ramya Rajaram * **Additional Support:** Kang Liu and Rui He, and the entire Data Mine staff. diff --git a/archived/tutorials/Quickstart_MacArm64.md b/archived/tutorials/Quickstart_MacArm64.md deleted file mode 100644 index ba95dbf..0000000 --- a/archived/tutorials/Quickstart_MacArm64.md +++ /dev/null @@ -1,86 +0,0 @@ -## Pre-requisites - -- [x] `miniconda` for Macbook M1/M2/M3 (`arm64`) -- [x] `homebrew`, `git`, `XCode` -- [x] Github account and authentication tokens -- [x] VS Code - -## Steps - -1. Open a Terminal application and `git clone` this repository inside a folder, e.g., `~/summer2025/`: - - ```sh - mkdir -p ~/summer2025; - git clone [https://github.com/Summer2025-SPAC/SPAC_Shiny](https://github.com/Summer2025-SPAC/SPAC_Shiny); - cd ~/summer2025/SPAC_Shiny - ``` - -2. `git checkout` your development branch, e.g., `gh_iss2_rk`: - - ```sh - git checkout branch_name; - ``` - -3. Open the folder in VS Code: - - ```sh - code ./ - ``` - -4. Inside the VS Code `terminal`, create a `conda` environment using the `environment.yml` file. This file specifies all necessary packages and ensures compatibility. - - - Ensure you are in the SPAC_Shiny directory - - ```sh - cd ~/summer2025/SPAC_Shiny; - ``` - - Make edits these edits to the `environment.yml` file: - - Under dependencies, add these lines: - - ```sh - - tables>=3.8.0 - - c-blosc2 - - libtiff - ``` - - Under pip, remove this line: - - ```sh - - tables==3.8.0 - ``` - - Installing tables and c-blosc2 with conda guarantees a smooth setup on MacOS by using pre-built, compatible binaries, while avoiding build errors that occur with pip installations. - - By adding libtiff directly to your conda dependencies, you instruct conda to download this library and place it correctly within your shiny environment's path. This ensures that when Pillow is imported, its dependencies are available and properly linked, resolving the error. - - # Create the environment from environment.yml - # This will install Python 3.9 and all specified packages. - conda env create -f environment.yml - - # Activate the newly created environment using the environment name - - ```sh - conda activate shiny - ``` - -5. Assuming all installation works, run the shiny app in the terminal. - - ```sh - shiny run app.py - ``` - -6. If you encounter ImportErrors with libtiff, carry out these commands: - - ```sh - conda activate shiny - pip uninstall pillow - conda install -c conda-forge --force-reinstall pillow libtiff - ``` - Then run the shiny app again in the terminal: - - ```sh - shiny run app.py - ``` \ No newline at end of file diff --git a/environment.yml b/environment.yml index 66ab338..adaffc2 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,22 @@ -# This file may be outdated. Use requirements.txt to create environment instead. +# Warning: +# This file may be outdated. Check with requirements.txt to get the latest environmental requirements. +# This is a file for conda users to create an environment. + +# Quick Guide: +# 1. Make sure you are in the repository's directory and 'conda' is installed. +# 2. Run 'conda env create -f environment.yml' to create the conda environment. +# 3. Run 'conda activate shiny' to activate this environment. +# 4. Run 'shiny run --reload app.py' to run the app. +# The app will be available at http://127.0.0.1:8000. + +# Special Reminder: +# For macOS (MacArm64) users, make these edits to the environment.yml file: +# Under dependencies, add these lines: +# - tables>=3.8.0 +# - c-blosc2 +# - libtiff +# Under pip, remove this line: +# - tables==3.8.0 name: shiny channels: @@ -7,7 +25,7 @@ channels: - leej3 - https://fnlcr-dmap.github.io/scimap/ dependencies: - - python=3.9.13 + - python=3.10.19 # To support datashader, need python>=3.10 - numpy=1.26.4 - pandas=1.5.3 - anndata=0.10.9 @@ -20,14 +38,16 @@ dependencies: - squidpy=1.2.2 - scikit-image=0.19.3 - scipy=1.10.1 + # For macOS (MacArm64) users, uncomment the following three lines + # - tables>=3.8.0 + # - c-blosc2 + # - libtiff - pip - pip: - - colorcet - - git+https://github.com/holoviz/datashader.git - ipykernel==6.29.5 - ipython==8.18.0 - ipython-genutils==0.2.0 - - ipywidgets==8.1.3 + - ipywidgets==8.1.7 # To be consistent with requirement.txt - jsonschema-specifications==2023.12.1 - jupyter-events==0.10.0 - jupyter_client==7.4.9 @@ -35,7 +55,7 @@ dependencies: - jupyter_server==2.14.1 - jupyter_server_terminals==0.5.3 - jupyterlab_pygments==0.3.0 - - jupyterlab_widgets==3.0.11 + - jupyterlab_widgets==3.0.15 # To be consistent with requirement.txt - nbclient==0.10.0 - nbconvert==7.16.4 - nbformat==5.10.4 @@ -54,7 +74,7 @@ dependencies: - shinyswatch==0.6.1 - shinywidgets==0.7.0 - sinfo==0.3.4 - - tables==3.8.0 + - tables==3.8.0 # For macOS (MacArm64) users, comment out this line. - tinycss2==1.3.0 - tqdm==4.66.4 - typeguard==4.3.0 @@ -64,7 +84,9 @@ dependencies: - websockets==12.0 - zipp==3.19.2 - tifffile + - shapely==2.0.7 # To be consistent with requirement.txt - sag-py-execution-time-decorator @ git+https://github.com/SamhammerAG/sag_py_execution_time_decorator.git@976c9683d561aadc0166495117c42cc1762633d7 - # - spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@76f381316acd57ff9fbe3e5bea4e37b0a9ae3dd9#egg=spac # SPAC package pinned to specific commit for template compatibility (dev branch as of 2025-10-15) - spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@49a6479d3083070064a6218f34125b5e86ad8e46#egg=spac + - colorcet + - git+https://github.com/holoviz/datashader.git diff --git a/server/anno_vs_anno_server.py b/server/anno_vs_anno_server.py index 86eec49..a5a2dee 100644 --- a/server/anno_vs_anno_server.py +++ b/server/anno_vs_anno_server.py @@ -79,4 +79,4 @@ def download_df_1(): def download_button_ui_1(): if shared['df_relational'].get() is not None: return ui.download_button("download_df_1", "Download Data", class_="btn-warning") - return None \ No newline at end of file + return None diff --git a/server/feat_vs_anno_server.py b/server/feat_vs_anno_server.py index 0cba34f..843ebd2 100644 --- a/server/feat_vs_anno_server.py +++ b/server/feat_vs_anno_server.py @@ -46,6 +46,8 @@ def spac_Heatmap(): layers=shared['layers_data'].get(), dtype=shared['X_data'].get().dtype ) + + # Refactor if adata is None: return None @@ -79,12 +81,15 @@ def spac_Heatmap(): shared['df_heatmap'].set(df) - #Rotate X and Y axis labels + # Rotate x-axis labels fig.ax_heatmap.set_xticklabels( fig.ax_heatmap.get_xticklabels(), rotation=input.hm_x_label_rotation(), horizontalalignment='right' ) + + # Added... + # Rotate y-axis labels fig.ax_heatmap.set_yticklabels( fig.ax_heatmap.get_yticklabels(), rotation=input.hm_y_label_rotation(), @@ -102,6 +107,7 @@ def abbreviate_labels(labels, limit): abbreviated_yticks = abbreviate_labels(fig.ax_heatmap.get_yticklabels(), limit) fig.ax_heatmap.set_yticklabels(abbreviated_yticks, rotation=input.hm_y_label_rotation()) + # Set axis font size and type for label in fig.ax_heatmap.get_xticklabels(): label.set_fontsize(fontsize) label.set_fontfamily("DejaVu Sans") @@ -109,6 +115,7 @@ def abbreviate_labels(labels, limit): label.set_fontsize(fontsize) label.set_fontfamily("DejaVu Sans") + # fig is a seaborn.matrix.ClusterGrid fig.fig.subplots_adjust(bottom=0.4, left=0.1) return fig @@ -203,6 +210,7 @@ def update_min_max(): where="beforeEnd", ) + # Set character limit slider @reactive.effect @reactive.event(input.enable_abbreviation) def toggle_label_char_limit_slider(): diff --git a/server/nearest_neighbor_server.py b/server/nearest_neighbor_server.py index 2de1229..7559c04 100644 --- a/server/nearest_neighbor_server.py +++ b/server/nearest_neighbor_server.py @@ -28,6 +28,7 @@ def nearest_neighbor_server(input, output, session, shared): def get_adata(): """Get the main AnnData object from shared state.""" return shared['adata_main'].get() + @reactive.calc def process_target_labels(): """ @@ -68,6 +69,11 @@ def get_color_mapping(): # Return None if input not available yet (dynamic UI not rendered) return None + @reactive.calc + def get_font_size(): + """Process font size, returning None if using default.""" + font_size = input.nn_x_title_fontsize() + return font_size if font_size != 12 else None @output @render.ui @@ -202,10 +208,14 @@ def nn_visualization_plot(): "Facet_Plot": input.nn_facet_plot(), "X_Axis_Label_Rotation": input.nn_x_axis_rotation(), "Shared_X_Axis_Title_": input.nn_shared_x_title(), + # "X_Axis_Title_Font_Size": (input.nn_x_title_fontsize() + # if input.nn_x_title_fontsize() + # else "None"), "Defined_Color_Mapping": get_color_mapping() or "None", "Figure_Width": input.nn_figure_width(), "Figure_Height": input.nn_figure_height(), "Figure_DPI": input.nn_figure_dpi(), + # "Font_Size": input.nn_font_size() } font_size = input.nn_font_size() with sns.plotting_context(rc={"font.size": font_size, diff --git a/server/scatterplot_server.py b/server/scatterplot_server.py index deb47bd..be07581 100644 --- a/server/scatterplot_server.py +++ b/server/scatterplot_server.py @@ -135,6 +135,7 @@ def spac_Scatter(): "legend.fontsize": font_size, "axes.titlesize": font_size * 1.2 }): + # Added: Heatmap mode if heatmap_mode: color = get_color_values() if color_enabled else None img = scatter_heatmap(x, y, color) @@ -145,6 +146,7 @@ def spac_Scatter(): ax.set_ylabel(y_label) ax.axis('on') # Show axes return fig + else: if color_enabled: fig, ax = spac.visualization.visualize_2D_scatter( @@ -159,5 +161,4 @@ def spac_Scatter(): ax.set_title(title, fontsize=font_size * 1.2) ax.set_xlabel(x_label, fontsize=font_size) ax.set_ylabel(y_label, fontsize=font_size) - - return ax + return ax diff --git a/ui/anno_vs_anno_ui.py b/ui/anno_vs_anno_ui.py index a068d19..a6e9161 100644 --- a/ui/anno_vs_anno_ui.py +++ b/ui/anno_vs_anno_ui.py @@ -144,13 +144,13 @@ def anno_vs_anno_ui(): selected=[] ), # Added... - ui.input_slider( - "heatmap_font_size", - "Font Size", - min=5, - max=30, - value=12 - ), + ui.input_slider( + "heatmap_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_action_button( "go_rhm1", "Generate Heatmap", diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index 63b9025..97d9cba 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -34,6 +34,7 @@ def annotations_ui(): value=0, step=1 ), + # Added... ui.div(style="height: 20px;"), ui.input_slider( "annotations_font_size", diff --git a/ui/feat_vs_anno_ui.py b/ui/feat_vs_anno_ui.py index c6c2d84..f0b967a 100644 --- a/ui/feat_vs_anno_ui.py +++ b/ui/feat_vs_anno_ui.py @@ -38,6 +38,9 @@ def feat_vs_anno_ui(): value=50, step=1 ), + + # Added... + # Y-axis label rotater ui.input_slider( "hm_y_label_rotation", "Rotate Y Axis Labels", @@ -46,6 +49,7 @@ def feat_vs_anno_ui(): value=25 ), + # Font slider ui.input_slider( "axis_label_fontsize", "Axis Label Font Size", @@ -54,13 +58,13 @@ def feat_vs_anno_ui(): value=10 ), - + # Abbreviate toggle ui.input_checkbox( "enable_abbreviation", "Abbreviate Axis Labels", value=False ), - + # place-holder for a dropdown character limit slider ui.div(id="main-hm1_check"), ui.input_checkbox( @@ -79,8 +83,6 @@ def feat_vs_anno_ui(): "Render Plot", class_="btn-success" ), - - ui.div( {"style": "padding-top: 20px;"}, diff --git a/ui/features_ui.py b/ui/features_ui.py index c895ff1..d92b87b 100644 --- a/ui/features_ui.py +++ b/ui/features_ui.py @@ -50,6 +50,7 @@ def features_ui(): value=0, step=1 ), + # Added... ui.div(style="height: 20px;"), ui.input_slider( "features_font_size", diff --git a/ui/scatterplot_ui.py b/ui/scatterplot_ui.py index b306e0d..4f37f7a 100644 --- a/ui/scatterplot_ui.py +++ b/ui/scatterplot_ui.py @@ -32,6 +32,7 @@ def scatterplot_ui(): "Color by Feature", value=False ), + # Added: heatmap mode checkbox ui.input_checkbox( "scatter_heatmap_mode", "Show as Heatmap", From b2798ff09c74ae6eed54f53b9561bdfb3049d79e Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 31 Oct 2025 23:14:09 -0400 Subject: [PATCH 47/49] docs: add a link to the SPAC contributing guide --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..14a9e61 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +# Contributing + +Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. + +Please refer to this link for detailed instructions: https://github.com/FNLCR-DMAP/SCSAWorkflow/blob/main/CONTRIBUTING.md \ No newline at end of file From 5111a994f3a935d7e6ad1d2f49ae274c9192e04e Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Fri, 31 Oct 2025 23:38:13 -0400 Subject: [PATCH 48/49] docs(contri): add link to the copilot-instructions file --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14a9e61..684c630 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,4 +2,6 @@ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. -Please refer to this link for detailed instructions: https://github.com/FNLCR-DMAP/SCSAWorkflow/blob/main/CONTRIBUTING.md \ No newline at end of file +Please refer to this copilot-based instructions for SPAC Shiny development: https://github.com/FNLCR-DMAP/SPAC_Shiny/blob/main/.github/copilot-instructions.md + +Also refer to this contributing guide in the SPAC Package project for more information: https://github.com/FNLCR-DMAP/SCSAWorkflow/blob/main/CONTRIBUTING.md \ No newline at end of file From 95d41db3a8f3758b9e4d024fdf85798146389033 Mon Sep 17 00:00:00 2001 From: zhan4329 Date: Mon, 3 Nov 2025 13:08:55 -0500 Subject: [PATCH 49/49] chore(env): update environment.yml accordingly --- environment.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/environment.yml b/environment.yml index adaffc2..efac738 100644 --- a/environment.yml +++ b/environment.yml @@ -86,7 +86,9 @@ dependencies: - tifffile - shapely==2.0.7 # To be consistent with requirement.txt - sag-py-execution-time-decorator @ git+https://github.com/SamhammerAG/sag_py_execution_time_decorator.git@976c9683d561aadc0166495117c42cc1762633d7 - # SPAC package pinned to specific commit for template compatibility (dev branch as of 2025-10-15) - - spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@49a6479d3083070064a6218f34125b5e86ad8e46#egg=spac + # Added: packages for heatmap mode in scatterplot - colorcet - git+https://github.com/holoviz/datashader.git + # SPAC package pinned to specific commit for template compatibility (dev branch as of 2025-10-15) + - spac @ git+https://github.com/FNLCR-DMAP/SCSAWorkflow.git@c2a248826b91880c62308e85e60cae9361c275d0#egg=spac +