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.
Files changed (45) hide show
  1. pylocuszoom-0.2.0/CHANGELOG.md +59 -0
  2. pylocuszoom-0.2.0/CONTRIBUTING.md +114 -0
  3. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/PKG-INFO +20 -25
  4. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/README.md +19 -24
  5. pylocuszoom-0.2.0/examples/eqtl_overlay.png +0 -0
  6. pylocuszoom-0.2.0/examples/finemapping_plot.png +0 -0
  7. pylocuszoom-0.2.0/examples/generate_readme_plots.py +235 -0
  8. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/examples/getting_started.ipynb +8 -119
  9. pylocuszoom-0.2.0/examples/regional_plot.png +0 -0
  10. pylocuszoom-0.2.0/examples/stacked_plot.png +0 -0
  11. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/pyproject.toml +1 -1
  12. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/__init__.py +39 -20
  13. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/__init__.py +1 -5
  14. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/base.py +1 -1
  15. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/bokeh_backend.py +4 -7
  16. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/matplotlib_backend.py +6 -1
  17. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/backends/plotly_backend.py +11 -12
  18. pylocuszoom-0.2.0/src/pylocuszoom/colors.py +239 -0
  19. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/eqtl.py +3 -2
  20. pylocuszoom-0.2.0/src/pylocuszoom/finemapping.py +224 -0
  21. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/gene_track.py +44 -31
  22. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/labels.py +32 -33
  23. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/ld.py +8 -7
  24. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/plotter.py +381 -66
  25. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/recombination.py +14 -14
  26. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/utils.py +3 -1
  27. pylocuszoom-0.2.0/tests/test_finemapping.py +154 -0
  28. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_labels.py +4 -4
  29. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_ld.py +26 -6
  30. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_plotter.py +135 -7
  31. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/uv.lock +1 -1
  32. pylocuszoom-0.1.0/src/pylocuszoom/colors.py +0 -107
  33. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/.github/workflows/ci.yml +0 -0
  34. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/.github/workflows/publish.yml +0 -0
  35. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/.gitignore +0 -0
  36. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/LICENSE.md +0 -0
  37. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/docs/ARCHITECTURE.md +0 -0
  38. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/logo.svg +0 -0
  39. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/logging.py +0 -0
  40. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/src/pylocuszoom/reference_data/__init__.py +0 -0
  41. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/conftest.py +0 -0
  42. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_colors.py +0 -0
  43. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_gene_track.py +0 -0
  44. {pylocuszoom-0.1.0 → pylocuszoom-0.2.0}/tests/test_logging.py +0 -0
  45. {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.1.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
+ ![Example regional association plot](examples/regional_plot.png)
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 dog)
92
- plotter = LocusZoomPlotter(species="dog")
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="dog", # or "cat", or None for custom
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 dog is CanFam3.1. For CanFam4 data:
139
+ The default genome build for canine is CanFam3.1. For CanFam4 data:
138
140
 
139
141
  ```python
140
- plotter = LocusZoomPlotter(species="dog", genome_build="canfam4")
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
- # Cat (LD and gene tracks, user provides recombination data)
149
- plotter = LocusZoomPlotter(species="cat")
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
- Choose between static (matplotlib) and interactive (plotly, bokeh) outputs:
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="dog", backend="matplotlib")
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
- Interactive plots show SNP details (RS ID, p-value, R²) on hover.
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
- Dog recombination maps are downloaded from [Campbell et al. 2016](https://github.com/cflerin/dog_recombination) on first use.
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 download_dog_recombination_maps
327
+ from pylocuszoom import download_canine_recombination_maps
333
328
 
334
- download_dog_recombination_maps()
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
+ ![Example regional association plot](examples/regional_plot.png)
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 dog)
50
- plotter = LocusZoomPlotter(species="dog")
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="dog", # or "cat", or None for custom
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 dog is CanFam3.1. For CanFam4 data:
97
+ The default genome build for canine is CanFam3.1. For CanFam4 data:
96
98
 
97
99
  ```python
98
- plotter = LocusZoomPlotter(species="dog", genome_build="canfam4")
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
- # Cat (LD and gene tracks, user provides recombination data)
107
- plotter = LocusZoomPlotter(species="cat")
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
- Choose between static (matplotlib) and interactive (plotly, bokeh) outputs:
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="dog", backend="matplotlib")
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
- Interactive plots show SNP details (RS ID, p-value, R²) on hover.
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
- Dog recombination maps are downloaded from [Campbell et al. 2016](https://github.com/cflerin/dog_recombination) on first use.
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 download_dog_recombination_maps
285
+ from pylocuszoom import download_canine_recombination_maps
291
286
 
292
- download_dog_recombination_maps()
287
+ download_canine_recombination_maps()
293
288
  ```
294
289
 
295
290
  ## Logging
@@ -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!")