pylocuszoom 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.2.0/CHANGELOG.md +59 -0
- pylocuszoom-0.2.0/CONTRIBUTING.md +114 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/PKG-INFO +20 -25
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/README.md +19 -24
- pylocuszoom-0.2.0/examples/eqtl_overlay.png +0 -0
- pylocuszoom-0.2.0/examples/finemapping_plot.png +0 -0
- pylocuszoom-0.2.0/examples/generate_readme_plots.py +235 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/examples/getting_started.ipynb +8 -119
- pylocuszoom-0.2.0/examples/regional_plot.png +0 -0
- pylocuszoom-0.2.0/examples/stacked_plot.png +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/pyproject.toml +1 -1
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/__init__.py +39 -20
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/__init__.py +1 -5
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/base.py +1 -1
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/bokeh_backend.py +4 -7
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/matplotlib_backend.py +6 -1
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/plotly_backend.py +11 -12
- pylocuszoom-0.2.0/src/pylocuszoom/colors.py +239 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/eqtl.py +3 -2
- pylocuszoom-0.2.0/src/pylocuszoom/finemapping.py +224 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/gene_track.py +44 -31
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/labels.py +32 -33
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/ld.py +8 -7
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/plotter.py +381 -66
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/recombination.py +14 -14
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/utils.py +3 -1
- pylocuszoom-0.2.0/tests/test_finemapping.py +154 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_labels.py +4 -4
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_ld.py +26 -6
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_plotter.py +135 -7
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/uv.lock +1 -1
- pylocuszoom-0.1.0/src/pylocuszoom/colors.py +0 -107
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/.github/workflows/ci.yml +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/.github/workflows/publish.yml +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/.gitignore +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/LICENSE.md +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/docs/ARCHITECTURE.md +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/logo.svg +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/logging.py +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/reference_data/__init__.py +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/conftest.py +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_colors.py +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_gene_track.py +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_logging.py +0 -0
- {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_recombination.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-01-26
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Fine-mapping/SuSiE visualization with credible set coloring
|
|
14
|
+
- Example plots in `examples/` directory
|
|
15
|
+
- Plot generation script for documentation
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Ruff linting and formatting errors
|
|
19
|
+
- Bokeh security vulnerability (bumped to >= 3.8.2)
|
|
20
|
+
- `plot()` KeyError when `rs_col` column missing with `ld_reference_file` provided
|
|
21
|
+
- `plot_stacked()` now validates eQTL DataFrame columns before use
|
|
22
|
+
- `plot_stacked()` now validates list lengths for `lead_positions`, `panel_labels`, and `ld_reference_files`
|
|
23
|
+
- `calculate_ld()` docstring now documents `ValidationError` for missing PLINK files
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Minimum Python version bumped to 3.10 (required by bokeh 3.8.2)
|
|
27
|
+
- Renamed species terminology: "dog" → "canine", "cat" → "feline"
|
|
28
|
+
- Clarified interactive backend status in README (coming soon)
|
|
29
|
+
|
|
30
|
+
## [0.1.0] - 2026-01-26
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- Initial release of pyLocusZoom
|
|
34
|
+
- Regional association plots with LD coloring
|
|
35
|
+
- Gene and exon track visualization
|
|
36
|
+
- Recombination rate overlay (canine only)
|
|
37
|
+
- Automatic SNP labeling with adjustText
|
|
38
|
+
- Species support: Canine (CanFam3.1/CanFam4), Feline (FelCat9), custom
|
|
39
|
+
- CanFam4 coordinate liftover via pyliftover
|
|
40
|
+
- Stacked plots for multi-GWAS comparison
|
|
41
|
+
- eQTL overlay panel support
|
|
42
|
+
- PySpark DataFrame support
|
|
43
|
+
- Backend infrastructure for matplotlib, plotly, bokeh (matplotlib only active)
|
|
44
|
+
- Logging via loguru
|
|
45
|
+
- Comprehensive test suite
|
|
46
|
+
|
|
47
|
+
### Dependencies
|
|
48
|
+
- matplotlib >= 3.5.0
|
|
49
|
+
- pandas >= 1.4.0
|
|
50
|
+
- numpy >= 1.21.0
|
|
51
|
+
- loguru >= 0.7.0
|
|
52
|
+
- pyliftover >= 0.4
|
|
53
|
+
- plotly >= 5.0.0
|
|
54
|
+
- bokeh >= 3.8.2
|
|
55
|
+
- kaleido >= 0.2.0
|
|
56
|
+
|
|
57
|
+
[Unreleased]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.2.0...HEAD
|
|
58
|
+
[0.2.0]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.1.0...v0.2.0
|
|
59
|
+
[0.1.0]: https://github.com/michael-denyer/pyLocusZoom/releases/tag/v0.1.0
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Contributing to pyLocusZoom
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing to pyLocusZoom!
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
1. Clone the repository:
|
|
8
|
+
```bash
|
|
9
|
+
git clone https://github.com/michael-denyer/pyLocusZoom.git
|
|
10
|
+
cd pyLocusZoom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Install dependencies with uv:
|
|
14
|
+
```bash
|
|
15
|
+
uv sync --all-extras
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
3. Run tests:
|
|
19
|
+
```bash
|
|
20
|
+
uv run python -m pytest tests/ -v
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
4. Run linting:
|
|
24
|
+
```bash
|
|
25
|
+
uv run ruff check src/
|
|
26
|
+
uv run ruff format --check src/ tests/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Code Style
|
|
30
|
+
|
|
31
|
+
- Follow [PEP 8](https://peps.python.org/pep-0008/) guidelines
|
|
32
|
+
- Use [ruff](https://github.com/astral-sh/ruff) for linting and formatting
|
|
33
|
+
- Maximum line length: 88 characters
|
|
34
|
+
- Use Google-style docstrings
|
|
35
|
+
|
|
36
|
+
### Docstring Example
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
def function_name(param1: str, param2: int = 10) -> bool:
|
|
40
|
+
"""Short one-line description.
|
|
41
|
+
|
|
42
|
+
Longer description if needed.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
param1: Description of first parameter.
|
|
46
|
+
param2: Description of second parameter.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Description of return value.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: When param1 is empty.
|
|
53
|
+
"""
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Pull Request Process
|
|
57
|
+
|
|
58
|
+
1. Fork the repository
|
|
59
|
+
2. Create a feature branch: `git checkout -b feature/your-feature`
|
|
60
|
+
3. Make your changes
|
|
61
|
+
4. Run tests and linting:
|
|
62
|
+
```bash
|
|
63
|
+
uv run python -m pytest tests/ -v
|
|
64
|
+
uv run ruff check src/
|
|
65
|
+
uv run ruff format src/ tests/
|
|
66
|
+
```
|
|
67
|
+
5. Commit with a descriptive message
|
|
68
|
+
6. Push and create a pull request
|
|
69
|
+
|
|
70
|
+
## Testing
|
|
71
|
+
|
|
72
|
+
- Write tests for new functionality
|
|
73
|
+
- Use pytest fixtures from `tests/conftest.py`
|
|
74
|
+
- Mock external dependencies (PLINK, network calls)
|
|
75
|
+
- Aim for test coverage of new code
|
|
76
|
+
|
|
77
|
+
### Running Tests
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# All tests
|
|
81
|
+
uv run python -m pytest tests/ -v
|
|
82
|
+
|
|
83
|
+
# Specific test file
|
|
84
|
+
uv run python -m pytest tests/test_plotter.py -v
|
|
85
|
+
|
|
86
|
+
# With coverage
|
|
87
|
+
uv run python -m pytest tests/ --cov=pylocuszoom --cov-report=html
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Architecture
|
|
91
|
+
|
|
92
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for project structure.
|
|
93
|
+
|
|
94
|
+
### Key Modules
|
|
95
|
+
|
|
96
|
+
| Module | Purpose |
|
|
97
|
+
|--------|---------|
|
|
98
|
+
| `plotter.py` | Main LocusZoomPlotter class |
|
|
99
|
+
| `backends/` | Rendering backends (matplotlib, plotly, bokeh) |
|
|
100
|
+
| `ld.py` | PLINK LD calculation |
|
|
101
|
+
| `gene_track.py` | Gene/exon visualization |
|
|
102
|
+
| `recombination.py` | Recombination map handling |
|
|
103
|
+
| `eqtl.py` | eQTL data support |
|
|
104
|
+
|
|
105
|
+
## Reporting Issues
|
|
106
|
+
|
|
107
|
+
- Use GitHub Issues
|
|
108
|
+
- Include Python version, OS, and package versions
|
|
109
|
+
- Provide a minimal reproducible example
|
|
110
|
+
- Include full error traceback
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
By contributing, you agree that your contributions will be licensed under the GPL-3.0-or-later license.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pylocuszoom
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Regional association plots for GWAS results with LD coloring, gene tracks, and recombination rate overlays
|
|
5
5
|
Project-URL: Homepage, https://github.com/michael-denyer/pylocuszoom
|
|
6
6
|
Project-URL: Documentation, https://github.com/michael-denyer/pylocuszoom#readme
|
|
@@ -71,6 +71,8 @@ Inspired by [LocusZoom](http://locuszoom.org/) and [locuszoomr](https://github.c
|
|
|
71
71
|
- **eQTL overlay**: Expression QTL data as separate panel
|
|
72
72
|
- **PySpark support**: Handles large-scale genomics DataFrames
|
|
73
73
|
|
|
74
|
+

|
|
75
|
+
|
|
74
76
|
## Installation
|
|
75
77
|
|
|
76
78
|
```bash
|
|
@@ -88,8 +90,8 @@ pip install pylocuszoom
|
|
|
88
90
|
```python
|
|
89
91
|
from pylocuszoom import LocusZoomPlotter
|
|
90
92
|
|
|
91
|
-
# Initialize plotter (loads reference data for
|
|
92
|
-
plotter = LocusZoomPlotter(species="
|
|
93
|
+
# Initialize plotter (loads reference data for canine)
|
|
94
|
+
plotter = LocusZoomPlotter(species="canine")
|
|
93
95
|
|
|
94
96
|
# Create regional plot
|
|
95
97
|
fig = plotter.plot(
|
|
@@ -109,7 +111,7 @@ fig.savefig("regional_plot.png", dpi=150)
|
|
|
109
111
|
from pylocuszoom import LocusZoomPlotter
|
|
110
112
|
|
|
111
113
|
plotter = LocusZoomPlotter(
|
|
112
|
-
species="
|
|
114
|
+
species="canine", # or "feline", or None for custom
|
|
113
115
|
plink_path="/path/to/plink", # Optional, auto-detects if on PATH
|
|
114
116
|
)
|
|
115
117
|
|
|
@@ -134,10 +136,10 @@ fig = plotter.plot(
|
|
|
134
136
|
|
|
135
137
|
## Genome Builds
|
|
136
138
|
|
|
137
|
-
The default genome build for
|
|
139
|
+
The default genome build for canine is CanFam3.1. For CanFam4 data:
|
|
138
140
|
|
|
139
141
|
```python
|
|
140
|
-
plotter = LocusZoomPlotter(species="
|
|
142
|
+
plotter = LocusZoomPlotter(species="canine", genome_build="canfam4")
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
Recombination maps are automatically lifted over from CanFam3.1 to CanFam4 coordinates using the UCSC liftOver chain file.
|
|
@@ -145,8 +147,8 @@ Recombination maps are automatically lifted over from CanFam3.1 to CanFam4 coord
|
|
|
145
147
|
## Using with Other Species
|
|
146
148
|
|
|
147
149
|
```python
|
|
148
|
-
#
|
|
149
|
-
plotter = LocusZoomPlotter(species="
|
|
150
|
+
# Feline (LD and gene tracks, user provides recombination data)
|
|
151
|
+
plotter = LocusZoomPlotter(species="feline")
|
|
150
152
|
|
|
151
153
|
# Custom species (provide all reference data)
|
|
152
154
|
plotter = LocusZoomPlotter(
|
|
@@ -163,27 +165,20 @@ fig = plotter.plot(
|
|
|
163
165
|
)
|
|
164
166
|
```
|
|
165
167
|
|
|
166
|
-
## Interactive Backends
|
|
168
|
+
## Interactive Backends (Coming Soon)
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
> **Note:** Interactive backends (plotly, bokeh) are planned but not yet fully integrated. Currently all plots use matplotlib.
|
|
169
171
|
|
|
170
172
|
```python
|
|
171
|
-
# Static publication-quality plot (default)
|
|
172
|
-
plotter = LocusZoomPlotter(species="
|
|
173
|
+
# Static publication-quality plot (default, currently only supported backend)
|
|
174
|
+
plotter = LocusZoomPlotter(species="canine", backend="matplotlib")
|
|
173
175
|
fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
|
|
174
176
|
fig.savefig("plot.png", dpi=150)
|
|
175
|
-
|
|
176
|
-
# Interactive with plotly (hover tooltips, zoom/pan)
|
|
177
|
-
plotter = LocusZoomPlotter(species="dog", backend="plotly")
|
|
178
|
-
fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
|
|
179
|
-
fig.write_html("plot.html")
|
|
180
|
-
|
|
181
|
-
# Interactive with bokeh (dashboard-friendly)
|
|
182
|
-
plotter = LocusZoomPlotter(species="dog", backend="bokeh")
|
|
183
|
-
fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
|
|
184
177
|
```
|
|
185
178
|
|
|
186
|
-
|
|
179
|
+
Future releases will support:
|
|
180
|
+
- **Plotly**: Interactive plots with hover tooltips, zoom/pan
|
|
181
|
+
- **Bokeh**: Dashboard-friendly interactive plots
|
|
187
182
|
|
|
188
183
|
## Stacked Plots
|
|
189
184
|
|
|
@@ -324,14 +319,14 @@ chr pos rate cM
|
|
|
324
319
|
|
|
325
320
|
## Reference Data
|
|
326
321
|
|
|
327
|
-
|
|
322
|
+
Canine recombination maps are downloaded from [Campbell et al. 2016](https://github.com/cflerin/dog_recombination) on first use.
|
|
328
323
|
|
|
329
324
|
To manually download:
|
|
330
325
|
|
|
331
326
|
```python
|
|
332
|
-
from pylocuszoom import
|
|
327
|
+
from pylocuszoom import download_canine_recombination_maps
|
|
333
328
|
|
|
334
|
-
|
|
329
|
+
download_canine_recombination_maps()
|
|
335
330
|
```
|
|
336
331
|
|
|
337
332
|
## Logging
|
|
@@ -29,6 +29,8 @@ Inspired by [LocusZoom](http://locuszoom.org/) and [locuszoomr](https://github.c
|
|
|
29
29
|
- **eQTL overlay**: Expression QTL data as separate panel
|
|
30
30
|
- **PySpark support**: Handles large-scale genomics DataFrames
|
|
31
31
|
|
|
32
|
+

|
|
33
|
+
|
|
32
34
|
## Installation
|
|
33
35
|
|
|
34
36
|
```bash
|
|
@@ -46,8 +48,8 @@ pip install pylocuszoom
|
|
|
46
48
|
```python
|
|
47
49
|
from pylocuszoom import LocusZoomPlotter
|
|
48
50
|
|
|
49
|
-
# Initialize plotter (loads reference data for
|
|
50
|
-
plotter = LocusZoomPlotter(species="
|
|
51
|
+
# Initialize plotter (loads reference data for canine)
|
|
52
|
+
plotter = LocusZoomPlotter(species="canine")
|
|
51
53
|
|
|
52
54
|
# Create regional plot
|
|
53
55
|
fig = plotter.plot(
|
|
@@ -67,7 +69,7 @@ fig.savefig("regional_plot.png", dpi=150)
|
|
|
67
69
|
from pylocuszoom import LocusZoomPlotter
|
|
68
70
|
|
|
69
71
|
plotter = LocusZoomPlotter(
|
|
70
|
-
species="
|
|
72
|
+
species="canine", # or "feline", or None for custom
|
|
71
73
|
plink_path="/path/to/plink", # Optional, auto-detects if on PATH
|
|
72
74
|
)
|
|
73
75
|
|
|
@@ -92,10 +94,10 @@ fig = plotter.plot(
|
|
|
92
94
|
|
|
93
95
|
## Genome Builds
|
|
94
96
|
|
|
95
|
-
The default genome build for
|
|
97
|
+
The default genome build for canine is CanFam3.1. For CanFam4 data:
|
|
96
98
|
|
|
97
99
|
```python
|
|
98
|
-
plotter = LocusZoomPlotter(species="
|
|
100
|
+
plotter = LocusZoomPlotter(species="canine", genome_build="canfam4")
|
|
99
101
|
```
|
|
100
102
|
|
|
101
103
|
Recombination maps are automatically lifted over from CanFam3.1 to CanFam4 coordinates using the UCSC liftOver chain file.
|
|
@@ -103,8 +105,8 @@ Recombination maps are automatically lifted over from CanFam3.1 to CanFam4 coord
|
|
|
103
105
|
## Using with Other Species
|
|
104
106
|
|
|
105
107
|
```python
|
|
106
|
-
#
|
|
107
|
-
plotter = LocusZoomPlotter(species="
|
|
108
|
+
# Feline (LD and gene tracks, user provides recombination data)
|
|
109
|
+
plotter = LocusZoomPlotter(species="feline")
|
|
108
110
|
|
|
109
111
|
# Custom species (provide all reference data)
|
|
110
112
|
plotter = LocusZoomPlotter(
|
|
@@ -121,27 +123,20 @@ fig = plotter.plot(
|
|
|
121
123
|
)
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
## Interactive Backends
|
|
126
|
+
## Interactive Backends (Coming Soon)
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
> **Note:** Interactive backends (plotly, bokeh) are planned but not yet fully integrated. Currently all plots use matplotlib.
|
|
127
129
|
|
|
128
130
|
```python
|
|
129
|
-
# Static publication-quality plot (default)
|
|
130
|
-
plotter = LocusZoomPlotter(species="
|
|
131
|
+
# Static publication-quality plot (default, currently only supported backend)
|
|
132
|
+
plotter = LocusZoomPlotter(species="canine", backend="matplotlib")
|
|
131
133
|
fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
|
|
132
134
|
fig.savefig("plot.png", dpi=150)
|
|
133
|
-
|
|
134
|
-
# Interactive with plotly (hover tooltips, zoom/pan)
|
|
135
|
-
plotter = LocusZoomPlotter(species="dog", backend="plotly")
|
|
136
|
-
fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
|
|
137
|
-
fig.write_html("plot.html")
|
|
138
|
-
|
|
139
|
-
# Interactive with bokeh (dashboard-friendly)
|
|
140
|
-
plotter = LocusZoomPlotter(species="dog", backend="bokeh")
|
|
141
|
-
fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
|
|
142
135
|
```
|
|
143
136
|
|
|
144
|
-
|
|
137
|
+
Future releases will support:
|
|
138
|
+
- **Plotly**: Interactive plots with hover tooltips, zoom/pan
|
|
139
|
+
- **Bokeh**: Dashboard-friendly interactive plots
|
|
145
140
|
|
|
146
141
|
## Stacked Plots
|
|
147
142
|
|
|
@@ -282,14 +277,14 @@ chr pos rate cM
|
|
|
282
277
|
|
|
283
278
|
## Reference Data
|
|
284
279
|
|
|
285
|
-
|
|
280
|
+
Canine recombination maps are downloaded from [Campbell et al. 2016](https://github.com/cflerin/dog_recombination) on first use.
|
|
286
281
|
|
|
287
282
|
To manually download:
|
|
288
283
|
|
|
289
284
|
```python
|
|
290
|
-
from pylocuszoom import
|
|
285
|
+
from pylocuszoom import download_canine_recombination_maps
|
|
291
286
|
|
|
292
|
-
|
|
287
|
+
download_canine_recombination_maps()
|
|
293
288
|
```
|
|
294
289
|
|
|
295
290
|
## Logging
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate example plots for README documentation.
|
|
3
|
+
|
|
4
|
+
Note: Backend integration (plotly/bokeh) is not yet fully implemented in the main
|
|
5
|
+
plot methods. This script generates matplotlib plots only.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import matplotlib
|
|
9
|
+
|
|
10
|
+
matplotlib.use("Agg") # Non-interactive backend
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from pylocuszoom import LocusZoomPlotter
|
|
16
|
+
|
|
17
|
+
# Generate synthetic GWAS data
|
|
18
|
+
np.random.seed(42)
|
|
19
|
+
n_snps = 500
|
|
20
|
+
positions = np.sort(np.random.randint(1_000_000, 2_000_000, n_snps))
|
|
21
|
+
|
|
22
|
+
# Ensure lead SNP exists at exact position
|
|
23
|
+
peak_center = 1_500_000
|
|
24
|
+
positions[250] = peak_center # Place lead SNP in middle of array
|
|
25
|
+
|
|
26
|
+
# Create a peak around position 1,500,000
|
|
27
|
+
p_values = np.ones(n_snps) * 0.5
|
|
28
|
+
for i, pos in enumerate(positions):
|
|
29
|
+
dist = abs(pos - peak_center)
|
|
30
|
+
if dist < 100_000:
|
|
31
|
+
p_values[i] = 10 ** -(8 * np.exp(-dist / 30_000))
|
|
32
|
+
else:
|
|
33
|
+
p_values[i] = np.random.uniform(0.01, 1)
|
|
34
|
+
|
|
35
|
+
# Generate synthetic LD values (R² with lead SNP at peak_center)
|
|
36
|
+
# LD decays with distance from lead SNP
|
|
37
|
+
ld_values = []
|
|
38
|
+
for pos in positions:
|
|
39
|
+
dist = abs(pos - peak_center)
|
|
40
|
+
if dist == 0:
|
|
41
|
+
r2 = 1.0
|
|
42
|
+
elif dist < 50_000:
|
|
43
|
+
# High LD close to lead
|
|
44
|
+
r2 = max(0, 0.9 * np.exp(-dist / 20_000) + np.random.uniform(-0.1, 0.1))
|
|
45
|
+
elif dist < 150_000:
|
|
46
|
+
# Moderate LD at medium distance
|
|
47
|
+
r2 = max(0, 0.5 * np.exp(-dist / 50_000) + np.random.uniform(-0.1, 0.1))
|
|
48
|
+
else:
|
|
49
|
+
# Low/no LD far from lead
|
|
50
|
+
r2 = max(0, np.random.uniform(0, 0.2))
|
|
51
|
+
ld_values.append(min(1.0, r2))
|
|
52
|
+
|
|
53
|
+
gwas_df = pd.DataFrame(
|
|
54
|
+
{
|
|
55
|
+
"ps": positions,
|
|
56
|
+
"p_wald": p_values,
|
|
57
|
+
"rs": [f"rs{i}" for i in range(n_snps)],
|
|
58
|
+
"ld_r2": ld_values, # Pre-computed LD column
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Create gene annotations
|
|
63
|
+
genes_df = pd.DataFrame(
|
|
64
|
+
{
|
|
65
|
+
"chr": ["1", "1", "1", "1"],
|
|
66
|
+
"start": [1_100_000, 1_400_000, 1_550_000, 1_800_000],
|
|
67
|
+
"end": [1_200_000, 1_520_000, 1_650_000, 1_900_000],
|
|
68
|
+
"gene_name": ["GENE1", "GENE2", "GENE3", "GENE4"],
|
|
69
|
+
"strand": ["+", "-", "+", "-"],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Create exon annotations
|
|
74
|
+
exons_df = pd.DataFrame(
|
|
75
|
+
{
|
|
76
|
+
"chr": ["1", "1", "1", "1", "1", "1"],
|
|
77
|
+
"start": [1_100_000, 1_150_000, 1_400_000, 1_450_000, 1_550_000, 1_600_000],
|
|
78
|
+
"end": [1_120_000, 1_170_000, 1_420_000, 1_470_000, 1_580_000, 1_630_000],
|
|
79
|
+
"gene_name": ["GENE1", "GENE1", "GENE2", "GENE2", "GENE3", "GENE3"],
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
print("Generating example plots...")
|
|
84
|
+
|
|
85
|
+
# 1. Basic matplotlib plot with LD coloring
|
|
86
|
+
print("1. Basic regional plot with LD coloring...")
|
|
87
|
+
plotter = LocusZoomPlotter(species="canine", log_level=None)
|
|
88
|
+
fig = plotter.plot(
|
|
89
|
+
gwas_df,
|
|
90
|
+
chrom=1,
|
|
91
|
+
start=1_000_000,
|
|
92
|
+
end=2_000_000,
|
|
93
|
+
lead_pos=1_500_000,
|
|
94
|
+
ld_col="ld_r2", # Use pre-computed LD values for coloring
|
|
95
|
+
genes_df=genes_df,
|
|
96
|
+
exons_df=exons_df,
|
|
97
|
+
show_recombination=False,
|
|
98
|
+
snp_labels=True,
|
|
99
|
+
label_top_n=1,
|
|
100
|
+
)
|
|
101
|
+
fig.savefig("examples/regional_plot.png", dpi=150, bbox_inches="tight")
|
|
102
|
+
print(" Saved: examples/regional_plot.png")
|
|
103
|
+
|
|
104
|
+
# 2. Stacked plot with LD coloring
|
|
105
|
+
print("2. Stacked plot with LD coloring...")
|
|
106
|
+
gwas_df2 = gwas_df.copy()
|
|
107
|
+
gwas_df2["p_wald"] = np.ones(n_snps) * 0.5
|
|
108
|
+
peak_center2 = 1_700_000
|
|
109
|
+
# Ensure lead SNP exists at exact position for second panel
|
|
110
|
+
positions[350] = peak_center2
|
|
111
|
+
gwas_df2.loc[350, "ps"] = peak_center2
|
|
112
|
+
for i, pos in enumerate(positions):
|
|
113
|
+
dist = abs(pos - peak_center2)
|
|
114
|
+
if dist < 80_000:
|
|
115
|
+
gwas_df2.loc[i, "p_wald"] = 10 ** -(6 * np.exp(-dist / 25_000))
|
|
116
|
+
else:
|
|
117
|
+
gwas_df2.loc[i, "p_wald"] = np.random.uniform(0.05, 1)
|
|
118
|
+
|
|
119
|
+
# Generate LD for second GWAS (different lead SNP)
|
|
120
|
+
ld_values2 = []
|
|
121
|
+
for pos in positions:
|
|
122
|
+
dist = abs(pos - peak_center2)
|
|
123
|
+
if dist == 0:
|
|
124
|
+
r2 = 1.0
|
|
125
|
+
elif dist < 50_000:
|
|
126
|
+
r2 = max(0, 0.9 * np.exp(-dist / 20_000) + np.random.uniform(-0.1, 0.1))
|
|
127
|
+
elif dist < 150_000:
|
|
128
|
+
r2 = max(0, 0.5 * np.exp(-dist / 50_000) + np.random.uniform(-0.1, 0.1))
|
|
129
|
+
else:
|
|
130
|
+
r2 = max(0, np.random.uniform(0, 0.2))
|
|
131
|
+
ld_values2.append(min(1.0, r2))
|
|
132
|
+
gwas_df2["ld_r2"] = ld_values2
|
|
133
|
+
|
|
134
|
+
fig = plotter.plot_stacked(
|
|
135
|
+
[gwas_df, gwas_df2],
|
|
136
|
+
chrom=1,
|
|
137
|
+
start=1_000_000,
|
|
138
|
+
end=2_000_000,
|
|
139
|
+
lead_positions=[1_500_000, 1_700_000], # Lead SNPs for each panel
|
|
140
|
+
ld_col="ld_r2", # Use pre-computed LD values for coloring
|
|
141
|
+
panel_labels=["Phenotype A", "Phenotype B"],
|
|
142
|
+
genes_df=genes_df,
|
|
143
|
+
show_recombination=False,
|
|
144
|
+
label_top_n=1,
|
|
145
|
+
)
|
|
146
|
+
fig.savefig("examples/stacked_plot.png", dpi=150, bbox_inches="tight")
|
|
147
|
+
print(" Saved: examples/stacked_plot.png")
|
|
148
|
+
|
|
149
|
+
# 3. eQTL overlay with effect sizes
|
|
150
|
+
print("3. eQTL overlay plot...")
|
|
151
|
+
eqtl_df = pd.DataFrame(
|
|
152
|
+
{
|
|
153
|
+
"pos": [1_420_000, 1_450_000, 1_480_000, 1_500_000, 1_520_000, 1_550_000, 1_600_000, 1_650_000],
|
|
154
|
+
"p_value": [1e-4, 1e-5, 1e-6, 1e-7, 1e-6, 1e-4, 1e-3, 0.01],
|
|
155
|
+
"gene": ["GENE2", "GENE2", "GENE2", "GENE2", "GENE2", "GENE2", "GENE3", "GENE3"],
|
|
156
|
+
"effect_size": [0.35, 0.28, 0.22, 0.15, -0.18, -0.25, -0.32, 0.12],
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
fig = plotter.plot_stacked(
|
|
161
|
+
[gwas_df],
|
|
162
|
+
chrom=1,
|
|
163
|
+
start=1_000_000,
|
|
164
|
+
end=2_000_000,
|
|
165
|
+
lead_positions=[1_500_000], # Lead SNP for LD coloring
|
|
166
|
+
ld_col="ld_r2", # Use pre-computed LD values for coloring
|
|
167
|
+
eqtl_df=eqtl_df,
|
|
168
|
+
eqtl_gene="GENE2",
|
|
169
|
+
genes_df=genes_df,
|
|
170
|
+
show_recombination=False,
|
|
171
|
+
label_top_n=1,
|
|
172
|
+
)
|
|
173
|
+
fig.savefig("examples/eqtl_overlay.png", dpi=150, bbox_inches="tight")
|
|
174
|
+
print(" Saved: examples/eqtl_overlay.png")
|
|
175
|
+
|
|
176
|
+
# 4. Fine-mapping/SuSiE plot
|
|
177
|
+
print("4. Fine-mapping/SuSiE plot with credible sets...")
|
|
178
|
+
|
|
179
|
+
# Generate synthetic fine-mapping data
|
|
180
|
+
# Create PIP values that peak around the lead SNP
|
|
181
|
+
finemapping_positions = positions.copy()
|
|
182
|
+
pip_values = []
|
|
183
|
+
cs_assignments = []
|
|
184
|
+
|
|
185
|
+
for i, pos in enumerate(finemapping_positions):
|
|
186
|
+
dist = abs(pos - peak_center)
|
|
187
|
+
if dist < 20_000:
|
|
188
|
+
# High PIP near causal variant
|
|
189
|
+
pip = max(0, 0.95 * np.exp(-dist / 8_000) + np.random.uniform(-0.05, 0.05))
|
|
190
|
+
cs = 1 if pip > 0.1 else 0
|
|
191
|
+
elif dist < 80_000:
|
|
192
|
+
# Moderate PIP in LD region
|
|
193
|
+
pip = max(0, 0.3 * np.exp(-dist / 30_000) + np.random.uniform(-0.02, 0.02))
|
|
194
|
+
cs = 1 if pip > 0.05 else 0
|
|
195
|
+
else:
|
|
196
|
+
# Low PIP elsewhere
|
|
197
|
+
pip = max(0, np.random.uniform(0, 0.02))
|
|
198
|
+
cs = 0
|
|
199
|
+
pip_values.append(min(1.0, pip))
|
|
200
|
+
cs_assignments.append(cs)
|
|
201
|
+
|
|
202
|
+
# Add a second credible set near a different peak
|
|
203
|
+
for i, pos in enumerate(finemapping_positions):
|
|
204
|
+
dist = abs(pos - 1_300_000) # Second signal
|
|
205
|
+
if dist < 30_000:
|
|
206
|
+
pip_values[i] = max(pip_values[i], 0.7 * np.exp(-dist / 12_000))
|
|
207
|
+
if pip_values[i] > 0.05:
|
|
208
|
+
cs_assignments[i] = 2 # Second credible set
|
|
209
|
+
|
|
210
|
+
finemapping_df = pd.DataFrame(
|
|
211
|
+
{
|
|
212
|
+
"pos": finemapping_positions,
|
|
213
|
+
"pip": pip_values,
|
|
214
|
+
"cs": cs_assignments,
|
|
215
|
+
"rs": [f"rs{i}" for i in range(n_snps)],
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
fig = plotter.plot_stacked(
|
|
220
|
+
[gwas_df],
|
|
221
|
+
chrom=1,
|
|
222
|
+
start=1_000_000,
|
|
223
|
+
end=2_000_000,
|
|
224
|
+
lead_positions=[1_500_000],
|
|
225
|
+
ld_col="ld_r2",
|
|
226
|
+
finemapping_df=finemapping_df,
|
|
227
|
+
finemapping_cs_col="cs",
|
|
228
|
+
genes_df=genes_df,
|
|
229
|
+
show_recombination=False,
|
|
230
|
+
label_top_n=1,
|
|
231
|
+
)
|
|
232
|
+
fig.savefig("examples/finemapping_plot.png", dpi=150, bbox_inches="tight")
|
|
233
|
+
print(" Saved: examples/finemapping_plot.png")
|
|
234
|
+
|
|
235
|
+
print("\nAll plots generated successfully!")
|