{"spec_id":"radar-innovation-timeline","library":"echarts","language":"javascript","code":"// anyplot.ai\n// radar-innovation-timeline: Innovation Radar with Time-Horizon Rings\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-26\n\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Deterministic PRNG (LCG) so radial jitter is reproducible --------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\n// --- Data: rings = adoption horizon (inner = now), sectors = theme ---------\nconst RINGS = [\"Adopt\", \"Trial\", \"Assess\", \"Hold\"];\nconst SECTORS = [\"AI & ML\", \"Data Platforms\", \"Cloud Infrastructure\", \"Security\"];\nconst SYMBOLS = [\"circle\", \"diamond\", \"triangle\", \"roundRect\"];\n\nconst ITEMS = [\n  { name: \"LLM Fine-Tuning\", sector: \"AI & ML\", ring: \"Adopt\" },\n  { name: \"Vector Databases\", sector: \"AI & ML\", ring: \"Adopt\" },\n  { name: \"RAG Pipelines\", sector: \"AI & ML\", ring: \"Trial\" },\n  { name: \"AI Agents\", sector: \"AI & ML\", ring: \"Trial\" },\n  { name: \"Multimodal Models\", sector: \"AI & ML\", ring: \"Assess\" },\n  { name: \"Synthetic Data Gen\", sector: \"AI & ML\", ring: \"Hold\" },\n  { name: \"Streaming ETL\", sector: \"Data Platforms\", ring: \"Adopt\" },\n  { name: \"Data Mesh\", sector: \"Data Platforms\", ring: \"Trial\" },\n  { name: \"Feature Stores\", sector: \"Data Platforms\", ring: \"Trial\" },\n  { name: \"Lakehouse Design\", sector: \"Data Platforms\", ring: \"Assess\" },\n  { name: \"Graph Databases\", sector: \"Data Platforms\", ring: \"Assess\" },\n  { name: \"Data Contracts\", sector: \"Data Platforms\", ring: \"Hold\" },\n  { name: \"Kubernetes Autoscaling\", sector: \"Cloud Infrastructure\", ring: \"Adopt\" },\n  { name: \"Serverless Functions\", sector: \"Cloud Infrastructure\", ring: \"Adopt\" },\n  { name: \"Service Mesh\", sector: \"Cloud Infrastructure\", ring: \"Trial\" },\n  { name: \"Edge Computing\", sector: \"Cloud Infrastructure\", ring: \"Assess\" },\n  { name: \"WASM Runtimes\", sector: \"Cloud Infrastructure\", ring: \"Hold\" },\n  { name: \"Confidential Compute\", sector: \"Cloud Infrastructure\", ring: \"Hold\" },\n  { name: \"Zero Trust Network\", sector: \"Security\", ring: \"Adopt\" },\n  { name: \"SBOM Tooling\", sector: \"Security\", ring: \"Trial\" },\n  { name: \"Passwordless Auth\", sector: \"Security\", ring: \"Trial\" },\n  { name: \"Post-Quantum Crypto\", sector: \"Security\", ring: \"Assess\" },\n  { name: \"AI Threat Detection\", sector: \"Security\", ring: \"Assess\" },\n  { name: \"Homomorphic Encryption\", sector: \"Security\", ring: \"Hold\" },\n];\n\n// A couple of items get a small visual accent (bigger halo-bordered marker +\n// bold label) so the chart points toward a specific insight beyond the base\n// ring/sector position encoding: the fastest-advancing item, and one facing\n// a looming regulatory deadline.\nconst HIGHLIGHTS = new Set([\"AI Agents\", \"Post-Quantum Crypto\"]);\n\n// --- Angular layout: three-quarter circle, the remaining 90° carries the ---\n// --- legend + ring labels instead of a sector, per the spec's guidance -----\nconst SWEEP = 270;\nconst SECTOR_WIDTH = SWEEP / SECTORS.length;\nconst ringIndexOf = (ring) => RINGS.indexOf(ring);\n\nconst groups = new Map();\nITEMS.forEach((item) => {\n  const key = `${item.sector}|${item.ring}`;\n  if (!groups.has(key)) groups.set(key, []);\n  groups.get(key).push(item);\n});\n\nITEMS.forEach((item) => {\n  const sectorIdx = SECTORS.indexOf(item.sector);\n  const siblings = groups.get(`${item.sector}|${item.ring}`);\n  const pos = siblings.indexOf(item);\n  const n = siblings.length;\n  const margin = SECTOR_WIDTH * 0.18;\n  const usable = SECTOR_WIDTH - margin * 2;\n  const spread = n === 1 ? usable / 2 : (pos / (n - 1)) * usable;\n  item.angle = sectorIdx * SECTOR_WIDTH + margin + spread;\n  const band = ringIndexOf(item.ring);\n  item.radius = band + 0.22 + rand() * 0.56; // stay clear of ring boundaries\n});\n\n// --- Polar geometry, shared between the ECharts polar grid and the manual --\n// --- pixel math used to place the sector/ring text labels ------------------\nconst CENTER_X = size.width * 0.5;\nconst CENTER_Y = size.height * 0.55;\nconst OUTER_R = Math.min(size.width, size.height) * 0.37;\nconst INNER_R = OUTER_R * 0.13;\n\nfunction polarPoint(radiusValue, angleValue) {\n  const std = ((90 - angleValue) * Math.PI) / 180;\n  const r = INNER_R + (radiusValue / 4) * (OUTER_R - INNER_R);\n  return [CENTER_X + r * Math.cos(std), CENTER_Y - r * Math.sin(std)];\n}\n\n// labels point outward from the ring/sector they belong to instead of all\n// defaulting to one side, so they stay legible around the full sweep\nfunction outwardLabelPosition(angleValue) {\n  const std = ((90 - angleValue) * Math.PI) / 180;\n  const dx = Math.cos(std);\n  const dy = Math.sin(std);\n  if (Math.abs(dx) > Math.abs(dy)) return dx >= 0 ? \"right\" : \"left\";\n  return dy >= 0 ? \"top\" : \"bottom\";\n}\n\n// text-graphic labels near the canvas edge must anchor away from that edge\n// (not centered on it) or long names clip past the frame\nfunction edgeSafeAlign(angleValue) {\n  const std = ((90 - angleValue) * Math.PI) / 180;\n  const dx = Math.cos(std);\n  const dy = Math.sin(std);\n  return {\n    align: dx > 0.25 ? \"right\" : dx < -0.25 ? \"left\" : \"center\",\n    verticalAlign: dy > 0.25 ? \"top\" : dy < -0.25 ? \"bottom\" : \"middle\",\n  };\n}\n\n// --- Subtle per-ring background, fading from the near-term core outward ----\nconst inkRgb = t.theme === \"light\" ? \"26,26,23\" : \"240,239,232\";\nconst ringFills = [0.16, 0.115, 0.07, 0.035].map((a) => `rgba(${inkRgb},${a})`);\n\n// --- Sector header + ring labels, placed via graphic elements so they can --\n// --- sit beyond the axis's own radius domain and read outward -------------\nconst sectorLabels = SECTORS.map((sector, i) => {\n  const angle = i * SECTOR_WIDTH + SECTOR_WIDTH / 2;\n  const [x, y] = polarPoint(4.35, angle);\n  return {\n    type: \"text\",\n    left: x,\n    top: y,\n    style: {\n      text: sector,\n      fill: t.palette[i],\n      fontSize: 17,\n      fontWeight: 600,\n      ...edgeSafeAlign(angle),\n    },\n  };\n});\n\nconst ringLabels = RINGS.map((ring, i) => {\n  const angle = 315; // mid-gap, the empty quarter\n  const [x, y] = polarPoint(i + 0.5, angle);\n  return {\n    type: \"text\",\n    left: x,\n    top: y,\n    style: {\n      text: ring,\n      fill: t.inkSoft,\n      fontSize: 13,\n      fontStyle: \"italic\",\n      ...edgeSafeAlign(angle),\n    },\n  };\n});\n\n// --- One series per sector so color + legend map 1:1 to the category -------\nconst series = SECTORS.map((sector, i) => ({\n  name: sector,\n  type: \"scatter\",\n  coordinateSystem: \"polar\",\n  symbol: SYMBOLS[i % SYMBOLS.length],\n  symbolSize: 24,\n  itemStyle: { color: t.palette[i], borderColor: t.pageBg, borderWidth: 2 },\n  label: {\n    show: true,\n    formatter: (p) => p.data.name,\n    distance: 9,\n    fontSize: 14,\n    color: t.inkSoft,\n  },\n  labelLayout: { hideOverlap: true },\n  data: ITEMS.filter((item) => item.sector === sector).map((item) => {\n    const highlighted = HIGHLIGHTS.has(item.name);\n    return {\n      name: item.name,\n      value: [item.radius, item.angle],\n      symbolSize: highlighted ? 32 : 24,\n      itemStyle: highlighted\n        ? { borderWidth: 3, shadowBlur: 14, shadowColor: t.palette[i] }\n        : undefined,\n      label: {\n        position: outwardLabelPosition(item.angle),\n        fontWeight: highlighted ? 700 : 400,\n        fontSize: highlighted ? 15 : 14,\n      },\n    };\n  }),\n}));\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"radar-innovation-timeline · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  legend: {\n    data: SECTORS,\n    orient: \"horizontal\",\n    left: \"center\",\n    top: 68,\n    itemWidth: 16,\n    itemHeight: 16,\n    itemGap: 26,\n    textStyle: { color: t.ink, fontSize: 15 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (p) => `${p.data.name}<br/>${p.seriesName} · ${RINGS[Math.floor(p.data.value[0])]}`,\n  },\n  polar: { center: [CENTER_X, CENTER_Y], radius: [INNER_R, OUTER_R] },\n  angleAxis: {\n    type: \"value\",\n    min: 0,\n    max: 360,\n    startAngle: 90,\n    clockwise: true,\n    axisLine: { show: false },\n    axisTick: { show: false },\n    axisLabel: { show: false },\n    splitLine: { show: false },\n  },\n  radiusAxis: {\n    type: \"value\",\n    min: 0,\n    max: 4,\n    interval: 1,\n    axisLine: { show: false },\n    axisTick: { show: false },\n    axisLabel: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n    splitArea: { show: true, areaStyle: { color: ringFills } },\n  },\n  graphic: [...sectorLabels, ...ringLabels],\n  series,\n});\n"}