{"spec_id":"heatmap-polar","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nheatmap-polar: Polar Heatmap for Cyclic Two-Dimensional Data\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 84/100 | Created: 2026-05-13\n\"\"\"\n\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColorBar, ColumnDataSource, Label, LinearColorMapper\nfrom bokeh.palettes import Viridis256\nfrom bokeh.plotting import figure\nfrom bokeh.transform import transform as bk_transform\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\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: hourly website visits by day of week\nnp.random.seed(42)\nn_hours = 24\nn_days = 7\ndays = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"]\n\ntraffic = np.zeros((n_days, n_hours))\nfor d in range(n_days):\n    for h in range(n_hours):\n        if d < 5:\n            morning = 820 * np.exp(-0.5 * ((h - 9) / 1.5) ** 2)\n            evening = 640 * np.exp(-0.5 * ((h - 20) / 2.0) ** 2)\n            traffic[d, h] = 100 + morning + evening\n        else:\n            afternoon = 510 * np.exp(-0.5 * ((h - 14) / 3.0) ** 2)\n            traffic[d, h] = 80 + afternoon\n        traffic[d, h] += np.random.normal(0, 25)\n        traffic[d, h] = max(20, traffic[d, h])\n\n# Build polar heatmap segments\nmin_r = 0.28\nmax_r = 1.05\nring_width = (max_r - min_r) / n_days\n\ninner_radii, outer_radii, start_angles, end_angles, cell_values = [], [], [], [], []\n\nfor d in range(n_days):\n    ir = min_r + d * ring_width + 0.004\n    or_ = min_r + (d + 1) * ring_width - 0.004\n    for h in range(n_hours):\n        sa = np.pi / 2 - 2 * np.pi * h / n_hours\n        ea = np.pi / 2 - 2 * np.pi * (h + 1) / n_hours\n        inner_radii.append(ir)\n        outer_radii.append(or_)\n        start_angles.append(sa)\n        end_angles.append(ea)\n        cell_values.append(traffic[d, h])\n\nsource = ColumnDataSource(\n    data={\n        \"inner_radius\": inner_radii,\n        \"outer_radius\": outer_radii,\n        \"start_angle\": start_angles,\n        \"end_angle\": end_angles,\n        \"value\": cell_values,\n    }\n)\n\ncolor_mapper = LinearColorMapper(palette=Viridis256, low=min(cell_values), high=max(cell_values))\n\n# Plot\nW, H = 3600, 3600\np = figure(\n    width=W,\n    height=H,\n    x_range=(-1.42, 1.42),\n    y_range=(-1.42, 1.42),\n    title=\"Website Traffic · heatmap-polar · bokeh · anyplot.ai\",\n    toolbar_location=None,\n)\n\np.annular_wedge(\n    x=0,\n    y=0,\n    inner_radius=\"inner_radius\",\n    outer_radius=\"outer_radius\",\n    start_angle=\"start_angle\",\n    end_angle=\"end_angle\",\n    direction=\"clock\",\n    fill_color=bk_transform(\"value\", color_mapper),\n    line_color=PAGE_BG,\n    line_width=0.6,\n    source=source,\n)\n\n# Spoke lines at major hour boundaries (every 3 hours)\nfor h in range(0, 24, 3):\n    angle = np.pi / 2 - 2 * np.pi * h / n_hours\n    cx, cy = np.cos(angle), np.sin(angle)\n    p.segment(\n        x0=[min_r * cx],\n        y0=[min_r * cy],\n        x1=[max_r * cx],\n        y1=[max_r * cy],\n        line_color=PAGE_BG,\n        line_width=1.5,\n        line_alpha=0.7,\n    )\n\n# Hour labels (every 3 hours, clock layout: midnight top, 6am right, noon bottom, 6pm left)\nfor h in range(0, 24, 3):\n    angle = np.pi / 2 - 2 * np.pi * h / n_hours\n    lx = 1.20 * np.cos(angle)\n    ly = 1.20 * np.sin(angle)\n    hour_12 = h % 12 or 12\n    suffix = \"am\" if h < 12 else \"pm\"\n    p.add_layout(\n        Label(\n            x=lx,\n            y=ly,\n            text=f\"{hour_12}{suffix}\",\n            text_align=\"center\",\n            text_baseline=\"middle\",\n            text_font_size=\"24px\",\n            text_color=INK_SOFT,\n        )\n    )\n\n# Day labels placed at the centroid of the midnight cell for each ring\ncell_angle = np.pi / 2 - np.pi / n_hours  # centre of hour-0 wedge (7.5° from top)\nfor d, day in enumerate(days):\n    mid_r = min_r + d * ring_width + ring_width / 2\n    lx = mid_r * np.cos(cell_angle)\n    ly = mid_r * np.sin(cell_angle)\n    p.add_layout(\n        Label(\n            x=lx,\n            y=ly,\n            text=day,\n            text_align=\"center\",\n            text_baseline=\"middle\",\n            text_font_size=\"20px\",\n            text_color=\"white\",\n            text_font_style=\"bold\",\n        )\n    )\n\n# Colorbar\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    width=40,\n    padding=15,\n    location=(0, 0),\n    title=\"Hourly Visits\",\n    title_text_font_size=\"22px\",\n    title_text_color=INK,\n    major_label_text_font_size=\"20px\",\n    major_label_text_color=INK_SOFT,\n    background_fill_color=PAGE_BG,\n)\np.add_layout(color_bar, \"right\")\n\n# Chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = \"28px\"\np.title.text_color = INK\np.title.text_font_style = \"normal\"\np.title.align = \"center\"\n\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = None\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome via Selenium\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)\n\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}