phykit 2.1.52__tar.gz → 2.1.55__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 (126) hide show
  1. {phykit-2.1.52 → phykit-2.1.55}/PKG-INFO +18 -2
  2. {phykit-2.1.52 → phykit-2.1.55}/phykit/cli_registry.py +2 -0
  3. phykit-2.1.55/phykit/helpers/color_annotations.py +250 -0
  4. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/plot_config.py +3 -0
  5. {phykit-2.1.52 → phykit-2.1.55}/phykit/phykit.py +259 -30
  6. {phykit-2.1.52 → phykit-2.1.55}/phykit/service_factories.py +1 -0
  7. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/__init__.py +1 -0
  8. phykit-2.1.55/phykit/services/alignment/alignment_subsample.py +281 -0
  9. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ancestral_reconstruction.py +97 -0
  10. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/character_map.py +64 -0
  11. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/concordance_asr.py +39 -0
  12. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/cont_map.py +39 -0
  13. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/cophylo.py +72 -4
  14. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/density_map.py +39 -0
  15. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/discordance_asymmetry.py +39 -0
  16. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylo_heatmap.py +68 -0
  17. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/quartet_pie.py +89 -0
  18. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/rate_heterogeneity.py +39 -0
  19. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/stochastic_character_map.py +39 -0
  20. phykit-2.1.55/phykit/version.py +1 -0
  21. {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/PKG-INFO +18 -2
  22. {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/SOURCES.txt +2 -0
  23. {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/entry_points.txt +3 -0
  24. {phykit-2.1.52 → phykit-2.1.55}/setup.py +3 -0
  25. phykit-2.1.52/phykit/version.py +0 -1
  26. {phykit-2.1.52 → phykit-2.1.55}/LICENSE.md +0 -0
  27. {phykit-2.1.52 → phykit-2.1.55}/README.md +0 -0
  28. {phykit-2.1.52 → phykit-2.1.55}/phykit/__init__.py +0 -0
  29. {phykit-2.1.52 → phykit-2.1.55}/phykit/__main__.py +0 -0
  30. {phykit-2.1.52 → phykit-2.1.55}/phykit/errors.py +0 -0
  31. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/__init__.py +0 -0
  32. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/boolean_argument_parsing.py +0 -0
  33. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/caching.py +0 -0
  34. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/circular_layout.py +0 -0
  35. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/discrete_models.py +0 -0
  36. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/files.py +0 -0
  37. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/json_output.py +0 -0
  38. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/parallel.py +0 -0
  39. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/parsimony_utils.py +0 -0
  40. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/quartet_utils.py +0 -0
  41. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/stats_summary.py +0 -0
  42. {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/streaming.py +0 -0
  43. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/__init__.py +0 -0
  44. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_entropy.py +0 -0
  45. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_length.py +0 -0
  46. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  47. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  48. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_recoding.py +0 -0
  49. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/base.py +0 -0
  50. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/column_score.py +0 -0
  51. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/composition_per_taxon.py +0 -0
  52. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  53. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  54. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/dna_threader.py +0 -0
  55. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  56. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/faidx.py +0 -0
  57. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/gc_content.py +0 -0
  58. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/mask_alignment.py +0 -0
  59. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  60. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/pairwise_identity.py +0 -0
  61. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  62. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  63. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/rcv.py +0 -0
  64. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/rcvt.py +0 -0
  65. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  66. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  67. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/variable_sites.py +0 -0
  68. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/base.py +0 -0
  69. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/__init__.py +0 -0
  70. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/base.py +0 -0
  71. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/bipartition_support_stats.py +0 -0
  72. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/branch_length_multiplier.py +0 -0
  73. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/collapse_branches.py +0 -0
  74. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/consensus_network.py +0 -0
  75. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/consensus_tree.py +0 -0
  76. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  77. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/dvmc.py +0 -0
  78. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/evo_tempo_map.py +0 -0
  79. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/evolutionary_rate.py +0 -0
  80. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/fit_continuous.py +0 -0
  81. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/fit_discrete.py +0 -0
  82. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  83. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/independent_contrasts.py +0 -0
  84. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/internal_branch_stats.py +0 -0
  85. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/internode_labeler.py +0 -0
  86. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/kf_distance.py +0 -0
  87. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  88. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/lb_score.py +0 -0
  89. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ltt.py +0 -0
  90. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/monophyly_check.py +0 -0
  91. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  92. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/network_signal.py +0 -0
  93. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ou_shift_detection.py +0 -0
  94. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ouwie.py +0 -0
  95. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/parsimony_score.py +0 -0
  96. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/patristic_distances.py +0 -0
  97. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phenogram.py +0 -0
  98. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_glm.py +0 -0
  99. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  100. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_regression.py +0 -0
  101. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_signal.py +0 -0
  102. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylomorphospace.py +0 -0
  103. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/polytomy_test.py +0 -0
  104. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/print_tree.py +0 -0
  105. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/prune_tree.py +0 -0
  106. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/quartet_network.py +0 -0
  107. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/relative_rate_test.py +0 -0
  108. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/rename_tree_tips.py +0 -0
  109. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/rf_distance.py +0 -0
  110. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/root_tree.py +0 -0
  111. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/saturation.py +0 -0
  112. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/spectral_discordance.py +0 -0
  113. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/spurious_sequence.py +0 -0
  114. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/terminal_branch_stats.py +0 -0
  115. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/threshold_model.py +0 -0
  116. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/tip_labels.py +0 -0
  117. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  118. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  119. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/total_tree_length.py +0 -0
  120. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/treeness.py +0 -0
  121. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/treeness_over_rcv.py +0 -0
  122. {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/vcv_utils.py +0 -0
  123. {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/dependency_links.txt +0 -0
  124. {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/requires.txt +0 -0
  125. {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/top_level.txt +0 -0
  126. {phykit-2.1.52 → phykit-2.1.55}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.52
3
+ Version: 2.1.55
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -14,6 +14,22 @@ Classifier: Topic :: Scientific/Engineering
14
14
  Requires-Python: >=3.11
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE.md
17
+ Requires-Dist: biopython>=1.82
18
+ Requires-Dist: matplotlib>=3.7.0
19
+ Requires-Dist: numpy>=1.24.0
20
+ Requires-Dist: scipy>=1.11.3
21
+ Requires-Dist: scikit-learn>=1.4.2
22
+ Requires-Dist: umap-learn>=0.5.0
23
+ Requires-Dist: tqdm>=4.65.0
24
+ Dynamic: author
25
+ Dynamic: author-email
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: license-file
31
+ Dynamic: requires-dist
32
+ Dynamic: requires-python
17
33
 
18
34
  <p align="center">
19
35
  <a href="https://github.com/jlsteenwyk/phykit">
@@ -13,6 +13,8 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
13
13
  "al": "alignment_length",
14
14
  "aln_len_no_gaps": "alignment_length_no_gaps",
15
15
  "alng": "alignment_length_no_gaps",
16
+ "aln_subsample": "alignment_subsample",
17
+ "subsample": "alignment_subsample",
16
18
  "aln_entropy": "alignment_entropy",
17
19
  "entropy": "alignment_entropy",
18
20
  "aln_recoding": "alignment_recoding",
@@ -0,0 +1,250 @@
1
+ """Shared utilities for parsing color annotation files and drawing
2
+ colored ranges/clades on phylogenetic tree plots."""
3
+
4
+ import sys
5
+ from math import atan2, degrees, pi
6
+
7
+ from phykit.errors import PhykitUserError
8
+
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Parsing
12
+ # ---------------------------------------------------------------------------
13
+
14
+
15
+ def parse_color_file(path: str) -> dict:
16
+ """Read a TSV color-annotation file.
17
+
18
+ Expected columns (tab-separated):
19
+ field1 field2 field3 [field4]
20
+ node type color [label]
21
+
22
+ *type* is one of ``label``, ``range``, or ``clade``.
23
+
24
+ Returns a dict with keys ``labels``, ``ranges``, ``clades``.
25
+ """
26
+ try:
27
+ with open(path) as fh:
28
+ lines = fh.readlines()
29
+ except FileNotFoundError:
30
+ raise PhykitUserError([f"Color file not found: {path}"])
31
+
32
+ labels: dict = {}
33
+ ranges: list = []
34
+ clades: list = []
35
+
36
+ for line in lines:
37
+ stripped = line.strip()
38
+ if not stripped or stripped.startswith("#"):
39
+ continue
40
+
41
+ parts = stripped.split("\t")
42
+ if len(parts) < 3:
43
+ continue
44
+
45
+ field1 = parts[0].strip()
46
+ field2 = parts[1].strip().lower()
47
+ field3 = parts[2].strip()
48
+ field4 = parts[3].strip() if len(parts) > 3 else None
49
+
50
+ if field2 == "label":
51
+ labels[field1] = field3
52
+ elif field2 == "range":
53
+ taxa = [t.strip() for t in field1.split(",")]
54
+ ranges.append((taxa, field3, field4))
55
+ elif field2 == "clade":
56
+ taxa = [t.strip() for t in field1.split(",")]
57
+ clades.append((taxa, field3, field4))
58
+
59
+ return {
60
+ "labels": labels,
61
+ "ranges": ranges,
62
+ "clades": clades,
63
+ }
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Tree helpers
68
+ # ---------------------------------------------------------------------------
69
+
70
+
71
+ def resolve_mrca(tree, taxa: list):
72
+ """Return the MRCA Clade for *taxa* in a BioPython Phylo tree.
73
+
74
+ Taxa not found in the tree are silently dropped. If fewer than two
75
+ valid taxa remain a warning is printed and ``None`` is returned.
76
+ """
77
+ tip_names = {tip.name for tip in tree.get_terminals()}
78
+ valid = [t for t in taxa if t in tip_names]
79
+ if len(valid) < 2:
80
+ print(
81
+ f"Warning: fewer than 2 valid taxa for MRCA lookup "
82
+ f"(requested: {taxa}, valid: {valid}); skipping.",
83
+ file=sys.stderr,
84
+ )
85
+ return None
86
+ return tree.common_ancestor(valid)
87
+
88
+
89
+ def get_clade_tip_ids(clade) -> set:
90
+ """Return ``{id(tip) for tip in clade.get_terminals()}``."""
91
+ return {id(tip) for tip in clade.get_terminals()}
92
+
93
+
94
+ def get_clade_branch_ids(tree, clade, parent_map) -> set:
95
+ """Return the set of ``id(node)`` for the MRCA and all its descendants."""
96
+ ids = set()
97
+ for node in clade.find_clades(order="preorder"):
98
+ ids.add(id(node))
99
+ return ids
100
+
101
+
102
+ def build_color_legend_handles(color_data):
103
+ """Build matplotlib legend handles for labeled ranges and clades.
104
+
105
+ Returns a list of Patch objects for any range or clade entry that
106
+ has a label (field 4). Import matplotlib lazily.
107
+ """
108
+ from matplotlib.patches import Patch
109
+
110
+ handles = []
111
+ for taxa_list, color, label in color_data.get("ranges", []):
112
+ if label:
113
+ handles.append(
114
+ Patch(facecolor=color, alpha=0.3, edgecolor="none", label=label)
115
+ )
116
+ for taxa_list, color, label in color_data.get("clades", []):
117
+ if label:
118
+ handles.append(
119
+ Patch(facecolor=color, edgecolor="black", linewidth=0.5, label=label)
120
+ )
121
+ return handles
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Drawing helpers
126
+ # ---------------------------------------------------------------------------
127
+
128
+
129
+ def draw_range_rect(ax, tree, clade, color, node_x, node_y, alpha=0.15):
130
+ """Draw a coloured rectangle behind *clade* in rectangular mode."""
131
+ from matplotlib.patches import Rectangle
132
+
133
+ tips = list(clade.get_terminals())
134
+ if not tips:
135
+ return
136
+
137
+ tip_xs = [node_x[id(t)] for t in tips if id(t) in node_x]
138
+ tip_ys = [node_y[id(t)] for t in tips if id(t) in node_y]
139
+ if not tip_xs or not tip_ys:
140
+ return
141
+
142
+ mrca_x = node_x.get(id(clade), min(tip_xs))
143
+
144
+ x_min = mrca_x
145
+ x_max = max(tip_xs)
146
+ # Small padding so the rectangle extends slightly past the tips
147
+ x_pad = (x_max - x_min) * 0.05 if x_max > x_min else 0.02
148
+ x_max += x_pad
149
+
150
+ y_min = min(tip_ys) - 0.4
151
+ y_max = max(tip_ys) + 0.4
152
+
153
+ width = x_max - x_min
154
+ height = y_max - y_min
155
+
156
+ ax.add_patch(
157
+ Rectangle(
158
+ (x_min, y_min),
159
+ width,
160
+ height,
161
+ facecolor=color,
162
+ alpha=alpha,
163
+ edgecolor="none",
164
+ zorder=0,
165
+ )
166
+ )
167
+
168
+
169
+ def draw_range_wedge(ax, tree, clade, color, coords, alpha=0.15):
170
+ """Draw a coloured wedge behind *clade* in circular mode.
171
+
172
+ *coords* should be a dict mapping ``id(node)`` to ``(x, y)`` pairs in
173
+ Cartesian coordinates (the circular layout).
174
+ """
175
+ from matplotlib.patches import Wedge
176
+
177
+ tips = list(clade.get_terminals())
178
+ if not tips:
179
+ return
180
+
181
+ tip_coords = [
182
+ coords[id(t)] for t in tips if id(t) in coords
183
+ ]
184
+ if len(tip_coords) < 2:
185
+ return
186
+
187
+ # Extract angles and radii from coords dicts
188
+ angles = [c["angle"] for c in tip_coords]
189
+ radii = [c["radius"] for c in tip_coords]
190
+
191
+ # Sort angles for range computation
192
+ sorted_angles = sorted(angles)
193
+
194
+ # Compute angular gap between consecutive sorted tips to determine padding
195
+ if len(sorted_angles) >= 2:
196
+ gaps = [
197
+ sorted_angles[i + 1] - sorted_angles[i]
198
+ for i in range(len(sorted_angles) - 1)
199
+ ]
200
+ min_gap = min(gaps) if gaps else 0.05
201
+ pad = min_gap * 0.5
202
+ else:
203
+ pad = 0.05
204
+
205
+ # Check if the clade wraps around (largest gap > pi)
206
+ # Compute the complement gap (from last to first going through 2*pi)
207
+ complement_gap = (2 * pi) - (sorted_angles[-1] - sorted_angles[0])
208
+ all_gaps = [
209
+ sorted_angles[i + 1] - sorted_angles[i]
210
+ for i in range(len(sorted_angles) - 1)
211
+ ]
212
+ all_gaps.append(complement_gap)
213
+
214
+ # The largest gap is where the clade does NOT span
215
+ max_gap_idx = all_gaps.index(max(all_gaps))
216
+ if max_gap_idx < len(sorted_angles) - 1:
217
+ angle_min = sorted_angles[max_gap_idx + 1] - pad
218
+ angle_max = sorted_angles[max_gap_idx] + pad
219
+ # This means we need to wrap
220
+ if angle_max < angle_min:
221
+ angle_max += 2 * pi
222
+ else:
223
+ # The biggest gap is the complement gap -> simple contiguous range
224
+ angle_min = sorted_angles[0] - pad
225
+ angle_max = sorted_angles[-1] + pad
226
+
227
+ # Radii
228
+ mrca_coord = coords.get(id(clade))
229
+ if mrca_coord is not None:
230
+ r_inner = mrca_coord["radius"]
231
+ else:
232
+ r_inner = min(radii)
233
+
234
+ r_outer = max(radii)
235
+ r_pad = (r_outer - r_inner) * 0.05 if r_outer > r_inner else 0.02
236
+ r_outer += r_pad
237
+
238
+ ax.add_patch(
239
+ Wedge(
240
+ (0, 0),
241
+ r_outer,
242
+ degrees(angle_min),
243
+ degrees(angle_max),
244
+ width=r_outer - r_inner,
245
+ facecolor=color,
246
+ alpha=alpha,
247
+ edgecolor="none",
248
+ zorder=0,
249
+ )
250
+ )
@@ -26,6 +26,7 @@ class PlotConfig:
26
26
  ladderize: bool = False
27
27
  cladogram: bool = False
28
28
  circular: bool = False
29
+ color_file: Optional[str] = None
29
30
 
30
31
  def validate(self) -> None:
31
32
  if self.fig_width is not None and self.fig_width <= 0:
@@ -197,6 +198,7 @@ class PlotConfig:
197
198
  ladderize=getattr(args, "ladderize", False),
198
199
  cladogram=getattr(args, "cladogram", False),
199
200
  circular=getattr(args, "circular", False),
201
+ color_file=getattr(args, "color_file", None),
200
202
  )
201
203
  config.validate()
202
204
  return config
@@ -218,6 +220,7 @@ def add_plot_arguments(parser) -> None:
218
220
  group.add_argument("--ladderize", action="store_true", default=False, help="Ladderize (sort) the tree before plotting")
219
221
  group.add_argument("--cladogram", action="store_true", default=False, help="Draw cladogram (equal branch lengths, tips aligned) instead of phylogram")
220
222
  group.add_argument("--circular", action="store_true", default=False, help="Draw circular (radial/fan) phylogram instead of rectangular")
223
+ group.add_argument("--color-file", type=str, default=None, help="Color annotation file for tip labels, clade ranges, and branch colors")
221
224
 
222
225
 
223
226
  def compute_node_x_cladogram(tree, parent_map):