{"spec_id":"polar-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\npolar-basic: Basic Polar Chart\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-07-24\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file (bokeh.py) from shadowing the installed bokeh package\n_here = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))\nsys.path = [p for p in sys.path if os.path.normpath(os.path.abspath(p or \".\")) != _here]\n\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, TapTool\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\")\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # Imprint palette position 1 — ALWAYS first series\n\nIMPL_DIR = os.path.dirname(os.path.abspath(__file__))\n\n# Data — hourly outdoor temperature over a 24-hour cycle\nnp.random.seed(42)\nhours = np.arange(24)\ntheta = hours * (2 * np.pi / 24)\nbase_temp = 15 + 8 * np.sin(theta - 5 * np.pi / 6)  # peak ~16:00, trough ~04:00\ntemperature = base_temp + np.random.normal(0, 0.7, 24)\nmin_temp = temperature.min()\nradius = temperature - min_temp + 2  # shift to strictly positive values\n\n# Bokeh has no native polar projection — convert to Cartesian manually.\n# Midnight (0h) sits at the top and hours advance clockwise, like a clock face.\nangle = np.pi / 2 - theta\nx = radius * np.cos(angle)\ny = radius * np.sin(angle)\nx_closed = np.append(x, x[0])\ny_closed = np.append(y, y[0])\n\nsource = ColumnDataSource(data={\"x\": x, \"y\": y, \"hour\": [f\"{h:02d}:00\" for h in hours], \"temp\": temperature.round(1)})\n\n# Plot — square canvas suits the chart's radial symmetry\nmax_radius = np.ceil(radius.max()) + 1\nlabel_r = max_radius + 1.6\ncanvas_limit = label_r + 2.5\n\np = figure(\n    width=2400,\n    height=2400,\n    title=\"polar-basic · python · bokeh · anyplot.ai\",\n    x_range=(-canvas_limit, canvas_limit),\n    y_range=(-canvas_limit, canvas_limit),\n    match_aspect=True,\n    toolbar_location=None,\n    min_border=60,\n)\n\n# Concentric radial gridlines with temperature scale labels — placed along the\n# lowest-value spoke so the label column stays clear of the filled area no\n# matter where the curve's minimum happens to fall.\nlabel_angle = angle[np.argmin(radius)]\nlabel_align = \"left\" if np.cos(label_angle) >= 0 else \"right\"\ngrid_radii = np.linspace(0, max_radius, 5)[1:]\ncircle_theta = np.linspace(0, 2 * np.pi, 120)\nfor r in grid_radii:\n    p.line(r * np.cos(circle_theta), r * np.sin(circle_theta), line_color=INK, line_width=2, line_alpha=0.10)\n    p.text(\n        x=[r * np.cos(label_angle)],\n        y=[r * np.sin(label_angle)],\n        text=[f\"{r + min_temp - 2:.0f}°C\"],\n        text_align=label_align,\n        text_baseline=\"middle\",\n        text_font_size=\"34pt\",\n        text_color=INK_MUTED,\n    )\n\n# Spoke gridlines + hour labels at 3-hour intervals\nfor h in range(0, 24, 3):\n    a = np.pi / 2 - h * (2 * np.pi / 24)\n    p.line([0, max_radius * np.cos(a)], [0, max_radius * np.sin(a)], line_color=INK, line_width=2, line_alpha=0.10)\n    p.text(\n        x=[label_r * np.cos(a)],\n        y=[label_r * np.sin(a)],\n        text=[f\"{h:02d}:00\"],\n        text_align=\"center\",\n        text_baseline=\"middle\",\n        text_font_size=\"34pt\",\n        text_color=INK_SOFT,\n    )\n\n# Filled area under the temperature curve — stronger fill on dark bg for contrast\np.patch(x_closed, y_closed, fill_color=BRAND, fill_alpha=0.30 if THEME == \"light\" else 0.42, line_color=None)\n\n# Closed data line + points (points carry the ColumnDataSource for hover + tap)\np.line(x_closed, y_closed, line_color=BRAND, line_width=5, line_alpha=0.9)\npoints = p.scatter(\n    x=\"x\",\n    y=\"y\",\n    source=source,\n    size=22,\n    color=BRAND,\n    line_color=PAGE_BG,\n    line_width=2,\n    # TapTool selection styling — clicking an hour makes it pop, dims the rest\n    selection_fill_color=IMPRINT_PALETTE[3],\n    selection_line_color=INK,\n    nonselection_fill_alpha=0.55,\n    nonselection_line_alpha=0.55,\n)\n\np.add_tools(HoverTool(renderers=[points], tooltips=[(\"Hour\", \"@hour\"), (\"Temperature\", \"@temp °C\")]))\np.add_tools(TapTool(renderers=[points]))\n\n# Style\np.title.text_font_size = \"50pt\"\np.title.align = \"center\"\np.title.text_color = INK\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\n\n# Save — HTML (interactive, with hover) + PNG (headless Chrome screenshot)\noutput_file(os.path.join(IMPL_DIR, f\"plot-{THEME}.html\"), title=\"polar-basic · python · bokeh · anyplot.ai\")\nsave(p)\n\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(os.path.join(IMPL_DIR, f'plot-{THEME}.html')).resolve()}\")\n# headless Chrome's --window-size sets the OUTER window; pin the viewport via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(os.path.join(IMPL_DIR, f\"plot-{THEME}.png\"))\ndriver.quit()\n"}