pylocuszoom 0.5.0__tar.gz → 0.8.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 (83) hide show
  1. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/.github/workflows/ci.yml +1 -8
  2. pylocuszoom-0.8.0/.github/workflows/publish.yml +110 -0
  3. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/.gitignore +2 -0
  4. pylocuszoom-0.8.0/.pre-commit-config.yaml +17 -0
  5. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/CHANGELOG.md +67 -1
  6. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/PKG-INFO +97 -28
  7. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/README.md +89 -24
  8. pylocuszoom-0.8.0/bioconda/meta.yaml +63 -0
  9. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/docs/USER_GUIDE.md +197 -1
  10. pylocuszoom-0.8.0/examples/eqtl_bokeh.html +61 -0
  11. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/examples/eqtl_overlay.png +0 -0
  12. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/examples/eqtl_plotly.html +1 -1
  13. pylocuszoom-0.8.0/examples/finemapping_bokeh.html +61 -0
  14. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/examples/finemapping_plot.png +0 -0
  15. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/examples/finemapping_plotly.html +1 -1
  16. pylocuszoom-0.8.0/examples/forest_plot.png +0 -0
  17. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/examples/generate_readme_plots.py +76 -2
  18. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/examples/getting_started.ipynb +167 -56
  19. pylocuszoom-0.8.0/examples/phewas_plot.png +0 -0
  20. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/examples/regional_plot.png +0 -0
  21. pylocuszoom-0.8.0/examples/stacked_plot.png +0 -0
  22. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/pyproject.toml +26 -4
  23. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/__init__.py +38 -2
  24. pylocuszoom-0.8.0/src/pylocuszoom/backends/__init__.py +147 -0
  25. pylocuszoom-0.8.0/src/pylocuszoom/backends/base.py +777 -0
  26. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/backends/bokeh_backend.py +192 -34
  27. pylocuszoom-0.8.0/src/pylocuszoom/backends/hover.py +198 -0
  28. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/backends/matplotlib_backend.py +332 -3
  29. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/backends/plotly_backend.py +187 -38
  30. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/colors.py +41 -0
  31. pylocuszoom-0.8.0/src/pylocuszoom/ensembl.py +476 -0
  32. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/eqtl.py +15 -19
  33. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/finemapping.py +17 -26
  34. pylocuszoom-0.8.0/src/pylocuszoom/forest.py +35 -0
  35. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/gene_track.py +161 -135
  36. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/loaders.py +38 -18
  37. pylocuszoom-0.8.0/src/pylocuszoom/phewas.py +34 -0
  38. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/plotter.py +370 -190
  39. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/recombination.py +64 -34
  40. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/schemas.py +37 -26
  41. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/utils.py +52 -0
  42. pylocuszoom-0.8.0/src/pylocuszoom/validation.py +172 -0
  43. pylocuszoom-0.8.0/tests/test_backends.py +294 -0
  44. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_colors.py +25 -0
  45. pylocuszoom-0.8.0/tests/test_ensembl.py +466 -0
  46. pylocuszoom-0.8.0/tests/test_ensembl_integration.py +77 -0
  47. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_finemapping.py +3 -7
  48. pylocuszoom-0.8.0/tests/test_forest.py +54 -0
  49. pylocuszoom-0.8.0/tests/test_hover.py +351 -0
  50. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_loaders.py +134 -0
  51. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_notebook_backends.py +133 -0
  52. pylocuszoom-0.8.0/tests/test_phewas.py +51 -0
  53. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_plotter.py +257 -0
  54. pylocuszoom-0.8.0/tests/test_utils.py +162 -0
  55. pylocuszoom-0.8.0/tests/test_validation.py +343 -0
  56. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/uv.lock +187 -2
  57. pylocuszoom-0.5.0/.github/workflows/publish.yml +0 -23
  58. pylocuszoom-0.5.0/.pre-commit-config.yaml +0 -7
  59. pylocuszoom-0.5.0/bioconda/meta.yaml +0 -54
  60. pylocuszoom-0.5.0/examples/eqtl_bokeh.html +0 -61
  61. pylocuszoom-0.5.0/examples/finemapping_bokeh.html +0 -61
  62. pylocuszoom-0.5.0/examples/stacked_plot.png +0 -0
  63. pylocuszoom-0.5.0/src/pylocuszoom/backends/__init__.py +0 -48
  64. pylocuszoom-0.5.0/src/pylocuszoom/backends/base.py +0 -388
  65. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/.gitattributes +0 -0
  66. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  67. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  68. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  69. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/CONTRIBUTING.md +0 -0
  70. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/LICENSE.md +0 -0
  71. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/docs/ARCHITECTURE.md +0 -0
  72. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/logo.svg +0 -0
  73. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/labels.py +0 -0
  74. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/ld.py +0 -0
  75. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/logging.py +0 -0
  76. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/py.typed +0 -0
  77. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/src/pylocuszoom/reference_data/__init__.py +0 -0
  78. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/conftest.py +0 -0
  79. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_gene_track.py +0 -0
  80. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_labels.py +0 -0
  81. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_ld.py +0 -0
  82. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_logging.py +0 -0
  83. {pylocuszoom-0.5.0 → pylocuszoom-0.8.0}/tests/test_recombination.py +0 -0
@@ -44,14 +44,7 @@ jobs:
44
44
  run: uv sync --extra dev --extra all
45
45
 
46
46
  - name: Run tests
47
- run: uv run pytest --cov=pylocuszoom --cov-report=xml
48
-
49
- - name: Upload coverage
50
- uses: codecov/codecov-action@v4
51
- if: matrix.python-version == '3.11'
52
- with:
53
- files: ./coverage.xml
54
- fail_ci_if_error: false
47
+ run: uv run pytest --cov=pylocuszoom --cov-report=term-missing
55
48
 
56
49
  build:
57
50
  runs-on: ubuntu-latest
@@ -0,0 +1,110 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ outputs:
14
+ version: ${{ steps.version.outputs.version }}
15
+ sha256: ${{ steps.sha256.outputs.sha256 }}
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v5
21
+
22
+ - name: Get version
23
+ id: version
24
+ run: |
25
+ VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
26
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
27
+
28
+ - name: Build package
29
+ run: uv build
30
+
31
+ - name: Publish to PyPI
32
+ uses: pypa/gh-action-pypi-publish@release/v1
33
+
34
+ - name: Wait for PyPI availability
35
+ env:
36
+ PKG_VERSION: ${{ steps.version.outputs.version }}
37
+ run: |
38
+ echo "Waiting for pylocuszoom $PKG_VERSION to be available on PyPI..."
39
+ for i in {1..30}; do
40
+ if curl -s "https://pypi.org/pypi/pylocuszoom/$PKG_VERSION/json" | grep -q '"version"'; then
41
+ echo "Package available on PyPI"
42
+ break
43
+ fi
44
+ echo "Attempt $i: Package not yet available, waiting 10s..."
45
+ sleep 10
46
+ done
47
+
48
+ - name: Get SHA256
49
+ id: sha256
50
+ env:
51
+ PKG_VERSION: ${{ steps.version.outputs.version }}
52
+ run: |
53
+ URL="https://pypi.io/packages/source/p/pylocuszoom/pylocuszoom-$PKG_VERSION.tar.gz"
54
+ SHA256=$(curl -sL "$URL" | sha256sum | cut -d' ' -f1)
55
+ echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
56
+ echo "SHA256: $SHA256"
57
+
58
+ update-bioconda:
59
+ needs: publish
60
+ runs-on: ubuntu-latest
61
+ steps:
62
+ - uses: actions/checkout@v4
63
+ with:
64
+ ref: main
65
+
66
+ - name: Update bioconda/meta.yaml
67
+ env:
68
+ PKG_VERSION: ${{ needs.publish.outputs.version }}
69
+ PKG_SHA256: ${{ needs.publish.outputs.sha256 }}
70
+ run: |
71
+ # Update version
72
+ sed -i "s/{% set version = \".*\" %}/{% set version = \"$PKG_VERSION\" %}/" bioconda/meta.yaml
73
+
74
+ # Update sha256
75
+ sed -i "s/sha256: .*/sha256: $PKG_SHA256/" bioconda/meta.yaml
76
+
77
+ # Update plotly version requirement
78
+ sed -i "s/plotly >=5.0.0/plotly >=5.15.0/" bioconda/meta.yaml
79
+
80
+ # Add new dependencies if missing
81
+ if ! grep -q "pydantic" bioconda/meta.yaml; then
82
+ sed -i '/adjusttext/a\ - pydantic >=2.0.0' bioconda/meta.yaml
83
+ fi
84
+ if ! grep -q "requests" bioconda/meta.yaml; then
85
+ sed -i '/pydantic/a\ - requests >=2.25.0' bioconda/meta.yaml
86
+ fi
87
+ if ! grep -q "tqdm" bioconda/meta.yaml; then
88
+ sed -i '/requests/a\ - tqdm >=4.60.0' bioconda/meta.yaml
89
+ fi
90
+
91
+ cat bioconda/meta.yaml
92
+
93
+ - name: Create Pull Request
94
+ uses: peter-evans/create-pull-request@v6
95
+ with:
96
+ token: ${{ secrets.GITHUB_TOKEN }}
97
+ commit-message: "chore: update bioconda recipe for new release"
98
+ branch: bioconda-update
99
+ title: "Update bioconda recipe"
100
+ body: |
101
+ Automated update of bioconda/meta.yaml after PyPI release.
102
+
103
+ **Next steps:**
104
+ 1. Review this PR
105
+ 2. Merge to main
106
+ 3. Copy `bioconda/meta.yaml` to your fork of bioconda-recipes
107
+ 4. Submit PR to bioconda-recipes
108
+ labels: |
109
+ bioconda
110
+ automated
@@ -26,3 +26,5 @@ htmlcov/
26
26
 
27
27
  # Project instructions (private)
28
28
  CLAUDE.md
29
+ docs/plans/
30
+ .planning/
@@ -0,0 +1,17 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.9.1
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: local
10
+ hooks:
11
+ - id: pytest-cov
12
+ name: pytest with coverage
13
+ entry: uv run python -m pytest -q
14
+ language: system
15
+ types: [python]
16
+ pass_filenames: false
17
+ always_run: true
@@ -7,6 +7,69 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-01-28
11
+
12
+ ### Added
13
+ - `set_yticks()` backend method for consistent y-axis labels across all backends
14
+ - Shared `convert_latex_to_unicode()` utility for interactive backends
15
+ - Automatic gene annotation fetching from Ensembl REST API (`auto_genes=True`)
16
+ - `get_genes_for_region()` function to fetch genes from Ensembl with disk caching
17
+ - `fetch_genes_from_ensembl()` and `fetch_exons_from_ensembl()` low-level API functions
18
+ - `clear_ensembl_cache()` utility to clear cached Ensembl data
19
+ - Support for human, mouse, rat, and any Ensembl species
20
+ - Retry logic with exponential backoff for Ensembl API resilience
21
+ - 5Mb region size validation (Ensembl API limit)
22
+ - `DataFrameValidator` builder class for consistent validation across modules
23
+ - `filter_by_region()` shared utility for chromosome/position filtering
24
+ - `HoverDataBuilder` for constructing hover tooltips across backends
25
+ - Backend capability system with `supports_*` properties for feature detection
26
+ - Backend registration system with `get_backend()` and automatic fallback
27
+ - Pre-commit hook for pytest with coverage enforcement (70% minimum)
28
+
29
+ ### Changed
30
+ - Forest plot example now uses odds ratios with `null_value=1.0` (more representative)
31
+ - PheWAS and forest plot y-axis labels now work correctly in Plotly and Bokeh backends
32
+ - Gene track styling: arrows now 75% height and 10% wider for better proportions
33
+ - Gene track labels increased from 5.5pt to 7pt for improved readability
34
+ - Migrated eQTL, finemapping, phewas, and forest validation to `DataFrameValidator`
35
+ - Plotter now uses capability-based dispatch instead of backend name checks
36
+ - Removed empty `__init__` methods from backend classes
37
+ - Removed unused matplotlib imports from plotter (now backend-agnostic)
38
+
39
+ ### Fixed
40
+ - `load_gwas()` now forwards `**kwargs` to format-specific loaders
41
+ - Forest plot validator now checks that effect and CI columns are numeric
42
+ - PheWAS validator now checks that p-values are numeric and within (0, 1] range
43
+
44
+ ### Security
45
+ - Tar extraction now includes path traversal protection for recombination map downloads
46
+
47
+ ## [0.7.0] - 2026-01-27
48
+
49
+ ## [0.6.0] - 2026-01-27
50
+
51
+ ### Added
52
+ - `plot_phewas()` method for phenome-wide association study plots
53
+ - `plot_forest()` method for forest plots (meta-analysis visualization)
54
+ - PheWAS category color palette with 12 distinct colors
55
+ - Forest plot and PheWAS validation utilities
56
+ - Backend methods: `axvline()`, `hbar()`, `errorbar_h()` for new plot types
57
+ - Example plots for PheWAS and forest plots
58
+ - Progress bars (tqdm) for recombination map and liftover chain downloads
59
+ - `requests` and `tqdm` as core dependencies for reliable downloads with progress
60
+ - `pytest-randomly` and `pytest-xdist` as dev dependencies for test randomization and parallel execution
61
+
62
+ ### Changed
63
+ - Bumped minimum Plotly version to 5.15.0 (required for multiple legends feature)
64
+ - eQTL loaders now output `effect_size` column instead of `effect` for plotter compatibility
65
+ - Download functions now use `requests` with streaming and progress bars instead of `urllib`
66
+
67
+ ### Fixed
68
+ - SAIGE loader now prefers SPA-adjusted p-values (`p.value.NA`) over raw p-values when both present
69
+ - BED loader now handles BED12 format and files with more than 6 columns
70
+ - eQTL panel in `plot_stacked()` now filters by chromosome in addition to position
71
+ - Validation errors for non-numeric p-values or positions now show clear "must be numeric" message instead of runtime errors
72
+
10
73
  ## [0.5.0] - 2026-01-27
11
74
 
12
75
  ### Added
@@ -118,7 +181,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
118
181
  - bokeh >= 3.8.2
119
182
  - kaleido >= 0.2.0
120
183
 
121
- [Unreleased]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.5.0...HEAD
184
+ [Unreleased]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.8.0...HEAD
185
+ [0.8.0]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.7.0...v0.8.0
186
+ [0.7.0]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.6.0...v0.7.0
187
+ [0.6.0]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.5.0...v0.6.0
122
188
  [0.5.0]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.4.0...v0.5.0
123
189
  [0.4.0]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.3.0...v0.4.0
124
190
  [0.3.0]: https://github.com/michael-denyer/pyLocusZoom/compare/v0.2.0...v0.3.0
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pylocuszoom
3
- Version: 0.5.0
3
+ Version: 0.8.0
4
4
  Summary: Publication-ready regional association plots with LD coloring, gene tracks, and recombination overlays
5
5
  Project-URL: Homepage, https://github.com/michael-denyer/pylocuszoom
6
6
  Project-URL: Documentation, https://github.com/michael-denyer/pylocuszoom#readme
7
7
  Project-URL: Repository, https://github.com/michael-denyer/pylocuszoom
8
- Author: Michael Denyer
8
+ Author-email: Michael Denyer <code.denyer@gmail.com>
9
9
  License-Expression: GPL-3.0-or-later
10
10
  License-File: LICENSE.md
11
11
  Keywords: genetics,gwas,locus-zoom,locuszoom,regional-plot,visualization
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Science/Research
14
14
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
15
  Classifier: Programming Language :: Python :: 3
@@ -26,13 +26,17 @@ Requires-Dist: loguru>=0.7.0
26
26
  Requires-Dist: matplotlib>=3.5.0
27
27
  Requires-Dist: numpy>=1.21.0
28
28
  Requires-Dist: pandas>=1.4.0
29
- Requires-Dist: plotly>=5.0.0
29
+ Requires-Dist: plotly>=5.15.0
30
30
  Requires-Dist: pydantic>=2.0.0
31
31
  Requires-Dist: pyliftover>=0.4
32
+ Requires-Dist: requests>=2.25.0
33
+ Requires-Dist: tqdm>=4.60.0
32
34
  Provides-Extra: all
33
35
  Requires-Dist: pyspark>=3.0.0; extra == 'all'
34
36
  Provides-Extra: dev
35
37
  Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
38
+ Requires-Dist: pytest-randomly>=3.0.0; extra == 'dev'
39
+ Requires-Dist: pytest-xdist>=3.0.0; extra == 'dev'
36
40
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
37
41
  Requires-Dist: ruff>=0.1.0; extra == 'dev'
38
42
  Provides-Extra: spark
@@ -40,20 +44,18 @@ Requires-Dist: pyspark>=3.0.0; extra == 'spark'
40
44
  Description-Content-Type: text/markdown
41
45
 
42
46
  [![CI](https://github.com/michael-denyer/pyLocusZoom/actions/workflows/ci.yml/badge.svg)](https://github.com/michael-denyer/pyLocusZoom/actions/workflows/ci.yml)
43
- [![codecov](https://codecov.io/gh/michael-denyer/pyLocusZoom/graph/badge.svg)](https://codecov.io/gh/michael-denyer/pyLocusZoom)
44
47
  [![PyPI](https://img.shields.io/pypi/v/pylocuszoom)](https://pypi.org/project/pylocuszoom/)
45
- [![Bioconda](https://img.shields.io/conda/vn/bioconda/pylocuszoom)](https://anaconda.org/bioconda/pylocuszoom)
46
48
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg)](https://www.gnu.org/licenses/gpl-3.0)
47
49
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
48
50
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
49
51
  [![Matplotlib](https://img.shields.io/badge/Matplotlib-3.5+-11557c.svg)](https://matplotlib.org/)
50
- [![Plotly](https://img.shields.io/badge/Plotly-5.0+-3F4F75.svg)](https://plotly.com/python/)
52
+ [![Plotly](https://img.shields.io/badge/Plotly-5.15+-3F4F75.svg)](https://plotly.com/python/)
51
53
  [![Bokeh](https://img.shields.io/badge/Bokeh-3.8+-E6526F.svg)](https://bokeh.org/)
52
54
  [![Pandas](https://img.shields.io/badge/Pandas-1.4+-150458.svg)](https://pandas.pydata.org/)
53
55
  <img src="logo.svg" alt="pyLocusZoom logo" width="120" align="right">
54
56
  # pyLocusZoom
55
57
 
56
- Publication-ready regional association plots with LD coloring, gene tracks, and recombination overlays.
58
+ Designed for publication-ready GWAS visualization with regional association plots, gene tracks, eQTL, PheWAS, fine-mapping, and forest plots.
57
59
 
58
60
  Inspired by [LocusZoom](http://locuszoom.org/) and [locuszoomr](https://github.com/myles-lewis/locuszoomr).
59
61
 
@@ -64,18 +66,22 @@ Inspired by [LocusZoom](http://locuszoom.org/) and [locuszoomr](https://github.c
64
66
  - **Multi-species support**: Built-in reference data for *Canis lupus familiaris* (CanFam3.1/CanFam4) and *Felis catus* (FelCat9), or optionally provide your own for any species
65
67
  - **LD coloring**: SNPs colored by linkage disequilibrium (R²) with lead variant
66
68
  - **Gene tracks**: Annotated gene/exon positions below the association plot
67
- - **Recombination rate**: Overlay showing recombination rate across region (*Canis lupus familiaris* only)
68
- - **SNP labels (matplotlib)**: Automatic labeling of lead SNPs with RS ID
69
- - **Tooltips (Bokeh and Plotly)**: Mouseover for detailed SNP data
69
+ - **Recombination rate**: Optional overlay across region (*Canis lupus familiaris* built-in, not shown in example image)
70
+ - **SNP labels (matplotlib)**: Automatic labeling of top SNPs by p-value (RS IDs)
71
+ - **Hover tooltips (Plotly and Bokeh)**: Detailed SNP data on hover
70
72
 
71
- ![Example regional association plot](examples/regional_plot.png)
73
+ ![Example regional association plot with LD coloring and gene track](examples/regional_plot.png)
74
+ *Regional association plot with LD coloring, gene/exon track, and top SNP labels (recombination overlay disabled in example).*
72
75
 
73
76
  2. **Stacked plots**: Compare multiple GWAS/phenotypes vertically
74
77
  3. **eQTL plot**: Expression QTL data aligned with association plots and gene tracks
75
78
  4. **Fine-mapping plots**: Visualize SuSiE credible sets with posterior inclusion probabilities
76
- 5. **Multiple charting libraries**: matplotlib (static), plotly (interactive), bokeh (dashboards)
77
- 6. **Pandas and PySpark support**: Works with both Pandas and PySpark DataFrames for large-scale genomics data
78
- 7. **Convenience data file loaders**: Load and validate common GWAS, eQTL and fine-mapping file formats
79
+ 5. **PheWAS plots**: Phenome-wide association study visualization across multiple phenotypes
80
+ 6. **Forest plots**: Meta-analysis effect size visualization with confidence intervals
81
+ 7. **Multiple backends**: matplotlib (publication-ready), plotly (interactive), bokeh (dashboard integration)
82
+ 8. **Pandas and PySpark support**: Works with both Pandas and PySpark DataFrames for large-scale genomics data
83
+ 9. **Convenience data file loaders**: Load and validate common GWAS, eQTL and fine-mapping file formats
84
+ 10. **Automatic gene annotations**: Fetch gene/exon data from Ensembl REST API with caching (human, mouse, rat, canine, feline, and any Ensembl species)
79
85
 
80
86
  ## Installation
81
87
 
@@ -175,28 +181,46 @@ fig = plotter.plot(
175
181
  )
176
182
  ```
177
183
 
184
+ ## Automatic Gene Annotations
185
+
186
+ pyLocusZoom can automatically fetch gene annotations from Ensembl for any species:
187
+
188
+ ```python
189
+ # Enable automatic gene fetching
190
+ plotter = LocusZoomPlotter(species="human", auto_genes=True)
191
+
192
+ # No need to provide genes_df - fetched automatically
193
+ fig = plotter.plot(gwas_df, chrom=13, start=32000000, end=33000000)
194
+ ```
195
+
196
+ Supported species aliases: `human`, `mouse`, `rat`, `canine`/`dog`, `feline`/`cat`, or any Ensembl species name.
197
+ Data is cached locally for fast subsequent plots. Maximum region size is 5Mb (Ensembl API limit).
198
+
178
199
  ## Backends
179
200
 
180
- pyLocusZoom supports multiple rendering backends:
201
+ pyLocusZoom supports multiple rendering backends (set at initialization):
181
202
 
182
203
  ```python
183
204
  # Static publication-quality plot (default)
184
- fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000, backend="matplotlib")
205
+ plotter = LocusZoomPlotter(species="canine", backend="matplotlib")
206
+ fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
185
207
  fig.savefig("plot.png", dpi=150)
186
208
 
187
209
  # Interactive Plotly (hover tooltips, pan/zoom)
188
- fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000, backend="plotly")
210
+ plotter = LocusZoomPlotter(species="canine", backend="plotly")
211
+ fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
189
212
  fig.write_html("plot.html")
190
213
 
191
214
  # Interactive Bokeh (dashboard-ready)
192
- fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000, backend="bokeh")
215
+ plotter = LocusZoomPlotter(species="canine", backend="bokeh")
216
+ fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
193
217
  ```
194
218
 
195
219
  | Backend | Output | Best For | Features |
196
220
  |---------|--------|----------|----------|
197
- | `matplotlib` | Static PNG/PDF/SVG | Publications, presentations | Full feature set with SNP labels |
198
- | `plotly` | Interactive HTML | Web reports, data exploration | Hover tooltips, pan/zoom |
199
- | `bokeh` | Interactive HTML | Dashboards, web apps | Hover tooltips, pan/zoom |
221
+ | `matplotlib` | Static PNG/PDF/SVG | Publication-ready figures | Full feature set with SNP labels |
222
+ | `plotly` | Interactive HTML | Web reports, exploration | Hover tooltips, pan/zoom |
223
+ | `bokeh` | Interactive HTML | Dashboard integration | Hover tooltips, pan/zoom |
200
224
 
201
225
  > **Note:** All backends support scatter plots, gene tracks, recombination overlay, and LD legend. SNP labels (auto-positioned with adjustText) are matplotlib-only; interactive backends use hover tooltips instead.
202
226
 
@@ -215,7 +239,8 @@ fig = plotter.plot_stacked(
215
239
  )
216
240
  ```
217
241
 
218
- ![Example stacked plot](examples/stacked_plot.png)
242
+ ![Example stacked plot comparing two phenotypes](examples/stacked_plot.png)
243
+ *Stacked plot comparing two phenotypes with LD coloring and shared gene track.*
219
244
 
220
245
  ## eQTL Overlay
221
246
 
@@ -238,6 +263,7 @@ fig = plotter.plot_stacked(
238
263
  ```
239
264
 
240
265
  ![Example eQTL overlay plot](examples/eqtl_overlay.png)
266
+ *eQTL overlay with effect direction (up/down triangles) and magnitude binning.*
241
267
 
242
268
  ## Fine-mapping Visualization
243
269
 
@@ -260,19 +286,62 @@ fig = plotter.plot_stacked(
260
286
  ```
261
287
 
262
288
  ![Example fine-mapping plot](examples/finemapping_plot.png)
289
+ *Fine-mapping visualization with PIP line and credible set coloring (CS1/CS2).*
290
+
291
+ ## PheWAS Plots
292
+
293
+ Visualize associations of a single variant across multiple phenotypes:
294
+
295
+ ```python
296
+ phewas_df = pd.DataFrame({
297
+ "phenotype": ["Height", "BMI", "T2D", "CAD", "HDL"],
298
+ "p_value": [1e-15, 0.05, 1e-8, 1e-3, 1e-10],
299
+ "category": ["Anthropometric", "Anthropometric", "Metabolic", "Cardiovascular", "Lipids"],
300
+ })
301
+
302
+ fig = plotter.plot_phewas(
303
+ phewas_df,
304
+ variant_id="rs12345",
305
+ category_col="category",
306
+ )
307
+ ```
308
+
309
+ ![Example PheWAS plot](examples/phewas_plot.png)
310
+ *PheWAS plot showing associations across phenotype categories with significance threshold.*
311
+
312
+ ## Forest Plots
313
+
314
+ Create forest plots for meta-analysis visualization:
315
+
316
+ ```python
317
+ forest_df = pd.DataFrame({
318
+ "study": ["Study A", "Study B", "Study C", "Meta-analysis"],
319
+ "effect": [0.45, 0.52, 0.38, 0.46],
320
+ "ci_lower": [0.30, 0.35, 0.20, 0.40],
321
+ "ci_upper": [0.60, 0.69, 0.56, 0.52],
322
+ "weight": [25, 35, 20, 100],
323
+ })
324
+
325
+ fig = plotter.plot_forest(
326
+ forest_df,
327
+ variant_id="rs12345",
328
+ weight_col="weight",
329
+ )
330
+ ```
331
+
332
+ ![Example forest plot](examples/forest_plot.png)
333
+ *Forest plot with effect sizes, confidence intervals, and weight-proportional markers.*
263
334
 
264
335
  ## PySpark Support
265
336
 
266
- For large-scale genomics data, pass PySpark DataFrames directly:
337
+ For large-scale genomics data, convert PySpark DataFrames with `to_pandas()` before plotting:
267
338
 
268
339
  ```python
269
340
  from pylocuszoom import LocusZoomPlotter, to_pandas
270
341
 
271
- # PySpark DataFrame (automatically converted)
272
- fig = plotter.plot(spark_gwas_df, chrom=1, start=1000000, end=2000000)
273
-
274
- # Or convert manually with sampling for very large data
342
+ # Convert PySpark DataFrame (optionally sampled for very large data)
275
343
  pandas_df = to_pandas(spark_gwas_df, sample_size=100000)
344
+ fig = plotter.plot(pandas_df, chrom=1, start=1000000, end=2000000)
276
345
  ```
277
346
 
278
347
  Install PySpark support: `uv add pylocuszoom[spark]`
@@ -1,18 +1,16 @@
1
1
  [![CI](https://github.com/michael-denyer/pyLocusZoom/actions/workflows/ci.yml/badge.svg)](https://github.com/michael-denyer/pyLocusZoom/actions/workflows/ci.yml)
2
- [![codecov](https://codecov.io/gh/michael-denyer/pyLocusZoom/graph/badge.svg)](https://codecov.io/gh/michael-denyer/pyLocusZoom)
3
2
  [![PyPI](https://img.shields.io/pypi/v/pylocuszoom)](https://pypi.org/project/pylocuszoom/)
4
- [![Bioconda](https://img.shields.io/conda/vn/bioconda/pylocuszoom)](https://anaconda.org/bioconda/pylocuszoom)
5
3
  [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-red.svg)](https://www.gnu.org/licenses/gpl-3.0)
6
4
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
5
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
8
6
  [![Matplotlib](https://img.shields.io/badge/Matplotlib-3.5+-11557c.svg)](https://matplotlib.org/)
9
- [![Plotly](https://img.shields.io/badge/Plotly-5.0+-3F4F75.svg)](https://plotly.com/python/)
7
+ [![Plotly](https://img.shields.io/badge/Plotly-5.15+-3F4F75.svg)](https://plotly.com/python/)
10
8
  [![Bokeh](https://img.shields.io/badge/Bokeh-3.8+-E6526F.svg)](https://bokeh.org/)
11
9
  [![Pandas](https://img.shields.io/badge/Pandas-1.4+-150458.svg)](https://pandas.pydata.org/)
12
10
  <img src="logo.svg" alt="pyLocusZoom logo" width="120" align="right">
13
11
  # pyLocusZoom
14
12
 
15
- Publication-ready regional association plots with LD coloring, gene tracks, and recombination overlays.
13
+ Designed for publication-ready GWAS visualization with regional association plots, gene tracks, eQTL, PheWAS, fine-mapping, and forest plots.
16
14
 
17
15
  Inspired by [LocusZoom](http://locuszoom.org/) and [locuszoomr](https://github.com/myles-lewis/locuszoomr).
18
16
 
@@ -23,18 +21,22 @@ Inspired by [LocusZoom](http://locuszoom.org/) and [locuszoomr](https://github.c
23
21
  - **Multi-species support**: Built-in reference data for *Canis lupus familiaris* (CanFam3.1/CanFam4) and *Felis catus* (FelCat9), or optionally provide your own for any species
24
22
  - **LD coloring**: SNPs colored by linkage disequilibrium (R²) with lead variant
25
23
  - **Gene tracks**: Annotated gene/exon positions below the association plot
26
- - **Recombination rate**: Overlay showing recombination rate across region (*Canis lupus familiaris* only)
27
- - **SNP labels (matplotlib)**: Automatic labeling of lead SNPs with RS ID
28
- - **Tooltips (Bokeh and Plotly)**: Mouseover for detailed SNP data
24
+ - **Recombination rate**: Optional overlay across region (*Canis lupus familiaris* built-in, not shown in example image)
25
+ - **SNP labels (matplotlib)**: Automatic labeling of top SNPs by p-value (RS IDs)
26
+ - **Hover tooltips (Plotly and Bokeh)**: Detailed SNP data on hover
29
27
 
30
- ![Example regional association plot](examples/regional_plot.png)
28
+ ![Example regional association plot with LD coloring and gene track](examples/regional_plot.png)
29
+ *Regional association plot with LD coloring, gene/exon track, and top SNP labels (recombination overlay disabled in example).*
31
30
 
32
31
  2. **Stacked plots**: Compare multiple GWAS/phenotypes vertically
33
32
  3. **eQTL plot**: Expression QTL data aligned with association plots and gene tracks
34
33
  4. **Fine-mapping plots**: Visualize SuSiE credible sets with posterior inclusion probabilities
35
- 5. **Multiple charting libraries**: matplotlib (static), plotly (interactive), bokeh (dashboards)
36
- 6. **Pandas and PySpark support**: Works with both Pandas and PySpark DataFrames for large-scale genomics data
37
- 7. **Convenience data file loaders**: Load and validate common GWAS, eQTL and fine-mapping file formats
34
+ 5. **PheWAS plots**: Phenome-wide association study visualization across multiple phenotypes
35
+ 6. **Forest plots**: Meta-analysis effect size visualization with confidence intervals
36
+ 7. **Multiple backends**: matplotlib (publication-ready), plotly (interactive), bokeh (dashboard integration)
37
+ 8. **Pandas and PySpark support**: Works with both Pandas and PySpark DataFrames for large-scale genomics data
38
+ 9. **Convenience data file loaders**: Load and validate common GWAS, eQTL and fine-mapping file formats
39
+ 10. **Automatic gene annotations**: Fetch gene/exon data from Ensembl REST API with caching (human, mouse, rat, canine, feline, and any Ensembl species)
38
40
 
39
41
  ## Installation
40
42
 
@@ -134,28 +136,46 @@ fig = plotter.plot(
134
136
  )
135
137
  ```
136
138
 
139
+ ## Automatic Gene Annotations
140
+
141
+ pyLocusZoom can automatically fetch gene annotations from Ensembl for any species:
142
+
143
+ ```python
144
+ # Enable automatic gene fetching
145
+ plotter = LocusZoomPlotter(species="human", auto_genes=True)
146
+
147
+ # No need to provide genes_df - fetched automatically
148
+ fig = plotter.plot(gwas_df, chrom=13, start=32000000, end=33000000)
149
+ ```
150
+
151
+ Supported species aliases: `human`, `mouse`, `rat`, `canine`/`dog`, `feline`/`cat`, or any Ensembl species name.
152
+ Data is cached locally for fast subsequent plots. Maximum region size is 5Mb (Ensembl API limit).
153
+
137
154
  ## Backends
138
155
 
139
- pyLocusZoom supports multiple rendering backends:
156
+ pyLocusZoom supports multiple rendering backends (set at initialization):
140
157
 
141
158
  ```python
142
159
  # Static publication-quality plot (default)
143
- fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000, backend="matplotlib")
160
+ plotter = LocusZoomPlotter(species="canine", backend="matplotlib")
161
+ fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
144
162
  fig.savefig("plot.png", dpi=150)
145
163
 
146
164
  # Interactive Plotly (hover tooltips, pan/zoom)
147
- fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000, backend="plotly")
165
+ plotter = LocusZoomPlotter(species="canine", backend="plotly")
166
+ fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
148
167
  fig.write_html("plot.html")
149
168
 
150
169
  # Interactive Bokeh (dashboard-ready)
151
- fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000, backend="bokeh")
170
+ plotter = LocusZoomPlotter(species="canine", backend="bokeh")
171
+ fig = plotter.plot(gwas_df, chrom=1, start=1000000, end=2000000)
152
172
  ```
153
173
 
154
174
  | Backend | Output | Best For | Features |
155
175
  |---------|--------|----------|----------|
156
- | `matplotlib` | Static PNG/PDF/SVG | Publications, presentations | Full feature set with SNP labels |
157
- | `plotly` | Interactive HTML | Web reports, data exploration | Hover tooltips, pan/zoom |
158
- | `bokeh` | Interactive HTML | Dashboards, web apps | Hover tooltips, pan/zoom |
176
+ | `matplotlib` | Static PNG/PDF/SVG | Publication-ready figures | Full feature set with SNP labels |
177
+ | `plotly` | Interactive HTML | Web reports, exploration | Hover tooltips, pan/zoom |
178
+ | `bokeh` | Interactive HTML | Dashboard integration | Hover tooltips, pan/zoom |
159
179
 
160
180
  > **Note:** All backends support scatter plots, gene tracks, recombination overlay, and LD legend. SNP labels (auto-positioned with adjustText) are matplotlib-only; interactive backends use hover tooltips instead.
161
181
 
@@ -174,7 +194,8 @@ fig = plotter.plot_stacked(
174
194
  )
175
195
  ```
176
196
 
177
- ![Example stacked plot](examples/stacked_plot.png)
197
+ ![Example stacked plot comparing two phenotypes](examples/stacked_plot.png)
198
+ *Stacked plot comparing two phenotypes with LD coloring and shared gene track.*
178
199
 
179
200
  ## eQTL Overlay
180
201
 
@@ -197,6 +218,7 @@ fig = plotter.plot_stacked(
197
218
  ```
198
219
 
199
220
  ![Example eQTL overlay plot](examples/eqtl_overlay.png)
221
+ *eQTL overlay with effect direction (up/down triangles) and magnitude binning.*
200
222
 
201
223
  ## Fine-mapping Visualization
202
224
 
@@ -219,19 +241,62 @@ fig = plotter.plot_stacked(
219
241
  ```
220
242
 
221
243
  ![Example fine-mapping plot](examples/finemapping_plot.png)
244
+ *Fine-mapping visualization with PIP line and credible set coloring (CS1/CS2).*
245
+
246
+ ## PheWAS Plots
247
+
248
+ Visualize associations of a single variant across multiple phenotypes:
249
+
250
+ ```python
251
+ phewas_df = pd.DataFrame({
252
+ "phenotype": ["Height", "BMI", "T2D", "CAD", "HDL"],
253
+ "p_value": [1e-15, 0.05, 1e-8, 1e-3, 1e-10],
254
+ "category": ["Anthropometric", "Anthropometric", "Metabolic", "Cardiovascular", "Lipids"],
255
+ })
256
+
257
+ fig = plotter.plot_phewas(
258
+ phewas_df,
259
+ variant_id="rs12345",
260
+ category_col="category",
261
+ )
262
+ ```
263
+
264
+ ![Example PheWAS plot](examples/phewas_plot.png)
265
+ *PheWAS plot showing associations across phenotype categories with significance threshold.*
266
+
267
+ ## Forest Plots
268
+
269
+ Create forest plots for meta-analysis visualization:
270
+
271
+ ```python
272
+ forest_df = pd.DataFrame({
273
+ "study": ["Study A", "Study B", "Study C", "Meta-analysis"],
274
+ "effect": [0.45, 0.52, 0.38, 0.46],
275
+ "ci_lower": [0.30, 0.35, 0.20, 0.40],
276
+ "ci_upper": [0.60, 0.69, 0.56, 0.52],
277
+ "weight": [25, 35, 20, 100],
278
+ })
279
+
280
+ fig = plotter.plot_forest(
281
+ forest_df,
282
+ variant_id="rs12345",
283
+ weight_col="weight",
284
+ )
285
+ ```
286
+
287
+ ![Example forest plot](examples/forest_plot.png)
288
+ *Forest plot with effect sizes, confidence intervals, and weight-proportional markers.*
222
289
 
223
290
  ## PySpark Support
224
291
 
225
- For large-scale genomics data, pass PySpark DataFrames directly:
292
+ For large-scale genomics data, convert PySpark DataFrames with `to_pandas()` before plotting:
226
293
 
227
294
  ```python
228
295
  from pylocuszoom import LocusZoomPlotter, to_pandas
229
296
 
230
- # PySpark DataFrame (automatically converted)
231
- fig = plotter.plot(spark_gwas_df, chrom=1, start=1000000, end=2000000)
232
-
233
- # Or convert manually with sampling for very large data
297
+ # Convert PySpark DataFrame (optionally sampled for very large data)
234
298
  pandas_df = to_pandas(spark_gwas_df, sample_size=100000)
299
+ fig = plotter.plot(pandas_df, chrom=1, start=1000000, end=2000000)
235
300
  ```
236
301
 
237
302
  Install PySpark support: `uv add pylocuszoom[spark]`