pylocuszoom 0.6.0__py3-none-any.whl → 1.0.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/__init__.py +34 -7
- pylocuszoom/backends/__init__.py +116 -17
- pylocuszoom/backends/base.py +363 -60
- pylocuszoom/backends/bokeh_backend.py +77 -15
- pylocuszoom/backends/hover.py +198 -0
- pylocuszoom/backends/matplotlib_backend.py +263 -3
- pylocuszoom/backends/plotly_backend.py +73 -16
- pylocuszoom/config.py +365 -0
- pylocuszoom/ensembl.py +476 -0
- pylocuszoom/eqtl.py +17 -25
- pylocuszoom/exceptions.py +33 -0
- pylocuszoom/finemapping.py +18 -32
- pylocuszoom/forest.py +10 -11
- pylocuszoom/gene_track.py +169 -142
- pylocuszoom/loaders.py +3 -1
- pylocuszoom/phewas.py +10 -11
- pylocuszoom/plotter.py +311 -277
- pylocuszoom/recombination.py +19 -3
- pylocuszoom/schemas.py +1 -6
- pylocuszoom/utils.py +54 -4
- pylocuszoom/validation.py +223 -0
- {pylocuszoom-0.6.0.dist-info → pylocuszoom-1.0.0.dist-info}/METADATA +82 -37
- pylocuszoom-1.0.0.dist-info/RECORD +31 -0
- pylocuszoom-0.6.0.dist-info/RECORD +0 -26
- {pylocuszoom-0.6.0.dist-info → pylocuszoom-1.0.0.dist-info}/WHEEL +0 -0
- {pylocuszoom-0.6.0.dist-info → pylocuszoom-1.0.0.dist-info}/licenses/LICENSE.md +0 -0
pylocuszoom/backends/base.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Defines the interface that matplotlib, plotly, and bokeh backends must implement.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Any, List, Optional, Protocol, Tuple, Union
|
|
6
|
+
from typing import Any, Callable, List, Optional, Protocol, Tuple, Union
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
@@ -13,8 +13,48 @@ class PlotBackend(Protocol):
|
|
|
13
13
|
|
|
14
14
|
All backends (matplotlib, plotly, bokeh) must implement these methods
|
|
15
15
|
to enable consistent plotting across different rendering engines.
|
|
16
|
+
|
|
17
|
+
Capability Properties:
|
|
18
|
+
supports_snp_labels: Whether backend supports text labels via adjustText.
|
|
19
|
+
supports_hover: Whether backend supports hover tooltips.
|
|
20
|
+
supports_secondary_axis: Whether backend supports twin y-axis for overlays.
|
|
16
21
|
"""
|
|
17
22
|
|
|
23
|
+
# =========================================================================
|
|
24
|
+
# Capability Properties
|
|
25
|
+
# =========================================================================
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def supports_snp_labels(self) -> bool:
|
|
29
|
+
"""Whether backend supports text labels via adjustText.
|
|
30
|
+
|
|
31
|
+
Matplotlib supports SNP labels using adjustText for automatic repositioning.
|
|
32
|
+
Interactive backends (Plotly, Bokeh) use hover tooltips instead.
|
|
33
|
+
"""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def supports_hover(self) -> bool:
|
|
38
|
+
"""Whether backend supports hover tooltips.
|
|
39
|
+
|
|
40
|
+
Interactive backends (Plotly, Bokeh) support hover tooltips.
|
|
41
|
+
Matplotlib does not support hover - use SNP labels instead.
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def supports_secondary_axis(self) -> bool:
|
|
47
|
+
"""Whether backend supports twin y-axis for recombination overlay.
|
|
48
|
+
|
|
49
|
+
All current backends support secondary axes, but this allows for
|
|
50
|
+
future backends that may not.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
# =========================================================================
|
|
55
|
+
# Figure Creation
|
|
56
|
+
# =========================================================================
|
|
57
|
+
|
|
18
58
|
def create_figure(
|
|
19
59
|
self,
|
|
20
60
|
n_panels: int,
|
|
@@ -35,6 +75,31 @@ class PlotBackend(Protocol):
|
|
|
35
75
|
"""
|
|
36
76
|
...
|
|
37
77
|
|
|
78
|
+
def finalize_layout(
|
|
79
|
+
self,
|
|
80
|
+
fig: Any,
|
|
81
|
+
left: float = 0.08,
|
|
82
|
+
right: float = 0.95,
|
|
83
|
+
top: float = 0.95,
|
|
84
|
+
bottom: float = 0.1,
|
|
85
|
+
hspace: float = 0.08,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Finalize figure layout with margins and spacing.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
fig: Figure object.
|
|
91
|
+
left: Left margin fraction.
|
|
92
|
+
right: Right margin fraction.
|
|
93
|
+
top: Top margin fraction.
|
|
94
|
+
bottom: Bottom margin fraction.
|
|
95
|
+
hspace: Vertical space between subplots.
|
|
96
|
+
"""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
# =========================================================================
|
|
100
|
+
# Basic Plotting
|
|
101
|
+
# =========================================================================
|
|
102
|
+
|
|
38
103
|
def scatter(
|
|
39
104
|
self,
|
|
40
105
|
ax: Any,
|
|
@@ -151,6 +216,36 @@ class PlotBackend(Protocol):
|
|
|
151
216
|
"""
|
|
152
217
|
...
|
|
153
218
|
|
|
219
|
+
def axvline(
|
|
220
|
+
self,
|
|
221
|
+
ax: Any,
|
|
222
|
+
x: float,
|
|
223
|
+
color: str = "grey",
|
|
224
|
+
linestyle: str = "--",
|
|
225
|
+
linewidth: float = 1.0,
|
|
226
|
+
alpha: float = 1.0,
|
|
227
|
+
zorder: int = 1,
|
|
228
|
+
) -> Any:
|
|
229
|
+
"""Add a vertical line across the axes.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
ax: Axes or panel.
|
|
233
|
+
x: X-value for the line.
|
|
234
|
+
color: Line color.
|
|
235
|
+
linestyle: Line style.
|
|
236
|
+
linewidth: Line width.
|
|
237
|
+
alpha: Line transparency (0-1).
|
|
238
|
+
zorder: Drawing order.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
The line object.
|
|
242
|
+
"""
|
|
243
|
+
...
|
|
244
|
+
|
|
245
|
+
# =========================================================================
|
|
246
|
+
# Text and Annotations
|
|
247
|
+
# =========================================================================
|
|
248
|
+
|
|
154
249
|
def add_text(
|
|
155
250
|
self,
|
|
156
251
|
ax: Any,
|
|
@@ -181,6 +276,57 @@ class PlotBackend(Protocol):
|
|
|
181
276
|
"""
|
|
182
277
|
...
|
|
183
278
|
|
|
279
|
+
def add_panel_label(
|
|
280
|
+
self,
|
|
281
|
+
ax: Any,
|
|
282
|
+
label: str,
|
|
283
|
+
x_frac: float = 0.02,
|
|
284
|
+
y_frac: float = 0.95,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Add label text at fractional position in panel.
|
|
287
|
+
|
|
288
|
+
Used for panel letters (A, B, C) in multi-panel figures.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
ax: Axes or panel.
|
|
292
|
+
label: Label text (e.g., "A", "B").
|
|
293
|
+
x_frac: Horizontal position as fraction of axes (0-1).
|
|
294
|
+
y_frac: Vertical position as fraction of axes (0-1).
|
|
295
|
+
"""
|
|
296
|
+
...
|
|
297
|
+
|
|
298
|
+
def add_snp_labels(
|
|
299
|
+
self,
|
|
300
|
+
ax: Any,
|
|
301
|
+
df: pd.DataFrame,
|
|
302
|
+
pos_col: str,
|
|
303
|
+
neglog10p_col: str,
|
|
304
|
+
rs_col: str,
|
|
305
|
+
label_top_n: int,
|
|
306
|
+
genes_df: Optional[pd.DataFrame],
|
|
307
|
+
chrom: int,
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Add SNP labels to plot.
|
|
310
|
+
|
|
311
|
+
No-op if supports_snp_labels=False. Matplotlib uses adjustText
|
|
312
|
+
for automatic label repositioning to avoid overlaps.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
ax: Axes or panel.
|
|
316
|
+
df: DataFrame with SNP data.
|
|
317
|
+
pos_col: Column name for position.
|
|
318
|
+
neglog10p_col: Column name for -log10(p-value).
|
|
319
|
+
rs_col: Column name for SNP ID.
|
|
320
|
+
label_top_n: Number of top SNPs to label.
|
|
321
|
+
genes_df: Gene annotations (unused, for signature compatibility).
|
|
322
|
+
chrom: Chromosome number (unused, for signature compatibility).
|
|
323
|
+
"""
|
|
324
|
+
...
|
|
325
|
+
|
|
326
|
+
# =========================================================================
|
|
327
|
+
# Shapes and Patches
|
|
328
|
+
# =========================================================================
|
|
329
|
+
|
|
184
330
|
def add_rectangle(
|
|
185
331
|
self,
|
|
186
332
|
ax: Any,
|
|
@@ -209,6 +355,36 @@ class PlotBackend(Protocol):
|
|
|
209
355
|
"""
|
|
210
356
|
...
|
|
211
357
|
|
|
358
|
+
def add_polygon(
|
|
359
|
+
self,
|
|
360
|
+
ax: Any,
|
|
361
|
+
points: List[List[float]],
|
|
362
|
+
facecolor: str = "blue",
|
|
363
|
+
edgecolor: str = "black",
|
|
364
|
+
linewidth: float = 0.5,
|
|
365
|
+
zorder: int = 2,
|
|
366
|
+
) -> Any:
|
|
367
|
+
"""Add polygon patch to axes.
|
|
368
|
+
|
|
369
|
+
Used for gene track directional arrows.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
ax: Axes or panel.
|
|
373
|
+
points: List of [x, y] coordinate pairs forming the polygon.
|
|
374
|
+
facecolor: Fill color.
|
|
375
|
+
edgecolor: Edge color.
|
|
376
|
+
linewidth: Edge width.
|
|
377
|
+
zorder: Drawing order.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
The polygon object.
|
|
381
|
+
"""
|
|
382
|
+
...
|
|
383
|
+
|
|
384
|
+
# =========================================================================
|
|
385
|
+
# Axis Configuration
|
|
386
|
+
# =========================================================================
|
|
387
|
+
|
|
212
388
|
def set_xlim(self, ax: Any, left: float, right: float) -> None:
|
|
213
389
|
"""Set x-axis limits.
|
|
214
390
|
|
|
@@ -249,6 +425,23 @@ class PlotBackend(Protocol):
|
|
|
249
425
|
"""
|
|
250
426
|
...
|
|
251
427
|
|
|
428
|
+
def set_yticks(
|
|
429
|
+
self,
|
|
430
|
+
ax: Any,
|
|
431
|
+
positions: List[float],
|
|
432
|
+
labels: List[str],
|
|
433
|
+
fontsize: int = 10,
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Set y-axis tick positions and labels.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
ax: Axes or panel.
|
|
439
|
+
positions: Tick positions.
|
|
440
|
+
labels: Tick labels.
|
|
441
|
+
fontsize: Font size.
|
|
442
|
+
"""
|
|
443
|
+
...
|
|
444
|
+
|
|
252
445
|
def set_title(self, ax: Any, title: str, fontsize: int = 14) -> None:
|
|
253
446
|
"""Set panel title.
|
|
254
447
|
|
|
@@ -259,6 +452,38 @@ class PlotBackend(Protocol):
|
|
|
259
452
|
"""
|
|
260
453
|
...
|
|
261
454
|
|
|
455
|
+
def hide_spines(self, ax: Any, spines: List[str]) -> None:
|
|
456
|
+
"""Hide specified axis spines.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
ax: Axes or panel.
|
|
460
|
+
spines: List of spine names ('top', 'right', 'bottom', 'left').
|
|
461
|
+
"""
|
|
462
|
+
...
|
|
463
|
+
|
|
464
|
+
def hide_yaxis(self, ax: Any) -> None:
|
|
465
|
+
"""Hide y-axis for gene track panels.
|
|
466
|
+
|
|
467
|
+
Hides y-axis ticks, labels, and line. Gene tracks don't need
|
|
468
|
+
a y-axis since the vertical position is just for layout.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
ax: Axes or panel.
|
|
472
|
+
"""
|
|
473
|
+
...
|
|
474
|
+
|
|
475
|
+
def format_xaxis_mb(self, ax: Any) -> None:
|
|
476
|
+
"""Format x-axis to show megabase values.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
ax: Axes or panel.
|
|
480
|
+
"""
|
|
481
|
+
...
|
|
482
|
+
|
|
483
|
+
# =========================================================================
|
|
484
|
+
# Secondary Y-Axis (for recombination overlay)
|
|
485
|
+
# =========================================================================
|
|
486
|
+
|
|
262
487
|
def create_twin_axis(self, ax: Any) -> Any:
|
|
263
488
|
"""Create a secondary y-axis sharing the same x-axis.
|
|
264
489
|
|
|
@@ -270,75 +495,138 @@ class PlotBackend(Protocol):
|
|
|
270
495
|
"""
|
|
271
496
|
...
|
|
272
497
|
|
|
273
|
-
def
|
|
498
|
+
def line_secondary(
|
|
274
499
|
self,
|
|
275
500
|
ax: Any,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
501
|
+
x: pd.Series,
|
|
502
|
+
y: pd.Series,
|
|
503
|
+
color: str = "blue",
|
|
504
|
+
linewidth: float = 1.5,
|
|
505
|
+
alpha: float = 1.0,
|
|
506
|
+
linestyle: str = "-",
|
|
507
|
+
label: Optional[str] = None,
|
|
508
|
+
yaxis_name: Any = None,
|
|
280
509
|
) -> Any:
|
|
281
|
-
"""
|
|
510
|
+
"""Create line on secondary y-axis.
|
|
282
511
|
|
|
283
512
|
Args:
|
|
284
|
-
ax: Axes or panel.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
513
|
+
ax: Axes or panel (may be tuple for Plotly).
|
|
514
|
+
x: X-axis values.
|
|
515
|
+
y: Y-axis values.
|
|
516
|
+
color: Line color.
|
|
517
|
+
linewidth: Line width.
|
|
518
|
+
alpha: Transparency.
|
|
519
|
+
linestyle: Line style.
|
|
520
|
+
label: Legend label.
|
|
521
|
+
yaxis_name: Backend-specific secondary axis identifier.
|
|
289
522
|
|
|
290
523
|
Returns:
|
|
291
|
-
The
|
|
524
|
+
The line object.
|
|
292
525
|
"""
|
|
293
526
|
...
|
|
294
527
|
|
|
295
|
-
def
|
|
296
|
-
|
|
528
|
+
def fill_between_secondary(
|
|
529
|
+
self,
|
|
530
|
+
ax: Any,
|
|
531
|
+
x: pd.Series,
|
|
532
|
+
y1: Union[float, pd.Series],
|
|
533
|
+
y2: Union[float, pd.Series],
|
|
534
|
+
color: str = "blue",
|
|
535
|
+
alpha: float = 0.3,
|
|
536
|
+
yaxis_name: Any = None,
|
|
537
|
+
) -> Any:
|
|
538
|
+
"""Fill area on secondary y-axis.
|
|
297
539
|
|
|
298
540
|
Args:
|
|
299
541
|
ax: Axes or panel.
|
|
300
|
-
|
|
542
|
+
x: X-axis values.
|
|
543
|
+
y1: Lower y boundary.
|
|
544
|
+
y2: Upper y boundary.
|
|
545
|
+
color: Fill color.
|
|
546
|
+
alpha: Transparency.
|
|
547
|
+
yaxis_name: Backend-specific secondary axis identifier.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
The fill object.
|
|
301
551
|
"""
|
|
302
552
|
...
|
|
303
553
|
|
|
304
|
-
def
|
|
305
|
-
|
|
554
|
+
def set_secondary_ylim(
|
|
555
|
+
self,
|
|
556
|
+
ax: Any,
|
|
557
|
+
bottom: float,
|
|
558
|
+
top: float,
|
|
559
|
+
yaxis_name: Any = None,
|
|
560
|
+
) -> None:
|
|
561
|
+
"""Set secondary y-axis limits.
|
|
306
562
|
|
|
307
563
|
Args:
|
|
308
564
|
ax: Axes or panel.
|
|
565
|
+
bottom: Minimum y value.
|
|
566
|
+
top: Maximum y value.
|
|
567
|
+
yaxis_name: Backend-specific secondary axis identifier.
|
|
309
568
|
"""
|
|
310
569
|
...
|
|
311
570
|
|
|
312
|
-
def
|
|
571
|
+
def set_secondary_ylabel(
|
|
313
572
|
self,
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
573
|
+
ax: Any,
|
|
574
|
+
label: str,
|
|
575
|
+
color: str = "black",
|
|
576
|
+
fontsize: int = 10,
|
|
577
|
+
yaxis_name: Any = None,
|
|
318
578
|
) -> None:
|
|
319
|
-
"""
|
|
579
|
+
"""Set secondary y-axis label.
|
|
320
580
|
|
|
321
581
|
Args:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
582
|
+
ax: Axes or panel.
|
|
583
|
+
label: Label text.
|
|
584
|
+
color: Label color.
|
|
585
|
+
fontsize: Font size.
|
|
586
|
+
yaxis_name: Backend-specific secondary axis identifier.
|
|
326
587
|
"""
|
|
327
588
|
...
|
|
328
589
|
|
|
329
|
-
|
|
330
|
-
|
|
590
|
+
# =========================================================================
|
|
591
|
+
# Legends
|
|
592
|
+
# =========================================================================
|
|
593
|
+
|
|
594
|
+
def add_legend(
|
|
595
|
+
self,
|
|
596
|
+
ax: Any,
|
|
597
|
+
handles: List[Any],
|
|
598
|
+
labels: List[str],
|
|
599
|
+
loc: str = "upper left",
|
|
600
|
+
title: Optional[str] = None,
|
|
601
|
+
) -> Any:
|
|
602
|
+
"""Add a legend to the axes.
|
|
331
603
|
|
|
332
604
|
Args:
|
|
333
|
-
|
|
605
|
+
ax: Axes or panel.
|
|
606
|
+
handles: Legend handle objects.
|
|
607
|
+
labels: Legend labels.
|
|
608
|
+
loc: Legend location.
|
|
609
|
+
title: Legend title.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
The legend object.
|
|
334
613
|
"""
|
|
335
614
|
...
|
|
336
615
|
|
|
337
|
-
def
|
|
338
|
-
|
|
616
|
+
def add_ld_legend(
|
|
617
|
+
self,
|
|
618
|
+
ax: Any,
|
|
619
|
+
ld_bins: List[Tuple[float, str, str]],
|
|
620
|
+
lead_snp_color: str,
|
|
621
|
+
) -> None:
|
|
622
|
+
"""Add LD color legend.
|
|
623
|
+
|
|
624
|
+
Shows the linkage disequilibrium (r^2) color scale and lead SNP marker.
|
|
339
625
|
|
|
340
626
|
Args:
|
|
341
|
-
|
|
627
|
+
ax: Axes or panel.
|
|
628
|
+
ld_bins: List of (threshold, label, color) tuples defining LD bins.
|
|
629
|
+
lead_snp_color: Color for lead SNP marker in legend.
|
|
342
630
|
"""
|
|
343
631
|
...
|
|
344
632
|
|
|
@@ -361,7 +649,7 @@ class PlotBackend(Protocol):
|
|
|
361
649
|
self,
|
|
362
650
|
ax: Any,
|
|
363
651
|
credible_sets: List[int],
|
|
364
|
-
get_color_func:
|
|
652
|
+
get_color_func: Callable[[int], str],
|
|
365
653
|
) -> None:
|
|
366
654
|
"""Add fine-mapping credible set legend to the axes.
|
|
367
655
|
|
|
@@ -387,31 +675,9 @@ class PlotBackend(Protocol):
|
|
|
387
675
|
"""
|
|
388
676
|
...
|
|
389
677
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
x: float,
|
|
394
|
-
color: str = "grey",
|
|
395
|
-
linestyle: str = "--",
|
|
396
|
-
linewidth: float = 1.0,
|
|
397
|
-
alpha: float = 1.0,
|
|
398
|
-
zorder: int = 1,
|
|
399
|
-
) -> Any:
|
|
400
|
-
"""Add a vertical line across the axes.
|
|
401
|
-
|
|
402
|
-
Args:
|
|
403
|
-
ax: Axes or panel.
|
|
404
|
-
x: X-value for the line.
|
|
405
|
-
color: Line color.
|
|
406
|
-
linestyle: Line style.
|
|
407
|
-
linewidth: Line width.
|
|
408
|
-
alpha: Line transparency (0-1).
|
|
409
|
-
zorder: Drawing order.
|
|
410
|
-
|
|
411
|
-
Returns:
|
|
412
|
-
The line object.
|
|
413
|
-
"""
|
|
414
|
-
...
|
|
678
|
+
# =========================================================================
|
|
679
|
+
# Specialized Charts
|
|
680
|
+
# =========================================================================
|
|
415
681
|
|
|
416
682
|
def hbar(
|
|
417
683
|
self,
|
|
@@ -472,3 +738,40 @@ class PlotBackend(Protocol):
|
|
|
472
738
|
The errorbar object.
|
|
473
739
|
"""
|
|
474
740
|
...
|
|
741
|
+
|
|
742
|
+
# =========================================================================
|
|
743
|
+
# File Operations
|
|
744
|
+
# =========================================================================
|
|
745
|
+
|
|
746
|
+
def save(
|
|
747
|
+
self,
|
|
748
|
+
fig: Any,
|
|
749
|
+
path: str,
|
|
750
|
+
dpi: int = 150,
|
|
751
|
+
bbox_inches: str = "tight",
|
|
752
|
+
) -> None:
|
|
753
|
+
"""Save figure to file.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
fig: Figure object.
|
|
757
|
+
path: Output file path (.png, .pdf, .html).
|
|
758
|
+
dpi: Resolution for raster formats.
|
|
759
|
+
bbox_inches: Bounding box adjustment.
|
|
760
|
+
"""
|
|
761
|
+
...
|
|
762
|
+
|
|
763
|
+
def show(self, fig: Any) -> None:
|
|
764
|
+
"""Display the figure.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
fig: Figure object.
|
|
768
|
+
"""
|
|
769
|
+
...
|
|
770
|
+
|
|
771
|
+
def close(self, fig: Any) -> None:
|
|
772
|
+
"""Close the figure and free resources.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
fig: Figure object.
|
|
776
|
+
"""
|
|
777
|
+
...
|
|
@@ -11,7 +11,10 @@ from bokeh.layouts import column
|
|
|
11
11
|
from bokeh.models import ColumnDataSource, DataRange1d, HoverTool, Span
|
|
12
12
|
from bokeh.plotting import figure
|
|
13
13
|
|
|
14
|
+
from . import convert_latex_to_unicode, register_backend
|
|
14
15
|
|
|
16
|
+
|
|
17
|
+
@register_backend("bokeh")
|
|
15
18
|
class BokehBackend:
|
|
16
19
|
"""Bokeh backend for interactive plot generation.
|
|
17
20
|
|
|
@@ -34,9 +37,20 @@ class BokehBackend:
|
|
|
34
37
|
"-.": "dashdot",
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
@property
|
|
41
|
+
def supports_snp_labels(self) -> bool:
|
|
42
|
+
"""Bokeh uses hover tooltips instead of labels."""
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def supports_hover(self) -> bool:
|
|
47
|
+
"""Bokeh supports hover tooltips."""
|
|
48
|
+
return True
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def supports_secondary_axis(self) -> bool:
|
|
52
|
+
"""Bokeh supports secondary y-axis."""
|
|
53
|
+
return True
|
|
40
54
|
|
|
41
55
|
def create_figure(
|
|
42
56
|
self,
|
|
@@ -356,6 +370,18 @@ class BokehBackend:
|
|
|
356
370
|
ax.yaxis.axis_label = label
|
|
357
371
|
ax.yaxis.axis_label_text_font_size = f"{fontsize}pt"
|
|
358
372
|
|
|
373
|
+
def set_yticks(
|
|
374
|
+
self,
|
|
375
|
+
ax: figure,
|
|
376
|
+
positions: List[float],
|
|
377
|
+
labels: List[str],
|
|
378
|
+
fontsize: int = 10,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""Set y-axis tick positions and labels."""
|
|
381
|
+
ax.yaxis.ticker = positions
|
|
382
|
+
ax.yaxis.major_label_overrides = dict(zip(positions, labels))
|
|
383
|
+
ax.yaxis.major_label_text_font_size = f"{fontsize}pt"
|
|
384
|
+
|
|
359
385
|
def _get_legend_location(self, loc: str, default: str = "top_left") -> str:
|
|
360
386
|
"""Map matplotlib-style legend location to Bokeh location."""
|
|
361
387
|
loc_map = {
|
|
@@ -368,18 +394,7 @@ class BokehBackend:
|
|
|
368
394
|
|
|
369
395
|
def _convert_label(self, label: str) -> str:
|
|
370
396
|
"""Convert LaTeX-style labels to Unicode for Bokeh display."""
|
|
371
|
-
|
|
372
|
-
(r"$-\log_{10}$ P", "-log₁₀(P)"),
|
|
373
|
-
(r"$-\log_{10}$", "-log₁₀"),
|
|
374
|
-
(r"\log_{10}", "log₁₀"),
|
|
375
|
-
(r"$r^2$", "r²"),
|
|
376
|
-
(r"$R^2$", "R²"),
|
|
377
|
-
]
|
|
378
|
-
for latex, unicode_str in conversions:
|
|
379
|
-
if latex in label:
|
|
380
|
-
label = label.replace(latex, unicode_str)
|
|
381
|
-
label = label.replace("$", "")
|
|
382
|
-
return label
|
|
397
|
+
return convert_latex_to_unicode(label)
|
|
383
398
|
|
|
384
399
|
def set_title(self, ax: figure, title: str, fontsize: int = 14) -> None:
|
|
385
400
|
"""Set figure title."""
|
|
@@ -489,6 +504,53 @@ class BokehBackend:
|
|
|
489
504
|
renderer.major_label_text_color = color
|
|
490
505
|
break
|
|
491
506
|
|
|
507
|
+
def add_snp_labels(
|
|
508
|
+
self,
|
|
509
|
+
ax: figure,
|
|
510
|
+
df: pd.DataFrame,
|
|
511
|
+
pos_col: str,
|
|
512
|
+
neglog10p_col: str,
|
|
513
|
+
rs_col: str,
|
|
514
|
+
label_top_n: int,
|
|
515
|
+
genes_df: Optional[pd.DataFrame],
|
|
516
|
+
chrom: int,
|
|
517
|
+
) -> None:
|
|
518
|
+
"""No-op: Bokeh uses hover tooltips instead of text labels."""
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
def add_panel_label(
|
|
522
|
+
self,
|
|
523
|
+
ax: figure,
|
|
524
|
+
label: str,
|
|
525
|
+
x_frac: float = 0.02,
|
|
526
|
+
y_frac: float = 0.95,
|
|
527
|
+
) -> None:
|
|
528
|
+
"""Add label text at fractional position in panel."""
|
|
529
|
+
from bokeh.models import Label
|
|
530
|
+
|
|
531
|
+
# Convert fraction to data coordinates using axis ranges
|
|
532
|
+
x_range = ax.x_range
|
|
533
|
+
y_range = ax.y_range
|
|
534
|
+
x = (
|
|
535
|
+
x_range.start + x_frac * (x_range.end - x_range.start)
|
|
536
|
+
if hasattr(x_range, "start") and x_range.start is not None
|
|
537
|
+
else 0
|
|
538
|
+
)
|
|
539
|
+
y = (
|
|
540
|
+
y_range.start + y_frac * (y_range.end - y_range.start)
|
|
541
|
+
if hasattr(y_range, "start") and y_range.start is not None
|
|
542
|
+
else 0
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
label_obj = Label(
|
|
546
|
+
x=x,
|
|
547
|
+
y=y,
|
|
548
|
+
text=label,
|
|
549
|
+
text_font_size="12px",
|
|
550
|
+
text_font_style="bold",
|
|
551
|
+
)
|
|
552
|
+
ax.add_layout(label_obj)
|
|
553
|
+
|
|
492
554
|
def _ensure_legend_range(self, ax: figure) -> Any:
|
|
493
555
|
"""Ensure legend range exists and return a dummy data source.
|
|
494
556
|
|