{"spec_id":"radar-multi","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nradar-multi: Multi-Series Radar Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-07\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, LabelSet, Legend\nfrom bokeh.plotting import figure\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\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data - Product comparison across 6 attributes\ncategories = [\"Performance\", \"Reliability\", \"Features\", \"Support\", \"Price Value\", \"Ease of Use\"]\nn_categories = len(categories)\n\n# Three products to compare\nproducts = {\n    \"Product A\": [85, 90, 75, 80, 70, 88],\n    \"Product B\": [70, 75, 95, 85, 80, 72],\n    \"Product C\": [92, 65, 80, 70, 95, 78],\n}\n\n# Calculate angles for each axis (starting from top, going clockwise)\nangles = np.linspace(0, 2 * np.pi, n_categories, endpoint=False)\nangles = angles + np.pi / 2\n\n# Create figure with square aspect for radar chart\np = figure(\n    width=3600,\n    height=3600,\n    title=\"radar-multi · bokeh · anyplot.ai\",\n    x_range=(-145, 145),\n    y_range=(-145, 145),\n    tools=\"\",\n    toolbar_location=None,\n)\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Remove axes and grid\np.axis.visible = False\np.grid.visible = False\n\n# Draw circular gridlines at 20, 40, 60, 80, 100\ngrid_values = [20, 40, 60, 80, 100]\nfor gv in grid_values:\n    theta = np.linspace(0, 2 * np.pi, 100)\n    x_grid = gv * np.cos(theta)\n    y_grid = gv * np.sin(theta)\n    p.line(x_grid, y_grid, line_color=INK_SOFT, line_width=2, line_alpha=0.4)\n\n# Draw radial lines from center to each axis\nfor angle in angles:\n    x_line = [0, 105 * np.cos(angle)]\n    y_line = [0, 105 * np.sin(angle)]\n    p.line(x_line, y_line, line_color=INK_SOFT, line_width=2, line_alpha=0.3)\n\n# Add axis labels at the outer edge\nlabel_radius = 125\nlabel_x = [label_radius * np.cos(a) for a in angles]\nlabel_y = [label_radius * np.sin(a) for a in angles]\nlabel_source = ColumnDataSource(data={\"x\": label_x, \"y\": label_y, \"text\": categories})\nlabels = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"text\",\n    source=label_source,\n    text_font_size=\"24pt\",\n    text_align=\"center\",\n    text_baseline=\"middle\",\n    text_color=INK,\n    text_font_style=\"bold\",\n)\np.add_layout(labels)\n\n# Add grid value labels on one axis (shifted for visibility)\nfor gv in grid_values:\n    p.text(\n        x=[gv * np.cos(angles[0]) + 10],\n        y=[gv * np.sin(angles[0]) + 10],\n        text=[str(gv)],\n        text_font_size=\"20pt\",\n        text_color=INK_SOFT,\n        text_alpha=0.9,\n    )\n\n# Plot each product series\nlegend_items = []\nfor idx, (product_name, values) in enumerate(products.items()):\n    # Convert values to x, y coordinates\n    x_vals = [v * np.cos(a) for v, a in zip(values, angles, strict=True)]\n    y_vals = [v * np.sin(a) for v, a in zip(values, angles, strict=True)]\n\n    # Close the polygon\n    x_vals.append(x_vals[0])\n    y_vals.append(y_vals[0])\n\n    # Draw filled polygon\n    fill_renderer = p.patch(\n        x_vals,\n        y_vals,\n        fill_color=IMPRINT[idx],\n        fill_alpha=0.25,\n        line_color=IMPRINT[idx],\n        line_width=4,\n        line_alpha=0.9,\n    )\n\n    # Draw points at vertices\n    p.scatter(x_vals[:-1], y_vals[:-1], size=22, fill_color=IMPRINT[idx], line_color=PAGE_BG, line_width=3, alpha=0.9)\n\n    legend_items.append((product_name, [fill_renderer]))\n\n# Add legend\nlegend = Legend(\n    items=legend_items,\n    location=\"top_right\",\n    label_text_font_size=\"22pt\",\n    glyph_height=40,\n    glyph_width=40,\n    spacing=20,\n    padding=25,\n    background_fill_color=PAGE_BG,\n    background_fill_alpha=0.85,\n    border_line_color=INK_SOFT,\n    label_text_color=INK_SOFT,\n)\np.add_layout(legend, \"right\")\n\n# Style title\np.title.text_font_size = \"32pt\"\np.title.align = \"center\"\np.title.text_color = INK\n\n# Save 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"}