{"spec_id":"volcano-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nvolcano-basic: Volcano Plot for Statistical Significance\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-14\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette (colorblind-safe)\nIMPRINT = (\n    \"#009E73\",  # position 1: green (brand)\n    \"#C475FD\",  # position 2: vermillion (up-regulated)\n    \"#4467A3\",  # position 3: blue (down-regulated)\n    \"#BD8233\",  # position 4: reddish purple\n)\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n    stroke_width=3,\n)\n\n# Data - Simulated differential gene expression results\nnp.random.seed(42)\nn_genes = 500\n\n# Generate fold changes (mostly near zero, some with larger effects)\nlog2_fc = np.concatenate(\n    [\n        np.random.normal(0, 0.5, 400),  # Non-significant genes\n        np.random.normal(2.5, 0.5, 50),  # Up-regulated\n        np.random.normal(-2.5, 0.5, 50),  # Down-regulated\n    ]\n)\n\n# Generate p-values (correlated with effect size)\nbase_pval = np.random.uniform(0.001, 0.9, n_genes)\nbase_pval[400:] = np.random.uniform(0.0001, 0.01, 100)\nneg_log10_pval = -np.log10(base_pval)\n\n# Classification thresholds\nfc_threshold = 1.0  # log2 fold change threshold (2-fold)\npval_threshold = 1.3  # -log10(0.05) ≈ 1.3\n\n# Classify genes\nup_regulated = (log2_fc > fc_threshold) & (neg_log10_pval > pval_threshold)\ndown_regulated = (log2_fc < -fc_threshold) & (neg_log10_pval > pval_threshold)\nnot_significant = ~(up_regulated | down_regulated)\n\n# Calculate axis ranges based on actual data\ny_max = float(np.ceil(max(neg_log10_pval) * 1.1))  # 10% padding\nx_min = float(np.floor(min(log2_fc) * 1.1))\nx_max = float(np.ceil(max(log2_fc) * 1.1))\n\n# Generate y-axis labels from 0 to max\ny_step = 0.5\ny_labels = [i * y_step for i in range(int(y_max / y_step) + 1)]\n\n# Create XY chart\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"volcano-basic · pygal · anyplot.ai\",\n    x_title=\"Log₂ Fold Change\",\n    y_title=\"-Log₁₀(p-value)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_box_size=32,\n    dots_size=12,\n    stroke=False,\n    show_x_guides=True,\n    show_y_guides=True,\n    x_label_rotation=0,\n    include_x_axis=True,\n    include_y_axis=True,\n    explicit_size=True,\n    truncate_legend=-1,\n    spacing=40,\n    margin=30,\n    margin_bottom=140,\n    margin_left=100,\n    range=(x_min, x_max),\n)\n\n# Set y-axis labels\nchart.y_labels = y_labels\n\n# Prepare data points for each category with gene labels for tooltips\nnot_sig_points = [\n    {\"value\": (float(log2_fc[i]), float(neg_log10_pval[i])), \"label\": f\"Gene {i}\"}\n    for i in range(n_genes)\n    if not_significant[i]\n]\nup_points = [\n    {\"value\": (float(log2_fc[i]), float(neg_log10_pval[i])), \"label\": f\"Gene {i}\"}\n    for i in range(n_genes)\n    if up_regulated[i]\n]\ndown_points = [\n    {\"value\": (float(log2_fc[i]), float(neg_log10_pval[i])), \"label\": f\"Gene {i}\"}\n    for i in range(n_genes)\n    if down_regulated[i]\n]\n\n# Add data series (NOT in legend - these are reference lines)\n# Gray for non-significant (neutral, not from Okabe-Ito)\nchart.add(\"Not Significant\", not_sig_points, color=\"#888888\")\nchart.add(\"Up-regulated\", up_points, color=\"#AE3030\")  # imprint red — up-regulated (semantic)\nchart.add(\"Down-regulated\", down_points, color=\"#4467A3\")  # imprint blue — down-regulated\n\n# Add threshold lines as separate series\n# These won't appear in legend due to no hover text, but will render as lines\n# Horizontal threshold line at p-value cutoff\nh_line_points = [(x_min, pval_threshold), (x_max, pval_threshold)]\nchart.add(\n    \"p=0.05\",\n    h_line_points,\n    stroke=True,\n    show_dots=False,\n    color=INK_MUTED,\n    stroke_style={\"width\": 3, \"dasharray\": \"8, 4\"},\n)\n\n# Vertical threshold lines at fold change cutoffs\nv_line_neg_points = [(float(-fc_threshold), 0.0), (float(-fc_threshold), y_max)]\nchart.add(\n    \"FC=-2\",\n    v_line_neg_points,\n    stroke=True,\n    show_dots=False,\n    color=INK_MUTED,\n    stroke_style={\"width\": 3, \"dasharray\": \"8, 4\"},\n)\n\nv_line_pos_points = [(float(fc_threshold), 0.0), (float(fc_threshold), y_max)]\nchart.add(\n    \"FC=+2\",\n    v_line_pos_points,\n    stroke=True,\n    show_dots=False,\n    color=INK_MUTED,\n    stroke_style={\"width\": 3, \"dasharray\": \"8, 4\"},\n)\n\n# Save as PNG and HTML\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}