---
title: "ggwas: Modern GWAS Visualizations"
author:
  - name: Bartosz Czech
    email: bartosz.w.czech@gmail.com
date: "`r Sys.Date()`"
package: ggwas
abstract: >
  The ggwas package provides a comprehensive, ggplot2-based toolkit for
  visualizing genome-wide association study results. It covers a range
  of GWAS plots, from standard Manhattan and QQ plots to novel
  visualizations like genome-wide heatmaps, enrichment overlays, and
  multi-trait comparisons. All functions return ggplot objects, making them
  fully composable with the tidyverse graphics ecosystem. This vignette
  demonstrates the main functionality with worked examples.
output:
  BiocStyle::html_document:
    toc: true
    toc_depth: 3
    number_sections: true
    toc_float: true
vignette: >
  %\VignetteIndexEntry{ggwas: Modern GWAS Visualizations}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r vig-01, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 6.5,
  fig.height = 4,
  out.width = "100%",
  dpi = 72
)
```

# Introduction

Genome-wide association studies (GWAS) generate large amounts of summary
statistics that require effective visualization for interpretation and
publication. The standard tools in R (`qqman`, `CMplot`) rely on base R
graphics and offer limited customization. Modern genomics workflows benefit
from a plotting system that integrates with ggplot2. This allows layered
customization, journal-specific theming, and composition of
multi-panel figures.

`ggwas` was designed with three goals:

1. **Broad coverage**: 20 plot types in one package, including views not
   found in other GWAS plotting packages.
2. **Publication-ready defaults**: sensible colors, proper axis formatting,
   and built-in journal themes so plots look good without tweaking.
3. **Performance at scale**: smart downsampling handles 10M+ variant datasets
   without manual intervention.

The package reads output from PLINK, REGENIE, GCTA, GEMMA, and any generic
tab-separated file. All plot functions return standard ggplot objects, so
you can add layers, override themes, or combine panels with `patchwork`
exactly as you would with any other ggplot.

# Getting started

## Installation

```{r vig-02, eval=FALSE}
# From GitHub
pak::pak("bczech/ggwas")

# Once available on Bioconductor:
# BiocManager::install("ggwas")
```

## Reading data

The fastest way to load GWAS results is `read_gwas_table()`, which
auto-detects common column naming conventions (CHR/CHROM/#CHROM,
BP/POS/GENPOS, P/PVALUE/LOG10P, etc.):

```{r vig-08}
library(ggwas)
library(ggplot2)

data(example_gwas)
example_gwas
```

The result is a `gwas_data` object: a data.frame with validated columns
and a custom print method showing key QC metrics at a glance.

For specific tools, dedicated readers handle format quirks automatically:

```{r vig-03, eval=FALSE}
gwas <- read_gwas_table("results.txt")       # auto-detect
gwas <- read_plink_assoc("results.assoc")    # PLINK
gwas <- read_plink_linear("results.assoc.linear")
gwas <- read_regenie("results.regenie")      # REGENIE (handles LOG10P)
gwas <- read_gcta_mlma("results.mlma")       # GCTA
gwas <- read_gemma("results.assoc.txt")      # GEMMA
```

You can also pass any `data.frame` directly, specifying non-standard
column names:

```{r vig-04, eval=FALSE}
manhattan_plot(my_df, chr = "chrom", bp = "position", p = "pval")
```

# Core plots

## Manhattan plot

The Manhattan plot is the most common GWAS visualization. Each point is a
tested variant, plotted by genomic position (x) against its association
strength as -log10(p-value) (y). Chromosomes alternate in color for visual
separation.

**How to interpret:** Look for "towers" of points rising above the
genome-wide significance line (red dashed, p = 5×10⁻⁸). Each tower is a
locus with evidence of true association. Isolated points above the line are
worth investigating but may be artifacts. A broad signal spanning multiple
chromosomes equally (rather than discrete peaks) can indicate unresolved
population stratification. Check the QQ plot in that case.

```{r manhattan-basic}
manhattan_plot(example_gwas)
```

Label the strongest signals and switch to a different palette:

```{r manhattan-custom}
manhattan_plot(
  example_gwas,
  label_top_n = 5,
  colors = gwas_palette("vibrant")
)
```

### Broken y-axis for extreme p-values

When a few loci have extremely low p-values (e.g., 1e-50), they compress
the rest of the plot. `y_truncate` breaks the y-axis: the region between
the two values is cut out, and a break symbol (//) marks the transition:

```{r manhattan-truncate, fig.height=4}
# Add extreme p-values for demonstration
gwas_extreme <- example_gwas
gwas_extreme$P[1:2] <- c(1e-50, 1e-30)

# Cut the region between 15 and 45 on the -log10(p) scale
manhattan_plot(gwas_extreme, y_truncate = c(15, 45), label_top_n = 3)
```

A single value auto-detects the resume point from the data:

```{r manhattan-truncate-auto, fig.height=4}
manhattan_plot(gwas_extreme, y_truncate = 15, label_top_n = 3)
```

### Significance thresholds

The genome-wide and suggestive significance lines are fully customizable.
The default 5e-8 assumes a standard human GWAS with ~1M independent tests,
but the correct threshold depends on the number of variants, correction
method, and organism:

```{r manhattan-thresholds, fig.height=4}
# Bonferroni for 500k SNPs
manhattan_plot(example_gwas, genome_wide = 0.05 / 500000, suggestive = 1e-4)
```

```{r manhattan-no-lines, fig.height=4}
# No threshold lines
manhattan_plot(example_gwas, genome_wide = NULL, suggestive = NULL)
```

### Effect-size confidence bound

For datasets with extreme p-values (e.g. biobank GWAS), small p-values
do not always imply large, robust effects: rare alleles with perfect
case/control assortment can produce astronomical p-values despite
negligible or unreplicable effects. `y_metric = "beta_min"` plots the
lower bound of the effect-size confidence interval: |beta| - 2*SE.
Variants whose CI overlaps zero are excluded, revealing only those with
large, robust effects:

```{r manhattan-beta-min, fig.height=7}
set.seed(42)
n <- 5000
sim <- data.frame(
  CHR = sample(1:22, n, replace = TRUE),
  BP = sample(1:250e6, n),
  SNP = paste0("rs", seq_len(n)),
  P = runif(n)^3,
  BETA = rnorm(n, 0, 0.02),
  SE = abs(rnorm(n, 0.03, 0.005))
)
real <- sample(n, 15)
sim$P[real] <- 10^(-runif(15, 5, 12))
sim$BETA[real] <- rnorm(15, 0, 0.5)
sim$SE[real] <- runif(15, 0.01, 0.03)
fake <- sample(setdiff(seq_len(n), real), 10)
sim$P[fake] <- 10^(-runif(10, 8, 15))
sim$BETA[fake] <- rnorm(10, 0, 0.05)
sim$SE[fake] <- runif(10, 0.08, 0.2)

sim_gwas <- as_gwas_data(sim)

library(patchwork)
p1 <- manhattan_plot(sim_gwas) + ggtitle("Standard: -log10(p)")
p2 <- manhattan_plot(sim_gwas, y_metric = "beta_min", label_top_n = 3) +
  ggtitle("Effect confidence: |beta| - 2*SE")
p1 / p2
```

Many hits visible in the standard plot disappear in the effect-confidence
view because their large SE makes the confidence interval overlap zero,
a hallmark of artifacts from rare variants.

### Palette and theme variations

The same Manhattan plot can look very different with alternative palettes
and themes, which is useful when matching journal style guides:

```{r manhattan-themes, fig.height=4}
manhattan_plot(example_gwas, colors = gwas_palette("lancet")) + theme_science()
```

```{r manhattan-cell, fig.height=4}
manhattan_plot(example_gwas, colors = gwas_palette("nejm")) + theme_cell()
```

## QQ plot

The quantile-quantile plot compares observed p-value distribution against the
expectation under the null hypothesis.

**How to interpret:** Points should follow the diagonal closely in the lower
left (bulk of non-associated variants) and only deviate upward in the tail
(true signals). If points lift off the diagonal early and uniformly, that
indicates genomic inflation, either population stratification or cryptic
relatedness in your sample. The grey band is the 95% confidence interval
under the null: points inside it are consistent with no association.
λ_GC > 1.05 in a well-powered GWAS with polygenic signal is expected;
λ_GC > 1.2 without a clear polygenic explanation warrants investigation.

```{r qq}
qq_plot(example_gwas, show_lambda = TRUE, ci = 0.95)
```

Stratifying by minor allele frequency helps diagnose whether inflation is
driven by rare variants (common in imputed data) or is uniform:

```{r qq-stratified}
example_gwas$MAF_bin <- cut(
  example_gwas$AF,
  breaks = c(0, 0.05, 0.2, 0.5),
  labels = c("Rare (<5%)", "Low (5-20%)", "Common (>20%)")
)
qq_plot(example_gwas, group = "MAF_bin")
```

## Miami plot

The Miami plot mirrors two Manhattan plots vertically: one pointing up, one
pointing down. This is the standard way to compare discovery and replication
cohorts, or two related traits, on a shared genomic coordinate axis.

**How to interpret:** Peaks at the same genomic position in both panels
replicate across cohorts. Peaks present in only one panel may be
cohort-specific (sample composition, ancestry differences) or underpowered
in the other study. Use this for discovery/replication comparison or for
two related phenotypes (e.g., BMI vs waist-hip ratio) to see shared loci:

```{r miami}
miami_plot(
  example_gwas, example_gwas,
  top_title = "Discovery",
  bottom_title = "Replication"
)
```

## Locus zoom

Once you identify a significant region, a locus plot shows the fine structure
of association within that locus.

**How to interpret:** The lead SNP (lowest p-value) anchors the plot. When
LD data is provided, points are colored by r² with the lead. Red points
(high LD) that also reach significance support the same association signal,
while blue points (low LD) that reach significance independently may
represent a secondary signal at the locus. The gene track underneath shows
which genes fall within the region, guiding biological follow-up:

```{r locus}
locus_plot(example_gwas, region_chr = 1, region_start = 1e6, region_end = 50e6)
```

# Novel visualizations

## Genome-wide p-value heatmap

A compact representation of association signals across the entire genome.
The x-axis is binned genomic position, the y-axis is chromosome, and color
intensity reflects the strongest signal in each bin.

**When to use:** This works well as a supplementary overview figure. It
shows genome-wide patterns that Manhattan plots can obscure through
overplotting. Hot spots (bright tiles) across multiple chromosomes suggest
polygenicity; a single intense cluster points to one major locus. It also
handles 10M+ variant datasets without downsampling because it pre-aggregates.

```{r heatmap}
pvalue_heatmap(example_gwas, bin_size = 10e6, palette = "magma")
```

Different palettes emphasize different signal ranges:

```{r heatmap-viridis}
pvalue_heatmap(example_gwas, bin_size = 10e6, palette = "viridis")
```

```{r heatmap-inferno}
pvalue_heatmap(example_gwas, bin_size = 10e6, palette = "inferno")
```

## Effect-size volcano

Unlike the RNA-seq volcano plot (which uses fold change), this plots GWAS
effect size ($\hat{\beta}$) against significance. You see at once which
signals are protective vs risk-increasing, and how large the effects are
relative to their statistical evidence. Points can be colored by
significance status or by chromosome:

```{r volcano}
volcano_plot(example_gwas, label_top_n = 5)
```

Color by chromosome instead of significance:

```{r volcano-chr}
volcano_plot(example_gwas, label_top_n = 5, color_by = "chromosome")
```

## Circular Manhattan

The circular (circos-style) Manhattan arranges chromosomes around a ring.
This layout is compact and works particularly well for multi-trait
comparisons where each trait occupies its own ring:

```{r circular, fig.width=8, fig.height=8}
circular_manhattan(example_gwas, colors = gwas_palette("nature"))
```

## Summary dashboard

When you need a single figure combining Manhattan, QQ,
top hits table, and p-value distribution, `gwas_summary()` assembles them
into a multi-panel layout with automatic tags (A, B, C, D) suitable for
supplementary figures:

```{r summary, fig.height=9}
gwas_summary(example_gwas)
```

# Gene annotation and top hits

## Gene labels on peaks

In manuscripts, rs IDs are less informative than gene names.
`manhattan_genes()` takes a gene annotation table and labels each lead SNP
with its nearest gene. In practice, you would extract this table from a GTF
file or via `biomaRt`. Here we define a small set manually:

```{r gene-labels}
genes <- data.frame(
  chr = c(1, 2, 5, 7, 11, 15, 19, 22),
  start = c(2e6, 50e6, 80e6, 30e6, 60e6, 40e6, 10e6, 20e6),
  end = c(3e6, 52e6, 85e6, 35e6, 65e6, 45e6, 15e6, 25e6),
  gene = c("MTOR", "NRXN1", "TCF7L2", "CDKAL1", "KCNQ1", "FTO", "LDLR", "COMT")
)

manhattan_genes(example_gwas, genes = genes, gene_p_threshold = 0.001)
```

When peaks are close together, arrow-style annotations avoid label overlap:

```{r gene-arrows}
manhattan_genes(
  example_gwas, genes = genes,
  gene_p_threshold = 0.001,
  arrow = TRUE, label_face = "italic"
)
```

## Top hits table

`top_hits()` extracts independent lead SNPs using window-based clumping
(1 Mb by default), annotates each with an estimated cytoband, and optionally
maps to the nearest gene. The result is a plain data.frame ready for export
to a manuscript table:

```{r top-hits}
top_hits(example_gwas, p_threshold = 0.001, genes = genes, n = 10)
```

## Region highlights

Mark specific genomic regions with a colored band, for example the MHC
(chr6:25-34 Mb), known risk loci from prior GWAS, or candidate regions
from linkage studies:

```{r regions}
plt <- manhattan_plot(example_gwas)

regions <- data.frame(
  chr = c(6, 1),
  start = c(25e6, 5e6),
  end = c(35e6, 30e6),
  label = c("MHC", "1p36")
)

highlight_regions(plt, regions, color = "#FFD700", alpha = 0.2)
```

## Genomic tracks

`gene_track()` creates a standalone gene annotation panel that can be
composed with any ggwas plot using `patchwork`. Genes are rendered as
directional chevrons showing strand orientation:

```{r gene-track, fig.height=5}
genes <- data.frame(
  chr = c(1, 1, 1, 1, 1),
  start = c(2e6, 15e6, 40e6, 55e6, 75e6),
  end = c(8e6, 25e6, 50e6, 65e6, 85e6),
  gene = c("MTOR", "NRXN1", "TCF7L2", "CDKAL1", "KCNQ1"),
  strand = c("+", "-", "+", "-", "+")
)

p <- locus_plot(example_gwas, region_chr = 1,
                region_start = 0, region_end = 100e6, point_size = 1)
gt <- gene_track(genes, region_chr = 1, region_start = 0, region_end = 100e6,
                  highlight_genes = "TCF7L2")
patchwork::wrap_plots(p, gt, ncol = 1, heights = c(0.75, 0.25))
```

For real data, use `read_gtf()` to parse Ensembl or GENCODE annotation
files directly, with no `biomaRt` needed:

```{r read-gtf, eval=FALSE}
genes <- read_gtf("Homo_sapiens.GRCh38.113.gtf.gz",
                   biotype = "protein_coding")
gene_track(genes, region_chr = 6, region_start = 25e6, region_end = 35e6)
```

## Non-human organisms

By default, ggwas uses human chromosome conventions (23 = X, 24 = Y).
For other organisms, use `set_sex_chr_map()`:

```{r sex-chr, eval=FALSE}
# Mouse (20 = X, 21 = Y)
set_sex_chr_map(c(X = 20L, Y = 21L))

# Disable entirely (all chromosomes as numbers)
set_sex_chr_map(NULL)

# Restore human default
set_sex_chr_map()
```

# Multi-study comparisons

## Multi-trait Manhattan

Pleiotropy (one variant influencing multiple traits) is pervasive in
complex genetics. Visualizing it requires overlaying GWAS results from
multiple traits on a shared genomic axis.

**How to interpret:** Diamond-shaped highlighted points are variants reaching
genome-wide significance in more than one trait simultaneously. Peaks that
appear in multiple colors at the same position suggest shared genetic
mechanisms, worth following up with formal colocalization or Mendelian
randomization:

```{r multitrait}
set.seed(123)
trait2 <- example_gwas
trait2$P <- runif(nrow(trait2))^3
trait2$P[sample(nrow(trait2), 10)] <- 10^(-runif(10, 5, 10))

multitrait_manhattan(
  BMI = example_gwas,
  Height = trait2,
  colors = "nature",
  highlight_shared = TRUE
)
```

## Enrichment Manhattan

Most GWAS hits fall in non-coding regions, making functional interpretation
challenging. Overlaying genomic annotations (enhancers, promoters, eQTLs,
open chromatin from ENCODE/Roadmap) helps determine whether signals enrich
in regulatory elements relevant to the tissue of interest.

**How to interpret:** Colored points fall within annotated functional
regions; grey background points do not. If significant peaks preferentially
overlap with one annotation type (e.g., liver enhancers for lipid traits),
that provides evidence for the regulatory mechanism and tissue of action:

```{r enrichment}
annotations <- data.frame(
  chr = c(1, 3, 5, 7, 11, 15, 19),
  start = c(5e6, 20e6, 50e6, 30e6, 60e6, 40e6, 10e6),
  end = c(30e6, 50e6, 120e6, 80e6, 100e6, 80e6, 40e6),
  category = c("Enhancer", "Gene", "eQTL", "Gene",
               "Enhancer", "eQTL", "Gene")
)

enrichment_manhattan(example_gwas, annotations = annotations, palette = "nature")
```

## Circular Manhattan (multi-ring)

The multi-ring layout stacks traits concentrically: each ring is an
independent GWAS, sharing the same angular coordinate system:

```{r circular-multi, fig.width=8, fig.height=8}
circular_manhattan(
  list(BMI = example_gwas, Height = trait2),
  colors = "nature", point_size = 0.6
)
```

# Post-GWAS analysis plots

## PheWAS plot

Phenome-wide association studies test one variant against hundreds or
thousands of phenotypes, common in biobank-era research (UK Biobank,
FinnGen, All of Us).

**How to interpret:** Each point is a phenotype. Phenotypes are grouped by
domain (metabolic, immune, cardiovascular, etc.) along the x-axis. Triangles
pointing up indicate risk-increasing effects; pointing down, protective.
A cluster of significant associations within one domain suggests the variant
influences a biological pathway rather than a single trait. Isolated
significant hits across multiple domains may indicate pleiotropy or a
shared confounder (e.g., BMI-mediated effects):

```{r phewas, fig.height=5}
phewas_data <- data.frame(
  phenotype = paste0("Pheno_", 1:60),
  p = 10^(-runif(60, 0, 10)),
  category = rep(c("Metabolic", "Immune", "Neurological",
                    "Cardiovascular", "Musculoskeletal", "Other"), 10),
  beta = rnorm(60, 0, 0.3)
)

phewas_plot(phewas_data, label_top_n = 5)
```

## Colocalization locus plot

Colocalization analysis asks: do a GWAS signal and an eQTL (or two GWAS
traits) share the same causal variant, or are they driven by different
variants that happen to be nearby?

**How to interpret:** The two panels share a genomic coordinate axis. If the
peak in the top panel (GWAS) aligns with the peak in the bottom panel
(eQTL), the signals likely share a causal variant, and the gene regulated by
that eQTL is a strong candidate for the GWAS hit. Offset peaks suggest
distinct causal variants in LD. This visual check complements formal
colocalization methods (coloc, eCAVIAR):

```{r coloc}
set.seed(42)
eqtl_data <- example_gwas
eqtl_data$P <- runif(nrow(eqtl_data))^2

coloc_plot(
  example_gwas, eqtl_data,
  region_chr = 1, region_start = 1e6, region_end = 30e6,
  top_title = "GWAS", bottom_title = "eQTL"
)
```

## Fine-mapping credible set

After identifying a GWAS locus, fine-mapping narrows down which variant
is most likely causal. Tools like SuSiE and FINEMAP assign each variant a
posterior inclusion probability (PIP, 0–1) and group high-PIP variants into
credible sets.

**How to interpret:** Large points have high PIP and are strong causal
candidates. Color indicates credible set membership: variants in the same
set are in LD and cannot be statistically distinguished. A locus with one
large red point (PIP > 0.8) is well-resolved; a locus with many
medium-sized points in one set has residual uncertainty. Multiple credible
sets at one locus suggest independent signals:

```{r finemapping}
fm_data <- example_gwas
fm_data$PIP <- runif(nrow(fm_data))^5
fm_data$PIP[which.min(fm_data$P)] <- 0.92
fm_data$PIP[order(fm_data$P)[2:3]] <- c(0.65, 0.4)
fm_data$credible_set <- NA_integer_
fm_data$credible_set[fm_data$PIP > 0.05] <- 1L

finemapping_plot(
  fm_data,
  region_chr = 1, region_start = 1e6, region_end = 50e6,
  label_pip_above = 0.3
)
```

## Genetic correlation matrix

Genetic correlation (rg) measures how much the genetic basis of two traits
overlaps. It ranges from -1 (perfectly opposing genetic effects) through 0
(independent) to +1 (shared genetic architecture). LDSC or GREML estimate
rg from GWAS summary statistics alone, without individual-level data.

**How to interpret:** Red cells indicate traits that share risk variants
(e.g., BMI and T2D). Blue cells indicate opposing genetics (e.g., height
and T2D). Clustering reveals groups of genetically related traits. Stars
mark statistically significant correlations. This figure is essential for
understanding trait relationships in cross-trait genomics:

```{r rg-matrix, fig.height=6}
traits <- c("BMI", "Height", "WHR", "T2D", "CAD", "SBP", "HDL")
set.seed(123)
rg <- matrix(0, 7, 7, dimnames = list(traits, traits))
rg[lower.tri(rg)] <- runif(21, -0.6, 0.8)
rg <- rg + t(rg)
diag(rg) <- 1

genetic_correlation(rg, show_values = TRUE, palette = "RdBu")
```

Alternative palette and without clustering:

```{r rg-prgn, fig.height=6}
genetic_correlation(rg, show_values = TRUE, palette = "PRGn", cluster = FALSE)
```

## Genetic architecture

The relationship between minor allele frequency and effect size reveals
a trait's genetic architecture.

**How to interpret:** Most complex traits show a cloud of small effects at
common frequencies (lower left): this is polygenicity. Rare variants with
large effects (upper left) are characteristic of more Mendelian-like
architecture. An empty upper-right corner (large effects at common MAF) is
expected because natural selection removes such variants. If you see points
there, check for coding errors or population-stratification artifacts:

```{r architecture}
architecture_plot(example_gwas, p_threshold = 0.001, label_top_n = 5)
```

## Trumpet plot: power to detect

The trumpet plot overlays statistical-power contours on the
effect-versus-frequency relationship, showing which variants a study of a
given sample size is powered to detect. The contours flare toward rare
variants, giving the plot its shape; points below them fall in the region the
study is underpowered for.

```{r trumpet}
trumpet_plot(example_gwas, n = 50000, label_top_n = 3)
```

## Forest plots and cross-study comparison

A forest plot summarizes effect estimates with confidence intervals, for
example across cohorts or for a set of lead variants:

```{r forest}
lead <- head(example_gwas[order(example_gwas$P), ], 10)
forest_plot(lead, order_by = "effect")
```

`effect_compare_plot()` scatters the effects of two studies on their shared
variants to assess replication and effect concordance:

```{r effect-compare}
set.seed(1)
replication <- example_gwas
replication$BETA <- replication$BETA + rnorm(nrow(replication), 0, 0.02)
effect_compare_plot(example_gwas, replication,
                    labels = c("Discovery", "Replication"), p_threshold = 1e-3)
```

Regional and gene-labelled plots can use the bundled protein-coding gene
models via `gene_annotation("GRCh38")` (or `"GRCh37"`), so no GTF download is
needed; passing `exon_data` to `gene_track()` draws full exon structure.

# Genome-wide density

## SNP density karyogram

Visualize how genotyped or imputed variants are distributed across the
genome. Regions with low coverage (centromeres, heterochromatin) appear
as gaps, while high-density regions stand out. Two rendering styles
are available.

**Heatmap style** bins variants and colors tiles by count, best for
large datasets:

```{r density-heatmap}
snp_density(example_gwas, bin_size = 5e6, chr_info = chr_info_human())
```

**Points style** draws individual variants on chromosome outlines,
where density is visible through natural clustering:

```{r density-points}
snp_density(example_gwas, style = "points", chr_info = chr_info_human())
```

Points can be colored by local density, association significance, or
uniformly:

```{r density-significance}
snp_density(example_gwas, style = "points",
  color_by = "significance", chr_info = chr_info_human())
```

The `palette` argument works with both styles:

```{r density-palette}
snp_density(example_gwas, bin_size = 5e6,
  palette = "magma", chr_info = chr_info_human())
```

### Species support

Built-in chromosome data is available for human, mouse, and cattle.
For other species, use `chr_info_ucsc()` to fetch data from UCSC:

```{r vig-05, eval=FALSE}
# Built-in (instant, offline)
snp_density(gwas, chr_info = chr_info_human())   # hg38
snp_density(gwas, chr_info = chr_info_mouse())   # mm39
snp_density(gwas, chr_info = chr_info_cattle())  # ARS-UCD1.2

# Any species from UCSC (requires internet)
snp_density(gwas, chr_info = chr_info_ucsc("canFam6"))   # dog
snp_density(gwas, chr_info = chr_info_ucsc("susScr11"))  # pig
```

## Density vs signal comparison

A critical quality-control visualization: compare genotyping density
(top track) against association signal strength (bottom track) for each
chromosome. This helps distinguish genuine signals from density
artifacts: if a region shows strong association AND high variant
density, the signal could be driven by uneven coverage rather than
biology.

```{r density-signal, fig.height=6}
density_signal_plot(example_gwas, bin_size = 5e6)
```

Regions where the bottom track lights up but the top track is uniform
are the most convincing signals.

# Themes, palettes, and presets

## Color palettes

ggwas ships with 14 palettes covering different use cases: colorblind-safe
defaults, journal-inspired schemes, and RColorBrewer integrations:

```{r vig-09}
gwas_palettes()
gwas_palette("nature", n = 5)
```

Any palette can be passed directly to plot functions via the `colors` argument.
Use `gwas_palette(name, n = k)` to interpolate when you need a specific number
of colors (e.g., for multi-trait plots).

## Journal themes

Each journal has its own style requirements for figure fonts, sizes, and axis
formatting. Built-in themes handle this so you don't need to manually adjust
parameters for each submission:

```{r theme-demo}
manhattan_plot(example_gwas, label_top_n = 3) + theme_nature()
```

Available themes: `theme_nature()`, `theme_science()`, `theme_cell()`,
`theme_plos()`, `theme_presentation()`, `theme_poster()`.

## Presets

A preset bundles together a theme, color palette, and appropriate point sizes
for a given context. This is the fastest way to switch between exploratory
analysis and publication-ready output:

```{r preset}
p <- gwas_preset("publication")
manhattan_plot(example_gwas, colors = p$colors, point_size = p$point_size) + p$theme
```

Available presets: `"publication"`, `"presentation"`, `"poster"`,
`"exploratory"`.

# Performance

For large datasets (common in imputed GWAS with 10-40M variants),
ggwas applies smart downsampling by default. The algorithm preserves all
significant and near-significant variants while spatially binning the dense
cloud of non-significant points. The resulting plot is visually
indistinguishable from plotting all points, but renders in seconds rather
than minutes:

```{r vig-06, eval=FALSE}
manhattan_plot(large_gwas)                        # auto (target: 200k points)
manhattan_plot(large_gwas, downsample = FALSE)    # all points (slow)
manhattan_plot(large_gwas, downsample_n = 500000) # custom target
```

# Multi-panel figures

Since every ggwas function returns a standard ggplot object, you can
compose multi-panel figures using `patchwork` the same way you would with
any other ggplot:

```{r patchwork, fig.height=8}
library(patchwork)

p1 <- manhattan_plot(example_gwas, label_top_n = 3) +
  labs(tag = "A") + theme_nature()
p2 <- qq_plot(example_gwas) +
  labs(tag = "B") + theme_nature()
p3 <- volcano_plot(example_gwas, label_top_n = 5) +
  labs(tag = "C") + theme_nature()

p1 / (p2 | p3) + plot_layout(heights = c(2, 1))
```

# Saving figures

Standard ggplot2 export with dimensions matching journal requirements:

```{r vig-07, eval=FALSE}
fig <- manhattan_plot(example_gwas, label_top_n = 5) + theme_nature()

# Nature: single column (89 mm width)
ggsave("figure1.pdf", fig, width = 89, height = 55, units = "mm")

# Full-width figure (183 mm)
ggsave("figure1_wide.pdf", fig, width = 183, height = 80, units = "mm")

# High-res PNG for presentations
ggsave("figure1.png", fig, width = 10, height = 5, dpi = 300)
```

# Session info

```{r vig-10}
sessionInfo()
```
