pycmplot 0.3.0__tar.gz → 0.4.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 (52) hide show
  1. {pycmplot-0.3.0 → pycmplot-0.4.0}/PKG-INFO +125 -24
  2. {pycmplot-0.3.0 → pycmplot-0.4.0}/README.md +124 -23
  3. pycmplot-0.4.0/benchmark/build_composite_figure.py +199 -0
  4. pycmplot-0.4.0/docs-bak/conf.py +90 -0
  5. pycmplot-0.4.0/pycmplot/__init__.py +50 -0
  6. pycmplot-0.4.0/pycmplot/__main__.py +6 -0
  7. pycmplot-0.4.0/pycmplot/_core.py +419 -0
  8. pycmplot-0.4.0/pycmplot/annotation.py +904 -0
  9. pycmplot-0.4.0/pycmplot/cache.py +832 -0
  10. pycmplot-0.4.0/pycmplot/cli.py +614 -0
  11. pycmplot-0.4.0/pycmplot/constants.py +91 -0
  12. pycmplot-0.4.0/pycmplot/io.py +1714 -0
  13. pycmplot-0.4.0/pycmplot/liftover.py +303 -0
  14. pycmplot-0.4.0/pycmplot/plotting/__init__.py +31 -0
  15. pycmplot-0.4.0/pycmplot/plotting/circular.py +832 -0
  16. pycmplot-0.4.0/pycmplot/plotting/linear.py +1728 -0
  17. pycmplot-0.4.0/pycmplot/plotting/qq.py +938 -0
  18. pycmplot-0.4.0/pycmplot/resources.py +199 -0
  19. pycmplot-0.4.0/pycmplot/stats.py +180 -0
  20. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/__init__.py +1 -1
  21. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/_core.py +36 -7
  22. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/annotation.py +298 -0
  23. pycmplot-0.4.0/pycmplot-bak/cache.py +832 -0
  24. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/cli.py +83 -7
  25. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/io.py +479 -107
  26. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/plotting/circular.py +113 -25
  27. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/plotting/linear.py +67 -7
  28. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/plotting/qq.py +248 -26
  29. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot.egg-info/PKG-INFO +125 -24
  30. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot.egg-info/SOURCES.txt +18 -0
  31. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot.egg-info/top_level.txt +2 -0
  32. {pycmplot-0.3.0 → pycmplot-0.4.0}/pyproject.toml +1 -1
  33. {pycmplot-0.3.0 → pycmplot-0.4.0}/setup.cfg +1 -1
  34. {pycmplot-0.3.0 → pycmplot-0.4.0}/LICENSE +0 -0
  35. {pycmplot-0.3.0 → pycmplot-0.4.0}/benchmark/bench_python.py +0 -0
  36. {pycmplot-0.3.0 → pycmplot-0.4.0}/benchmark/collect_results.py +0 -0
  37. {pycmplot-0.3.0 → pycmplot-0.4.0}/benchmark/generate_multi_sumstats.py +0 -0
  38. {pycmplot-0.3.0 → pycmplot-0.4.0}/benchmark/generate_sumstats.py +0 -0
  39. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot/data/Homo_sapiens.GRCh37.geneinfo.tsv.gz +0 -0
  40. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot/data/Homo_sapiens.GRCh38.geneinfo.tsv.gz +0 -0
  41. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot/data/hg18ToHg38.over.chain.gz +0 -0
  42. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot/data/hg19ToHg38.over.chain.gz +0 -0
  43. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/__main__.py +0 -0
  44. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/constants.py +0 -0
  45. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/liftover.py +0 -0
  46. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/plotting/__init__.py +0 -0
  47. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/resources.py +0 -0
  48. {pycmplot-0.3.0/pycmplot → pycmplot-0.4.0/pycmplot-bak}/stats.py +0 -0
  49. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot.egg-info/dependency_links.txt +0 -0
  50. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot.egg-info/entry_points.txt +0 -0
  51. {pycmplot-0.3.0 → pycmplot-0.4.0}/pycmplot.egg-info/requires.txt +0 -0
  52. {pycmplot-0.3.0 → pycmplot-0.4.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycmplot
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Multi-track circular and linear Manhattan plot generation for GWAS summary statistics
5
5
  Author: Kevin Esoh
6
6
  Author-email: Kevin Esoh <kesohku1@jh.edu>
@@ -39,16 +39,60 @@ Dynamic: license-file
39
39
 
40
40
  # pycmplot
41
41
 
42
- Multi-track **circular** and **linear** Manhattan plot generation for GWAS summary statistics.
42
+ <div>
43
+ <h1 align="center">Multi-track circular and linear Manhattan plotting in Python.</h1>
44
+ </div>
45
+
46
+ <p align="center">
47
+ <img width="600" height="400" src="https://github.com/esohkevin/pycmplot/blob/main/docs/pycmplot-logo-circular.png">
48
+ </p>
49
+
43
50
 
44
51
  ```
45
- #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
46
- | PACKAGE FOR CIRCULAR AND LINEAR MANHATTAN PLOTTING |
47
- | Kevin Esoh, 2026 |
48
- | kesohku1@jh.edu |
49
- #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
52
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
53
+ | MULTI-TRACK CIRCULAR AND LINEAR MANHATTAN PLOTTING |
54
+ | in Python |
55
+ | Kevin Esoh, 2026 |
56
+ | kesohku1@jh.edu |
57
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
50
58
  ```
51
59
 
60
+ ----------------------------------------------
61
+
62
+ ## Content
63
+ 1. [Overview](#overview)
64
+ 2. [Key features](#key-features)
65
+ - [Column auto-detection](#column-auto-detection)
66
+ - [Density-aware sub-sampling](#density-aware-sub-sampling)
67
+ - [Trim insignificant variants for faster plotting](#trim-insignificant-variants-for-faster-plotting)
68
+ - [Genome build conversion (liftover)](#genome-build-conversion-liftover)
69
+ - [Nearest-gene annotation for GWAS lead SNPs](#nearest-gene-annotation-for-gwas-lead-snps)
70
+ - [Caching & warm resume](#caching--warm-resume)
71
+ - [User-editable hits overlay](#user-editable-hits-overlay)
72
+ - [Per-locus highlight colours & custom legend](#per-locus-highlight-colours--custom-legend)
73
+ - [Multi-panel canvas](#multi-panel-canvas)
74
+ 3. [Application](#application)
75
+ 4. [Tip](#tip)
76
+ 5. [Installation](#installation)
77
+ - [From PyPI](#from-pypi)
78
+ - [From GitHub](#from-github)
79
+ - [Python virtual environment](#python-virtual-environment)
80
+ 6. [Test the installation](#test-the-installation)
81
+ 7. [Dependencies](#dependencies)
82
+ 8. [Command-line usage](#command-line-usage)
83
+ - [Linear Manhattan (default)](#linear-manhattan-default)
84
+ - [Circular Manhattan](#circular-manhattan)
85
+ - [Key options](#key-options)
86
+ 9. [Python API](#python-api)
87
+ 10. [Contributing](#contributing)
88
+ 11. [Contributors](#contributors)
89
+
90
+
91
+ ---------------------------------------------
92
+
93
+
94
+ ## Overview
95
+
52
96
  This package will take any number of per SNP/variant summary statistics, be it GWAS,
53
97
  selection scans (e.g. iHS, EHH, FST), etc and generate Manhattan plots. If given a single
54
98
  file, a single one-track Manhattan plot will be generated. Multiple files will result in
@@ -68,8 +112,8 @@ option of the package should be used to indicate the column and then the package
68
112
  postions in hg19 to hg38 ensuring that hits table generation and plotting are done with one unified
69
113
  corrdinate system.
70
114
 
71
- # Key features
72
- ## Column auto-detection
115
+ ## Key features
116
+ ### Column auto-detection
73
117
  A key functionality of the package is its ability to auto-detect certain columns if ommited on the
74
118
  command-line or python API:
75
119
  - Chromosome column: `-chr, --chrom_column` or ommited
@@ -92,7 +136,7 @@ bld_candidates = [build, 'BUILD', 'Genome', 'Genome_Build', 'Genome-build']
92
136
 
93
137
  > NB: Upper and lower cases of the candidates are also considered, making each candidate expanded 3 times.
94
138
 
95
- ## Density-aware sub-sampling
139
+ ### Density-aware sub-sampling
96
140
  Another key feature is density-aware sub-sampling for Manhattan-style scatter plots.
97
141
  This was inspired by ``gwaslab``'s default behaviour (https://cloufield.github.io/gwaslab/).
98
142
 
@@ -103,14 +147,14 @@ scan with the defaults below, this typically cuts the plotted point count from 1
103
147
  to ~200 K + a few hundred peaks — visually indistinguishable above the suggestive
104
148
  band, but two orders of magnitude faster to render.
105
149
 
106
- ## Trim insignificant variants for faster plotting
150
+ ### Trim insignificant variants for faster plotting
107
151
  An optional parameter `-tp, --trim_pval` is provided to increase speed even further.
108
152
  Set with a value to exclude variants with p-value above a certain threshold,
109
153
  e.g. `0.01 (1e-2)` or `0.001 (1e-3)`. Performed on top of the default auto-thin
110
154
  feature above, it siginificant increases speed and reduces peak memory usage.
111
155
  See benchmark figure (manuscript in preparation).
112
156
 
113
- ## Genome build conversion (liftover)
157
+ ### Genome build conversion (liftover)
114
158
  Conversion of a both hg18 and hg19 positions to their hg38 equivalent is included through
115
159
  `pyliftover.LiftOver`.
116
160
 
@@ -120,7 +164,7 @@ column to specify the genome build of each position ('hg18', 'hg19', or 'hg38')
120
164
  using one coordinate system. If only 'hg18' or 'hg19' positions are present, no liftover
121
165
  be necessary. Hence, liftover is only performed in cases of mixed genome builds.
122
166
 
123
- ## Nearest-gene annotation for GWAS lead SNPs
167
+ ### Nearest-gene annotation for GWAS lead SNPs
124
168
  The package bundles GFF3 files in hg19 and hg38 coordinates processed to reduce size
125
169
  for gene annotation. Also included are UCSC chain files for coordinate conversion (liftover).
126
170
  - ``chain_hg19_hg38`` -- UCSC LiftOver chain file for hg19 to hg38
@@ -139,12 +183,52 @@ for gene annotation. Also included are UCSC chain files for coordinate conversio
139
183
  ``Homo_sapiens.GRCh37.geneinfo.tsv.gz``.
140
184
 
141
185
 
142
- # Application
186
+ ### Caching & warm resume
187
+ Loading is the expensive step (I/O + trim + liftover + lead extraction), so
188
+ pycmplot ships a per-track cache keyed on `SHA-256(raw_file_sha256 + version + Stage-1 params)`.
189
+ Warm re-runs of the same `(files, parameters)` combination complete in
190
+ milliseconds; changing any parameter transparently invalidates only the
191
+ affected tracks and regenerates them. Enable with `--cache` (CLI) or
192
+ `cache=True` (Python API); use `--clear_cache` to wipe the tree.
193
+
194
+ ```bash
195
+ pycmplot --sum_stats hb.tsv,mcv.tsv --labels Hb,MCV \
196
+ --logp --highlight --cache --cache_dir ./.pycmplot
197
+ ```
198
+
199
+ ### User-editable hits overlay
200
+ When caching is on, the auto-generated hits table is written to a
201
+ group-scoped TSV at `<cache_dir>/annotations/hits.<group_key>.tsv`
202
+ that you're *expected to hand-edit*. Rows added or changed there feed
203
+ straight back into the next plot — no re-plumbing required. Each row
204
+ carries `source` (`auto` vs `user`), `highlight_color`, and `category`
205
+ columns; user edits are inherited across cache regenerations by
206
+ `(CHR, POS)` lookup, so re-running with a new parameter never loses
207
+ your annotations.
208
+
209
+ ### Per-locus highlight colours & custom legend
210
+ Set the `highlight_color` column on any row of the hits overlay to a
211
+ matplotlib-parseable colour (name, `#rrggbb`, or an RGB tuple) to give
212
+ that locus its own highlight colour; leave `auto` to fall back to the
213
+ plot-wide `--highlight_color`. Set the `category` column
214
+ (e.g. `novel`, `replicated`, `MHC`) to group loci in a
215
+ **"Highlighted Categories"** legend that both the linear and circular
216
+ plotters render automatically. If nothing is edited, no legend is
217
+ added — the pre-feature layout is preserved.
218
+
219
+ ### Multi-panel canvas
220
+ Place multiple *groups* of sumstats on the same figure by passing an
221
+ explicit matplotlib `Axes` or `SubFigure` via `ax=` to the plotter.
222
+ Cache files and hits overlays are group-scoped, so two panels with
223
+ different sumstats never clobber each other's artefacts.
224
+
225
+
226
+ ## Application
143
227
  A potential useful application is **comparative visualization** of results from multiple imputation panels,
144
228
  multiple populations, or multiple traits to observe shared genetic architecture.
145
229
 
146
230
 
147
- # Tip
231
+ ## Tip
148
232
 
149
233
  For signed non-pvalue statistics such as iHS, use `--signif_threshold` and
150
234
  `--suggest_threshold` to specify lines to draw for iHS cutoff above and below zero,
@@ -153,7 +237,7 @@ that is in the positive and negative axes.
153
237
 
154
238
  Read more in the package documentation page: https://pycmplot.readthedocs.io/en/latest/
155
239
 
156
- ---
240
+ ----------------------------
157
241
 
158
242
  ## Installation
159
243
 
@@ -180,24 +264,25 @@ pip install -e . --break-system-packages
180
264
  ```
181
265
 
182
266
 
183
- ### Use python virtual environment if local installation is not possible
184
- ```bash
185
- python -m venv ~/bin/pycmplot
267
+ ### Python virtual environment
186
268
 
187
- source ~/bin/pycmplot/bin/activate
269
+ Use python virtual environment if local installation is not possible
188
270
 
189
- pip install --upgrade pip setuptools wheel
271
+ ```bash
272
+ python -m venv ~/envs/pycmplot
273
+ source ~/envs/pycmplot/bin/activate
190
274
 
191
- # then follow any of the installation steps above
275
+ pip install --upgrade pip setuptools wheel
276
+ pip install pycmplot
192
277
  ```
193
278
 
194
279
 
195
- # Test the installation
280
+ ## Test the installation
196
281
  ```bash
197
282
  pycmplot -h
198
283
  ```
199
284
 
200
- ### Dependencies
285
+ ## Dependencies
201
286
 
202
287
  | Package | Purpose |
203
288
  |---------|---------|
@@ -264,6 +349,16 @@ pycmplot \
264
349
  | `-st, --sort_track` | Sort tracks by `label` or `chrom_len` | input order |
265
350
  | `-od, --output_dir` | Output directory | `.` |
266
351
  | `-of, --output_format` | Output format (`png`, `pdf`, `svg`, `jpg`) | `png` |
352
+ | `--cache` | Enable per-track cache + user-editable hits overlay | off |
353
+ | `--cache_dir` | Where to store cache artefacts | `./.pycmplot` |
354
+ | `--no_resume` | Regenerate but still write fresh cache entries | resume on |
355
+ | `--clear_cache` | Delete the cache tree and exit | off |
356
+ | `-V, --version` | Print version and exit | — |
357
+
358
+ > **QQ note (0.4.0+):** the loader no longer materialises the full sorted
359
+ > p-value array by default. `-qq/--qq_plot` sets `compute_pvals=True`
360
+ > automatically on the CLI; Python-API callers must pass it explicitly
361
+ > to feed `bundle['pvals']` into a QQ plotter.
267
362
 
268
363
  Run `pycmplot -h` for the full option list.
269
364
 
@@ -273,6 +368,12 @@ Run `pycmplot -h` for the full option list.
273
368
 
274
369
  A demonstration of how to use the python API is provided in this notebook: https://github.com/esohkevin/pycmplot/blob/main/pycmplot_python_api.ipynb
275
370
 
371
+ For an end-to-end walkthrough of every feature (caching, hits overlay,
372
+ per-locus colours & categories, multi-panel canvas, mixed-build
373
+ liftover, QQ opt-in), see the
374
+ [Tutorial](https://pycmplot.readthedocs.io/en/latest/tutorial.html)
375
+ page in the docs.
376
+
276
377
 
277
378
  ---
278
379
 
@@ -1,15 +1,59 @@
1
1
  # pycmplot
2
2
 
3
- Multi-track **circular** and **linear** Manhattan plot generation for GWAS summary statistics.
3
+ <div>
4
+ <h1 align="center">Multi-track circular and linear Manhattan plotting in Python.</h1>
5
+ </div>
6
+
7
+ <p align="center">
8
+ <img width="600" height="400" src="https://github.com/esohkevin/pycmplot/blob/main/docs/pycmplot-logo-circular.png">
9
+ </p>
10
+
4
11
 
5
12
  ```
6
- #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
7
- | PACKAGE FOR CIRCULAR AND LINEAR MANHATTAN PLOTTING |
8
- | Kevin Esoh, 2026 |
9
- | kesohku1@jh.edu |
10
- #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
13
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
14
+ | MULTI-TRACK CIRCULAR AND LINEAR MANHATTAN PLOTTING |
15
+ | in Python |
16
+ | Kevin Esoh, 2026 |
17
+ | kesohku1@jh.edu |
18
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#
11
19
  ```
12
20
 
21
+ ----------------------------------------------
22
+
23
+ ## Content
24
+ 1. [Overview](#overview)
25
+ 2. [Key features](#key-features)
26
+ - [Column auto-detection](#column-auto-detection)
27
+ - [Density-aware sub-sampling](#density-aware-sub-sampling)
28
+ - [Trim insignificant variants for faster plotting](#trim-insignificant-variants-for-faster-plotting)
29
+ - [Genome build conversion (liftover)](#genome-build-conversion-liftover)
30
+ - [Nearest-gene annotation for GWAS lead SNPs](#nearest-gene-annotation-for-gwas-lead-snps)
31
+ - [Caching & warm resume](#caching--warm-resume)
32
+ - [User-editable hits overlay](#user-editable-hits-overlay)
33
+ - [Per-locus highlight colours & custom legend](#per-locus-highlight-colours--custom-legend)
34
+ - [Multi-panel canvas](#multi-panel-canvas)
35
+ 3. [Application](#application)
36
+ 4. [Tip](#tip)
37
+ 5. [Installation](#installation)
38
+ - [From PyPI](#from-pypi)
39
+ - [From GitHub](#from-github)
40
+ - [Python virtual environment](#python-virtual-environment)
41
+ 6. [Test the installation](#test-the-installation)
42
+ 7. [Dependencies](#dependencies)
43
+ 8. [Command-line usage](#command-line-usage)
44
+ - [Linear Manhattan (default)](#linear-manhattan-default)
45
+ - [Circular Manhattan](#circular-manhattan)
46
+ - [Key options](#key-options)
47
+ 9. [Python API](#python-api)
48
+ 10. [Contributing](#contributing)
49
+ 11. [Contributors](#contributors)
50
+
51
+
52
+ ---------------------------------------------
53
+
54
+
55
+ ## Overview
56
+
13
57
  This package will take any number of per SNP/variant summary statistics, be it GWAS,
14
58
  selection scans (e.g. iHS, EHH, FST), etc and generate Manhattan plots. If given a single
15
59
  file, a single one-track Manhattan plot will be generated. Multiple files will result in
@@ -29,8 +73,8 @@ option of the package should be used to indicate the column and then the package
29
73
  postions in hg19 to hg38 ensuring that hits table generation and plotting are done with one unified
30
74
  corrdinate system.
31
75
 
32
- # Key features
33
- ## Column auto-detection
76
+ ## Key features
77
+ ### Column auto-detection
34
78
  A key functionality of the package is its ability to auto-detect certain columns if ommited on the
35
79
  command-line or python API:
36
80
  - Chromosome column: `-chr, --chrom_column` or ommited
@@ -53,7 +97,7 @@ bld_candidates = [build, 'BUILD', 'Genome', 'Genome_Build', 'Genome-build']
53
97
 
54
98
  > NB: Upper and lower cases of the candidates are also considered, making each candidate expanded 3 times.
55
99
 
56
- ## Density-aware sub-sampling
100
+ ### Density-aware sub-sampling
57
101
  Another key feature is density-aware sub-sampling for Manhattan-style scatter plots.
58
102
  This was inspired by ``gwaslab``'s default behaviour (https://cloufield.github.io/gwaslab/).
59
103
 
@@ -64,14 +108,14 @@ scan with the defaults below, this typically cuts the plotted point count from 1
64
108
  to ~200 K + a few hundred peaks — visually indistinguishable above the suggestive
65
109
  band, but two orders of magnitude faster to render.
66
110
 
67
- ## Trim insignificant variants for faster plotting
111
+ ### Trim insignificant variants for faster plotting
68
112
  An optional parameter `-tp, --trim_pval` is provided to increase speed even further.
69
113
  Set with a value to exclude variants with p-value above a certain threshold,
70
114
  e.g. `0.01 (1e-2)` or `0.001 (1e-3)`. Performed on top of the default auto-thin
71
115
  feature above, it siginificant increases speed and reduces peak memory usage.
72
116
  See benchmark figure (manuscript in preparation).
73
117
 
74
- ## Genome build conversion (liftover)
118
+ ### Genome build conversion (liftover)
75
119
  Conversion of a both hg18 and hg19 positions to their hg38 equivalent is included through
76
120
  `pyliftover.LiftOver`.
77
121
 
@@ -81,7 +125,7 @@ column to specify the genome build of each position ('hg18', 'hg19', or 'hg38')
81
125
  using one coordinate system. If only 'hg18' or 'hg19' positions are present, no liftover
82
126
  be necessary. Hence, liftover is only performed in cases of mixed genome builds.
83
127
 
84
- ## Nearest-gene annotation for GWAS lead SNPs
128
+ ### Nearest-gene annotation for GWAS lead SNPs
85
129
  The package bundles GFF3 files in hg19 and hg38 coordinates processed to reduce size
86
130
  for gene annotation. Also included are UCSC chain files for coordinate conversion (liftover).
87
131
  - ``chain_hg19_hg38`` -- UCSC LiftOver chain file for hg19 to hg38
@@ -100,12 +144,52 @@ for gene annotation. Also included are UCSC chain files for coordinate conversio
100
144
  ``Homo_sapiens.GRCh37.geneinfo.tsv.gz``.
101
145
 
102
146
 
103
- # Application
147
+ ### Caching & warm resume
148
+ Loading is the expensive step (I/O + trim + liftover + lead extraction), so
149
+ pycmplot ships a per-track cache keyed on `SHA-256(raw_file_sha256 + version + Stage-1 params)`.
150
+ Warm re-runs of the same `(files, parameters)` combination complete in
151
+ milliseconds; changing any parameter transparently invalidates only the
152
+ affected tracks and regenerates them. Enable with `--cache` (CLI) or
153
+ `cache=True` (Python API); use `--clear_cache` to wipe the tree.
154
+
155
+ ```bash
156
+ pycmplot --sum_stats hb.tsv,mcv.tsv --labels Hb,MCV \
157
+ --logp --highlight --cache --cache_dir ./.pycmplot
158
+ ```
159
+
160
+ ### User-editable hits overlay
161
+ When caching is on, the auto-generated hits table is written to a
162
+ group-scoped TSV at `<cache_dir>/annotations/hits.<group_key>.tsv`
163
+ that you're *expected to hand-edit*. Rows added or changed there feed
164
+ straight back into the next plot — no re-plumbing required. Each row
165
+ carries `source` (`auto` vs `user`), `highlight_color`, and `category`
166
+ columns; user edits are inherited across cache regenerations by
167
+ `(CHR, POS)` lookup, so re-running with a new parameter never loses
168
+ your annotations.
169
+
170
+ ### Per-locus highlight colours & custom legend
171
+ Set the `highlight_color` column on any row of the hits overlay to a
172
+ matplotlib-parseable colour (name, `#rrggbb`, or an RGB tuple) to give
173
+ that locus its own highlight colour; leave `auto` to fall back to the
174
+ plot-wide `--highlight_color`. Set the `category` column
175
+ (e.g. `novel`, `replicated`, `MHC`) to group loci in a
176
+ **"Highlighted Categories"** legend that both the linear and circular
177
+ plotters render automatically. If nothing is edited, no legend is
178
+ added — the pre-feature layout is preserved.
179
+
180
+ ### Multi-panel canvas
181
+ Place multiple *groups* of sumstats on the same figure by passing an
182
+ explicit matplotlib `Axes` or `SubFigure` via `ax=` to the plotter.
183
+ Cache files and hits overlays are group-scoped, so two panels with
184
+ different sumstats never clobber each other's artefacts.
185
+
186
+
187
+ ## Application
104
188
  A potential useful application is **comparative visualization** of results from multiple imputation panels,
105
189
  multiple populations, or multiple traits to observe shared genetic architecture.
106
190
 
107
191
 
108
- # Tip
192
+ ## Tip
109
193
 
110
194
  For signed non-pvalue statistics such as iHS, use `--signif_threshold` and
111
195
  `--suggest_threshold` to specify lines to draw for iHS cutoff above and below zero,
@@ -114,7 +198,7 @@ that is in the positive and negative axes.
114
198
 
115
199
  Read more in the package documentation page: https://pycmplot.readthedocs.io/en/latest/
116
200
 
117
- ---
201
+ ----------------------------
118
202
 
119
203
  ## Installation
120
204
 
@@ -141,24 +225,25 @@ pip install -e . --break-system-packages
141
225
  ```
142
226
 
143
227
 
144
- ### Use python virtual environment if local installation is not possible
145
- ```bash
146
- python -m venv ~/bin/pycmplot
228
+ ### Python virtual environment
147
229
 
148
- source ~/bin/pycmplot/bin/activate
230
+ Use python virtual environment if local installation is not possible
149
231
 
150
- pip install --upgrade pip setuptools wheel
232
+ ```bash
233
+ python -m venv ~/envs/pycmplot
234
+ source ~/envs/pycmplot/bin/activate
151
235
 
152
- # then follow any of the installation steps above
236
+ pip install --upgrade pip setuptools wheel
237
+ pip install pycmplot
153
238
  ```
154
239
 
155
240
 
156
- # Test the installation
241
+ ## Test the installation
157
242
  ```bash
158
243
  pycmplot -h
159
244
  ```
160
245
 
161
- ### Dependencies
246
+ ## Dependencies
162
247
 
163
248
  | Package | Purpose |
164
249
  |---------|---------|
@@ -225,6 +310,16 @@ pycmplot \
225
310
  | `-st, --sort_track` | Sort tracks by `label` or `chrom_len` | input order |
226
311
  | `-od, --output_dir` | Output directory | `.` |
227
312
  | `-of, --output_format` | Output format (`png`, `pdf`, `svg`, `jpg`) | `png` |
313
+ | `--cache` | Enable per-track cache + user-editable hits overlay | off |
314
+ | `--cache_dir` | Where to store cache artefacts | `./.pycmplot` |
315
+ | `--no_resume` | Regenerate but still write fresh cache entries | resume on |
316
+ | `--clear_cache` | Delete the cache tree and exit | off |
317
+ | `-V, --version` | Print version and exit | — |
318
+
319
+ > **QQ note (0.4.0+):** the loader no longer materialises the full sorted
320
+ > p-value array by default. `-qq/--qq_plot` sets `compute_pvals=True`
321
+ > automatically on the CLI; Python-API callers must pass it explicitly
322
+ > to feed `bundle['pvals']` into a QQ plotter.
228
323
 
229
324
  Run `pycmplot -h` for the full option list.
230
325
 
@@ -234,6 +329,12 @@ Run `pycmplot -h` for the full option list.
234
329
 
235
330
  A demonstration of how to use the python API is provided in this notebook: https://github.com/esohkevin/pycmplot/blob/main/pycmplot_python_api.ipynb
236
331
 
332
+ For an end-to-end walkthrough of every feature (caching, hits overlay,
333
+ per-locus colours & categories, multi-panel canvas, mixed-build
334
+ liftover, QQ opt-in), see the
335
+ [Tutorial](https://pycmplot.readthedocs.io/en/latest/tutorial.html)
336
+ page in the docs.
337
+
237
338
 
238
339
  ---
239
340
 
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """Build the composite scaling figure used in the manuscript.
3
+
4
+ Output: ``benchmark_composite_scaling.png`` (and an accompanying PDF), written
5
+ to the directory passed via ``--outdir`` (default: same directory as this
6
+ script).
7
+
8
+ Layout (2 rows x 3 columns):
9
+
10
+ Top row -- single-track wall-time scaling at 500K-10M variants:
11
+ (A) Manhattan
12
+ (B) Circular Manhattan
13
+ (C) QQ
14
+
15
+ Bottom row -- multi-track wall-time scaling at 1M and 2M variants
16
+ (multi-track benchmarks were only collected at 1M / 2M):
17
+ (D) Multi-track Manhattan (pycmplot vs CMplot)
18
+ (E) Multi-track circular Manhattan (pycmplot vs CMplot)
19
+ (F) Shared legend
20
+
21
+ The figure reuses :data:`collect_results.SERIES_STYLE` so styling stays
22
+ consistent with the per-plot-type PDFs produced by
23
+ ``collect_results.py --plot``.
24
+
25
+ Usage
26
+ -----
27
+ ::
28
+
29
+ cd benchmark
30
+ python collect_results.py --resultsdir results --out results/summary.csv
31
+ python build_composite_figure.py # writes alongside this script
32
+ python build_composite_figure.py --outdir ../figures # custom output dir
33
+ python build_composite_figure.py --summary path/to/summary.csv
34
+ """
35
+ from __future__ import annotations
36
+
37
+ import argparse
38
+ import os
39
+ import sys
40
+ from pathlib import Path
41
+
42
+ import matplotlib
43
+ matplotlib.use("Agg")
44
+ import matplotlib.pyplot as plt
45
+ import pandas as pd
46
+
47
+ # Resolve ``collect_results`` relative to this script so the figure builder
48
+ # works regardless of the current working directory.
49
+ SCRIPT_DIR = Path(__file__).resolve().parent
50
+ sys.path.insert(0, str(SCRIPT_DIR))
51
+
52
+ from collect_results import SERIES_STYLE, SIZE_N # noqa: E402
53
+
54
+
55
+ PANELS = [
56
+ # (row, col, letter, plot_type, title)
57
+ (0, 0, "A", "manhattan", "Manhattan plot"),
58
+ (0, 1, "B", "circular", "Circular Manhattan plot"),
59
+ (0, 2, "C", "qq", "QQ plot"),
60
+ (1, 0, "D", "multitrack_manhattan", "Multi-track Manhattan plot"),
61
+ (1, 1, "E", "multitrack_circular", "Multi-track circular Manhattan plot"),
62
+ ]
63
+
64
+
65
+ def build(summary_path: Path, outdir: Path, basename: str = "benchmark_composite_scaling") -> Path:
66
+ """Render the composite figure and return the path to the PNG output."""
67
+ summary = pd.read_csv(summary_path)
68
+
69
+ #fig, axes = plt.subplots(2, 3, figsize=(13, 7.5))
70
+ fig, axes = plt.subplots(2, 3, figsize=(6, 3.5))
71
+ plt.subplots_adjust(
72
+ top=0.94, bottom=0.07, left=0.06, right=0.98, wspace=0.30, hspace=0.45,
73
+ )
74
+
75
+ all_handles: list = []
76
+ all_labels: list = []
77
+ seen: set = set()
78
+
79
+ for r, c, letter, pt, title in PANELS:
80
+ ax = axes[r, c]
81
+ sub = summary[summary["plot_type"] == pt].copy()
82
+ sub["n_approx"] = sub["size_label"].map(SIZE_N)
83
+ sub = sub.dropna(subset=["n_approx"])
84
+
85
+ tools_in_data = list(sub["tool"].unique())
86
+ ordered_tools = (
87
+ [t for t in SERIES_STYLE if t in tools_in_data]
88
+ + [t for t in tools_in_data if t not in SERIES_STYLE]
89
+ )
90
+
91
+ for tool in ordered_tools:
92
+ tdf = sub[sub["tool"] == tool].sort_values("n_approx")
93
+ if tdf.empty:
94
+ continue
95
+ s = SERIES_STYLE.get(
96
+ tool, dict(label=tool, color="#888780", ls="-", marker="o"),
97
+ )
98
+ line, = ax.plot(
99
+ tdf["n_approx"], tdf["wall_time_mean"],
100
+ marker=s["marker"], linestyle=s["ls"], color=s["color"],
101
+ label=s["label"] or tool,
102
+ linewidth=1, markersize=2,
103
+ )
104
+ ax.fill_between(
105
+ tdf["n_approx"],
106
+ tdf["wall_time_mean"] - tdf["wall_time_sd"].fillna(0),
107
+ tdf["wall_time_mean"] + tdf["wall_time_sd"].fillna(0),
108
+ alpha=0.12, color=s["color"], linewidth=0,
109
+ )
110
+ label = s["label"] or tool
111
+ if label not in seen:
112
+ all_handles.append(line)
113
+ all_labels.append(label)
114
+ seen.add(label)
115
+
116
+ ax.set_xscale("log")
117
+ ax.set_yscale("log")
118
+ ax.set_xlabel("Number of variants", fontsize=7) #fontsize=10
119
+ if c == 0:
120
+ ax.set_ylabel("Wall-clock time (s)", fontsize=7) #fontsize=10
121
+ ax.set_title(f"({letter}) {title}", fontsize=7, loc="left") #fontsize=10
122
+ ax.grid(True, which="both", linestyle="--", linewidth=0.3, alpha=0.5)
123
+ ax.tick_params(labelsize=5)
124
+
125
+ # Bottom-right cell (1, 2) holds the shared legend so each axes stays
126
+ # uncluttered.
127
+ legend_ax = axes[1, 2]
128
+ legend_ax.axis("off")
129
+ legend_ax.legend(
130
+ all_handles, all_labels,
131
+ loc="center", ncol=1, fontsize=5, frameon=False,
132
+ title="Tool", title_fontsize=5,
133
+ )
134
+
135
+ outdir.mkdir(parents=True, exist_ok=True)
136
+ png_path = outdir / f"{basename}.png"
137
+ pdf_path = outdir / f"{basename}.pdf"
138
+ tif_path = outdir / f"{basename}.TIF"
139
+ #eps_path = outdir / f"{basename}.eps"
140
+ fig.savefig(png_path, dpi=300, bbox_inches="tight")
141
+ fig.savefig(tif_path, dpi=300, bbox_inches="tight")
142
+ #fig.savefig(eps_path, dpi=300, bbox_inches="tight")
143
+ fig.savefig(pdf_path, bbox_inches="tight")
144
+ plt.close(fig)
145
+
146
+ print(f"Wrote {png_path}")
147
+ print(f"Wrote {tif_path}")
148
+ #print(f"Wrote {eps_path}")
149
+ print(f"Wrote {pdf_path}")
150
+ return png_path
151
+
152
+
153
+ def main() -> None:
154
+ parser = argparse.ArgumentParser(
155
+ description="Build the composite scaling figure for the manuscript.",
156
+ )
157
+ parser.add_argument(
158
+ "--summary",
159
+ type=Path,
160
+ default=None,
161
+ help=(
162
+ "Path to summary.csv produced by collect_results.py. "
163
+ "Default: <script_dir>/summary.csv, then <script_dir>/results/summary.csv."
164
+ ),
165
+ )
166
+ parser.add_argument(
167
+ "--outdir",
168
+ type=Path,
169
+ default=SCRIPT_DIR,
170
+ help="Output directory for the figure files (default: this script's directory).",
171
+ )
172
+ parser.add_argument(
173
+ "--basename",
174
+ type=str,
175
+ default="benchmark_composite_scaling",
176
+ help="Output file basename (without extension). Default: benchmark_composite_scaling.",
177
+ )
178
+ args = parser.parse_args()
179
+
180
+ if args.summary is None:
181
+ candidates = [SCRIPT_DIR / "summary.csv", SCRIPT_DIR / "results" / "summary.csv"]
182
+ for c in candidates:
183
+ if c.exists():
184
+ args.summary = c
185
+ break
186
+ else:
187
+ sys.exit(
188
+ "Could not locate summary.csv. Pass --summary or run "
189
+ "`python collect_results.py --resultsdir results --out results/summary.csv` first."
190
+ )
191
+
192
+ if not args.summary.exists():
193
+ sys.exit(f"summary file not found: {args.summary}")
194
+
195
+ build(args.summary, args.outdir, basename=args.basename)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()