diff --git a/.gitignore b/.gitignore index 32132e8..f3d266d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Jupyter +.ipynb_checkpoints/ + # Python __pycache__/ *.py[cod] @@ -65,4 +68,4 @@ SCSAWorkflow/ # Security documentation (internal use only) SECURITY_DOCUMENTATION.md SECURITY_IMPLEMENTATION.md -SECURITY_FIXES.md \ No newline at end of file +SECURITY_FIXES.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..684c630 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,7 @@ +# Contributing + +Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. + +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 diff --git a/Dockerfile b/Dockerfile index 5b72c9e..e04349d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,18 @@ # Use official Python image as base -FROM python:3.9.19-slim-bookworm +# 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 +# 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 + # 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 e80a4ce..728b54e 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,15 @@ This project was a success thanks to the invaluable collaboration and support fr * Suriya Selvarajan * Qianyue Wang * Andree Kolliegbo + * Sungmin Lee + * Saran Nagubandi + * Noah Lee + * Boqiang Zhang + * Heaven Golladay-Watkins + * Arjun Chhabra * **Teaching Assistants (TAs) from Purdue's Data Mine:** * Alex Liu * Omar Eldaghar * Thomas Sheeley + * Ramya Rajaram * **Additional Support:** Kang Liu and Rui He, and the entire Data Mine staff. diff --git a/environment.yml b/environment.yml index 64daffc..efac738 100644 --- a/environment.yml +++ b/environment.yml @@ -1,3 +1,23 @@ +# 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: - ohsu-comp-bio @@ -5,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 @@ -18,12 +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: - 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 @@ -31,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 @@ -50,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 @@ -60,5 +84,11 @@ 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 + # 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 + diff --git a/requirements.txt b/requirements.txt index bfa0fa1..04560b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,5 +52,8 @@ zipp==3.19.2 tifffile shapely==2.0.7 sag-py-execution-time-decorator @ git+https://github.com/SamhammerAG/sag_py_execution_time_decorator.git@976c9683d561aadc0166495117c42cc1762633d7 +# 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 diff --git a/server/anno_vs_anno_server.py b/server/anno_vs_anno_server.py index 90a96a1..a5a2dee 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/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..8d97b1d 100644 --- a/server/boxplot_server.py +++ b/server/boxplot_server.py @@ -3,7 +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): # Helper functions for reusability @@ -32,34 +33,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 +86,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 +106,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/feat_vs_anno_server.py b/server/feat_vs_anno_server.py index b559023..843ebd2 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,85 @@ 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,} - - cluster_annotations, cluster_features = on_dendro_check() + + # Refactor + 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, + 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-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(), + 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()) + + # Set axis font size and type + 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 is a seaborn.matrix.ClusterGrid + fig.fig.subplots_adjust(bottom=0.4, left=0.1) + return fig - return None - heatmap_ui_initialized = reactive.Value(False) @reactive.effect @@ -78,27 +127,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 +156,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 +163,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 +209,23 @@ def update_min_max(): selector="#main-max_num", where="beforeEnd", ) + + # Set character limit slider + @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/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/server/nearest_neighbor_server.py b/server/nearest_neighbor_server.py index fe4cbb0..7559c04 100644 --- a/server/nearest_neighbor_server.py +++ b/server/nearest_neighbor_server.py @@ -6,7 +6,7 @@ """ from shiny import ui, render, reactive, req - +import seaborn as sns def nearest_neighbor_server(input, output, session, shared): """ @@ -28,7 +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(): """ @@ -208,26 +208,32 @@ 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"), + # "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() } - - 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..be07581 100644 --- a/server/scatterplot_server.py +++ b/server/scatterplot_server.py @@ -2,10 +2,13 @@ import anndata as ad import pandas as pd import spac.visualization +import seaborn as sns +from utils.datashader_utils import scatter_heatmap +import matplotlib.pyplot as plt 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() @@ -120,22 +123,42 @@ 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()}") - 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) - - return ax + 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 + }): + # Added: Heatmap mode + 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()}") + 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 diff --git a/server/spatial_server.py b/server/spatial_server.py index 7ab1731..c458153 100644 --- a/server/spatial_server.py +++ b/server/spatial_server.py @@ -3,7 +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): slide_ui_initialized = reactive.Value(False) @@ -15,8 +16,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 +26,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 +59,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 +69,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 +101,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 +146,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 +170,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 +220,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 +232,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 +246,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 fbbf96e..a6e9161 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,41 +128,46 @@ 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.div( - {"style": "padding-top: 20px;"}, - ui.output_ui("download_button_ui_1") - ) + ui.row( + ui.column( + 2, + ui.input_select( + "rhm_anno1", + "Select Source Annotation", + choices=[], + selected=[] ), - ui.column( - 10, - ui.div( - output_widget("spac_Relational"), - style="width:100%; height:80vh;" - ) + 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.input_action_button( + "go_rhm1", + "Generate Heatmap", + class_="btn-success" + ), + 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;" ) ) ) ) - ) + ) \ No newline at end of file diff --git a/ui/annotations_ui.py b/ui/annotations_ui.py index 4767809..97d9cba 100644 --- a/ui/annotations_ui.py +++ b/ui/annotations_ui.py @@ -34,6 +34,15 @@ def annotations_ui(): value=0, step=1 ), + # Added... + 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", @@ -58,4 +67,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/data_input_ui.py b/ui/data_input_ui.py index fe282de..80e58ba 100644 --- a/ui/data_input_ui.py +++ b/ui/data_input_ui.py @@ -231,4 +231,4 @@ def data_input_ui(): ) ) ) - ) + ) \ No newline at end of file diff --git a/ui/feat_vs_anno_ui.py b/ui/feat_vs_anno_ui.py index 3c64a66..f0b967a 100644 --- a/ui/feat_vs_anno_ui.py +++ b/ui/feat_vs_anno_ui.py @@ -38,20 +38,52 @@ 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", + min=0, + max=90, + value=25 + ), + + # Font slider + ui.input_slider( + "axis_label_fontsize", + "Axis Label Font Size", + min=3, + max=24, + 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( "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") diff --git a/ui/features_ui.py b/ui/features_ui.py index 5ade9bb..d92b87b 100644 --- a/ui/features_ui.py +++ b/ui/features_ui.py @@ -50,6 +50,15 @@ def features_ui(): value=0, step=1 ), + # Added... + 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", diff --git a/ui/nearest_neighbor_ui.py b/ui/nearest_neighbor_ui.py index 2c69cd7..764007e 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( @@ -150,9 +150,9 @@ def nearest_neighbor_ui(): ui.output_ui("nn_color_mapping_ui"), ) ), - + ui.hr(), - + # Figure configuration in expandable section ui.div( ui.input_checkbox( @@ -186,17 +186,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( @@ -211,7 +208,7 @@ def nearest_neighbor_ui(): ), ) ), - + # Axis settings in expandable section ui.div( ui.input_checkbox( @@ -242,7 +239,7 @@ def nearest_neighbor_ui(): ), ) ), - + ui.br(), ui.input_action_button( "go_nn_viz", @@ -276,5 +273,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..4f37f7a 100644 --- a/ui/scatterplot_ui.py +++ b/ui/scatterplot_ui.py @@ -32,7 +32,21 @@ def scatterplot_ui(): "Color by Feature", value=False ), + # Added: heatmap mode checkbox + ui.input_checkbox( + "scatter_heatmap_mode", + "Show as Heatmap", + 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 +64,4 @@ def scatterplot_ui(): ) ) ) - ) - + ) \ No newline at end of file diff --git a/ui/spatial_ui.py b/ui/spatial_ui.py index 27d4e4e..32e22f2 100644 --- a/ui/spatial_ui.py +++ b/ui/spatial_ui.py @@ -33,6 +33,14 @@ def spatial_ui(): value=3, step=1 ), + # Added... + ui.input_slider( + "spatial_font_size", + "Font Size", + min=5, + max=30, + value=12 + ), ui.input_checkbox( "slide_select_check", "Stratify by Slide", @@ -64,5 +72,4 @@ def spatial_ui(): ) ) ) - ) - + ) \ No newline at end of file diff --git a/ui/umap_ui.py b/ui/umap_ui.py index f82e64c..9390061 100644 --- a/ui/umap_ui.py +++ b/ui/umap_ui.py @@ -32,6 +32,14 @@ def umap_ui(): value=3, step=0.1 ), + # Added... + 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 +74,14 @@ def umap_ui(): value=3, step=0.1 ), + # 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", @@ -80,5 +96,4 @@ def umap_ui(): ) ) ) - ) - + ) \ No newline at end of file diff --git a/utils/datashader_utils.py b/utils/datashader_utils.py new file mode 100644 index 0000000..c69fc56 --- /dev/null +++ b/utils/datashader_utils.py @@ -0,0 +1,17 @@ +import pandas as pd +import datashader as ds +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) + 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