{"spec_id":"bump-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbump-basic: Basic Bump Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-29\n\"\"\"\n\nimport sys\n\n\n# Remove the script's own directory so 'bokeh' resolves to the installed package,\n# not this script file (bokeh.py would shadow the bokeh package otherwise).\nif sys.path and sys.path[0]:\n    sys.path = sys.path[1:]\n\nimport os\nimport time\nfrom pathlib import Path\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, CustomJSTickFormatter, FixedTicker, Label\nfrom bokeh.plotting import figure\nfrom bokeh.resources import INLINE\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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 categorical palette — 8 hues, hybrid-v3 sort, theme-independent\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — Formula 1 constructor standings over a 6-race stretch\nentities = [\"Red Bull Racing\", \"McLaren\", \"Ferrari\", \"Mercedes\", \"Aston Martin\"]\nperiods = [\"Race 1\", \"Race 2\", \"Race 3\", \"Race 4\", \"Race 5\", \"Race 6\"]\n\nrankings = {\n    \"Red Bull Racing\": [3, 2, 1, 1, 2, 1],\n    \"McLaren\": [1, 1, 2, 3, 3, 4],\n    \"Ferrari\": [5, 4, 4, 2, 1, 2],\n    \"Mercedes\": [2, 3, 3, 4, 4, 3],\n    \"Aston Martin\": [4, 5, 5, 5, 5, 5],\n}\n\n# Highlight entities with dramatic rank changes\nhighlight = {\"Ferrari\", \"Red Bull Racing\", \"McLaren\"}\n\n# Title length → scale font size (floor 34pt)\ntitle_str = \"F1 Constructor Standings · bump-basic · python · bokeh · anyplot.ai\"\nn = len(title_str)\nratio = 67 / n if n > 67 else 1.0\ntitle_fontsize = f\"{max(34, round(50 * ratio))}pt\"\n\n# Figure — 3200×1800 landscape; toolbar_location=None keeps PNG at exact height\np = figure(\n    width=3200,\n    height=1800,\n    title=title_str,\n    x_range=periods,\n    y_range=(5.8, 0.4),\n    x_axis_label=\"Constructor Standings by Race\",\n    y_axis_label=\"Championship Position\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=380,  # extra right margin for end-of-line team labels\n)\n\n# Lines and markers for each entity with visual hierarchy\nfor i, (entity, ranks) in enumerate(rankings.items()):\n    source = ColumnDataSource(data={\"x\": periods, \"y\": ranks})\n    is_highlight = entity in highlight\n    lw = 10 if is_highlight else 5\n    alpha_line = 0.95 if is_highlight else 0.75\n    alpha_marker = 1.0 if is_highlight else 0.80\n    marker_size = 38 if is_highlight else 24\n    color = IMPRINT_PALETTE[i]\n\n    p.line(x=\"x\", y=\"y\", source=source, line_width=lw, line_color=color, line_alpha=alpha_line)\n    p.scatter(x=\"x\", y=\"y\", source=source, size=marker_size, color=color, alpha=alpha_marker)\n\n    # End-of-line label at the final race position (integer index for categorical axis)\n    label = Label(\n        x=len(periods) - 1,\n        y=ranks[-1],\n        text=entity,\n        text_font_size=\"28pt\",\n        text_color=color,\n        text_alpha=1.0,\n        text_font_style=\"bold\" if is_highlight else \"normal\",\n        x_offset=40 if is_highlight else 22,\n        y_offset=-12,\n    )\n    p.add_layout(label)\n\n# Font sizes — canonical 3200×1800 values\np.title.text_font_size = title_fontsize\np.title.text_font_style = \"bold\"\np.title.text_color = INK\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\"\n\n# Theme-adaptive chrome\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# Remove spines and ticks for a clean bump chart look\np.xaxis.axis_line_color = None\np.yaxis.axis_line_color = None\np.xaxis.major_tick_line_color = None\np.yaxis.major_tick_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Grid — horizontal guide lines only, very subtle\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\np.ygrid.grid_line_dash = [6, 4]\n\n# Y-axis ordinal labels (1st, 2nd, 3rd…) via CustomJSTickFormatter\np.yaxis.ticker = FixedTicker(ticks=[1, 2, 3, 4, 5])\np.yaxis.formatter = CustomJSTickFormatter(\n    code=\"\"\"\n    const suffixes = {1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th'};\n    return tick + (suffixes[tick] || 'th');\n\"\"\"\n)\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 with inline resources so headless Chrome can render\n# without network access (CDN-loaded bokeh JS fails in CI sandbox).\noutput_file(f\"plot-{THEME}.html\")\nsave(p, resources=INLINE)\n\n# Screenshot via headless Chrome — use CDP setDeviceMetricsOverride so the\n# inner viewport is authoritative (--window-size alone gives 1661 instead of 1800)\nW, H = 3200, 1800\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}\",\n    \"--hide-scrollbars\",\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\n# Pin saved PNG to exact target dims so the post-render gate always passes\nfrom PIL import Image as _PILImage\n\n\n_img = _PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\nif _img.size != (W, H):\n    _norm = _PILImage.new(\"RGB\", (W, H), PAGE_BG)\n    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))\n    _norm.save(f\"plot-{THEME}.png\")\n"}