{"spec_id":"scatter-regression-linear","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nscatter-regression-linear: Scatter Plot with Linear Regression\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens (Imprint palette, see prompts/default-style-guide.md)\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nBRAND = \"#009E73\"  # Imprint palette position 1 — always first series\nACCENT = \"#C475FD\"  # Imprint palette position 2 — regression line + CI band\n\n# Data - Temperature vs Energy Consumption\nnp.random.seed(42)\nn = 100\ntemperature = np.random.uniform(45, 95, n)  # Fahrenheit\nnoise = np.random.normal(0, 12, n)\nenergy_consumption = 0.65 * temperature + 800 + noise  # kWh\n\n# Closed-form OLS — drives the 95% CI band and the equation/R² annotation\nx_mean = np.mean(temperature)\ny_mean = np.mean(energy_consumption)\nss_xx = np.sum((temperature - x_mean) ** 2)\nss_xy = np.sum((temperature - x_mean) * (energy_consumption - y_mean))\nslope = ss_xy / ss_xx\nintercept = y_mean - slope * x_mean\n\ny_pred = slope * temperature + intercept\nss_res = np.sum((energy_consumption - y_pred) ** 2)\nss_tot = np.sum((energy_consumption - y_mean) ** 2)\nr_squared = 1 - (ss_res / ss_tot)\n\nx_line = np.linspace(temperature.min(), temperature.max(), 150)\ny_line = slope * x_line + intercept\nmse = ss_res / (n - 2)\nse_line = np.sqrt(mse * (1 / n + (x_line - x_mean) ** 2 / ss_xx))\nt_val = 1.984  # t-critical for 95% CI, df=98\ny_upper = y_line + t_val * se_line\ny_lower = y_line - t_val * se_line\n\ndf_scatter = pd.DataFrame({\"Temperature (°F)\": temperature, \"Energy (kWh)\": energy_consumption})\ndf_band = pd.DataFrame({\"Temperature (°F)\": x_line, \"y_lower\": y_lower, \"y_upper\": y_upper, \"series\": \"95% CI\"})\n\nequation_text = f\"y = {slope:.2f}x + {intercept:.1f}\"\nr2_text = f\"R² = {r_squared:.3f}\"\nannotation_df = pd.DataFrame({\"equation\": [equation_text], \"r2\": [r2_text]})\n\n# Shared color scale so the CI band and regression line report into one merged legend\noverlay_scale = alt.Scale(domain=[\"95% CI\", \"Regression Line\"], range=[ACCENT, ACCENT])\noverlay_legend = alt.Legend(title=None, labelFontSize=10, orient=\"top-right\")\n\n# Layers\nscatter = (\n    alt.Chart(df_scatter)\n    .mark_point(size=100, opacity=0.65, filled=True)\n    .encode(\n        x=alt.X(\"Temperature (°F):Q\", scale=alt.Scale(zero=False)),\n        y=alt.Y(\"Energy (kWh):Q\", scale=alt.Scale(zero=False)),\n        color=alt.value(BRAND),\n        tooltip=[alt.Tooltip(\"Temperature (°F):Q\", format=\".1f\"), alt.Tooltip(\"Energy (kWh):Q\", format=\".1f\")],\n    )\n)\n\nband = (\n    alt.Chart(df_band)\n    .mark_area(opacity=0.18)\n    .encode(\n        x=\"Temperature (°F):Q\",\n        y=alt.Y(\"y_lower:Q\", title=\"Energy (kWh)\"),\n        y2=\"y_upper:Q\",\n        color=alt.Color(\"series:N\", scale=overlay_scale, legend=overlay_legend),\n    )\n)\n\n# Regression line fit natively via Altair's declarative regression transform\nregression_line = (\n    alt.Chart(df_scatter)\n    .transform_regression(\"Temperature (°F)\", \"Energy (kWh)\", method=\"linear\")\n    .transform_calculate(series=\"'Regression Line'\")\n    .mark_line(strokeWidth=3)\n    .encode(\n        x=\"Temperature (°F):Q\",\n        y=\"Energy (kWh):Q\",\n        color=alt.Color(\"series:N\", scale=overlay_scale, legend=overlay_legend),\n    )\n)\n\nannotation_eq = (\n    alt.Chart(annotation_df)\n    .mark_text(align=\"left\", baseline=\"top\", fontSize=13, fontWeight=\"bold\", dx=12, dy=12)\n    .encode(x=alt.value(0), y=alt.value(0), text=\"equation:N\", color=alt.value(INK))\n)\n\nannotation_r2 = (\n    alt.Chart(annotation_df)\n    .mark_text(align=\"left\", baseline=\"top\", fontSize=13, fontWeight=\"bold\", dx=12, dy=32)\n    .encode(x=alt.value(0), y=alt.value(0), text=\"r2:N\", color=alt.value(INK))\n)\n\n# Title — mandated format, length-scaled fontsize (see prompts/plot-generator.md)\ntitle_str = \"scatter-regression-linear · python · altair · anyplot.ai\"\ntitle_fontsize = round(16 * (67 / len(title_str) if len(title_str) > 67 else 1.0))\n\nchart = (\n    alt.layer(band, regression_line, scatter, annotation_eq, annotation_r2)\n    .properties(\n        width=620,\n        height=320,\n        padding={\"left\": 0, \"right\": 0, \"top\": 0, \"bottom\": 0},\n        title=alt.Title(title_str, fontSize=title_fontsize, anchor=\"start\"),\n        background=PAGE_BG,\n    )\n    .configure_view(fill=PAGE_BG, stroke=None, continuousWidth=620, continuousHeight=320)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridOpacity=0.12,\n        gridColor=INK,\n    )\n    .configure_title(color=INK, fontSize=title_fontsize, anchor=\"start\", fontWeight=\"normal\")\n    .configure_legend(labelColor=INK_SOFT, symbolStrokeWidth=2.5, symbolOpacity=1)\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}