{"spec_id":"radar-innovation-timeline","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nradar-innovation-timeline: Innovation Radar with Time-Horizon Rings\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport matplotlib.lines as mlines\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — first 4 positions for sector encoding\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\nnp.random.seed(42)\n\n# Configuration\nsectors = [\"AI & ML\", \"Cloud & Infra\", \"Sustainability\", \"Biotech\"]\nrings = [\"Adopt\", \"Trial\", \"Assess\", \"Hold\"]\nring_radii = {\"Adopt\": 1.0, \"Trial\": 2.0, \"Assess\": 3.0, \"Hold\": 4.0}\nring_importance = {\"Adopt\": 4, \"Trial\": 3, \"Assess\": 2, \"Hold\": 1}\n\nsector_colors = IMPRINT_PALETTE[:4]\nsector_palette = dict(zip(sectors, sector_colors, strict=True))\nsector_markers = {\"AI & ML\": \"o\", \"Cloud & Infra\": \"s\", \"Sustainability\": \"D\", \"Biotech\": \"^\"}\n\ntotal_angle_deg = 270\nsector_width_deg = total_angle_deg / len(sectors)\nstart_angle_deg = 135\n\n# Ring fills — Imprint colors at low alpha, visible on both themes\nring_fill_colors = [\"#009E73\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\nring_boundaries = [0.5, 1.5, 2.5, 3.5, 4.5]\n# Boundary accent gradient via seaborn blend\nring_accent = sns.blend_palette([\"#009E73\", \"#BD8233\", \"#AE3030\"], n_colors=5)\n\n# Innovation data (27 items across 4 rings × 4 sectors)\ninnovations = [\n    (\"LLM Agents\", \"Adopt\", \"AI & ML\"),\n    (\"RAG Pipelines\", \"Adopt\", \"AI & ML\"),\n    (\"Multimodal Models\", \"Trial\", \"AI & ML\"),\n    (\"Federated Learning\", \"Assess\", \"AI & ML\"),\n    (\"Neuromorphic Chips\", \"Hold\", \"AI & ML\"),\n    (\"AI Code Review\", \"Trial\", \"AI & ML\"),\n    (\"Synthetic Data Gen\", \"Assess\", \"AI & ML\"),\n    (\"Platform Eng.\", \"Adopt\", \"Cloud & Infra\"),\n    (\"eBPF Observability\", \"Trial\", \"Cloud & Infra\"),\n    (\"Wasm Edge\", \"Assess\", \"Cloud & Infra\"),\n    (\"Confid. Compute\", \"Trial\", \"Cloud & Infra\"),\n    (\"Serverless GPUs\", \"Assess\", \"Cloud & Infra\"),\n    (\"Quantum Network\", \"Hold\", \"Cloud & Infra\"),\n    (\"RISC-V Servers\", \"Hold\", \"Cloud & Infra\"),\n    (\"Carbon Accounting\", \"Adopt\", \"Sustainability\"),\n    (\"Green Software\", \"Trial\", \"Sustainability\"),\n    (\"Digital Twins\", \"Assess\", \"Sustainability\"),\n    (\"Circular Supply\", \"Trial\", \"Sustainability\"),\n    (\"Ocean Carbon Cap.\", \"Hold\", \"Sustainability\"),\n    (\"Energy Harvest IoT\", \"Assess\", \"Sustainability\"),\n    (\"mRNA Therapeutics\", \"Adopt\", \"Biotech\"),\n    (\"CRISPR Diagnostics\", \"Trial\", \"Biotech\"),\n    (\"Organ-on-Chip\", \"Assess\", \"Biotech\"),\n    (\"Biocomputing\", \"Hold\", \"Biotech\"),\n    (\"Precision Nutrition\", \"Trial\", \"Biotech\"),\n    (\"Longevity Biomarkers\", \"Assess\", \"Biotech\"),\n    (\"Phage Therapy\", \"Hold\", \"Biotech\"),\n]\n\n# Build DataFrame with polar positions\nrecords = []\nfor name, ring, sector in innovations:\n    sector_idx = sectors.index(sector)\n    same_group = [(n, r, s) for n, r, s in innovations if s == sector and r == ring]\n    item_idx = same_group.index((name, ring, sector))\n    n_in_group = len(same_group)\n\n    sector_start = np.deg2rad(start_angle_deg - sector_idx * sector_width_deg)\n    sector_end = np.deg2rad(start_angle_deg - (sector_idx + 1) * sector_width_deg)\n    margin = 0.10 * (sector_start - sector_end)\n    usable_start = sector_start - margin\n    usable_end = sector_end + margin\n\n    if n_in_group == 1:\n        angle = (usable_start + usable_end) / 2\n    elif n_in_group == 2:\n        mid = (usable_start + usable_end) / 2\n        half = 0.80 * (usable_start - usable_end) / 2\n        angle = mid + half if item_idx == 0 else mid - half\n    else:\n        angle = usable_start + (usable_end - usable_start) * item_idx / (n_in_group - 1)\n\n    radial_jitter = (\n        0.24\n        if (n_in_group == 2 and item_idx == 0)\n        else (-0.24 if (n_in_group == 2 and item_idx == 1) else np.random.uniform(-0.22, 0.22))\n    )\n    radius = ring_radii[ring] + radial_jitter\n\n    records.append(\n        {\n            \"name\": name,\n            \"ring\": ring,\n            \"sector\": sector,\n            \"angle\": angle,\n            \"radius\": radius,\n            \"importance\": ring_importance[ring],\n        }\n    )\n\ndf = pd.DataFrame(records)\n\n# Plot — square canvas for radar (2400×2400 px); leave bottom 20% for legend\nfig = plt.figure(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\nax = fig.add_subplot(111, projection=\"polar\")\nax.set_facecolor(PAGE_BG)\nax.set_theta_zero_location(\"N\")\nax.set_theta_direction(-1)\nfig.subplots_adjust(left=0.05, right=0.95, top=0.91, bottom=0.20)\n\n# Ring background fills — Imprint colors at very low alpha, theme-neutral\ntheta_fill = np.linspace(0, 2 * np.pi, 200)\nfor i in range(len(rings)):\n    ax.fill_between(\n        theta_fill, ring_boundaries[i], ring_boundaries[i + 1], color=ring_fill_colors[i], alpha=0.18, zorder=0\n    )\n\n# Ring boundary lines with Imprint-derived gradient\nfor i, rb in enumerate(ring_boundaries):\n    lw = 1.2 if i == 1 else 0.7\n    ax.plot(\n        theta_fill,\n        np.full_like(theta_fill, rb),\n        color=sns.desaturate(ring_accent[i], 0.5),\n        linewidth=lw,\n        alpha=0.5,\n        zorder=1,\n    )\n\n# Sector divider lines\nfor i in range(len(sectors) + 1):\n    angle = np.deg2rad(start_angle_deg - i * sector_width_deg)\n    ax.plot([angle, angle], [0.5, 4.5], color=INK_SOFT, linewidth=0.8, alpha=0.4, zorder=1)\n\n# Plot innovations — seaborn scatterplot with color + shape + size encoding\nsize_map = {1: 150, 2: 250, 3: 340, 4: 440}\nsns.scatterplot(\n    data=df,\n    x=\"angle\",\n    y=\"radius\",\n    hue=\"sector\",\n    style=\"sector\",\n    size=\"importance\",\n    sizes=size_map,\n    markers=sector_markers,\n    palette=sector_palette,\n    edgecolor=PAGE_BG,\n    linewidth=1.2,\n    alpha=0.9,\n    legend=False,\n    ax=ax,\n    zorder=5,\n)\n\n# Subtle halo per sector using seaborn scatterplot\nfor sector_name in sectors:\n    sector_df = df[df[\"sector\"] == sector_name]\n    sns.scatterplot(\n        data=sector_df,\n        x=\"angle\",\n        y=\"radius\",\n        color=sector_palette[sector_name],\n        s=500,\n        alpha=0.07,\n        legend=False,\n        ax=ax,\n        zorder=3,\n    )\n\n# Axes setup — clean polar frame\nax.set_ylim(0, 6.0)\nax.set_yticks([])\nax.set_xticks([])\nax.set_xlabel(\"\")\nax.set_ylabel(\"\")\nax.grid(False)\nax.spines[\"polar\"].set_visible(False)\n\nfig.canvas.draw()\n\n# Innovation labels with improved collision detection and canvas boundary awareness\nplaced_boxes = []\nDPI = fig.dpi\nPT = DPI / 72.0\nFONT_SIZE = 10\nCHAR_W = FONT_SIZE * 0.62 * PT\nCHAR_H = FONT_SIZE * 1.3 * PT\nBOX_PAD = 6 * PT\n\nfig_width_px = fig.get_figwidth() * DPI\nfig_height_px = fig.get_figheight() * DPI\nMARGIN_PX = 8 * PT\n\ndf_sorted = df.sort_values(\"radius\", ascending=False).reset_index(drop=True)\n\nfor _, row in df_sorted.iterrows():\n    angle, radius, name = row[\"angle\"], row[\"radius\"], row[\"name\"]\n    angle_deg = np.rad2deg(angle) % 360\n    px, py = ax.transData.transform((angle, radius))\n\n    if 30 < angle_deg < 150:\n        ha, base_x = \"left\", 10\n    elif 210 < angle_deg < 330:\n        ha, base_x = \"right\", -10\n    else:\n        ha, base_x = \"center\", 0\n\n    best_pos, best_score = (12, base_x), float(\"inf\")\n    for y in [12, -12, 20, -20, 30, -30, 40, -40, 50, -50]:\n        for dx_adj in [0, 10, -10, 20, -20, 30, -30]:\n            x_off = base_x + dx_adj\n            va_c = \"bottom\" if y > 0 else \"top\"\n            cx = px + x_off * PT\n            cy = py + y * PT\n            w = len(name) * CHAR_W + BOX_PAD * 2\n            h = CHAR_H + BOX_PAD * 2\n            x0 = cx if ha == \"left\" else (cx - w if ha == \"right\" else cx - w / 2)\n            y0 = cy if va_c == \"bottom\" else cy - h\n            # Canvas boundary penalty — strongly prefer in-bounds placements\n            boundary_penalty = 0\n            if x0 < MARGIN_PX or x0 + w > fig_width_px - MARGIN_PX:\n                boundary_penalty += 50\n            if y0 < MARGIN_PX or y0 + h > fig_height_px - MARGIN_PX:\n                boundary_penalty += 50\n            m = 3 * PT\n            overlap_count = sum(\n                1\n                for bx in placed_boxes\n                if not (x0 + w + m < bx[0] or bx[0] + bx[2] + m < x0 or y0 + h + m < bx[1] or bx[1] + bx[3] + m < y0)\n            )\n            score = boundary_penalty + overlap_count\n            if score < best_score:\n                best_score = score\n                best_pos = (y, x_off)\n                if score == 0:\n                    break\n        if best_score == 0:\n            break\n\n    y_off, x_off = best_pos\n    va = \"bottom\" if y_off > 0 else \"top\"\n\n    ax.annotate(\n        name,\n        xy=(angle, radius),\n        xytext=(x_off, y_off),\n        textcoords=\"offset points\",\n        fontsize=FONT_SIZE,\n        color=INK,\n        fontweight=\"medium\",\n        ha=ha,\n        va=va,\n        bbox={\"boxstyle\": \"round,pad=0.18\", \"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"alpha\": 0.88},\n        arrowprops={\"arrowstyle\": \"-\", \"color\": INK_SOFT, \"linewidth\": 0.5},\n        zorder=6,\n    )\n    cx_f = px + x_off * PT\n    cy_f = py + y_off * PT\n    w_f = len(name) * CHAR_W + BOX_PAD * 2\n    h_f = CHAR_H + BOX_PAD * 2\n    x0_f = cx_f if ha == \"left\" else (cx_f - w_f if ha == \"right\" else cx_f - w_f / 2)\n    y0_f = cy_f if va == \"bottom\" else cy_f - h_f\n    placed_boxes.append((x0_f, y0_f, w_f, h_f))\n\n# Sector header labels\nfor i, sector_name in enumerate(sectors):\n    mid_angle = np.deg2rad(start_angle_deg - (i + 0.5) * sector_width_deg)\n    ax.text(\n        mid_angle,\n        5.4,\n        sector_name,\n        ha=\"center\",\n        va=\"center\",\n        fontsize=16,\n        fontweight=\"bold\",\n        color=sector_palette[sector_name],\n        zorder=7,\n    )\n\n# Ring labels along the gap edge\nlabel_angle = np.deg2rad(start_angle_deg - total_angle_deg - 8)\nfor ring_name, ring_r in zip(rings, [1.0, 2.0, 3.0, 4.0], strict=True):\n    ax.text(\n        label_angle,\n        ring_r,\n        ring_name,\n        ha=\"center\",\n        va=\"center\",\n        fontsize=12,\n        fontweight=\"bold\",\n        color=INK_SOFT,\n        bbox={\"boxstyle\": \"round,pad=0.2\", \"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"alpha\": 0.88},\n        zorder=7,\n    )\n\n# Directional storytelling cues (Imprint-derived colors)\ndir_angle = np.deg2rad(start_angle_deg - total_angle_deg - 22)\nax.text(\n    dir_angle,\n    0.7,\n    \"◂ Ready\",\n    fontsize=9,\n    color=sns.desaturate(\"#009E73\", 0.55),\n    fontweight=\"bold\",\n    ha=\"center\",\n    va=\"center\",\n    zorder=7,\n)\nax.text(\n    dir_angle,\n    4.7,\n    \"Emerging ▸\",\n    fontsize=9,\n    color=sns.desaturate(\"#AE3030\", 0.55),\n    fontweight=\"bold\",\n    ha=\"center\",\n    va=\"center\",\n    zorder=7,\n)\n\n# Title — figure-centered via suptitle so it doesn't clip at axes boundaries\ntitle = \"radar-innovation-timeline · python · seaborn · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title)))\nfig.suptitle(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK, y=0.97)\n\n# Combined legend — placed at bottom of figure using the 270° chart's natural gap\nsector_handles = [\n    mlines.Line2D(\n        [],\n        [],\n        marker=sector_markers[s],\n        color=\"w\",\n        markerfacecolor=sector_palette[s],\n        markeredgecolor=PAGE_BG,\n        markersize=10,\n        label=s,\n    )\n    for s in sectors\n]\nring_sizes_legend = {\"Adopt (Now)\": 440, \"Trial (Next)\": 340, \"Assess (Explore)\": 250, \"Hold (Watch)\": 150}\nring_handles = [\n    mlines.Line2D(\n        [],\n        [],\n        marker=\"o\",\n        color=\"w\",\n        markerfacecolor=INK_MUTED,\n        markeredgecolor=PAGE_BG,\n        markersize=np.sqrt(sz) / 3,\n        label=label,\n    )\n    for label, sz in ring_sizes_legend.items()\n]\nlegend = fig.legend(\n    handles=sector_handles + ring_handles,\n    loc=\"lower center\",\n    bbox_to_anchor=(0.5, 0.01),\n    ncols=4,\n    fontsize=8,\n    title=\"Sectors (shape+color) · Time Horizons (size)\",\n    title_fontsize=9,\n    framealpha=0.95,\n    edgecolor=INK_SOFT,\n    fancybox=True,\n    handletextpad=0.8,\n    borderpad=0.9,\n)\nlegend.get_frame().set_facecolor(ELEVATED_BG)\n\n# Save — square canvas, no bbox_inches trim (seaborn hard rule)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}