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/__init__.py
CHANGED
|
@@ -51,9 +51,19 @@ from .colors import (
|
|
|
51
51
|
get_phewas_category_palette,
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
+
# Configuration classes (internal - use kwargs directly on plot()/plot_stacked())
|
|
55
|
+
# from .config import PlotConfig, StackedPlotConfig # Internal use only
|
|
56
|
+
# Ensembl integration
|
|
57
|
+
from .ensembl import (
|
|
58
|
+
clear_ensembl_cache,
|
|
59
|
+
fetch_exons_from_ensembl,
|
|
60
|
+
fetch_genes_from_ensembl,
|
|
61
|
+
get_ensembl_species_name,
|
|
62
|
+
get_genes_for_region,
|
|
63
|
+
)
|
|
64
|
+
|
|
54
65
|
# eQTL support
|
|
55
66
|
from .eqtl import (
|
|
56
|
-
EQTLValidationError,
|
|
57
67
|
calculate_colocalization_overlap,
|
|
58
68
|
filter_eqtl_by_gene,
|
|
59
69
|
filter_eqtl_by_region,
|
|
@@ -62,9 +72,19 @@ from .eqtl import (
|
|
|
62
72
|
validate_eqtl_df,
|
|
63
73
|
)
|
|
64
74
|
|
|
75
|
+
# Exception hierarchy
|
|
76
|
+
from .exceptions import (
|
|
77
|
+
BackendError,
|
|
78
|
+
DataDownloadError,
|
|
79
|
+
EQTLValidationError,
|
|
80
|
+
FinemappingValidationError,
|
|
81
|
+
LoaderValidationError,
|
|
82
|
+
PyLocusZoomError,
|
|
83
|
+
ValidationError,
|
|
84
|
+
)
|
|
85
|
+
|
|
65
86
|
# Fine-mapping/SuSiE support
|
|
66
87
|
from .finemapping import (
|
|
67
|
-
FinemappingValidationError,
|
|
68
88
|
filter_by_credible_set,
|
|
69
89
|
filter_finemapping_by_region,
|
|
70
90
|
get_credible_sets,
|
|
@@ -125,11 +145,8 @@ from .recombination import (
|
|
|
125
145
|
load_recombination_map,
|
|
126
146
|
)
|
|
127
147
|
|
|
128
|
-
# Schema validation
|
|
129
|
-
from .schemas import LoaderValidationError
|
|
130
|
-
|
|
131
148
|
# Validation utilities
|
|
132
|
-
from .utils import
|
|
149
|
+
from .utils import to_pandas
|
|
133
150
|
|
|
134
151
|
__all__ = [
|
|
135
152
|
# Core
|
|
@@ -178,8 +195,12 @@ __all__ = [
|
|
|
178
195
|
# Logging
|
|
179
196
|
"enable_logging",
|
|
180
197
|
"disable_logging",
|
|
181
|
-
#
|
|
198
|
+
# Exceptions
|
|
199
|
+
"PyLocusZoomError",
|
|
182
200
|
"ValidationError",
|
|
201
|
+
"BackendError",
|
|
202
|
+
"DataDownloadError",
|
|
203
|
+
# Utils
|
|
183
204
|
"to_pandas",
|
|
184
205
|
# PheWAS
|
|
185
206
|
"validate_phewas_df",
|
|
@@ -208,4 +229,10 @@ __all__ = [
|
|
|
208
229
|
"load_ensembl_genes",
|
|
209
230
|
# Schema validation
|
|
210
231
|
"LoaderValidationError",
|
|
232
|
+
# Ensembl integration
|
|
233
|
+
"get_genes_for_region",
|
|
234
|
+
"fetch_genes_from_ensembl",
|
|
235
|
+
"fetch_exons_from_ensembl",
|
|
236
|
+
"get_ensembl_species_name",
|
|
237
|
+
"clear_ensembl_cache",
|
|
211
238
|
]
|
pylocuszoom/backends/__init__.py
CHANGED
|
@@ -1,18 +1,77 @@
|
|
|
1
1
|
"""Pluggable plotting backends for pyLocusZoom.
|
|
2
2
|
|
|
3
3
|
Supports matplotlib (default), plotly, and bokeh backends.
|
|
4
|
+
|
|
5
|
+
Backend Registration:
|
|
6
|
+
Backends are registered using the @register_backend decorator.
|
|
7
|
+
This enables extensibility without modifying core code.
|
|
8
|
+
|
|
9
|
+
@register_backend("custom")
|
|
10
|
+
class CustomBackend:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
Fallback Behavior:
|
|
14
|
+
When an optional backend (plotly, bokeh) is requested but not installed,
|
|
15
|
+
get_backend() falls back to matplotlib with a warning. This ensures
|
|
16
|
+
code works even without optional dependencies.
|
|
4
17
|
"""
|
|
5
18
|
|
|
19
|
+
import warnings
|
|
6
20
|
from typing import Literal
|
|
7
21
|
|
|
8
22
|
from .base import PlotBackend
|
|
9
|
-
from .matplotlib_backend import MatplotlibBackend
|
|
10
23
|
|
|
11
24
|
BackendType = Literal["matplotlib", "plotly", "bokeh"]
|
|
12
25
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
26
|
+
# LaTeX to Unicode conversion table for interactive backends
|
|
27
|
+
_LATEX_TO_UNICODE = [
|
|
28
|
+
(r"$-\log_{10}$ P", "-log10(P)"),
|
|
29
|
+
(r"$-\log_{10}$", "-log10"),
|
|
30
|
+
(r"\log_{10}", "log10"),
|
|
31
|
+
(r"$r^2$", "r"),
|
|
32
|
+
(r"$R^2$", "R"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def convert_latex_to_unicode(label: str) -> str:
|
|
37
|
+
"""Convert LaTeX-style labels to Unicode for display in interactive backends.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
label: Label text possibly containing LaTeX notation.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Label with LaTeX converted to Unicode characters.
|
|
44
|
+
"""
|
|
45
|
+
for latex, unicode_str in _LATEX_TO_UNICODE:
|
|
46
|
+
if latex in label:
|
|
47
|
+
label = label.replace(latex, unicode_str)
|
|
48
|
+
return label.replace("$", "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Backend registry - populated by @register_backend decorator
|
|
52
|
+
_BACKENDS: dict[str, type[PlotBackend]] = {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def register_backend(name: str):
|
|
56
|
+
"""Decorator to register a backend class.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name: Backend name (e.g., "matplotlib", "plotly", "bokeh").
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Decorator function that registers the class.
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
@register_backend("custom")
|
|
66
|
+
class CustomBackend:
|
|
67
|
+
...
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def decorator(cls: type[PlotBackend]) -> type[PlotBackend]:
|
|
71
|
+
_BACKENDS[name] = cls
|
|
72
|
+
return cls
|
|
73
|
+
|
|
74
|
+
return decorator
|
|
16
75
|
|
|
17
76
|
|
|
18
77
|
def get_backend(name: BackendType) -> PlotBackend:
|
|
@@ -25,24 +84,64 @@ def get_backend(name: BackendType) -> PlotBackend:
|
|
|
25
84
|
Instantiated backend.
|
|
26
85
|
|
|
27
86
|
Raises:
|
|
28
|
-
ValueError: If backend name is
|
|
29
|
-
ImportError: If backend dependencies are not installed.
|
|
30
|
-
"""
|
|
31
|
-
if name == "plotly":
|
|
32
|
-
from .plotly_backend import PlotlyBackend
|
|
87
|
+
ValueError: If backend name is completely unknown.
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
89
|
+
Note:
|
|
90
|
+
When optional backends (plotly, bokeh) are unavailable,
|
|
91
|
+
falls back to matplotlib with a UserWarning.
|
|
92
|
+
"""
|
|
93
|
+
# Ensure matplotlib is always registered (it's always available)
|
|
94
|
+
if "matplotlib" not in _BACKENDS:
|
|
95
|
+
from .matplotlib_backend import MatplotlibBackend # noqa: F401
|
|
37
96
|
|
|
38
|
-
|
|
97
|
+
# Try lazy import for optional backends
|
|
98
|
+
if name not in _BACKENDS:
|
|
99
|
+
if name == "plotly":
|
|
100
|
+
try:
|
|
101
|
+
from .plotly_backend import PlotlyBackend # noqa: F401
|
|
102
|
+
except ImportError:
|
|
103
|
+
warnings.warn(
|
|
104
|
+
"Plotly not installed, falling back to matplotlib. "
|
|
105
|
+
"Install plotly with: pip install plotly",
|
|
106
|
+
UserWarning,
|
|
107
|
+
stacklevel=2,
|
|
108
|
+
)
|
|
109
|
+
name = "matplotlib"
|
|
110
|
+
elif name == "bokeh":
|
|
111
|
+
try:
|
|
112
|
+
from .bokeh_backend import BokehBackend # noqa: F401
|
|
113
|
+
except ImportError:
|
|
114
|
+
warnings.warn(
|
|
115
|
+
"Bokeh not installed, falling back to matplotlib. "
|
|
116
|
+
"Install bokeh with: pip install bokeh",
|
|
117
|
+
UserWarning,
|
|
118
|
+
stacklevel=2,
|
|
119
|
+
)
|
|
120
|
+
name = "matplotlib"
|
|
39
121
|
|
|
40
122
|
if name not in _BACKENDS:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
123
|
+
available = list(_BACKENDS.keys())
|
|
124
|
+
raise ValueError(f"Unknown backend: {name}. Available: {available}")
|
|
44
125
|
|
|
45
126
|
return _BACKENDS[name]()
|
|
46
127
|
|
|
47
128
|
|
|
48
|
-
|
|
129
|
+
# Lazy import for backward compatibility - MatplotlibBackend available at module level
|
|
130
|
+
# The actual registration happens when matplotlib_backend is imported
|
|
131
|
+
def __getattr__(name: str):
|
|
132
|
+
"""Lazy attribute access for backward compatibility."""
|
|
133
|
+
if name == "MatplotlibBackend":
|
|
134
|
+
from .matplotlib_backend import MatplotlibBackend
|
|
135
|
+
|
|
136
|
+
return MatplotlibBackend
|
|
137
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
__all__ = [
|
|
141
|
+
"PlotBackend",
|
|
142
|
+
"BackendType",
|
|
143
|
+
"get_backend",
|
|
144
|
+
"register_backend",
|
|
145
|
+
"MatplotlibBackend",
|
|
146
|
+
"convert_latex_to_unicode",
|
|
147
|
+
]
|