{"spec_id":"radar-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nradar-basic: Basic Radar Chart\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 89/100 | Updated: 2026-07-24\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\n# Theme-adaptive chrome (Imprint palette data colors stay constant across themes)\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_PALETTE = [\"#009E73\", \"#C475FD\"]\n\n# Data - Employee performance metrics (6 categories, 2 employees)\ncategories = [\"Technical\", \"Communication\", \"Leadership\", \"Creativity\", \"Teamwork\", \"Problem Solving\"]\nvalues_alice = [85, 70, 60, 90, 75, 80]\nvalues_bob = [70, 85, 75, 65, 90, 70]\n\nn = len(categories)\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 series\ndata_rows = []\nfor i, (cat, val_a, val_b, angle) in enumerate(zip(categories, values_alice, values_bob, angles, strict=True)):\n    # Convert polar to cartesian (0 degrees at top, clockwise)\n    x_a = val_a * math.cos(angle - math.pi / 2)\n    y_a = val_a * math.sin(angle - math.pi / 2)\n    x_b = val_b * math.cos(angle - math.pi / 2)\n    y_b = val_b * math.sin(angle - math.pi / 2)\n    data_rows.append({\"category\": cat, \"value\": val_a, \"x\": x_a, \"y\": y_a, \"series\": \"Alice\", \"order\": i})\n    data_rows.append({\"category\": cat, \"value\": val_b, \"x\": x_b, \"y\": y_b, \"series\": \"Bob\", \"order\": i})\n\n# Close the polygon by repeating first point\nx_a = values_alice[0] * math.cos(angles[0] - math.pi / 2)\ny_a = values_alice[0] * math.sin(angles[0] - math.pi / 2)\nx_b = values_bob[0] * math.cos(angles[0] - math.pi / 2)\ny_b = values_bob[0] * math.sin(angles[0] - math.pi / 2)\ndata_rows.append(\n    {\"category\": categories[0], \"value\": values_alice[0], \"x\": x_a, \"y\": y_a, \"series\": \"Alice\", \"order\": n}\n)\ndata_rows.append({\"category\": categories[0], \"value\": values_bob[0], \"x\": x_b, \"y\": y_b, \"series\": \"Bob\", \"order\": 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 = 120 * math.cos(angle - math.pi / 2)\n    y = 120 * 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 on the angular bisector between the first\n# two category spokes (a gap with no data) rather than on a data-bearing spoke,\n# so they never overlap a series' marker/line\nvalue_label_angle = (angles[0] + angles[1]) / 2 - math.pi / 2\nvalue_label_rows = []\nfor val in grid_values:\n    x = val * math.cos(value_label_angle)\n    y = val * math.sin(value_label_angle)\n    value_label_rows.append({\"label\": str(val), \"x\": x, \"y\": y})\n\nvalue_label_df = pd.DataFrame(value_label_rows)\n\n# Descriptive prefix clarifies this instance is an employee comparison, giving\n# the reader an immediate frame for the two complementary skill profiles\ntitle = \"Employee Skills Comparison · radar-basic · python · letsplot · anyplot.ai\"\ntitle_fontsize = round(16 * (60 / len(title) if len(title) > 60 else 1.0))\ntitle_fontsize = max(title_fontsize, 11)\n\n# Build the plot\nplot = (\n    ggplot()\n    # Gridlines (concentric circles) - geom_path preserves point order (geom_line\n    # sorts by x, which breaks a circle traced by angle into a star pattern)\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"radius\"), data=grid_df, color=INK_SOFT, size=0.6, alpha=0.3)\n    # Spokes (radial lines)\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"group\"), data=spoke_df, color=INK_SOFT, size=0.6, alpha=0.3)\n    # Filled polygons for each series\n    + geom_polygon(aes(x=\"x\", y=\"y\", fill=\"series\", group=\"series\"), data=df, alpha=0.25)\n    # Ink-color outline behind each line, peeking out a touch on either side, so the\n    # lower-contrast lavender series still reads clearly against the page background\n    + geom_path(aes(x=\"x\", y=\"y\", group=\"series\"), data=df, color=INK, size=2.8, alpha=0.5)\n    # Lines connecting points, in category order (geom_path, not geom_line)\n    + geom_path(aes(x=\"x\", y=\"y\", color=\"series\", group=\"series\"), data=df, size=2)\n    # Points at each vertex (exclude the closing point to avoid double dot) - shape 21\n    # gives a filled marker with an ink-color border stroke for the same contrast boost\n    + geom_point(aes(x=\"x\", y=\"y\", fill=\"series\"), data=df[df[\"order\"] < n], shape=21, color=INK, size=6, stroke=1.2)\n    # Imprint palette - brand green first, lavender second\n    + scale_fill_manual(values=IMPRINT_PALETTE)\n    + scale_color_manual(values=IMPRINT_PALETTE)\n    # Axis limits for square plot\n    + scale_x_continuous(limits=(-150, 150))\n    + scale_y_continuous(limits=(-150, 150))\n    # Title and legend\n    + labs(title=title, fill=\"Employee\", color=\"Employee\")\n    # Square format for symmetric radar chart\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, color=PAGE_BG),\n        plot_title=element_text(size=title_fontsize, color=INK),\n        legend_title=element_text(size=12, color=INK),\n        legend_text=element_text(size=10, 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        panel_border=element_blank(),\n    )\n)\n\n# Add category labels as text (theme-adaptive ink)\nplot = plot + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=label_df, size=4.2, color=INK)\n\n# Add grid value labels (theme-adaptive soft ink)\nplot = plot + geom_text(aes(x=\"x\", y=\"y\", label=\"label\"), data=value_label_df, size=3.5, color=INK_SOFT)\n\n# Save outputs (PNG scaled 4x to 2400x2400, plus interactive HTML)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}