phykit 2.1.73__tar.gz → 2.1.75__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 (135) hide show
  1. {phykit-2.1.73 → phykit-2.1.75}/PKG-INFO +1 -1
  2. {phykit-2.1.73 → phykit-2.1.75}/phykit/phykit.py +30 -0
  3. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/ancestral_reconstruction.py +21 -13
  4. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/concordance_asr.py +10 -6
  5. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/consensus_network.py +123 -13
  6. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/cont_map.py +10 -6
  7. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/density_map.py +10 -6
  8. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/discordance_asymmetry.py +49 -33
  9. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phenogram.py +8 -6
  10. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/rate_heterogeneity.py +10 -6
  11. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/stochastic_character_map.py +10 -6
  12. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/trait_rate_map.py +12 -8
  13. phykit-2.1.75/phykit/version.py +1 -0
  14. {phykit-2.1.73 → phykit-2.1.75}/phykit.egg-info/PKG-INFO +1 -1
  15. phykit-2.1.73/phykit/version.py +0 -1
  16. {phykit-2.1.73 → phykit-2.1.75}/LICENSE.md +0 -0
  17. {phykit-2.1.73 → phykit-2.1.75}/README.md +0 -0
  18. {phykit-2.1.73 → phykit-2.1.75}/phykit/__init__.py +0 -0
  19. {phykit-2.1.73 → phykit-2.1.75}/phykit/__main__.py +0 -0
  20. {phykit-2.1.73 → phykit-2.1.75}/phykit/cli_registry.py +0 -0
  21. {phykit-2.1.73 → phykit-2.1.75}/phykit/errors.py +0 -0
  22. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/__init__.py +0 -0
  23. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/boolean_argument_parsing.py +0 -0
  24. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/caching.py +0 -0
  25. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/circular_layout.py +0 -0
  26. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/color_annotations.py +0 -0
  27. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/discrete_models.py +0 -0
  28. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/files.py +0 -0
  29. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/json_output.py +0 -0
  30. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/parallel.py +0 -0
  31. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/parsimony_utils.py +0 -0
  32. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/plot_config.py +0 -0
  33. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/quartet_utils.py +0 -0
  34. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/stats_summary.py +0 -0
  35. {phykit-2.1.73 → phykit-2.1.75}/phykit/helpers/streaming.py +0 -0
  36. {phykit-2.1.73 → phykit-2.1.75}/phykit/service_factories.py +0 -0
  37. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/__init__.py +0 -0
  38. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/__init__.py +0 -0
  39. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/alignment_entropy.py +0 -0
  40. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/alignment_length.py +0 -0
  41. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  42. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  43. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/alignment_recoding.py +0 -0
  44. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/alignment_subsample.py +0 -0
  45. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/base.py +0 -0
  46. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/column_score.py +0 -0
  47. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/composition_per_taxon.py +0 -0
  48. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  49. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  50. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/dfoil.py +0 -0
  51. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/dna_threader.py +0 -0
  52. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/dstatistic.py +0 -0
  53. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  54. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/faidx.py +0 -0
  55. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/gc_content.py +0 -0
  56. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/identity_matrix.py +0 -0
  57. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/mask_alignment.py +0 -0
  58. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  59. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/pairwise_identity.py +0 -0
  60. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  61. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/phylo_gwas.py +0 -0
  62. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  63. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/rcv.py +0 -0
  64. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/rcvt.py +0 -0
  65. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  66. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  67. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/alignment/variable_sites.py +0 -0
  68. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/base.py +0 -0
  69. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/__init__.py +0 -0
  70. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/base.py +0 -0
  71. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/bipartition_support_stats.py +0 -0
  72. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/branch_length_multiplier.py +0 -0
  73. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/character_map.py +0 -0
  74. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/collapse_branches.py +0 -0
  75. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/consensus_tree.py +0 -0
  76. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/cophylo.py +0 -0
  77. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  78. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/dvmc.py +0 -0
  79. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/evo_tempo_map.py +0 -0
  80. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/evolutionary_rate.py +0 -0
  81. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/fit_continuous.py +0 -0
  82. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/fit_discrete.py +0 -0
  83. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  84. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/independent_contrasts.py +0 -0
  85. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/internal_branch_stats.py +0 -0
  86. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/internode_labeler.py +0 -0
  87. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/kf_distance.py +0 -0
  88. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  89. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/lb_score.py +0 -0
  90. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/ltt.py +0 -0
  91. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/monophyly_check.py +0 -0
  92. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  93. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/network_signal.py +0 -0
  94. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/ou_shift_detection.py +0 -0
  95. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/ouwie.py +0 -0
  96. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/parsimony_score.py +0 -0
  97. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/patristic_distances.py +0 -0
  98. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylo_heatmap.py +0 -0
  99. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylo_impute.py +0 -0
  100. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylo_logistic.py +0 -0
  101. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylogenetic_glm.py +0 -0
  102. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  103. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylogenetic_regression.py +0 -0
  104. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylogenetic_signal.py +0 -0
  105. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/phylomorphospace.py +0 -0
  106. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/polytomy_test.py +0 -0
  107. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/print_tree.py +0 -0
  108. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/prune_tree.py +0 -0
  109. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/quartet_network.py +0 -0
  110. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/quartet_pie.py +0 -0
  111. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/relative_rate_test.py +0 -0
  112. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/rename_tree_tips.py +0 -0
  113. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/rf_distance.py +0 -0
  114. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/root_tree.py +0 -0
  115. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/saturation.py +0 -0
  116. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/spectral_discordance.py +0 -0
  117. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/spurious_sequence.py +0 -0
  118. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/terminal_branch_stats.py +0 -0
  119. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/threshold_model.py +0 -0
  120. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/tip_labels.py +0 -0
  121. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  122. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  123. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/total_tree_length.py +0 -0
  124. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/trait_correlation.py +0 -0
  125. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/tree_space.py +0 -0
  126. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/treeness.py +0 -0
  127. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/treeness_over_rcv.py +0 -0
  128. {phykit-2.1.73 → phykit-2.1.75}/phykit/services/tree/vcv_utils.py +0 -0
  129. {phykit-2.1.73 → phykit-2.1.75}/phykit.egg-info/SOURCES.txt +0 -0
  130. {phykit-2.1.73 → phykit-2.1.75}/phykit.egg-info/dependency_links.txt +0 -0
  131. {phykit-2.1.73 → phykit-2.1.75}/phykit.egg-info/entry_points.txt +0 -0
  132. {phykit-2.1.73 → phykit-2.1.75}/phykit.egg-info/requires.txt +0 -0
  133. {phykit-2.1.73 → phykit-2.1.75}/phykit.egg-info/top_level.txt +0 -0
  134. {phykit-2.1.73 → phykit-2.1.75}/setup.cfg +0 -0
  135. {phykit-2.1.73 → phykit-2.1.75}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.73
3
+ Version: 2.1.75
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -5759,6 +5759,15 @@ class Phykit:
5759
5759
  circular splits network
5760
5760
  plot (optional)
5761
5761
 
5762
+ --max-splits maximum number of splits
5763
+ to include in the network
5764
+ graph (default: 30; higher
5765
+ splits are still reported
5766
+ in text/JSON output)
5767
+
5768
+ --histogram output filename for a split
5769
+ frequency histogram (optional)
5770
+
5762
5771
  --fig-width figure width in inches
5763
5772
  (auto-scaled if omitted)
5764
5773
 
@@ -5833,6 +5842,20 @@ class Phykit:
5833
5842
  required=False,
5834
5843
  help=SUPPRESS,
5835
5844
  )
5845
+ parser.add_argument(
5846
+ "--max-splits",
5847
+ type=int,
5848
+ default=30,
5849
+ required=False,
5850
+ help=SUPPRESS,
5851
+ )
5852
+ parser.add_argument(
5853
+ "--histogram",
5854
+ type=str,
5855
+ default=None,
5856
+ required=False,
5857
+ help=SUPPRESS,
5858
+ )
5836
5859
  add_plot_arguments(parser)
5837
5860
  _add_json_argument(parser)
5838
5861
  _run_service(parser, argv, ConsensusNetwork)
@@ -7598,6 +7621,9 @@ class Phykit:
7598
7621
 
7599
7622
  -v/--verbose print per-branch details
7600
7623
 
7624
+ --annotate show gCF values on the
7625
+ plot near each branch
7626
+
7601
7627
  --fig-width figure width in inches
7602
7628
  (auto-scaled if omitted)
7603
7629
 
@@ -7662,6 +7688,10 @@ class Phykit:
7662
7688
  parser.add_argument(
7663
7689
  "-v", "--verbose", action="store_true", required=False, help=SUPPRESS
7664
7690
  )
7691
+ parser.add_argument(
7692
+ "--annotate", action="store_true", required=False, default=False,
7693
+ help=SUPPRESS,
7694
+ )
7665
7695
  add_plot_arguments(parser)
7666
7696
  _add_json_argument(parser)
7667
7697
  _run_service(parser, argv, DiscordanceAsymmetry)
@@ -939,7 +939,9 @@ class AncestralReconstruction(Tree):
939
939
 
940
940
  # Tip labels
941
941
  max_x = max(node_x.values()) if node_x else 1.0
942
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
942
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
943
+ if label_fontsize > 0:
944
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
943
945
 
944
946
  # Apply color annotations (range + label only; branches are trait-colored)
945
947
  if self.plot_config.color_file:
@@ -1012,11 +1014,13 @@ class AncestralReconstruction(Tree):
1012
1014
  # Tip labels
1013
1015
  max_x = max(node_x.values()) if node_x else 0
1014
1016
  offset = max_x * 0.02
1015
- for tip in tips:
1016
- ax.text(
1017
- node_x[id(tip)] + offset, node_y[id(tip)],
1018
- tip.name, va="center", fontsize=9,
1019
- )
1017
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
1018
+ if label_fontsize > 0:
1019
+ for tip in tips:
1020
+ ax.text(
1021
+ node_x[id(tip)] + offset, node_y[id(tip)],
1022
+ tip.name, va="center", fontsize=label_fontsize,
1023
+ )
1020
1024
 
1021
1025
  # Apply color annotations (range + label only; branches are trait-colored)
1022
1026
  if self.plot_config.color_file:
@@ -1655,7 +1659,9 @@ class AncestralReconstruction(Tree):
1655
1659
  start_angle += sweep
1656
1660
 
1657
1661
  # Tip labels
1658
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
1662
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
1663
+ if label_fontsize > 0:
1664
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
1659
1665
 
1660
1666
  # Apply color annotations
1661
1667
  if self.plot_config.color_file:
@@ -1750,12 +1756,14 @@ class AncestralReconstruction(Tree):
1750
1756
  # Tip labels with state color
1751
1757
  max_x_val = max(node_x.values()) if node_x else 0
1752
1758
  offset = max_x_val * 0.02
1753
- for tip in tips:
1754
- color = state_colors.get(tip_states.get(tip.name, ""), "black")
1755
- ax.text(
1756
- node_x[id(tip)] + offset, node_y[id(tip)],
1757
- tip.name, va="center", fontsize=9, color=color,
1758
- )
1759
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
1760
+ if label_fontsize > 0:
1761
+ for tip in tips:
1762
+ color = state_colors.get(tip_states.get(tip.name, ""), "black")
1763
+ ax.text(
1764
+ node_x[id(tip)] + offset, node_y[id(tip)],
1765
+ tip.name, va="center", fontsize=label_fontsize, color=color,
1766
+ )
1759
1767
 
1760
1768
  # Apply color annotations
1761
1769
  if self.plot_config.color_file:
@@ -844,7 +844,9 @@ class ConcordanceAsr(Tree):
844
844
 
845
845
  # Tip labels
846
846
  max_x = max(node_x.values()) if node_x else 1.0
847
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
847
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
848
+ if label_fontsize > 0:
849
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
848
850
 
849
851
  # Apply color annotations (range + label only; branches are trait-colored)
850
852
  if self.plot_config.color_file:
@@ -915,11 +917,13 @@ class ConcordanceAsr(Tree):
915
917
  # Tip labels
916
918
  max_x = max(node_x.values()) if node_x else 0
917
919
  offset = max_x * 0.02
918
- for tip in tips:
919
- ax.text(
920
- node_x[id(tip)] + offset, node_y[id(tip)],
921
- tip.name, va="center", fontsize=9,
922
- )
920
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
921
+ if label_fontsize > 0:
922
+ for tip in tips:
923
+ ax.text(
924
+ node_x[id(tip)] + offset, node_y[id(tip)],
925
+ tip.name, va="center", fontsize=label_fontsize,
926
+ )
923
927
 
924
928
  # Apply color annotations (range + label only; branches are trait-colored)
925
929
  if self.plot_config.color_file:
@@ -19,6 +19,8 @@ class ConsensusNetwork(Tree):
19
19
  super().__init__(trees=parsed["trees"])
20
20
  self.threshold = parsed["threshold"]
21
21
  self.missing_taxa = parsed["missing_taxa"]
22
+ self.max_splits = parsed["max_splits"]
23
+ self.histogram = parsed["histogram"]
22
24
  self.plot_output = parsed["plot_output"]
23
25
  self.json_output = parsed["json_output"]
24
26
  self.plot_config = parsed["plot_config"]
@@ -28,6 +30,8 @@ class ConsensusNetwork(Tree):
28
30
  trees=args.trees,
29
31
  threshold=args.threshold,
30
32
  missing_taxa=args.missing_taxa,
33
+ max_splits=getattr(args, "max_splits", 30),
34
+ histogram=getattr(args, "histogram", None),
31
35
  plot_output=getattr(args, "plot_output", None),
32
36
  json_output=getattr(args, "json", False),
33
37
  plot_config=PlotConfig.from_args(args),
@@ -414,19 +418,41 @@ class ConsensusNetwork(Tree):
414
418
  )
415
419
 
416
420
  config = self.plot_config
417
- config.resolve(n_rows=len(all_taxa), n_cols=None)
421
+ n = len(ordering)
422
+ # Force square figure for network graphs
423
+ if config.fig_width is None and config.fig_height is None:
424
+ size = max(10, min(30, 8 + n * 0.05))
425
+ config.fig_width = size
426
+ config.fig_height = size
427
+ config.resolve(n_rows=n, n_cols=None)
418
428
  fig, ax = plt.subplots(1, 1, figsize=(config.fig_width, config.fig_height))
419
429
  ax.set_aspect("equal")
420
430
 
431
+ # Determine label fontsize: auto-suppress for large trees
432
+ if config.ylabel_fontsize is not None:
433
+ label_fontsize = config.ylabel_fontsize
434
+ elif n > 100:
435
+ label_fontsize = 0 # auto-hide for very large trees
436
+ elif n > 50:
437
+ label_fontsize = max(3, 8 - (n - 50) * 0.1)
438
+ else:
439
+ label_fontsize = 10
440
+
421
441
  if not splits_list:
422
442
  # No splits: place taxa evenly on a circle
423
443
  for taxon in ordering:
424
444
  angle = angles[taxon]
425
445
  x = math.cos(angle)
426
446
  y = math.sin(angle)
427
- deg = math.degrees(angle)
428
- ha = "left" if -90 < deg < 90 or deg > 270 else "right"
429
- ax.text(x, y, taxon, ha=ha, va="center", fontsize=10)
447
+ if label_fontsize > 0:
448
+ deg = math.degrees(angle)
449
+ ha = "left" if -90 < deg < 90 or deg > 270 else "right"
450
+ rotation = deg if -90 < deg < 90 or deg > 270 else deg + 180
451
+ ax.text(x, y, taxon, ha=ha, va="center",
452
+ fontsize=label_fontsize, rotation=rotation,
453
+ rotation_mode="anchor")
454
+ else:
455
+ ax.plot(x, y, "o", color="black", markersize=2)
430
456
  else:
431
457
  # Compute node positions: pos = sum(sign_i * weight_i * dir_i) / 2
432
458
  node_positions = {}
@@ -453,6 +479,16 @@ class ConsensusNetwork(Tree):
453
479
  x2, y2 = node_positions[s2]
454
480
  ax.plot([x1, x2], [y1, y2], "-", color="black", linewidth=1.5, zorder=2)
455
481
 
482
+ # Find taxa on split boundaries (participating in displayed splits)
483
+ boundary_taxa = set()
484
+ for split, count, freq in circular_splits:
485
+ for i in range(n):
486
+ curr = ordering[i]
487
+ nxt = ordering[(i + 1) % n]
488
+ if (curr in split) != (nxt in split):
489
+ boundary_taxa.add(curr)
490
+ boundary_taxa.add(nxt)
491
+
456
492
  # Draw pendant edges and taxon labels
457
493
  for taxon in ordering:
458
494
  angle = angles[taxon]
@@ -460,14 +496,30 @@ class ConsensusNetwork(Tree):
460
496
  nx, ny = node_positions[taxon_signs[taxon]]
461
497
  else:
462
498
  nx, ny = 0.0, 0.0
463
- tx = nx + pendant_len * math.cos(angle)
464
- ty = ny + pendant_len * math.sin(angle)
465
- ax.plot([nx, tx], [ny, ty], "-", color="black", linewidth=1.5, zorder=2)
466
- lx = tx + 0.03 * math.cos(angle)
467
- ly = ty + 0.03 * math.sin(angle)
468
- deg = math.degrees(angle)
469
- ha = "left" if -90 < deg < 90 or deg > 270 else "right"
470
- ax.text(lx, ly, taxon, ha=ha, va="center", fontsize=10, zorder=4)
499
+
500
+ # Only draw full pendant edges for boundary taxa;
501
+ # non-boundary taxa get a short stub or are skipped
502
+ if taxon in boundary_taxa or n <= 50:
503
+ plen = pendant_len
504
+ else:
505
+ plen = pendant_len * 0.3 # short stub
506
+
507
+ tx = nx + plen * math.cos(angle)
508
+ ty = ny + plen * math.sin(angle)
509
+ ax.plot([nx, tx], [ny, ty], "-", color="black",
510
+ linewidth=0.8 if taxon not in boundary_taxa else 1.5,
511
+ alpha=0.3 if taxon not in boundary_taxa else 1.0,
512
+ zorder=2)
513
+
514
+ if label_fontsize > 0 and (taxon in boundary_taxa or n <= 50):
515
+ lx = tx + 0.03 * math.cos(angle)
516
+ ly = ty + 0.03 * math.sin(angle)
517
+ deg = math.degrees(angle)
518
+ ha = "left" if -90 < deg < 90 or deg > 270 else "right"
519
+ rotation = deg if -90 < deg < 90 or deg > 270 else deg + 180
520
+ ax.text(lx, ly, taxon, ha=ha, va="center",
521
+ fontsize=label_fontsize, rotation=rotation,
522
+ rotation_mode="anchor", zorder=4)
471
523
 
472
524
  # Frequency labels on internal edges (one per split)
473
525
  labeled_splits = set()
@@ -497,6 +549,35 @@ class ConsensusNetwork(Tree):
497
549
  # Output
498
550
  # ------------------------------------------------------------------
499
551
 
552
+ def _draw_histogram(self, split_counts, n_trees, output_path):
553
+ """Draw a histogram of split frequencies."""
554
+ import matplotlib
555
+ matplotlib.use("Agg")
556
+ import matplotlib.pyplot as plt
557
+
558
+ config = self.plot_config
559
+ config.resolve(n_rows=10, n_cols=None)
560
+
561
+ frequencies = [count / n_trees for count in split_counts.values()]
562
+
563
+ fig, ax = plt.subplots(figsize=(config.fig_width or 10, config.fig_height or 6))
564
+ ax.hist(frequencies, bins=50, color="#377eb8", edgecolor="black", linewidth=0.5)
565
+ ax.set_xlabel("Split frequency", fontsize=config.axis_fontsize or 12)
566
+ ax.set_ylabel("Number of splits", fontsize=config.axis_fontsize or 12)
567
+ ax.axvline(x=self.threshold, color="red", linestyle="--", lw=1,
568
+ label=f"Threshold ({self.threshold})")
569
+ ax.legend(fontsize=9)
570
+
571
+ if config.show_title:
572
+ ax.set_title(
573
+ config.title or "Split Frequency Distribution",
574
+ fontsize=config.title_fontsize or 14,
575
+ )
576
+
577
+ fig.tight_layout()
578
+ fig.savefig(output_path, dpi=config.dpi, bbox_inches="tight")
579
+ plt.close(fig)
580
+
500
581
  def _format_split(self, split: frozenset) -> str:
501
582
  return "{" + ", ".join(sorted(split)) + "}"
502
583
 
@@ -551,6 +632,35 @@ class ConsensusNetwork(Tree):
551
632
  for split, count, freq in filtered:
552
633
  print(f"{self._format_split(split)}\t{count}/{n_trees}\t{freq:.4f}")
553
634
 
635
+ if self.histogram:
636
+ self._draw_histogram(split_counts, n_trees, self.histogram)
637
+ if not self.json_output:
638
+ print(f"Histogram saved: {self.histogram}")
639
+
554
640
  if self.plot_output:
641
+ import sys
642
+ n_taxa = len(all_taxa)
643
+
644
+ if n_taxa > 100:
645
+ print(
646
+ f"Warning: {n_taxa} taxa — network graph may not be "
647
+ f"informative at this scale. Consider using "
648
+ f"--histogram for a split frequency distribution instead.",
649
+ file=sys.stderr,
650
+ )
651
+
555
652
  ordering = self._compute_circular_ordering(trees, all_taxa)
556
- self._draw_network(ordering, filtered, all_taxa, self.plot_output)
653
+
654
+ # Cap splits for graph visualization to avoid exponential blowup
655
+ plot_filtered = filtered
656
+ if len(filtered) > self.max_splits:
657
+ print(
658
+ f"Warning: {len(filtered)} splits above threshold; "
659
+ f"using top {self.max_splits} for network graph "
660
+ f"(use --max-splits to adjust).",
661
+ file=sys.stderr,
662
+ )
663
+ # filtered is already sorted by frequency (descending)
664
+ plot_filtered = filtered[:self.max_splits]
665
+
666
+ self._draw_network(ordering, plot_filtered, all_taxa, self.plot_output)
@@ -522,7 +522,9 @@ class ContMap(Tree):
522
522
 
523
523
  # Tip labels
524
524
  max_x = max(node_x.values()) if node_x else 1.0
525
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
525
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
526
+ if label_fontsize > 0:
527
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
526
528
 
527
529
  # Apply color annotations (range + label only; branches are trait-colored)
528
530
  if self.plot_config.color_file:
@@ -592,11 +594,13 @@ class ContMap(Tree):
592
594
  # Tip labels
593
595
  max_x = max(node_x.values()) if node_x else 0
594
596
  offset = max_x * 0.02
595
- for tip in tips:
596
- ax.text(
597
- node_x[id(tip)] + offset, node_y[id(tip)],
598
- tip.name, va="center", fontsize=9,
599
- )
597
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
598
+ if label_fontsize > 0:
599
+ for tip in tips:
600
+ ax.text(
601
+ node_x[id(tip)] + offset, node_y[id(tip)],
602
+ tip.name, va="center", fontsize=label_fontsize,
603
+ )
600
604
 
601
605
  # Apply color annotations (range + label only; branches are trait-colored)
602
606
  if self.plot_config.color_file:
@@ -347,7 +347,9 @@ class DensityMap(Tree):
347
347
 
348
348
  # Tip labels
349
349
  max_x = max(node_x.values()) if node_x else 1.0
350
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
350
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
351
+ if label_fontsize > 0:
352
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
351
353
 
352
354
  # Apply color annotations (range + label only; branches are trait-colored)
353
355
  if self.plot_config.color_file:
@@ -453,11 +455,13 @@ class DensityMap(Tree):
453
455
  # Tip labels
454
456
  max_x = max(node_x.values()) if node_x else 0
455
457
  offset = max_x * 0.02
456
- for tip in tips:
457
- ax.text(
458
- node_x[id(tip)] + offset, node_y[id(tip)],
459
- tip.name, va="center", fontsize=9,
460
- )
458
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
459
+ if label_fontsize > 0:
460
+ for tip in tips:
461
+ ax.text(
462
+ node_x[id(tip)] + offset, node_y[id(tip)],
463
+ tip.name, va="center", fontsize=label_fontsize,
464
+ )
461
465
 
462
466
  # Apply color annotations (range + label only; branches are trait-colored)
463
467
  if self.plot_config.color_file:
@@ -33,6 +33,7 @@ class DiscordanceAsymmetry(Tree):
33
33
  super().__init__(tree_file_path=parsed["tree_file_path"])
34
34
  self.gene_trees_path = parsed["gene_trees_path"]
35
35
  self.verbose = parsed["verbose"]
36
+ self.annotate = parsed["annotate"]
36
37
  self.json_output = parsed["json_output"]
37
38
  self.plot_output = parsed["plot_output"]
38
39
  self.plot_config = parsed["plot_config"]
@@ -42,6 +43,7 @@ class DiscordanceAsymmetry(Tree):
42
43
  tree_file_path=args.tree,
43
44
  gene_trees_path=args.gene_trees,
44
45
  verbose=getattr(args, "verbose", False),
46
+ annotate=getattr(args, "annotate", False),
45
47
  json_output=getattr(args, "json", False),
46
48
  plot_output=getattr(args, "plot_output", None),
47
49
  plot_config=PlotConfig.from_args(args),
@@ -314,7 +316,9 @@ class DiscordanceAsymmetry(Tree):
314
316
 
315
317
  # Tip labels
316
318
  max_x = max(node_x.values()) if node_x else 1.0
317
- draw_circular_tip_labels(ax, species_tree, coords, fontsize=9, offset=max_x * 0.02)
319
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
320
+ if label_fontsize > 0:
321
+ draw_circular_tip_labels(ax, species_tree, coords, fontsize=label_fontsize, offset=max_x * 0.02)
318
322
 
319
323
  # Apply color annotations (range + label only; branches are trait-colored)
320
324
  if self.plot_config.color_file:
@@ -345,23 +349,28 @@ class DiscordanceAsymmetry(Tree):
345
349
 
346
350
  total = entry["n_concordant"] + entry["n_alt1"] + entry["n_alt2"]
347
351
  gcf = entry["n_concordant"] / total if total > 0 else 1.0
348
- ax.annotate(
349
- f"gCF={gcf:.2f}",
350
- (cx, cy),
351
- textcoords="offset points",
352
- xytext=(5, 5),
353
- fontsize=7,
354
- )
352
+
353
+ if self.annotate:
354
+ ax.annotate(
355
+ f"{gcf:.2f}",
356
+ (cx, cy),
357
+ textcoords="offset points",
358
+ xytext=(5, 5),
359
+ fontsize=max(4, 7 - n_tips * 0.01),
360
+ )
355
361
 
356
362
  if (entry["fdr_p"] is not None and entry["fdr_p"] < 0.05
357
363
  and entry["favored_alt"] is not None):
358
364
  ax.scatter(cx, cy, s=100, c="red", marker="*", zorder=5)
359
365
 
360
- # Colorbar
361
- sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
362
- sm.set_array([])
363
- cbar = fig.colorbar(sm, ax=ax, pad=0.15)
364
- cbar.set_label("Asymmetry ratio")
366
+ # Colorbar (hide if legend-position is none)
367
+ legend_loc = config.legend_position or "upper right"
368
+ if legend_loc != "none":
369
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
370
+ sm.set_array([])
371
+ cbar = fig.colorbar(sm, ax=ax, pad=0.15)
372
+ cbar_fontsize = config.axis_fontsize if config.axis_fontsize else 10
373
+ cbar.set_label("Asymmetry ratio", fontsize=cbar_fontsize)
365
374
 
366
375
  if config.show_title:
367
376
  ax.set_title(config.title or "Discordance Asymmetry", fontsize=config.title_fontsize)
@@ -409,16 +418,18 @@ class DiscordanceAsymmetry(Tree):
409
418
  x = node_x.get(id(clade), 0)
410
419
  y = node_y.get(id(clade), 0)
411
420
 
412
- # Show gCF value
421
+ # Show gCF value (only if --annotate)
413
422
  total = entry["n_concordant"] + entry["n_alt1"] + entry["n_alt2"]
414
423
  gcf = entry["n_concordant"] / total if total > 0 else 1.0
415
- ax.annotate(
416
- f"gCF={gcf:.2f}",
417
- (x, y),
418
- textcoords="offset points",
419
- xytext=(5, 5),
420
- fontsize=7,
421
- )
424
+
425
+ if self.annotate:
426
+ ax.annotate(
427
+ f"{gcf:.2f}",
428
+ (x, y),
429
+ textcoords="offset points",
430
+ xytext=(5, 5),
431
+ fontsize=max(4, 7 - n_tips * 0.01),
432
+ )
422
433
 
423
434
  # Mark significant branches (FDR < 0.05)
424
435
  if (entry["fdr_p"] is not None and entry["fdr_p"] < 0.05
@@ -426,13 +437,15 @@ class DiscordanceAsymmetry(Tree):
426
437
  ax.scatter(x, y, s=100, c="red", marker="*", zorder=5)
427
438
 
428
439
  # Tip labels
429
- max_x = max(node_x.values()) if node_x else 0
430
- offset = max_x * 0.02
431
- for tip in tips:
432
- ax.text(
433
- node_x[id(tip)] + offset, node_y[id(tip)],
434
- tip.name, va="center", fontsize=9,
435
- )
440
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
441
+ if label_fontsize > 0:
442
+ max_x = max(node_x.values()) if node_x else 0
443
+ offset = max_x * 0.02
444
+ for tip in tips:
445
+ ax.text(
446
+ node_x[id(tip)] + offset, node_y[id(tip)],
447
+ tip.name, va="center", fontsize=label_fontsize,
448
+ )
436
449
 
437
450
  # Apply color annotations (range + label only; branches are trait-colored)
438
451
  if self.plot_config.color_file:
@@ -450,11 +463,14 @@ class DiscordanceAsymmetry(Tree):
450
463
  if color_legend:
451
464
  ax.legend(handles=color_legend, loc="upper right", fontsize=8, frameon=True)
452
465
 
453
- # Colorbar
454
- sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
455
- sm.set_array([])
456
- cbar = fig.colorbar(sm, ax=ax, pad=0.15)
457
- cbar.set_label("Asymmetry ratio")
466
+ # Colorbar (hide if legend-position is none)
467
+ legend_loc = config.legend_position or "upper right"
468
+ if legend_loc != "none":
469
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
470
+ sm.set_array([])
471
+ cbar = fig.colorbar(sm, ax=ax, pad=0.15)
472
+ cbar_fontsize = config.axis_fontsize if config.axis_fontsize else 10
473
+ cbar.set_label("Asymmetry ratio", fontsize=cbar_fontsize)
458
474
 
459
475
  ax.set_xlabel("Branch length (subs/site)")
460
476
  ax.set_yticks([])
@@ -487,12 +487,14 @@ class Phenogram(Tree):
487
487
  # Label tips with taxon names (offset to the right)
488
488
  max_x = max(node_x.values()) if node_x else 0
489
489
  offset = max_x * 0.02
490
- for tip in tips:
491
- if id(tip) in node_x and tip.name in trait_values:
492
- ax.text(
493
- node_x[id(tip)] + offset, trait_values[tip.name],
494
- tip.name, va="center", fontsize=9,
495
- )
490
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
491
+ if label_fontsize > 0:
492
+ for tip in tips:
493
+ if id(tip) in node_x and tip.name in trait_values:
494
+ ax.text(
495
+ node_x[id(tip)] + offset, trait_values[tip.name],
496
+ tip.name, va="center", fontsize=label_fontsize,
497
+ )
496
498
 
497
499
  ax.set_xlabel("Distance from root")
498
500
  ax.set_ylabel("Trait value")
@@ -759,7 +759,9 @@ class RateHeterogeneity(Tree):
759
759
 
760
760
  # Tip labels
761
761
  max_x = max(node_x.values()) if node_x else 1.0
762
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.02)
762
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
763
+ if label_fontsize > 0:
764
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.02)
763
765
 
764
766
  # Apply color annotations (range + label only; branches are trait-colored)
765
767
  if self.plot_config.color_file:
@@ -825,11 +827,13 @@ class RateHeterogeneity(Tree):
825
827
 
826
828
  max_x = max(node_x.values()) if node_x else 0
827
829
  offset = max_x * 0.02
828
- for tip in tips:
829
- ax.text(
830
- node_x[id(tip)] + offset, node_y[id(tip)],
831
- tip.name, va="center", fontsize=9,
832
- )
830
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
831
+ if label_fontsize > 0:
832
+ for tip in tips:
833
+ ax.text(
834
+ node_x[id(tip)] + offset, node_y[id(tip)],
835
+ tip.name, va="center", fontsize=label_fontsize,
836
+ )
833
837
 
834
838
  # Apply color annotations (range + label only; branches are trait-colored)
835
839
  if self.plot_config.color_file:
@@ -600,7 +600,9 @@ class StochasticCharacterMap(Tree):
600
600
 
601
601
  # Tip labels
602
602
  max_x = max(node_x.values()) if node_x else 1.0
603
- draw_circular_tip_labels(ax, tree, coords, fontsize=9, offset=max_x * 0.03)
603
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
604
+ if label_fontsize > 0:
605
+ draw_circular_tip_labels(ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03)
604
606
 
605
607
  # Apply color annotations (range + label only; branches are trait-colored)
606
608
  if self.plot_config.color_file:
@@ -677,11 +679,13 @@ class StochasticCharacterMap(Tree):
677
679
  # Tip labels
678
680
  max_x = max(node_x.values()) if node_x else 0
679
681
  offset = max_x * 0.02
680
- for tip in tips:
681
- ax.text(
682
- node_x[id(tip)] + offset, node_y[id(tip)],
683
- tip.name, va="center", fontsize=9
684
- )
682
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
683
+ if label_fontsize > 0:
684
+ for tip in tips:
685
+ ax.text(
686
+ node_x[id(tip)] + offset, node_y[id(tip)],
687
+ tip.name, va="center", fontsize=label_fontsize
688
+ )
685
689
 
686
690
  # Apply color annotations (range + label only; branches are trait-colored)
687
691
  if self.plot_config.color_file:
@@ -544,9 +544,11 @@ class TraitRateMap(Tree):
544
544
 
545
545
  # Tip labels
546
546
  max_x = max(node_x.values()) if node_x else 1.0
547
- draw_circular_tip_labels(
548
- ax, tree, coords, fontsize=9, offset=max_x * 0.03
549
- )
547
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
548
+ if label_fontsize > 0:
549
+ draw_circular_tip_labels(
550
+ ax, tree, coords, fontsize=label_fontsize, offset=max_x * 0.03
551
+ )
550
552
 
551
553
  # Color annotations
552
554
  if self.plot_config.color_file:
@@ -611,11 +613,13 @@ class TraitRateMap(Tree):
611
613
  # Tip labels
612
614
  max_x = max(node_x.values()) if node_x else 0
613
615
  offset = max_x * 0.02
614
- for tip in tips:
615
- ax.text(
616
- node_x[id(tip)] + offset, node_y[id(tip)],
617
- tip.name, va="center", fontsize=9,
618
- )
616
+ label_fontsize = config.ylabel_fontsize if config.ylabel_fontsize is not None else 9
617
+ if label_fontsize > 0:
618
+ for tip in tips:
619
+ ax.text(
620
+ node_x[id(tip)] + offset, node_y[id(tip)],
621
+ tip.name, va="center", fontsize=label_fontsize,
622
+ )
619
623
 
620
624
  # Color annotations
621
625
  if self.plot_config.color_file:
@@ -0,0 +1 @@
1
+ __version__ = "2.1.75"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.73
3
+ Version: 2.1.75
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -1 +0,0 @@
1
- __version__ = "2.1.73"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes