{"spec_id":"scatter-regression-linear","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nscatter-regression-linear: Scatter Plot with Linear Regression\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\nsys.path.pop(0)\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import Band, ColumnDataSource, HoverTool, Label, Title\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\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\"\n\n# Imprint palette\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\nOUTLIER_COLOR = IMPRINT[4]  # matte red — semantic anchor for \"anomalous / off-model\" points\n\n# Data - Study hours vs exam scores\nnp.random.seed(42)\nn_points = 80\nx = np.random.uniform(1, 10, n_points)  # Study hours\nnoise = np.random.normal(0, 7, n_points)\ny = 45 + 5 * x + noise  # Exam scores\ny = np.clip(y, 0, 100)  # Ensure realistic scores (0-100%)\n\n# Linear regression calculation\nslope, intercept = np.polyfit(x, y, 1)\ny_pred = slope * x + intercept\n\n# Calculate R-squared\nss_res = np.sum((y - y_pred) ** 2)\nss_tot = np.sum((y - np.mean(y)) ** 2)\nr_squared = 1 - (ss_res / ss_tot)\n\n# Flag statistical outliers (|residual| > 2 std) to surface model fit quality\nresiduals = y - y_pred\nresidual_std = np.std(residuals)\nis_outlier = np.abs(residuals) > 2 * residual_std\n\n# Calculate 95% confidence interval\nn = len(x)\nx_mean = np.mean(x)\nse = np.sqrt(ss_res / (n - 2))\nt_value = 1.99  # t-value for 95% CI with ~78 degrees of freedom\n\n# Create sorted x values for smooth regression line and confidence band\nx_line = np.linspace(x.min(), x.max(), 100)\ny_line = slope * x_line + intercept\n\n# Standard error of prediction for confidence interval\nse_y = se * np.sqrt(1 / n + (x_line - x_mean) ** 2 / np.sum((x - x_mean) ** 2))\nci_upper = y_line + t_value * se_y\nci_lower = y_line - t_value * se_y\n\n# Create figure\n# `width`/`height` are the TOTAL canvas; min_border_* reserve room for the\n# 34-50pt native-pixel chrome so nothing clips at the PNG edges.\np = figure(\n    width=3200,\n    height=1800,\n    title=\"scatter-regression-linear · bokeh · anyplot.ai\",\n    x_axis_label=\"Study Hours\",\n    y_axis_label=\"Exam Score (%)\",\n    toolbar_location=None,  # default toolbar adds ~30-50px above the plot, shrinking the PNG below 1800px\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=140,\n    min_border_right=50,\n)\n\n# Subtitle giving the sample size + fit quality at a glance (bokeh Title layout)\np.add_layout(\n    Title(\n        text=f\"n = {n_points} study sessions · linear fit with 95% confidence band\",\n        text_font_size=\"24pt\",\n        text_font_style=\"normal\",\n        text_color=INK_SOFT,\n    ),\n    \"above\",\n)\n\n# Create data sources — outliers get their own source so they can carry a\n# redundant non-color channel (larger size + heavier outline), since red-vs-green\n# alone is a CVD confusion pair\nnormal_source = ColumnDataSource(\n    data={\"x\": x[~is_outlier], \"y\": y[~is_outlier], \"y_pred\": y_pred[~is_outlier], \"residual\": residuals[~is_outlier]}\n)\noutlier_source = ColumnDataSource(\n    data={\"x\": x[is_outlier], \"y\": y[is_outlier], \"y_pred\": y_pred[is_outlier], \"residual\": residuals[is_outlier]}\n)\nline_source = ColumnDataSource(data={\"x\": x_line, \"y\": y_line})\nband_source = ColumnDataSource(data={\"x\": x_line, \"lower\": ci_lower, \"upper\": ci_upper})\n\n# Add confidence interval band\nband = Band(\n    base=\"x\",\n    lower=\"lower\",\n    upper=\"upper\",\n    source=band_source,\n    fill_color=IMPRINT[0],\n    fill_alpha=0.15,\n    line_color=IMPRINT[0],\n    line_alpha=0.2,\n    line_width=1,\n)\np.add_layout(band)\n\n# Add regression line\np.line(\"x\", \"y\", source=line_source, line_color=IMPRINT[1], line_width=4, legend_label=\"Linear Regression\")\n\n# Add scatter points\nnormal_scatter = p.scatter(\n    \"x\",\n    \"y\",\n    source=normal_source,\n    size=11,\n    color=IMPRINT[0],\n    alpha=0.7,\n    line_color=PAGE_BG,\n    line_width=1,\n    legend_label=\"Data Points\",\n)\n\n# Outliers beyond 2 std of the residual get the matte-red semantic anchor PLUS a\n# redundant non-color channel (larger size, heavier ink-colored outline) so the\n# distinction survives when red/green cannot be resolved (CVD-safe)\noutlier_scatter = p.scatter(\n    \"x\",\n    \"y\",\n    source=outlier_source,\n    size=17,\n    color=OUTLIER_COLOR,\n    alpha=0.85,\n    line_color=INK,\n    line_width=2.5,\n    legend_label=\"Outlier (|residual| > 2σ)\",\n)\n\n# Add hover tooltip to both point layers\nhover = HoverTool(\n    renderers=[normal_scatter, outlier_scatter],\n    tooltips=[\n        (\"Study Hours\", \"@x{0.0}\"),\n        (\"Exam Score\", \"@y{0.0}\"),\n        (\"Predicted\", \"@y_pred{0.0}\"),\n        (\"Residual\", \"@residual{+0.0}\"),\n    ],\n)\np.add_tools(hover)\n\n# Add R² and equation annotation\nr2_text = f\"R² = {r_squared:.3f}\"\nequation_text = f\"y = {slope:.2f}x + {intercept:.2f}\"\nannotation = Label(\n    x=1.5,\n    y=92,\n    text=f\"{equation_text}\\n{r2_text}\",\n    text_font_size=\"22pt\",\n    text_color=INK,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=INK_SOFT,\n    border_line_width=1.5,\n    border_radius=8,\n)\np.add_layout(annotation)\n\n# Styling - theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\n\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\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\np.legend.label_text_font_size = \"34pt\"\np.legend.location = \"bottom_right\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.label_text_color = INK_SOFT\np.legend.click_policy = \"hide\"  # interactive: click a legend entry to toggle it (HTML view)\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with Selenium\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}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\n# headless Chrome's --window-size sets the OUTER window, which still reserves\n# a phantom title-bar height even headless — pin the viewport exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}