{"spec_id":"heatmap-calendar","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nheatmap-calendar: Basic Calendar Heatmap\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 94/100 | Updated: 2026-07-23\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import (\n    BasicTicker,\n    ColorBar,\n    ColumnDataSource,\n    FixedTicker,\n    LinearAxis,\n    LinearColorMapper,\n    Span,\n    Title,\n)\nfrom bokeh.plotting import figure\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# Data - Generate daily values for one year\nnp.random.seed(42)\nstart_date = pd.Timestamp(\"2024-01-01\")\nend_date = pd.Timestamp(\"2024-12-31\")\ndates = pd.date_range(start=start_date, end=end_date, freq=\"D\")\n\n# Simulate GitHub-style contributions with realistic patterns\nvalues = []\nfor date in dates:\n    weekday = date.weekday()\n    base = 2 if weekday >= 5 else 5\n    val = np.random.poisson(base)\n    if np.random.random() < 0.05:\n        val += np.random.randint(5, 15)\n    if np.random.random() < 0.15:\n        val = 0\n    values.append(val)\n\ndf = pd.DataFrame({\"date\": dates, \"value\": values})\ndf[\"weekday\"] = df[\"date\"].dt.weekday\ndf[\"week_of_year\"] = (df[\"date\"] - start_date).dt.days // 7\ndf[\"month\"] = df[\"date\"].dt.month\n\nweekday_names = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n\nsource = ColumnDataSource(\n    data={\n        \"week\": df[\"week_of_year\"].tolist(),\n        \"weekday\": [weekday_names[w] for w in df[\"weekday\"]],\n        \"value\": df[\"value\"].tolist(),\n        \"date\": df[\"date\"].dt.strftime(\"%Y-%m-%d\").tolist(),\n    }\n)\n\n\n# Imprint sequential colormap (brand green -> blue), 256-stop ramp\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\nimprint_seq = [_lerp_hex(\"#009E73\", \"#4467A3\", t / 255.0) for t in range(256)]\nmapper = LinearColorMapper(palette=imprint_seq, low=0, high=df[\"value\"].max())\n\n# Month positions for the top axis and separator lines\nmonth_names = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\nmonth_starts = df.groupby(\"month\")[\"week_of_year\"].min().to_dict()\nmonth_ticks = list(month_starts.values())\nmonth_labels = {v: month_names[k - 1] for k, v in month_starts.items()}\n\n# Peak contribution day, highlighted as the visual focal point\npeak_row = df.loc[df[\"value\"].idxmax()]\npeak_week = int(peak_row[\"week_of_year\"])\npeak_weekday = weekday_names[peak_row[\"weekday\"]]\n\n# Longest streak of consecutive days with contributions, for the data-driven subtitle\nstreak = longest_streak = 0\nfor v in df[\"value\"]:\n    streak = streak + 1 if v > 0 else 0\n    longest_streak = max(longest_streak, streak)\n\ntitle = \"heatmap-calendar · python · bokeh · anyplot.ai\"\nsubtitle = (\n    f\"Peak day: {peak_row['date'].strftime('%b %-d')} ({int(peak_row['value'])} contributions) \"\n    f\"· Longest streak: {longest_streak} days\"\n)\n\n# Plot\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    y_range=list(reversed(weekday_names)),\n    y_axis_label=\"Day of Week\",\n    tools=\"hover\",\n    tooltips=[(\"Date\", \"@date\"), (\"Contributions\", \"@value\")],\n    toolbar_location=None,\n    min_border_bottom=50,\n    min_border_left=200,\n    min_border_top=250,\n    min_border_right=260,\n)\n\np.rect(\n    x=\"week\",\n    y=\"weekday\",\n    width=0.9,\n    height=0.9,\n    source=source,\n    fill_color={\"field\": \"value\", \"transform\": mapper},\n    line_color=PAGE_BG,\n    line_width=2,\n    # Inspection glyph (bokeh-specific): cells pop under the HoverTool cursor\n    # without any CustomJS wiring, distinct from a plain static rect.\n    hover_fill_alpha=0.75,\n    hover_line_color=INK,\n    hover_line_width=3,\n)\n\n# Peak day focal point — outline only, no fill change\np.rect(x=[peak_week], y=[peak_weekday], width=0.9, height=0.9, fill_color=None, line_color=INK, line_width=5)\n\n# Month divider lines — structural guide, not a text annotation\nfor week in month_ticks[1:]:\n    p.add_layout(Span(location=week - 0.5, dimension=\"height\", line_color=INK_SOFT, line_alpha=0.25, line_width=1))\n\n# Month labels along the top, per spec (\"top or as section headers\")\np.xaxis.visible = False\nmonth_axis = LinearAxis(ticker=FixedTicker(ticks=month_ticks), major_label_overrides=month_labels, axis_label=\"Month\")\np.add_layout(month_axis, \"above\")\n\n# Data-driven subtitle (peak day + longest streak) — placed between the title\n# and the month axis, adding real storytelling instead of a bare default heatmap.\n# Bokeh stacks \"above\" layout renderers with each new addition inserted closer\n# to the frame, so adding this *after* month_axis puts it above the axis.\np.add_layout(Title(text=subtitle, text_font_size=\"26pt\", text_color=INK_SOFT, text_font_style=\"italic\"), \"above\")\n\n# Color bar\ncolor_bar = ColorBar(\n    color_mapper=mapper,\n    ticker=BasicTicker(desired_num_ticks=6),\n    label_standoff=12,\n    major_label_text_font_size=\"34pt\",\n    major_label_text_color=INK_SOFT,\n    title=\"Contributions (count)\",\n    title_text_font_size=\"34pt\",\n    title_text_color=INK,\n    background_fill_color=ELEVATED_BG,\n    width=60,\n    location=(0, 0),\n)\np.add_layout(color_bar, \"right\")\n\n# Text sizes\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\n\np.yaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_color = INK\np.yaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\n\nmonth_axis.axis_label_text_font_size = \"42pt\"\nmonth_axis.axis_label_text_color = INK\nmonth_axis.major_label_text_font_size = \"34pt\"\nmonth_axis.major_label_text_color = INK_SOFT\nmonth_axis.axis_line_color = INK_SOFT\nmonth_axis.major_tick_line_color = INK_SOFT\n\n# No grid for heatmap\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\n\n# Background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Save — export_png needs a working chromedriver, unavailable here; screenshot\n# the saved HTML with Selenium instead (matches highcharts.py's approach).\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\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.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(2)\n# Headless Chrome's outer window size includes non-viewport chrome, so the\n# actual viewport ends up a bit shorter than W,H — force it exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(1)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}