pylocuszoom 0.3.0__py3-none-any.whl → 0.6.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 +74 -2
- pylocuszoom/backends/base.py +131 -0
- pylocuszoom/backends/bokeh_backend.py +254 -68
- pylocuszoom/backends/matplotlib_backend.py +173 -0
- pylocuszoom/backends/plotly_backend.py +327 -87
- pylocuszoom/colors.py +44 -1
- pylocuszoom/forest.py +37 -0
- pylocuszoom/gene_track.py +1 -0
- pylocuszoom/loaders.py +880 -0
- pylocuszoom/phewas.py +35 -0
- pylocuszoom/plotter.py +342 -117
- pylocuszoom/py.typed +0 -0
- pylocuszoom/recombination.py +49 -35
- pylocuszoom/schemas.py +406 -0
- {pylocuszoom-0.3.0.dist-info → pylocuszoom-0.6.0.dist-info}/METADATA +153 -25
- pylocuszoom-0.6.0.dist-info/RECORD +26 -0
- pylocuszoom-0.3.0.dist-info/RECORD +0 -21
- {pylocuszoom-0.3.0.dist-info → pylocuszoom-0.6.0.dist-info}/WHEEL +0 -0
- {pylocuszoom-0.3.0.dist-info → pylocuszoom-0.6.0.dist-info}/licenses/LICENSE.md +0 -0
pylocuszoom/__init__.py
CHANGED
|
@@ -34,14 +34,22 @@ Species Support:
|
|
|
34
34
|
- Custom: User provides all reference data
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.6.0"
|
|
38
38
|
|
|
39
39
|
# Main plotter class
|
|
40
40
|
# Backend types
|
|
41
41
|
from .backends import BackendType, get_backend
|
|
42
42
|
|
|
43
43
|
# Colors and LD
|
|
44
|
-
from .colors import
|
|
44
|
+
from .colors import (
|
|
45
|
+
LEAD_SNP_COLOR,
|
|
46
|
+
PHEWAS_CATEGORY_COLORS,
|
|
47
|
+
get_ld_bin,
|
|
48
|
+
get_ld_color,
|
|
49
|
+
get_ld_color_palette,
|
|
50
|
+
get_phewas_category_color,
|
|
51
|
+
get_phewas_category_palette,
|
|
52
|
+
)
|
|
45
53
|
|
|
46
54
|
# eQTL support
|
|
47
55
|
from .eqtl import (
|
|
@@ -65,6 +73,9 @@ from .finemapping import (
|
|
|
65
73
|
validate_finemapping_df,
|
|
66
74
|
)
|
|
67
75
|
|
|
76
|
+
# Forest plot support
|
|
77
|
+
from .forest import validate_forest_df
|
|
78
|
+
|
|
68
79
|
# Gene track
|
|
69
80
|
from .gene_track import get_nearest_gene, plot_gene_track
|
|
70
81
|
|
|
@@ -74,8 +85,36 @@ from .labels import add_snp_labels
|
|
|
74
85
|
# LD calculation
|
|
75
86
|
from .ld import calculate_ld
|
|
76
87
|
|
|
88
|
+
# File format loaders
|
|
89
|
+
from .loaders import (
|
|
90
|
+
load_bed,
|
|
91
|
+
load_bolt_lmm,
|
|
92
|
+
load_caviar,
|
|
93
|
+
load_ensembl_genes,
|
|
94
|
+
load_eqtl_catalogue,
|
|
95
|
+
load_finemap,
|
|
96
|
+
load_gemma,
|
|
97
|
+
# eQTL loaders
|
|
98
|
+
load_gtex_eqtl,
|
|
99
|
+
# Gene annotation loaders
|
|
100
|
+
load_gtf,
|
|
101
|
+
# GWAS loaders
|
|
102
|
+
load_gwas,
|
|
103
|
+
load_gwas_catalog,
|
|
104
|
+
load_matrixeqtl,
|
|
105
|
+
load_plink_assoc,
|
|
106
|
+
load_polyfun,
|
|
107
|
+
load_regenie,
|
|
108
|
+
load_saige,
|
|
109
|
+
# Fine-mapping loaders
|
|
110
|
+
load_susie,
|
|
111
|
+
)
|
|
112
|
+
|
|
77
113
|
# Logging configuration
|
|
78
114
|
from .logging import disable_logging, enable_logging
|
|
115
|
+
|
|
116
|
+
# PheWAS support
|
|
117
|
+
from .phewas import validate_phewas_df
|
|
79
118
|
from .plotter import LocusZoomPlotter
|
|
80
119
|
|
|
81
120
|
# Reference data management
|
|
@@ -86,6 +125,9 @@ from .recombination import (
|
|
|
86
125
|
load_recombination_map,
|
|
87
126
|
)
|
|
88
127
|
|
|
128
|
+
# Schema validation
|
|
129
|
+
from .schemas import LoaderValidationError
|
|
130
|
+
|
|
89
131
|
# Validation utilities
|
|
90
132
|
from .utils import ValidationError, to_pandas
|
|
91
133
|
|
|
@@ -102,7 +144,10 @@ __all__ = [
|
|
|
102
144
|
"get_ld_color",
|
|
103
145
|
"get_ld_bin",
|
|
104
146
|
"get_ld_color_palette",
|
|
147
|
+
"get_phewas_category_color",
|
|
148
|
+
"get_phewas_category_palette",
|
|
105
149
|
"LEAD_SNP_COLOR",
|
|
150
|
+
"PHEWAS_CATEGORY_COLORS",
|
|
106
151
|
# Gene track
|
|
107
152
|
"get_nearest_gene",
|
|
108
153
|
"plot_gene_track",
|
|
@@ -136,4 +181,31 @@ __all__ = [
|
|
|
136
181
|
# Validation & Utils
|
|
137
182
|
"ValidationError",
|
|
138
183
|
"to_pandas",
|
|
184
|
+
# PheWAS
|
|
185
|
+
"validate_phewas_df",
|
|
186
|
+
# Forest plot
|
|
187
|
+
"validate_forest_df",
|
|
188
|
+
# GWAS loaders
|
|
189
|
+
"load_gwas",
|
|
190
|
+
"load_plink_assoc",
|
|
191
|
+
"load_regenie",
|
|
192
|
+
"load_bolt_lmm",
|
|
193
|
+
"load_gemma",
|
|
194
|
+
"load_saige",
|
|
195
|
+
"load_gwas_catalog",
|
|
196
|
+
# eQTL loaders
|
|
197
|
+
"load_gtex_eqtl",
|
|
198
|
+
"load_eqtl_catalogue",
|
|
199
|
+
"load_matrixeqtl",
|
|
200
|
+
# Fine-mapping loaders
|
|
201
|
+
"load_susie",
|
|
202
|
+
"load_finemap",
|
|
203
|
+
"load_caviar",
|
|
204
|
+
"load_polyfun",
|
|
205
|
+
# Gene annotation loaders
|
|
206
|
+
"load_gtf",
|
|
207
|
+
"load_bed",
|
|
208
|
+
"load_ensembl_genes",
|
|
209
|
+
# Schema validation
|
|
210
|
+
"LoaderValidationError",
|
|
139
211
|
]
|
pylocuszoom/backends/base.py
CHANGED
|
@@ -341,3 +341,134 @@ class PlotBackend(Protocol):
|
|
|
341
341
|
fig: Figure object.
|
|
342
342
|
"""
|
|
343
343
|
...
|
|
344
|
+
|
|
345
|
+
def add_eqtl_legend(
|
|
346
|
+
self,
|
|
347
|
+
ax: Any,
|
|
348
|
+
eqtl_positive_bins: List[Tuple[float, float, str, str]],
|
|
349
|
+
eqtl_negative_bins: List[Tuple[float, float, str, str]],
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Add eQTL effect size legend to the axes.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
ax: Axes or panel.
|
|
355
|
+
eqtl_positive_bins: List of (min, max, label, color) for positive effects.
|
|
356
|
+
eqtl_negative_bins: List of (min, max, label, color) for negative effects.
|
|
357
|
+
"""
|
|
358
|
+
...
|
|
359
|
+
|
|
360
|
+
def add_finemapping_legend(
|
|
361
|
+
self,
|
|
362
|
+
ax: Any,
|
|
363
|
+
credible_sets: List[int],
|
|
364
|
+
get_color_func: Any,
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Add fine-mapping credible set legend to the axes.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
ax: Axes or panel.
|
|
370
|
+
credible_sets: List of credible set IDs to show.
|
|
371
|
+
get_color_func: Function that takes CS ID and returns color.
|
|
372
|
+
"""
|
|
373
|
+
...
|
|
374
|
+
|
|
375
|
+
def add_simple_legend(
|
|
376
|
+
self,
|
|
377
|
+
ax: Any,
|
|
378
|
+
label: str,
|
|
379
|
+
loc: str = "upper right",
|
|
380
|
+
) -> None:
|
|
381
|
+
"""Add a simple legend entry for labeled data already in the plot.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
ax: Axes or panel.
|
|
385
|
+
label: Legend label for labeled scatter data.
|
|
386
|
+
loc: Legend location.
|
|
387
|
+
"""
|
|
388
|
+
...
|
|
389
|
+
|
|
390
|
+
def axvline(
|
|
391
|
+
self,
|
|
392
|
+
ax: Any,
|
|
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
|
+
...
|
|
415
|
+
|
|
416
|
+
def hbar(
|
|
417
|
+
self,
|
|
418
|
+
ax: Any,
|
|
419
|
+
y: pd.Series,
|
|
420
|
+
width: pd.Series,
|
|
421
|
+
height: float = 0.8,
|
|
422
|
+
left: Union[float, pd.Series] = 0,
|
|
423
|
+
color: Union[str, List[str]] = "blue",
|
|
424
|
+
edgecolor: str = "black",
|
|
425
|
+
linewidth: float = 0.5,
|
|
426
|
+
zorder: int = 2,
|
|
427
|
+
) -> Any:
|
|
428
|
+
"""Create horizontal bar chart.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
ax: Axes or panel.
|
|
432
|
+
y: Y positions for bars.
|
|
433
|
+
width: Bar widths (x-extent).
|
|
434
|
+
height: Bar height.
|
|
435
|
+
left: Left edge positions.
|
|
436
|
+
color: Bar colors.
|
|
437
|
+
edgecolor: Edge color.
|
|
438
|
+
linewidth: Edge width.
|
|
439
|
+
zorder: Drawing order.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
The bar collection object.
|
|
443
|
+
"""
|
|
444
|
+
...
|
|
445
|
+
|
|
446
|
+
def errorbar_h(
|
|
447
|
+
self,
|
|
448
|
+
ax: Any,
|
|
449
|
+
x: pd.Series,
|
|
450
|
+
y: pd.Series,
|
|
451
|
+
xerr_lower: pd.Series,
|
|
452
|
+
xerr_upper: pd.Series,
|
|
453
|
+
color: str = "black",
|
|
454
|
+
linewidth: float = 1.5,
|
|
455
|
+
capsize: float = 3,
|
|
456
|
+
zorder: int = 3,
|
|
457
|
+
) -> Any:
|
|
458
|
+
"""Add horizontal error bars (for forest plots).
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
ax: Axes or panel.
|
|
462
|
+
x: X positions (effect sizes).
|
|
463
|
+
y: Y positions.
|
|
464
|
+
xerr_lower: Lower error (distance from x).
|
|
465
|
+
xerr_upper: Upper error (distance from x).
|
|
466
|
+
color: Line color.
|
|
467
|
+
linewidth: Line width.
|
|
468
|
+
capsize: Cap size in points.
|
|
469
|
+
zorder: Drawing order.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
The errorbar object.
|
|
473
|
+
"""
|
|
474
|
+
...
|
|
@@ -19,15 +19,24 @@ class BokehBackend:
|
|
|
19
19
|
applications and dashboards.
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
# Class constants for style mappings
|
|
23
|
+
_MARKER_MAP = {
|
|
24
|
+
"o": "circle",
|
|
25
|
+
"D": "diamond",
|
|
26
|
+
"s": "square",
|
|
27
|
+
"^": "triangle",
|
|
28
|
+
"v": "inverted_triangle",
|
|
29
|
+
}
|
|
30
|
+
_DASH_MAP = {
|
|
31
|
+
"-": "solid",
|
|
32
|
+
"--": "dashed",
|
|
33
|
+
":": "dotted",
|
|
34
|
+
"-.": "dashdot",
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
def __init__(self) -> None:
|
|
23
38
|
"""Initialize the bokeh backend."""
|
|
24
|
-
|
|
25
|
-
"o": "circle",
|
|
26
|
-
"D": "diamond",
|
|
27
|
-
"s": "square",
|
|
28
|
-
"^": "triangle",
|
|
29
|
-
"v": "inverted_triangle",
|
|
30
|
-
}
|
|
39
|
+
pass
|
|
31
40
|
|
|
32
41
|
def create_figure(
|
|
33
42
|
self,
|
|
@@ -67,9 +76,13 @@ class BokehBackend:
|
|
|
67
76
|
toolbar_location="above" if i == 0 else None,
|
|
68
77
|
)
|
|
69
78
|
|
|
70
|
-
# Style
|
|
71
|
-
p.grid.
|
|
79
|
+
# Style - no grid lines, black axes for clean LocusZoom appearance
|
|
80
|
+
p.grid.visible = False
|
|
72
81
|
p.outline_line_color = None
|
|
82
|
+
p.xaxis.axis_line_color = "black"
|
|
83
|
+
p.yaxis.axis_line_color = "black"
|
|
84
|
+
p.xaxis.minor_tick_line_color = None
|
|
85
|
+
p.yaxis.minor_tick_line_color = None
|
|
73
86
|
|
|
74
87
|
figures.append(p)
|
|
75
88
|
|
|
@@ -115,9 +128,9 @@ class BokehBackend:
|
|
|
115
128
|
for col in hover_data.columns:
|
|
116
129
|
data[col] = hover_data[col].values
|
|
117
130
|
col_lower = col.lower()
|
|
118
|
-
if col_lower
|
|
131
|
+
if col_lower in ("p-value", "pval", "p_value"):
|
|
119
132
|
tooltips.append((col, "@{" + col + "}{0.2e}"))
|
|
120
|
-
elif
|
|
133
|
+
elif any(x in col_lower for x in ("r2", "r²", "ld")):
|
|
121
134
|
tooltips.append((col, "@{" + col + "}{0.3f}"))
|
|
122
135
|
elif "pos" in col_lower:
|
|
123
136
|
tooltips.append((col, "@{" + col + "}{0,0}"))
|
|
@@ -127,7 +140,7 @@ class BokehBackend:
|
|
|
127
140
|
source = ColumnDataSource(data)
|
|
128
141
|
|
|
129
142
|
# Get marker type for scatter()
|
|
130
|
-
marker_type = self.
|
|
143
|
+
marker_type = self._MARKER_MAP.get(marker, "circle")
|
|
131
144
|
|
|
132
145
|
# Create scatter using scatter() method (Bokeh 3.4+ preferred API)
|
|
133
146
|
scatter_kwargs = {
|
|
@@ -167,14 +180,7 @@ class BokehBackend:
|
|
|
167
180
|
label: Optional[str] = None,
|
|
168
181
|
) -> Any:
|
|
169
182
|
"""Create a line plot on the given figure."""
|
|
170
|
-
|
|
171
|
-
dash_map = {
|
|
172
|
-
"-": "solid",
|
|
173
|
-
"--": "dashed",
|
|
174
|
-
":": "dotted",
|
|
175
|
-
"-.": "dashdot",
|
|
176
|
-
}
|
|
177
|
-
line_dash = dash_map.get(linestyle, "solid")
|
|
183
|
+
line_dash = self._DASH_MAP.get(linestyle, "solid")
|
|
178
184
|
|
|
179
185
|
line_kwargs = {
|
|
180
186
|
"line_color": color,
|
|
@@ -229,8 +235,7 @@ class BokehBackend:
|
|
|
229
235
|
zorder: int = 1,
|
|
230
236
|
) -> Any:
|
|
231
237
|
"""Add a horizontal line across the figure."""
|
|
232
|
-
|
|
233
|
-
line_dash = dash_map.get(linestyle, "dashed")
|
|
238
|
+
line_dash = self._DASH_MAP.get(linestyle, "dashed")
|
|
234
239
|
|
|
235
240
|
span = Span(
|
|
236
241
|
location=y,
|
|
@@ -351,6 +356,16 @@ class BokehBackend:
|
|
|
351
356
|
ax.yaxis.axis_label = label
|
|
352
357
|
ax.yaxis.axis_label_text_font_size = f"{fontsize}pt"
|
|
353
358
|
|
|
359
|
+
def _get_legend_location(self, loc: str, default: str = "top_left") -> str:
|
|
360
|
+
"""Map matplotlib-style legend location to Bokeh location."""
|
|
361
|
+
loc_map = {
|
|
362
|
+
"upper left": "top_left",
|
|
363
|
+
"upper right": "top_right",
|
|
364
|
+
"lower left": "bottom_left",
|
|
365
|
+
"lower right": "bottom_right",
|
|
366
|
+
}
|
|
367
|
+
return loc_map.get(loc, default)
|
|
368
|
+
|
|
354
369
|
def _convert_label(self, label: str) -> str:
|
|
355
370
|
"""Convert LaTeX-style labels to Unicode for Bokeh display."""
|
|
356
371
|
conversions = [
|
|
@@ -397,8 +412,7 @@ class BokehBackend:
|
|
|
397
412
|
yaxis_name: str = "secondary",
|
|
398
413
|
) -> Any:
|
|
399
414
|
"""Create a line plot on secondary y-axis."""
|
|
400
|
-
|
|
401
|
-
line_dash = dash_map.get(linestyle, "solid")
|
|
415
|
+
line_dash = self._DASH_MAP.get(linestyle, "solid")
|
|
402
416
|
|
|
403
417
|
return ax.line(
|
|
404
418
|
x.values,
|
|
@@ -465,60 +479,90 @@ class BokehBackend:
|
|
|
465
479
|
label = self._convert_label(label)
|
|
466
480
|
# Find the secondary axis and update its label
|
|
467
481
|
for renderer in ax.right:
|
|
468
|
-
if
|
|
482
|
+
if (
|
|
483
|
+
hasattr(renderer, "y_range_name")
|
|
484
|
+
and renderer.y_range_name == yaxis_name
|
|
485
|
+
):
|
|
469
486
|
renderer.axis_label = label
|
|
470
487
|
renderer.axis_label_text_font_size = f"{fontsize}pt"
|
|
471
488
|
renderer.axis_label_text_color = color
|
|
472
489
|
renderer.major_label_text_color = color
|
|
473
490
|
break
|
|
474
491
|
|
|
475
|
-
def
|
|
476
|
-
|
|
477
|
-
ax: figure,
|
|
478
|
-
ld_bins: List[Tuple[float, str, str]],
|
|
479
|
-
lead_snp_color: str,
|
|
480
|
-
) -> None:
|
|
481
|
-
"""Add LD color legend using invisible dummy glyphs.
|
|
492
|
+
def _ensure_legend_range(self, ax: figure) -> Any:
|
|
493
|
+
"""Ensure legend range exists and return a dummy data source.
|
|
482
494
|
|
|
483
|
-
Creates
|
|
484
|
-
the
|
|
495
|
+
Creates a separate y-range for legend glyphs so they don't affect
|
|
496
|
+
the main plot's axis scaling.
|
|
485
497
|
"""
|
|
486
|
-
from bokeh.models import ColumnDataSource,
|
|
487
|
-
|
|
488
|
-
legend_items = []
|
|
498
|
+
from bokeh.models import ColumnDataSource, Range1d
|
|
489
499
|
|
|
490
|
-
# Create a separate range for legend glyphs that won't affect the main plot
|
|
491
500
|
if "legend_range" not in ax.extra_y_ranges:
|
|
492
501
|
ax.extra_y_ranges["legend_range"] = Range1d(start=0, end=1)
|
|
502
|
+
return ColumnDataSource(data={"x": [0], "y": [0]})
|
|
493
503
|
|
|
494
|
-
|
|
495
|
-
|
|
504
|
+
def _add_legend_item(
|
|
505
|
+
self,
|
|
506
|
+
ax: figure,
|
|
507
|
+
source: Any,
|
|
508
|
+
label: str,
|
|
509
|
+
color: str,
|
|
510
|
+
marker: str,
|
|
511
|
+
size: int = 10,
|
|
512
|
+
) -> Any:
|
|
513
|
+
"""Create an invisible scatter renderer for a legend entry."""
|
|
514
|
+
from bokeh.models import LegendItem
|
|
515
|
+
|
|
516
|
+
renderer = ax.scatter(
|
|
517
|
+
x="x",
|
|
518
|
+
y="y",
|
|
519
|
+
source=source,
|
|
520
|
+
marker=marker,
|
|
521
|
+
size=size,
|
|
522
|
+
fill_color=color,
|
|
523
|
+
line_color="black",
|
|
524
|
+
line_width=0.5,
|
|
525
|
+
y_range_name="legend_range",
|
|
526
|
+
visible=False,
|
|
527
|
+
)
|
|
528
|
+
return LegendItem(label=label, renderers=[renderer])
|
|
496
529
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
x="x",
|
|
501
|
-
y="y",
|
|
502
|
-
marker="square",
|
|
503
|
-
size=10,
|
|
504
|
-
fill_color=color,
|
|
505
|
-
line_color="black",
|
|
506
|
-
line_width=0.5,
|
|
507
|
-
)
|
|
508
|
-
renderer = ax.add_glyph(dummy_source, glyph)
|
|
509
|
-
renderer.y_range_name = "legend_range"
|
|
510
|
-
renderer.visible = False
|
|
511
|
-
legend_items.append(LegendItem(label=label, renderers=[renderer]))
|
|
530
|
+
def _create_legend(self, ax: figure, items: List[Any], title: str) -> None:
|
|
531
|
+
"""Create and add a styled legend to the figure."""
|
|
532
|
+
from bokeh.models import Legend
|
|
512
533
|
|
|
513
534
|
legend = Legend(
|
|
514
|
-
items=
|
|
535
|
+
items=items,
|
|
515
536
|
location="top_right",
|
|
516
|
-
title=
|
|
537
|
+
title=title,
|
|
517
538
|
background_fill_alpha=0.9,
|
|
518
539
|
border_line_color="black",
|
|
540
|
+
spacing=0,
|
|
541
|
+
padding=4,
|
|
542
|
+
label_height=12,
|
|
543
|
+
glyph_height=12,
|
|
519
544
|
)
|
|
520
545
|
ax.add_layout(legend)
|
|
521
546
|
|
|
547
|
+
def add_ld_legend(
|
|
548
|
+
self,
|
|
549
|
+
ax: figure,
|
|
550
|
+
ld_bins: List[Tuple[float, str, str]],
|
|
551
|
+
lead_snp_color: str,
|
|
552
|
+
) -> None:
|
|
553
|
+
"""Add LD color legend using invisible dummy glyphs.
|
|
554
|
+
|
|
555
|
+
Creates legend entries with dummy renderers that are excluded from
|
|
556
|
+
the data range calculation to avoid affecting axis scaling.
|
|
557
|
+
"""
|
|
558
|
+
source = self._ensure_legend_range(ax)
|
|
559
|
+
items = [
|
|
560
|
+
self._add_legend_item(ax, source, "Lead SNP", lead_snp_color, "diamond", 12)
|
|
561
|
+
]
|
|
562
|
+
for _, label, color in ld_bins:
|
|
563
|
+
items.append(self._add_legend_item(ax, source, label, color, "square"))
|
|
564
|
+
self._create_legend(ax, items, "r²")
|
|
565
|
+
|
|
522
566
|
def add_legend(
|
|
523
567
|
self,
|
|
524
568
|
ax: figure,
|
|
@@ -528,17 +572,7 @@ class BokehBackend:
|
|
|
528
572
|
title: Optional[str] = None,
|
|
529
573
|
) -> Any:
|
|
530
574
|
"""Configure legend on the figure."""
|
|
531
|
-
|
|
532
|
-
# Just configure position
|
|
533
|
-
|
|
534
|
-
loc_map = {
|
|
535
|
-
"upper left": "top_left",
|
|
536
|
-
"upper right": "top_right",
|
|
537
|
-
"lower left": "bottom_left",
|
|
538
|
-
"lower right": "bottom_right",
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
ax.legend.location = loc_map.get(loc, "top_left")
|
|
575
|
+
ax.legend.location = self._get_legend_location(loc, "top_left")
|
|
542
576
|
if title:
|
|
543
577
|
ax.legend.title = title
|
|
544
578
|
ax.legend.background_fill_alpha = 0.9
|
|
@@ -554,6 +588,11 @@ class BokehBackend:
|
|
|
554
588
|
"""
|
|
555
589
|
pass
|
|
556
590
|
|
|
591
|
+
def hide_yaxis(self, ax: figure) -> None:
|
|
592
|
+
"""Hide y-axis ticks, labels, line, and grid for gene track panels."""
|
|
593
|
+
ax.yaxis.visible = False
|
|
594
|
+
ax.ygrid.visible = False
|
|
595
|
+
|
|
557
596
|
def format_xaxis_mb(self, ax: figure) -> None:
|
|
558
597
|
"""Format x-axis to show megabase values."""
|
|
559
598
|
from bokeh.models import CustomJSTickFormatter
|
|
@@ -593,6 +632,153 @@ class BokehBackend:
|
|
|
593
632
|
"""Close the figure (no-op for bokeh)."""
|
|
594
633
|
pass
|
|
595
634
|
|
|
635
|
+
def add_eqtl_legend(
|
|
636
|
+
self,
|
|
637
|
+
ax: figure,
|
|
638
|
+
eqtl_positive_bins: List[Tuple[float, float, str, str]],
|
|
639
|
+
eqtl_negative_bins: List[Tuple[float, float, str, str]],
|
|
640
|
+
) -> None:
|
|
641
|
+
"""Add eQTL effect size legend using invisible dummy glyphs."""
|
|
642
|
+
source = self._ensure_legend_range(ax)
|
|
643
|
+
items = []
|
|
644
|
+
for _, _, label, color in eqtl_positive_bins:
|
|
645
|
+
items.append(self._add_legend_item(ax, source, label, color, "triangle"))
|
|
646
|
+
for _, _, label, color in eqtl_negative_bins:
|
|
647
|
+
items.append(
|
|
648
|
+
self._add_legend_item(ax, source, label, color, "inverted_triangle")
|
|
649
|
+
)
|
|
650
|
+
self._create_legend(ax, items, "eQTL effect")
|
|
651
|
+
|
|
652
|
+
def add_finemapping_legend(
|
|
653
|
+
self,
|
|
654
|
+
ax: figure,
|
|
655
|
+
credible_sets: List[int],
|
|
656
|
+
get_color_func: Any,
|
|
657
|
+
) -> None:
|
|
658
|
+
"""Add fine-mapping credible set legend using invisible dummy glyphs."""
|
|
659
|
+
if not credible_sets:
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
source = self._ensure_legend_range(ax)
|
|
663
|
+
items = [
|
|
664
|
+
self._add_legend_item(
|
|
665
|
+
ax, source, f"CS{cs_id}", get_color_func(cs_id), "circle"
|
|
666
|
+
)
|
|
667
|
+
for cs_id in credible_sets
|
|
668
|
+
]
|
|
669
|
+
self._create_legend(ax, items, "Credible sets")
|
|
670
|
+
|
|
671
|
+
def add_simple_legend(
|
|
672
|
+
self,
|
|
673
|
+
ax: figure,
|
|
674
|
+
label: str,
|
|
675
|
+
loc: str = "upper right",
|
|
676
|
+
) -> None:
|
|
677
|
+
"""Configure legend position.
|
|
678
|
+
|
|
679
|
+
Bokeh handles legends automatically from legend_label.
|
|
680
|
+
This just positions the legend.
|
|
681
|
+
"""
|
|
682
|
+
ax.legend.location = self._get_legend_location(loc, "top_right")
|
|
683
|
+
ax.legend.background_fill_alpha = 0.9
|
|
684
|
+
ax.legend.border_line_color = "black"
|
|
685
|
+
|
|
686
|
+
def axvline(
|
|
687
|
+
self,
|
|
688
|
+
ax: figure,
|
|
689
|
+
x: float,
|
|
690
|
+
color: str = "grey",
|
|
691
|
+
linestyle: str = "--",
|
|
692
|
+
linewidth: float = 1.0,
|
|
693
|
+
alpha: float = 1.0,
|
|
694
|
+
zorder: int = 1,
|
|
695
|
+
) -> Any:
|
|
696
|
+
"""Add a vertical line across the figure."""
|
|
697
|
+
line_dash = self._DASH_MAP.get(linestyle, "dashed")
|
|
698
|
+
|
|
699
|
+
span = Span(
|
|
700
|
+
location=x,
|
|
701
|
+
dimension="height",
|
|
702
|
+
line_color=color,
|
|
703
|
+
line_dash=line_dash,
|
|
704
|
+
line_width=linewidth,
|
|
705
|
+
line_alpha=alpha,
|
|
706
|
+
)
|
|
707
|
+
ax.add_layout(span)
|
|
708
|
+
return span
|
|
709
|
+
|
|
710
|
+
def hbar(
|
|
711
|
+
self,
|
|
712
|
+
ax: figure,
|
|
713
|
+
y: pd.Series,
|
|
714
|
+
width: pd.Series,
|
|
715
|
+
height: float = 0.8,
|
|
716
|
+
left: Union[float, pd.Series] = 0,
|
|
717
|
+
color: Union[str, List[str]] = "blue",
|
|
718
|
+
edgecolor: str = "black",
|
|
719
|
+
linewidth: float = 0.5,
|
|
720
|
+
zorder: int = 2,
|
|
721
|
+
) -> Any:
|
|
722
|
+
"""Create horizontal bar chart."""
|
|
723
|
+
# Convert left to array if scalar
|
|
724
|
+
if isinstance(left, (int, float)):
|
|
725
|
+
left_arr = [left] * len(y)
|
|
726
|
+
else:
|
|
727
|
+
left_arr = list(left) if hasattr(left, "tolist") else left
|
|
728
|
+
|
|
729
|
+
# Calculate right edge
|
|
730
|
+
right_arr = [left_val + w for left_val, w in zip(left_arr, width)]
|
|
731
|
+
|
|
732
|
+
return ax.hbar(
|
|
733
|
+
y=y.values,
|
|
734
|
+
right=right_arr,
|
|
735
|
+
left=left_arr,
|
|
736
|
+
height=height,
|
|
737
|
+
fill_color=color,
|
|
738
|
+
line_color=edgecolor,
|
|
739
|
+
line_width=linewidth,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
def errorbar_h(
|
|
743
|
+
self,
|
|
744
|
+
ax: figure,
|
|
745
|
+
x: pd.Series,
|
|
746
|
+
y: pd.Series,
|
|
747
|
+
xerr_lower: pd.Series,
|
|
748
|
+
xerr_upper: pd.Series,
|
|
749
|
+
color: str = "black",
|
|
750
|
+
linewidth: float = 1.5,
|
|
751
|
+
capsize: float = 3,
|
|
752
|
+
zorder: int = 3,
|
|
753
|
+
) -> Any:
|
|
754
|
+
"""Add horizontal error bars."""
|
|
755
|
+
from bokeh.models import Whisker
|
|
756
|
+
|
|
757
|
+
# Calculate bounds
|
|
758
|
+
lower = x - xerr_lower
|
|
759
|
+
upper = x + xerr_upper
|
|
760
|
+
|
|
761
|
+
source = ColumnDataSource(
|
|
762
|
+
data={
|
|
763
|
+
"y": y.values,
|
|
764
|
+
"lower": lower.values,
|
|
765
|
+
"upper": upper.values,
|
|
766
|
+
}
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
# Add horizontal whisker
|
|
770
|
+
whisker = Whisker(
|
|
771
|
+
source=source,
|
|
772
|
+
base="y",
|
|
773
|
+
lower="lower",
|
|
774
|
+
upper="upper",
|
|
775
|
+
dimension="width",
|
|
776
|
+
line_color=color,
|
|
777
|
+
line_width=linewidth,
|
|
778
|
+
)
|
|
779
|
+
ax.add_layout(whisker)
|
|
780
|
+
return whisker
|
|
781
|
+
|
|
596
782
|
def finalize_layout(
|
|
597
783
|
self,
|
|
598
784
|
fig: Any,
|