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.
- {phykit-2.1.52 → phykit-2.1.55}/PKG-INFO +18 -2
- {phykit-2.1.52 → phykit-2.1.55}/phykit/cli_registry.py +2 -0
- phykit-2.1.55/phykit/helpers/color_annotations.py +250 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/plot_config.py +3 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/phykit.py +259 -30
- {phykit-2.1.52 → phykit-2.1.55}/phykit/service_factories.py +1 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/__init__.py +1 -0
- phykit-2.1.55/phykit/services/alignment/alignment_subsample.py +281 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ancestral_reconstruction.py +97 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/character_map.py +64 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/concordance_asr.py +39 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/cont_map.py +39 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/cophylo.py +72 -4
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/density_map.py +39 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/discordance_asymmetry.py +39 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylo_heatmap.py +68 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/quartet_pie.py +89 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/rate_heterogeneity.py +39 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/stochastic_character_map.py +39 -0
- phykit-2.1.55/phykit/version.py +1 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/PKG-INFO +18 -2
- {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/SOURCES.txt +2 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/entry_points.txt +3 -0
- {phykit-2.1.52 → phykit-2.1.55}/setup.py +3 -0
- phykit-2.1.52/phykit/version.py +0 -1
- {phykit-2.1.52 → phykit-2.1.55}/LICENSE.md +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/README.md +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/__init__.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/__main__.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/errors.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/circular_layout.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/files.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/parsimony_utils.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/quartet_utils.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/__init__.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/base.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/__init__.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/independent_contrasts.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/parsimony_score.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.52 → phykit-2.1.55}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: phykit
|
|
3
|
-
Version: 2.1.
|
|
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):
|