{"spec_id":"scatter-connected-temporal","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nscatter-connected-temporal: Connected Scatter Plot with Temporal Path\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-09\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BasicTicker, ColorBar, ColumnDataSource, Label, LinearColorMapper, NumeralTickFormatter\nfrom bokeh.plotting import figure\nfrom bokeh.transform import transform\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    r, g, b = (int(round(a + (b - a) * t)) for a, b in ((r0, r1), (g0, g1), (b0, b1)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\n# Imprint sequential palette: brand green (#009E73) → blue (#4467A3)\nANYPLOT_SEQ256 = [_lerp_hex(\"#009E73\", \"#4467A3\", t / 255.0) for t in range(256)]\n\n# Data — US unemployment rate vs inflation rate (1990–2023)\nyears = np.arange(1990, 2024)\nn = len(years)\n\nunemployment = np.array(\n    [\n        5.6,\n        6.8,\n        7.5,\n        6.9,\n        6.1,\n        5.6,\n        5.4,\n        4.9,\n        4.5,\n        4.2,  # 1990s recovery\n        4.0,\n        4.7,\n        5.8,\n        6.0,\n        5.5,\n        5.1,\n        4.6,\n        4.6,\n        5.8,\n        9.3,  # 2000s + GFC\n        9.6,\n        8.9,\n        8.1,\n        7.4,\n        6.2,\n        5.3,\n        4.9,\n        4.4,\n        3.9,\n        3.7,  # 2010s recovery\n        8.1,\n        5.4,\n        3.6,\n        3.6,  # COVID + recovery\n    ]\n)\n\ninflation = np.array(\n    [\n        5.4,\n        4.2,\n        3.0,\n        3.0,\n        2.6,\n        2.8,\n        3.0,\n        2.3,\n        1.6,\n        2.2,  # 1990s\n        3.4,\n        2.8,\n        1.6,\n        2.3,\n        2.7,\n        3.4,\n        3.2,\n        2.8,\n        3.8,\n        -0.4,  # 2000s\n        1.6,\n        3.2,\n        2.1,\n        1.5,\n        1.6,\n        0.1,\n        1.3,\n        2.1,\n        2.4,\n        1.8,  # 2010s\n        1.2,\n        4.7,\n        8.0,\n        4.1,  # 2020s\n    ]\n)\n\nsource = ColumnDataSource(data={\"unemployment\": unemployment, \"inflation\": inflation, \"year_val\": years.astype(float)})\n\n# Color mapper from Imprint seq palette so ColorBar shows year → color\ncolor_mapper = LinearColorMapper(palette=ANYPLOT_SEQ256, low=1990, high=2023)\n\n# Title is ~90 chars; scale from default 50pt: round(50 * 67 / 90) = 37pt\np = figure(\n    width=3200,\n    height=1800,\n    title=\"US Phillips Curve Dynamics (1990–2023) · scatter-connected-temporal · bokeh · anyplot.ai\",\n    x_axis_label=\"Unemployment Rate (%)\",\n    y_axis_label=\"Inflation Rate (%)\",\n    toolbar_location=None,\n    x_range=(2.5, 11.0),\n    y_range=(-1.5, 9.5),\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=80,\n)\n\n# Connecting lines — per-segment color from Imprint seq, increasing opacity over time\nxs = [[unemployment[i], unemployment[i + 1]] for i in range(n - 1)]\nys = [[inflation[i], inflation[i + 1]] for i in range(n - 1)]\nline_colors = [ANYPLOT_SEQ256[int((i / (n - 2)) * 255)] for i in range(n - 1)]\n# Older segments thinner + more transparent to reduce congestion in the central cluster\nline_widths = [2.0 + 3.0 * (i / (n - 2)) for i in range(n - 1)]\nline_alphas = [0.45 + 0.5 * (i / (n - 2)) for i in range(n - 1)]\n\nline_source = ColumnDataSource(\n    data={\"xs\": xs, \"ys\": ys, \"colors\": line_colors, \"widths\": line_widths, \"alphas\": line_alphas}\n)\np.multi_line(xs=\"xs\", ys=\"ys\", source=line_source, line_width=\"widths\", line_color=\"colors\", line_alpha=\"alphas\")\n\n# Scatter points with Imprint temporal color gradient\np.scatter(\n    x=\"unemployment\",\n    y=\"inflation\",\n    source=source,\n    size=20,\n    color=transform(\"year_val\", color_mapper),\n    alpha=0.92,\n    line_color=INK,\n    line_width=2,\n)\n\n# ColorBar for year → Imprint color mapping\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    location=(0, 0),\n    title=\"Year\",\n    title_text_font_size=\"34pt\",\n    title_text_color=INK_SOFT,\n    major_label_text_font_size=\"28pt\",\n    major_label_text_color=INK_SOFT,\n    label_standoff=14,\n    width=34,\n    padding=24,\n    formatter=NumeralTickFormatter(format=\"0\"),\n    major_tick_line_color=None,\n    bar_line_color=None,\n    ticker=BasicTicker(desired_num_ticks=5),\n    background_fill_color=PAGE_BG,\n)\np.add_layout(color_bar, \"right\")\n\n# Annotate key economic events\nannotations = {\n    0: (\"1990 ▸\", -100, 15),\n    9: (\"1999\", -20, 18),\n    19: (\"2009\", 15, -25),\n    25: (\"2015\", 15, 12),\n    30: (\"2020\", 15, -18),\n    32: (\"2022\", 15, 18),\n    33: (\"◂ 2023\", 30, -10),\n}\n\nfor idx, (label_text, x_offset, y_offset) in annotations.items():\n    label = Label(\n        x=unemployment[idx],\n        y=inflation[idx],\n        text=label_text,\n        text_font_size=\"26pt\",\n        text_color=INK_SOFT,\n        text_font_style=\"bold\",\n        x_offset=x_offset,\n        y_offset=y_offset,\n    )\n    p.add_layout(label)\n\n# Title styling — scaled down for ~90-char title\np.title.text_font_size = \"37pt\"\np.title.text_color = INK\n\n# Axis styling\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\n# Axis lines and ticks — use INK_SOFT for subtle structural chrome\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.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Grid — subtle, both axes suit a scatter plot\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.12\np.ygrid.grid_line_alpha = 0.12\n\n# Background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Save interactive HTML (catalog artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — use CDP to force exact viewport dimensions\n# set_window_size alone leaves a ~139px gap between outer and inner height\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    \"--hide-scrollbars\",\n    \"--force-device-scale-factor=1\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}