{"spec_id":"radar-innovation-timeline","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// radar-innovation-timeline: Innovation Radar with Time-Horizon Rings\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-26\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Time horizon rings, innermost (near-term) to outermost (long-term).\nconst RINGS = [\"Now\", \"Next 6 Months\", \"1-2 Years\", \"3+ Years\"];\n// Thematic sectors, placed clockwise starting just right of the top gap.\nconst SECTORS = [\"AI & ML\", \"Cloud Infrastructure\", \"Cybersecurity\", \"Sustainability\"];\nconst SYMBOLS = [\"circle\", \"diamond\", \"triangle\", \"square\"];\n\nconst ITEMS = [\n  { name: \"Generative Search\", sector: 0, ring: 0 },\n  { name: \"Multimodal Assistants\", sector: 0, ring: 0 },\n  { name: \"On-device LLMs\", sector: 0, ring: 1 },\n  { name: \"Agentic Workflows\", sector: 0, ring: 1 },\n  { name: \"Neuromorphic Chips\", sector: 0, ring: 3 },\n  { name: \"Serverless GPU Pools\", sector: 1, ring: 0 },\n  { name: \"Edge Caching Mesh\", sector: 1, ring: 1 },\n  { name: \"Confidential Computing\", sector: 1, ring: 1 },\n  { name: \"WASM Microservices\", sector: 1, ring: 2 },\n  { name: \"Quantum Cloud APIs\", sector: 1, ring: 3 },\n  { name: \"Passwordless Auth\", sector: 2, ring: 0 },\n  { name: \"Zero Trust Mesh\", sector: 2, ring: 0 },\n  { name: \"AI Threat Hunting\", sector: 2, ring: 1 },\n  { name: \"Homomorphic Encryption\", sector: 2, ring: 2 },\n  { name: \"Post-Quantum Crypto\", sector: 2, ring: 3 },\n  { name: \"Carbon-aware Compute\", sector: 3, ring: 0 },\n  { name: \"Green Data Centers\", sector: 3, ring: 1 },\n  { name: \"Circular Hardware\", sector: 3, ring: 2 },\n  { name: \"Direct Air Capture Tech\", sector: 3, ring: 2 },\n  { name: \"Fusion Power Grids\", sector: 3, ring: 3 },\n];\n\n// --- Polar layout (near-full circle, small top gap for the ring-axis labels) --\nconst GAP_DEG = 60;\nconst SECTOR_SPAN_DEG = (360 - GAP_DEG) / SECTORS.length;\nconst SECTOR_START_DEG = GAP_DEG / 2; // first sector begins just right of 12 o'clock\nconst RING_BOUNDS = [0, 0.24, 0.48, 0.72, 0.96]; // normalized radius per ring boundary\nconst MAX_R = RING_BOUNDS[RING_BOUNDS.length - 1];\nconst DEG2RAD = Math.PI / 180;\n\n// angle 0 = 12 o'clock, increases clockwise (matches Highcharts pie/arc convention)\nfunction toXY(radius, angleDeg) {\n  const a = angleDeg * DEG2RAD;\n  return [radius * Math.sin(a), radius * Math.cos(a)];\n}\n\n// Spread items evenly across their sector's angular span (8% inset each side)\n// so consecutive items never share an angle, independent of ring assignment.\nconst bySector = SECTORS.map((_, i) => ITEMS.filter((item) => item.sector === i));\nconst seriesData = SECTORS.map((sectorName, sectorIndex) => {\n  const items = bySector[sectorIndex];\n  const inset = SECTOR_SPAN_DEG * 0.08;\n  const usableSpan = SECTOR_SPAN_DEG - 2 * inset;\n  const sectorStart = SECTOR_START_DEG + sectorIndex * SECTOR_SPAN_DEG;\n  const points = items.map((item, i) => {\n    const angleDeg = sectorStart + inset + ((i + 0.5) / items.length) * usableSpan;\n    const ringMid = (RING_BOUNDS[item.ring] + RING_BOUNDS[item.ring + 1]) / 2;\n    const [x, y] = toXY(ringMid, angleDeg);\n    // Push the data label radially outward from the chart center.\n    const [dx, dy] = toXY(1, angleDeg);\n    return {\n      x,\n      y,\n      name: item.name,\n      ring: RINGS[item.ring],\n      sector: sectorName,\n      dataLabels: {\n        x: dx * 24,\n        y: -dy * 24,\n        align: dx >= 0 ? \"left\" : \"right\",\n        verticalAlign: dy >= 0 ? \"bottom\" : \"top\",\n      },\n    };\n  });\n  return {\n    name: sectorName,\n    color: t.palette[sectorIndex],\n    marker: { symbol: SYMBOLS[sectorIndex] },\n    data: points,\n  };\n});\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load() {\n        drawRadarChrome(this);\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"radar-innovation-timeline · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: { min: -1.18, max: 1.18, visible: false },\n  yAxis: { min: -1.18, max: 1.18, visible: false, title: { text: null } },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: {\n    headerFormat: \"\",\n    pointFormat: \"<b>{point.name}</b><br/>{point.sector} · {point.ring}\",\n  },\n  plotOptions: {\n    series: { animation: false },\n    scatter: {\n      marker: { radius: 9, lineColor: t.pageBg, lineWidth: 1.5 },\n      dataLabels: {\n        enabled: true,\n        allowOverlap: false,\n        style: {\n          color: t.inkSoft,\n          fontSize: \"12px\",\n          fontWeight: \"normal\",\n          textOutline: \"none\",\n        },\n        formatter() {\n          return this.point.name;\n        },\n      },\n    },\n  },\n  series: seriesData.map((s) => ({ type: \"scatter\", ...s })),\n});\n\n// Draws the concentric ring bands, boundary rings, sector dividers, and the\n// ring/sector labels — geometry Highcharts core has no polar/radar support\n// for (that lives in the highcharts-more module, which isn't loaded), so the\n// radial chrome is built by hand with the SVG renderer and kept in lockstep\n// with the scatter series by reusing the same axis pixel conversion.\nfunction drawRadarChrome(chart) {\n  const r = chart.renderer;\n  const xAxis = chart.xAxis[0];\n  const yAxis = chart.yAxis[0];\n  const cx = xAxis.toPixels(0);\n  const cy = yAxis.toPixels(0);\n  const pxPerUnitX = xAxis.toPixels(1) - cx;\n  const pxPerUnitY = cy - yAxis.toPixels(1);\n  const toPx = (x, y) => [cx + x * pxPerUnitX, cy - y * pxPerUnitY];\n\n  // Highcharts' arc angles are 0 = 12 o'clock, clockwise — the same\n  // convention as toXY() — but renderer.arc() only draws true circles, and\n  // the plot area here isn't guaranteed square (the legend eats vertical\n  // space asymmetrically). So ring geometry is built as a polygon that\n  // samples toPx() every 3°, which stays exactly in lockstep with the\n  // scatter series (positioned through the same per-axis pixel conversion)\n  // even when x and y pixel-per-unit differ.\n  const arcPoints = (radius, fromDeg, toDeg) => {\n    const steps = Math.ceil((toDeg - fromDeg) / 3);\n    const pts = [];\n    for (let s = 0; s <= steps; s++) {\n      const deg = fromDeg + ((toDeg - fromDeg) * s) / steps;\n      pts.push(toPx(...toXY(radius, deg)));\n    }\n    return pts;\n  };\n  const ringStart = SECTOR_START_DEG;\n  const ringEnd = SECTOR_START_DEG + (360 - GAP_DEG);\n  const pathFromPoints = (pts, closed) => {\n    const cmds = pts.flatMap(([x, y], i) => [i === 0 ? \"M\" : \"L\", x, y]);\n    return closed ? [...cmds, \"Z\"] : cmds;\n  };\n\n  const group = r.g(\"radar-chrome\").add();\n  group.attr({ zIndex: 1 });\n\n  // Alternating ring bands (subtle fill for time-horizon separation) — only\n  // every other ring gets the elevated tone so the banding stays legible.\n  RING_BOUNDS.slice(0, -1).forEach((inner, i) => {\n    if (i % 2 !== 0) return;\n    const outer = RING_BOUNDS[i + 1];\n    const outline = [...arcPoints(outer, ringStart, ringEnd), ...arcPoints(inner, ringEnd, ringStart)];\n    r.path(pathFromPoints(outline, true))\n      .attr({ fill: t.elevatedBg })\n      .add(group);\n  });\n\n  // Ring boundary lines.\n  RING_BOUNDS.slice(1).forEach((radius) => {\n    r.path(pathFromPoints(arcPoints(radius, ringStart, ringEnd), false))\n      .attr({ stroke: t.grid, \"stroke-width\": 1.5, fill: \"none\" })\n      .add(group);\n  });\n\n  // Sector divider lines (radial spokes at every sector boundary).\n  for (let i = 0; i <= SECTORS.length; i++) {\n    const angleDeg = SECTOR_START_DEG + i * SECTOR_SPAN_DEG;\n    const [x, y] = toXY(MAX_R, angleDeg);\n    const [px, py] = toPx(x, y);\n    r.path([\"M\", cx, cy, \"L\", px, py])\n      .attr({ stroke: t.grid, \"stroke-width\": 1.5 })\n      .add(group);\n  }\n\n  // Sector labels along the outer edge, anchored to stay inside the canvas.\n  SECTORS.forEach((name, i) => {\n    const angleDeg = SECTOR_START_DEG + (i + 0.5) * SECTOR_SPAN_DEG;\n    const [x, y] = toXY(MAX_R + 0.05, angleDeg);\n    const [px, py] = toPx(x, y);\n    const align = x > 0.15 ? \"left\" : x < -0.15 ? \"right\" : \"center\";\n    r.text(name, px, py + 5)\n      .attr({ align, \"text-anchor\": align === \"left\" ? \"start\" : align === \"right\" ? \"end\" : \"middle\" })\n      .css({ color: t.ink, fontSize: \"15px\", fontWeight: \"600\" })\n      .add(group);\n  });\n\n  // Ring labels stacked along the vertical gap axis (angle = 0, straight up).\n  RING_BOUNDS.slice(1).forEach((_, i) => {\n    const ringMid = (RING_BOUNDS[i] + RING_BOUNDS[i + 1]) / 2;\n    const [, py] = toPx(0, ringMid);\n    r.text(RINGS[i], cx, py + 4)\n      .attr({ align: \"center\", \"text-anchor\": \"middle\" })\n      .css({ color: t.inkSoft, fontSize: \"13px\" })\n      .add(group);\n  });\n}\n"}