{"spec_id":"scatter-regression-polynomial","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nscatter-regression-polynomial: Scatter Plot with Polynomial Regression\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-08-11\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\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\"\n\n# Imprint palette\nBRAND = \"#009E73\"  # First series (scatter points)\nSECONDARY = \"#C475FD\"  # Second series (regression curve + band)\n\n# Data - Quadratic relationship with noise (fertilizer vs crop yield)\nnp.random.seed(42)\nn_points = 80\nx = np.linspace(0.5, 10, n_points)\n# Quadratic relationship: yield increases then plateaus (diminishing returns)\ny_true = -0.6 * x**2 + 7.5 * x + 8\ny = y_true + np.random.randn(n_points) * 2.5\n# Clip to realistic range (crop yield must be positive); ceiling sits well\n# above the curve's peak (~31.4) so it never flattens the plateau region.\ny = np.clip(y, 1, 36)\n\n# Fit polynomial regression (degree 2)\ncoeffs = np.polyfit(x, y, 2)\ny_pred = np.polyval(coeffs, x)\n\n# Calculate R² value\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# Residual standard error, used for a ~95% empirical confidence band\nresid_se = np.sqrt(ss_res / (n_points - 3))\nband_halfwidth = 1.96 * resid_se\n\n# Create equation string\na, b, c = coeffs\nequation = f\"y = {a:.2f}x² + {b:.2f}x + {c:.2f}\"\n\n# Peak of the fitted curve (vertex of the parabola) - focal point for the story\nx_peak = -b / (2 * a)\ny_peak = np.polyval(coeffs, x_peak)\n\n# Prepare DataFrames\ndf_points = pd.DataFrame({\"Fertilizer (kg/ha)\": x, \"Crop Yield (tons/ha)\": y})\n\n# Generate smooth curve + confidence band for the regression fit\nx_smooth = np.linspace(x.min(), x.max(), 200)\ny_smooth = np.polyval(coeffs, x_smooth)\ndf_curve = pd.DataFrame(\n    {\n        \"Fertilizer (kg/ha)\": x_smooth,\n        \"Crop Yield (tons/ha)\": y_smooth,\n        \"Lower\": y_smooth - band_halfwidth,\n        \"Upper\": y_smooth + band_halfwidth,\n    }\n)\n\n# Hover selection - highlights the nearest point and drives its tooltip\nhover = alt.selection_point(on=\"pointerover\", nearest=True, fields=[\"Fertilizer (kg/ha)\"], empty=False)\n\n# Confidence band (drawn first, sits behind the scatter + curve)\nband = (\n    alt.Chart(df_curve)\n    .mark_area(color=SECONDARY, opacity=0.15)\n    .encode(x=\"Fertilizer (kg/ha):Q\", y=alt.Y(\"Lower:Q\", title=\"Crop Yield (tons/ha)\"), y2=\"Upper:Q\")\n)\n\n# Scatter plot\nscatter = (\n    alt.Chart(df_points)\n    .mark_circle(color=BRAND, stroke=PAGE_BG, strokeWidth=0.75)\n    .encode(\n        x=alt.X(\"Fertilizer (kg/ha):Q\", title=\"Fertilizer (kg/ha)\"),\n        y=alt.Y(\"Crop Yield (tons/ha):Q\", title=\"Crop Yield (tons/ha)\"),\n        size=alt.condition(hover, alt.value(260), alt.value(110)),\n        opacity=alt.condition(hover, alt.value(0.95), alt.value(0.65)),\n        tooltip=[\"Fertilizer (kg/ha)\", \"Crop Yield (tons/ha)\"],\n    )\n    .add_params(hover)\n)\n\n# Polynomial regression curve\ncurve = (\n    alt.Chart(df_curve).mark_line(size=3, color=SECONDARY).encode(x=\"Fertilizer (kg/ha):Q\", y=\"Crop Yield (tons/ha):Q\")\n)\n\n# Peak marker + label - highlights the optimal fertilizer rate as the focal point\npeak_df = pd.DataFrame({\"Fertilizer (kg/ha)\": [x_peak], \"Crop Yield (tons/ha)\": [y_peak]})\npeak_label_df = pd.DataFrame({\"x\": [x_peak], \"y\": [y_peak], \"text\": [f\"Peak: {y_peak:.1f} t/ha @ {x_peak:.1f} kg/ha\"]})\n\npeak_marker = (\n    alt.Chart(peak_df)\n    .mark_point(shape=\"diamond\", size=200, filled=True, color=INK, stroke=PAGE_BG, strokeWidth=1.5)\n    .encode(x=\"Fertilizer (kg/ha):Q\", y=\"Crop Yield (tons/ha):Q\")\n)\n\npeak_label = (\n    alt.Chart(peak_label_df)\n    .mark_text(align=\"center\", baseline=\"bottom\", dy=-12, fontSize=11, fontWeight=\"bold\", color=INK)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"text:N\")\n)\n\n# Annotation for R² and equation\nr2_text_df = pd.DataFrame({\"x\": [0.5], \"y\": [28.5], \"text\": [f\"R² = {r_squared:.3f}\"]})\neq_text_df = pd.DataFrame({\"x\": [0.5], \"y\": [26.3], \"text\": [equation]})\n\nr2_annotation = (\n    alt.Chart(r2_text_df)\n    .mark_text(align=\"left\", baseline=\"top\", fontSize=14, fontWeight=\"bold\", color=INK)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"text:N\")\n)\n\neq_annotation = (\n    alt.Chart(eq_text_df)\n    .mark_text(align=\"left\", baseline=\"top\", fontSize=12, fontWeight=\"normal\", color=INK_SOFT)\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"text:N\")\n)\n\n# Combine layers\nchart = (\n    (band + scatter + curve + peak_marker + peak_label + r2_annotation + eq_annotation)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\"scatter-regression-polynomial · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\"),\n    )\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        tickSize=6,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_title(color=INK)\n    .interactive()\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD-only to the canonical 3200x1800 landscape target (never crop - see\n# prompts/library/altair.md \"Canvas\" for why vl-convert overshoot must fail\n# loudly instead of being cropped).\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}×{_h}, exceeds target {TW}×{TH}. 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"}