This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
gwasplot is an R package for visualizing and annotating GWAS summary statistics.
It targets output from regenie or SAIGE run on WGS data with many rare
variants, so it is built around DuckDB: data is read from parquet/CSV and
manipulated lazily in the database rather than collected into R memory. This is
the central design constraint — prefer keeping work in DuckDB/dbplyr and only
collect() when a step genuinely needs in-memory data (e.g. handing a frame to
ggplot).
Dependencies are managed with renv (renv.lock). Run from the repo root:
R -q -e "renv::restore()" # install locked deps
R -q -e "devtools::load_all()" # load package for interactive work
R -q -e "devtools::document()" # regenerate NAMESPACE + man/*.Rd from roxygen
R -q -e "devtools::test()" # run the testthat suite
R -q -e "devtools::check()" # full R CMD checkRun a single test file:
R -q -e 'devtools::load_all(); testthat::test_file("tests/testthat/test-meta_analysis.R")'C++ (Rcpp) lives in src/ (qqplot.cpp). After editing it, regenerate the
glue with R -q -e "Rcpp::compileAttributes()" then devtools::load_all() /
devtools::document().
Quick syntax check without loading deps (useful when a needed package is missing
from the renv): Rscript --no-save -e "parse('R/foo.R')".
Code is formatted with air (config in air.toml: 80-col, 2-space indent).
Format before committing:
air format R/ # whole package
air format --check R/ # CI-style check, non-zero exit if unformattedKeep formatting-only changes in their own commit, separate from behavior changes
— a blanket reformat otherwise buries real diffs and churns git blame.
reformat_summary_statistics(file_path) (in R/reformat.R) is the main entry
point. It returns a GWASFormatter R6 object that wraps:
$con— the DuckDB connection (db_connect()openslocal.duckdb)$data— a lazy dbplyrtblover the current active table$table_name/$source_table_name— the active and original table names
On construction it detects the input format (detect_format()), standardizes
column names (reformat_names() via reformat_lookup()), undoes LOG10P, maps
chr23→chrX, builds an ID (CHROM_POS_REF_ALT), and materializes its own
uniquely-named DuckDB table. The unique naming is deliberate: multiple
GWASFormatter objects can coexist in one working directory without clobbering a
shared summary_stats table.
Standard columns after reformatting: CHROM, POS, REF, ALT, ID, PVALUE, BETA, SE (plus annotation columns added downstream — see the memory index for
the full column-provenance map).
Almost every user-facing function is an S3 generic with three methods:
.GWASFormatter, .data.frame, and .tbl_df. The convention:
.tbl_dfdelegates to.data.frame..GWASFormattereither runs the work in DuckDB (for scale) orcollect()s and delegates to.data.frame.
When adding functionality, follow this pattern and register all three methods.
NAMESPACE is regenerated by devtools::document() from roxygen #' tags —
do not hand-edit it (likewise man/*.Rd).
The QC/filter pipeline — filter_variants(), exclude_difficult_regions() —
mutates the GWASFormatter in place. Each step builds a lazy dbplyr query
(semi/anti-joins against whitelist parquet or packaged BED region datasets) and
calls materialize_gwas_tbl_() (R/db.R), which compute()s the result into a
new DuckDB table and repoints $data / $table_name. So $table_name always
reflects the current filtered state, and filters are cumulative.
write_summary_statistics() (R/write.R) persists the result: the
.GWASFormatter method renders the active lazy query with dbplyr::sql_render()
and streams to disk via DuckDB COPY (...) TO, so applied filters are captured
without collecting into R. In-memory methods write through a throwaway
in-memory DuckDB connection (never touching local.duckdb).
data/*.rda holds reference datasets (difficult-region BEDs like hg19diff,
UCSC_unusual, GRC_exclusions, GIAB_difficult_regions; cytoband ideogram;
gene models). They are loaded on demand with data(list = ...). Build scripts
live in data-raw/. Note BED regions are 0-based and converted to 1-based
(start + 1) before joining.
R/annotate.R covers nearest-gene assignment, locus identification, and
Open Targets locus-to-gene (annotate_with_l2g()) via either per-variant GraphQL
or bulk parquet pulled from the OT FTP with a local cache. R/meta_analysis.R
does fixed-effects meta-analysis (meta_analyze_fe()), which requires the DuckDB
stochastic community extension for accurate extreme-tail p-values.