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.
- {pycmplot-0.2.8 → pycmplot-0.2.9}/PKG-INFO +9 -1
- {pycmplot-0.2.8 → pycmplot-0.2.9}/README.md +8 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/__init__.py +1 -1
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/_core.py +1 -2
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/circular.py +12 -32
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/linear.py +346 -20
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/PKG-INFO +9 -1
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pyproject.toml +1 -1
- {pycmplot-0.2.8 → pycmplot-0.2.9}/setup.cfg +1 -1
- {pycmplot-0.2.8 → pycmplot-0.2.9}/LICENSE +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/bench_python.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/collect_results.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/generate_multi_sumstats.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/benchmark/generate_sumstats.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/__main__.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/annotation.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/cli.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/constants.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/Homo_sapiens.GRCh37.geneinfo.tsv.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/Homo_sapiens.GRCh38.geneinfo.tsv.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/hg18ToHg38.over.chain.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/data/hg19ToHg38.over.chain.gz +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/io.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/liftover.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/__init__.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/plotting/qq.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/resources.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot/stats.py +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/SOURCES.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/dependency_links.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/entry_points.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/requires.txt +0 -0
- {pycmplot-0.2.8 → pycmplot-0.2.9}/pycmplot.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
---
|
|
@@ -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="
|
|
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,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=
|
|
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=
|
|
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.
|
|
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
|
---
|
|
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
|
|
File without changes
|