{"spec_id":"radar-innovation-timeline","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nradar-innovation-timeline: Innovation Radar with Time-Horizon Rings\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n\n# Theme tokens (Imprint palette — prompts/default-style-guide.md)\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\n# Imprint palette — 8 hues, canonical order, first series always position 1\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data\nnp.random.seed(42)\n\nrings = [\"Adopt\", \"Trial\", \"Assess\", \"Hold\"]\nsectors = [\"AI & ML\", \"Cloud & Infra\", \"Data Engineering\", \"Security\"]\nn_sectors = len(sectors)\n\ninnovations = [\n    (\"LLM Agents\", \"Adopt\", \"AI & ML\"),\n    (\"RAG Pipelines\", \"Trial\", \"AI & ML\"),\n    (\"AI Code Assistants\", \"Trial\", \"AI & ML\"),\n    (\"Vision Transformers\", \"Assess\", \"AI & ML\"),\n    (\"Federated Learning\", \"Assess\", \"AI & ML\"),\n    (\"Neuromorphic Chips\", \"Hold\", \"AI & ML\"),\n    (\"Kubernetes\", \"Adopt\", \"Cloud & Infra\"),\n    (\"FinOps Tooling\", \"Trial\", \"Cloud & Infra\"),\n    (\"Edge Computing\", \"Assess\", \"Cloud & Infra\"),\n    (\"Platform Engineering\", \"Assess\", \"Cloud & Infra\"),\n    (\"Quantum Cloud APIs\", \"Hold\", \"Cloud & Infra\"),\n    (\"Serverless Containers\", \"Hold\", \"Cloud & Infra\"),\n    (\"Apache Iceberg\", \"Adopt\", \"Data Engineering\"),\n    (\"Real-time Lakehouse\", \"Trial\", \"Data Engineering\"),\n    (\"Data Contracts\", \"Trial\", \"Data Engineering\"),\n    (\"Streaming SQL\", \"Assess\", \"Data Engineering\"),\n    (\"Data Mesh\", \"Hold\", \"Data Engineering\"),\n    (\"Zero Trust Arch\", \"Adopt\", \"Security\"),\n    (\"SBOM Tooling\", \"Trial\", \"Security\"),\n    (\"AI Threat Detection\", \"Assess\", \"Security\"),\n    (\"Post-Quantum Crypto\", \"Assess\", \"Security\"),\n    (\"Confidential Computing\", \"Hold\", \"Security\"),\n    (\"Homomorphic Encryption\", \"Hold\", \"Security\"),\n]\n\nring_index = {name: i for i, name in enumerate(rings)}\nsector_index = {name: i for i, name in enumerate(sectors)}\n\n# Sector colors: Imprint palette positions 1–4 in canonical order\nsector_colors = {\n    \"AI & ML\": IMPRINT_PALETTE[0],\n    \"Cloud & Infra\": IMPRINT_PALETTE[1],\n    \"Data Engineering\": IMPRINT_PALETTE[2],\n    \"Security\": IMPRINT_PALETTE[3],\n}\nsector_markers = {\"AI & ML\": \"o\", \"Cloud & Infra\": \"s\", \"Data Engineering\": \"D\", \"Security\": \"^\"}\n\n# Marker sizes by ring — visual hierarchy: near-term prominent, far-future subtle\nring_marker_sizes = {\"Adopt\": 110, \"Trial\": 85, \"Assess\": 65, \"Hold\": 50}\n\n# Layout: 300-degree arc (wider spread reduces inner-ring label crowding), 60-degree gap for ring labels\narc_span = 5 / 6 * 2 * np.pi\narc_start = np.deg2rad(115)\nsector_width = arc_span / n_sectors\n\n# Ring band boundaries — inner ring starts at 1.5 (not 0.5) to push Adopt items\n# further from the center, providing ~52% more arc-space between adjacent items\nring_boundaries = [1.5, 3.5, 5.5, 7.5, 9.5]\n\n# Count items per ring-sector cell for spread calculation\nsector_ring_counts = {}\nfor _, ring_name, sector_name in innovations:\n    key = (ring_name, sector_name)\n    sector_ring_counts[key] = sector_ring_counts.get(key, 0) + 1\n\n# Compute marker positions with wider angular spread to reduce label crowding\npositions = []\nsector_ring_placed = {}\nfor name, ring_name, sector_name in innovations:\n    r_idx = ring_index[ring_name]\n    s_idx = sector_index[sector_name]\n    key = (ring_name, sector_name)\n    placed = sector_ring_placed.get(key, 0)\n    total = sector_ring_counts[key]\n    sector_ring_placed[key] = placed + 1\n\n    band_width = ring_boundaries[r_idx + 1] - ring_boundaries[r_idx]\n    r_lo = ring_boundaries[r_idx] + band_width * 0.25\n    r_hi = ring_boundaries[r_idx] + band_width * 0.75\n    if total == 1:\n        r = (r_lo + r_hi) / 2\n    elif total == 2:\n        r = r_lo + (r_hi - r_lo) * (0.25 + 0.50 * placed)\n    else:\n        r = r_lo + (r_hi - r_lo) * (placed + 0.5) / total\n\n    # Wider angular spread within sector (8% margin instead of 10%) to reduce crowding\n    sector_start = arc_start + s_idx * sector_width\n    margin = sector_width * 0.08\n    a_lo = sector_start + margin\n    a_hi = sector_start + sector_width - margin\n    if total == 1:\n        theta = (a_lo + a_hi) / 2\n    elif total == 2:\n        frac = 0.20 + 0.60 * placed\n        if r_idx % 2 == 1:\n            frac = 1.0 - frac\n        theta = a_lo + (a_hi - a_lo) * frac\n    else:\n        frac = placed / (total - 1)\n        if r_idx % 2 == 1:\n            frac = 1.0 - frac\n        theta = a_lo + (a_hi - a_lo) * frac\n\n    # Minimal angular jitter; smaller radial jitter to stay within ring band\n    theta += np.random.uniform(-0.008, 0.008)\n    r += np.random.uniform(-0.01, 0.01)\n\n    positions.append((name, theta, r, sector_name, ring_name, placed, total, r_idx))\n\n# Plot — square canvas 2400×2400 px (figsize=(6,6), dpi=400)\nfig = plt.figure(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax = fig.add_subplot(111, polar=True)\nax.set_facecolor(PAGE_BG)\n\n# Ring fills — Imprint-tinted pastels (light) / darks (dark) for ring separation\nif THEME == \"light\":\n    ring_fill_colors = [\"#E0F5EE\", \"#F2E5FF\", \"#E4EAF4\", \"#F4EAD8\"]\nelse:\n    ring_fill_colors = [\"#1E2E25\", \"#261A2E\", \"#1A2030\", \"#2E2018\"]\n\nfor i in range(len(rings)):\n    theta_fill = np.linspace(arc_start, arc_start + arc_span, 300)\n    ax.fill_between(\n        theta_fill,\n        np.full(300, ring_boundaries[i]),\n        np.full(300, ring_boundaries[i + 1]),\n        color=ring_fill_colors[i],\n        alpha=1.0,\n    )\n\n# Ring boundary arcs\nfor boundary in ring_boundaries:\n    theta_arc = np.linspace(arc_start, arc_start + arc_span, 300)\n    ax.plot(theta_arc, np.full(300, boundary), color=INK_MUTED, linewidth=0.7, alpha=0.6)\n\n# Sector divider lines\nfor i in range(n_sectors + 1):\n    angle = arc_start + i * sector_width\n    ax.plot([angle, angle], [ring_boundaries[0], ring_boundaries[-1]], color=INK_MUTED, linewidth=0.7, alpha=0.6)\n\n# Ring labels in the arc gap — push labels 0.35 rad past arc end to clear the boundary divider\nlabel_angle = arc_start + arc_span + 0.35\nfor i, ring_name in enumerate(rings):\n    r_mid = (ring_boundaries[i] + ring_boundaries[i + 1]) / 2\n    ax.text(\n        label_angle,\n        r_mid,\n        ring_name,\n        ha=\"left\",\n        va=\"center\",\n        fontsize=9,\n        fontweight=\"bold\",\n        color=INK_SOFT,\n        fontstyle=\"italic\",\n        clip_on=False,\n        zorder=10,\n    )\n\n# Sector labels along outer edge\nfor i, sector_name in enumerate(sectors):\n    angle = arc_start + (i + 0.5) * sector_width\n    ax.text(\n        angle,\n        ring_boundaries[-1] + 0.50,\n        sector_name,\n        ha=\"center\",\n        va=\"center\",\n        fontsize=10,\n        fontweight=\"bold\",\n        color=INK,\n        clip_on=False,\n        zorder=10,\n    )\n\n# Label background for readability — theme-adaptive elevated surface\nlabel_bbox = {\"boxstyle\": \"round,pad=0.10\", \"facecolor\": ELEVATED_BG, \"alpha\": 0.92, \"edgecolor\": \"none\"}\n\n# Markers and labels\nfor name, theta, r, sector_name, ring_name, placed, _total, r_idx in positions:\n    color = sector_colors[sector_name]\n    marker = sector_markers[sector_name]\n    msize = ring_marker_sizes[ring_name]\n\n    ax.scatter(theta, r, s=msize, color=color, marker=marker, edgecolors=PAGE_BG, linewidth=0.8, zorder=5, alpha=0.95)\n\n    # Alternate label radially above/below; inner rings prefer outward, outer prefer inward\n    # to keep labels away from both the ring boundary and the chart edge\n    outward = placed % 2 == 0\n    if r_idx >= 2:  # Assess / Hold: flip to avoid outer boundary crowding\n        outward = not outward\n    # Inner rings: larger offset to push labels away from crowded center; smaller font\n    base_offset = 0.55 if r_idx <= 1 else 0.42\n    label_fontsize = 7 if r_idx <= 1 else 8\n    label_offset = base_offset if outward else -base_offset\n    va = \"bottom\" if outward else \"top\"\n\n    ax.text(\n        theta,\n        r + label_offset,\n        name,\n        fontsize=label_fontsize,\n        ha=\"center\",\n        va=va,\n        color=INK,\n        fontweight=\"medium\",\n        bbox=label_bbox,\n        zorder=6,\n        clip_on=False,\n    )\n\n# Style — hide default polar decorations\nax.set_ylim(0, ring_boundaries[-1] + 2.5)\nax.set_yticklabels([])\nax.set_xticklabels([])\nax.set_xticks([])\nax.set_yticks([])\nax.grid(False)\nax.spines[\"polar\"].set_visible(False)\n\n# Title — scale fontsize with title length per prompts/plot-generator.md\ntitle = \"radar-innovation-timeline · python · matplotlib · anyplot.ai\"\nn = len(title)\nratio = 67 / n if n > 67 else 1.0\ntitle_fontsize = max(8, round(12 * ratio))\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", pad=20, color=INK)\n\n# Legend — theme-adaptive frame and text\nlegend_handles = [\n    plt.scatter(\n        [], [], s=80, color=sector_colors[s], marker=sector_markers[s], edgecolors=PAGE_BG, linewidth=0.8, label=s\n    )\n    for s in sectors\n]\nleg = fig.legend(\n    handles=legend_handles,\n    loc=\"lower right\",\n    fontsize=8,\n    title=\"Sectors\",\n    title_fontsize=9,\n    handletextpad=0.8,\n    borderpad=0.8,\n    bbox_to_anchor=(0.97, 0.02),\n)\nif leg:\n    leg.get_frame().set_facecolor(ELEVATED_BG)\n    leg.get_frame().set_edgecolor(INK_SOFT)\n    plt.setp(leg.get_texts(), color=INK_SOFT)\n    leg.get_title().set_color(INK_SOFT)\n\nfig.subplots_adjust(left=0.05, right=0.90, top=0.92, bottom=0.08)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}