{"spec_id":"polar-scatter","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\npolar-scatter: Polar Scatter Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-05-09\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 ColumnDataSource, HoverTool, Legend, LegendItem\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# Okabe-Ito palette (categorical)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data - Wind measurements with prevailing directions\nnp.random.seed(42)\n\nn_points = 120\n\n# Create realistic wind data with prevailing directions (NE and SW)\n# Morning winds (prevailing from NE ~45°)\nn_morning = 50\nmorning_angles = np.random.normal(45, 30, n_morning) % 360\nmorning_speeds = np.random.gamma(3, 3, n_morning) + 5\n\n# Afternoon winds (prevailing from SW ~225°)\nn_afternoon = 45\nafternoon_angles = np.random.normal(225, 35, n_afternoon) % 360\nafternoon_speeds = np.random.gamma(2.5, 4, n_afternoon) + 3\n\n# Evening winds (variable)\nn_evening = 25\nevening_angles = np.random.uniform(0, 360, n_evening)\nevening_speeds = np.random.gamma(2, 2, n_evening) + 2\n\n# Combine data\nangles_deg = np.concatenate([morning_angles, afternoon_angles, evening_angles])\nspeeds = np.concatenate([morning_speeds, afternoon_speeds, evening_speeds])\ncategories = [\"Morning\"] * n_morning + [\"Afternoon\"] * n_afternoon + [\"Evening\"] * n_evening\n\n# Convert to radians for polar coordinates\nangles_rad = np.radians(angles_deg)\n\n# Convert polar to Cartesian for Bokeh (Bokeh doesn't have native polar support)\nx = speeds * np.cos(angles_rad)\ny = speeds * np.sin(angles_rad)\n\n# Calculate max radius for gridlines\nmax_speed = np.ceil(np.max(speeds) / 5) * 5\n\n# Create figure\np = figure(\n    width=3600,\n    height=3600,\n    title=\"polar-scatter · bokeh · anyplot.ai\",\n    x_range=(-max_speed * 1.2, max_speed * 1.2),\n    y_range=(-max_speed * 1.35, max_speed * 1.2),\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Draw polar grid - radial circles\nfor r in np.arange(5, max_speed + 1, 5):\n    theta_circle = np.linspace(0, 2 * np.pi, 100)\n    circle_x = r * np.cos(theta_circle)\n    circle_y = r * np.sin(theta_circle)\n    p.line(circle_x, circle_y, line_color=INK_SOFT, line_width=2, line_alpha=0.15)\n    # Add radius labels at 45 degrees\n    label_x = r * np.cos(np.radians(45)) + 0.5\n    label_y = r * np.sin(np.radians(45)) + 0.5\n    p.text([label_x], [label_y], text=[f\"{int(r)}\"], text_font_size=\"20pt\", text_color=INK_SOFT, text_alpha=0.8)\n\n# Draw angular gridlines (spokes) at 30° intervals\nfor angle in range(0, 360, 30):\n    rad = np.radians(angle)\n    p.line(\n        [0, max_speed * 1.05 * np.cos(rad)],\n        [0, max_speed * 1.05 * np.sin(rad)],\n        line_color=INK_SOFT,\n        line_width=2,\n        line_alpha=0.15,\n    )\n\n# Add cardinal direction labels\ndirections = {0: \"E\", 90: \"N\", 180: \"W\", 270: \"S\"}\nfor angle, label in directions.items():\n    rad = np.radians(angle)\n    label_r = max_speed * 1.1\n    p.text(\n        [label_r * np.cos(rad)],\n        [label_r * np.sin(rad)],\n        text=[label],\n        text_font_size=\"24pt\",\n        text_font_style=\"bold\",\n        text_color=INK,\n        text_align=\"center\",\n        text_baseline=\"middle\",\n    )\n\n# Add intermediate direction labels\nintermediate = {45: \"NE\", 135: \"NW\", 225: \"SW\", 315: \"SE\"}\nfor angle, label in intermediate.items():\n    rad = np.radians(angle)\n    label_r = max_speed * 1.1\n    p.text(\n        [label_r * np.cos(rad)],\n        [label_r * np.sin(rad)],\n        text=[label],\n        text_font_size=\"20pt\",\n        text_color=INK_SOFT,\n        text_align=\"center\",\n        text_baseline=\"middle\",\n    )\n\n# Add label for radius axis\np.text(\n    [0],\n    [-max_speed * 1.25],\n    text=[\"Wind Speed (m/s)\"],\n    text_font_size=\"22pt\",\n    text_color=INK_SOFT,\n    text_align=\"center\",\n    text_baseline=\"top\",\n)\n\n# Create scatter plot for each category with hover tooltips\nlegend_items = []\nrenderers = []\nfor i, cat in enumerate([\"Morning\", \"Afternoon\", \"Evening\"]):\n    mask = np.array(categories) == cat\n    source = ColumnDataSource(\n        data={\n            \"x\": x[mask],\n            \"y\": y[mask],\n            \"angle\": angles_deg[mask],\n            \"speed\": speeds[mask],\n            \"category\": [cat] * np.sum(mask),\n        }\n    )\n\n    # Add hover tool for this renderer\n    hover = HoverTool(tooltips=[(\"Direction\", \"@angle{0.0}°\"), (\"Speed\", \"@speed{0.0} m/s\"), (\"Category\", \"@category\")])\n\n    r = p.scatter(\"x\", \"y\", source=source, size=24, color=IMPRINT[i], alpha=0.75, line_color=PAGE_BG, line_width=2)\n    p.add_tools(hover)\n    renderers.append(r)\n    legend_items.append(LegendItem(label=cat, renderers=[r]))\n\n# Add legend\nlegend = Legend(\n    items=legend_items,\n    location=\"top_right\",\n    label_text_font_size=\"20pt\",\n    spacing=15,\n    glyph_height=28,\n    glyph_width=28,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=INK_SOFT,\n    padding=15,\n    margin=15,\n    border_line_width=2,\n)\np.add_layout(legend, \"right\")\n\n# Styling\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.title.align = \"center\"\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Hide axes (using polar grid instead)\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\n\n# Save as HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome\nW, H = 3600, 3600\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)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}