{"spec_id":"radar-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nradar-basic: Basic Radar Chart\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-07-24\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\n\n# Prevent this file (altair.py) from shadowing the installed altair package\n_here = os.path.realpath(os.path.dirname(__file__))\nsys.path = [p for p in sys.path if not (p and os.path.realpath(p) == _here)]\ndel _here\n\nalt = importlib.import_module(\"altair\")\nnp = importlib.import_module(\"numpy\")\npd = importlib.import_module(\"pandas\")\n\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\"]\n\n# Square canonical inner view (see prompts/library/altair.md \"Canvas\"). Width and\n# height differ (unlike the data domain, which is symmetric), so the x domain is\n# widened by the same ratio to keep the polar grid circular instead of elliptical.\nVIEW_W, VIEW_H = 500, 460\nTARGET_W, TARGET_H = 2400, 2400\n\ncategories = [\"Communication\", \"Technical Skills\", \"Teamwork\", \"Problem Solving\", \"Leadership\", \"Creativity\"]\nn = len(categories)\nMAX_VAL = 100\n\nalice_vals = [85, 90, 75, 88, 70, 82]\nbob_vals = [72, 78, 88, 75, 85, 68]\n\nangles = np.linspace(0, 2 * np.pi, n, endpoint=False).tolist()\n\n\ndef to_xy(values):\n    scaled = [v / MAX_VAL for v in values]\n    return (\n        [s * np.cos(a - np.pi / 2) for s, a in zip(scaled, angles, strict=True)],\n        [s * np.sin(a - np.pi / 2) for s, a in zip(scaled, angles, strict=True)],\n    )\n\n\ndef to_xy_closed(values):\n    x, y = to_xy(values)\n    return x + [x[0]], y + [y[0]]\n\n\n# Grid rings (hexagonal at 5 levels)\ngrid_data = []\nfor level in [20, 40, 60, 80, 100]:\n    ls = level / MAX_VAL\n    for i, angle in enumerate(angles):\n        grid_data.append(\n            {\"x\": ls * np.cos(angle - np.pi / 2), \"y\": ls * np.sin(angle - np.pi / 2), \"level\": level, \"order\": i}\n        )\n    grid_data.append(\n        {\"x\": ls * np.cos(angles[0] - np.pi / 2), \"y\": ls * np.sin(angles[0] - np.pi / 2), \"level\": level, \"order\": n}\n    )\ndf_grid = pd.DataFrame(grid_data)\n\n# Spokes from center to outer edge\nspokes_data = []\nfor cat, angle in zip(categories, angles, strict=True):\n    spokes_data.extend(\n        [\n            {\"x\": 0.0, \"y\": 0.0, \"cat\": cat, \"ord\": 0},\n            {\"x\": np.cos(angle - np.pi / 2), \"y\": np.sin(angle - np.pi / 2), \"cat\": cat, \"ord\": 1},\n        ]\n    )\ndf_spokes = pd.DataFrame(spokes_data)\n\n# Outer axis labels\nlabel_off = 1.24\ndf_labels = pd.DataFrame(\n    [\n        {\"x\": label_off * np.cos(a - np.pi / 2), \"y\": label_off * np.sin(a - np.pi / 2), \"label\": c}\n        for c, a in zip(categories, angles, strict=True)\n    ]\n)\n\n# Grid ring value annotations along the top (vertical) spoke\ndf_ring_labels = pd.DataFrame(\n    [{\"x\": 0.05, \"y\": level / MAX_VAL, \"label\": str(level)} for level in [20, 40, 60, 80, 100]]\n)\n\n# Series line data (closed polygons for outlines)\nseries_line_rows = []\nfor name, vals in [(\"Alice\", alice_vals), (\"Bob\", bob_vals)]:\n    x_c, y_c = to_xy_closed(vals)\n    for i, (x, y) in enumerate(zip(x_c, y_c, strict=True)):\n        series_line_rows.append({\"Employee\": name, \"x\": x, \"y\": y, \"order\": i})\ndf_series_line = pd.DataFrame(series_line_rows)\n\n# Series point data (unclosed, for tooltips and click selection)\npts_rows = []\nfor name, vals in [(\"Alice\", alice_vals), (\"Bob\", bob_vals)]:\n    x_p, y_p = to_xy(vals)\n    for x, y, v, cat in zip(x_p, y_p, vals, categories, strict=True):\n        pts_rows.append({\"Employee\": name, \"x\": x, \"y\": y, \"value\": v, \"category\": cat})\ndf_pts = pd.DataFrame(pts_rows)\n\n# Click-based interactive selection: click a vertex point to highlight its series\nselection = alt.selection_point(fields=[\"Employee\"])\n\ncolor_scale = alt.Scale(domain=[\"Alice\", \"Bob\"], range=[IMPRINT[0], IMPRINT[1]])\n\n# y domain is shifted +0.10 (not centered on 0) to tighten the margin below the\n# lowest label and ease the margin above the highest one — the title sits above\n# the view (not mirrored below it), so a symmetric domain left more empty\n# canvas below the grid than above it once the PNG was padded to a square.\ndomain_y = [-1.35, 1.55]\ndomain_x = [-1.45 * VIEW_W / VIEW_H, 1.45 * VIEW_W / VIEW_H]\n\n# Static grid rings\ngrid_lines = (\n    alt.Chart(df_grid)\n    .mark_line(strokeWidth=1.5, color=INK_SOFT, opacity=0.3)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None, scale=alt.Scale(domain=domain_x)),\n        y=alt.Y(\"y:Q\", axis=None, scale=alt.Scale(domain=domain_y)),\n        detail=\"level:N\",\n        order=\"order:O\",\n    )\n)\n\n# Static spokes\nspokes = (\n    alt.Chart(df_spokes)\n    .mark_line(strokeWidth=1, color=INK_SOFT, opacity=0.25)\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"y:Q\", axis=None), detail=\"cat:N\", order=\"ord:O\")\n)\n\n# Filled polygons — mark_line with interpolate=\"linear-closed\" draws a closed fill\n# from the same df_series_line data and quantitative x/y scale as the outline layer\n# below, so fill and outline are always coordinate-identical (a prior mark_geoshape +\n# identity-projection approach fit its own bounding box independently of the shared\n# scale, causing the fill to render oversized relative to the outline). fill/fillOpacity\n# are static mark properties (not data-driven encodings) so they cannot collide with\n# the Employee color legend defined on the series_lines layer below.\nalice_fill = (\n    alt.Chart(df_series_line[df_series_line[\"Employee\"] == \"Alice\"])\n    .mark_line(interpolate=\"linear-closed\", fill=IMPRINT[0], fillOpacity=0.25, stroke=None)\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"y:Q\", axis=None), order=\"order:O\")\n)\n\nbob_fill = (\n    alt.Chart(df_series_line[df_series_line[\"Employee\"] == \"Bob\"])\n    .mark_line(interpolate=\"linear-closed\", fill=IMPRINT[1], fillOpacity=0.25, stroke=None)\n    .encode(x=alt.X(\"x:Q\", axis=None), y=alt.Y(\"y:Q\", axis=None), order=\"order:O\")\n)\n\n# Interactive polygon outlines — click a series to highlight it (dims the other)\nseries_lines = (\n    alt.Chart(df_series_line)\n    .mark_line(strokeWidth=3.5)\n    .encode(\n        x=alt.X(\"x:Q\", axis=None),\n        y=alt.Y(\"y:Q\", axis=None),\n        color=alt.Color(\n            \"Employee:N\",\n            scale=color_scale,\n            legend=alt.Legend(\n                title=\"Employee\",\n                titleFontSize=22,\n                titleFontWeight=\"bold\",\n                labelFontSize=20,\n                symbolSize=300,\n                symbolStrokeWidth=4,\n                symbolOpacity=1.0,\n                orient=\"top-right\",\n                offset=10,\n            ),\n        ),\n        detail=\"Employee:N\",\n        order=\"order:O\",\n        opacity=alt.condition(selection, alt.value(1.0), alt.value(0.15)),\n    )\n)\n\n# Interactive vertex points with hover tooltips — click to select series\npoints = (\n    alt.Chart(df_pts)\n    .mark_point(filled=True, size=350)\n    .encode(\n        x=alt.X(\"x:Q\"),\n        y=alt.Y(\"y:Q\"),\n        color=alt.Color(\"Employee:N\", scale=color_scale, legend=None),\n        opacity=alt.condition(selection, alt.value(1.0), alt.value(0.10)),\n        tooltip=[\n            alt.Tooltip(\"Employee:N\", title=\"Employee\"),\n            alt.Tooltip(\"category:N\", title=\"Competency\"),\n            alt.Tooltip(\"value:Q\", title=\"Score\"),\n        ],\n    )\n    .add_params(selection)\n)\n\n# Outer axis category labels\naxis_labels = (\n    alt.Chart(df_labels)\n    .mark_text(fontSize=23, fontWeight=\"bold\")\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"label:N\", color=alt.value(INK))\n)\n\n# Grid ring value annotations (20, 40, 60, 80, 100) along the top spoke\nring_labels = (\n    alt.Chart(df_ring_labels)\n    .mark_text(fontSize=19, align=\"left\")\n    .encode(x=alt.X(\"x:Q\"), y=alt.Y(\"y:Q\"), text=\"label:N\", color=alt.value(INK_MUTED))\n)\n\nchart = (\n    alt.layer(grid_lines, spokes, alice_fill, bob_fill, series_lines, points, axis_labels, ring_labels)\n    .properties(\n        width=VIEW_W,\n        height=VIEW_H,\n        background=PAGE_BG,\n        title=alt.Title(\n            text=\"radar-basic · python · altair · anyplot.ai\", fontSize=29, color=INK, fontWeight=\"bold\", offset=24\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        labelFontSize=20,\n        titleFontSize=22,\n        padding=16,\n        cornerRadius=4,\n    )\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# PAD-only to the canonical (2400, 2400) target — never crop (see prompts/library/altair.md \"Canvas\").\nfrom PIL import Image\n\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TARGET_W or _h > TARGET_H:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TARGET_W}x{TARGET_H}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TARGET_W or _h < TARGET_H:\n    _canvas = Image.new(\"RGB\", (TARGET_W, TARGET_H), PAGE_BG)\n    _canvas.paste(_img, ((TARGET_W - _w) // 2, (TARGET_H - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}