pylocuszoom 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
- pylocuszoom/backends/base.py +2 -0
- pylocuszoom/backends/bokeh_backend.py +220 -48
- pylocuszoom/backends/matplotlib_backend.py +29 -7
- pylocuszoom/backends/plotly_backend.py +262 -20
- pylocuszoom/finemapping.py +0 -1
- pylocuszoom/gene_track.py +231 -23
- pylocuszoom/plotter.py +277 -139
- {pylocuszoom-0.2.0.dist-info → pylocuszoom-0.3.0.dist-info}/METADATA +28 -14
- pylocuszoom-0.3.0.dist-info/RECORD +21 -0
- pylocuszoom-0.2.0.dist-info/RECORD +0 -21
- {pylocuszoom-0.2.0.dist-info → pylocuszoom-0.3.0.dist-info}/WHEEL +0 -0
- {pylocuszoom-0.2.0.dist-info → pylocuszoom-0.3.0.dist-info}/licenses/LICENSE.md +0 -0
pylocuszoom/plotter.py
CHANGED
|
@@ -19,7 +19,6 @@ from matplotlib.axes import Axes
|
|
|
19
19
|
from matplotlib.figure import Figure
|
|
20
20
|
from matplotlib.lines import Line2D
|
|
21
21
|
from matplotlib.patches import Patch
|
|
22
|
-
from matplotlib.ticker import FuncFormatter, MaxNLocator
|
|
23
22
|
|
|
24
23
|
from .backends import BackendType, get_backend
|
|
25
24
|
from .colors import (
|
|
@@ -38,11 +37,16 @@ from .finemapping import (
|
|
|
38
37
|
get_credible_sets,
|
|
39
38
|
prepare_finemapping_for_plotting,
|
|
40
39
|
)
|
|
41
|
-
from .gene_track import
|
|
40
|
+
from .gene_track import (
|
|
41
|
+
assign_gene_positions,
|
|
42
|
+
plot_gene_track,
|
|
43
|
+
plot_gene_track_generic,
|
|
44
|
+
)
|
|
42
45
|
from .labels import add_snp_labels
|
|
43
46
|
from .ld import calculate_ld, find_plink
|
|
44
47
|
from .logging import enable_logging, logger
|
|
45
48
|
from .recombination import (
|
|
49
|
+
RECOMB_COLOR,
|
|
46
50
|
add_recombination_overlay,
|
|
47
51
|
download_canine_recombination_maps,
|
|
48
52
|
get_default_data_dir,
|
|
@@ -50,8 +54,8 @@ from .recombination import (
|
|
|
50
54
|
)
|
|
51
55
|
from .utils import normalize_chrom, validate_genes_df, validate_gwas_df
|
|
52
56
|
|
|
53
|
-
# Default significance threshold: 5e-8
|
|
54
|
-
DEFAULT_GENOMEWIDE_THRESHOLD = 5e-
|
|
57
|
+
# Default significance threshold: 5e-8 (genome-wide significance)
|
|
58
|
+
DEFAULT_GENOMEWIDE_THRESHOLD = 5e-8
|
|
55
59
|
DEFAULT_GENOMEWIDE_LINE = -np.log10(DEFAULT_GENOMEWIDE_THRESHOLD)
|
|
56
60
|
|
|
57
61
|
|
|
@@ -288,62 +292,70 @@ class LocusZoomPlotter:
|
|
|
288
292
|
fig, ax, gene_ax = self._create_figure(genes_df, chrom, start, end, figsize)
|
|
289
293
|
|
|
290
294
|
# Plot association data
|
|
291
|
-
self._plot_association(ax, df, pos_col, ld_col, lead_pos)
|
|
295
|
+
self._plot_association(ax, df, pos_col, ld_col, lead_pos, rs_col, p_col)
|
|
292
296
|
|
|
293
297
|
# Add significance line
|
|
294
|
-
|
|
298
|
+
self._backend.axhline(
|
|
299
|
+
ax,
|
|
295
300
|
y=self._genomewide_line,
|
|
296
301
|
color="red",
|
|
297
|
-
linestyle=
|
|
302
|
+
linestyle="--",
|
|
298
303
|
linewidth=1,
|
|
299
|
-
alpha=0.
|
|
304
|
+
alpha=0.65,
|
|
300
305
|
zorder=1,
|
|
301
306
|
)
|
|
302
307
|
|
|
303
|
-
# Add SNP labels
|
|
308
|
+
# Add SNP labels (matplotlib only - interactive backends use hover tooltips)
|
|
304
309
|
if snp_labels and rs_col in df.columns and label_top_n > 0 and not df.empty:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
310
|
+
if self.backend_name == "matplotlib":
|
|
311
|
+
add_snp_labels(
|
|
312
|
+
ax,
|
|
313
|
+
df,
|
|
314
|
+
pos_col=pos_col,
|
|
315
|
+
neglog10p_col="neglog10p",
|
|
316
|
+
rs_col=rs_col,
|
|
317
|
+
label_top_n=label_top_n,
|
|
318
|
+
genes_df=genes_df,
|
|
319
|
+
chrom=chrom,
|
|
320
|
+
)
|
|
315
321
|
|
|
316
|
-
# Add recombination overlay
|
|
322
|
+
# Add recombination overlay (all backends)
|
|
317
323
|
if recomb_df is not None and not recomb_df.empty:
|
|
318
|
-
|
|
324
|
+
if self.backend_name == "matplotlib":
|
|
325
|
+
add_recombination_overlay(ax, recomb_df, start, end)
|
|
326
|
+
else:
|
|
327
|
+
self._add_recombination_overlay_generic(ax, recomb_df, start, end)
|
|
319
328
|
|
|
320
329
|
# Format axes
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
ax
|
|
324
|
-
ax.spines["right"].set_visible(False)
|
|
330
|
+
self._backend.set_ylabel(ax, r"$-\log_{10}$ P")
|
|
331
|
+
self._backend.set_xlim(ax, start, end)
|
|
332
|
+
self._backend.hide_spines(ax, ["top", "right"])
|
|
325
333
|
|
|
326
|
-
# Add LD legend
|
|
334
|
+
# Add LD legend (all backends)
|
|
327
335
|
if ld_col is not None and ld_col in df.columns:
|
|
328
|
-
self.
|
|
336
|
+
if self.backend_name == "matplotlib":
|
|
337
|
+
self._add_ld_legend(ax)
|
|
338
|
+
else:
|
|
339
|
+
self._backend.add_ld_legend(ax, LD_BINS, LEAD_SNP_COLOR)
|
|
329
340
|
|
|
330
|
-
# Plot gene track
|
|
341
|
+
# Plot gene track (all backends)
|
|
331
342
|
if genes_df is not None and gene_ax is not None:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
343
|
+
if self.backend_name == "matplotlib":
|
|
344
|
+
plot_gene_track(gene_ax, genes_df, chrom, start, end, exons_df)
|
|
345
|
+
else:
|
|
346
|
+
plot_gene_track_generic(
|
|
347
|
+
gene_ax, self._backend, genes_df, chrom, start, end, exons_df
|
|
348
|
+
)
|
|
349
|
+
self._backend.set_xlabel(gene_ax, f"Chromosome {chrom} (Mb)")
|
|
350
|
+
self._backend.hide_spines(gene_ax, ["top", "right", "left"])
|
|
337
351
|
else:
|
|
338
|
-
|
|
352
|
+
self._backend.set_xlabel(ax, f"Chromosome {chrom} (Mb)")
|
|
339
353
|
|
|
340
354
|
# Format x-axis with Mb labels
|
|
341
|
-
|
|
342
|
-
ax.xaxis.set_major_locator(MaxNLocator(nbins=6))
|
|
355
|
+
self._backend.format_xaxis_mb(ax)
|
|
343
356
|
|
|
344
357
|
# Adjust layout
|
|
345
|
-
|
|
346
|
-
plt.ion()
|
|
358
|
+
self._backend.finalize_layout(fig, hspace=0.1)
|
|
347
359
|
|
|
348
360
|
return fig
|
|
349
361
|
|
|
@@ -381,18 +393,20 @@ class LocusZoomPlotter:
|
|
|
381
393
|
assoc_height = figsize[1] * 0.6
|
|
382
394
|
total_height = assoc_height + gene_track_height
|
|
383
395
|
|
|
384
|
-
fig, axes =
|
|
385
|
-
2,
|
|
386
|
-
1,
|
|
387
|
-
figsize=(figsize[0], total_height),
|
|
396
|
+
fig, axes = self._backend.create_figure(
|
|
397
|
+
n_panels=2,
|
|
388
398
|
height_ratios=[assoc_height, gene_track_height],
|
|
399
|
+
figsize=(figsize[0], total_height),
|
|
389
400
|
sharex=True,
|
|
390
|
-
gridspec_kw={"hspace": 0},
|
|
391
401
|
)
|
|
392
402
|
return fig, axes[0], axes[1]
|
|
393
403
|
else:
|
|
394
|
-
fig,
|
|
395
|
-
|
|
404
|
+
fig, axes = self._backend.create_figure(
|
|
405
|
+
n_panels=1,
|
|
406
|
+
height_ratios=[1.0],
|
|
407
|
+
figsize=(figsize[0], figsize[1] * 0.75),
|
|
408
|
+
)
|
|
409
|
+
return fig, axes[0], None
|
|
396
410
|
|
|
397
411
|
def _plot_association(
|
|
398
412
|
self,
|
|
@@ -401,8 +415,28 @@ class LocusZoomPlotter:
|
|
|
401
415
|
pos_col: str,
|
|
402
416
|
ld_col: Optional[str],
|
|
403
417
|
lead_pos: Optional[int],
|
|
418
|
+
rs_col: Optional[str] = None,
|
|
419
|
+
p_col: Optional[str] = None,
|
|
404
420
|
) -> None:
|
|
405
421
|
"""Plot association scatter with LD coloring."""
|
|
422
|
+
|
|
423
|
+
def _build_hover_data(subset_df: pd.DataFrame) -> Optional[pd.DataFrame]:
|
|
424
|
+
"""Build hover data for interactive backends."""
|
|
425
|
+
hover_cols = {}
|
|
426
|
+
# RS ID first (will be bold in hover)
|
|
427
|
+
if rs_col and rs_col in subset_df.columns:
|
|
428
|
+
hover_cols["SNP"] = subset_df[rs_col].values
|
|
429
|
+
# Position
|
|
430
|
+
if pos_col in subset_df.columns:
|
|
431
|
+
hover_cols["Position"] = subset_df[pos_col].values
|
|
432
|
+
# P-value
|
|
433
|
+
if p_col and p_col in subset_df.columns:
|
|
434
|
+
hover_cols["P-value"] = subset_df[p_col].values
|
|
435
|
+
# LD
|
|
436
|
+
if ld_col and ld_col in subset_df.columns:
|
|
437
|
+
hover_cols["R²"] = subset_df[ld_col].values
|
|
438
|
+
return pd.DataFrame(hover_cols) if hover_cols else None
|
|
439
|
+
|
|
406
440
|
# LD-based coloring
|
|
407
441
|
if ld_col is not None and ld_col in df.columns:
|
|
408
442
|
df["ld_bin"] = df[ld_col].apply(get_ld_bin)
|
|
@@ -411,40 +445,46 @@ class LocusZoomPlotter:
|
|
|
411
445
|
palette = get_ld_color_palette()
|
|
412
446
|
for bin_label in df["ld_bin"].unique():
|
|
413
447
|
bin_data = df[df["ld_bin"] == bin_label]
|
|
414
|
-
|
|
448
|
+
self._backend.scatter(
|
|
449
|
+
ax,
|
|
415
450
|
bin_data[pos_col],
|
|
416
451
|
bin_data["neglog10p"],
|
|
417
|
-
|
|
418
|
-
|
|
452
|
+
colors=palette.get(bin_label, "#BEBEBE"),
|
|
453
|
+
sizes=60,
|
|
419
454
|
edgecolor="black",
|
|
420
455
|
linewidth=0.5,
|
|
421
456
|
zorder=2,
|
|
457
|
+
hover_data=_build_hover_data(bin_data),
|
|
422
458
|
)
|
|
423
459
|
else:
|
|
424
460
|
# Default: grey points
|
|
425
|
-
|
|
461
|
+
self._backend.scatter(
|
|
462
|
+
ax,
|
|
426
463
|
df[pos_col],
|
|
427
464
|
df["neglog10p"],
|
|
428
|
-
|
|
429
|
-
|
|
465
|
+
colors="#BEBEBE",
|
|
466
|
+
sizes=60,
|
|
430
467
|
edgecolor="black",
|
|
431
468
|
linewidth=0.5,
|
|
432
469
|
zorder=2,
|
|
470
|
+
hover_data=_build_hover_data(df),
|
|
433
471
|
)
|
|
434
472
|
|
|
435
|
-
# Highlight lead SNP
|
|
473
|
+
# Highlight lead SNP with larger, more prominent marker
|
|
436
474
|
if lead_pos is not None:
|
|
437
475
|
lead_snp = df[df[pos_col] == lead_pos]
|
|
438
476
|
if not lead_snp.empty:
|
|
439
|
-
|
|
477
|
+
self._backend.scatter(
|
|
478
|
+
ax,
|
|
440
479
|
lead_snp[pos_col],
|
|
441
480
|
lead_snp["neglog10p"],
|
|
442
|
-
|
|
443
|
-
|
|
481
|
+
colors=LEAD_SNP_COLOR,
|
|
482
|
+
sizes=120, # Larger than regular points for visibility
|
|
444
483
|
marker="D",
|
|
445
|
-
|
|
446
|
-
|
|
484
|
+
edgecolor="black",
|
|
485
|
+
linewidth=1.5,
|
|
447
486
|
zorder=10,
|
|
487
|
+
hover_data=_build_hover_data(lead_snp),
|
|
448
488
|
)
|
|
449
489
|
|
|
450
490
|
def _add_ld_legend(self, ax: Axes) -> None:
|
|
@@ -485,6 +525,70 @@ class LocusZoomPlotter:
|
|
|
485
525
|
labelspacing=0.4,
|
|
486
526
|
)
|
|
487
527
|
|
|
528
|
+
def _add_recombination_overlay_generic(
|
|
529
|
+
self,
|
|
530
|
+
ax: Any,
|
|
531
|
+
recomb_df: pd.DataFrame,
|
|
532
|
+
start: int,
|
|
533
|
+
end: int,
|
|
534
|
+
) -> None:
|
|
535
|
+
"""Add recombination overlay for interactive backends (plotly/bokeh).
|
|
536
|
+
|
|
537
|
+
Creates a secondary y-axis with recombination rate line and fill.
|
|
538
|
+
"""
|
|
539
|
+
# Filter to region
|
|
540
|
+
region_recomb = recomb_df[
|
|
541
|
+
(recomb_df["pos"] >= start) & (recomb_df["pos"] <= end)
|
|
542
|
+
].copy()
|
|
543
|
+
|
|
544
|
+
if region_recomb.empty:
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
# Create secondary y-axis
|
|
548
|
+
yaxis_name = self._backend.create_twin_axis(ax)
|
|
549
|
+
|
|
550
|
+
# For plotly, yaxis_name is a tuple (fig, row, secondary_y)
|
|
551
|
+
# For bokeh, yaxis_name is just a string
|
|
552
|
+
if isinstance(yaxis_name, tuple):
|
|
553
|
+
_, _, secondary_y = yaxis_name
|
|
554
|
+
else:
|
|
555
|
+
secondary_y = yaxis_name
|
|
556
|
+
|
|
557
|
+
# Plot fill under curve
|
|
558
|
+
self._backend.fill_between_secondary(
|
|
559
|
+
ax,
|
|
560
|
+
region_recomb["pos"],
|
|
561
|
+
0,
|
|
562
|
+
region_recomb["rate"],
|
|
563
|
+
color=RECOMB_COLOR,
|
|
564
|
+
alpha=0.15,
|
|
565
|
+
yaxis_name=secondary_y,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Plot recombination rate line
|
|
569
|
+
self._backend.line_secondary(
|
|
570
|
+
ax,
|
|
571
|
+
region_recomb["pos"],
|
|
572
|
+
region_recomb["rate"],
|
|
573
|
+
color=RECOMB_COLOR,
|
|
574
|
+
linewidth=1.5,
|
|
575
|
+
alpha=0.7,
|
|
576
|
+
yaxis_name=secondary_y,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Set y-axis limits and label
|
|
580
|
+
max_rate = region_recomb["rate"].max()
|
|
581
|
+
self._backend.set_secondary_ylim(
|
|
582
|
+
ax, 0, max(max_rate * 1.2, 20), yaxis_name=secondary_y
|
|
583
|
+
)
|
|
584
|
+
self._backend.set_secondary_ylabel(
|
|
585
|
+
ax,
|
|
586
|
+
"Recombination rate (cM/Mb)",
|
|
587
|
+
color=RECOMB_COLOR,
|
|
588
|
+
fontsize=9,
|
|
589
|
+
yaxis_name=secondary_y,
|
|
590
|
+
)
|
|
591
|
+
|
|
488
592
|
def _add_eqtl_legend(self, ax: Axes) -> None:
|
|
489
593
|
"""Add eQTL effect size legend to plot."""
|
|
490
594
|
legend_elements = []
|
|
@@ -557,7 +661,8 @@ class LocusZoomPlotter:
|
|
|
557
661
|
df = df.sort_values(pos_col)
|
|
558
662
|
|
|
559
663
|
# Plot PIP as line
|
|
560
|
-
|
|
664
|
+
self._backend.line(
|
|
665
|
+
ax,
|
|
561
666
|
df[pos_col],
|
|
562
667
|
df[pip_col],
|
|
563
668
|
color=PIP_LINE_COLOR,
|
|
@@ -575,11 +680,12 @@ class LocusZoomPlotter:
|
|
|
575
680
|
for cs_id in credible_sets:
|
|
576
681
|
cs_data = df[df[cs_col] == cs_id]
|
|
577
682
|
color = get_credible_set_color(cs_id)
|
|
578
|
-
|
|
683
|
+
self._backend.scatter(
|
|
684
|
+
ax,
|
|
579
685
|
cs_data[pos_col],
|
|
580
686
|
cs_data[pip_col],
|
|
581
|
-
|
|
582
|
-
|
|
687
|
+
colors=color,
|
|
688
|
+
sizes=50,
|
|
583
689
|
marker="o",
|
|
584
690
|
edgecolor="black",
|
|
585
691
|
linewidth=0.5,
|
|
@@ -591,27 +697,28 @@ class LocusZoomPlotter:
|
|
|
591
697
|
if not non_cs_data.empty and pip_threshold > 0:
|
|
592
698
|
non_cs_data = non_cs_data[non_cs_data[pip_col] >= pip_threshold]
|
|
593
699
|
if not non_cs_data.empty:
|
|
594
|
-
|
|
700
|
+
self._backend.scatter(
|
|
701
|
+
ax,
|
|
595
702
|
non_cs_data[pos_col],
|
|
596
703
|
non_cs_data[pip_col],
|
|
597
|
-
|
|
598
|
-
|
|
704
|
+
colors="#BEBEBE",
|
|
705
|
+
sizes=30,
|
|
599
706
|
marker="o",
|
|
600
707
|
edgecolor="black",
|
|
601
708
|
linewidth=0.3,
|
|
602
709
|
zorder=2,
|
|
603
|
-
alpha=0.6,
|
|
604
710
|
)
|
|
605
711
|
else:
|
|
606
712
|
# No credible sets - show all points above threshold
|
|
607
713
|
if pip_threshold > 0:
|
|
608
714
|
high_pip = df[df[pip_col] >= pip_threshold]
|
|
609
715
|
if not high_pip.empty:
|
|
610
|
-
|
|
716
|
+
self._backend.scatter(
|
|
717
|
+
ax,
|
|
611
718
|
high_pip[pos_col],
|
|
612
719
|
high_pip[pip_col],
|
|
613
|
-
|
|
614
|
-
|
|
720
|
+
colors=PIP_LINE_COLOR,
|
|
721
|
+
sizes=50,
|
|
615
722
|
marker="o",
|
|
616
723
|
edgecolor="black",
|
|
617
724
|
linewidth=0.5,
|
|
@@ -825,24 +932,17 @@ class LocusZoomPlotter:
|
|
|
825
932
|
f"Creating stacked plot with {n_panels} panels for chr{chrom}:{start}-{end}"
|
|
826
933
|
)
|
|
827
934
|
|
|
828
|
-
# Prevent auto-display in interactive environments
|
|
829
|
-
plt.ioff()
|
|
830
|
-
|
|
831
935
|
# Load recombination data if needed
|
|
832
936
|
if show_recombination and recomb_df is None:
|
|
833
937
|
recomb_df = self._get_recomb_for_region(chrom, start, end)
|
|
834
938
|
|
|
835
|
-
# Create figure
|
|
836
|
-
fig, axes =
|
|
837
|
-
n_panels,
|
|
838
|
-
1,
|
|
839
|
-
figsize=actual_figsize,
|
|
939
|
+
# Create figure using backend
|
|
940
|
+
fig, axes = self._backend.create_figure(
|
|
941
|
+
n_panels=n_panels,
|
|
840
942
|
height_ratios=height_ratios,
|
|
943
|
+
figsize=actual_figsize,
|
|
841
944
|
sharex=True,
|
|
842
|
-
gridspec_kw={"hspace": 0.05},
|
|
843
945
|
)
|
|
844
|
-
if n_panels == 1:
|
|
845
|
-
axes = [axes]
|
|
846
946
|
|
|
847
947
|
# Plot each GWAS panel
|
|
848
948
|
for i, (gwas_df, lead_pos) in enumerate(zip(gwas_dfs, lead_positions)):
|
|
@@ -868,56 +968,91 @@ class LocusZoomPlotter:
|
|
|
868
968
|
panel_ld_col = "R2"
|
|
869
969
|
|
|
870
970
|
# Plot association
|
|
871
|
-
self._plot_association(ax, df, pos_col, panel_ld_col, lead_pos)
|
|
971
|
+
self._plot_association(ax, df, pos_col, panel_ld_col, lead_pos, rs_col, p_col)
|
|
872
972
|
|
|
873
973
|
# Add significance line
|
|
874
|
-
|
|
974
|
+
self._backend.axhline(
|
|
975
|
+
ax,
|
|
875
976
|
y=self._genomewide_line,
|
|
876
977
|
color="red",
|
|
877
978
|
linestyle="--",
|
|
878
979
|
linewidth=1,
|
|
879
|
-
alpha=0.
|
|
980
|
+
alpha=0.65,
|
|
880
981
|
zorder=1,
|
|
881
982
|
)
|
|
882
983
|
|
|
883
|
-
# Add SNP labels
|
|
984
|
+
# Add SNP labels (matplotlib only - interactive backends use hover tooltips)
|
|
884
985
|
if snp_labels and rs_col in df.columns and label_top_n > 0 and not df.empty:
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
986
|
+
if self.backend_name == "matplotlib":
|
|
987
|
+
add_snp_labels(
|
|
988
|
+
ax,
|
|
989
|
+
df,
|
|
990
|
+
pos_col=pos_col,
|
|
991
|
+
neglog10p_col="neglog10p",
|
|
992
|
+
rs_col=rs_col,
|
|
993
|
+
label_top_n=label_top_n,
|
|
994
|
+
genes_df=genes_df,
|
|
995
|
+
chrom=chrom,
|
|
996
|
+
)
|
|
895
997
|
|
|
896
|
-
# Add recombination overlay (only on first panel)
|
|
998
|
+
# Add recombination overlay (only on first panel, all backends)
|
|
897
999
|
if i == 0 and recomb_df is not None and not recomb_df.empty:
|
|
898
|
-
|
|
1000
|
+
if self.backend_name == "matplotlib":
|
|
1001
|
+
add_recombination_overlay(ax, recomb_df, start, end)
|
|
1002
|
+
else:
|
|
1003
|
+
self._add_recombination_overlay_generic(ax, recomb_df, start, end)
|
|
899
1004
|
|
|
900
1005
|
# Format axes
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
ax
|
|
904
|
-
ax.spines["right"].set_visible(False)
|
|
1006
|
+
self._backend.set_ylabel(ax, r"$-\log_{10}$ P")
|
|
1007
|
+
self._backend.set_xlim(ax, start, end)
|
|
1008
|
+
self._backend.hide_spines(ax, ["top", "right"])
|
|
905
1009
|
|
|
906
1010
|
# Add panel label
|
|
907
1011
|
if panel_labels and i < len(panel_labels):
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1012
|
+
if self.backend_name == "matplotlib":
|
|
1013
|
+
ax.annotate(
|
|
1014
|
+
panel_labels[i],
|
|
1015
|
+
xy=(0.02, 0.95),
|
|
1016
|
+
xycoords="axes fraction",
|
|
1017
|
+
fontsize=11,
|
|
1018
|
+
fontweight="bold",
|
|
1019
|
+
va="top",
|
|
1020
|
+
ha="left",
|
|
1021
|
+
)
|
|
1022
|
+
elif self.backend_name == "plotly":
|
|
1023
|
+
fig, row = ax
|
|
1024
|
+
fig.add_annotation(
|
|
1025
|
+
text=f"<b>{panel_labels[i]}</b>",
|
|
1026
|
+
xref=f"x{row} domain" if row > 1 else "x domain",
|
|
1027
|
+
yref=f"y{row} domain" if row > 1 else "y domain",
|
|
1028
|
+
x=0.02,
|
|
1029
|
+
y=0.95,
|
|
1030
|
+
showarrow=False,
|
|
1031
|
+
font=dict(size=11),
|
|
1032
|
+
xanchor="left",
|
|
1033
|
+
yanchor="top",
|
|
1034
|
+
)
|
|
1035
|
+
elif self.backend_name == "bokeh":
|
|
1036
|
+
from bokeh.models import Label
|
|
1037
|
+
|
|
1038
|
+
# Get y-axis range for positioning
|
|
1039
|
+
y_max = ax.y_range.end if ax.y_range.end else 10
|
|
1040
|
+
x_min = ax.x_range.start if ax.x_range.start else start
|
|
1041
|
+
label = Label(
|
|
1042
|
+
x=x_min + (end - start) * 0.02,
|
|
1043
|
+
y=y_max * 0.95,
|
|
1044
|
+
text=panel_labels[i],
|
|
1045
|
+
text_font_size="11pt",
|
|
1046
|
+
text_font_style="bold",
|
|
1047
|
+
)
|
|
1048
|
+
ax.add_layout(label)
|
|
917
1049
|
|
|
918
|
-
# Add LD legend (only on first panel)
|
|
1050
|
+
# Add LD legend (only on first panel, all backends)
|
|
919
1051
|
if i == 0 and panel_ld_col is not None and panel_ld_col in df.columns:
|
|
920
|
-
self.
|
|
1052
|
+
if self.backend_name == "matplotlib":
|
|
1053
|
+
self._add_ld_legend(ax)
|
|
1054
|
+
else:
|
|
1055
|
+
self._backend.add_ld_legend(ax, LD_BINS, LEAD_SNP_COLOR)
|
|
921
1056
|
|
|
922
1057
|
# Track current panel index
|
|
923
1058
|
panel_idx = n_gwas
|
|
@@ -950,10 +1085,9 @@ class LocusZoomPlotter:
|
|
|
950
1085
|
if credible_sets:
|
|
951
1086
|
self._add_finemapping_legend(ax, credible_sets)
|
|
952
1087
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
ax
|
|
956
|
-
ax.spines["right"].set_visible(False)
|
|
1088
|
+
self._backend.set_ylabel(ax, "PIP")
|
|
1089
|
+
self._backend.set_ylim(ax, -0.05, 1.05)
|
|
1090
|
+
self._backend.hide_spines(ax, ["top", "right"])
|
|
957
1091
|
panel_idx += 1
|
|
958
1092
|
|
|
959
1093
|
# Plot eQTL panel if provided
|
|
@@ -986,11 +1120,12 @@ class LocusZoomPlotter:
|
|
|
986
1120
|
effect = row["effect_size"]
|
|
987
1121
|
color = get_eqtl_color(effect)
|
|
988
1122
|
marker = "^" if effect >= 0 else "v"
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
row["
|
|
992
|
-
|
|
993
|
-
|
|
1123
|
+
self._backend.scatter(
|
|
1124
|
+
ax,
|
|
1125
|
+
pd.Series([row["pos"]]),
|
|
1126
|
+
pd.Series([row["neglog10p"]]),
|
|
1127
|
+
colors=color,
|
|
1128
|
+
sizes=50,
|
|
994
1129
|
marker=marker,
|
|
995
1130
|
edgecolor="black",
|
|
996
1131
|
linewidth=0.5,
|
|
@@ -1000,11 +1135,12 @@ class LocusZoomPlotter:
|
|
|
1000
1135
|
self._add_eqtl_legend(ax)
|
|
1001
1136
|
else:
|
|
1002
1137
|
# No effect sizes - plot as diamonds
|
|
1003
|
-
|
|
1138
|
+
self._backend.scatter(
|
|
1139
|
+
ax,
|
|
1004
1140
|
eqtl_data["pos"],
|
|
1005
1141
|
eqtl_data["neglog10p"],
|
|
1006
|
-
|
|
1007
|
-
|
|
1142
|
+
colors="#FF6B6B",
|
|
1143
|
+
sizes=60,
|
|
1008
1144
|
marker="D",
|
|
1009
1145
|
edgecolor="black",
|
|
1010
1146
|
linewidth=0.5,
|
|
@@ -1013,36 +1149,38 @@ class LocusZoomPlotter:
|
|
|
1013
1149
|
)
|
|
1014
1150
|
ax.legend(loc="upper right", fontsize=9)
|
|
1015
1151
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1152
|
+
self._backend.set_ylabel(ax, r"$-\log_{10}$ P (eQTL)")
|
|
1153
|
+
self._backend.axhline(
|
|
1154
|
+
ax,
|
|
1018
1155
|
y=self._genomewide_line,
|
|
1019
1156
|
color="red",
|
|
1020
1157
|
linestyle="--",
|
|
1021
1158
|
linewidth=1,
|
|
1022
|
-
alpha=0.
|
|
1159
|
+
alpha=0.65,
|
|
1023
1160
|
)
|
|
1024
|
-
ax
|
|
1025
|
-
ax.spines["right"].set_visible(False)
|
|
1161
|
+
self._backend.hide_spines(ax, ["top", "right"])
|
|
1026
1162
|
panel_idx += 1
|
|
1027
1163
|
|
|
1028
|
-
# Plot gene track
|
|
1164
|
+
# Plot gene track (all backends)
|
|
1029
1165
|
if genes_df is not None:
|
|
1030
1166
|
gene_ax = axes[panel_idx]
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1167
|
+
if self.backend_name == "matplotlib":
|
|
1168
|
+
plot_gene_track(gene_ax, genes_df, chrom, start, end, exons_df)
|
|
1169
|
+
else:
|
|
1170
|
+
plot_gene_track_generic(
|
|
1171
|
+
gene_ax, self._backend, genes_df, chrom, start, end, exons_df
|
|
1172
|
+
)
|
|
1173
|
+
self._backend.set_xlabel(gene_ax, f"Chromosome {chrom} (Mb)")
|
|
1174
|
+
self._backend.hide_spines(gene_ax, ["top", "right", "left"])
|
|
1036
1175
|
else:
|
|
1037
1176
|
# Set x-label on bottom panel
|
|
1038
|
-
axes[-1]
|
|
1177
|
+
self._backend.set_xlabel(axes[-1], f"Chromosome {chrom} (Mb)")
|
|
1039
1178
|
|
|
1040
|
-
# Format x-axis
|
|
1041
|
-
|
|
1042
|
-
|
|
1179
|
+
# Format x-axis (call for all axes - Plotly needs each subplot formatted)
|
|
1180
|
+
for ax in axes:
|
|
1181
|
+
self._backend.format_xaxis_mb(ax)
|
|
1043
1182
|
|
|
1044
1183
|
# Adjust layout
|
|
1045
|
-
|
|
1046
|
-
plt.ion()
|
|
1184
|
+
self._backend.finalize_layout(fig, hspace=0.1)
|
|
1047
1185
|
|
|
1048
1186
|
return fig
|