pylocuszoom 0.5.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 +23 -2
- pylocuszoom/backends/base.py +86 -0
- pylocuszoom/backends/bokeh_backend.py +116 -20
- pylocuszoom/backends/matplotlib_backend.py +69 -0
- pylocuszoom/backends/plotly_backend.py +115 -23
- pylocuszoom/colors.py +41 -0
- pylocuszoom/forest.py +37 -0
- pylocuszoom/loaders.py +35 -17
- pylocuszoom/phewas.py +35 -0
- pylocuszoom/plotter.py +258 -4
- pylocuszoom/recombination.py +45 -31
- pylocuszoom/schemas.py +37 -26
- {pylocuszoom-0.5.0.dist-info → pylocuszoom-0.6.0.dist-info}/METADATA +53 -5
- pylocuszoom-0.6.0.dist-info/RECORD +26 -0
- pylocuszoom-0.5.0.dist-info/RECORD +0 -24
- {pylocuszoom-0.5.0.dist-info → pylocuszoom-0.6.0.dist-info}/WHEEL +0 -0
- {pylocuszoom-0.5.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
|
|
|
@@ -101,6 +112,9 @@ from .loaders import (
|
|
|
101
112
|
|
|
102
113
|
# Logging configuration
|
|
103
114
|
from .logging import disable_logging, enable_logging
|
|
115
|
+
|
|
116
|
+
# PheWAS support
|
|
117
|
+
from .phewas import validate_phewas_df
|
|
104
118
|
from .plotter import LocusZoomPlotter
|
|
105
119
|
|
|
106
120
|
# Reference data management
|
|
@@ -130,7 +144,10 @@ __all__ = [
|
|
|
130
144
|
"get_ld_color",
|
|
131
145
|
"get_ld_bin",
|
|
132
146
|
"get_ld_color_palette",
|
|
147
|
+
"get_phewas_category_color",
|
|
148
|
+
"get_phewas_category_palette",
|
|
133
149
|
"LEAD_SNP_COLOR",
|
|
150
|
+
"PHEWAS_CATEGORY_COLORS",
|
|
134
151
|
# Gene track
|
|
135
152
|
"get_nearest_gene",
|
|
136
153
|
"plot_gene_track",
|
|
@@ -164,6 +181,10 @@ __all__ = [
|
|
|
164
181
|
# Validation & Utils
|
|
165
182
|
"ValidationError",
|
|
166
183
|
"to_pandas",
|
|
184
|
+
# PheWAS
|
|
185
|
+
"validate_phewas_df",
|
|
186
|
+
# Forest plot
|
|
187
|
+
"validate_forest_df",
|
|
167
188
|
# GWAS loaders
|
|
168
189
|
"load_gwas",
|
|
169
190
|
"load_plink_assoc",
|
pylocuszoom/backends/base.py
CHANGED
|
@@ -386,3 +386,89 @@ class PlotBackend(Protocol):
|
|
|
386
386
|
loc: Legend location.
|
|
387
387
|
"""
|
|
388
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,
|
|
@@ -131,7 +140,7 @@ class BokehBackend:
|
|
|
131
140
|
source = ColumnDataSource(data)
|
|
132
141
|
|
|
133
142
|
# Get marker type for scatter()
|
|
134
|
-
marker_type = self.
|
|
143
|
+
marker_type = self._MARKER_MAP.get(marker, "circle")
|
|
135
144
|
|
|
136
145
|
# Create scatter using scatter() method (Bokeh 3.4+ preferred API)
|
|
137
146
|
scatter_kwargs = {
|
|
@@ -171,14 +180,7 @@ class BokehBackend:
|
|
|
171
180
|
label: Optional[str] = None,
|
|
172
181
|
) -> Any:
|
|
173
182
|
"""Create a line plot on the given figure."""
|
|
174
|
-
|
|
175
|
-
dash_map = {
|
|
176
|
-
"-": "solid",
|
|
177
|
-
"--": "dashed",
|
|
178
|
-
":": "dotted",
|
|
179
|
-
"-.": "dashdot",
|
|
180
|
-
}
|
|
181
|
-
line_dash = dash_map.get(linestyle, "solid")
|
|
183
|
+
line_dash = self._DASH_MAP.get(linestyle, "solid")
|
|
182
184
|
|
|
183
185
|
line_kwargs = {
|
|
184
186
|
"line_color": color,
|
|
@@ -233,8 +235,7 @@ class BokehBackend:
|
|
|
233
235
|
zorder: int = 1,
|
|
234
236
|
) -> Any:
|
|
235
237
|
"""Add a horizontal line across the figure."""
|
|
236
|
-
|
|
237
|
-
line_dash = dash_map.get(linestyle, "dashed")
|
|
238
|
+
line_dash = self._DASH_MAP.get(linestyle, "dashed")
|
|
238
239
|
|
|
239
240
|
span = Span(
|
|
240
241
|
location=y,
|
|
@@ -411,8 +412,7 @@ class BokehBackend:
|
|
|
411
412
|
yaxis_name: str = "secondary",
|
|
412
413
|
) -> Any:
|
|
413
414
|
"""Create a line plot on secondary y-axis."""
|
|
414
|
-
|
|
415
|
-
line_dash = dash_map.get(linestyle, "solid")
|
|
415
|
+
line_dash = self._DASH_MAP.get(linestyle, "solid")
|
|
416
416
|
|
|
417
417
|
return ax.line(
|
|
418
418
|
x.values,
|
|
@@ -683,6 +683,102 @@ class BokehBackend:
|
|
|
683
683
|
ax.legend.background_fill_alpha = 0.9
|
|
684
684
|
ax.legend.border_line_color = "black"
|
|
685
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
|
+
|
|
686
782
|
def finalize_layout(
|
|
687
783
|
self,
|
|
688
784
|
fig: Any,
|
|
@@ -394,6 +394,75 @@ class MatplotlibBackend:
|
|
|
394
394
|
"""Add simple legend for labeled scatter data."""
|
|
395
395
|
ax.legend(loc=loc, fontsize=9)
|
|
396
396
|
|
|
397
|
+
def axvline(
|
|
398
|
+
self,
|
|
399
|
+
ax: Axes,
|
|
400
|
+
x: float,
|
|
401
|
+
color: str = "grey",
|
|
402
|
+
linestyle: str = "--",
|
|
403
|
+
linewidth: float = 1.0,
|
|
404
|
+
alpha: float = 1.0,
|
|
405
|
+
zorder: int = 1,
|
|
406
|
+
) -> Any:
|
|
407
|
+
"""Add a vertical line across the axes."""
|
|
408
|
+
return ax.axvline(
|
|
409
|
+
x=x,
|
|
410
|
+
color=color,
|
|
411
|
+
linestyle=linestyle,
|
|
412
|
+
linewidth=linewidth,
|
|
413
|
+
alpha=alpha,
|
|
414
|
+
zorder=zorder,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def hbar(
|
|
418
|
+
self,
|
|
419
|
+
ax: Axes,
|
|
420
|
+
y: pd.Series,
|
|
421
|
+
width: pd.Series,
|
|
422
|
+
height: float = 0.8,
|
|
423
|
+
left: Union[float, pd.Series] = 0,
|
|
424
|
+
color: Union[str, List[str]] = "blue",
|
|
425
|
+
edgecolor: str = "black",
|
|
426
|
+
linewidth: float = 0.5,
|
|
427
|
+
zorder: int = 2,
|
|
428
|
+
) -> Any:
|
|
429
|
+
"""Create horizontal bar chart."""
|
|
430
|
+
return ax.barh(
|
|
431
|
+
y=y,
|
|
432
|
+
width=width,
|
|
433
|
+
height=height,
|
|
434
|
+
left=left,
|
|
435
|
+
color=color,
|
|
436
|
+
edgecolor=edgecolor,
|
|
437
|
+
linewidth=linewidth,
|
|
438
|
+
zorder=zorder,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
def errorbar_h(
|
|
442
|
+
self,
|
|
443
|
+
ax: Axes,
|
|
444
|
+
x: pd.Series,
|
|
445
|
+
y: pd.Series,
|
|
446
|
+
xerr_lower: pd.Series,
|
|
447
|
+
xerr_upper: pd.Series,
|
|
448
|
+
color: str = "black",
|
|
449
|
+
linewidth: float = 1.5,
|
|
450
|
+
capsize: float = 3,
|
|
451
|
+
zorder: int = 3,
|
|
452
|
+
) -> Any:
|
|
453
|
+
"""Add horizontal error bars."""
|
|
454
|
+
xerr = [xerr_lower.values, xerr_upper.values]
|
|
455
|
+
return ax.errorbar(
|
|
456
|
+
x=x,
|
|
457
|
+
y=y,
|
|
458
|
+
xerr=xerr,
|
|
459
|
+
fmt="none",
|
|
460
|
+
ecolor=color,
|
|
461
|
+
elinewidth=linewidth,
|
|
462
|
+
capsize=capsize,
|
|
463
|
+
zorder=zorder,
|
|
464
|
+
)
|
|
465
|
+
|
|
397
466
|
def finalize_layout(
|
|
398
467
|
self,
|
|
399
468
|
fig: Figure,
|
|
@@ -20,15 +20,24 @@ class PlotlyBackend:
|
|
|
20
20
|
- Nearest gene
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
|
+
# Class constants for style mappings
|
|
24
|
+
_MARKER_SYMBOLS = {
|
|
25
|
+
"o": "circle",
|
|
26
|
+
"D": "diamond",
|
|
27
|
+
"s": "square",
|
|
28
|
+
"^": "triangle-up",
|
|
29
|
+
"v": "triangle-down",
|
|
30
|
+
}
|
|
31
|
+
_DASH_MAP = {
|
|
32
|
+
"-": "solid",
|
|
33
|
+
"--": "dash",
|
|
34
|
+
":": "dot",
|
|
35
|
+
"-.": "dashdot",
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
def __init__(self) -> None:
|
|
24
39
|
"""Initialize the plotly backend."""
|
|
25
|
-
|
|
26
|
-
"o": "circle",
|
|
27
|
-
"D": "diamond",
|
|
28
|
-
"s": "square",
|
|
29
|
-
"^": "triangle-up",
|
|
30
|
-
"v": "triangle-down",
|
|
31
|
-
}
|
|
40
|
+
pass
|
|
32
41
|
|
|
33
42
|
def create_figure(
|
|
34
43
|
self,
|
|
@@ -111,7 +120,7 @@ class PlotlyBackend:
|
|
|
111
120
|
fig, row = ax
|
|
112
121
|
|
|
113
122
|
# Convert matplotlib marker to plotly symbol
|
|
114
|
-
symbol = self.
|
|
123
|
+
symbol = self._MARKER_SYMBOLS.get(marker, "circle")
|
|
115
124
|
|
|
116
125
|
# Convert size (matplotlib uses area, plotly uses diameter)
|
|
117
126
|
if isinstance(sizes, (int, float)):
|
|
@@ -178,15 +187,7 @@ class PlotlyBackend:
|
|
|
178
187
|
) -> Any:
|
|
179
188
|
"""Create a line plot on the given panel."""
|
|
180
189
|
fig, row = ax
|
|
181
|
-
|
|
182
|
-
# Convert linestyle
|
|
183
|
-
dash_map = {
|
|
184
|
-
"-": "solid",
|
|
185
|
-
"--": "dash",
|
|
186
|
-
":": "dot",
|
|
187
|
-
"-.": "dashdot",
|
|
188
|
-
}
|
|
189
|
-
dash = dash_map.get(linestyle, "solid")
|
|
190
|
+
dash = self._DASH_MAP.get(linestyle, "solid")
|
|
190
191
|
|
|
191
192
|
trace = go.Scatter(
|
|
192
193
|
x=x,
|
|
@@ -244,9 +245,7 @@ class PlotlyBackend:
|
|
|
244
245
|
) -> Any:
|
|
245
246
|
"""Add a horizontal line across the panel."""
|
|
246
247
|
fig, row = ax
|
|
247
|
-
|
|
248
|
-
dash_map = {"-": "solid", "--": "dash", ":": "dot", "-.": "dashdot"}
|
|
249
|
-
dash = dash_map.get(linestyle, "dash")
|
|
248
|
+
dash = self._DASH_MAP.get(linestyle, "dash")
|
|
250
249
|
|
|
251
250
|
fig.add_hline(
|
|
252
251
|
y=y,
|
|
@@ -461,9 +460,7 @@ class PlotlyBackend:
|
|
|
461
460
|
) -> Any:
|
|
462
461
|
"""Create a line plot on secondary y-axis."""
|
|
463
462
|
fig, row = ax
|
|
464
|
-
|
|
465
|
-
dash_map = {"-": "solid", "--": "dash", ":": "dot", "-.": "dashdot"}
|
|
466
|
-
dash = dash_map.get(linestyle, "solid")
|
|
463
|
+
dash = self._DASH_MAP.get(linestyle, "solid")
|
|
467
464
|
|
|
468
465
|
trace = go.Scatter(
|
|
469
466
|
x=x,
|
|
@@ -782,6 +779,101 @@ class PlotlyBackend:
|
|
|
782
779
|
)
|
|
783
780
|
)
|
|
784
781
|
|
|
782
|
+
def axvline(
|
|
783
|
+
self,
|
|
784
|
+
ax: Tuple[go.Figure, int],
|
|
785
|
+
x: float,
|
|
786
|
+
color: str = "grey",
|
|
787
|
+
linestyle: str = "--",
|
|
788
|
+
linewidth: float = 1.0,
|
|
789
|
+
alpha: float = 1.0,
|
|
790
|
+
zorder: int = 1,
|
|
791
|
+
) -> Any:
|
|
792
|
+
"""Add a vertical line across the panel."""
|
|
793
|
+
fig, row = ax
|
|
794
|
+
dash = self._DASH_MAP.get(linestyle, "dash")
|
|
795
|
+
|
|
796
|
+
fig.add_vline(
|
|
797
|
+
x=x,
|
|
798
|
+
line_dash=dash,
|
|
799
|
+
line_color=color,
|
|
800
|
+
line_width=linewidth,
|
|
801
|
+
opacity=alpha,
|
|
802
|
+
row=row,
|
|
803
|
+
col=1,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
def hbar(
|
|
807
|
+
self,
|
|
808
|
+
ax: Tuple[go.Figure, int],
|
|
809
|
+
y: pd.Series,
|
|
810
|
+
width: pd.Series,
|
|
811
|
+
height: float = 0.8,
|
|
812
|
+
left: Union[float, pd.Series] = 0,
|
|
813
|
+
color: Union[str, List[str]] = "blue",
|
|
814
|
+
edgecolor: str = "black",
|
|
815
|
+
linewidth: float = 0.5,
|
|
816
|
+
zorder: int = 2,
|
|
817
|
+
) -> Any:
|
|
818
|
+
"""Create horizontal bar chart."""
|
|
819
|
+
fig, row = ax
|
|
820
|
+
|
|
821
|
+
# Convert left to array if scalar
|
|
822
|
+
if isinstance(left, (int, float)):
|
|
823
|
+
left_arr = [left] * len(y)
|
|
824
|
+
else:
|
|
825
|
+
left_arr = list(left) if hasattr(left, "tolist") else left
|
|
826
|
+
|
|
827
|
+
trace = go.Bar(
|
|
828
|
+
y=y,
|
|
829
|
+
x=width,
|
|
830
|
+
orientation="h",
|
|
831
|
+
base=left_arr,
|
|
832
|
+
marker=dict(
|
|
833
|
+
color=color,
|
|
834
|
+
line=dict(color=edgecolor, width=linewidth),
|
|
835
|
+
),
|
|
836
|
+
showlegend=False,
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
fig.add_trace(trace, row=row, col=1)
|
|
840
|
+
return trace
|
|
841
|
+
|
|
842
|
+
def errorbar_h(
|
|
843
|
+
self,
|
|
844
|
+
ax: Tuple[go.Figure, int],
|
|
845
|
+
x: pd.Series,
|
|
846
|
+
y: pd.Series,
|
|
847
|
+
xerr_lower: pd.Series,
|
|
848
|
+
xerr_upper: pd.Series,
|
|
849
|
+
color: str = "black",
|
|
850
|
+
linewidth: float = 1.5,
|
|
851
|
+
capsize: float = 3,
|
|
852
|
+
zorder: int = 3,
|
|
853
|
+
) -> Any:
|
|
854
|
+
"""Add horizontal error bars."""
|
|
855
|
+
fig, row = ax
|
|
856
|
+
|
|
857
|
+
trace = go.Scatter(
|
|
858
|
+
x=x,
|
|
859
|
+
y=y,
|
|
860
|
+
mode="markers",
|
|
861
|
+
marker=dict(size=0),
|
|
862
|
+
error_x=dict(
|
|
863
|
+
type="data",
|
|
864
|
+
symmetric=False,
|
|
865
|
+
array=xerr_upper,
|
|
866
|
+
arrayminus=xerr_lower,
|
|
867
|
+
color=color,
|
|
868
|
+
thickness=linewidth,
|
|
869
|
+
width=capsize,
|
|
870
|
+
),
|
|
871
|
+
showlegend=False,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
fig.add_trace(trace, row=row, col=1)
|
|
875
|
+
return trace
|
|
876
|
+
|
|
785
877
|
def finalize_layout(
|
|
786
878
|
self,
|
|
787
879
|
fig: go.Figure,
|
pylocuszoom/colors.py
CHANGED
|
@@ -239,3 +239,44 @@ def get_credible_set_color_palette(n_sets: int = 10) -> dict[int, str]:
|
|
|
239
239
|
return {
|
|
240
240
|
i + 1: CREDIBLE_SET_COLORS[i % len(CREDIBLE_SET_COLORS)] for i in range(n_sets)
|
|
241
241
|
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# PheWAS category colors - distinct colors for phenotype categories
|
|
245
|
+
PHEWAS_CATEGORY_COLORS: List[str] = [
|
|
246
|
+
"#E41A1C", # red
|
|
247
|
+
"#377EB8", # blue
|
|
248
|
+
"#4DAF4A", # green
|
|
249
|
+
"#984EA3", # purple
|
|
250
|
+
"#FF7F00", # orange
|
|
251
|
+
"#FFFF33", # yellow
|
|
252
|
+
"#A65628", # brown
|
|
253
|
+
"#F781BF", # pink
|
|
254
|
+
"#999999", # grey
|
|
255
|
+
"#66C2A5", # teal
|
|
256
|
+
"#FC8D62", # salmon
|
|
257
|
+
"#8DA0CB", # periwinkle
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_phewas_category_color(category_idx: int) -> str:
|
|
262
|
+
"""Get color for a PheWAS category by index.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
category_idx: Zero-indexed category number.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Hex color code string.
|
|
269
|
+
"""
|
|
270
|
+
return PHEWAS_CATEGORY_COLORS[category_idx % len(PHEWAS_CATEGORY_COLORS)]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def get_phewas_category_palette(categories: List[str]) -> dict[str, str]:
|
|
274
|
+
"""Get color palette mapping category names to colors.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
categories: List of unique category names.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Dictionary mapping category names to hex colors.
|
|
281
|
+
"""
|
|
282
|
+
return {cat: get_phewas_category_color(i) for i, cat in enumerate(categories)}
|
pylocuszoom/forest.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Forest plot data validation and preparation.
|
|
2
|
+
|
|
3
|
+
Validates and prepares meta-analysis/forest plot data for visualization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from .utils import ValidationError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_forest_df(
|
|
12
|
+
df: pd.DataFrame,
|
|
13
|
+
study_col: str = "study",
|
|
14
|
+
effect_col: str = "effect",
|
|
15
|
+
ci_lower_col: str = "ci_lower",
|
|
16
|
+
ci_upper_col: str = "ci_upper",
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Validate forest plot DataFrame has required columns.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
df: Forest plot data DataFrame.
|
|
22
|
+
study_col: Column name for study/phenotype names.
|
|
23
|
+
effect_col: Column name for effect sizes (beta, OR, HR).
|
|
24
|
+
ci_lower_col: Column name for lower confidence interval.
|
|
25
|
+
ci_upper_col: Column name for upper confidence interval.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValidationError: If required columns are missing.
|
|
29
|
+
"""
|
|
30
|
+
required = [study_col, effect_col, ci_lower_col, ci_upper_col]
|
|
31
|
+
missing = [col for col in required if col not in df.columns]
|
|
32
|
+
|
|
33
|
+
if missing:
|
|
34
|
+
raise ValidationError(
|
|
35
|
+
f"Forest plot DataFrame missing required columns: {missing}. "
|
|
36
|
+
f"Required: {required}. Found: {list(df.columns)}"
|
|
37
|
+
)
|