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 48b9ace..fe11570 100644 Binary files a/__pycache__/app.cpython-39.pyc and b/__pycache__/app.cpython-39.pyc differ 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/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/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/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 diff --git a/ui/feat_vs_anno_ui.py b/ui/feat_vs_anno_ui.py index e57aa27..8984f9e 100644 --- a/ui/feat_vs_anno_ui.py +++ b/ui/feat_vs_anno_ui.py @@ -36,20 +36,50 @@ 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( + "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") 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