diff --git a/config/README.md b/config/README.md index f5988a1..7dc080a 100644 --- a/config/README.md +++ b/config/README.md @@ -106,6 +106,8 @@ Parameters are settings that live exclusively in `config/config.yaml` and determ | `integration.integrate_key` | **Mandatory** | Variable Harmony corrects over during integration (e.g. `sample`). | | `extra_annotations.columns` | **Optional** | `core_samples.tsv` columns to surface in downstream plots (e.g. `[patient, batch]`). All sample-sheet columns are carried into `obs` and can be used by pseudobulk models whether or not they are plotted. | | `analysis.pseudobulk.analyses` | Pseudobulk | Named aggregation/model specifications containing the comparison variables, explicit contrasts, exclusions, optional pairing/covariates, and optional LRT. | +| `analysis.umap_point_size` | **Optional** | Marker size for ordinary UMAP embedding plots (default: `2`). Highlight and split UMAPs remain at least size `10`. | +| `analysis.spatial_point_size` | **Optional** | Marker size for spatial maps (default: `20`). This is independent of the UMAP marker size; coordinate-only spatial-niche maps are scaled proportionally. | ## 4. Color scale customization diff --git a/config/config.yaml b/config/config.yaml index d5a97c8..8123b70 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -92,6 +92,9 @@ per_sample_qc: '' analysis: # qc thresholds plot_dpi: 300 + # marker sizes for UMAP embeddings and spatial maps, respectively + umap_point_size: 2 + spatial_point_size: 20 min_counts: 1 min_cells: 3 min_genes: 100 diff --git a/workflow/rules/annotate_cells.smk b/workflow/rules/annotate_cells.smk index 94defc5..6530b56 100644 --- a/workflow/rules/annotate_cells.smk +++ b/workflow/rules/annotate_cells.smk @@ -32,6 +32,8 @@ rule annotate_cells: annotation_colors=config.get("annotation_colors", {}), region_colors=ANALYSIS.get("region_colors", {}), dpi=ANALYSIS.get("plot_dpi", 300), + umap_point_size=ANALYSIS.get("umap_point_size", 2), + spatial_point_size=ANALYSIS.get("spatial_point_size", 20), niche_column=GENE_EXPLORATION.get("niche_column", ""), script: "../scripts/annotate_cells.py" diff --git a/workflow/rules/explore_genes.smk b/workflow/rules/explore_genes.smk index 72c9264..0db4ae1 100644 --- a/workflow/rules/explore_genes.smk +++ b/workflow/rules/explore_genes.smk @@ -29,6 +29,7 @@ rule explore_genes_integrated: aucell_fraction=GENE_EXPLORATION.get("aucell_max_rank_fraction", 0.05), niche_column=GENE_EXPLORATION.get("niche_column", ""), dpi=GENE_EXPLORATION.get("dpi", ANALYSIS.get("plot_dpi", 300)), + umap_point_size=ANALYSIS.get("umap_point_size", 2), annotation_colors=config.get("annotation_colors", {}), region_colors=ANALYSIS.get("region_colors", {}), extra_annot_columns=EXTRA_ANNOT_COLUMNS, @@ -71,6 +72,7 @@ rule explore_genes_sample: niche_column=GENE_EXPLORATION.get("niche_column", ""), aucell_fraction=GENE_EXPLORATION.get("aucell_max_rank_fraction", 0.05), dpi=GENE_EXPLORATION.get("dpi", ANALYSIS.get("plot_dpi", 300)), + spatial_point_size=ANALYSIS.get("spatial_point_size", 20), annotation_colors=config.get("annotation_colors", {}), region_colors=ANALYSIS.get("region_colors", {}), script: diff --git a/workflow/rules/ingest_ref.smk b/workflow/rules/ingest_ref.smk index 2f7b569..a88d4d2 100644 --- a/workflow/rules/ingest_ref.smk +++ b/workflow/rules/ingest_ref.smk @@ -33,5 +33,7 @@ rule ingest_ref: ref_label_key=config.get("ingest_ref_label_key", "cell_type"), de_n_genes=ANALYSIS.get("de_n_genes", 10), annotation_colors=config.get("annotation_colors", {}), + umap_point_size=ANALYSIS.get("umap_point_size", 2), + spatial_point_size=ANALYSIS.get("spatial_point_size", 20), script: "../scripts/ingest_ref.py" diff --git a/workflow/rules/integrate_samples.smk b/workflow/rules/integrate_samples.smk index 120240e..dc2e16a 100644 --- a/workflow/rules/integrate_samples.smk +++ b/workflow/rules/integrate_samples.smk @@ -32,6 +32,7 @@ rule integrate_samples: annotation_colors=config.get("annotation_colors", {}), region_colors=ANALYSIS.get("region_colors", {}), dpi=ANALYSIS.get("plot_dpi", 300), + umap_point_size=ANALYSIS.get("umap_point_size", 2), niche_column=GENE_EXPLORATION.get("niche_column", ""), extra_annot_columns=EXTRA_ANNOT_COLUMNS, sample_colors=SAMPLE_COLORS, diff --git a/workflow/rules/leiden_analysis.smk b/workflow/rules/leiden_analysis.smk index 79581d7..0acee35 100644 --- a/workflow/rules/leiden_analysis.smk +++ b/workflow/rules/leiden_analysis.smk @@ -33,5 +33,7 @@ rule leiden_analysis: resolution_scan_step=ANALYSIS.get("resolution_scan_step", 0.1), annotation_colors=config.get("annotation_colors", {}), region_colors=ANALYSIS.get("region_colors", {}), + umap_point_size=ANALYSIS.get("umap_point_size", 2), + spatial_point_size=ANALYSIS.get("spatial_point_size", 20), script: "../scripts/leiden_analysis.py" diff --git a/workflow/rules/preprocess_umap.smk b/workflow/rules/preprocess_umap.smk index 654ddcf..44da7fe 100644 --- a/workflow/rules/preprocess_umap.smk +++ b/workflow/rules/preprocess_umap.smk @@ -41,5 +41,6 @@ rule preprocess_umap: "keep_unannotated", True ), region_colors=ANALYSIS.get("region_colors", {}), + spatial_point_size=ANALYSIS.get("spatial_point_size", 20), script: "../scripts/preprocess_umap.py" diff --git a/workflow/rules/qc_sweep.smk b/workflow/rules/qc_sweep.smk index 8e77c07..081a7c3 100644 --- a/workflow/rules/qc_sweep.smk +++ b/workflow/rules/qc_sweep.smk @@ -42,5 +42,6 @@ rule qc_sweep: ref_label_key=config.get("ingest_ref_label_key", "cell_type"), region_colors=config.get("analysis", {}).get("region_colors", {}), region_levels=config.get("analysis", {}).get("region_levels", []), + spatial_point_size=config.get("analysis", {}).get("spatial_point_size", 20), script: "../scripts/qc_sweep.py" diff --git a/workflow/rules/sample_report.smk b/workflow/rules/sample_report.smk index d8868c0..833a2a3 100644 --- a/workflow/rules/sample_report.smk +++ b/workflow/rules/sample_report.smk @@ -26,6 +26,8 @@ rule sample_report: annotation_colors=config.get("annotation_colors", {}), region_colors=ANALYSIS.get("region_colors", {}), dpi=ANALYSIS.get("plot_dpi", 300), + umap_point_size=ANALYSIS.get("umap_point_size", 2), + spatial_point_size=ANALYSIS.get("spatial_point_size", 20), niche_column=GENE_EXPLORATION.get("niche_column", ""), script: "../scripts/sample_report.py" diff --git a/workflow/rules/spatial_niches.smk b/workflow/rules/spatial_niches.smk index 60258c6..e25b89f 100644 --- a/workflow/rules/spatial_niches.smk +++ b/workflow/rules/spatial_niches.smk @@ -54,5 +54,7 @@ rule spatial_niches: annotation_colors=config.get("annotation_colors", {}), region_colors=ANALYSIS.get("region_colors", {}), dpi=ANALYSIS.get("plot_dpi", 300), + umap_point_size=ANALYSIS.get("umap_point_size", 2), + spatial_point_size=ANALYSIS.get("spatial_point_size", 20), script: "../scripts/spatial_niches.py" diff --git a/workflow/rules/subcluster.smk b/workflow/rules/subcluster.smk index d0f0e01..2cca051 100644 --- a/workflow/rules/subcluster.smk +++ b/workflow/rules/subcluster.smk @@ -46,6 +46,7 @@ rule subcluster: region_colors=ANALYSIS.get("region_colors", {}), region_levels=REGION_LEVELS, dpi=ANALYSIS.get("plot_dpi", 300), + umap_point_size=ANALYSIS.get("umap_point_size", 2), niche_column=GENE_EXPLORATION.get("niche_column", ""), extra_annot_columns=EXTRA_ANNOT_COLUMNS, sample_colors=SAMPLE_COLORS, diff --git a/workflow/schemas/config.schema.yaml b/workflow/schemas/config.schema.yaml index c4f95dc..0dd450d 100644 --- a/workflow/schemas/config.schema.yaml +++ b/workflow/schemas/config.schema.yaml @@ -448,6 +448,16 @@ properties: type: integer minimum: 1 description: Default figure DPI. + umap_point_size: + type: number + exclusiveMinimum: 0 + default: 2 + description: Marker size for ordinary UMAP embedding plots. + spatial_point_size: + type: number + exclusiveMinimum: 0 + default: 20 + description: Marker size for spatial maps. ingest_ref: type: string diff --git a/workflow/scripts/annotate_cells.py b/workflow/scripts/annotate_cells.py index 9a495fd..38ffbe6 100644 --- a/workflow/scripts/annotate_cells.py +++ b/workflow/scripts/annotate_cells.py @@ -93,7 +93,7 @@ def generate_annotation_plots(adata, annot_key, label, plots_dir, sample_id, # UMAP log.info(" [%s] UMAP …", label) - sc.pl.umap(adata, color=[annot_key], size=2, wspace=0.25, frameon=False, + sc.pl.umap(adata, color=[annot_key], size=UMAP_POINT_SIZE, wspace=0.25, frameon=False, title=f"Cell types ({label})") plt.savefig(os.path.join(subdir, f"UMAP_{label}_{sample_id}.png"), dpi=DPI, bbox_inches="tight") @@ -101,7 +101,7 @@ def generate_annotation_plots(adata, annot_key, label, plots_dir, sample_id, # Spatial overview try: - sc.pl.spatial(adata, color=annot_key, spot_size=20, frameon=False, + sc.pl.spatial(adata, color=annot_key, spot_size=SPATIAL_POINT_SIZE, frameon=False, title=f"Spatial – {label}", library_id=library_id) plt.savefig(os.path.join(subdir, f"spatial_all_{label}_{sample_id}.png"), dpi=DPI, bbox_inches="tight") @@ -119,7 +119,7 @@ def generate_annotation_plots(adata, annot_key, label, plots_dir, sample_id, lambda x, c=ct: c if x == c else "Other" ) try: - sc.pl.spatial(adata, color="_hl", spot_size=20, frameon=False, + sc.pl.spatial(adata, color="_hl", spot_size=SPATIAL_POINT_SIZE, frameon=False, palette={"Other": "#d3d3d3", ct: "#000000"}, title=ct, library_id=library_id) safe = ct.replace("/", "_").replace(" ", "_") @@ -179,6 +179,8 @@ def generate_annotation_plots(adata, annot_key, label, plots_dir, sample_id, ANNOTATION_COLORS = snakemake.params.annotation_colors REGION_COLORS = snakemake.params.region_colors DPI = int(getattr(snakemake.params, "dpi", 300)) +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) +SPATIAL_POINT_SIZE = float(getattr(snakemake.params, "spatial_point_size", 20)) NICHE_COLUMN = getattr(snakemake.params, "niche_column", "") adata_path = str(snakemake.input.adata) @@ -397,7 +399,7 @@ def generate_annotation_plots(adata, annot_key, label, plots_dir, sample_id, fig, axes = plt.subplots(1, n_panels, figsize=(8 * n_panels, 6), gridspec_kw={"wspace": 0.5}) for ax, col in zip(axes, annot_cols_present): - sc.pl.umap(adata, color=col, size=2, frameon=False, + sc.pl.umap(adata, color=col, size=UMAP_POINT_SIZE, frameon=False, title=col.replace("cell_type_", ""), ax=ax, show=False, legend_fontsize=6, na_in_legend=False) plt.savefig(os.path.join(plots_dir, f"UMAP_comparison_{sample_id}.png"), @@ -410,7 +412,7 @@ def generate_annotation_plots(adata, annot_key, label, plots_dir, sample_id, fig, axes = plt.subplots(1, n_panels, figsize=(8 * n_panels, 6), gridspec_kw={"wspace": 0.5}) for ax, col in zip(axes, annot_cols_present): - sc.pl.spatial(adata, color=col, spot_size=20, frameon=False, + sc.pl.spatial(adata, color=col, spot_size=SPATIAL_POINT_SIZE, frameon=False, title=col.replace("cell_type_", ""), library_id=library_id, ax=ax, show=False, legend_fontsize=6, na_in_legend=False) diff --git a/workflow/scripts/explore_genes_integrated.py b/workflow/scripts/explore_genes_integrated.py index 51d8de1..84876cf 100644 --- a/workflow/scripts/explore_genes_integrated.py +++ b/workflow/scripts/explore_genes_integrated.py @@ -206,14 +206,14 @@ def generate_umap_composite(adata, color_col, annot_key, has_regions, out_path, gridspec_kw={"wspace": 0.5}) try: - sc.pl.umap(adata, color=color_col, size=2, frameon=False, + sc.pl.umap(adata, color=color_col, size=UMAP_POINT_SIZE, frameon=False, vmin=vmin, vmax=vmax, cmap="viridis", title=title, ax=axes[0], show=False) except Exception as e: log.warning(" UMAP %s failed: %s", title, e) try: - sc.pl.umap(adata, color=annot_key, size=2, frameon=False, + sc.pl.umap(adata, color=annot_key, size=UMAP_POINT_SIZE, frameon=False, title="Cell types", legend_fontsize=6, na_in_legend=False, ax=axes[1], show=False) @@ -223,7 +223,7 @@ def generate_umap_composite(adata, color_col, annot_key, has_regions, out_path, idx = 2 if has_regions: try: - sc.pl.umap(adata, color="region_annotation", size=2, frameon=False, + sc.pl.umap(adata, color="region_annotation", size=UMAP_POINT_SIZE, frameon=False, title="Regions", legend_fontsize=6, na_in_legend=False, ax=axes[idx], show=False) @@ -239,7 +239,7 @@ def generate_umap_composite(adata, color_col, annot_key, has_regions, out_path, if niche_palette: adata.uns[f"{niche_col}_colors"] = [ niche_palette.get(str(c), "#cccccc") for c in cats] - sc.pl.umap(adata, color=niche_col, size=2, frameon=False, + sc.pl.umap(adata, color=niche_col, size=UMAP_POINT_SIZE, frameon=False, title="Spatial niches", legend_fontsize=5, na_in_legend=False, ax=axes[idx], show=False) _compact_legend(axes[idx], title="Niche") @@ -261,6 +261,7 @@ def generate_umap_composite(adata, color_col, annot_key, has_regions, out_path, AUCELL_FRACTION = float(snakemake.params.aucell_fraction) NICHE_COLUMN = str(snakemake.params.niche_column) if snakemake.params.niche_column else "" DPI = int(snakemake.params.dpi) +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) ANNOTATION_COLORS = snakemake.params.annotation_colors REGION_COLORS = snakemake.params.region_colors EXTRA_ANNOT_COLUMNS = list(getattr(snakemake.params, "extra_annot_columns", []) or []) @@ -299,7 +300,7 @@ def generate_umap_composite(adata, color_col, annot_key, has_regions, out_path, adata.uns[f"{_dc}_colors"] = [pal.get(str(c), "#cccccc") for c in cats] _dd = os.path.join(base_dir, "_design") os.makedirs(_dd, exist_ok=True) - sc.pl.umap(adata, color=[_dc], size=2, frameon=False, + sc.pl.umap(adata, color=[_dc], size=UMAP_POINT_SIZE, frameon=False, title=f"Integrated – by {_dc}", show=False) plt.savefig(os.path.join(_dd, f"UMAP_by_{_dc}.png"), dpi=DPI, bbox_inches="tight") diff --git a/workflow/scripts/explore_genes_sample.py b/workflow/scripts/explore_genes_sample.py index b58d98e..f6de536 100644 --- a/workflow/scripts/explore_genes_sample.py +++ b/workflow/scripts/explore_genes_sample.py @@ -78,7 +78,7 @@ def generate_spatial_composite(adata, color_col, annot_key, has_regions, # (1) Expression / AUCell try: - sc.pl.spatial(adata, color=color_col, spot_size=20, frameon=False, + sc.pl.spatial(adata, color=color_col, spot_size=SPATIAL_POINT_SIZE, frameon=False, vmin=vmin, vmax=vmax, cmap="viridis", title=title, library_id=library_id, ax=axes[0], show=False) @@ -88,7 +88,7 @@ def generate_spatial_composite(adata, color_col, annot_key, has_regions, # (2) Cell type try: - sc.pl.spatial(adata, color=annot_key, spot_size=20, frameon=False, + sc.pl.spatial(adata, color=annot_key, spot_size=SPATIAL_POINT_SIZE, frameon=False, title="Cell types", library_id=library_id, legend_fontsize=6, na_in_legend=False, ax=axes[1], show=False) @@ -100,7 +100,7 @@ def generate_spatial_composite(adata, color_col, annot_key, has_regions, # (3) Region if has_regions: try: - sc.pl.spatial(adata, color="region_annotation", spot_size=20, + sc.pl.spatial(adata, color="region_annotation", spot_size=SPATIAL_POINT_SIZE, frameon=False, title="Regions", library_id=library_id, legend_fontsize=6, na_in_legend=False, @@ -120,7 +120,7 @@ def generate_spatial_composite(adata, color_col, annot_key, has_regions, if niche_palette: adata.uns[f"{niche_col}_colors"] = [ niche_palette.get(str(c), "#cccccc") for c in cats] - sc.pl.spatial(adata, color=niche_col, spot_size=20, frameon=False, + sc.pl.spatial(adata, color=niche_col, spot_size=SPATIAL_POINT_SIZE, frameon=False, title="Spatial niches", library_id=library_id, legend_fontsize=5, na_in_legend=False, ax=axes[idx], show=False) @@ -235,6 +235,7 @@ def generate_dotplot_composite(adata, var_names, annot_key, has_regions, ANNOT_KEY = str(snakemake.params.annot_key) AUCELL_FRACTION = float(snakemake.params.aucell_fraction) DPI = int(snakemake.params.dpi) +SPATIAL_POINT_SIZE = float(getattr(snakemake.params, "spatial_point_size", 20)) ANNOTATION_COLORS = snakemake.params.annotation_colors REGION_COLORS = snakemake.params.region_colors NICHE_COLUMN = str(getattr(snakemake.params, "niche_column", "") or "") diff --git a/workflow/scripts/ingest_ref.py b/workflow/scripts/ingest_ref.py index 480b217..e4b5f24 100644 --- a/workflow/scripts/ingest_ref.py +++ b/workflow/scripts/ingest_ref.py @@ -47,6 +47,8 @@ ref_label_key = snakemake.params.ref_label_key DE_N_GENES = int(snakemake.params.de_n_genes) ANNOTATION_COLORS = snakemake.params.annotation_colors +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) +SPATIAL_POINT_SIZE = float(getattr(snakemake.params, "spatial_point_size", 20)) adata_path = str(snakemake.input.adata) ref_path = str(snakemake.input.ingest_ref) @@ -139,7 +141,7 @@ library_id = list(adata.uns["spatial"].keys())[0] # UMAP (original embedding) coloured by ingest labels - sc.pl.umap(adata, color=["cell_type_ingest"], size=2, frameon=False, + sc.pl.umap(adata, color=["cell_type_ingest"], size=UMAP_POINT_SIZE, frameon=False, title="Ingest annotation (original UMAP)") plt.savefig(os.path.join(plots_dir, f"UMAP_ingest_{sample_id}.png"), dpi=300, bbox_inches="tight") @@ -148,7 +150,7 @@ # UMAP from ingest projection adata.obsm["X_umap_orig"] = adata.obsm["X_umap"].copy() adata.obsm["X_umap"] = adata.obsm["X_umap_ingest"] - sc.pl.umap(adata, color=["cell_type_ingest"], size=2, frameon=False, + sc.pl.umap(adata, color=["cell_type_ingest"], size=UMAP_POINT_SIZE, frameon=False, title="Ingest annotation (projected UMAP)") plt.savefig(os.path.join(plots_dir, f"UMAP_ingest_projected_{sample_id}.png"), dpi=300, bbox_inches="tight") @@ -159,7 +161,7 @@ # Spatial overview try: - sc.pl.spatial(adata, color="cell_type_ingest", spot_size=20, + sc.pl.spatial(adata, color="cell_type_ingest", spot_size=SPATIAL_POINT_SIZE, frameon=False, title="Ingest – spatial", library_id=library_id) plt.savefig(os.path.join(plots_dir, f"spatial_ingest_{sample_id}.png"), @@ -176,7 +178,7 @@ lambda x, c=ct: c if x == c else "Other" ) try: - sc.pl.spatial(adata, color="_hl", spot_size=20, frameon=False, + sc.pl.spatial(adata, color="_hl", spot_size=SPATIAL_POINT_SIZE, frameon=False, palette={"Other": "#d3d3d3", ct: "#e41a1c"}, title=ct, library_id=library_id) safe = ct.replace("/", "_").replace(" ", "_") diff --git a/workflow/scripts/integrate_samples.py b/workflow/scripts/integrate_samples.py index fd3ddb2..d2d6e97 100644 --- a/workflow/scripts/integrate_samples.py +++ b/workflow/scripts/integrate_samples.py @@ -66,6 +66,7 @@ ANNOTATION_COLORS = snakemake.params.annotation_colors REGION_COLORS = snakemake.params.region_colors DPI = int(getattr(snakemake.params, "dpi", 300)) +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) NICHE_COLUMN = getattr(snakemake.params, "niche_column", "") EXTRA_ANNOT_COLUMNS = list(getattr(snakemake.params, "extra_annot_columns", []) or []) SAMPLE_COLORS = getattr(snakemake.params, "sample_colors", {}) or {} @@ -112,7 +113,7 @@ def _niche_umap(ad, title, out_path): if isinstance(ANNOTATION_COLORS, dict) else {}) pal = build_niche_palette(cats, niche_cfg) ad.uns[f"{niche_col}_colors"] = [pal.get(str(c), "#cccccc") for c in cats] - sc.pl.umap(ad, color=[niche_col], size=2, frameon=False, title=title) + sc.pl.umap(ad, color=[niche_col], size=UMAP_POINT_SIZE, frameon=False, title=title) plt.savefig(out_path, dpi=DPI, bbox_inches="tight") plt.close() except Exception as e: @@ -176,14 +177,14 @@ def _niche_umap(ad, title, out_path): adata.uns[f"{obs_key}_colors"] = [cd.get(str(c), "#cccccc") for c in cats] # Plot uncorrected - sc.pl.umap(adata, color=["sample_batch"], size=2, frameon=False, + sc.pl.umap(adata, color=["sample_batch"], size=UMAP_POINT_SIZE, frameon=False, title="Uncorrected – by sample") plt.savefig(os.path.join(output_dir, "UMAP_uncorrected_by_sample.png"), dpi=DPI, bbox_inches="tight") plt.close() if _has_real_annotation(adata, "cell_type_tsv"): - sc.pl.umap(adata, color=["cell_type_tsv"], size=2, frameon=False, + sc.pl.umap(adata, color=["cell_type_tsv"], size=UMAP_POINT_SIZE, frameon=False, title="Uncorrected – by cell type (TSV)") plt.savefig(os.path.join(output_dir, "UMAP_uncorrected_by_celltype.png"), dpi=DPI, bbox_inches="tight") @@ -193,7 +194,7 @@ def _niche_umap(ad, title, out_path): for _dc in EXTRA_ANNOT_COLUMNS: if _apply_design_palette(adata, _dc): try: - sc.pl.umap(adata, color=[_dc], size=2, frameon=False, + sc.pl.umap(adata, color=[_dc], size=UMAP_POINT_SIZE, frameon=False, title=f"Uncorrected – by {_dc}") plt.savefig(os.path.join(output_dir, f"UMAP_uncorrected_by_{_dc}.png"), dpi=DPI, bbox_inches="tight") @@ -233,14 +234,14 @@ def _niche_umap(ad, title, out_path): adata_harmony.uns[key] = adata.uns[key] # Plots - sc.pl.umap(adata_harmony, color=["sample_batch"], size=2, frameon=False, + sc.pl.umap(adata_harmony, color=["sample_batch"], size=UMAP_POINT_SIZE, frameon=False, title="Harmony – by sample") plt.savefig(os.path.join(output_dir, "UMAP_harmony_by_sample.png"), dpi=DPI, bbox_inches="tight") plt.close() if _has_real_annotation(adata_harmony, "cell_type_tsv"): - sc.pl.umap(adata_harmony, color=["cell_type_tsv"], size=2, frameon=False, + sc.pl.umap(adata_harmony, color=["cell_type_tsv"], size=UMAP_POINT_SIZE, frameon=False, title="Harmony – by cell type (TSV)") plt.savefig(os.path.join(output_dir, "UMAP_harmony_by_celltype.png"), dpi=DPI, bbox_inches="tight") @@ -250,7 +251,7 @@ def _niche_umap(ad, title, out_path): for _dc in EXTRA_ANNOT_COLUMNS: if _apply_design_palette(adata_harmony, _dc): try: - sc.pl.umap(adata_harmony, color=[_dc], size=2, frameon=False, + sc.pl.umap(adata_harmony, color=[_dc], size=UMAP_POINT_SIZE, frameon=False, title=f"Harmony – by {_dc}") plt.savefig(os.path.join(output_dir, f"UMAP_harmony_by_{_dc}.png"), dpi=DPI, bbox_inches="tight") @@ -392,13 +393,13 @@ def _niche_umap(ad, title, out_path): adata_ingested.write(out_sketched) # Plot - sc.pl.umap(adata_ingested, color=["clusters"], size=2, frameon=False, + sc.pl.umap(adata_ingested, color=["clusters"], size=UMAP_POINT_SIZE, frameon=False, title="Geosketch – ingested clusters") plt.savefig(os.path.join(output_dir, "UMAP_sketched_clusters.png"), dpi=DPI, bbox_inches="tight") plt.close() - sc.pl.umap(adata_ingested, color=["sample_batch"], size=2, frameon=False, + sc.pl.umap(adata_ingested, color=["sample_batch"], size=UMAP_POINT_SIZE, frameon=False, title="Geosketch – by sample") plt.savefig(os.path.join(output_dir, "UMAP_sketched_by_sample.png"), dpi=DPI, bbox_inches="tight") diff --git a/workflow/scripts/leiden_analysis.py b/workflow/scripts/leiden_analysis.py index 708c12d..b745123 100644 --- a/workflow/scripts/leiden_analysis.py +++ b/workflow/scripts/leiden_analysis.py @@ -52,6 +52,8 @@ ANNOTATION_COLORS = snakemake.params.annotation_colors REGION_COLORS = snakemake.params.region_colors DPI = int(getattr(snakemake.params, "dpi", 300)) +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) +SPATIAL_POINT_SIZE = float(getattr(snakemake.params, "spatial_point_size", 20)) def read_tsv_to_dict(tsv_path): @@ -193,7 +195,7 @@ def _set_palette(adata, obs_key, colors_cfg): sc.pl.umap( adata, color=["n_genes_by_counts", "total_counts", "pct_counts_mt", "pct_counts_hb"], - size=2, wspace=0.25, frameon=False, + size=UMAP_POINT_SIZE, wspace=0.25, frameon=False, ) plt.savefig(os.path.join(qc_dir, f"QC_UMAPs_{sample_id}.png"), dpi=DPI, bbox_inches="tight") @@ -213,18 +215,20 @@ def _set_palette(adata, obs_key, colors_cfg): # ── UMAPs ──────────────────────────────────────────────────────────── log.info("UMAP plots …") - sc.pl.umap(adata, color=["leiden"], size=2, wspace=0.25, frameon=False) + sc.pl.umap(adata, color=["leiden"], size=UMAP_POINT_SIZE, wspace=0.25, frameon=False) plt.savefig(os.path.join(res_dir, f"UMAP_clusters_{sample_id}.png"), dpi=DPI, bbox_inches="tight") plt.close() if has_annotations: - sc.pl.umap(adata, color=["region_annotation"], size=2, wspace=0.25, frameon=False) + sc.pl.umap(adata, color=["region_annotation"], size=UMAP_POINT_SIZE, + wspace=0.25, frameon=False) plt.savefig(os.path.join(umap_dir, f"UMAP_region_{sample_id}.png"), dpi=DPI, bbox_inches="tight") plt.close() - split_umap(adata, color="leiden", split_by="region_annotation", size=10) + split_umap(adata, color="leiden", split_by="region_annotation", + size=max(UMAP_POINT_SIZE, 10.0)) plt.savefig(os.path.join(umap_dir, f"UMAP_split_{sample_id}.png"), dpi=DPI, bbox_inches="tight") plt.close() @@ -245,7 +249,8 @@ def _set_palette(adata, obs_key, colors_cfg): # ── Spatial cluster maps ───────────────────────────────────────────── log.info("Spatial plots …") - sc.pl.spatial(adata, color="leiden", spot_size=20, title="Leiden Clusters", frameon=False) + sc.pl.spatial(adata, color="leiden", spot_size=SPATIAL_POINT_SIZE, + title="Leiden Clusters", frameon=False) plt.savefig(os.path.join(res_dir, f"spatial_all_{sample_id}.png"), dpi=DPI, bbox_inches="tight") plt.close() @@ -253,7 +258,7 @@ def _set_palette(adata, obs_key, colors_cfg): for cluster in adata.obs["leiden"].unique(): adata_sub = adata[adata.obs["leiden"] == cluster, :] sc.pl.spatial( - adata_sub, color="leiden", spot_size=20, + adata_sub, color="leiden", spot_size=SPATIAL_POINT_SIZE, title=f"Cluster {cluster}", frameon=False, palette=["black"], alpha_img=0.5, ) @@ -269,7 +274,7 @@ def _set_palette(adata, obs_key, colors_cfg): categories=["Other", str(cluster)], ) sc.pl.umap( - adata, color="_highlight", size=2, wspace=0.25, frameon=False, + adata, color="_highlight", size=UMAP_POINT_SIZE, wspace=0.25, frameon=False, palette={"Other": "gray", str(cluster): "red"}, title=f"Cluster {cluster}", ) @@ -327,7 +332,7 @@ def _set_palette(adata, obs_key, colors_cfg): plt.close() for gene in genes: - sc.pl.umap(adata, color=gene, show=False) + sc.pl.umap(adata, color=gene, size=UMAP_POINT_SIZE, show=False) plt.savefig(os.path.join(ct_dir, f"UMAP_{gene}.png"), dpi=DPI, bbox_inches="tight") plt.close() diff --git a/workflow/scripts/preprocess_umap.py b/workflow/scripts/preprocess_umap.py index 3ba58b3..2a579cb 100644 --- a/workflow/scripts/preprocess_umap.py +++ b/workflow/scripts/preprocess_umap.py @@ -58,6 +58,7 @@ RANDOM_SEED = int(snakemake.params.random_seed) USE_PRECOMPUTED = bool(snakemake.params.use_precomputed) REGION_COLORS = snakemake.params.region_colors +SPATIAL_POINT_SIZE = float(getattr(snakemake.params, "spatial_point_size", 20)) # Mitochondrial gene prefixes. Honour contract.mito_prefix when the rule passes it, # else default to human + mouse. The old hardcoded "MT-" silently disabled the # max_pct_mt filter on mouse ("mt-") data, even though the config advertises the key. @@ -247,7 +248,7 @@ def _range(series): # the scalefactor-derived Visium spot size, so without this the dots render # invisibly and only the tissue image shows); add library_id to draw them # over the embedded image when one is present. - spatial_kw = {"spot_size": 20} + spatial_kw = {"spot_size": SPATIAL_POINT_SIZE} if isinstance(adata.uns.get("spatial"), dict) and adata.uns["spatial"]: spatial_kw["library_id"] = list(adata.uns["spatial"].keys())[0] sc.pl.spatial(adata, color="region_annotation", title="Annotated Regions", diff --git a/workflow/scripts/qc_sweep.py b/workflow/scripts/qc_sweep.py index 050ac1a..b394a12 100644 --- a/workflow/scripts/qc_sweep.py +++ b/workflow/scripts/qc_sweep.py @@ -46,6 +46,7 @@ ref_label_key = str(getattr(snakemake.params, "ref_label_key", "cell_type")) region_colors = dict(getattr(snakemake.params, "region_colors", {}) or {}) region_levels = list(getattr(snakemake.params, "region_levels", []) or []) + spatial_point_size = float(getattr(snakemake.params, "spatial_point_size", 20)) out = snakemake.output out_dir = os.path.dirname(str(out.violins_png)) @@ -96,7 +97,7 @@ # (cell centroids are far smaller than the scalefactor-derived Visium spot size, # so without it the dots render invisibly and only the tissue image shows); add # library_id to draw the dots over the embedded image when one is present. - spatial_kw = {"spot_size": 20} + spatial_kw = {"spot_size": spatial_point_size} if isinstance(adata.uns.get("spatial"), dict) and adata.uns["spatial"]: spatial_kw["library_id"] = list(adata.uns["spatial"].keys())[0] has_coords = "spatial" in adata.obsm diff --git a/workflow/scripts/sample_report.py b/workflow/scripts/sample_report.py index a5e3d09..d12fabe 100644 --- a/workflow/scripts/sample_report.py +++ b/workflow/scripts/sample_report.py @@ -132,13 +132,13 @@ def _build_page1(adata, sample_id, keys, library_id, has_regions=False): fig.suptitle(f"Sample: {sample_id}", fontsize=18, fontweight="bold", y=0.99) for j, (key, title) in enumerate(panels): try: - sc.pl.umap(adata, color=key, size=2, frameon=False, title=title, + sc.pl.umap(adata, color=key, size=UMAP_POINT_SIZE, frameon=False, title=title, legend_fontsize=6, na_in_legend=False, ax=axes[0, j], show=False) except Exception as e: log.warning(" UMAP %s failed: %s", title, e); axes[0, j].set_title(f"{title} (failed)") try: - sc.pl.spatial(adata, color=key, spot_size=20, frameon=False, + sc.pl.spatial(adata, color=key, spot_size=SPATIAL_POINT_SIZE, frameon=False, title=title, library_id=library_id, legend_fontsize=6, na_in_legend=False, ax=axes[1, j], show=False) @@ -307,6 +307,8 @@ def _build_page2(adata, sample_id, keys, annotation_colors, sample_col, ANNOTATION_COLORS = snakemake.params.annotation_colors REGION_COLORS = snakemake.params.region_colors DPI = int(getattr(snakemake.params, "dpi", 300)) +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) +SPATIAL_POINT_SIZE = float(getattr(snakemake.params, "spatial_point_size", 20)) NICHE_COLUMN = getattr(snakemake.params, "niche_column", "") out_report = str(snakemake.output.report) diff --git a/workflow/scripts/spatial_niches.py b/workflow/scripts/spatial_niches.py index 2143aa3..430e015 100644 --- a/workflow/scripts/spatial_niches.py +++ b/workflow/scripts/spatial_niches.py @@ -79,7 +79,9 @@ REFINE_AUTO = bool(getattr(snakemake.params, "refine_auto", False)) ANNOTATION_COLORS = snakemake.params.annotation_colors REGION_COLORS = snakemake.params.region_colors -DPI = int(getattr(snakemake.params, "dpi", 300)) +DPI = int(getattr(snakemake.params, "dpi", 300)) +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) +SPATIAL_POINT_SIZE = float(getattr(snakemake.params, "spatial_point_size", 20)) out_concat = str(snakemake.output.concatenated) niche_tsvs = [str(p) for p in snakemake.output.niche_tsvs] @@ -89,6 +91,17 @@ COORD_KEYS = ("x_pixel", "y_pixel", "coord_xy") # BANKSY reads obsm[coord_keys[2]] +def _spatial_scatter_area(default_area): + """Scale Matplotlib scatter area with the configured spatial diameter. + + ``sc.pl.spatial`` interprets ``spot_size`` as a diameter in coordinate units, + whereas Matplotlib's ``s`` is an area in points squared. Squaring the ratio + keeps their visible diameters proportional and preserves the existing areas + when ``spatial_point_size`` retains its default value of 20. + """ + return default_area * (SPATIAL_POINT_SIZE / 20.0) ** 2 + + def harmony_embedding(emb, obs, batch_key, seed): """Harmony-correct a PCA embedding directly (not scanpy's wrapper, which hard-codes a Z_corr transpose that breaks across harmonypy versions). @@ -159,8 +172,10 @@ def _spatial_scatter(adata, sample_key, color_key, out_path, dpi, color_map): for c in cats: mm = vals == c if mm.any(): - ax.scatter(sp[mm, 0], sp[mm, 1], s=2, linewidths=0, - color=colour[c], rasterized=True) + ax.scatter( + sp[mm, 0], sp[mm, 1], s=_spatial_scatter_area(2), + linewidths=0, color=colour[c], rasterized=True, + ) ax.set_title(str(s), fontsize=9) ax.set_aspect("equal") ax.invert_yaxis() @@ -194,11 +209,15 @@ def _spatial_scatter_highlight(adata, sample_key, color_key, target, out_path, d v = vals_all[m] bg, hi = v != str(target), v == str(target) if bg.any(): - ax.scatter(sp[bg, 0], sp[bg, 1], s=2, linewidths=0, - color=bg_color, rasterized=True) + ax.scatter( + sp[bg, 0], sp[bg, 1], s=_spatial_scatter_area(2), + linewidths=0, color=bg_color, rasterized=True, + ) if hi.any(): - ax.scatter(sp[hi, 0], sp[hi, 1], s=2, linewidths=0, - color=hi_color, rasterized=True) + ax.scatter( + sp[hi, 0], sp[hi, 1], s=_spatial_scatter_area(2), + linewidths=0, color=hi_color, rasterized=True, + ) ax.set_title(str(s), fontsize=9) ax.set_aspect("equal") ax.invert_yaxis() @@ -482,7 +501,8 @@ def _leiden(res, key): xy = adata.obsm[COORD_KEYS[2]] codes = pd.Categorical(adata.obs["sample"]).codes fig, ax = plt.subplots(figsize=(min(40, 4 * len(sample_ids)), 5)) - ax.scatter(xy[:, 0], xy[:, 1], c=codes, cmap="tab20", s=1, + ax.scatter(xy[:, 0], xy[:, 1], c=codes, cmap="tab20", + s=_spatial_scatter_area(1), linewidths=0, rasterized=True) ax.set_aspect("equal"); ax.invert_yaxis(); ax.set_axis_off() ax.set_title("Staggered coordinates (coloured by sample)", fontsize=10) @@ -497,7 +517,8 @@ def _leiden(res, key): xy = adata.obsm[COORD_KEYS[2]] colors = adata.obs[NICHE_KEY].astype(str).map(niche_palette).values fig, ax = plt.subplots(figsize=(min(40, 4 * len(sample_ids)), 5)) - ax.scatter(xy[:, 0], xy[:, 1], c=colors, s=1, + ax.scatter(xy[:, 0], xy[:, 1], c=colors, + s=_spatial_scatter_area(1), linewidths=0, rasterized=True) ax.set_aspect("equal"); ax.invert_yaxis(); ax.set_axis_off() ax.set_title("BANKSY spatial niches (staggered, all samples)", fontsize=10) @@ -516,7 +537,8 @@ def _leiden(res, key): for color, fname in [(NICHE_KEY, "umap_by_niche.png"), ("sample", "umap_by_sample.png")]: try: - sc.pl.embedding(adata, basis="X_umap_banksy", color=color, size=3, + sc.pl.embedding(adata, basis="X_umap_banksy", color=color, + size=UMAP_POINT_SIZE, frameon=False, show=False, title=f"BANKSY (Harmony) – {color}") plt.savefig(os.path.join(plots_dir, fname), dpi=DPI, diff --git a/workflow/scripts/subcluster.py b/workflow/scripts/subcluster.py index 19a3bab..ea01cd4 100644 --- a/workflow/scripts/subcluster.py +++ b/workflow/scripts/subcluster.py @@ -318,7 +318,7 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, qc_cols = [c for c in ["n_genes_by_counts", "total_counts", "pct_counts_mt", "pct_counts_hb"] if c in adata.obs.columns] if qc_cols: - sc.pl.umap(adata, color=qc_cols, size=2, wspace=0.25, frameon=False) + sc.pl.umap(adata, color=qc_cols, size=UMAP_POINT_SIZE, wspace=0.25, frameon=False) plt.savefig(os.path.join(qc_dir, "QC_UMAPs.png"), dpi=DPI, bbox_inches="tight") plt.close() @@ -358,21 +358,21 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, adata.uns[f"{annot_col}_colors"] = [cd.get(str(c), "#cccccc") for c in cats] for res, key in zip(resolutions, leiden_keys): - sc.pl.umap(adata, color=[key], size=2, wspace=0.25, frameon=False, + sc.pl.umap(adata, color=[key], size=UMAP_POINT_SIZE, wspace=0.25, frameon=False, title=f"Leiden {res}") plt.savefig(os.path.join(umap_dir, f"UMAP_clusters_res{res}.png"), dpi=DPI, bbox_inches="tight") plt.close() if sample_col: - sc.pl.umap(adata, color=[sample_col], size=2, wspace=0.25, frameon=False, + sc.pl.umap(adata, color=[sample_col], size=UMAP_POINT_SIZE, wspace=0.25, frameon=False, title="By sample") plt.savefig(os.path.join(umap_dir, "UMAP_by_sample.png"), dpi=DPI, bbox_inches="tight") plt.close() if annot_col in adata.obs.columns: - sc.pl.umap(adata, color=[annot_col], size=2, wspace=0.25, frameon=False, + sc.pl.umap(adata, color=[annot_col], size=UMAP_POINT_SIZE, wspace=0.25, frameon=False, title=f"Original annotation ({annot_col})") plt.savefig(os.path.join(umap_dir, f"UMAP_by_{annot_col}.png"), dpi=DPI, bbox_inches="tight") @@ -391,7 +391,8 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, adata.obs["region_annotation"].astype(str), categories=ordered) adata.uns["region_annotation_colors"] = [REGION_COLORS.get(str(c), "#cccccc") for c in ordered] - sc.pl.umap(adata, color=["region_annotation"], size=2, wspace=0.25, frameon=False, + sc.pl.umap(adata, color=["region_annotation"], size=UMAP_POINT_SIZE, + wspace=0.25, frameon=False, title="By region") plt.savefig(os.path.join(umap_dir, "UMAP_by_region.png"), dpi=DPI, bbox_inches="tight") @@ -405,7 +406,8 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, cats = adata.obs[_dc].cat.categories pal = sample_colors.get(_dc, {}) if isinstance(sample_colors, dict) else {} adata.uns[f"{_dc}_colors"] = [pal.get(str(c), "#cccccc") for c in cats] - sc.pl.umap(adata, color=[_dc], size=2, wspace=0.25, frameon=False, + sc.pl.umap(adata, color=[_dc], size=UMAP_POINT_SIZE, + wspace=0.25, frameon=False, title=f"By {_dc}") plt.savefig(os.path.join(umap_dir, f"UMAP_by_{_dc}.png"), dpi=DPI, bbox_inches="tight") @@ -423,7 +425,8 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, if isinstance(ANNOTATION_COLORS, dict) else {}) _npal = build_niche_palette(_ncats, _ncfg) adata.uns[f"{_niche_col}_colors"] = [_npal.get(str(c), "#cccccc") for c in _ncats] - sc.pl.umap(adata, color=[_niche_col], size=2, wspace=0.25, frameon=False, + sc.pl.umap(adata, color=[_niche_col], size=UMAP_POINT_SIZE, + wspace=0.25, frameon=False, title="By spatial niche") plt.savefig(os.path.join(umap_dir, "UMAP_by_spatial_niche.png"), dpi=DPI, bbox_inches="tight") @@ -440,7 +443,7 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, try: adata.obs[sample_col] = adata.obs[sample_col].astype("category") split_umap(adata, color=split_leiden_key, split_by=sample_col, - size=10, ncol=4) + size=max(UMAP_POINT_SIZE, 10.0), ncol=4) plt.savefig(os.path.join(umap_dir, "UMAP_split_by_sample.png"), dpi=DPI, bbox_inches="tight") plt.close() @@ -450,7 +453,7 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, if has_regions: try: split_umap(adata, color=split_leiden_key, split_by="region_annotation", - size=10, ncol=3) + size=max(UMAP_POINT_SIZE, 10.0), ncol=3) plt.savefig(os.path.join(umap_dir, "UMAP_split_by_region.png"), dpi=DPI, bbox_inches="tight") plt.close() @@ -503,6 +506,7 @@ def run_clustering_branch(adata, branch_name, branch_dir, resolutions, REGION_COLORS = snakemake.params.region_colors REGION_LEVELS = list(getattr(snakemake.params, "region_levels", []) or []) DPI = int(getattr(snakemake.params, "dpi", 300)) +UMAP_POINT_SIZE = float(getattr(snakemake.params, "umap_point_size", 2)) EXTRA_ANNOT_COLUMNS = list(getattr(snakemake.params, "extra_annot_columns", []) or []) SAMPLE_COLORS = getattr(snakemake.params, "sample_colors", {}) or {} sub_dir = str(snakemake.output.sub_dir)