{"spec_id":"heatmap-stripes-climate","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nheatmap-stripes-climate: Climate Warming Stripes\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove script's own directory from sys.path so 'bokeh' resolves to the installed package\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nif _this_dir in sys.path:\n    sys.path.remove(_this_dir)\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import HoverTool, LinearColorMapper, Range1d\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-adaptive chrome — Imprint palette\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\n\n# Data: synthetic global temperature anomalies (1850–2024) relative to 1961–1990 baseline\nnp.random.seed(42)\nyears = np.arange(1850, 2025)\nn_years = len(years)\n\nbase_trend = np.concatenate(\n    [\n        np.linspace(-0.3, -0.2, 60),\n        np.linspace(-0.2, -0.1, 40),\n        np.linspace(-0.1, 0.0, 25),\n        np.linspace(0.0, 0.4, 25),\n        np.linspace(0.4, 1.2, 25),\n    ]\n)\nnoise = np.random.normal(0, 0.1, n_years)\nanomalies = base_trend + noise\n\n# Symmetric color range centered at 0\nvmax = np.ceil(max(abs(anomalies.min()), abs(anomalies.max())) * 10) / 10\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 = int(round(r0 + (r1 - r0) * t))\n    g = int(round(g0 + (g1 - g0) * t))\n    b = int(round(b0 + (b1 - b0) * t))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\n# Imprint diverging palette: #4467A3 (blue/cold) → neutral midpoint → #AE3030 (red/warm)\n# Negative anomalies = cold = blue; positive anomalies = warm = red\n# Dark midpoint uses #3A3A37 (not #1A1A17) so near-zero stripes remain visible against dark bg\n_midpoint = \"#FAF8F1\" if THEME == \"light\" else \"#3A3A37\"\nANYPLOT_DIV256 = [_lerp_hex(\"#4467A3\", _midpoint, t / 127.0) for t in range(128)] + [\n    _lerp_hex(_midpoint, \"#AE3030\", t / 127.0) for t in range(128)\n]\n\ncolor_mapper = LinearColorMapper(palette=ANYPLOT_DIV256, low=-vmax, high=vmax)\n\n# Reshape anomalies as 2D image (1 row × n_years columns) for seamless stripe rendering\nimg_data = anomalies.reshape(1, -1)\n\n# Canvas: 3200×1800 px landscape (hard contract — no deviation)\nW, H = 3200, 1800\n\np = figure(\n    width=W,\n    height=H,\n    title=\"heatmap-stripes-climate · python · bokeh · anyplot.ai\",\n    tools=\"\",\n    toolbar_location=None,\n    x_range=Range1d(years[0], years[-1] + 1),\n    y_range=Range1d(0, 1),\n    min_border_left=0,\n    min_border_right=0,\n    min_border_top=110,  # room for 50pt title\n    min_border_bottom=0,  # no x-axis — extend stripes to bottom edge\n)\n\np.image(image=[img_data], x=years[0], y=0, dw=n_years, dh=1, color_mapper=color_mapper)\n\n# Minimalist style: no axes, no labels, no gridlines, no ticks — pure color encoding\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"normal\"\np.title.align = \"center\"\np.title.text_color = INK\n\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\np.outline_line_color = None\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Interactive hover for HTML version — shows year and anomaly on hover\nhover = HoverTool(tooltips=[(\"Year\", \"$x{0}\"), (\"Anomaly\", \"@image{+0.00}°C\")])\np.add_tools(hover)\n\n# Save interactive HTML (required artifact for interactive libraries)\noutput_file(f\"plot-{THEME}.html\")\nsave(p, title=\"Climate Warming Stripes\")\n\n# Screenshot with headless Chrome via Selenium (do NOT use export_png — chromedriver snap issues)\n# Use a taller window to accommodate browser overhead, then use CDP to override viewport exactly.\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)\n# Override viewport to exactly W×H so the full Bokeh canvas is in-view\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)  # let Bokeh JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}