{"spec_id":"radar-multi","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nradar-multi: Multi-Series Radar Chart\nLibrary: matplotlib 3.11.1 | Python 3.13.15\nQuality: 86/100 | Updated: 2026-08-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\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# Imprint palette (first three series, canonical order)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data: product comparison across key attributes — Product B is the overall leader\ncategories = [\"Performance\", \"Battery Life\", \"Camera\", \"Display\", \"Build Quality\", \"Value\"]\nproducts = {\n    \"Product A\": [85, 70, 90, 88, 75, 65],\n    \"Product B\": [72, 95, 78, 82, 88, 80],\n    \"Product C\": [90, 60, 85, 75, 70, 90],\n}\nleader_idx = 1  # Product B carries the highest average score — given visual emphasis\n\n# Number of variables\nn_categories = len(categories)\n\n# Compute angle for each axis\nangles = np.linspace(0, 2 * np.pi, n_categories, endpoint=False).tolist()\nangles += angles[:1]  # Close the polygon\n\n# Create figure (square format for radar)\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, subplot_kw={\"polar\": True}, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Orient the radar with the first category at the top, going clockwise, so\n# no category label lands due-east where the legend sits\nax.set_theta_zero_location(\"N\")\nax.set_theta_direction(-1)\n\n# Per-series visual weight — the leader draws bolder and on top, guiding the eye\n# to the strongest overall performer without adding any text annotation\nmarkers = [\"o\", \"s\", \"^\"]\nlinewidths = [2.5, 2.5, 2.5]\nmarkersizes = [8, 8, 8]\nfill_alphas = [0.18, 0.18, 0.18]\nzorders = [1, 1, 1]  # below the default radial-grid/tick-label zorder (~2.5)\nlinewidths[leader_idx] = 3.5\nmarkersizes[leader_idx] = 12\nfill_alphas[leader_idx] = 0.35\nzorders[leader_idx] = 1.5\n\n# Plot each product\nfor idx, (product, values) in enumerate(products.items()):\n    values_closed = values + values[:1]  # Close the polygon\n    ax.plot(\n        angles,\n        values_closed,\n        marker=markers[idx],\n        linestyle=\"-\",\n        linewidth=linewidths[idx],\n        label=product,\n        color=IMPRINT[idx],\n        markersize=markersizes[idx],\n        markeredgecolor=PAGE_BG,\n        markeredgewidth=1,\n        zorder=zorders[idx],\n    )\n    ax.fill(angles, values_closed, alpha=fill_alphas[idx], color=IMPRINT[idx], zorder=zorders[idx])\n\n# Set category labels at each axis, nudged outward and aligned by their\n# rendered screen position (accounting for the N-zero/clockwise rotation\n# above) so labels on the right lean left, labels on the left lean right,\n# and top/bottom labels stay centered — none collide with their own markers\nax.set_xticks(angles[:-1])\ntick_labels = ax.set_xticklabels(categories, fontsize=13, fontweight=\"bold\", color=INK)\nax.tick_params(axis=\"x\", pad=14)\nfor label, raw_angle in zip(tick_labels, angles[:-1], strict=True):\n    cos_disp = np.cos(np.pi / 2 - raw_angle)\n    if cos_disp > 0.3:\n        label.set_horizontalalignment(\"left\")\n    elif cos_disp < -0.3:\n        label.set_horizontalalignment(\"right\")\n    else:\n        label.set_horizontalalignment(\"center\")\n\n# Set radial grid — fewer labels (25/50/75/100) placed in the empty sector\n# between Display and Build Quality, away from every category axis, so they\n# no longer collide with each other or with the data. Labels keep an opaque\n# halo so they stay legible where polygon fills cross the radial axis\nax.set_ylim(0, 100)\nax.set_yticks([25, 50, 75, 100])\nr_tick_labels = ax.set_yticklabels([\"25\", \"50\", \"75\", \"100\"], fontsize=13, color=INK_SOFT)\n# set_rlabel_position takes a RAW (pre-rotation) angle; raw=210 (Build\n# Quality's own axis) renders at display angle 240 = between Display (270)\n# and Build Quality (210) after the theta_zero_location/direction rotation\nax.set_rlabel_position(210)\nfor label in r_tick_labels:\n    label.set_bbox({\"facecolor\": PAGE_BG, \"edgecolor\": \"none\", \"alpha\": 0.9, \"pad\": 1.5})\n\n# Grid styling\nax.yaxis.grid(True, linestyle=\"-\", alpha=0.15, linewidth=0.8, color=INK_SOFT)\nax.xaxis.grid(True, linestyle=\"-\", alpha=0.15, linewidth=0.8, color=INK_SOFT)\n\n# Title — figure-level (not axes-level) so it stays centered on the full\n# canvas regardless of how the legend reshapes the polar axes below\nfig.suptitle(\"radar-multi · python · matplotlib · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK, y=0.97)\n\n# Legend outside the circular grid, vertically centered — leader label\n# rendered bold to echo its bolder polygon\nleg = ax.legend(loc=\"center left\", bbox_to_anchor=(1.08, 0.5), fontsize=13, framealpha=1.0)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nleg.get_frame().set_linewidth(0.8)\nfor idx, text in enumerate(leg.get_texts()):\n    text.set_color(INK)\n    if idx == leader_idx:\n        text.set_fontweight(\"bold\")\n\n# Explicit margins (not tight_layout) so the polar axes sit vertically\n# centered in the square canvas — top/bottom margins balanced, right margin\n# reserved for the legend\nfig.subplots_adjust(left=0.32, right=0.66, top=0.80, bottom=0.20)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}