cytocommunity2 1.0.3__tar.gz → 1.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {cytocommunity2-1.0.3/src/cytocommunity2.egg-info → cytocommunity2-1.0.4}/PKG-INFO +3 -1
  2. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/README.md +2 -0
  3. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/pyproject.toml +1 -1
  4. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/__init__.py +1 -1
  5. cytocommunity2-1.0.4/src/cytocommunity2/colors.py +34 -0
  6. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/config.py +1 -0
  7. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/cca.py +14 -0
  8. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/differential_composition.py +4 -10
  9. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/dominant_celltype.py +4 -18
  10. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/enrichment.py +22 -12
  11. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/moran.py +2 -3
  12. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/cn_selection.py +12 -27
  13. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/coherence.py +4 -13
  14. cytocommunity2-1.0.4/src/cytocommunity2/downstream/plotting/common.py +146 -0
  15. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/communication.py +26 -39
  16. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/dominant.py +15 -24
  17. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/dotplots.py +53 -114
  18. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/recurrence.py +63 -105
  19. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/visualization/spatial.py +2 -1
  20. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4/src/cytocommunity2.egg-info}/PKG-INFO +3 -1
  21. cytocommunity2-1.0.3/src/cytocommunity2/colors.py +0 -14
  22. cytocommunity2-1.0.3/src/cytocommunity2/downstream/plotting/common.py +0 -79
  23. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/LICENSE +0 -0
  24. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/setup.cfg +0 -0
  25. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/_io.py +0 -0
  26. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/__init__.py +0 -0
  27. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/coherence.py +0 -0
  28. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/__init__.py +0 -0
  29. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/between.py +0 -0
  30. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/utils.py +0 -0
  31. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/communication/within.py +0 -0
  32. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/plotting/__init__.py +0 -0
  33. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/recurrence.py +0 -0
  34. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/downstream/wilcoxon.py +0 -0
  35. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/ensemble.py +0 -0
  36. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/__init__.py +0 -0
  37. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/api.py +0 -0
  38. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/dataset.py +0 -0
  39. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/model.py +0 -0
  40. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/selection.py +0 -0
  41. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/learning/training.py +0 -0
  42. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/paths.py +0 -0
  43. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2/visualization/__init__.py +0 -0
  44. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/SOURCES.txt +0 -0
  45. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/dependency_links.txt +0 -0
  46. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/requires.txt +0 -0
  47. {cytocommunity2-1.0.3 → cytocommunity2-1.0.4}/src/cytocommunity2.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cytocommunity2
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: Weakly supervised cellular-neighborhood learning for spatial omics
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -30,6 +30,8 @@ Dynamic: license-file
30
30
 
31
31
  ## Overview
32
32
 
33
+ ![CytoCommunity2 overview](support/Fig.1.png)
34
+
33
35
  To enhance CytoCommunity (https://github.com/huBioinfo/CytoCommunity), we
34
36
  present CytoCommunity2, **a unified weakly-supervised framework** for
35
37
  identifying and comparing tissue cellular neighborhoods (TCNs or CNs) across
@@ -10,6 +10,8 @@
10
10
 
11
11
  ## Overview
12
12
 
13
+ ![CytoCommunity2 overview](support/Fig.1.png)
14
+
13
15
  To enhance CytoCommunity (https://github.com/huBioinfo/CytoCommunity), we
14
16
  present CytoCommunity2, **a unified weakly-supervised framework** for
15
17
  identifying and comparing tissue cellular neighborhoods (TCNs or CNs) across
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cytocommunity2"
7
- version = "1.0.3"
7
+ version = "1.0.4"
8
8
  description = "Weakly supervised cellular-neighborhood learning for spatial omics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -10,7 +10,7 @@ from .learning import (
10
10
  )
11
11
  from .visualization import visualize_results
12
12
 
13
- __version__ = "1.0.3"
13
+ __version__ = "1.0.4"
14
14
 
15
15
  __all__ = [
16
16
  "CytoCommunityConfig",
@@ -0,0 +1,34 @@
1
+ """Shared categorical colors used across CytoCommunity2 figures."""
2
+
3
+
4
+ CN_PALETTE = "d3_category20"
5
+
6
+
7
+ def cn_color_map(cns):
8
+ """Map CN numbers or CN-prefixed labels to fixed spatial/OC colors."""
9
+ import sci_palettes
10
+
11
+ palette = list(sci_palettes.PALETTES[CN_PALETTE].values())
12
+ colors = {}
13
+ for cn in cns:
14
+ number = int(str(cn).removeprefix("CN"))
15
+ if not 1 <= number <= len(palette):
16
+ raise ValueError(
17
+ f"CN {cn!r} is outside the 1..{len(palette)} range of "
18
+ f"palette {CN_PALETTE!r}."
19
+ )
20
+ colors[cn] = palette[number - 1]
21
+ return colors
22
+
23
+
24
+ def celltype_color_map(celltypes):
25
+ """Return the canonical cell-type colors in the supplied display order."""
26
+ import matplotlib.pyplot as plt
27
+
28
+ palette = []
29
+ for name in ("tab20", "tab20b", "tab20c", "Set1", "Dark2", "Accent", "Paired"):
30
+ palette.extend(plt.get_cmap(name).colors)
31
+ return {
32
+ celltype: palette[index]
33
+ for index, celltype in enumerate(celltypes)
34
+ }
@@ -29,6 +29,7 @@ DOWNSTREAM_PARAMETERS = {
29
29
  "cca_min_std": 1e-8,
30
30
  "cca_max_iterations": 1000,
31
31
  "cca_n_components": 2,
32
+ "cca_top_celltypes": 5,
32
33
  },
33
34
  }
34
35
 
@@ -60,10 +60,23 @@ def build_score_list(frame, cn_list):
60
60
  return matrices
61
61
 
62
62
 
63
+ def select_top_features(matrix, top_n=5):
64
+ """Select cell types by decreasing mean enrichment before variance filtering."""
65
+ if matrix is None or matrix.shape[1] == 0:
66
+ return matrix
67
+ means = matrix.mean(axis=0).fillna(-np.inf)
68
+ selected = means.sort_values(ascending=False, kind="stable").head(top_n).index
69
+ return matrix.loc[:, selected]
70
+
71
+
63
72
  def run_cca_for_scores(
64
73
  score_list, cn_list, communication_params, random_seed, condition=None
65
74
  ):
66
75
  """Run CCA and permutation tests on in-memory enrichment matrices."""
76
+ score_list = {
77
+ cn: select_top_features(matrix, communication_params["cca_top_celltypes"])
78
+ for cn, matrix in score_list.items()
79
+ }
67
80
  result_rows = []
68
81
  cache = {}
69
82
  for cn_a, cn_b in combinations(map(str, cn_list), 2):
@@ -312,5 +325,6 @@ __all__ = [
312
325
  "pair_rng",
313
326
  "run_cca_analysis",
314
327
  "run_cca_for_scores",
328
+ "select_top_features",
315
329
  "write_cca_loadings",
316
330
  ]
@@ -7,31 +7,25 @@ import pandas as pd
7
7
 
8
8
  from ..config import CytoCommunityConfig
9
9
  from ..paths import build_paths
10
- from .dominant_celltype import load_enrichment_matrices
10
+ from .enrichment import load_enrichment_by_sample
11
11
  from .wilcoxon import rank_sum_test
12
12
 
13
13
 
14
14
  def load_condition_enrichment(enrichment_dir, input_dir):
15
15
  """Load enrichment matrices together with each sample's graph label."""
16
- celltypes, matrices = load_enrichment_matrices(
16
+ celltypes, matrices = load_enrichment_by_sample(
17
17
  enrichment_dir, input_dir
18
18
  )
19
19
  input_dir = Path(input_dir)
20
- samples = (
21
- pd.read_csv(input_dir / "ImageNameList.txt", sep="\t", header=None)
22
- .iloc[:, 0]
23
- .astype(str)
24
- .tolist()
25
- )
26
20
  records = [
27
21
  {
28
22
  "sample": sample,
29
23
  "condition": int(
30
24
  np.loadtxt(input_dir / f"{sample}_GraphLabel.txt", dtype="int64")
31
25
  ),
32
- "matrix": matrices[index],
26
+ "matrix": matrix,
33
27
  }
34
- for index, sample in enumerate(samples)
28
+ for sample, matrix in matrices.items()
35
29
  ]
36
30
  return celltypes, records
37
31
 
@@ -9,6 +9,7 @@ from scipy.cluster.hierarchy import leaves_list, linkage
9
9
 
10
10
  from ..config import CytoCommunityConfig
11
11
  from ..paths import build_paths
12
+ from .enrichment import load_enrichment_by_sample
12
13
 
13
14
 
14
15
  @dataclass(frozen=True)
@@ -22,24 +23,9 @@ class DominantCellTypeResult:
22
23
 
23
24
 
24
25
  def load_enrichment_matrices(enrichment_dir, input_dir):
25
- """Load per-sample enrichment matrices in their stored cell-type order."""
26
- enrichment_dir = Path(enrichment_dir)
27
- input_dir = Path(input_dir)
28
- source_celltypes = (
29
- pd.read_csv(enrichment_dir / "CellTypeVec_List.csv")
30
- .iloc[:, 0]
31
- .astype(str)
32
- .str.strip()
33
- .tolist()
34
- )
35
- samples = pd.read_csv(input_dir / "ImageNameList.txt", header=None).iloc[:, 0]
36
- matrices = [
37
- pd.read_csv(
38
- enrichment_dir / f"{sample}_EnrichScoreMatrix.csv", header=None
39
- ).to_numpy(dtype=float)
40
- for sample in samples
41
- ]
42
- return source_celltypes, np.stack(matrices, axis=0)
26
+ """Retain the stacked-array interface for dominant-cell-type analysis."""
27
+ celltypes, matrices = load_enrichment_by_sample(enrichment_dir, input_dir)
28
+ return celltypes, np.stack(list(matrices.values()), axis=0)
43
29
 
44
30
 
45
31
  def analyze_dominant_celltypes(
@@ -9,6 +9,26 @@ from scipy.stats import false_discovery_control, hypergeom
9
9
  from .._io import read_nonempty_lines
10
10
 
11
11
 
12
+ def load_enrichment_by_sample(enrichment_dir, input_dir):
13
+ """Load enrichment matrices keyed by sample, in the input-list order."""
14
+ enrichment_dir = Path(enrichment_dir)
15
+ celltypes = (
16
+ pd.read_csv(enrichment_dir / "CellTypeVec_List.csv")
17
+ .iloc[:, 0]
18
+ .astype(str)
19
+ .str.strip()
20
+ .tolist()
21
+ )
22
+ samples = read_nonempty_lines(Path(input_dir) / "ImageNameList.txt")
23
+ matrices = {
24
+ sample: pd.read_csv(
25
+ enrichment_dir / f"{sample}_EnrichScoreMatrix.csv", header=None
26
+ ).to_numpy(dtype=float)
27
+ for sample in samples
28
+ }
29
+ return celltypes, matrices
30
+
31
+
12
32
  def calculate_enrichment_matrix(
13
33
  cell_labels,
14
34
  cn_labels,
@@ -126,19 +146,9 @@ def calculate_enrichment(
126
146
  def export_enrichment_long(dataset, enrichment_dir, output_file):
127
147
  """Export the selected model's enrichment matrices for communication analysis."""
128
148
  dataset = Path(dataset)
129
- enrichment_dir = Path(enrichment_dir)
130
- samples = read_nonempty_lines(dataset / "ImageNameList.txt")
131
- celltypes = (
132
- pd.read_csv(enrichment_dir / "CellTypeVec_List.csv")
133
- .iloc[:, 0]
134
- .astype(str)
135
- .tolist()
136
- )
149
+ celltypes, matrices = load_enrichment_by_sample(enrichment_dir, dataset)
137
150
  tables = []
138
- for sample in samples:
139
- matrix = pd.read_csv(
140
- enrichment_dir / f"{sample}_EnrichScoreMatrix.csv", header=None
141
- ).to_numpy(dtype=float)
151
+ for sample, matrix in matrices.items():
142
152
  table = (
143
153
  pd.DataFrame(
144
154
  matrix,
@@ -7,6 +7,7 @@ from libpysal.weights import DistanceBand
7
7
  import numpy as np
8
8
  import pandas as pd
9
9
 
10
+ from .._io import read_nonempty_lines
10
11
  from ..config import CytoCommunityConfig
11
12
  from ..paths import build_paths
12
13
  from .wilcoxon import rank_sum_test
@@ -25,9 +26,7 @@ MORAN_COLUMNS = [
25
26
  def load_graph_labels(input_dir):
26
27
  """Read each sample's condition from its GraphLabel file."""
27
28
  input_dir = Path(input_dir)
28
- samples = pd.read_csv(
29
- input_dir / "ImageNameList.txt", sep="\t", header=None, names=["Sample"]
30
- )["Sample"].astype(str)
29
+ samples = read_nonempty_lines(input_dir / "ImageNameList.txt")
31
30
  return {
32
31
  sample: int(np.loadtxt(input_dir / f"{sample}_GraphLabel.txt", dtype="int64"))
33
32
  for sample in samples
@@ -6,47 +6,32 @@ import matplotlib.pyplot as plt
6
6
  import pandas as pd
7
7
 
8
8
  from ...paths import build_paths
9
+ from .common import save_figure
9
10
 
10
11
 
11
- def _save_figure(figure, output_stem):
12
- output_stem = Path(output_stem)
13
- output_stem.parent.mkdir(parents=True, exist_ok=True)
14
- figure.savefig(output_stem.with_suffix(".pdf"), bbox_inches="tight")
15
- figure.savefig(output_stem.with_suffix(".png"), dpi=300, bbox_inches="tight")
16
- plt.close(figure)
17
- return output_stem
18
-
19
-
20
- def _plot_cri(summary, output_stem):
12
+ def _plot_cri(summary, output_stem, *, png=False):
21
13
  values = summary["CRI"]
22
14
  k_values = summary["NumCN"].astype(int)
23
15
  best_index = values.idxmax()
24
16
  best_k = int(k_values.loc[best_index])
25
- best_value = float(values.loc[best_index])
26
17
 
27
- figure, axis = plt.subplots(figsize=(6.0, 4.8))
18
+ figure, axis = plt.subplots(figsize=(7.4, 6.0))
28
19
  axis.bar(k_values, values, width=0.62, color="#c9c9c9", edgecolor="none")
29
20
  axis.plot(k_values, values, color="black", linestyle=(0, (2, 2)), marker="o")
30
- axis.annotate(
31
- f"Optimal: K={best_k}",
32
- xy=(best_k, best_value),
33
- xytext=(0, 10),
34
- textcoords="offset points",
35
- ha="center",
36
- va="bottom",
37
- fontsize=11,
38
- )
39
- axis.set_xlabel(r"$K$", fontsize=13)
40
- axis.set_ylabel("CN resolution index (CRI)", fontsize=12)
21
+ axis.set_title(rf"Optimal: $K$={best_k}", fontsize=18, pad=10, loc="center")
22
+ axis.set_xlabel(r"$K$", fontsize=18)
23
+ axis.set_ylabel("CN resolution index", fontsize=16)
41
24
  axis.set_xticks(k_values)
42
25
  axis.spines[["top", "right"]].set_visible(False)
43
- axis.tick_params(labelsize=10)
26
+ axis.tick_params(labelsize=13)
44
27
  axis.margins(x=0.04)
45
28
  figure.tight_layout()
46
- return _save_figure(figure, output_stem)
29
+ output = save_figure(figure, output_stem, png=png)
30
+ plt.close(figure)
31
+ return output
47
32
 
48
33
 
49
- def plot_cn_selection_scores(selection_summary, config):
34
+ def plot_cn_selection_scores(selection_summary, config, *, png=False):
50
35
  """Plot the composite CRI score across candidate CN numbers."""
51
36
  if isinstance(selection_summary, (str, Path)):
52
37
  summary = pd.read_csv(selection_summary)
@@ -55,4 +40,4 @@ def plot_cn_selection_scores(selection_summary, config):
55
40
 
56
41
  summary = summary.sort_values("NumCN").reset_index(drop=True)
57
42
  output_dir = build_paths(config).downstream_plot / "CNSelection"
58
- return _plot_cri(summary, output_dir / "CRI_by_K")
43
+ return _plot_cri(summary, output_dir / "CRI_by_K", png=png)
@@ -1,23 +1,15 @@
1
1
  """Spatial coherence figures."""
2
2
 
3
3
  from ...paths import build_paths
4
+ from .common import configure_style, save_figure
4
5
 
5
6
 
6
7
  def plot_coherence_results(result, config):
7
8
  """Save CHAOS and PAS boxplots."""
8
- import matplotlib as mpl
9
9
  import matplotlib.pyplot as plt
10
10
  import seaborn as sns
11
11
 
12
- mpl.rcParams.update({
13
- "font.family": "Arial",
14
- "font.size": 12,
15
- "axes.linewidth": 0.8,
16
- "lines.linewidth": 1.5,
17
- "xtick.labelsize": 10,
18
- "ytick.labelsize": 10,
19
- "pdf.fonttype": 42,
20
- })
12
+ configure_style()
21
13
  output_dir = build_paths(config).downstream_plot / "Coherence"
22
14
  output_dir.mkdir(parents=True, exist_ok=True)
23
15
  frame = result.loc[
@@ -28,7 +20,7 @@ def plot_coherence_results(result, config):
28
20
  plt.rcParams["grid.alpha"] = 0.25
29
21
  outputs = {}
30
22
  for column, axis_max in (("CHAOS", 0.02), ("PAS", 0.5)):
31
- figure, axis = plt.subplots(figsize=(8, 6), dpi=1200)
23
+ figure, axis = plt.subplots(figsize=(8, 6))
32
24
  sns.boxplot(
33
25
  data=frame,
34
26
  y=column,
@@ -52,8 +44,7 @@ def plot_coherence_results(result, config):
52
44
  axis.spines["top"].set_visible(False)
53
45
  axis.spines["right"].set_visible(False)
54
46
  figure.tight_layout()
55
- output = output_dir / f"{column}.pdf"
56
- figure.savefig(output, dpi=1200, bbox_inches="tight")
47
+ output = save_figure(figure, output_dir / f"{column}.pdf")
57
48
  plt.close(figure)
58
49
  outputs[column] = output
59
50
  return outputs
@@ -0,0 +1,146 @@
1
+ """Plotting primitives shared by multiple downstream figures."""
2
+
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+
7
+
8
+ def configure_style(*, line_width=1.5, ps_fonttype=None):
9
+ """Apply the shared figure style while allowing per-plot line widths."""
10
+ import matplotlib as mpl
11
+
12
+ style = {
13
+ "font.family": "Arial",
14
+ "font.size": 12,
15
+ "axes.linewidth": 0.8,
16
+ "lines.linewidth": line_width,
17
+ "xtick.labelsize": 10,
18
+ "ytick.labelsize": 10,
19
+ "pdf.fonttype": 42,
20
+ }
21
+ if ps_fonttype is not None:
22
+ style["ps.fonttype"] = ps_fonttype
23
+ mpl.rcParams.update(style)
24
+
25
+
26
+ def save_figure(figure, output, *, png=False):
27
+ """Save a PDF, plus a PNG when explicitly requested for notebook display."""
28
+ output = Path(output).with_suffix(".pdf")
29
+ output.parent.mkdir(parents=True, exist_ok=True)
30
+ figure.savefig(output, bbox_inches="tight", dpi=1200)
31
+ if png:
32
+ figure.savefig(output.with_suffix(".png"), bbox_inches="tight", dpi=300)
33
+ return output
34
+
35
+
36
+ def add_colorbar(figure, axis, norm, title, ticks=None, *, cmap, orientation="vertical"):
37
+ """Draw a borderless color scale with a side label and visible ticks."""
38
+ import matplotlib as mpl
39
+ from matplotlib.ticker import MaxNLocator, StrMethodFormatter
40
+
41
+ scalar = mpl.cm.ScalarMappable(norm=norm, cmap=cmap)
42
+ if ticks is None:
43
+ step = 1.0 if norm.vmax - norm.vmin > 4 else 0.5
44
+ ticks = np.arange(np.ceil(norm.vmin / step), np.floor(norm.vmax / step) + 1) * step
45
+ if len(ticks) < 2:
46
+ ticks = MaxNLocator(nbins=4, steps=[1, 2, 5, 10]).tick_values(norm.vmin, norm.vmax)
47
+ ticks = ticks[(ticks >= norm.vmin) & (ticks <= norm.vmax)]
48
+ colorbar = figure.colorbar(scalar, cax=axis, ticks=ticks, orientation=orientation)
49
+ colorbar.formatter = StrMethodFormatter("{x:g}")
50
+ colorbar.update_ticks()
51
+ colorbar.set_label(title, fontsize=12, labelpad=10)
52
+ if orientation == "vertical":
53
+ colorbar.ax.yaxis.set_label_position("left")
54
+ colorbar.ax.tick_params(
55
+ axis="y" if orientation == "vertical" else "x",
56
+ labelsize=10, length=3, width=0.8, pad=5, direction="out",
57
+ **({"right": True, "left": False, "labelright": True, "labelleft": False}
58
+ if orientation == "vertical" else
59
+ {"bottom": True, "top": False, "labelbottom": True, "labeltop": False}),
60
+ )
61
+ colorbar.outline.set_visible(False)
62
+ return colorbar
63
+
64
+
65
+ def save_legend(handles, labels, output, *, title=None, columns=1):
66
+ """Save a categorical legend separately from its main figure."""
67
+ import matplotlib.pyplot as plt
68
+
69
+ figure, axis = plt.subplots(figsize=(max(3.4, 1.7 * columns), 2.5))
70
+ axis.axis("off")
71
+ axis.legend(handles, labels, title=title, ncol=columns, loc="center", frameon=False)
72
+ output = save_figure(figure, output)
73
+ plt.close(figure)
74
+ return output
75
+
76
+
77
+ def dot_size(pvalues):
78
+ strength = np.minimum(
79
+ -np.log10(np.clip(np.asarray(pvalues, dtype=float), 1e-300, 1.0)),
80
+ 5.0,
81
+ )
82
+ return legend_size(strength)
83
+
84
+
85
+ def legend_size(strength):
86
+ return 30.0 + 600.0 * np.asarray(strength, dtype=float) / 5.0
87
+
88
+
89
+ def plot_enrichment_scatter(
90
+ x,
91
+ y,
92
+ xlabel,
93
+ ylabel,
94
+ rho,
95
+ pval,
96
+ out_pdf,
97
+ enrichment_score_max=20.0,
98
+ ):
99
+ """Draw the shared enrichment-score scatter style."""
100
+ import matplotlib.pyplot as plt
101
+ import matplotlib.ticker as ticker
102
+
103
+ figure, axis = plt.subplots(figsize=(6, 6))
104
+ x = np.asarray(x, dtype=float)
105
+ y = np.asarray(y, dtype=float)
106
+ valid = np.isfinite(x) & np.isfinite(y)
107
+ x, y = x[valid], y[valid]
108
+ if len(x) >= 2 and len(np.unique(x)) > 1 and len(np.unique(y)) > 1:
109
+ slope, intercept = np.polyfit(x, y, 1)
110
+ x_line = np.linspace(float(np.min(x)), float(np.max(x)), 200)
111
+ axis.plot(
112
+ x_line,
113
+ slope * x_line + intercept,
114
+ linewidth=1.2,
115
+ color="black",
116
+ zorder=1,
117
+ )
118
+ axis.scatter(x, y, s=30, edgecolor="black", alpha=0.9, zorder=2)
119
+ axis.set_xlabel(xlabel, fontsize=10)
120
+ axis.set_ylabel(ylabel, fontsize=10)
121
+ interval = enrichment_score_max / 8
122
+ margin = interval / 5
123
+ ticks = np.linspace(0.0, enrichment_score_max, 9)
124
+ axis.set_xlim(-margin, enrichment_score_max + margin)
125
+ axis.set_ylim(-margin, enrichment_score_max + margin)
126
+ axis.set_xticks(ticks)
127
+ axis.set_yticks(ticks)
128
+ axis.xaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f"))
129
+ axis.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.1f"))
130
+ axis.tick_params(axis="x", labelrotation=30, labelsize=10)
131
+ axis.tick_params(axis="y", labelsize=10)
132
+ axis.spines["top"].set_visible(False)
133
+ axis.spines["right"].set_visible(False)
134
+ axis.text(
135
+ 0.80,
136
+ 0.98,
137
+ f"rho = {rho:.2f}\np = {pval:.2g}",
138
+ transform=axis.transAxes,
139
+ ha="left",
140
+ va="top",
141
+ fontsize=10,
142
+ )
143
+ figure.tight_layout()
144
+ output = save_figure(figure, out_pdf)
145
+ plt.close(figure)
146
+ return output