pylocuszoom 0.3.0__py3-none-any.whl → 0.5.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 +52 -1
- pylocuszoom/backends/base.py +45 -0
- pylocuszoom/backends/bokeh_backend.py +138 -48
- pylocuszoom/backends/matplotlib_backend.py +104 -0
- pylocuszoom/backends/plotly_backend.py +212 -64
- pylocuszoom/colors.py +3 -1
- pylocuszoom/gene_track.py +1 -0
- pylocuszoom/loaders.py +862 -0
- pylocuszoom/plotter.py +84 -113
- pylocuszoom/py.typed +0 -0
- pylocuszoom/recombination.py +4 -4
- pylocuszoom/schemas.py +395 -0
- {pylocuszoom-0.3.0.dist-info → pylocuszoom-0.5.0.dist-info}/METADATA +104 -24
- pylocuszoom-0.5.0.dist-info/RECORD +24 -0
- pylocuszoom-0.3.0.dist-info/RECORD +0 -21
- {pylocuszoom-0.3.0.dist-info → pylocuszoom-0.5.0.dist-info}/WHEEL +0 -0
- {pylocuszoom-0.3.0.dist-info → pylocuszoom-0.5.0.dist-info}/licenses/LICENSE.md +0 -0
pylocuszoom/plotter.py
CHANGED
|
@@ -139,11 +139,8 @@ class LocusZoomPlotter:
|
|
|
139
139
|
@staticmethod
|
|
140
140
|
def _default_build(species: str) -> Optional[str]:
|
|
141
141
|
"""Get default genome build for species."""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if species == "feline":
|
|
145
|
-
return "felCat9"
|
|
146
|
-
return None
|
|
142
|
+
builds = {"canine": "canfam3.1", "feline": "felCat9"}
|
|
143
|
+
return builds.get(species)
|
|
147
144
|
|
|
148
145
|
def _ensure_recomb_maps(self) -> Optional[Path]:
|
|
149
146
|
"""Ensure recombination maps are downloaded.
|
|
@@ -157,8 +154,8 @@ class LocusZoomPlotter:
|
|
|
157
154
|
default_dir = get_default_data_dir()
|
|
158
155
|
if (
|
|
159
156
|
default_dir.exists()
|
|
160
|
-
and len(list(default_dir.glob("chr*_recomb.tsv"))) >=
|
|
161
|
-
):
|
|
157
|
+
and len(list(default_dir.glob("chr*_recomb.tsv"))) >= 39
|
|
158
|
+
): # 38 autosomes + X
|
|
162
159
|
return default_dir
|
|
163
160
|
# Download
|
|
164
161
|
try:
|
|
@@ -215,7 +212,7 @@ class LocusZoomPlotter:
|
|
|
215
212
|
p_col: str = "p_wald",
|
|
216
213
|
rs_col: str = "rs",
|
|
217
214
|
figsize: Tuple[int, int] = (12, 8),
|
|
218
|
-
) ->
|
|
215
|
+
) -> Any:
|
|
219
216
|
"""Create a regional association plot.
|
|
220
217
|
|
|
221
218
|
Args:
|
|
@@ -589,53 +586,6 @@ class LocusZoomPlotter:
|
|
|
589
586
|
yaxis_name=secondary_y,
|
|
590
587
|
)
|
|
591
588
|
|
|
592
|
-
def _add_eqtl_legend(self, ax: Axes) -> None:
|
|
593
|
-
"""Add eQTL effect size legend to plot."""
|
|
594
|
-
legend_elements = []
|
|
595
|
-
|
|
596
|
-
# Positive effects (upward triangles)
|
|
597
|
-
for _, _, label, color in EQTL_POSITIVE_BINS:
|
|
598
|
-
legend_elements.append(
|
|
599
|
-
Line2D(
|
|
600
|
-
[0],
|
|
601
|
-
[0],
|
|
602
|
-
marker="^",
|
|
603
|
-
color="w",
|
|
604
|
-
markerfacecolor=color,
|
|
605
|
-
markeredgecolor="black",
|
|
606
|
-
markersize=7,
|
|
607
|
-
label=label,
|
|
608
|
-
)
|
|
609
|
-
)
|
|
610
|
-
|
|
611
|
-
# Negative effects (downward triangles)
|
|
612
|
-
for _, _, label, color in EQTL_NEGATIVE_BINS:
|
|
613
|
-
legend_elements.append(
|
|
614
|
-
Line2D(
|
|
615
|
-
[0],
|
|
616
|
-
[0],
|
|
617
|
-
marker="v",
|
|
618
|
-
color="w",
|
|
619
|
-
markerfacecolor=color,
|
|
620
|
-
markeredgecolor="black",
|
|
621
|
-
markersize=7,
|
|
622
|
-
label=label,
|
|
623
|
-
)
|
|
624
|
-
)
|
|
625
|
-
|
|
626
|
-
ax.legend(
|
|
627
|
-
handles=legend_elements,
|
|
628
|
-
loc="upper right",
|
|
629
|
-
fontsize=8,
|
|
630
|
-
frameon=True,
|
|
631
|
-
framealpha=0.9,
|
|
632
|
-
title="eQTL effect",
|
|
633
|
-
title_fontsize=9,
|
|
634
|
-
handlelength=1.2,
|
|
635
|
-
handleheight=1.0,
|
|
636
|
-
labelspacing=0.3,
|
|
637
|
-
)
|
|
638
|
-
|
|
639
589
|
def _plot_finemapping(
|
|
640
590
|
self,
|
|
641
591
|
ax: Axes,
|
|
@@ -657,6 +607,23 @@ class LocusZoomPlotter:
|
|
|
657
607
|
show_credible_sets: Whether to color points by credible set.
|
|
658
608
|
pip_threshold: Minimum PIP to display as scatter point.
|
|
659
609
|
"""
|
|
610
|
+
|
|
611
|
+
def _build_finemapping_hover_data(
|
|
612
|
+
subset_df: pd.DataFrame,
|
|
613
|
+
) -> Optional[pd.DataFrame]:
|
|
614
|
+
"""Build hover data for interactive backends."""
|
|
615
|
+
hover_cols = {}
|
|
616
|
+
# Position
|
|
617
|
+
if pos_col in subset_df.columns:
|
|
618
|
+
hover_cols["Position"] = subset_df[pos_col].values
|
|
619
|
+
# PIP
|
|
620
|
+
if pip_col in subset_df.columns:
|
|
621
|
+
hover_cols["PIP"] = subset_df[pip_col].values
|
|
622
|
+
# Credible set
|
|
623
|
+
if cs_col and cs_col in subset_df.columns:
|
|
624
|
+
hover_cols["Credible Set"] = subset_df[cs_col].values
|
|
625
|
+
return pd.DataFrame(hover_cols) if hover_cols else None
|
|
626
|
+
|
|
660
627
|
# Sort by position for line plotting
|
|
661
628
|
df = df.sort_values(pos_col)
|
|
662
629
|
|
|
@@ -690,7 +657,7 @@ class LocusZoomPlotter:
|
|
|
690
657
|
edgecolor="black",
|
|
691
658
|
linewidth=0.5,
|
|
692
659
|
zorder=3,
|
|
693
|
-
|
|
660
|
+
hover_data=_build_finemapping_hover_data(cs_data),
|
|
694
661
|
)
|
|
695
662
|
# Plot variants not in any credible set
|
|
696
663
|
non_cs_data = df[(df[cs_col].isna()) | (df[cs_col] == 0)]
|
|
@@ -707,6 +674,7 @@ class LocusZoomPlotter:
|
|
|
707
674
|
edgecolor="black",
|
|
708
675
|
linewidth=0.3,
|
|
709
676
|
zorder=2,
|
|
677
|
+
hover_data=_build_finemapping_hover_data(non_cs_data),
|
|
710
678
|
)
|
|
711
679
|
else:
|
|
712
680
|
# No credible sets - show all points above threshold
|
|
@@ -723,51 +691,9 @@ class LocusZoomPlotter:
|
|
|
723
691
|
edgecolor="black",
|
|
724
692
|
linewidth=0.5,
|
|
725
693
|
zorder=3,
|
|
694
|
+
hover_data=_build_finemapping_hover_data(high_pip),
|
|
726
695
|
)
|
|
727
696
|
|
|
728
|
-
def _add_finemapping_legend(
|
|
729
|
-
self,
|
|
730
|
-
ax: Axes,
|
|
731
|
-
credible_sets: List[int],
|
|
732
|
-
) -> None:
|
|
733
|
-
"""Add fine-mapping legend showing credible sets.
|
|
734
|
-
|
|
735
|
-
Args:
|
|
736
|
-
ax: Matplotlib axes object.
|
|
737
|
-
credible_sets: List of credible set IDs to include.
|
|
738
|
-
"""
|
|
739
|
-
if not credible_sets:
|
|
740
|
-
return
|
|
741
|
-
|
|
742
|
-
legend_elements = []
|
|
743
|
-
for cs_id in credible_sets:
|
|
744
|
-
color = get_credible_set_color(cs_id)
|
|
745
|
-
legend_elements.append(
|
|
746
|
-
Line2D(
|
|
747
|
-
[0],
|
|
748
|
-
[0],
|
|
749
|
-
marker="o",
|
|
750
|
-
color="w",
|
|
751
|
-
markerfacecolor=color,
|
|
752
|
-
markeredgecolor="black",
|
|
753
|
-
markersize=7,
|
|
754
|
-
label=f"CS{cs_id}",
|
|
755
|
-
)
|
|
756
|
-
)
|
|
757
|
-
|
|
758
|
-
ax.legend(
|
|
759
|
-
handles=legend_elements,
|
|
760
|
-
loc="upper right",
|
|
761
|
-
fontsize=8,
|
|
762
|
-
frameon=True,
|
|
763
|
-
framealpha=0.9,
|
|
764
|
-
title="Credible sets",
|
|
765
|
-
title_fontsize=9,
|
|
766
|
-
handlelength=1.2,
|
|
767
|
-
handleheight=1.0,
|
|
768
|
-
labelspacing=0.3,
|
|
769
|
-
)
|
|
770
|
-
|
|
771
697
|
def plot_stacked(
|
|
772
698
|
self,
|
|
773
699
|
gwas_dfs: List[pd.DataFrame],
|
|
@@ -968,7 +894,9 @@ class LocusZoomPlotter:
|
|
|
968
894
|
panel_ld_col = "R2"
|
|
969
895
|
|
|
970
896
|
# Plot association
|
|
971
|
-
self._plot_association(
|
|
897
|
+
self._plot_association(
|
|
898
|
+
ax, df, pos_col, panel_ld_col, lead_pos, rs_col, p_col
|
|
899
|
+
)
|
|
972
900
|
|
|
973
901
|
# Add significance line
|
|
974
902
|
self._backend.axhline(
|
|
@@ -1080,10 +1008,12 @@ class LocusZoomPlotter:
|
|
|
1080
1008
|
pip_threshold=0.01,
|
|
1081
1009
|
)
|
|
1082
1010
|
|
|
1083
|
-
# Add legend for credible sets
|
|
1011
|
+
# Add legend for credible sets (all backends)
|
|
1084
1012
|
credible_sets = get_credible_sets(fm_data, finemapping_cs_col)
|
|
1085
1013
|
if credible_sets:
|
|
1086
|
-
self.
|
|
1014
|
+
self._backend.add_finemapping_legend(
|
|
1015
|
+
ax, credible_sets, get_credible_set_color
|
|
1016
|
+
)
|
|
1087
1017
|
|
|
1088
1018
|
self._backend.set_ylabel(ax, "PIP")
|
|
1089
1019
|
self._backend.set_ylim(ax, -0.05, 1.05)
|
|
@@ -1111,30 +1041,70 @@ class LocusZoomPlotter:
|
|
|
1111
1041
|
eqtl_data["p_value"].clip(lower=1e-300)
|
|
1112
1042
|
)
|
|
1113
1043
|
|
|
1044
|
+
def _build_eqtl_hover_data(
|
|
1045
|
+
subset_df: pd.DataFrame,
|
|
1046
|
+
) -> Optional[pd.DataFrame]:
|
|
1047
|
+
"""Build hover data for eQTL interactive backends."""
|
|
1048
|
+
hover_cols = {}
|
|
1049
|
+
# Position
|
|
1050
|
+
if "pos" in subset_df.columns:
|
|
1051
|
+
hover_cols["Position"] = subset_df["pos"].values
|
|
1052
|
+
# P-value
|
|
1053
|
+
if "p_value" in subset_df.columns:
|
|
1054
|
+
hover_cols["P-value"] = subset_df["p_value"].values
|
|
1055
|
+
# Effect size
|
|
1056
|
+
if "effect_size" in subset_df.columns:
|
|
1057
|
+
hover_cols["Effect"] = subset_df["effect_size"].values
|
|
1058
|
+
# Gene
|
|
1059
|
+
if "gene" in subset_df.columns:
|
|
1060
|
+
hover_cols["Gene"] = subset_df["gene"].values
|
|
1061
|
+
return pd.DataFrame(hover_cols) if hover_cols else None
|
|
1062
|
+
|
|
1114
1063
|
# Check if effect_size column exists for directional coloring
|
|
1115
1064
|
has_effect = "effect_size" in eqtl_data.columns
|
|
1116
1065
|
|
|
1117
1066
|
if has_effect:
|
|
1118
|
-
# Plot triangles by effect direction
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1067
|
+
# Plot triangles by effect direction (batch by sign for efficiency)
|
|
1068
|
+
pos_effects = eqtl_data[eqtl_data["effect_size"] >= 0]
|
|
1069
|
+
neg_effects = eqtl_data[eqtl_data["effect_size"] < 0]
|
|
1070
|
+
|
|
1071
|
+
# Plot positive effects (up triangles)
|
|
1072
|
+
for _, row in pos_effects.iterrows():
|
|
1073
|
+
row_df = pd.DataFrame([row])
|
|
1074
|
+
self._backend.scatter(
|
|
1075
|
+
ax,
|
|
1076
|
+
pd.Series([row["pos"]]),
|
|
1077
|
+
pd.Series([row["neglog10p"]]),
|
|
1078
|
+
colors=get_eqtl_color(row["effect_size"]),
|
|
1079
|
+
sizes=50,
|
|
1080
|
+
marker="^",
|
|
1081
|
+
edgecolor="black",
|
|
1082
|
+
linewidth=0.5,
|
|
1083
|
+
zorder=2,
|
|
1084
|
+
hover_data=_build_eqtl_hover_data(row_df),
|
|
1085
|
+
)
|
|
1086
|
+
# Plot negative effects (down triangles)
|
|
1087
|
+
for _, row in neg_effects.iterrows():
|
|
1088
|
+
row_df = pd.DataFrame([row])
|
|
1123
1089
|
self._backend.scatter(
|
|
1124
1090
|
ax,
|
|
1125
1091
|
pd.Series([row["pos"]]),
|
|
1126
1092
|
pd.Series([row["neglog10p"]]),
|
|
1127
|
-
colors=
|
|
1093
|
+
colors=get_eqtl_color(row["effect_size"]),
|
|
1128
1094
|
sizes=50,
|
|
1129
|
-
marker=
|
|
1095
|
+
marker="v",
|
|
1130
1096
|
edgecolor="black",
|
|
1131
1097
|
linewidth=0.5,
|
|
1132
1098
|
zorder=2,
|
|
1099
|
+
hover_data=_build_eqtl_hover_data(row_df),
|
|
1133
1100
|
)
|
|
1134
|
-
# Add eQTL effect legend
|
|
1135
|
-
self.
|
|
1101
|
+
# Add eQTL effect legend (all backends)
|
|
1102
|
+
self._backend.add_eqtl_legend(
|
|
1103
|
+
ax, EQTL_POSITIVE_BINS, EQTL_NEGATIVE_BINS
|
|
1104
|
+
)
|
|
1136
1105
|
else:
|
|
1137
1106
|
# No effect sizes - plot as diamonds
|
|
1107
|
+
label = f"eQTL ({eqtl_gene})" if eqtl_gene else "eQTL"
|
|
1138
1108
|
self._backend.scatter(
|
|
1139
1109
|
ax,
|
|
1140
1110
|
eqtl_data["pos"],
|
|
@@ -1145,9 +1115,10 @@ class LocusZoomPlotter:
|
|
|
1145
1115
|
edgecolor="black",
|
|
1146
1116
|
linewidth=0.5,
|
|
1147
1117
|
zorder=2,
|
|
1148
|
-
label=
|
|
1118
|
+
label=label,
|
|
1119
|
+
hover_data=_build_eqtl_hover_data(eqtl_data),
|
|
1149
1120
|
)
|
|
1150
|
-
|
|
1121
|
+
self._backend.add_simple_legend(ax, label, loc="upper right")
|
|
1151
1122
|
|
|
1152
1123
|
self._backend.set_ylabel(ax, r"$-\log_{10}$ P (eQTL)")
|
|
1153
1124
|
self._backend.axhline(
|
pylocuszoom/py.typed
ADDED
|
File without changes
|
pylocuszoom/recombination.py
CHANGED
|
@@ -42,7 +42,7 @@ def _normalize_build(build: Optional[str]) -> Optional[str]:
|
|
|
42
42
|
if build is None:
|
|
43
43
|
return None
|
|
44
44
|
build_lower = build.lower().replace(".", "").replace("_", "")
|
|
45
|
-
if
|
|
45
|
+
if any(x in build_lower for x in ("canfam4", "uucfamgsd")):
|
|
46
46
|
return "canfam4"
|
|
47
47
|
if "canfam3" in build_lower:
|
|
48
48
|
return "canfam3"
|
|
@@ -158,9 +158,9 @@ def get_default_data_dir() -> Path:
|
|
|
158
158
|
"""Get default directory for recombination map data.
|
|
159
159
|
|
|
160
160
|
Returns platform-appropriate cache directory:
|
|
161
|
-
- macOS:
|
|
162
|
-
- Linux: ~/.cache/snp-scope-plot
|
|
161
|
+
- macOS/Linux: ~/.cache/snp-scope-plot (or $XDG_CACHE_HOME if set)
|
|
163
162
|
- Windows: %LOCALAPPDATA%/snp-scope-plot
|
|
163
|
+
- Databricks: /dbfs/FileStore/reference_data/recombination_maps
|
|
164
164
|
"""
|
|
165
165
|
if os.name == "nt": # Windows
|
|
166
166
|
base = Path(os.environ.get("LOCALAPPDATA", Path.home()))
|
|
@@ -207,7 +207,7 @@ def download_canine_recombination_maps(
|
|
|
207
207
|
# Check if already downloaded
|
|
208
208
|
if output_path.exists() and not force:
|
|
209
209
|
existing_files = list(output_path.glob("chr*_recomb.tsv"))
|
|
210
|
-
if len(existing_files) >=
|
|
210
|
+
if len(existing_files) >= 39: # 38 autosomes + X
|
|
211
211
|
return output_path
|
|
212
212
|
|
|
213
213
|
# Create output directory
|