{"spec_id":"recurrence-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nrecurrence-basic: Recurrence Plot for Nonlinear Time Series\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-10\n\"\"\"\n\nimport io\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom PIL import Image\n\n\n# Workaround: remove current directory from import path to avoid shadowing\n# the bokeh package with this file (bokeh.py) when run in-place\noriginal_path = sys.path.copy()\nsys.path = [p for p in sys.path if p != \"\" and not (os.path.isfile(os.path.join(p, \"bokeh.py\")) if p else False)]\n\ntry:\n    from bokeh.io import output_file, save\n    from bokeh.models import BasicTicker, ColorBar, ColumnDataSource, HoverTool, Label, LinearColorMapper\n    from bokeh.plotting import figure\n    from bokeh.resources import CDN\n    from selenium import webdriver\n    from selenium.webdriver.chrome.options import Options\nfinally:\n    sys.path = original_path\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 sequential colormap: brand green → blue (256 stops, single-polarity)\n_c0 = np.array([0x00, 0x9E, 0x73])  # #009E73\n_c1 = np.array([0x44, 0x67, 0xA3])  # #4467A3\nIMPRINT_SEQ256 = [\n    \"#{:02X}{:02X}{:02X}\".format(*(int(round(v)) for v in (_c0 + (_c1 - _c0) * t / 255.0))) for t in range(256)\n]\n\n# Data — Lorenz attractor x-component\nnp.random.seed(42)\ndt = 0.01\nnum_steps = 5000\nlx, ly, lz = 1.0, 1.0, 1.0\nsigma, rho, beta = 10.0, 28.0, 8.0 / 3.0\n\ntrajectory = np.empty(num_steps)\nfor step in range(num_steps):\n    dx = sigma * (ly - lx) * dt\n    dy = (lx * (rho - lz) - ly) * dt\n    dz = (lx * ly - beta * lz) * dt\n    lx, ly, lz = lx + dx, ly + dy, lz + dz\n    trajectory[step] = lx\n\nsignal = trajectory[::10]\nn_points = len(signal)\n\n# Time-delay embedding (Takens' theorem): dim=3, delay=5\ntau = 5\ndim = 3\nn_embedded = n_points - (dim - 1) * tau\nembedded = np.empty((n_embedded, dim))\nfor d in range(dim):\n    embedded[:, d] = signal[d * tau : d * tau + n_embedded]\n\n# Pairwise Euclidean distance matrix\ndiff = embedded[:, np.newaxis, :] - embedded[np.newaxis, :, :]\ndist_matrix = np.sqrt(np.sum(diff**2, axis=2))\nthreshold = np.percentile(dist_matrix, 10)\nmax_dist = dist_matrix.max()\ndist_normalized = dist_matrix / max_dist\ndist_flipped = dist_normalized[::-1, :]  # row 0 at top\n\ncolor_mapper = LinearColorMapper(palette=IMPRINT_SEQ256, low=0.0, high=1.0)\n\n# Plot — square 2400×2400 canonical\nn = n_embedded\np = figure(\n    width=2400,\n    height=2400,\n    title=\"recurrence-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Time Index (Lorenz x-component, dt=0.01)\",\n    y_axis_label=\"Time Index (Lorenz x-component, dt=0.01)\",\n    toolbar_location=None,\n    tools=\"\",\n    x_range=(0, n),\n    y_range=(0, n),\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\np.image(image=[dist_flipped], x=0, y=0, dw=n, dh=n, color_mapper=color_mapper)\n\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    ticker=BasicTicker(desired_num_ticks=6),\n    label_standoff=16,\n    border_line_color=None,\n    background_fill_color=PAGE_BG,\n    location=(0, 0),\n    title=\"Normalized Distance\",\n    title_text_font_size=\"28pt\",\n    title_text_color=INK,\n    major_label_text_font_size=\"24pt\",\n    major_label_text_color=INK_SOFT,\n    width=40,\n    padding=30,\n)\np.add_layout(color_bar, \"right\")\n\np.add_layout(\n    Label(\n        x=200,\n        y=260,\n        text=\"Laminar regime\",\n        text_font_size=\"28pt\",\n        text_color=INK,\n        text_font_style=\"bold\",\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.88,\n    )\n)\np.add_layout(\n    Label(\n        x=120,\n        y=160,\n        text=\"Deterministic diagonals\",\n        text_font_size=\"28pt\",\n        text_color=INK,\n        text_font_style=\"bold\",\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.88,\n        angle=0.78,\n    )\n)\np.add_layout(\n    Label(\n        x=10,\n        y=n - 25,\n        text=f\"Recurrence threshold ε = {threshold:.1f} (10th percentile)\",\n        text_font_size=\"24pt\",\n        text_color=INK,\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.88,\n    )\n)\n\n# HoverTool via invisible scatter overlay (idiomatic Bokeh for image plots)\nhover_step = 20\nhover_xs, hover_ys, hover_dists, hover_recs = [], [], [], []\nfor i in range(0, n, hover_step):\n    for j in range(0, n, hover_step):\n        hover_xs.append(i + hover_step // 2)\n        hover_ys.append(j + hover_step // 2)\n        dist_ij = dist_matrix[i, j]\n        hover_dists.append(round(float(dist_ij), 2))\n        hover_recs.append(\"Yes\" if dist_ij <= threshold else \"No\")\n\nhover_source = ColumnDataSource(data={\"x\": hover_xs, \"y\": hover_ys, \"distance\": hover_dists, \"recurrent\": hover_recs})\ninvisible_scatter = p.scatter(x=\"x\", y=\"y\", source=hover_source, size=hover_step, fill_alpha=0, line_alpha=0)\np.add_tools(\n    HoverTool(\n        renderers=[invisible_scatter],\n        tooltips=[\n            (\"Time i\", \"@x\"),\n            (\"Time j\", \"@y\"),\n            (\"Distance\", \"@distance\"),\n            (\"Recurrent (d < {:.1f})\".format(threshold), \"@recurrent\"),\n        ],\n    )\n)\n\n# Style — theme-adaptive chrome\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\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\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\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\np.axis.minor_tick_line_color = None\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\np.outline_line_color = INK_SOFT\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Save HTML then screenshot with headless Chrome (export_png not used — snap shim fails)\noutput_file(f\"plot-{THEME}.html\")\nsave(p, resources=CDN)\n\n# Window is H+200 tall so the full bokeh canvas renders; PIL crops to W×H.\nW, H = 2400, 2400\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 + 200}\",\n    \"--hide-scrollbars\",\n    \"--force-device-scale-factor=1\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H + 200)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\nraw = driver.get_screenshot_as_png()\ndriver.quit()\nImage.open(io.BytesIO(raw)).crop((0, 0, W, H)).save(f\"plot-{THEME}.png\")\n"}