{"spec_id":"radar-innovation-timeline","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nradar-innovation-timeline: Innovation Radar with Time-Horizon Rings\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-29\n\"\"\"\n\nimport math\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_label,\n    geom_path,\n    geom_point,\n    geom_polygon,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_void,\n)\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette + theme-adaptive chrome\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 categorical palette — 8 hues, hybrid-v3 sort\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\nnp.random.seed(42)\n\n# --- Data ---\nrings = [\"Adopt\", \"Trial\", \"Assess\", \"Hold\"]\n# Sector order chosen to separate heavy-label sectors (AI & ML, Infrastructure) across the arc\nsectors = [\"AI & ML\", \"Biotech\", \"Sustainability\", \"Infrastructure\"]\n\n# Sector colors: Imprint palette positions 1–4 in canonical order\nsector_colors = {\n    \"AI & ML\": IMPRINT_PALETTE[0],  # #009E73 brand green\n    \"Biotech\": IMPRINT_PALETTE[1],  # #C475FD lavender\n    \"Sustainability\": IMPRINT_PALETTE[2],  # #4467A3 blue\n    \"Infrastructure\": IMPRINT_PALETTE[3],  # #BD8233 ochre\n}\n\n# Ring fills: semantic green→amber→ochre→red gradient communicates adoption readiness\nif THEME == \"light\":\n    ring_fills = {\"Adopt\": \"#DCEDC8\", \"Trial\": \"#FFF9C4\", \"Assess\": \"#FFE0B2\", \"Hold\": \"#FFCDD2\"}\n    ring_alpha = 0.55\nelse:\n    # Imprint-derived tints at low alpha for dark background\n    ring_fills = {\"Adopt\": \"#009E73\", \"Trial\": \"#DDCC77\", \"Assess\": \"#BD8233\", \"Hold\": \"#AE3030\"}\n    ring_alpha = 0.14\n\nring_inner = {\"Adopt\": 0.5, \"Trial\": 1.5, \"Assess\": 2.5, \"Hold\": 3.5}\nring_outer = {\"Adopt\": 1.5, \"Trial\": 2.5, \"Assess\": 3.5, \"Hold\": 4.5}\n# Angular nudge for Hold ring items to prevent overlap with sector header labels\nring_nudge = {\"Adopt\": 0.0, \"Trial\": 0.0, \"Assess\": 0.0, \"Hold\": 0.20}\n\n# 270-degree arc (gap at top for ring labels)\nARC_START = math.pi * 3 / 4\nARC_SPAN = math.pi * 3 / 2\nARC_END = ARC_START + ARC_SPAN\nSECTOR_SPAN = ARC_SPAN / len(sectors)\n\ninnovations = [\n    {\"name\": \"LLM Agents\", \"ring\": \"Adopt\", \"sector\": \"AI & ML\"},\n    {\"name\": \"RAG Pipelines\", \"ring\": \"Adopt\", \"sector\": \"AI & ML\"},\n    {\"name\": \"Vision Models\", \"ring\": \"Trial\", \"sector\": \"AI & ML\"},\n    {\"name\": \"AI Code Review\", \"ring\": \"Trial\", \"sector\": \"AI & ML\"},\n    {\"name\": \"Neuro-symbolic AI\", \"ring\": \"Assess\", \"sector\": \"AI & ML\"},\n    {\"name\": \"Quantum ML\", \"ring\": \"Hold\", \"sector\": \"AI & ML\"},\n    {\"name\": \"mRNA Platforms\", \"ring\": \"Adopt\", \"sector\": \"Biotech\"},\n    {\"name\": \"Gene Editing\", \"ring\": \"Trial\", \"sector\": \"Biotech\"},\n    {\"name\": \"Synthetic Biology\", \"ring\": \"Trial\", \"sector\": \"Biotech\"},\n    {\"name\": \"Digital Twins (Bio)\", \"ring\": \"Assess\", \"sector\": \"Biotech\"},\n    {\"name\": \"Organ-on-Chip\", \"ring\": \"Assess\", \"sector\": \"Biotech\"},\n    {\"name\": \"Nanomedicine\", \"ring\": \"Hold\", \"sector\": \"Biotech\"},\n    {\"name\": \"Carbon Tracking\", \"ring\": \"Adopt\", \"sector\": \"Sustainability\"},\n    {\"name\": \"Green Cloud\", \"ring\": \"Trial\", \"sector\": \"Sustainability\"},\n    {\"name\": \"Circular Design\", \"ring\": \"Trial\", \"sector\": \"Sustainability\"},\n    {\"name\": \"Biodegradable PCBs\", \"ring\": \"Assess\", \"sector\": \"Sustainability\"},\n    {\"name\": \"Ocean Cleanup AI\", \"ring\": \"Assess\", \"sector\": \"Sustainability\"},\n    {\"name\": \"Fusion Energy\", \"ring\": \"Hold\", \"sector\": \"Sustainability\"},\n    {\"name\": \"Edge Computing\", \"ring\": \"Adopt\", \"sector\": \"Infrastructure\"},\n    {\"name\": \"WebAssembly\", \"ring\": \"Adopt\", \"sector\": \"Infrastructure\"},\n    {\"name\": \"Service Mesh\", \"ring\": \"Trial\", \"sector\": \"Infrastructure\"},\n    {\"name\": \"Satellite Internet\", \"ring\": \"Trial\", \"sector\": \"Infrastructure\"},\n    {\"name\": \"Confidential Compute\", \"ring\": \"Assess\", \"sector\": \"Infrastructure\"},\n    {\"name\": \"6G Research\", \"ring\": \"Hold\", \"sector\": \"Infrastructure\"},\n]\n\n# --- Compute positions ---\ndf = pd.DataFrame(innovations)\nsector_idx_map = {s: i for i, s in enumerate(sectors)}\nring_mid_map = {r: (ring_inner[r] + ring_outer[r]) / 2 for r in rings}\n\n# Vectorized angular placement per sector/ring group\nangles = np.zeros(len(df))\nfor (sector, ring), group in df.groupby([\"sector\", \"ring\"], sort=False):\n    center = ARC_START + sector_idx_map[sector] * SECTOR_SPAN + SECTOR_SPAN / 2\n    n = len(group)\n    spread = SECTOR_SPAN * 0.72\n    offsets = np.linspace(-spread / 2, spread / 2, n) if n > 1 else np.array([0.0])\n    angles[group.index] = center + offsets + ring_nudge[ring]\n\ndf[\"angle\"] = angles\ndf[\"radius\"] = df[\"ring\"].map(ring_mid_map) + np.random.uniform(-0.15, 0.15, len(df))\ndf[\"x\"] = df[\"radius\"] * np.cos(df[\"angle\"])\ndf[\"y\"] = df[\"radius\"] * np.sin(df[\"angle\"])\n\n# Label positions: pushed radially outward\nlabel_offsets = {\"Adopt\": 0.72, \"Trial\": 0.62, \"Assess\": 0.52, \"Hold\": 0.42}\ndf[\"label_r\"] = df[\"radius\"] + df[\"ring\"].map(label_offsets)\ndf[\"lx\"] = df[\"label_r\"] * np.cos(df[\"angle\"])\ndf[\"ly\"] = df[\"label_r\"] * np.sin(df[\"angle\"])\ndf[\"side\"] = np.where(df[\"lx\"] < 0, \"left\", \"right\")\n\n# Label repulsion: push overlapping labels apart vertically on each side\nMIN_Y_SEP = 0.75\nfor _ in range(35):\n    for side in [\"left\", \"right\"]:\n        side_idx = df.loc[df[\"side\"] == side].sort_values(\"ly\").index.tolist()\n        for k in range(len(side_idx) - 1):\n            i, j = side_idx[k], side_idx[k + 1]\n            if abs(df.loc[j, \"lx\"] - df.loc[i, \"lx\"]) < 2.5:\n                dy = df.loc[j, \"ly\"] - df.loc[i, \"ly\"]\n                if dy < MIN_Y_SEP:\n                    push = (MIN_Y_SEP - dy) / 2\n                    df.loc[j, \"ly\"] += push\n                    df.loc[i, \"ly\"] -= push\n\n# --- Structural geometry ---\narc_pts = np.linspace(ARC_START, ARC_END, 120)\n\n# Ring background polygons (annular sectors)\nring_bg_rows = []\nfor rname in rings:\n    r_in, r_out = ring_inner[rname], ring_outer[rname]\n    xs = np.concatenate([r_out * np.cos(arc_pts), r_in * np.cos(arc_pts[::-1])])\n    ys = np.concatenate([r_out * np.sin(arc_pts), r_in * np.sin(arc_pts[::-1])])\n    for px, py in zip(np.append(xs, xs[0]), np.append(ys, ys[0]), strict=True):\n        ring_bg_rows.append({\"x\": px, \"y\": py, \"ring\": rname})\nring_bg_df = pd.DataFrame(ring_bg_rows)\n\n# Ring boundary arcs\nbnd_rows = []\nfor r in [0.5, 1.5, 2.5, 3.5, 4.5]:\n    for idx, a in enumerate(arc_pts):\n        bnd_rows.append({\"x\": r * math.cos(a), \"y\": r * math.sin(a), \"g\": f\"r{r}\", \"o\": idx})\nbnd_df = pd.DataFrame(bnd_rows).sort_values([\"g\", \"o\"])\n\n# Sector divider spokes\nspoke_angles = [ARC_START + i * SECTOR_SPAN for i in range(len(sectors) + 1)]\nspoke_df = pd.DataFrame(\n    [\n        {\"x\": 0.5 * math.cos(a), \"y\": 0.5 * math.sin(a), \"xend\": 4.5 * math.cos(a), \"yend\": 4.5 * math.sin(a)}\n        for a in spoke_angles\n    ]\n)\n\n# Sector header labels along outer edge\nsector_label_df = pd.DataFrame(\n    [\n        {\n            \"label\": s,\n            \"x\": 5.5 * math.cos(ARC_START + (i + 0.5) * SECTOR_SPAN),\n            \"y\": 5.5 * math.sin(ARC_START + (i + 0.5) * SECTOR_SPAN),\n        }\n        for i, s in enumerate(sectors)\n    ]\n)\n\n# Ring name labels in arc gap (90 degrees = top center)\ngap_angle = math.pi / 2\nring_label_df = pd.DataFrame(\n    [\n        {\"label\": r, \"x\": ring_mid_map[r] * math.cos(gap_angle), \"y\": ring_mid_map[r] * math.sin(gap_angle)}\n        for r in rings\n    ]\n)\n\n# --- Build plot ---\nplot = ggplot()\n\n# Ring background fills with semantic color gradient (green=safe → pink=risky)\nfor rname in rings:\n    rdata = ring_bg_df[ring_bg_df[\"ring\"] == rname]\n    plot += geom_polygon(aes(\"x\", \"y\"), data=rdata, fill=ring_fills[rname], alpha=ring_alpha)\n\n# Structural lines: ring boundaries and sector spokes\nplot += geom_path(aes(\"x\", \"y\", group=\"g\"), data=bnd_df, color=INK_SOFT, size=0.3, alpha=0.5)\nplot += geom_segment(aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), data=spoke_df, color=INK_SOFT, size=0.3, alpha=0.5)\n\n# Sector header labels\nplot += geom_text(aes(\"x\", \"y\", label=\"label\"), data=sector_label_df, size=5, color=INK, fontface=\"bold\")\n\n# Ring labels with background box (geom_label for visual clarity over ring fills)\nplot += geom_label(\n    aes(\"x\", \"y\", label=\"label\"),\n    data=ring_label_df,\n    size=4,\n    color=INK_SOFT,\n    fontface=\"bold\",\n    fill=ELEVATED_BG,\n    alpha=0.9,\n)\n\n# Thin connector lines from points to labels (aids readability after repulsion)\nplot += geom_segment(aes(x=\"x\", y=\"y\", xend=\"lx\", yend=\"ly\"), data=df, size=0.3, alpha=0.25, color=INK_MUTED)\n\n# Innovation points with interactive tooltips (letsplot-specific for HTML export)\nplot += geom_point(\n    aes(\"x\", \"y\", color=\"sector\"),\n    data=df,\n    size=4,\n    alpha=0.9,\n    tooltips=layer_tooltips().line(\"@name\").line(\"Ring: @ring\").line(\"Sector: @sector\"),\n)\n\n# Innovation labels split by side for outward text alignment\nfor side, hj in [(\"left\", 1), (\"right\", 0)]:\n    side_df = df[df[\"side\"] == side]\n    plot += geom_text(aes(\"lx\", \"ly\", label=\"name\", color=\"sector\"), data=side_df, size=4.5, hjust=hj)\n\ntitle = \"radar-innovation-timeline · python · letsplot · anyplot.ai\"\n\n# Styling — theme_void() removes all axis/grid defaults; custom theme restores chrome\nplot += (\n    theme_void()\n    + scale_color_manual(values=sector_colors)\n    + scale_x_continuous(limits=(-7.0, 7.0))\n    + scale_y_continuous(limits=(-6.2, 5.5))\n    + coord_fixed()\n    + labs(title=title, color=\"Sector\")\n    + ggsize(600, 600)\n    + theme(\n        plot_title=element_text(size=16, color=INK, face=\"bold\"),\n        legend_title=element_text(size=12, color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        legend_position=\"bottom\",\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid=element_blank(),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    )\n)\n\n# Save PNG (scale=4 → 2400×2400 px) and interactive HTML\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}