{"spec_id":"heatmap-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nheatmap-basic: Basic Heatmap\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-28\n\"\"\"\n\nimport base64\nimport io\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove this script's directory from sys.path so \"bokeh.py\" doesn't shadow the bokeh package\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import BasicTicker, ColumnDataSource, HoverTool, LabelSet\nfrom bokeh.plotting import figure\nfrom bokeh.transform import linear_cmap\nfrom PIL import Image\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\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\n# Imprint diverging colormap — 256-stop ramp, theme-adaptive midpoint (red → neutral → blue)\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_mid = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nIMPRINT_DIV256 = [_lerp_hex(\"#AE3030\", _mid, t / 127.0) for t in range(128)] + [\n    _lerp_hex(_mid, \"#4467A3\", t / 127.0) for t in range(128)\n]\n\n# Data — monthly temperature anomalies (°C) for 7 cities across all 12 months\nnp.random.seed(42)\nmonths = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\ncities = [\"Oslo\", \"Berlin\", \"Madrid\", \"Cairo\", \"Mumbai\", \"Tokyo\", \"Sydney\"]\n\nbase_anomalies = np.random.randn(len(cities), len(months)) * 0.6\nfor i, city in enumerate(cities):\n    seasonal = np.sin(np.linspace(-np.pi / 2, 3 * np.pi / 2, len(months)))\n    if city in (\"Oslo\", \"Berlin\"):\n        base_anomalies[i] += seasonal * 1.5 - 0.3\n    elif city in (\"Madrid\", \"Cairo\"):\n        base_anomalies[i] += seasonal * 1.2 + 0.4\n    elif city == \"Mumbai\":\n        base_anomalies[i] += 0.8\n    elif city == \"Sydney\":\n        base_anomalies[i] -= seasonal * 0.9\n    elif city == \"Tokyo\":\n        base_anomalies[i] += seasonal * 0.7\n\nvalues = np.round(base_anomalies, 1)\n\nrecords = []\nfor i, city in enumerate(cities):\n    for j, month in enumerate(months):\n        val = values[i, j]\n        # Light theme: dark text on near-zero (cream) cells, light text on deep red/blue cells\n        tc = (\"#F0EFE8\" if abs(val) > 1.0 else \"#1A1A17\") if THEME == \"light\" else \"#F0EFE8\"\n        records.append({\"month\": month, \"city\": city, \"anomaly\": val, \"label\": f\"{val:+.1f}\", \"text_color\": tc})\n\nsource = ColumnDataSource(pd.DataFrame(records))\n\n# Color mapping — Imprint diverging palette\ncmap = linear_cmap(\"anomaly\", IMPRINT_DIV256, low=-2.5, high=2.5)\n\n# Figure — square canvas for symmetric grid\ntitle = \"heatmap-basic · python · bokeh · anyplot.ai\"\np = figure(\n    width=2400,\n    height=2400,\n    x_range=months,\n    y_range=list(reversed(cities)),\n    title=title,\n    x_axis_label=\"Month (2024)\",\n    y_axis_label=\"City\",\n    toolbar_location=None,\n    tools=\"\",\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=300,\n)\n\n# Heatmap rectangles\nr = p.rect(x=\"month\", y=\"city\", width=1, height=1, source=source, fill_color=cmap, line_color=\"white\", line_width=2)\n\n# Cell value annotations\nlabels = LabelSet(\n    x=\"month\",\n    y=\"city\",\n    text=\"label\",\n    text_color=\"text_color\",\n    source=source,\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_font_size=\"26pt\",\n)\np.add_layout(labels)\n\n# Storytelling callout: highlight the most extreme anomaly cell with a bold border + label\nextreme_idx = np.unravel_index(np.argmax(np.abs(values)), values.shape)\nextreme_city = cities[extreme_idx[0]]\nextreme_month = months[extreme_idx[1]]\nextreme_val = values[extreme_idx[0], extreme_idx[1]]\np.rect(\n    x=[extreme_month], y=[extreme_city], width=1, height=1, fill_color=None, fill_alpha=0, line_color=INK, line_width=8\n)\n\n# Color bar (construct from renderer — idiomatic Bokeh)\ncolor_bar = r.construct_color_bar(\n    width=50,\n    ticker=BasicTicker(desired_num_ticks=10),\n    label_standoff=16,\n    major_label_text_font_size=\"34pt\",\n    major_label_text_color=INK_SOFT,\n    border_line_color=None,\n    padding=20,\n    title=\"Anomaly (°C)\",\n    title_text_font_size=\"34pt\",\n    title_text_color=INK,\n    title_standoff=20,\n    background_fill_color=PAGE_BG,\n)\np.add_layout(color_bar, \"right\")\n\n# HoverTool for interactive HTML\nhover = HoverTool(tooltips=[(\"City\", \"@city\"), (\"Month\", \"@month\"), (\"Anomaly\", \"@anomaly{+0.0} °C\")], renderers=[r])\np.add_tools(hover)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\n\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\n\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\n\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.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Save PNG via headless Chrome (Selenium — export_png unavailable in this env)\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}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\nscreenshot = driver.execute_cdp_cmd(\"Page.captureScreenshot\", {\"format\": \"png\", \"captureBeyondViewport\": True})\ndriver.quit()\nImage.open(io.BytesIO(base64.b64decode(screenshot[\"data\"]))).crop((0, 0, W, H)).save(f\"plot-{THEME}.png\")\n"}