{"spec_id":"qq-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nqq-basic: Basic Q-Q Plot\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-07-24\n\"\"\"\n\nimport io\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool\nfrom bokeh.plotting import figure\nfrom PIL import Image\nfrom scipy import stats\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint palette)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"  # Imprint palette position 1\n\n# Data - adult heights (cm) with a few outliers to showcase Q-Q interpretation\nnp.random.seed(42)\ncore = np.random.normal(loc=170, scale=8, size=88)\noutliers = np.array([145.0, 147.0, 149.0, 196.0, 199.0, 202.0, 148.0, 198.0, 200.0, 203.0, 145.5, 201.5])\nsample = np.concatenate([core, outliers])\n\n# Theoretical and sample quantiles (standardised to z-scores)\nsample_sorted = np.sort(sample)\nn = len(sample_sorted)\nprobabilities = (np.arange(1, n + 1) - 0.5) / n\ntheoretical_quantiles = stats.norm.ppf(probabilities)\n\nsample_mean = np.mean(sample)\nsample_std = np.std(sample, ddof=1)\nsample_quantiles = (sample_sorted - sample_mean) / sample_std\n\nsource = ColumnDataSource(\n    data={\"theoretical\": theoretical_quantiles, \"sample\": sample_quantiles, \"value\": sample_sorted}\n)\n\n# Pointwise 95% confidence envelope for the reference line (order-statistic SE,\n# same construction as R's car::qqPlot). It narrows near the median and widens\n# in the tails, giving a principled visual cue for which points genuinely\n# deviate from normality rather than relying on a text callout.\nquantile_se = np.sqrt(probabilities * (1 - probabilities) / n) / stats.norm.pdf(theoretical_quantiles)\nz_crit = 1.96\nenvelope_source = ColumnDataSource(\n    data={\n        \"base\": theoretical_quantiles,\n        \"lower\": theoretical_quantiles - z_crit * quantile_se,\n        \"upper\": theoretical_quantiles + z_crit * quantile_se,\n    }\n)\n\n# Reference line (y=x, bounded to data range)\npad = 0.3\nx_min = theoretical_quantiles.min() - pad\nx_max = theoretical_quantiles.max() + pad\n\n# Figure — canvas is a hard 3200x1800 contract; min_border_* reserve room for\n# the 42pt axis labels + 34pt tick labels so they aren't clipped from the PNG.\np = figure(\n    width=3200,\n    height=1800,\n    title=\"qq-basic · bokeh · anyplot.ai\",\n    x_axis_label=\"Theoretical Quantiles\",\n    y_axis_label=\"Sample Quantiles\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Confidence envelope drawn first so the line and points sit on top of it\np.varea(\n    x=\"base\",\n    y1=\"lower\",\n    y2=\"upper\",\n    source=envelope_source,\n    fill_color=INK_MUTED,\n    fill_alpha=0.18,\n    legend_label=\"95% quantile envelope\",\n)\n\n# Reference line drawn as a line glyph so it can carry a legend label\np.line(\n    [x_min, x_max],\n    [x_min, x_max],\n    line_color=INK_SOFT,\n    line_width=3,\n    line_dash=\"dashed\",\n    legend_label=\"Normal reference\",\n)\n\n# Q-Q scatter points\np.scatter(\n    x=\"theoretical\",\n    y=\"sample\",\n    source=source,\n    size=14,\n    color=BRAND,\n    alpha=0.75,\n    line_color=PAGE_BG,\n    line_width=1,\n    legend_label=\"Sample quantiles\",\n)\n\n# Hover tooltips\nhover = HoverTool(\n    tooltips=[(\"Theoretical Q\", \"@theoretical{0.00}\"), (\"Sample Q\", \"@sample{0.00}\"), (\"Height\", \"@value{0.0} cm\")]\n)\np.add_tools(hover)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None  # drop the boxed-in frame; left/bottom axis lines are enough\n\np.title.text_color = INK\np.title.text_font_size = \"50pt\"\n\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.15\np.ygrid.grid_line_alpha = 0.15\n\nif p.legend:\n    p.legend.background_fill_color = ELEVATED_BG\n    p.legend.border_line_color = INK_SOFT\n    p.legend.label_text_color = INK_SOFT\n    p.legend.label_text_font_size = \"34pt\"\n    p.legend.location = \"top_left\"\n    p.legend.click_policy = \"hide\"  # toggle envelope / reference / points\n\n# Save HTML (interactive artifact) and screenshot it with headless Chrome.\n# bokeh.io.export_png is avoided: it probes /usr/bin/chromedriver, which is a\n# snap shim on this box and fails regardless of xvfb — Selenium's own driver\n# resolution (Selenium Manager) works instead.\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Window is H+200 tall so the browser chrome (present even headless) doesn't\n# eat into the bokeh canvas; PIL crops the screenshot back to exactly W x H.\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H + 200}\",\n    \"--hide-scrollbars\",\n    \"--force-device-scale-factor=1\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H + 200)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)  # let bokeh's JS render the canvas\nraw = driver.get_screenshot_as_png()\ndriver.quit()\nImage.open(io.BytesIO(raw)).crop((0, 0, W, H)).save(f\"plot-{THEME}.png\")\n"}