pylocuszoom 1.2.0__py3-none-any.whl → 1.3.1__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 +16 -2
- pylocuszoom/backends/base.py +94 -2
- pylocuszoom/backends/bokeh_backend.py +160 -6
- pylocuszoom/backends/matplotlib_backend.py +142 -2
- pylocuszoom/backends/plotly_backend.py +101 -1
- pylocuszoom/coloc.py +82 -0
- pylocuszoom/coloc_plotter.py +390 -0
- pylocuszoom/colors.py +26 -0
- pylocuszoom/config.py +61 -0
- pylocuszoom/labels.py +41 -16
- pylocuszoom/ld.py +239 -0
- pylocuszoom/ld_heatmap_plotter.py +252 -0
- pylocuszoom/miami_plotter.py +490 -0
- pylocuszoom/plotter.py +472 -6
- {pylocuszoom-1.2.0.dist-info → pylocuszoom-1.3.1.dist-info}/METADATA +166 -21
- {pylocuszoom-1.2.0.dist-info → pylocuszoom-1.3.1.dist-info}/RECORD +18 -14
- pylocuszoom-1.3.1.dist-info/licenses/LICENSE.md +595 -0
- pylocuszoom-1.2.0.dist-info/licenses/LICENSE.md +0 -17
- {pylocuszoom-1.2.0.dist-info → pylocuszoom-1.3.1.dist-info}/WHEEL +0 -0
pylocuszoom/__init__.py
CHANGED
|
@@ -40,6 +40,9 @@ __version__ = "1.1.2"
|
|
|
40
40
|
# Backend types
|
|
41
41
|
from .backends import BackendType, get_backend
|
|
42
42
|
|
|
43
|
+
# Colocalization visualization
|
|
44
|
+
from .coloc_plotter import ColocPlotter
|
|
45
|
+
|
|
43
46
|
# Colors and LD
|
|
44
47
|
from .colors import (
|
|
45
48
|
LEAD_SNP_COLOR,
|
|
@@ -101,10 +104,13 @@ from .forest import validate_forest_df
|
|
|
101
104
|
from .gene_track import get_nearest_gene, plot_gene_track
|
|
102
105
|
|
|
103
106
|
# Labels
|
|
104
|
-
from .labels import add_snp_labels
|
|
107
|
+
from .labels import add_snp_labels, adjust_snp_labels
|
|
105
108
|
|
|
106
109
|
# LD calculation
|
|
107
|
-
from .ld import calculate_ld
|
|
110
|
+
from .ld import calculate_ld, calculate_pairwise_ld
|
|
111
|
+
|
|
112
|
+
# LD heatmap visualization
|
|
113
|
+
from .ld_heatmap_plotter import LDHeatmapPlotter
|
|
108
114
|
|
|
109
115
|
# File format loaders
|
|
110
116
|
from .loaders import (
|
|
@@ -137,6 +143,9 @@ from .logging import disable_logging, enable_logging
|
|
|
137
143
|
# Manhattan and QQ plotting
|
|
138
144
|
from .manhattan_plotter import ManhattanPlotter
|
|
139
145
|
|
|
146
|
+
# Miami plot (mirrored Manhattan comparison)
|
|
147
|
+
from .miami_plotter import MiamiPlotter
|
|
148
|
+
|
|
140
149
|
# PheWAS support
|
|
141
150
|
from .phewas import validate_phewas_df
|
|
142
151
|
from .plotter import LocusZoomPlotter
|
|
@@ -161,7 +170,10 @@ __all__ = [
|
|
|
161
170
|
"__version__",
|
|
162
171
|
"LocusZoomPlotter",
|
|
163
172
|
"ManhattanPlotter",
|
|
173
|
+
"MiamiPlotter",
|
|
164
174
|
"StatsPlotter",
|
|
175
|
+
"LDHeatmapPlotter",
|
|
176
|
+
"ColocPlotter",
|
|
165
177
|
# Backends
|
|
166
178
|
"BackendType",
|
|
167
179
|
"get_backend",
|
|
@@ -180,8 +192,10 @@ __all__ = [
|
|
|
180
192
|
"plot_gene_track",
|
|
181
193
|
# LD
|
|
182
194
|
"calculate_ld",
|
|
195
|
+
"calculate_pairwise_ld",
|
|
183
196
|
# Labels
|
|
184
197
|
"add_snp_labels",
|
|
198
|
+
"adjust_snp_labels",
|
|
185
199
|
# Recombination
|
|
186
200
|
"add_recombination_overlay",
|
|
187
201
|
"ensure_recomb_maps",
|
pylocuszoom/backends/base.py
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
Defines the interface that matplotlib, plotly, and bokeh backends must implement.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Any, Callable, List, Optional, Protocol, Tuple, Union
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Protocol, Tuple, Union
|
|
7
7
|
|
|
8
8
|
import pandas as pd
|
|
9
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
pass
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
class PlotBackend(Protocol):
|
|
12
15
|
"""Protocol defining the backend interface for LocusZoom plots.
|
|
@@ -330,7 +333,8 @@ class PlotBackend(Protocol):
|
|
|
330
333
|
label_top_n: int,
|
|
331
334
|
genes_df: Optional[pd.DataFrame],
|
|
332
335
|
chrom: int,
|
|
333
|
-
|
|
336
|
+
adjust: bool = True,
|
|
337
|
+
) -> List[Any]:
|
|
334
338
|
"""Add SNP labels to plot.
|
|
335
339
|
|
|
336
340
|
No-op if supports_snp_labels=False. Matplotlib uses adjustText
|
|
@@ -345,6 +349,23 @@ class PlotBackend(Protocol):
|
|
|
345
349
|
label_top_n: Number of top SNPs to label.
|
|
346
350
|
genes_df: Gene annotations (unused, for signature compatibility).
|
|
347
351
|
chrom: Chromosome number (unused, for signature compatibility).
|
|
352
|
+
adjust: If True, run adjustText immediately. If False, caller
|
|
353
|
+
must call adjust_snp_labels() after setting axis limits.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
List of text annotation objects (empty for non-matplotlib backends).
|
|
357
|
+
"""
|
|
358
|
+
...
|
|
359
|
+
|
|
360
|
+
def adjust_snp_labels(self, ax: Any, texts: List[Any]) -> None:
|
|
361
|
+
"""Adjust SNP label positions after axis limits are set.
|
|
362
|
+
|
|
363
|
+
This should be called AFTER all axis limits have been finalized,
|
|
364
|
+
as adjustText needs to know the final plot bounds.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
ax: Axes or panel.
|
|
368
|
+
texts: List of text annotation objects from add_snp_labels().
|
|
348
369
|
"""
|
|
349
370
|
...
|
|
350
371
|
|
|
@@ -686,6 +707,21 @@ class PlotBackend(Protocol):
|
|
|
686
707
|
"""
|
|
687
708
|
...
|
|
688
709
|
|
|
710
|
+
def add_effect_legend(
|
|
711
|
+
self,
|
|
712
|
+
ax: Any,
|
|
713
|
+
effect_bins: List[Tuple[float, str, str]],
|
|
714
|
+
) -> None:
|
|
715
|
+
"""Add effect direction legend for colocalization plots.
|
|
716
|
+
|
|
717
|
+
Shows effect direction categories (same direction, opposite, missing).
|
|
718
|
+
|
|
719
|
+
Args:
|
|
720
|
+
ax: Axes or panel.
|
|
721
|
+
effect_bins: List of (threshold, label, color) tuples.
|
|
722
|
+
"""
|
|
723
|
+
...
|
|
724
|
+
|
|
689
725
|
def add_eqtl_legend(
|
|
690
726
|
self,
|
|
691
727
|
ax: Any,
|
|
@@ -831,3 +867,59 @@ class PlotBackend(Protocol):
|
|
|
831
867
|
fig: Figure object.
|
|
832
868
|
"""
|
|
833
869
|
...
|
|
870
|
+
|
|
871
|
+
# =========================================================================
|
|
872
|
+
# Heatmap Rendering (for LD heatmaps)
|
|
873
|
+
# =========================================================================
|
|
874
|
+
|
|
875
|
+
def add_heatmap(
|
|
876
|
+
self,
|
|
877
|
+
ax: Any,
|
|
878
|
+
data: Any,
|
|
879
|
+
x_coords: List[float],
|
|
880
|
+
y_coords: List[float],
|
|
881
|
+
cmap_colors: Optional[List[str]] = None,
|
|
882
|
+
vmin: float = 0.0,
|
|
883
|
+
vmax: float = 1.0,
|
|
884
|
+
mask_upper: bool = True,
|
|
885
|
+
) -> Any:
|
|
886
|
+
"""Render heatmap with optional triangular masking.
|
|
887
|
+
|
|
888
|
+
Used for LD heatmap visualization where data is a symmetric matrix
|
|
889
|
+
and typically only the lower triangle is displayed.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
ax: Axes or panel to plot on.
|
|
893
|
+
data: 2D numpy array of values (use NaN for missing data).
|
|
894
|
+
x_coords: X coordinates for cell positions.
|
|
895
|
+
y_coords: Y coordinates for cell positions.
|
|
896
|
+
cmap_colors: Color gradient endpoints [start_color, end_color].
|
|
897
|
+
Defaults to white-to-red ["#FFFFFF", "#FF0000"].
|
|
898
|
+
vmin: Minimum value for color scale.
|
|
899
|
+
vmax: Maximum value for color scale.
|
|
900
|
+
mask_upper: If True, mask upper triangle for lower-triangular display.
|
|
901
|
+
|
|
902
|
+
Returns:
|
|
903
|
+
Heatmap object (mappable for colorbar attachment).
|
|
904
|
+
"""
|
|
905
|
+
...
|
|
906
|
+
|
|
907
|
+
def add_colorbar(
|
|
908
|
+
self,
|
|
909
|
+
ax: Any,
|
|
910
|
+
mappable: Any,
|
|
911
|
+
label: str = "R²",
|
|
912
|
+
orientation: str = "vertical",
|
|
913
|
+
) -> Any:
|
|
914
|
+
"""Add colorbar legend for heatmap.
|
|
915
|
+
|
|
916
|
+
Args:
|
|
917
|
+
ax: Axes or panel (or figure for some backends).
|
|
918
|
+
mappable: Heatmap object returned by add_heatmap.
|
|
919
|
+
label: Colorbar label (e.g., "R²" or "D'").
|
|
920
|
+
orientation: "vertical" or "horizontal".
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
Colorbar object.
|
|
924
|
+
"""
|
|
925
|
+
...
|
|
@@ -623,7 +623,12 @@ class BokehBackend:
|
|
|
623
623
|
label_top_n: int,
|
|
624
624
|
genes_df: Optional[pd.DataFrame],
|
|
625
625
|
chrom: int,
|
|
626
|
-
|
|
626
|
+
adjust: bool = True,
|
|
627
|
+
) -> List[Any]:
|
|
628
|
+
"""No-op: Bokeh uses hover tooltips instead of text labels."""
|
|
629
|
+
return []
|
|
630
|
+
|
|
631
|
+
def adjust_snp_labels(self, ax: Any, texts: List[Any]) -> None:
|
|
627
632
|
"""No-op: Bokeh uses hover tooltips instead of text labels."""
|
|
628
633
|
pass
|
|
629
634
|
|
|
@@ -645,11 +650,16 @@ class BokehBackend:
|
|
|
645
650
|
if hasattr(x_range, "start") and x_range.start is not None
|
|
646
651
|
else 0
|
|
647
652
|
)
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
+
|
|
654
|
+
# Handle both normal and inverted y-axis
|
|
655
|
+
# For inverted axis (start > end), y_frac=0.95 should be near start (top visually)
|
|
656
|
+
if hasattr(y_range, "start") and y_range.start is not None:
|
|
657
|
+
y_min = min(y_range.start, y_range.end)
|
|
658
|
+
y_max = max(y_range.start, y_range.end)
|
|
659
|
+
# Position label at top of visible range regardless of axis direction
|
|
660
|
+
y = y_min + y_frac * (y_max - y_min)
|
|
661
|
+
else:
|
|
662
|
+
y = 0
|
|
653
663
|
|
|
654
664
|
label_obj = Label(
|
|
655
665
|
x=x,
|
|
@@ -734,6 +744,18 @@ class BokehBackend:
|
|
|
734
744
|
items.append(self._add_legend_item(ax, source, label, color, "square"))
|
|
735
745
|
self._create_legend(ax, items, "r²")
|
|
736
746
|
|
|
747
|
+
def add_effect_legend(
|
|
748
|
+
self,
|
|
749
|
+
ax: figure,
|
|
750
|
+
effect_bins: List[Tuple[float, str, str]],
|
|
751
|
+
) -> None:
|
|
752
|
+
"""Add effect direction legend for colocalization plots."""
|
|
753
|
+
source = self._ensure_legend_range(ax)
|
|
754
|
+
items = []
|
|
755
|
+
for _, label, color in effect_bins:
|
|
756
|
+
items.append(self._add_legend_item(ax, source, label, color, "circle"))
|
|
757
|
+
self._create_legend(ax, items, "Effect")
|
|
758
|
+
|
|
737
759
|
def add_legend(
|
|
738
760
|
self,
|
|
739
761
|
ax: figure,
|
|
@@ -968,3 +990,135 @@ class BokehBackend:
|
|
|
968
990
|
"""
|
|
969
991
|
# Bokeh handles layout differently - column spacing is fixed
|
|
970
992
|
pass
|
|
993
|
+
|
|
994
|
+
def add_heatmap(
|
|
995
|
+
self,
|
|
996
|
+
ax: figure,
|
|
997
|
+
data: Any,
|
|
998
|
+
x_coords: List[float],
|
|
999
|
+
y_coords: List[float],
|
|
1000
|
+
cmap_colors: Optional[List[str]] = None,
|
|
1001
|
+
vmin: float = 0.0,
|
|
1002
|
+
vmax: float = 1.0,
|
|
1003
|
+
mask_upper: bool = True,
|
|
1004
|
+
) -> Any:
|
|
1005
|
+
"""Render heatmap with optional triangular masking.
|
|
1006
|
+
|
|
1007
|
+
Args:
|
|
1008
|
+
ax: Bokeh figure.
|
|
1009
|
+
data: 2D numpy array of values (NaN for missing).
|
|
1010
|
+
x_coords: X coordinates for cells.
|
|
1011
|
+
y_coords: Y coordinates for cells.
|
|
1012
|
+
cmap_colors: Color gradient endpoints [start, end].
|
|
1013
|
+
vmin: Minimum value for color scale.
|
|
1014
|
+
vmax: Maximum value for color scale.
|
|
1015
|
+
mask_upper: If True, mask upper triangle.
|
|
1016
|
+
|
|
1017
|
+
Returns:
|
|
1018
|
+
LinearColorMapper for colorbar attachment.
|
|
1019
|
+
"""
|
|
1020
|
+
import numpy as np
|
|
1021
|
+
from bokeh.models import LinearColorMapper
|
|
1022
|
+
|
|
1023
|
+
if cmap_colors is None:
|
|
1024
|
+
cmap_colors = ["#FFFFFF", "#FF0000"]
|
|
1025
|
+
|
|
1026
|
+
# Create custom palette from start to end color
|
|
1027
|
+
# For a simple 2-color gradient, create a palette of intermediate colors
|
|
1028
|
+
palette = _create_color_palette(cmap_colors[0], cmap_colors[1], 256)
|
|
1029
|
+
|
|
1030
|
+
mapper = LinearColorMapper(
|
|
1031
|
+
palette=palette,
|
|
1032
|
+
low=vmin,
|
|
1033
|
+
high=vmax,
|
|
1034
|
+
nan_color="#808080", # Grey for missing
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
# Build rect data for lower triangle
|
|
1038
|
+
xs, ys, values = [], [], []
|
|
1039
|
+
n = data.shape[0]
|
|
1040
|
+
for i in range(n):
|
|
1041
|
+
for j in range(n):
|
|
1042
|
+
# Lower triangle including diagonal
|
|
1043
|
+
if mask_upper and j > i:
|
|
1044
|
+
continue
|
|
1045
|
+
val = data[i, j]
|
|
1046
|
+
xs.append(j)
|
|
1047
|
+
ys.append(i)
|
|
1048
|
+
values.append(val if not np.isnan(val) else float("nan"))
|
|
1049
|
+
|
|
1050
|
+
source = ColumnDataSource({"x": xs, "y": ys, "value": values})
|
|
1051
|
+
|
|
1052
|
+
ax.rect(
|
|
1053
|
+
x="x",
|
|
1054
|
+
y="y",
|
|
1055
|
+
width=1,
|
|
1056
|
+
height=1,
|
|
1057
|
+
fill_color={"field": "value", "transform": mapper},
|
|
1058
|
+
line_color=None,
|
|
1059
|
+
source=source,
|
|
1060
|
+
)
|
|
1061
|
+
return mapper
|
|
1062
|
+
|
|
1063
|
+
def add_colorbar(
|
|
1064
|
+
self,
|
|
1065
|
+
ax: figure,
|
|
1066
|
+
mappable: Any,
|
|
1067
|
+
label: str = "R²",
|
|
1068
|
+
orientation: str = "vertical",
|
|
1069
|
+
) -> Any:
|
|
1070
|
+
"""Add colorbar legend for heatmap.
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
ax: Bokeh figure.
|
|
1074
|
+
mappable: LinearColorMapper from add_heatmap.
|
|
1075
|
+
label: Colorbar label.
|
|
1076
|
+
orientation: "vertical" or "horizontal".
|
|
1077
|
+
|
|
1078
|
+
Returns:
|
|
1079
|
+
ColorBar object.
|
|
1080
|
+
"""
|
|
1081
|
+
from bokeh.models import BasicTicker, ColorBar
|
|
1082
|
+
|
|
1083
|
+
orientation_map = {"vertical": "vertical", "horizontal": "horizontal"}
|
|
1084
|
+
color_bar = ColorBar(
|
|
1085
|
+
color_mapper=mappable,
|
|
1086
|
+
ticker=BasicTicker(),
|
|
1087
|
+
label_standoff=6,
|
|
1088
|
+
title=label,
|
|
1089
|
+
orientation=orientation_map.get(orientation, "vertical"),
|
|
1090
|
+
)
|
|
1091
|
+
ax.add_layout(color_bar, "right")
|
|
1092
|
+
return color_bar
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def _create_color_palette(start_color: str, end_color: str, n_colors: int) -> List[str]:
|
|
1096
|
+
"""Create a linear color palette between two colors.
|
|
1097
|
+
|
|
1098
|
+
Args:
|
|
1099
|
+
start_color: Starting hex color.
|
|
1100
|
+
end_color: Ending hex color.
|
|
1101
|
+
n_colors: Number of colors in palette.
|
|
1102
|
+
|
|
1103
|
+
Returns:
|
|
1104
|
+
List of hex color strings.
|
|
1105
|
+
"""
|
|
1106
|
+
|
|
1107
|
+
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
|
|
1108
|
+
hex_color = hex_color.lstrip("#")
|
|
1109
|
+
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
|
|
1110
|
+
|
|
1111
|
+
def rgb_to_hex(rgb: Tuple[int, int, int]) -> str:
|
|
1112
|
+
return "#{:02x}{:02x}{:02x}".format(*rgb)
|
|
1113
|
+
|
|
1114
|
+
start_rgb = hex_to_rgb(start_color)
|
|
1115
|
+
end_rgb = hex_to_rgb(end_color)
|
|
1116
|
+
|
|
1117
|
+
palette = []
|
|
1118
|
+
for i in range(n_colors):
|
|
1119
|
+
t = i / (n_colors - 1) if n_colors > 1 else 0
|
|
1120
|
+
r = int(start_rgb[0] + t * (end_rgb[0] - start_rgb[0]))
|
|
1121
|
+
g = int(start_rgb[1] + t * (end_rgb[1] - start_rgb[1]))
|
|
1122
|
+
b = int(start_rgb[2] + t * (end_rgb[2] - start_rgb[2]))
|
|
1123
|
+
palette.append(rgb_to_hex((r, g, b)))
|
|
1124
|
+
return palette
|
|
@@ -264,7 +264,8 @@ class MatplotlibBackend:
|
|
|
264
264
|
label_top_n: int,
|
|
265
265
|
genes_df: Optional[pd.DataFrame],
|
|
266
266
|
chrom: int,
|
|
267
|
-
|
|
267
|
+
adjust: bool = True,
|
|
268
|
+
) -> List[Any]:
|
|
268
269
|
"""Add SNP labels using adjustText.
|
|
269
270
|
|
|
270
271
|
Args:
|
|
@@ -276,10 +277,15 @@ class MatplotlibBackend:
|
|
|
276
277
|
label_top_n: Number of top SNPs to label.
|
|
277
278
|
genes_df: Gene annotations (unused, for signature compatibility).
|
|
278
279
|
chrom: Chromosome number (unused, for signature compatibility).
|
|
280
|
+
adjust: If True, run adjustText immediately. If False, caller
|
|
281
|
+
must call adjust_snp_labels() after setting axis limits.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List of text annotation objects.
|
|
279
285
|
"""
|
|
280
286
|
from ..labels import add_snp_labels as _add_snp_labels
|
|
281
287
|
|
|
282
|
-
_add_snp_labels(
|
|
288
|
+
return _add_snp_labels(
|
|
283
289
|
ax,
|
|
284
290
|
df,
|
|
285
291
|
pos_col=pos_col,
|
|
@@ -288,8 +294,20 @@ class MatplotlibBackend:
|
|
|
288
294
|
label_top_n=label_top_n,
|
|
289
295
|
genes_df=genes_df,
|
|
290
296
|
chrom=chrom,
|
|
297
|
+
adjust=adjust,
|
|
291
298
|
)
|
|
292
299
|
|
|
300
|
+
def adjust_snp_labels(self, ax: Axes, texts: List[Any]) -> None:
|
|
301
|
+
"""Adjust SNP label positions after axis limits are set.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
ax: Matplotlib axes.
|
|
305
|
+
texts: List of text annotation objects from add_snp_labels().
|
|
306
|
+
"""
|
|
307
|
+
from ..labels import adjust_snp_labels as _adjust_snp_labels
|
|
308
|
+
|
|
309
|
+
_adjust_snp_labels(ax, texts)
|
|
310
|
+
|
|
293
311
|
def add_rectangle(
|
|
294
312
|
self,
|
|
295
313
|
ax: Axes,
|
|
@@ -714,6 +732,41 @@ class MatplotlibBackend:
|
|
|
714
732
|
labelspacing=0.4,
|
|
715
733
|
)
|
|
716
734
|
|
|
735
|
+
def add_effect_legend(
|
|
736
|
+
self,
|
|
737
|
+
ax: Axes,
|
|
738
|
+
effect_bins: List[Tuple[float, str, str]],
|
|
739
|
+
) -> None:
|
|
740
|
+
"""Add effect direction legend for colocalization plots.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
ax: Matplotlib axes.
|
|
744
|
+
effect_bins: List of (threshold, label, color) tuples.
|
|
745
|
+
"""
|
|
746
|
+
from matplotlib.lines import Line2D
|
|
747
|
+
|
|
748
|
+
legend_elements = []
|
|
749
|
+
for _threshold, label, color in effect_bins:
|
|
750
|
+
legend_elements.append(
|
|
751
|
+
Line2D(
|
|
752
|
+
[0],
|
|
753
|
+
[0],
|
|
754
|
+
marker="o",
|
|
755
|
+
color="w",
|
|
756
|
+
markerfacecolor=color,
|
|
757
|
+
markeredgecolor="black",
|
|
758
|
+
markersize=8,
|
|
759
|
+
label=label,
|
|
760
|
+
)
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
ax.legend(
|
|
764
|
+
handles=legend_elements,
|
|
765
|
+
loc="upper right",
|
|
766
|
+
fontsize=8,
|
|
767
|
+
framealpha=0.9,
|
|
768
|
+
)
|
|
769
|
+
|
|
717
770
|
def axvline(
|
|
718
771
|
self,
|
|
719
772
|
ax: Axes,
|
|
@@ -806,3 +859,90 @@ class MatplotlibBackend:
|
|
|
806
859
|
left=left, right=right, top=top, bottom=bottom, hspace=hspace
|
|
807
860
|
)
|
|
808
861
|
plt.ion()
|
|
862
|
+
|
|
863
|
+
def add_heatmap(
|
|
864
|
+
self,
|
|
865
|
+
ax: Axes,
|
|
866
|
+
data: Any,
|
|
867
|
+
x_coords: List[float],
|
|
868
|
+
y_coords: List[float],
|
|
869
|
+
cmap_colors: Optional[List[str]] = None,
|
|
870
|
+
vmin: float = 0.0,
|
|
871
|
+
vmax: float = 1.0,
|
|
872
|
+
mask_upper: bool = True,
|
|
873
|
+
) -> Any:
|
|
874
|
+
"""Render heatmap with optional triangular masking.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
ax: Matplotlib axes.
|
|
878
|
+
data: 2D numpy array of values (NaN for missing).
|
|
879
|
+
x_coords: X coordinates for cells.
|
|
880
|
+
y_coords: Y coordinates for cells.
|
|
881
|
+
cmap_colors: Color gradient endpoints [start, end].
|
|
882
|
+
vmin: Minimum value for color scale.
|
|
883
|
+
vmax: Maximum value for color scale.
|
|
884
|
+
mask_upper: If True, mask upper triangle.
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
AxesImage object for colorbar attachment.
|
|
888
|
+
"""
|
|
889
|
+
import numpy as np
|
|
890
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
891
|
+
|
|
892
|
+
# Default white-to-red gradient
|
|
893
|
+
if cmap_colors is None:
|
|
894
|
+
cmap_colors = ["#FFFFFF", "#FF0000"]
|
|
895
|
+
|
|
896
|
+
cmap = LinearSegmentedColormap.from_list("ld_heatmap", cmap_colors, N=256)
|
|
897
|
+
|
|
898
|
+
# Mask upper triangle if requested
|
|
899
|
+
plot_data = data.copy()
|
|
900
|
+
if mask_upper:
|
|
901
|
+
mask = np.triu(np.ones_like(data, dtype=bool), k=1)
|
|
902
|
+
plot_data = np.ma.array(data, mask=mask)
|
|
903
|
+
|
|
904
|
+
# Compute extent from coordinates if non-trivial (genomic coordinates)
|
|
905
|
+
# This allows heatmap to align with regional plot x-axis
|
|
906
|
+
extent = None
|
|
907
|
+
if x_coords and len(x_coords) > 1:
|
|
908
|
+
x_min, x_max = min(x_coords), max(x_coords)
|
|
909
|
+
# Only use extent if coordinates are not simple indices
|
|
910
|
+
if x_max - x_min > len(x_coords):
|
|
911
|
+
# Genomic coordinates - use extent for alignment
|
|
912
|
+
# Add half-cell padding for proper cell centering
|
|
913
|
+
y_min, y_max = min(y_coords), max(y_coords)
|
|
914
|
+
extent = [x_min, x_max, y_min - 0.5, y_max + 0.5]
|
|
915
|
+
|
|
916
|
+
# Use imshow for heatmap rendering
|
|
917
|
+
im = ax.imshow(
|
|
918
|
+
plot_data,
|
|
919
|
+
cmap=cmap,
|
|
920
|
+
vmin=vmin,
|
|
921
|
+
vmax=vmax,
|
|
922
|
+
aspect="auto",
|
|
923
|
+
origin="lower",
|
|
924
|
+
extent=extent,
|
|
925
|
+
)
|
|
926
|
+
return im
|
|
927
|
+
|
|
928
|
+
def add_colorbar(
|
|
929
|
+
self,
|
|
930
|
+
ax: Axes,
|
|
931
|
+
mappable: Any,
|
|
932
|
+
label: str = "R²",
|
|
933
|
+
orientation: str = "vertical",
|
|
934
|
+
) -> Any:
|
|
935
|
+
"""Add colorbar legend for heatmap.
|
|
936
|
+
|
|
937
|
+
Args:
|
|
938
|
+
ax: Matplotlib axes.
|
|
939
|
+
mappable: AxesImage from add_heatmap.
|
|
940
|
+
label: Colorbar label.
|
|
941
|
+
orientation: "vertical" or "horizontal".
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
Colorbar object.
|
|
945
|
+
"""
|
|
946
|
+
cbar = plt.colorbar(mappable, ax=ax, orientation=orientation)
|
|
947
|
+
cbar.set_label(label)
|
|
948
|
+
return cbar
|
|
@@ -841,7 +841,12 @@ class PlotlyBackend:
|
|
|
841
841
|
label_top_n: int,
|
|
842
842
|
genes_df: Optional[pd.DataFrame],
|
|
843
843
|
chrom: int,
|
|
844
|
-
|
|
844
|
+
adjust: bool = True,
|
|
845
|
+
) -> List[Any]:
|
|
846
|
+
"""No-op: Plotly uses hover tooltips instead of text labels."""
|
|
847
|
+
return []
|
|
848
|
+
|
|
849
|
+
def adjust_snp_labels(self, ax: Any, texts: List[Any]) -> None:
|
|
845
850
|
"""No-op: Plotly uses hover tooltips instead of text labels."""
|
|
846
851
|
pass
|
|
847
852
|
|
|
@@ -887,6 +892,19 @@ class PlotlyBackend:
|
|
|
887
892
|
|
|
888
893
|
self._configure_legend(fig, row, "legend", "r²")
|
|
889
894
|
|
|
895
|
+
def add_effect_legend(
|
|
896
|
+
self,
|
|
897
|
+
ax: Tuple[go.Figure, int],
|
|
898
|
+
effect_bins: List[Tuple[float, str, str]],
|
|
899
|
+
) -> None:
|
|
900
|
+
"""Add effect direction legend for colocalization plots."""
|
|
901
|
+
fig, row, col, _ = self._extract_row_col(ax)
|
|
902
|
+
|
|
903
|
+
for _, label, color in effect_bins:
|
|
904
|
+
self._add_legend_item(fig, row, label, color, "circle", 10, "legend")
|
|
905
|
+
|
|
906
|
+
self._configure_legend(fig, row, "legend", "Effect")
|
|
907
|
+
|
|
890
908
|
def add_legend(
|
|
891
909
|
self,
|
|
892
910
|
ax: Tuple[go.Figure, int],
|
|
@@ -1219,3 +1237,85 @@ class PlotlyBackend:
|
|
|
1219
1237
|
)
|
|
1220
1238
|
}
|
|
1221
1239
|
)
|
|
1240
|
+
|
|
1241
|
+
def add_heatmap(
|
|
1242
|
+
self,
|
|
1243
|
+
ax: Tuple[go.Figure, int],
|
|
1244
|
+
data: Any,
|
|
1245
|
+
x_coords: List[float],
|
|
1246
|
+
y_coords: List[float],
|
|
1247
|
+
cmap_colors: Optional[List[str]] = None,
|
|
1248
|
+
vmin: float = 0.0,
|
|
1249
|
+
vmax: float = 1.0,
|
|
1250
|
+
mask_upper: bool = True,
|
|
1251
|
+
) -> Any:
|
|
1252
|
+
"""Render heatmap with optional triangular masking.
|
|
1253
|
+
|
|
1254
|
+
Args:
|
|
1255
|
+
ax: Tuple of (figure, row_number).
|
|
1256
|
+
data: 2D numpy array of values (NaN for missing).
|
|
1257
|
+
x_coords: X coordinates for cells.
|
|
1258
|
+
y_coords: Y coordinates for cells.
|
|
1259
|
+
cmap_colors: Color gradient endpoints [start, end].
|
|
1260
|
+
vmin: Minimum value for color scale.
|
|
1261
|
+
vmax: Maximum value for color scale.
|
|
1262
|
+
mask_upper: If True, mask upper triangle.
|
|
1263
|
+
|
|
1264
|
+
Returns:
|
|
1265
|
+
Heatmap trace.
|
|
1266
|
+
"""
|
|
1267
|
+
import numpy as np
|
|
1268
|
+
|
|
1269
|
+
fig, row, col, _ = self._extract_row_col(ax)
|
|
1270
|
+
|
|
1271
|
+
if cmap_colors is None:
|
|
1272
|
+
cmap_colors = ["#FFFFFF", "#FF0000"]
|
|
1273
|
+
|
|
1274
|
+
# Mask upper triangle by setting to NaN
|
|
1275
|
+
plot_data = data.copy()
|
|
1276
|
+
if mask_upper:
|
|
1277
|
+
for i in range(data.shape[0]):
|
|
1278
|
+
for j in range(i + 1, data.shape[1]):
|
|
1279
|
+
plot_data[i, j] = np.nan
|
|
1280
|
+
|
|
1281
|
+
# Replace NaN with None for Plotly
|
|
1282
|
+
z = np.where(np.isnan(plot_data), None, plot_data)
|
|
1283
|
+
|
|
1284
|
+
colorscale = [[0, cmap_colors[0]], [1, cmap_colors[1]]]
|
|
1285
|
+
|
|
1286
|
+
trace = go.Heatmap(
|
|
1287
|
+
z=z.tolist(),
|
|
1288
|
+
x=list(range(len(x_coords))),
|
|
1289
|
+
y=list(range(len(y_coords))),
|
|
1290
|
+
colorscale=colorscale,
|
|
1291
|
+
zmin=vmin,
|
|
1292
|
+
zmax=vmax,
|
|
1293
|
+
showscale=True,
|
|
1294
|
+
colorbar=dict(title="R²"),
|
|
1295
|
+
)
|
|
1296
|
+
fig.add_trace(trace, row=row, col=col)
|
|
1297
|
+
return trace
|
|
1298
|
+
|
|
1299
|
+
def add_colorbar(
|
|
1300
|
+
self,
|
|
1301
|
+
ax: Tuple[go.Figure, int],
|
|
1302
|
+
mappable: Any,
|
|
1303
|
+
label: str = "R²",
|
|
1304
|
+
orientation: str = "vertical",
|
|
1305
|
+
) -> Any:
|
|
1306
|
+
"""Add colorbar legend for heatmap.
|
|
1307
|
+
|
|
1308
|
+
For Plotly, colorbar is added via showscale=True in add_heatmap.
|
|
1309
|
+
This method is a no-op but kept for API consistency.
|
|
1310
|
+
|
|
1311
|
+
Args:
|
|
1312
|
+
ax: Tuple of (figure, row_number).
|
|
1313
|
+
mappable: Heatmap trace (unused).
|
|
1314
|
+
label: Colorbar label (unused, set in add_heatmap).
|
|
1315
|
+
orientation: Orientation (unused).
|
|
1316
|
+
|
|
1317
|
+
Returns:
|
|
1318
|
+
None.
|
|
1319
|
+
"""
|
|
1320
|
+
# Colorbar is configured in add_heatmap via showscale=True
|
|
1321
|
+
pass
|