phykit 2.1.74__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.74 → phykit-2.1.75}/PKG-INFO +1 -1
  2. {phykit-2.1.74 → phykit-2.1.75}/phykit/phykit.py +23 -0
  3. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/consensus_network.py +123 -13
  4. phykit-2.1.75/phykit/version.py +1 -0
  5. {phykit-2.1.74 → phykit-2.1.75}/phykit.egg-info/PKG-INFO +1 -1
  6. phykit-2.1.74/phykit/version.py +0 -1
  7. {phykit-2.1.74 → phykit-2.1.75}/LICENSE.md +0 -0
  8. {phykit-2.1.74 → phykit-2.1.75}/README.md +0 -0
  9. {phykit-2.1.74 → phykit-2.1.75}/phykit/__init__.py +0 -0
  10. {phykit-2.1.74 → phykit-2.1.75}/phykit/__main__.py +0 -0
  11. {phykit-2.1.74 → phykit-2.1.75}/phykit/cli_registry.py +0 -0
  12. {phykit-2.1.74 → phykit-2.1.75}/phykit/errors.py +0 -0
  13. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/__init__.py +0 -0
  14. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/boolean_argument_parsing.py +0 -0
  15. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/caching.py +0 -0
  16. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/circular_layout.py +0 -0
  17. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/color_annotations.py +0 -0
  18. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/discrete_models.py +0 -0
  19. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/files.py +0 -0
  20. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/json_output.py +0 -0
  21. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/parallel.py +0 -0
  22. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/parsimony_utils.py +0 -0
  23. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/plot_config.py +0 -0
  24. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/quartet_utils.py +0 -0
  25. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/stats_summary.py +0 -0
  26. {phykit-2.1.74 → phykit-2.1.75}/phykit/helpers/streaming.py +0 -0
  27. {phykit-2.1.74 → phykit-2.1.75}/phykit/service_factories.py +0 -0
  28. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/__init__.py +0 -0
  29. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/__init__.py +0 -0
  30. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/alignment_entropy.py +0 -0
  31. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/alignment_length.py +0 -0
  32. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  33. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  34. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/alignment_recoding.py +0 -0
  35. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/alignment_subsample.py +0 -0
  36. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/base.py +0 -0
  37. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/column_score.py +0 -0
  38. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/composition_per_taxon.py +0 -0
  39. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  40. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  41. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/dfoil.py +0 -0
  42. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/dna_threader.py +0 -0
  43. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/dstatistic.py +0 -0
  44. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  45. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/faidx.py +0 -0
  46. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/gc_content.py +0 -0
  47. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/identity_matrix.py +0 -0
  48. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/mask_alignment.py +0 -0
  49. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  50. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/pairwise_identity.py +0 -0
  51. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  52. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/phylo_gwas.py +0 -0
  53. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  54. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/rcv.py +0 -0
  55. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/rcvt.py +0 -0
  56. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  57. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  58. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/alignment/variable_sites.py +0 -0
  59. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/base.py +0 -0
  60. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/__init__.py +0 -0
  61. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  62. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/base.py +0 -0
  63. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/bipartition_support_stats.py +0 -0
  64. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/branch_length_multiplier.py +0 -0
  65. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/character_map.py +0 -0
  66. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/collapse_branches.py +0 -0
  67. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/concordance_asr.py +0 -0
  68. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/consensus_tree.py +0 -0
  69. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/cont_map.py +0 -0
  70. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/cophylo.py +0 -0
  71. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  72. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/density_map.py +0 -0
  73. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/discordance_asymmetry.py +0 -0
  74. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/dvmc.py +0 -0
  75. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/evo_tempo_map.py +0 -0
  76. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/evolutionary_rate.py +0 -0
  77. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/fit_continuous.py +0 -0
  78. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/fit_discrete.py +0 -0
  79. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  80. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/independent_contrasts.py +0 -0
  81. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/internal_branch_stats.py +0 -0
  82. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/internode_labeler.py +0 -0
  83. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/kf_distance.py +0 -0
  84. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  85. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/lb_score.py +0 -0
  86. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/ltt.py +0 -0
  87. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/monophyly_check.py +0 -0
  88. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  89. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/network_signal.py +0 -0
  90. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/ou_shift_detection.py +0 -0
  91. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/ouwie.py +0 -0
  92. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/parsimony_score.py +0 -0
  93. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/patristic_distances.py +0 -0
  94. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phenogram.py +0 -0
  95. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylo_heatmap.py +0 -0
  96. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylo_impute.py +0 -0
  97. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylo_logistic.py +0 -0
  98. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylogenetic_glm.py +0 -0
  99. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  100. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylogenetic_regression.py +0 -0
  101. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylogenetic_signal.py +0 -0
  102. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/phylomorphospace.py +0 -0
  103. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/polytomy_test.py +0 -0
  104. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/print_tree.py +0 -0
  105. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/prune_tree.py +0 -0
  106. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/quartet_network.py +0 -0
  107. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/quartet_pie.py +0 -0
  108. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/rate_heterogeneity.py +0 -0
  109. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/relative_rate_test.py +0 -0
  110. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/rename_tree_tips.py +0 -0
  111. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/rf_distance.py +0 -0
  112. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/root_tree.py +0 -0
  113. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/saturation.py +0 -0
  114. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/spectral_discordance.py +0 -0
  115. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/spurious_sequence.py +0 -0
  116. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/stochastic_character_map.py +0 -0
  117. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/terminal_branch_stats.py +0 -0
  118. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/threshold_model.py +0 -0
  119. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/tip_labels.py +0 -0
  120. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  121. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  122. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/total_tree_length.py +0 -0
  123. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/trait_correlation.py +0 -0
  124. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/trait_rate_map.py +0 -0
  125. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/tree_space.py +0 -0
  126. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/treeness.py +0 -0
  127. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/treeness_over_rcv.py +0 -0
  128. {phykit-2.1.74 → phykit-2.1.75}/phykit/services/tree/vcv_utils.py +0 -0
  129. {phykit-2.1.74 → phykit-2.1.75}/phykit.egg-info/SOURCES.txt +0 -0
  130. {phykit-2.1.74 → phykit-2.1.75}/phykit.egg-info/dependency_links.txt +0 -0
  131. {phykit-2.1.74 → phykit-2.1.75}/phykit.egg-info/entry_points.txt +0 -0
  132. {phykit-2.1.74 → phykit-2.1.75}/phykit.egg-info/requires.txt +0 -0
  133. {phykit-2.1.74 → phykit-2.1.75}/phykit.egg-info/top_level.txt +0 -0
  134. {phykit-2.1.74 → phykit-2.1.75}/setup.cfg +0 -0
  135. {phykit-2.1.74 → 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.74
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)
@@ -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)
@@ -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.74
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.74"
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