{"spec_id":"bar-heart-rate-zones","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbar-heart-rate-zones: Time in Heart Rate Zones Bar Chart\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 91/100 | Created: 2026-06-14\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove current dir from import path so bokeh.py doesn't shadow the bokeh package\nsys.path = [p for p in sys.path if p != \"\" and not (os.path.isfile(os.path.join(p, \"bokeh.py\")) if p else False)]\n\nimport io\n\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, FixedTicker, LabelSet, Range1d\nfrom bokeh.plotting import figure\nfrom PIL import Image\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint palette — see prompts/default-style-guide.md)\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# Zone colors — semantic exception: conventional heart rate zone colors map to Imprint roles\n# Z1→grey (Imprint muted), Z2→blue (Imprint pos 3), Z3→green (Imprint brand pos 1),\n# Z4→amber (Imprint warning anchor — caution/threshold), Z5→red (Imprint semantic anchor)\nZONE_COLORS = [INK_MUTED, \"#4467A3\", \"#009E73\", \"#DDCC77\", \"#AE3030\"]\n\n# Data — 60-minute tempo run\nzones = [\"Z1 Recovery\", \"Z2 Endurance\", \"Z3 Aerobic\", \"Z4 Threshold\", \"Z5 Maximum\"]\nminutes_data = [8, 22, 15, 12, 3]\nhr_ranges_text = [\"< 115 bpm\", \"115–138 bpm\", \"138–152 bpm\", \"152–166 bpm\", \"> 166 bpm\"]\nduration_labels = [f\"{m} min\" for m in minutes_data]\n\nsource = ColumnDataSource(\n    data={\n        \"zones\": zones,\n        \"minutes\": minutes_data,\n        \"colors\": ZONE_COLORS,\n        \"durations\": duration_labels,\n        \"hr_ranges\": hr_ranges_text,\n    }\n)\n\n# Title — fontsize scaled for length (default 50pt for ~67-char title)\ntitle = \"60-Minute Tempo Run · bar-heart-rate-zones · python · bokeh · anyplot.ai\"\nn = len(title)\ntitle_fontsize = f\"{max(34, round(50 * 67 / n))}pt\"\n\n# Figure — 3200×1800 landscape (toolbar_location=None avoids height drift)\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_range=zones,\n    y_range=Range1d(start=0, end=27),\n    y_axis_label=\"Time (minutes)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Bars\np.vbar(x=\"zones\", top=\"minutes\", width=0.65, source=source, color=\"colors\", line_color=None)\n\n# Duration labels — just above each bar\nduration_set = LabelSet(\n    x=\"zones\",\n    y=\"minutes\",\n    text=\"durations\",\n    source=source,\n    y_offset=12,\n    text_align=\"center\",\n    text_baseline=\"bottom\",\n    text_font_size=\"36pt\",\n    text_font_style=\"bold\",\n    text_color=INK,\n)\np.add_layout(duration_set)\n\n# HR range labels — above the duration labels\nhr_set = LabelSet(\n    x=\"zones\",\n    y=\"minutes\",\n    text=\"hr_ranges\",\n    source=source,\n    y_offset=72,\n    text_align=\"center\",\n    text_baseline=\"bottom\",\n    text_font_size=\"26pt\",\n    text_color=INK_SOFT,\n)\np.add_layout(hr_set)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.title.text_font_size = title_fontsize\np.title.text_color = INK\np.title.text_font_style = \"normal\"\n\np.yaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_font_style = \"normal\"\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\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\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\np.ygrid.ticker = FixedTicker(ticks=[0, 5, 10, 15, 20, 25])\np.yaxis.ticker = FixedTicker(ticks=[0, 5, 10, 15, 20, 25])\n\n# Save HTML artifact\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome (Selenium 4 / Selenium Manager)\n# Window is set taller than the 1800px target to absorb any OS/browser chrome\n# overhead; PIL crops the screenshot to exactly 3200×1800 to pass the canvas gate.\nFIG_W, FIG_H = 3200, 1800\nWIN_W, WIN_H = FIG_W, FIG_H + 200\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    \"--force-device-scale-factor=1\",\n    f\"--window-size={WIN_W},{WIN_H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(WIN_W, WIN_H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\nraw = driver.get_screenshot_as_png()\ndriver.quit()\nimg = Image.open(io.BytesIO(raw))\nimg.crop((0, 0, FIG_W, FIG_H)).save(f\"plot-{THEME}.png\")\n"}