Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 72 additions & 40 deletions plots/manhattan-gwas/implementations/python/altair.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
""" pyplots.ai
""" anyplot.ai
manhattan-gwas: Manhattan Plot for GWAS
Library: altair 6.0.0 | Python 3.13.11
Quality: 91/100 | Created: 2025-12-31
Library: altair 6.1.0 | Python 3.13.13
Quality: 88/100 | Updated: 2026-05-15
"""

import os

import altair as alt
import numpy as np
import pandas as pd


# Data - Simulated GWAS data with random p-values and significant peaks
# Theme configuration
THEME = os.getenv("ANYPLOT_THEME", "light")

PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
RULE = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"

# Okabe-Ito palette for alternating chromosomes
OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9"]

# Data generation
np.random.seed(42)

# Chromosome lengths (approximate human chromosome sizes in Mb)
Expand Down Expand Up @@ -87,82 +102,99 @@
suggestive_threshold = -np.log10(1e-5) # 5.0

# Create chromosome label data
chrom_label_df = pd.DataFrame(
[{"chrom_label": chrom, "center": center, "y_pos": -0.5} for chrom, center in chrom_centers.items()]
)
chrom_label_df = pd.DataFrame([{"chrom_label": chrom, "center": center} for chrom, center in chrom_centers.items()])

# Define color scheme - alternating for chromosomes
color_scale = alt.Scale(
domain=[str(i) for i in range(1, 23)],
range=["#306998", "#7F7F7F"] * 11, # Alternating Python blue and gray
)
# Create alternating color mapping with Okabe-Ito palette
chrom_list = list(chrom_lengths.keys())
color_mapping = {}
for i, chrom in enumerate(chrom_list):
color_mapping[chrom] = OKABE_ITO[i % len(OKABE_ITO)]

color_scale = alt.Scale(domain=chrom_list, range=[color_mapping[chrom] for chrom in chrom_list])

# Main scatter plot
points = (
alt.Chart(df)
.mark_circle(opacity=0.7)
.mark_circle(opacity=0.7, size=80)
.encode(
x=alt.X(
"cumulative_position:Q",
axis=alt.Axis(title="Chromosome", labels=False, ticks=False, titleFontSize=22),
axis=alt.Axis(title="Genomic Position", labels=False, ticks=False, titleFontSize=22, titleColor=INK),
scale=alt.Scale(domain=[0, cumulative_pos]),
),
y=alt.Y(
"neg_log_p:Q",
title="-log₁₀(p-value)",
axis=alt.Axis(titleFontSize=22, labelFontSize=16),
axis=alt.Axis(
titleFontSize=22,
labelFontSize=18,
titleColor=INK,
labelColor=INK_SOFT,
domainColor=INK_SOFT,
tickColor=INK_SOFT,
gridColor=INK,
gridOpacity=0.10,
),
scale=alt.Scale(domain=[0, max(df["neg_log_p"]) + 1]),
),
color=alt.Color("chromosome:N", scale=color_scale, legend=None),
size=alt.condition(
alt.datum.neg_log_p > genome_wide_threshold,
alt.value(100), # Larger for significant hits
alt.value(30), # Smaller for others
alt.value(60), # Smaller for others
),
tooltip=["chromosome:N", "position:Q", "p_value:Q", "neg_log_p:Q"],
tooltip=[
"chromosome:N",
alt.Tooltip("position:Q", format=","),
alt.Tooltip("p_value:Q", format=".2e"),
"neg_log_p:Q",
],
)
)

# Genome-wide significance threshold line
gw_line = (
alt.Chart(pd.DataFrame({"y": [genome_wide_threshold]}))
.mark_rule(strokeDash=[8, 4], color="#E74C3C", strokeWidth=2)
.mark_rule(strokeDash=[8, 4], color=INK_MUTED, size=2, opacity=0.8)
.encode(y="y:Q")
)

# Suggestive threshold line
sug_line = (
alt.Chart(pd.DataFrame({"y": [suggestive_threshold]}))
.mark_rule(strokeDash=[4, 4], color="#F39C12", strokeWidth=2)
.mark_rule(strokeDash=[4, 4], color=INK_MUTED, size=1.5, opacity=0.6)
.encode(y="y:Q")
)

# Chromosome labels as text marks at bottom
# Chromosome labels at bottom
chrom_text = (
alt.Chart(chrom_label_df)
.mark_text(fontSize=14, fontWeight="bold", baseline="top", dy=5)
.encode(x=alt.X("center:Q", scale=alt.Scale(domain=[0, cumulative_pos])), text="chrom_label:N")
.mark_text(fontSize=16, baseline="top", dy=10, color=INK_SOFT, fontWeight="normal")
.encode(x=alt.X("center:Q", axis=None, scale=alt.Scale(domain=[0, cumulative_pos])), text="chrom_label:N")
)

# Combine layers - points with threshold lines
main_chart = alt.layer(points, gw_line, sug_line).properties(width=1500, height=750)
# Combine layers
main_chart = alt.layer(points, gw_line, sug_line).properties(width=1600, height=850)

# Chromosome labels at bottom
labels_chart = (
alt.Chart(chrom_label_df)
.mark_text(fontSize=16, baseline="middle")
.encode(x=alt.X("center:Q", scale=alt.Scale(domain=[0, cumulative_pos]), axis=None), text="chrom_label:N")
.properties(width=1500, height=30)
)

# Vertical concat with shared x-axis
# Final chart with labels
chart = (
alt.vconcat(main_chart, labels_chart, spacing=0)
.properties(title=alt.Title("manhattan-gwas · altair · pyplots.ai", fontSize=28, anchor="middle"))
.configure_axis(labelFontSize=16, titleFontSize=22, grid=True, gridOpacity=0.3)
.configure_view(strokeWidth=0)
alt.vconcat(main_chart, chrom_text.properties(width=1600, height=40), spacing=5)
.properties(
title=alt.Title(
"Manhattan Plot: GWAS Results",
fontSize=28,
anchor="middle",
color=INK,
subtitle="Genome-wide association study with significance thresholds",
),
background=PAGE_BG,
)
.configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=1)
.configure_axis(labelFontSize=18, titleFontSize=22, labelColor=INK_SOFT, titleColor=INK)
.configure_title(fontSize=28, color=INK, subtitleFontSize=16, subtitleColor=INK_SOFT)
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
)

# Save as PNG and HTML
chart.save("plot.png", scale_factor=3.0)
chart.save("plot.html")
# Save with theme-specific filenames
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
chart.save(f"plot-{THEME}.html")
Loading
Loading