{"spec_id":"radar-multi","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nradar-multi: Multi-Series Radar Chart\nLibrary: letsplot 4.11.0 | Python 3.13.15\nQuality: 87/100 | Updated: 2026-08-17\n\"\"\"\n\nimport math\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\n\n\nLetsPlot.setup_html()\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data - Smartphone comparison across 6 key attributes (4 products)\ncategories = [\"Battery\", \"Camera\", \"Display\", \"Performance\", \"Storage\", \"Price Value\"]\nproducts = {\n    \"Galaxy S24\": [85, 92, 90, 88, 75, 70],\n    \"iPhone 15\": [75, 95, 88, 92, 70, 65],\n    \"Pixel 8\": [80, 90, 82, 85, 65, 85],\n    \"OnePlus 12\": [90, 78, 85, 90, 80, 90],\n}\n\nn = len(categories)\n\n# Product with the highest average score gets a subtle emphasis (focal point)\nhero = max(products, key=lambda name: sum(products[name]) / len(products[name]))\n\n# Create angles for each category (evenly spaced, starting from top)\nangles = [i * 2 * math.pi / n for i in range(n)]\n\n# Build dataframe with cartesian coordinates for each product\ndata_rows = []\nfor product_name, values in products.items():\n    for i, (cat, val, angle) in enumerate(zip(categories, values, angles, strict=True)):\n        # Convert polar to cartesian (0 degrees at top, clockwise)\n        x = val * math.cos(angle - math.pi / 2)\n        y = val * math.sin(angle - math.pi / 2)\n        data_rows.append({\"category\": cat, \"value\": val, \"x\": x, \"y\": y, \"series\": product_name, \"order\": i})\n\n    # Close the polygon by repeating first point\n    x = values[0] * math.cos(angles[0] - math.pi / 2)\n    y = values[0] * math.sin(angles[0] - math.pi / 2)\n    data_rows.append(\n        {\"category\": categories[0], \"value\": values[0], \"x\": x, \"y\": y, \"series\": product_name, \"order\": n}\n    )\n\ndf = pd.DataFrame(data_rows)\n\n# Create gridlines data (circles at 20, 40, 60, 80, 100)\ngrid_rows = []\ngrid_values = [20, 40, 60, 80, 100]\ngrid_angles = [i * 2 * math.pi / 72 for i in range(73)]  # 73 points for smooth circles\nfor radius in grid_values:\n    for angle in grid_angles:\n        x = radius * math.cos(angle - math.pi / 2)\n        y = radius * math.sin(angle - math.pi / 2)\n        grid_rows.append({\"x\": x, \"y\": y, \"radius\": radius})\n\ngrid_df = pd.DataFrame(grid_rows)\n\n# Create axis lines (spokes from center to edge)\nspoke_rows = []\nfor i, angle in enumerate(angles):\n    x = 105 * math.cos(angle - math.pi / 2)\n    y = 105 * math.sin(angle - math.pi / 2)\n    spoke_rows.append({\"x\": 0, \"y\": 0, \"group\": i})\n    spoke_rows.append({\"x\": x, \"y\": y, \"group\": i})\n\nspoke_df = pd.DataFrame(spoke_rows)\n\n# Create axis labels (category names at outer edge)\nlabel_rows = []\nfor cat, angle in zip(categories, angles, strict=True):\n    x = 125 * math.cos(angle - math.pi / 2)\n    y = 125 * math.sin(angle - math.pi / 2)\n    label_rows.append({\"label\": cat, \"x\": x, \"y\": y})\n\nlabel_df = pd.DataFrame(label_rows)\n\n# Create grid value labels (placed in the gap between the Performance and\n# Storage spokes: a near-vertical sector, so the stacked labels don't overlap\n# each other, with a low combined data range so they clear the polygons too)\ngap_angle = (angles[3] + angles[4]) / 2\nvalue_label_rows = []\nfor val in grid_values:\n    x = val * math.cos(gap_angle - math.pi / 2)\n    y = val * math.sin(gap_angle - math.pi / 2)\n    value_label_rows.append({\"label\": str(val), \"x\": x, \"y\": y})\n\nvalue_label_df = pd.DataFrame(value_label_rows)\n\n# Build the plot\n# Note: geom_line() connects points sorted by x, which turns a circular/radial\n# path into a chaotic zigzag; geom_path() preserves the data's own point order,\n# which is required for these polar-via-cartesian shapes.\nplot = (\n    ggplot()\n    # Gridlines (concentric circles) - thin solid low-alpha rings\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"radius\"), data=grid_df, color=INK_SOFT, size=0.4, alpha=0.15)\n    # Spokes (radial lines)\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"group\"), data=spoke_df, color=INK_SOFT, size=0.4, alpha=0.3)\n    # Filled polygons for each series (lower alpha for 4 overlapping series)\n    + geom_polygon(aes(x=\"x\", y=\"y\", fill=\"series\", group=\"series\"), data=df, alpha=0.2)\n    # Lines connecting points, in category order (not x-sorted)\n    + geom_path(aes(x=\"x\", y=\"y\", color=\"series\", group=\"series\"), data=df, size=1.2)\n    # Emphasize the top-scoring product as a focal point (thicker outline)\n    + geom_path(\n        aes(x=\"x\", y=\"y\", color=\"series\", group=\"series\"), data=df[df[\"series\"] == hero], size=2.2, show_legend=False\n    )\n    # Points at each vertex (exclude the closing point to avoid double dot)\n    + geom_point(aes(x=\"x\", y=\"y\", color=\"series\"), data=df[df[\"order\"] < n], size=3.5)\n    # Custom color palette (Imprint)\n    + scale_fill_manual(values=IMPRINT)\n    + scale_color_manual(values=IMPRINT)\n    # Axis limits for square plot\n    + scale_x_continuous(limits=(-160, 160))\n    + scale_y_continuous(limits=(-160, 160))\n    # Title and legend\n    + labs(\n        title=\"Smartphone Comparison · radar-multi · python · letsplot · anyplot.ai\", fill=\"Product\", color=\"Product\"\n    )\n    # Square format for symmetric radar chart (600x600 @ scale=4 -> 2400x2400)\n    + ggsize(600, 600)\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, color=INK),\n        legend_title=element_text(size=13, color=INK),\n        legend_text=element_text(size=12, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_position=\"right\",\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid=element_blank(),\n    )\n)\n\n# Add category labels as text\nplot = plot + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df, size=5.5, color=INK)\n\n# Add grid value labels\nplot = plot + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=value_label_df, size=4.5, color=INK_SOFT)\n\n# Save outputs\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}