pycmplot 0.2.8__tar.gz → 0.3.0__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.
- {pycmplot-0.2.8 → pycmplot-0.3.0}/PKG-INFO +9 -1
- {pycmplot-0.2.8 → pycmplot-0.3.0}/README.md +8 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/__init__.py +1 -1
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/_core.py +1 -2
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/annotation.py +2 -2
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/plotting/circular.py +12 -32
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/plotting/linear.py +345 -20
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot.egg-info/PKG-INFO +9 -1
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pyproject.toml +1 -1
- {pycmplot-0.2.8 → pycmplot-0.3.0}/setup.cfg +1 -1
- {pycmplot-0.2.8 → pycmplot-0.3.0}/LICENSE +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/benchmark/bench_python.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/benchmark/collect_results.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/benchmark/generate_multi_sumstats.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/benchmark/generate_sumstats.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/__main__.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/cli.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/constants.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/data/Homo_sapiens.GRCh37.geneinfo.tsv.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/data/Homo_sapiens.GRCh38.geneinfo.tsv.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/data/hg18ToHg38.over.chain.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/data/hg19ToHg38.over.chain.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/io.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/liftover.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/plotting/__init__.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/plotting/qq.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/resources.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot/stats.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot.egg-info/SOURCES.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot.egg-info/dependency_links.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot.egg-info/entry_points.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot.egg-info/requires.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/pycmplot.egg-info/top_level.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.3.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycmplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
---
|
|
@@ -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,
|
|
@@ -589,11 +589,11 @@ def get_annotation_column(
|
|
|
589
589
|
try:
|
|
590
590
|
if row["genic"]:
|
|
591
591
|
label_clm = "nearest_upstream_gene"
|
|
592
|
-
label_msg = "'POS' is genic"
|
|
592
|
+
label_msg = f"Signal {row['SNP']} at {row['POS']} is genic [{row['nearest_upstream_gene']}]"
|
|
593
593
|
else:
|
|
594
594
|
label_clm = "top_gene"
|
|
595
595
|
label_msg = "'POS' is not genic"
|
|
596
|
-
logger.info("%s", label_msg)
|
|
596
|
+
#logger.info("%s", label_msg)
|
|
597
597
|
except Exception:
|
|
598
598
|
logger.warning(
|
|
599
599
|
"Annotation columns '%s' and '%s' not found in hits table: %s; "
|
|
@@ -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="
|
|
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
|
|
703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
874
|
-
|
|
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="
|
|
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="
|
|
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,6 @@ 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
|
-
|
|
1046
1378
|
if len(less_than_spread_width) < 5:
|
|
1047
1379
|
_draw_annotation_arrows(
|
|
1048
1380
|
ax_annot,
|
|
@@ -1073,7 +1405,7 @@ def plot_linearm(
|
|
|
1073
1405
|
y_tip=0.0,
|
|
1074
1406
|
y_text_base=0.3,
|
|
1075
1407
|
y_stack_step=0.17,
|
|
1076
|
-
min_sep=
|
|
1408
|
+
min_sep=6e6,
|
|
1077
1409
|
)
|
|
1078
1410
|
|
|
1079
1411
|
ax_annot.set_ylim(0, 1)
|
|
@@ -1145,12 +1477,10 @@ def plot_linearm(
|
|
|
1145
1477
|
|
|
1146
1478
|
def plot_linear(
|
|
1147
1479
|
sumstats_loaded: list[str],
|
|
1148
|
-
trim_pval: Optional[float] = None,
|
|
1149
1480
|
track_heights: list[float] = None,
|
|
1150
1481
|
logp: bool = False,
|
|
1151
1482
|
point_size: Optional[float] = 8,
|
|
1152
1483
|
highlight: bool = False,
|
|
1153
|
-
highlight_thresh: float = 5e-8,
|
|
1154
1484
|
highlight_color: str = 'brown',
|
|
1155
1485
|
highlight_line: bool = False,
|
|
1156
1486
|
highlight_line_color: str = 'grey',
|
|
@@ -1276,9 +1606,6 @@ def plot_linear(
|
|
|
1276
1606
|
dfs = [v[0] for v in sumstats_loaded.values()]
|
|
1277
1607
|
t_labels = list(sumstats_loaded.keys())
|
|
1278
1608
|
|
|
1279
|
-
if track_heights is not None:
|
|
1280
|
-
t_heights = [float(x) for x in track_heights]
|
|
1281
|
-
|
|
1282
1609
|
label = 'SNP'
|
|
1283
1610
|
if annotate:
|
|
1284
1611
|
label = get_annotation_column(
|
|
@@ -1304,11 +1631,9 @@ def plot_linear(
|
|
|
1304
1631
|
fig, axes = plot_linearm(
|
|
1305
1632
|
tracks=dfs,
|
|
1306
1633
|
track_labels=t_labels,
|
|
1307
|
-
trim_pval=trim_pval,
|
|
1308
1634
|
logp=True if logp else False,
|
|
1309
1635
|
point_size=point_size,
|
|
1310
1636
|
highlight=highlight,
|
|
1311
|
-
highlight_thresh=highlight_thresh,
|
|
1312
1637
|
highlight_color = highlight_color,
|
|
1313
1638
|
highlight_line = highlight_line,
|
|
1314
1639
|
highlight_line_color = highlight_line_color,
|
|
@@ -1317,7 +1642,7 @@ def plot_linear(
|
|
|
1317
1642
|
annot_df=hits_table if hits_table is not None and not hits_table.empty else None,
|
|
1318
1643
|
label_col=label,
|
|
1319
1644
|
chr_spacing=chr_spacing,
|
|
1320
|
-
track_heights=
|
|
1645
|
+
track_heights=track_heights,
|
|
1321
1646
|
linear_track_spacing=linear_track_spacing,
|
|
1322
1647
|
annot_rail_frac=annot_rail_frac,
|
|
1323
1648
|
colors=colors,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycmplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
---
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|