Scatter Plot with Polynomial Regression — Bokeh

A scatter plot displaying the relationship between two numeric variables with a fitted polynomial regression curve (degree 2-4). This visualization extends beyond linear regression to capture non-linear relationships in data, making it ideal for modeling curved trends, parabolic patterns, and complex data relationships where a straight line would not adequately represent the underlying pattern.

Scatter Plot with Polynomial Regression rendered with Bokeh

Renders

Python source (Bokeh)

""" anyplot.ai
scatter-regression-polynomial: Scatter Plot with Polynomial Regression
Library: bokeh 3.9.2 | Python 3.13.14
Quality: 92/100 | Updated: 2026-08-11
"""

import os
import time
from pathlib import Path

import numpy as np
from bokeh.events import MouseMove
from bokeh.io import output_file, save
from bokeh.models import Band, BoxAnnotation, ColumnDataSource, CustomJS, HoverTool, Label, Span
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
BRAND = IMPRINT_PALETTE[0]  # data points - always first series
ACCENT = IMPRINT_PALETTE[1]  # regression curve - lavender, second series
MUTED = INK_MUTED  # confidence band fill - "other" semantic anchor

# Data - Manufacturing efficiency curve (diminishing returns pattern)
np.random.seed(42)
n_points = 100

# Investment amount (thousands of dollars)
x = np.linspace(10, 100, n_points)
# Efficiency gains follow a quadratic pattern with diminishing returns
# True relationship: y = -0.005x^2 + 1.2x + 20 + noise
y = -0.005 * x**2 + 1.2 * x + 20 + np.random.normal(0, 3, n_points)

# Polynomial regression (degree 2 - quadratic)
coeffs = np.polyfit(x, y, 2)
poly = np.poly1d(coeffs)

# Calculate R-squared
y_pred = poly(x)
ss_res = np.sum((y - y_pred) ** 2)
ss_tot = np.sum((y - np.mean(y)) ** 2)
r_squared = 1 - (ss_res / ss_tot)

# Create smooth curve for regression line + a 95% prediction band from the
# residual spread (approximate, but conveys fit uncertainty at a glance)
residual_std = np.std(y - y_pred)
x_smooth = np.linspace(x.min(), x.max(), 200)
y_smooth = poly(x_smooth)
y_lower = y_smooth - 1.96 * residual_std
y_upper = y_smooth + 1.96 * residual_std

# Format polynomial equation
a, b, c = coeffs
equation = f"y = {a:.4f}x² + {b:.2f}x + {c:.2f}"

# Marginal gain dy/dx = 2ax + b — the story this curve is telling. Mark where
# the marginal gain has fallen to half its value at x.min(): everything past
# that point is the "diminishing returns" regime the spec's domain is about.
marginal_at_xmin = 2 * a * x.min() + b
zone_start = (0.5 * marginal_at_xmin - b) / (2 * a)

# Create data sources
scatter_source = ColumnDataSource(data={"x": x, "y": y})
line_source = ColumnDataSource(data={"x": x_smooth, "y": y_smooth})
band_source = ColumnDataSource(data={"x": x_smooth, "lower": y_lower, "upper": y_upper})

# Create figure — canvas is the hard 3200x1800 contract; min_border reserves
# room for the 34-42pt axis text so it isn't clipped at the PNG edge.
p = figure(
    width=3200,
    height=1800,
    title="scatter-regression-polynomial · bokeh · anyplot.ai",
    x_axis_label="Investment (thousands $)",
    y_axis_label="Efficiency Gain (%)",
    toolbar_location=None,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=50,
)

# Shade the diminishing-returns regime (past zone_start) so the curve's own
# story — gains flattening out — reads at a glance, not just from the equation
box = BoxAnnotation(left=zone_start, fill_color=MUTED, fill_alpha=0.07, line_color=None, level="underlay")
p.add_layout(box)
zone_label = Label(
    x=zone_start + 1.5,
    y=93,
    text="Diminishing returns",
    text_font_size="22pt",
    text_font_style="italic",
    text_color=INK_MUTED,
)
p.add_layout(zone_label)

# Confidence band first so scatter + curve render on top of it. A faint
# dashed edge (vs. no line) gives the band a defined silhouette instead of
# just a flat fill — a small but deliberate refinement over the bare default.
band = Band(
    base="x",
    lower="lower",
    upper="upper",
    source=band_source,
    fill_color=MUTED,
    fill_alpha=0.18,
    line_color=ACCENT,
    line_alpha=0.3,
    line_dash="dashed",
    line_width=1.5,
)
band.level = "underlay"
p.add_layout(band)

# Plot scatter points
p.scatter(x="x", y="y", source=scatter_source, size=12, color=BRAND, alpha=0.65, legend_label="Data Points")

# Plot polynomial regression curve
p.line(x="x", y="y", source=line_source, line_width=3.5, color=ACCENT, legend_label="Polynomial Fit (degree 2)")

# Add HoverTool for interactivity
hover = HoverTool(tooltips=[("Investment", "@x{0.0}"), ("Efficiency", "@y{0.0}")])
p.add_tools(hover)

# Add R² and equation annotation
annotation_text = f"R² = {r_squared:.4f}\n{equation}"
annotation = Label(
    x=68,
    y=78,
    text=annotation_text,
    text_font_size="30pt",
    text_color=INK,
    text_line_height=1.3,
    background_fill_color=ELEVATED_BG,
    background_fill_alpha=0.9,
    border_line_color=INK_SOFT,
)
p.add_layout(annotation)

# Bokeh-distinctive touch: a live marginal-gain readout. CustomJS recomputes
# dy/dx = 2ax + b from the mouse's data-space x on every move, so the HTML
# detail view lets a reader probe exactly where the curve is still climbing
# vs. already flattening — a live derivative isn't something a static-image
# library can offer.
crosshair_x = 35.0
marginal_initial = 2 * a * crosshair_x + b
crosshair = Span(location=crosshair_x, dimension="height", line_color=INK_SOFT, line_dash="dashed", line_width=2)
p.add_layout(crosshair)
marginal_label = Label(
    x=13,
    y=34,
    text=f"Marginal gain: {marginal_initial:.2f}%/$k at $35k (hover to probe)",
    text_font_size="22pt",
    text_color=INK_SOFT,
)
p.add_layout(marginal_label)
p.js_on_event(
    MouseMove,
    CustomJS(
        args={
            "span": crosshair,
            "label": marginal_label,
            "a": float(a),
            "b": float(b),
            "xmin": float(x.min()),
            "xmax": float(x.max()),
        },
        code="""
        const px = cb_obj.x
        if (px < xmin || px > xmax) { return }
        span.location = px
        const marginal = 2 * a * px + b
        label.x = px < (xmin + xmax) / 2 ? px + 1 : px - 24
        label.text = `Marginal gain: ${marginal.toFixed(2)}%/$k at $${px.toFixed(0)}k (hover to probe)`
        """,
    ),
)

# Styling - text sizes for the 3200x1800 canonical canvas
p.title.text_font_size = "50pt"
p.title.text_color = INK
p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT
p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT
p.xaxis.minor_tick_line_color = INK_SOFT
p.yaxis.minor_tick_line_color = INK_SOFT
p.xaxis.minor_tick_line_alpha = 0.35
p.yaxis.minor_tick_line_alpha = 0.35

# Grid styling
p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK
p.xgrid.grid_line_alpha = 0.15
p.ygrid.grid_line_alpha = 0.15

# Legend styling - top left placement for better visibility
p.legend.location = "top_left"
p.legend.label_text_font_size = "34pt"
p.legend.label_text_color = INK_SOFT
p.legend.background_fill_color = ELEVATED_BG
p.legend.background_fill_alpha = 0.9
p.legend.border_line_color = INK_SOFT
p.legend.border_line_width = 1.5
p.legend.padding = 16
p.legend.spacing = 10
p.legend.margin = 20

# Background and outline
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = INK_SOFT

# Save as HTML
output_file(f"plot-{THEME}.html")
save(p)

# Screenshot with headless Chrome using Selenium
W, H = 3200, 1800
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
# Headless Chrome's --window-size sets the OUTER window, which still reserves
# a phantom title-bar height even headless; pin the viewport exactly via CDP.
driver.execute_cdp_cmd(
    "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False}
)
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/scatter-regression-polynomial/bokeh/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "scatter-regression-polynomial",
  "language": "python",
  "library": "bokeh",
  "page": "https://anyplot.ai/scatter-regression-polynomial/python/bokeh",
  "hub": "https://anyplot.ai/scatter-regression-polynomial",
  "code_json": "https://api.anyplot.ai/specs/scatter-regression-polynomial/bokeh/code",
  "spec_json": "https://api.anyplot.ai/specs/scatter-regression-polynomial",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-polynomial/python/bokeh/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-polynomial/python/bokeh/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-polynomial/python/bokeh/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-polynomial/python/bokeh/plot-dark.html",
  "quality_score": 92.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Scatter Plot with Polynomial Regression on anyplot.ai.

Other implementations