Lag Plot for Time Series Autocorrelation Diagnosis — Bokeh

A lag plot is a scatter plot of a time series against a lagged version of itself, plotting y(t) on the x-axis versus y(t+k) on the y-axis for a given lag order k. If the data is purely random, points scatter uniformly with no visible structure; if autocorrelation is present, distinctive patterns emerge — linear clusters for autoregressive processes, elliptical shapes for seasonal data. This provides a quick visual diagnostic for time series dependence, complementing numerical tools like ACF/PACF.

Lag Plot for Time Series Autocorrelation Diagnosis rendered with Bokeh

Python source (Bokeh)

""" anyplot.ai
scatter-lag: Lag Plot for Time Series Autocorrelation Diagnosis
Library: bokeh 3.9.1 | Python 3.13.14
Quality: 90/100 | Updated: 2026-06-24
"""

import os
import sys


# Prevent self-import: this file is named bokeh.py which shadows the installed
# bokeh package when its directory is at the front of sys.path.
_this_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _this_dir]

import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import ColorBar, ColumnDataSource, HoverTool, Label, LinearColorMapper
from bokeh.plotting import figure
from bokeh.transform import linear_cmap
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Theme tokens (Imprint palette — see prompts/default-style-guide.md)
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Imprint sequential colormap for time index (single-polarity: green → blue)
_seq = np.round(np.linspace([0x00, 0x9E, 0x73], [0x44, 0x67, 0xA3], 256)).astype(int)
IMPRINT_SEQ256 = ["#{:02X}{:02X}{:02X}".format(*row) for row in _seq]
del _seq

# Data — AR(1) process with strong positive autocorrelation (phi=0.85)
np.random.seed(42)
n_obs = 500
phi = 0.85
noise = np.random.normal(0, 1, n_obs)
series = np.zeros(n_obs)
series[0] = noise[0]
for i in range(1, n_obs):
    series[i] = phi * series[i - 1] + noise[i]

lag = 1
y_t = series[:-lag]
y_t_lag = series[lag:]
time_index = np.arange(len(y_t))
correlation = np.corrcoef(y_t, y_t_lag)[0, 1]

source = ColumnDataSource(data={"y_t": y_t, "y_t_lag": y_t_lag, "time_index": time_index})

# Plot
title = "scatter-lag · python · bokeh · anyplot.ai"

p = figure(
    width=3200,
    height=1800,
    title=title,
    x_axis_label="AR(1) Series y(t)",
    y_axis_label="AR(1) Series y(t + 1)",
    toolbar_location=None,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=200,
)

cmap = linear_cmap("time_index", palette=IMPRINT_SEQ256, low=time_index.min(), high=time_index.max())

p.scatter(
    x="y_t", y="y_t_lag", source=source, size=25, fill_color=cmap, line_color=PAGE_BG, line_width=0.8, fill_alpha=0.75
)

# Diagonal reference line (y = x)
axis_min = min(y_t.min(), y_t_lag.min()) - 0.5
axis_max = max(y_t.max(), y_t_lag.max()) + 0.5
p.line(
    [axis_min, axis_max], [axis_min, axis_max], line_color=INK_SOFT, line_dash="dashed", line_width=3, line_alpha=0.5
)

# Color bar for temporal progression
color_mapper = LinearColorMapper(palette=IMPRINT_SEQ256, low=time_index.min(), high=time_index.max())
color_bar = ColorBar(
    color_mapper=color_mapper,
    title="Time Step",
    title_text_font_size="28pt",
    title_text_color=INK,
    title_standoff=20,
    major_label_text_font_size="24pt",
    major_label_text_color=INK_SOFT,
    label_standoff=12,
    width=60,
    padding=40,
    background_fill_color=PAGE_BG,
)
p.add_layout(color_bar, "right")

# Correlation annotation
corr_label = Label(
    x=axis_min + 0.3,
    y=axis_max - 0.7,
    text=f"r = {correlation:.3f}",
    text_font_size="32pt",
    text_color=INK,
    text_font_style="bold",
)
p.add_layout(corr_label)

# HoverTool — distinctive Bokeh interactive feature (active in HTML export)
hover = HoverTool(tooltips=[("y(t)", "@y_t{0.000}"), ("y(t + 1)", "@y_t_lag{0.000}"), ("Time step", "@time_index")])
p.add_tools(hover)

# Style — canonical font sizes for 3200×1800 (see prompts/library/bokeh.md)
p.title.text_font_size = "50pt"
p.title.text_font_style = "normal"
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 = None
p.yaxis.minor_tick_line_color = None

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

p.outline_line_color = None
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG

# Save HTML (interactive catalog artifact) then PNG via headless Chrome
output_file(f"plot-{THEME}.html")
save(p)

W, H = 3200, 1800
WIN_H = H + 150  # browser viewport offset buffer
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{WIN_H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, WIN_H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.execute_script(
    "document.body.style.backgroundColor = arguments[0];document.documentElement.style.backgroundColor = arguments[0];",
    PAGE_BG,
)
time.sleep(1)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

Part of Lag Plot for Time Series Autocorrelation Diagnosis on anyplot.ai.

Other implementations