Manhattan plot

Genome-wide association hits on a locus-aware chromosome axis. Alternating chromosome colors separate the blocks, dashed rules mark significance thresholds, and the strongest peaks are outlined for emphasis.

Move the −log10 p cutoff slider to change the red significance line. Points above the line turn red immediately; a higher cutoff is stricter. GenomeSpy updates the rule and highlighted set in the browser.

Data use and provenance

The packaged table is a subset of HapMap variants from the MIT-licensed manhattanly and Plotly datasets projects. Genomic coordinates and annotations are real, but the association statistics are simulated and are not biological findings. During data loading, the package removes invalid p-values and calculates -log10(p). GenomeSpy then renders the prepared table and applies the interactive threshold in the browser.

Code

"""Manhattan plot.

Genome-wide association hits on a locus-aware chromosome axis. Alternating
chromosome colors separate the blocks, dashed rules mark significance
thresholds, and the strongest peaks are outlined for emphasis.
"""

import genome_spy as gs
from genome_spy.datasets._hapmap import hapmap_manhattan_data
from genome_spy.schema import GenomeAxis, Scale


GENOME_WIDE_P = 5e-8
SUGGESTIVE_P = 1e-5


# Load association results and the starting significance thresholds.
data, _top_hits, domains = hapmap_manhattan_data(
    genome_wide_p=GENOME_WIDE_P,
    suggestive_p=SUGGESTIVE_P,
)
# Moving this slider redraws the red guide and outlines the hits above it.
significance_cutoff = gs.param(
    "manhattanSignificanceCutoff",
    value=round(domains["genome_wide_y"], 1),
    bind=gs.binding_range(
        min=3,
        max=domains["y_domain"][1],
        step=0.1,
        name="−log10 p cutoff: ",
    ),
)

# Alternate colors to make neighboring chromosomes easier to distinguish.
chrom_colors = Scale().range(["#5b8fd6", "#8f98a3"])

# Label chromosomes and add faint backgrounds to separate them.
axis = (
    GenomeAxis()
    .title("Genomic position")
    .chromGrid(True)
    .chromGridOpacity(0.14)
    .chromGridFillEven("#f4f6fb")
    .chromGridFillOdd("#ffffff")
    .chromLabels(True)
    .chromLabelFontSize(11)
    .chromTicks(True)
    .chromTickSize(10)
    .labelFontSize(10)
    .grid(False)
)

# Draw one point per variant at its genomic position.
points = (
    gs.Chart()
    .mark_point(size=20, filled=True, opacity=0.82)
    .encode(
        x=gs.Locus("chrom", "BP").scale(assembly="hg18").axis(axis),
        y=gs.Y("neglog:Q")
        .scale(reverse=False, domain=domains["y_domain"])
        .title("−log10 p"),
        color=gs.Color("chrom_group:N")
        .scale(chrom_colors)
        .legend(title="Chromosome group"),
    )
)

# Move the red significance line with the slider.
genome_wide_rule = (
    gs.Chart([{}])
    .mark_rule(strokeDash=[6, 4], size=1.4, color="#c53b2c")
    .encode(
        y=gs.Y(gs.datum(significance_cutoff), type="quantitative")
        .scale(reverse=False, domain=domains["y_domain"])
        .title("−log10 p")
    )
)

# Keep a second, less strict threshold visible for comparison.
suggestive_rule = (
    gs.Chart([{"threshold": domains["suggestive_y"]}])
    .mark_rule(strokeDash=[2, 4], size=1.2, color="#d48b31")
    .encode(
        y=gs.Y("threshold:Q")
        .scale(reverse=False, domain=domains["y_domain"])
        .title("−log10 p")
    )
)

# Highlight variants above the slider's current threshold.
highlight_points = (
    gs.Chart()
    .transform_collect()
    .transform_filter(gs.datum.neglog >= significance_cutoff)
    .mark_point(
        size=48,
        filled=True,
        color="#c53b2c",
        stroke="black",
        strokeWidth=0.5,
    )
    .encode(
        x=gs.Locus("chrom", "BP").scale(assembly="hg18"),
        y=gs.Y("neglog:Q")
        .scale(reverse=False, domain=domains["y_domain"])
        .title("−log10 p"),
    )
)

# Combine the points and guide lines, then attach the slider.
association_track = genome_wide_rule + suggestive_rule + points + highlight_points

chart = association_track.properties(
    assembly="hg18",
    title="HapMap genome-wide association scan",
    data=data,
    description=(
        "A Manhattan plot with a locus-aware chromosome axis and an "
        "interactive significance threshold."
    ),
).add_params(significance_cutoff)