pycmplot 0.2.8__tar.gz → 0.2.9__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 (34) hide show
  1. {pycmplot-0.2.8 → pycmplot-0.2.9}/PKG-INFO +9 -1
  2. {pycmplot-0.2.8 → pycmplot-0.2.9}/README.md +8 -0
  3. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/__init__.py +1 -1
  4. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/_core.py +1 -2
  5. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/circular.py +12 -32
  6. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/linear.py +346 -20
  7. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/PKG-INFO +9 -1
  8. {pycmplot-0.2.8 → pycmplot-0.2.9}/pyproject.toml +1 -1
  9. {pycmplot-0.2.8 → pycmplot-0.2.9}/setup.cfg +1 -1
  10. {pycmplot-0.2.8 → pycmplot-0.2.9}/LICENSE +0 -0
  11. {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/bench_python.py +0 -0
  12. {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/collect_results.py +0 -0
  13. {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/generate_multi_sumstats.py +0 -0
  14. {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/generate_sumstats.py +0 -0
  15. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/__main__.py +0 -0
  16. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/annotation.py +0 -0
  17. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/cli.py +0 -0
  18. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/constants.py +0 -0
  19. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/Homo_sapiens.GRCh37.geneinfo.tsv.gz +0 -0
  20. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/Homo_sapiens.GRCh38.geneinfo.tsv.gz +0 -0
  21. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/hg18ToHg38.over.chain.gz +0 -0
  22. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/hg19ToHg38.over.chain.gz +0 -0
  23. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/io.py +0 -0
  24. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/liftover.py +0 -0
  25. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/__init__.py +0 -0
  26. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/qq.py +0 -0
  27. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/resources.py +0 -0
  28. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/stats.py +0 -0
  29. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/SOURCES.txt +0 -0
  30. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/dependency_links.txt +0 -0
  31. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/entry_points.txt +0 -0
  32. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/requires.txt +0 -0
  33. {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/top_level.txt +0 -0
  34. {pycmplot-0.2.8 → pycmplot-0.2.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycmplot
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Multi-track circular and linear Manhattan plot generation for GWAS summary statistics
5
5
  Author: Kevin Esoh
6
6
  Author-email: Kevin Esoh <kesohku1@jh.edu>
@@ -143,6 +143,14 @@ for gene annotation. Also included are UCSC chain files for coordinate conversio
143
143
  A potential useful application is **comparative visualization** of results from multiple imputation panels,
144
144
  multiple populations, or multiple traits to observe shared genetic architecture.
145
145
 
146
+
147
+ # Tip
148
+
149
+ For signed non-pvalue statistics such as iHS, use `--signif_threshold` and
150
+ `--suggest_threshold` to specify lines to draw for iHS cutoff above and below zero,
151
+ that is in the positive and negative axes.
152
+
153
+
146
154
  Read more in the package documentation page: https://pycmplot.readthedocs.io/en/latest/
147
155
 
148
156
  ---
@@ -104,6 +104,14 @@ for gene annotation. Also included are UCSC chain files for coordinate conversio
104
104
  A potential useful application is **comparative visualization** of results from multiple imputation panels,
105
105
  multiple populations, or multiple traits to observe shared genetic architecture.
106
106
 
107
+
108
+ # Tip
109
+
110
+ For signed non-pvalue statistics such as iHS, use `--signif_threshold` and
111
+ `--suggest_threshold` to specify lines to draw for iHS cutoff above and below zero,
112
+ that is in the positive and negative axes.
113
+
114
+
107
115
  Read more in the package documentation page: https://pycmplot.readthedocs.io/en/latest/
108
116
 
109
117
  ---
@@ -47,4 +47,4 @@ __all__ = [
47
47
  "ResourceConfig",
48
48
  ]
49
49
 
50
- __version__ = "0.2.8"
50
+ __version__ = "0.2.9"
@@ -298,6 +298,7 @@ def main() -> None:
298
298
  plot_title_size = plot_title_size,
299
299
  no_track_labels = no_track_labels,
300
300
  dpi = dpi,
301
+ ylabel=ylabel,
301
302
  output_format=output_format,
302
303
  output_dir=output_dir
303
304
  )
@@ -311,11 +312,9 @@ def main() -> None:
311
312
  plot_linear(
312
313
  sumstats_loaded=sumstats_loaded,
313
314
  track_heights=t_heights,
314
- trim_pval=trim_pval,
315
315
  logp=True if logp else False,
316
316
  point_size=point_size,
317
317
  highlight=highlight,
318
- highlight_thresh=highlight_thresh,
319
318
  highlight_color=highlight_color,
320
319
  highlight_line=highlight_line,
321
320
  highlight_line_color=highlight_line_color,
@@ -121,7 +121,6 @@ def compute_track_radii_dict(
121
121
  def plot_circosm(
122
122
  sector=None,
123
123
  sector_radius=None,
124
- annotation_r=None,
125
124
  assoc: Optional[pd.DataFrame] = None,
126
125
  assoc_by_chr: pd.DataFrame = None,
127
126
  sector_sizes: Optional[dict] = None,
@@ -137,7 +136,6 @@ def plot_circosm(
137
136
  suggest_line: Optional[float] = 1e-5,
138
137
  suggest_threshold: Optional[float] = 1e-5,
139
138
  highlight: bool = False,
140
- highlight_thresh: Optional[float] = 5e-8,
141
139
  highlight_color: str = 'brown',
142
140
  colors: Optional[list[str]] = ['steelblue','orange'],
143
141
  point_size: float = 6,
@@ -158,9 +156,6 @@ def plot_circosm(
158
156
  sector_radius : tuple of (float, float)
159
157
  ``(r_start, r_end)`` radial limits for this track within *sector*,
160
158
  as returned by :func:`compute_track_radii_dict`.
161
- annotation_r : tuple of (float, float) or None
162
- Radial limits reserved for the annotation ring. Passed for context
163
- but not used directly inside this function.
164
159
  assoc : pandas.DataFrame, optional
165
160
  Full summary statistics DataFrame (all chromosomes). Filtered to the
166
161
  current sector's chromosome internally. Must have columns ``CHR``,
@@ -202,34 +197,16 @@ def plot_circosm(
202
197
  If ``True``, variants within significant loci (``in_locus == True``
203
198
  after :func:`~pycmplot.stats.get_highlight_snps`) are rendered in
204
199
  ``highlight_color`` (see below). Default ``False``.
205
- highlight_thresh : float, optional
206
- P-value threshold passed to
207
- :func:`~pycmplot.stats.get_highlight_snps` when *highlight* is
208
- ``True``. Default ``5e-8``.
209
200
  highlight_color : str, optional
210
201
  Color of highlighted positions when *highlight* is ``True``.
211
202
  Default ``brown``.
212
203
  colors : list of str, optional
213
204
  Two alternating colours for even/odd chromosome numbers.
214
- Default ``['steelblue', 'orange']``.
205
+ Default ``['steelblue', 'orange']``.
215
206
  no_track_labels : bool, optional
216
207
  Suppress the track label on the spacer sector. Default ``False``.
217
208
  """
218
209
 
219
- #if colors is None:
220
- # colors = ["steelblue", "orange"]
221
-
222
- #if highlight:
223
- # assoc, _ = get_highlight_snps(
224
- # df=assoc,
225
- # window=500_000,
226
- # highlight_thresh=highlight_thresh,
227
- # logp=logp,
228
- # )
229
-
230
- #assoc = assoc.copy()
231
- #assoc["POS"] = assoc["POS"].fillna(0).astype(int)
232
-
233
210
  genome_wide_sig = signif_threshold
234
211
  suggestive = suggest_threshold
235
212
 
@@ -372,7 +349,7 @@ def plot_circosm(
372
349
  x=[sector.start, sector.end],
373
350
  y=[suggestive, suggestive],
374
351
  vmin=v_min, vmax=v_max,
375
- color="lightblue", linestyle="--",
352
+ color="navy", linestyle="--",
376
353
  )
377
354
 
378
355
 
@@ -405,6 +382,7 @@ def plot_circular(
405
382
  dpi: Optional[int] = None,
406
383
  output_format: Optional[str] = 'png',
407
384
  output_dir: Optional[str] = '.',
385
+ ylabel: Optional[str] = None,
408
386
  no_track_labels: bool = False
409
387
  ):
410
388
  """Generate a multi-track Circos-style circular Manhattan plot.
@@ -492,6 +470,11 @@ def plot_circular(
492
470
  Default ``'png'``.
493
471
  output_dir : str or pathlib.Path, optional
494
472
  Output directory. Default ``'.'``.
473
+ ylabel : str, optional
474
+ Override the shared y-axis label (left margin). Useful for
475
+ non-p-value statistics such as iHS, F_ST or XP-EHH (e.g.
476
+ ``ylabel="iHS"``). When ``None`` (the default), the label is
477
+ ``"-log₁₀(p-value)"`` if *logp* is ``True`` and ``"P"`` otherwise.
495
478
  no_track_labels : bool, optional
496
479
  Suppress track labels on the spacer sector. Default ``False``.
497
480
 
@@ -614,7 +597,6 @@ def plot_circular(
614
597
  plot_circosm(
615
598
  sector=sector,
616
599
  sector_radius=sector_radius,
617
- annotation_r=annotation_track_radius if annotate else None,
618
600
  sector_sizes=sector_sizes,
619
601
  track_index=index,
620
602
  chrom_label_loc=chrom_label_loc,
@@ -630,7 +612,6 @@ def plot_circular(
630
612
  suggest_line=True if signif_line else False,
631
613
  suggest_threshold=sug_thresh,
632
614
  highlight=highlight,
633
- highlight_thresh=highlight_thresh,
634
615
  highlight_color=highlight_color,
635
616
  colors=colors,
636
617
  point_size=point_size,
@@ -699,18 +680,17 @@ def plot_circular(
699
680
  # ------------------------------------------------------------------
700
681
  for sector in circos.sectors:
701
682
  if sector.name == list(sector_sizes.keys())[-1]:
702
- if logp:
703
- SUB = str.maketrans("0123456789", "₀₁₂₃₄₅₆₇₈₉")
704
- y_label = "-log10(p-value)".translate(SUB)
683
+ if ylabel is None:
684
+ ylabel_text = "-log\u2081\u2080(P)" if logp else "P"
705
685
  else:
706
- y_label = "p-value"
686
+ ylabel_text = ylabel
707
687
 
708
688
  sector_rlim = [t.r_lim for t in sector.tracks]
709
689
  sector_min_r = min(sector_rlim)[0]
710
690
  sector_max_r = max(sector_rlim)[1]
711
691
 
712
692
  sector.text(
713
- y_label,
693
+ ylabel_text,
714
694
  x=sector.end - (sector.end - sector.start) / 5,
715
695
  r=(sector_min_r + sector_max_r) / 2
716
696
  + (sector_min_r + sector_max_r) / 12,
@@ -608,10 +608,348 @@ def _draw_annotation_arrows_multirail(
608
608
  y_text_base + (max_rail + 2) * y_stack_step,
609
609
  )
610
610
 
611
+
612
+ def _draw_annotation_arrows_multirail1(
613
+ ax,
614
+ annot_df,
615
+ chr_col: str,
616
+ label_col: str,
617
+ offsets: dict,
618
+ chr_max: dict,
619
+ spread_width: float = 60e6,
620
+ y_text_base: float = 0.25,
621
+ y_stack_step: float = 0.1,
622
+ max_rad: float = 0.35,
623
+ y_tip: float = 0.0,
624
+ fsize: float = 8,
625
+ rail_frac: float = 0.95,
626
+ min_sep: float = 6e6,
627
+ asize: float = 8,
628
+ ) -> None:
629
+ """
630
+ Dense annotation renderer with relaxation-driven multi-rail
631
+ stacking, linspace rank-reassignment, alternating rail stagger,
632
+ curved arrows, and adaptive ylim.
633
+
634
+ Layout pipeline
635
+ ---------------
636
+ 1. **Relaxation pass**:
637
+ All labels are sorted by ``x_signal`` and a bidirectional
638
+ relaxation loop enforces ``min_sep`` between every adjacent pair.
639
+ Labels are pushed apart until no two are closer than ``min_sep``.
640
+ The relaxed positions are stored as ``x_relaxed``.
641
+
642
+ 2. **Rail assignment from relaxation drift**:
643
+ Each label's rail is determined by how far its relaxed position
644
+ drifted from its signal::
645
+
646
+ drift = |x_relaxed − x_signal|
647
+ rail_id = clip(floor(drift / rail_stride), 0, max_rails − 1)
648
+
649
+ Labels in dense regions drift more and receive higher rail
650
+ indices proportionally. ``rail_stride = rail_width / max_rails``
651
+ so rail assignment scales correctly with ``rail_frac``.
652
+
653
+ 3. **linspace rank-reassignment**:
654
+ Labels are sorted by ``x_signal`` and assigned evenly-spaced
655
+ ``x_text`` slots via ``np.linspace(rail_start, rail_end, n)``.
656
+ This guarantees:
657
+
658
+ - ``x_text`` rank == ``x_signal`` rank → no arrow crossings by
659
+ construction.
660
+ - Full rail coverage regardless of ``rail_frac`` or signal
661
+ clustering.
662
+ - Even slot spacing = ``rail_width / (n − 1)``.
663
+
664
+ 4. **Cyclic rail stagger (k_min-computed)**:
665
+ The minimum number of rails required to avoid horizontal slot
666
+ overlap is computed from the actual slot interval and
667
+ ``char_width``, with a 1.5× safety factor to account for
668
+ underestimation of rendered glyph widths by the ``0.6 * fsize``
669
+ approximation::
670
+
671
+ slot_interval = rail_width / (n − 1)
672
+ k_min = ceil(1.5 * char_width / slot_interval)
673
+
674
+ Labels are then assigned a stagger offset of ``rank % k_min``
675
+ (cycling through 0, 1, …, k_min−1 in left-to-right
676
+ ``x_signal`` order) which is added to their drift-assigned
677
+ ``rail_id``. This guarantees same-rail neighbours are at least
678
+ ``k_min * slot_interval >= 1.5 * char_width`` apart. At lower
679
+ ``rail_frac`` or larger ``fsize``, ``slot_interval`` shrinks and
680
+ ``k_min`` grows automatically, creating more rails as needed.
681
+
682
+ 5. **Rendering pass**:
683
+ ``rail_id`` is read here for the first time to compute
684
+ ``y = y_text_base + rail_id * y_stack_step``.
685
+
686
+ Parameters
687
+ ----------
688
+ ax : matplotlib.axes.Axes
689
+ Target axes.
690
+ annot_df : pd.DataFrame
691
+ Annotation table. Must contain ``chr_col``, ``"x"`` (cumulative
692
+ genomic position in bp), and ``label_col``.
693
+ chr_col : str
694
+ Column name for chromosome identifiers.
695
+ label_col : str
696
+ Column name for annotation labels.
697
+ offsets : dict
698
+ Mapping of chromosome name → cumulative start offset (bp).
699
+ chr_max : dict
700
+ Mapping of chromosome name → chromosome length (bp).
701
+ spread_width : float, optional
702
+ Genomic window (bp) used for arrow tip jitter. Default 60e6.
703
+ y_text_base : float, optional
704
+ Axes-fraction y-coordinate for rail 0 labels. Default 0.25.
705
+ y_stack_step : float, optional
706
+ Axes-fraction increment per rail. Default 0.1.
707
+ max_rad : float, optional
708
+ Maximum arc curvature for ``FancyArrowPatch``. Default 0.35.
709
+ y_tip : float, optional
710
+ Axes-fraction y-coordinate for arrow tips. Default 0.0.
711
+ fsize : float, optional
712
+ Font size (pt) used to estimate label widths. Default 8.
713
+ rail_frac : float, optional
714
+ Fraction of genome width occupied by the label rail. Default 0.95.
715
+ min_sep : float, optional
716
+ Minimum genomic separation (bp) between any two adjacent label
717
+ centres. Default 6e6.
718
+ asize : float, optional
719
+ Font size (pt) for rendered label text. Default 8.
720
+ """
721
+
722
+ # ------------------------------------------------------------------
723
+ # Deduplication
724
+ # ------------------------------------------------------------------
725
+ annot_df = annot_df.drop_duplicates(subset=[chr_col, "x", label_col])
726
+
727
+ # ------------------------------------------------------------------
728
+ # Sort annotations
729
+ # ------------------------------------------------------------------
730
+ annot_df = (
731
+ annot_df
732
+ .sort_values(by=[chr_col, "x"], key=natsort_keygen())
733
+ .reset_index(drop=True)
734
+ )
735
+
736
+ x_signals = annot_df["x"].to_numpy(dtype=float)
737
+ labels = annot_df[label_col].astype(str).to_numpy()
738
+ n = len(x_signals)
739
+
740
+ if n == 0:
741
+ return
742
+
743
+ # ------------------------------------------------------------------
744
+ # Genome span and rail bounds
745
+ # ------------------------------------------------------------------
746
+ genome_start = min(offsets.values())
747
+ genome_end = max(offsets[c] + chr_max[c] for c in chr_max)
748
+ genome_width = genome_end - genome_start
749
+ rail_width = genome_width * rail_frac
750
+ rail_start = genome_start + (genome_width - rail_width) / 2
751
+ rail_end = rail_start + rail_width
752
+
753
+ # ------------------------------------------------------------------
754
+ # Auto char_width from axes geometry
755
+ # ------------------------------------------------------------------
756
+ # For vertical text (rotation=90°) the horizontal footprint of every
757
+ # label is one character wide regardless of string length.
758
+ try:
759
+ fig = ax.get_figure()
760
+ renderer = fig.canvas.get_renderer()
761
+ ax_bbox = ax.get_window_extent(renderer=renderer)
762
+ xmin, xmax = ax.get_xlim()
763
+ px_per_bp = ax_bbox.width / (xmax - xmin)
764
+ char_width = 0.6 * fsize * (fig.dpi / 72.0) / px_per_bp
765
+ except Exception:
766
+ char_width = genome_width * 0.01 / fsize
767
+
768
+ # ------------------------------------------------------------------
769
+ # 1. Relaxation pass
770
+ # ------------------------------------------------------------------
771
+ # Start from x_signal positions and enforce min_sep between every
772
+ # adjacent pair. Bidirectional passes (rightward then leftward)
773
+ # distribute pressure symmetrically so labels spread around their
774
+ # signals rather than cascading in one direction.
775
+ x_relaxed = x_signals.copy()
776
+ max_relax_iter = 50
777
+
778
+ for _ in range(max_relax_iter):
779
+ moved = False
780
+
781
+ # Rightward pass
782
+ for i in range(1, n):
783
+ gap = x_relaxed[i] - x_relaxed[i - 1]
784
+ if gap < min_sep:
785
+ x_relaxed[i] = x_relaxed[i - 1] + min_sep
786
+ moved = True
787
+
788
+ # Leftward pass
789
+ for i in range(n - 2, -1, -1):
790
+ gap = x_relaxed[i + 1] - x_relaxed[i]
791
+ if gap < min_sep:
792
+ x_relaxed[i] = x_relaxed[i + 1] - min_sep
793
+ moved = True
794
+
795
+ if not moved:
796
+ break
797
+
798
+ # ------------------------------------------------------------------
799
+ # 2. Rail assignment from relaxation drift
800
+ # ------------------------------------------------------------------
801
+ # How far each label drifted from its signal during relaxation is a
802
+ # direct measure of local density: labels in dense regions drift more
803
+ # and should be stacked higher. We bin the drift into rails using
804
+ # a stride of (rail_width / max_rails) so rails fill proportionally.
805
+ max_rails = 10
806
+ rail_stride = rail_width / max_rails
807
+ drift = np.abs(x_relaxed - x_signals)
808
+ rail_ids = np.clip(
809
+ (drift / rail_stride).astype(int),
810
+ 0,
811
+ max_rails - 1,
812
+ )
813
+
814
+ # ------------------------------------------------------------------
815
+ # 3. linspace rank-reassignment
816
+ # ------------------------------------------------------------------
817
+ # Sort by x_signal and assign evenly-spaced x_text slots across the
818
+ # full [rail_start, rail_end] range. This guarantees:
819
+ # - x_text rank == x_signal rank → no arrow crossings
820
+ # - Full, even rail coverage regardless of rail_frac
821
+ sig_order = np.argsort(x_signals)
822
+ x_texts = np.empty(n)
823
+ slots = (
824
+ np.linspace(rail_start, rail_end, n)
825
+ if n > 1
826
+ else np.array([(rail_start + rail_end) / 2])
827
+ )
828
+ x_texts[sig_order] = slots
829
+
830
+ # ------------------------------------------------------------------
831
+ # 4. Compute minimum rails required (k_min) and apply cyclic stagger
832
+ # ------------------------------------------------------------------
833
+ # With n labels evenly spaced across rail_width, the slot interval is:
834
+ # slot_interval = rail_width / (n - 1)
835
+ #
836
+ # With a cyclic stagger of k rails, same-rail neighbours are k slots
837
+ # apart, giving a same-rail interval of k * slot_interval.
838
+ # The no-overlap condition requires:
839
+ # k * slot_interval >= char_width
840
+ # → k_min = ceil(char_width / slot_interval)
841
+ #
842
+ # A safety factor of 1.5 is applied to char_width to account for
843
+ # the fact that 0.6 * fsize underestimates the true rendered glyph
844
+ # width, which varies by font and renderer. Without this correction
845
+ # k_min is systematically too small at larger font sizes and lower
846
+ # rail_frac values, causing labels to still visually overlap even
847
+ # after staggering.
848
+ #
849
+ # The stagger cycles through k_min values (0, 1, …, k_min-1) in
850
+ # left-to-right x_signal order. The drift-based rail_id is used as
851
+ # a base elevation and the stagger offset is added on top, so dense
852
+ # regions are still elevated relative to sparse ones while adjacent
853
+ # labels are guaranteed to be on different rails.
854
+ slot_interval = rail_width / max(n - 1, 1)
855
+ char_width_safe = char_width * 3
856
+ k_min = max(1, int(np.ceil(char_width_safe / slot_interval)))
857
+
858
+ for rank, idx in enumerate(sig_order):
859
+ stagger = rank % k_min
860
+ rail_ids[idx] = min(rail_ids[idx] + stagger, max_rails - 1)
861
+
862
+ # ------------------------------------------------------------------
863
+ # Build layout table
864
+ # ------------------------------------------------------------------
865
+ layout = pd.DataFrame({
866
+ "label" : labels,
867
+ "x_signal" : x_signals,
868
+ "x_text" : x_texts,
869
+ "rail_id" : rail_ids,
870
+ }).sort_values("x_signal").reset_index(drop=True)
871
+
872
+ # ------------------------------------------------------------------
873
+ # 4. Rendering pass — rail_id first used here
874
+ # ------------------------------------------------------------------
875
+ jitter = np.linspace(
876
+ -spread_width * 0.03,
877
+ spread_width * 0.03,
878
+ len(layout),
879
+ )
880
+
881
+ for i, row in enumerate(layout.itertuples(index=False)):
882
+ x_sig = row.x_signal
883
+ x_txt = row.x_text
884
+ r_idx = int(row.rail_id)
885
+ label = row.label
886
+ x_tip = x_sig + jitter[i]
887
+ y_txt = y_text_base + r_idx * y_stack_step
888
+
889
+ dx = x_txt - x_sig
890
+ rad = np.clip(
891
+ dx / (genome_width * 0.15),
892
+ -max_rad,
893
+ max_rad,
894
+ )
895
+
896
+ if r_idx == 0:
897
+ anglea = 0
898
+ arma = 0
899
+ armb = 30
900
+ else:
901
+ anglea = -90
902
+ arma = 90 * r_idx
903
+ armb = 30
904
+
905
+ arrow = FancyArrowPatch(
906
+ (x_txt, y_txt),
907
+ (x_tip, y_tip),
908
+ arrowstyle="-|>",
909
+ mutation_scale=6,
910
+ lw=0.4,
911
+ color="grey",
912
+ alpha=0.5,
913
+ connectionstyle=(
914
+ f"arc,"
915
+ f"angleA={anglea},"
916
+ f"armA={arma},"
917
+ f"angleB=90,"
918
+ f"armB={armb},"
919
+ f"rad={rad}"
920
+ ),
921
+ transform=ax.transData,
922
+ )
923
+ ax.add_patch(arrow)
924
+
925
+ ax.text(
926
+ x_txt + r_idx * spread_width * 0.12,
927
+ y_txt + 0.001,
928
+ str(label),
929
+ rotation=90,
930
+ ha="center",
931
+ va="bottom",
932
+ fontsize=asize,
933
+ clip_on=False,
934
+ color="black",
935
+ fontstyle="italic",
936
+ fontweight="regular",
937
+ )
938
+
939
+ # ------------------------------------------------------------------
940
+ # Adaptive ylim
941
+ # ------------------------------------------------------------------
942
+ max_rail = int(layout["rail_id"].max())
943
+ ax.set_ylim(
944
+ y_tip - 0.05,
945
+ y_text_base + (max_rail + 2) * y_stack_step,
946
+ )
947
+
948
+
949
+
611
950
  # ---------------------------------------------------------------------------
612
951
  # Public function
613
952
  # ---------------------------------------------------------------------------
614
-
615
953
  def plot_linearm(
616
954
  tracks: list,
617
955
  track_labels: Optional[list[str]] = None,
@@ -619,11 +957,9 @@ def plot_linearm(
619
957
  annotate: bool = False,
620
958
  annotation_size: float = 8,
621
959
  highlight: bool = False,
622
- highlight_thresh: float = 5e-8,
623
960
  highlight_color: str = 'brown',
624
961
  highlight_line: bool = False,
625
962
  highlight_line_color: str = 'grey',
626
- trim_pval: Optional[float] = None,
627
963
  logp: bool = True,
628
964
  label_col: Optional[str] = 'SNP',
629
965
  chr_order: Optional[list[str]] = None,
@@ -870,11 +1206,8 @@ def plot_linearm(
870
1206
  f"{expected_n} are required"
871
1207
  + (" (one extra for the annotation track)" if annotate else "")
872
1208
  )
873
- except TypeError:
874
- raise TypeError(
875
- "track_heights must be a sized iterable (e.g. list or tuple), "
876
- f"got {type(track_heights).__name__}"
877
- )
1209
+ except Exception:
1210
+ track_heights = [float(x) for x in track_heights]
878
1211
 
879
1212
  # ------------------------------------------------------------------
880
1213
  # y-label position
@@ -1023,9 +1356,9 @@ def plot_linearm(
1023
1356
  if sig_lines is not None and i < len(sig_lines):
1024
1357
  sl = sig_lines[i]
1025
1358
  if "genome" in sl:
1026
- ax.axhline(y=sl["genome"], color="red", linestyle="--", linewidth=0.5)
1359
+ ax.axhline(y=sl["genome"], color="orangered", linestyle="--", linewidth=0.5)
1027
1360
  if "suggestive" in sl:
1028
- ax.axhline(y=sl["suggestive"], color="blue", linestyle="--", linewidth=0.4)
1361
+ ax.axhline(y=sl["suggestive"], color="navy", linestyle="--", linewidth=0.5)
1029
1362
 
1030
1363
  ax.spines[["top", "right"]].set_visible(False)
1031
1364
 
@@ -1042,7 +1375,7 @@ def plot_linearm(
1042
1375
  differences = np.diff(df_chr['POS']).tolist()
1043
1376
  less_than_spread_width.append(list(filter(lambda x: x < s_width, differences)))
1044
1377
  less_than_spread_width = [l for l in less_than_spread_width if not len(l) == 0]
1045
-
1378
+ print(len(less_than_spread_width))
1046
1379
  if len(less_than_spread_width) < 5:
1047
1380
  _draw_annotation_arrows(
1048
1381
  ax_annot,
@@ -1073,7 +1406,7 @@ def plot_linearm(
1073
1406
  y_tip=0.0,
1074
1407
  y_text_base=0.3,
1075
1408
  y_stack_step=0.17,
1076
- min_sep=1e6,
1409
+ min_sep=6e6,
1077
1410
  )
1078
1411
 
1079
1412
  ax_annot.set_ylim(0, 1)
@@ -1145,12 +1478,10 @@ def plot_linearm(
1145
1478
 
1146
1479
  def plot_linear(
1147
1480
  sumstats_loaded: list[str],
1148
- trim_pval: Optional[float] = None,
1149
1481
  track_heights: list[float] = None,
1150
1482
  logp: bool = False,
1151
1483
  point_size: Optional[float] = 8,
1152
1484
  highlight: bool = False,
1153
- highlight_thresh: float = 5e-8,
1154
1485
  highlight_color: str = 'brown',
1155
1486
  highlight_line: bool = False,
1156
1487
  highlight_line_color: str = 'grey',
@@ -1276,9 +1607,6 @@ def plot_linear(
1276
1607
  dfs = [v[0] for v in sumstats_loaded.values()]
1277
1608
  t_labels = list(sumstats_loaded.keys())
1278
1609
 
1279
- if track_heights is not None:
1280
- t_heights = [float(x) for x in track_heights]
1281
-
1282
1610
  label = 'SNP'
1283
1611
  if annotate:
1284
1612
  label = get_annotation_column(
@@ -1304,11 +1632,9 @@ def plot_linear(
1304
1632
  fig, axes = plot_linearm(
1305
1633
  tracks=dfs,
1306
1634
  track_labels=t_labels,
1307
- trim_pval=trim_pval,
1308
1635
  logp=True if logp else False,
1309
1636
  point_size=point_size,
1310
1637
  highlight=highlight,
1311
- highlight_thresh=highlight_thresh,
1312
1638
  highlight_color = highlight_color,
1313
1639
  highlight_line = highlight_line,
1314
1640
  highlight_line_color = highlight_line_color,
@@ -1317,7 +1643,7 @@ def plot_linear(
1317
1643
  annot_df=hits_table if hits_table is not None and not hits_table.empty else None,
1318
1644
  label_col=label,
1319
1645
  chr_spacing=chr_spacing,
1320
- track_heights=t_heights,
1646
+ track_heights=track_heights,
1321
1647
  linear_track_spacing=linear_track_spacing,
1322
1648
  annot_rail_frac=annot_rail_frac,
1323
1649
  colors=colors,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycmplot
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: Multi-track circular and linear Manhattan plot generation for GWAS summary statistics
5
5
  Author: Kevin Esoh
6
6
  Author-email: Kevin Esoh <kesohku1@jh.edu>
@@ -143,6 +143,14 @@ for gene annotation. Also included are UCSC chain files for coordinate conversio
143
143
  A potential useful application is **comparative visualization** of results from multiple imputation panels,
144
144
  multiple populations, or multiple traits to observe shared genetic architecture.
145
145
 
146
+
147
+ # Tip
148
+
149
+ For signed non-pvalue statistics such as iHS, use `--signif_threshold` and
150
+ `--suggest_threshold` to specify lines to draw for iHS cutoff above and below zero,
151
+ that is in the positive and negative axes.
152
+
153
+
146
154
  Read more in the package documentation page: https://pycmplot.readthedocs.io/en/latest/
147
155
 
148
156
  ---
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pycmplot"
7
- version = "0.2.8"
7
+ version = "0.2.9"
8
8
  description = "Multi-track circular and linear Manhattan plot generation for GWAS summary statistics"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = pycmplot
3
- version = 0.2.8
3
+ version = 0.2.9
4
4
  author = Kevin Esoh
5
5
  author_email = kesohku1@jh.edu
6
6
  description = Multi-track circular and linear Manhattan plot generation for GWAS summary statistics
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