{"spec_id":"radar-innovation-timeline","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nradar-innovation-timeline: Innovation Radar with Time-Horizon Rings\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-29\n\"\"\"\n\nimport sys\n\n\nsys.path = sys.path[1:]  # prevent this file from shadowing the installed plotly package\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens (Imprint palette — see 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\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\nnp.random.seed(42)\n\n# Data — technology innovations across sectors and time horizons\nsectors = [\"AI & ML\", \"Cloud & Infra\", \"Sustainability\", \"Biotech\"]\nrings = [\"Adopt\", \"Trial\", \"Assess\", \"Hold\"]\n# Wider spread separates rings to reduce inner-zone label collision\nring_radii = {\"Adopt\": 1.5, \"Trial\": 2.7, \"Assess\": 3.9, \"Hold\": 5.1}\nring_marker_sizes = {\"Adopt\": 18, \"Trial\": 14, \"Assess\": 11, \"Hold\": 9}\nring_boundaries = [2.1, 3.3, 4.5, 5.7]\nR_MAX = 6.3\n\n# Polar domain (landscape): x=[0.02, 0.70], y=[0.04, 0.96]\n# radius limited by y: min(0.34*800, 0.46*450)=min(272,207)=207 px\nPOLAR_CX = 0.36\nPOLAR_CY = 0.50\nPOLAR_RX = 0.259  # paper-x per radial unit (207/800)\nPOLAR_RY = 0.460  # paper-y per radial unit (207/450)\nHEADER_R = 5.9\n\ninnovations = [\n    # AI & ML — \"AI Code Review\" flipped to left textpos to prevent right-boundary clipping\n    {\"name\": \"LLM Agents\", \"sector\": \"AI & ML\", \"ring\": \"Adopt\", \"textpos\": \"top center\"},\n    {\"name\": \"RAG Pipelines\", \"sector\": \"AI & ML\", \"ring\": \"Adopt\", \"textpos\": \"bottom left\"},\n    {\"name\": \"Multimodal Models\", \"sector\": \"AI & ML\", \"ring\": \"Trial\", \"textpos\": \"bottom center\"},\n    {\"name\": \"AI Code Review\", \"sector\": \"AI & ML\", \"ring\": \"Trial\", \"textpos\": \"top center\"},\n    {\"name\": \"Neuromorphic Chips\", \"sector\": \"AI & ML\", \"ring\": \"Assess\", \"textpos\": \"top center\"},\n    {\"name\": \"Autonomous Research\", \"sector\": \"AI & ML\", \"ring\": \"Hold\", \"textpos\": \"bottom center\"},\n    {\"name\": \"Quantum ML\", \"sector\": \"AI & ML\", \"ring\": \"Hold\", \"textpos\": \"top center\"},\n    # Cloud & Infra — right-edge items flipped to left textpos to prevent boundary clipping\n    {\"name\": \"Platform Engineering\", \"sector\": \"Cloud & Infra\", \"ring\": \"Adopt\", \"textpos\": \"bottom right\"},\n    {\"name\": \"FinOps\", \"sector\": \"Cloud & Infra\", \"ring\": \"Adopt\", \"textpos\": \"top right\"},\n    {\"name\": \"WebAssembly\", \"sector\": \"Cloud & Infra\", \"ring\": \"Trial\", \"textpos\": \"top center\"},\n    {\"name\": \"Edge Computing\", \"sector\": \"Cloud & Infra\", \"ring\": \"Trial\", \"textpos\": \"bottom center\"},\n    {\"name\": \"Confidential Computing\", \"sector\": \"Cloud & Infra\", \"ring\": \"Assess\", \"textpos\": \"top left\"},\n    {\"name\": \"Serverless GPUs\", \"sector\": \"Cloud & Infra\", \"ring\": \"Assess\", \"textpos\": \"bottom left\"},\n    {\"name\": \"Decentralized Cloud\", \"sector\": \"Cloud & Infra\", \"ring\": \"Hold\", \"textpos\": \"top center\"},\n    # Sustainability — \"Circular Economy Platforms\" flipped to left to avoid right clipping\n    {\"name\": \"Carbon Tracking APIs\", \"sector\": \"Sustainability\", \"ring\": \"Adopt\", \"textpos\": \"top left\"},\n    {\"name\": \"Green Software\", \"sector\": \"Sustainability\", \"ring\": \"Trial\", \"textpos\": \"top right\"},\n    {\"name\": \"Digital Product Passports\", \"sector\": \"Sustainability\", \"ring\": \"Trial\", \"textpos\": \"bottom left\"},\n    {\"name\": \"Circular Economy Platforms\", \"sector\": \"Sustainability\", \"ring\": \"Assess\", \"textpos\": \"bottom left\"},\n    {\"name\": \"Climate AI\", \"sector\": \"Sustainability\", \"ring\": \"Assess\", \"textpos\": \"top left\"},\n    {\"name\": \"Fusion Energy Tech\", \"sector\": \"Sustainability\", \"ring\": \"Hold\", \"textpos\": \"top center\"},\n    # Biotech — text positions flipped inward to prevent left-edge clipping\n    {\"name\": \"mRNA Therapeutics\", \"sector\": \"Biotech\", \"ring\": \"Adopt\", \"textpos\": \"top center\"},\n    {\"name\": \"CRISPR Diagnostics\", \"sector\": \"Biotech\", \"ring\": \"Trial\", \"textpos\": \"bottom right\"},\n    {\"name\": \"Digital Twins (Health)\", \"sector\": \"Biotech\", \"ring\": \"Assess\", \"textpos\": \"bottom right\"},\n    {\"name\": \"Synthetic Biology\", \"sector\": \"Biotech\", \"ring\": \"Assess\", \"textpos\": \"top right\"},\n    {\"name\": \"Brain-Computer Interfaces\", \"sector\": \"Biotech\", \"ring\": \"Hold\", \"textpos\": \"bottom center\"},\n    {\"name\": \"Longevity Engineering\", \"sector\": \"Biotech\", \"ring\": \"Hold\", \"textpos\": \"top right\"},\n]\n\n# Sector colors using Imprint palette canonical order\nsector_colors = {\n    \"AI & ML\": IMPRINT_PALETTE[0],  # #009E73 brand green\n    \"Cloud & Infra\": IMPRINT_PALETTE[1],  # #C475FD lavender\n    \"Sustainability\": IMPRINT_PALETTE[2],  # #4467A3 blue\n    \"Biotech\": IMPRINT_PALETTE[3],  # #BD8233 ochre\n}\n\nsector_symbols = {\"AI & ML\": \"circle\", \"Cloud & Infra\": \"diamond\", \"Sustainability\": \"square\", \"Biotech\": \"triangle-up\"}\n\n# 270° layout, 4 equal sectors\nsector_span = 270 / len(sectors)\nsector_starts = {s: i * sector_span for i, s in enumerate(sectors)}\n\n# Compute item positions within each sector ring\nitems_by_sector = {s: [] for s in sectors}\n\nfor sector in sectors:\n    start_angle = sector_starts[sector]\n    sector_items = [inn for inn in innovations if inn[\"sector\"] == sector]\n\n    for ring in rings:\n        ring_items = [it for it in sector_items if it[\"ring\"] == ring]\n        if not ring_items:\n            continue\n        n = len(ring_items)\n        padding = sector_span * 0.22\n        usable_span = sector_span - 2 * padding\n        for idx, item in enumerate(ring_items):\n            if n == 1:\n                angle = start_angle + sector_span / 2\n            else:\n                angle = start_angle + padding + usable_span * idx / (n - 1)\n            base_r = ring_radii[ring]\n            jitter_range = 0.10 if ring == \"Adopt\" else 0.15\n            jitter = np.random.uniform(-jitter_range, jitter_range)\n            items_by_sector[sector].append(\n                {\"name\": item[\"name\"], \"ring\": ring, \"angle\": angle, \"r\": base_r + jitter, \"textpos\": item[\"textpos\"]}\n            )\n\n# Ring fill colors — theme-adaptive, Imprint green tinted bands\nif THEME == \"light\":\n    ring_fill_colors = [\"rgba(0,158,115,0.10)\", \"rgba(0,158,115,0.06)\", \"rgba(0,158,115,0.04)\", \"rgba(0,158,115,0.02)\"]\nelse:\n    ring_fill_colors = [\"rgba(0,158,115,0.18)\", \"rgba(0,158,115,0.10)\", \"rgba(0,158,115,0.06)\", \"rgba(0,158,115,0.03)\"]\n\nring_line_color = \"rgba(26,26,23,0.12)\" if THEME == \"light\" else \"rgba(240,239,232,0.12)\"\nsector_line_color = \"rgba(26,26,23,0.18)\" if THEME == \"light\" else \"rgba(240,239,232,0.18)\"\n\n# Plot\nfig = go.Figure()\n\n# Ring background fills — non-overlapping bands from center outward\ntheta_fill = np.linspace(0, 270, 200).tolist()\nring_bands = [(0, ring_boundaries[0])] + list(zip(ring_boundaries[:-1], ring_boundaries[1:], strict=False))\nfor i, (r_inner, r_outer) in enumerate(ring_bands):\n    fig.add_trace(\n        go.Scatterpolar(\n            r=[r_outer] * len(theta_fill) + [r_inner] * len(theta_fill),\n            theta=theta_fill + theta_fill[::-1],\n            fill=\"toself\",\n            fillcolor=ring_fill_colors[i],\n            line={\"width\": 0},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Ring boundary lines\nfor radius in ring_boundaries:\n    theta_circle = np.linspace(0, 270, 200)\n    fig.add_trace(\n        go.Scatterpolar(\n            r=np.full_like(theta_circle, radius).tolist(),\n            theta=theta_circle.tolist(),\n            mode=\"lines\",\n            line={\"color\": ring_line_color, \"width\": 1.2},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Sector divider lines\nfor angle in [sector_starts[s] for s in sectors] + [270]:\n    fig.add_trace(\n        go.Scatterpolar(\n            r=[0, R_MAX],\n            theta=[angle, angle],\n            mode=\"lines\",\n            line={\"color\": sector_line_color, \"width\": 1, \"dash\": \"dot\"},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n# Innovation items per sector — marker size encodes time horizon priority;\n# inner rings (Adopt/Trial) use smaller textfont (10px) to reduce label crowding\nfor sector in sectors:\n    items = items_by_sector[sector]\n    color = sector_colors[sector]\n    fig.add_trace(\n        go.Scatterpolar(\n            r=[it[\"r\"] for it in items],\n            theta=[it[\"angle\"] for it in items],\n            mode=\"markers+text\",\n            marker={\n                \"size\": [ring_marker_sizes[it[\"ring\"]] for it in items],\n                \"color\": color,\n                \"symbol\": sector_symbols[sector],\n                \"line\": {\"color\": PAGE_BG, \"width\": 2},\n                \"opacity\": [1.0 if it[\"ring\"] == \"Adopt\" else 0.82 for it in items],\n            },\n            text=[f\"<b>{it['name']}</b>\" if it[\"ring\"] == \"Adopt\" else it[\"name\"] for it in items],\n            textposition=[it[\"textpos\"] for it in items],\n            textfont={\"size\": [10 if it[\"ring\"] in (\"Adopt\", \"Trial\") else 12 for it in items], \"color\": color},\n            name=sector,\n            legendgroup=sector,\n            hovertemplate=\"%{text}<br>Ring: %{customdata}<extra>\" + sector + \"</extra>\",\n            customdata=[it[\"ring\"] for it in items],\n        )\n    )\n\n# Ring labels as paper-coordinate annotations in the empty arc gap (270°–360°).\n# Placed at angle=282° (well inside the gap, upper-left of the circle) with\n# xanchor=\"right\" so text extends leftward into the unoccupied region.\n# sin(282°) ≈ -0.978, cos(282°) ≈ 0.208\n_gap_sin = np.sin(np.radians(282))\n_gap_cos = np.cos(np.radians(282))\nring_label_annotations = []\nfor ring_name, label in [\n    (\"Adopt\", \"Adopt (Now)\"),\n    (\"Trial\", \"Trial (0–1 yr)\"),\n    (\"Assess\", \"Assess (1–3 yr)\"),\n    (\"Hold\", \"Hold (3+ yr)\"),\n]:\n    r = ring_radii[ring_name]\n    frac = r / R_MAX\n    lx = float(POLAR_CX + frac * POLAR_RX * _gap_sin)\n    ly = float(POLAR_CY + frac * POLAR_RY * _gap_cos)\n    ring_label_annotations.append(\n        {\n            \"text\": f\"<b>{label}</b>\",\n            \"x\": lx,\n            \"y\": ly,\n            \"xref\": \"paper\",\n            \"yref\": \"paper\",\n            \"xanchor\": \"right\",\n            \"yanchor\": \"middle\",\n            \"showarrow\": False,\n            \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        }\n    )\n\n# Sector header labels in paper coordinates to avoid polar-domain clipping.\n\nsector_header_annotations = []\nfor sector in sectors:\n    mid_deg = sector_starts[sector] + sector_span / 2\n    mid_rad = np.radians(mid_deg)\n    frac = HEADER_R / R_MAX\n    px = float(POLAR_CX + frac * POLAR_RX * np.sin(mid_rad))\n    py = float(POLAR_CY + frac * POLAR_RY * np.cos(mid_rad))\n    sector_header_annotations.append(\n        {\n            \"text\": f\"<b>{sector}</b>\",\n            \"x\": px,\n            \"y\": py,\n            \"xref\": \"paper\",\n            \"yref\": \"paper\",\n            \"xanchor\": \"center\",\n            \"yanchor\": \"middle\",\n            \"showarrow\": False,\n            \"font\": {\"size\": 17, \"color\": sector_colors[sector]},\n        }\n    )\n\n# Title\ntitle_text = \"radar-innovation-timeline · python · plotly · anyplot.ai\"\nn_chars = len(title_text)\ntitle_fontsize = max(11, round(16 * 67 / n_chars)) if n_chars > 67 else 16\n\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": (\n            title_text + \"<br><sup style='color:\" + INK_MUTED + \"; font-weight:normal'>\"\n            \"Technology adoption radar — inner rings = near-term, outer = longer horizon\"\n            \"</sup>\"\n        ),\n        \"font\": {\"size\": title_fontsize, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n        \"y\": 0.98,\n    },\n    polar={\n        \"radialaxis\": {\"visible\": False, \"range\": [0, R_MAX]},\n        \"angularaxis\": {\"visible\": False, \"direction\": \"clockwise\", \"rotation\": 90},\n        \"bgcolor\": PAGE_BG,\n        \"domain\": {\"x\": [0.02, 0.70], \"y\": [0.04, 0.96]},\n    },\n    legend={\n        \"font\": {\"size\": 12, \"color\": INK_SOFT},\n        \"x\": 0.72,\n        \"y\": 0.68,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"middle\",\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"title\": {\"text\": \"<b>Sectors</b>\", \"font\": {\"size\": 14, \"color\": INK}},\n    },\n    paper_bgcolor=PAGE_BG,\n    margin={\"l\": 30, \"r\": 30, \"t\": 70, \"b\": 30},\n    annotations=sector_header_annotations\n    + ring_label_annotations\n    + [\n        {\n            \"text\": (\n                \"<b>Time Horizons</b><br>\"\n                \"● <b>Adopt</b> — ready now<br>\"\n                \"● <b>Trial</b> — evaluating 0–1 yr<br>\"\n                \"● <b>Assess</b> — exploring 1–3 yr<br>\"\n                \"● <b>Hold</b> — future watch 3+ yr\"\n            ),\n            \"x\": 0.72,\n            \"y\": 0.42,\n            \"xref\": \"paper\",\n            \"yref\": \"paper\",\n            \"xanchor\": \"left\",\n            \"yanchor\": \"top\",\n            \"showarrow\": False,\n            \"font\": {\"size\": 12, \"color\": INK_SOFT},\n            \"bgcolor\": ELEVATED_BG,\n            \"bordercolor\": INK_SOFT,\n            \"borderwidth\": 1,\n            \"borderpad\": 8,\n        }\n    ],\n)\n\n# Save — landscape canvas (3200×1800) reduces empty top space and gives more room for labels\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}