{"spec_id":"radar-innovation-timeline","library":"muix","language":"javascript","code":"// anyplot.ai\n// radar-innovation-timeline: Innovation Radar with Time-Horizon Rings\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-26\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Strategic technology planning: emerging tech mapped by adoption horizon\n// (ring, inner→outer) across four thematic sectors (angular wedge).\nconst RINGS = [\n  { key: \"now\", label: \"Now (0–6 mo)\" },\n  { key: \"near\", label: \"Near-Term (6–18 mo)\" },\n  { key: \"mid\", label: \"Mid-Term (18–36 mo)\" },\n  { key: \"future\", label: \"Future (3+ yr)\" },\n];\n\nconst SECTORS = [\n  { key: \"ai\", label: \"AI & ML\" },\n  { key: \"cloud\", label: \"Cloud Infra\" },\n  { key: \"security\", label: \"Security\" },\n  { key: \"sustain\", label: \"Green Tech\" },\n];\n\n// Uneven per-(ring, sector) density — real technology radars cluster rather\n// than filling a uniform grid: most cells hold one item, several hold two or\n// three. Every ring and sector still has at least one item.\nconst ITEMS = [\n  { name: \"LLM Fine-Tuning\", ring: 0, sector: 0 },\n  { name: \"AI Coding Assistants\", ring: 0, sector: 0 },\n  { name: \"Multimodal Agents\", ring: 1, sector: 0 },\n  { name: \"RAG Pipelines\", ring: 1, sector: 0 },\n  { name: \"On-Device Inference\", ring: 2, sector: 0 },\n  { name: \"Autonomous Coding Agents\", ring: 2, sector: 0 },\n  { name: \"Neuromorphic Chips\", ring: 3, sector: 0 },\n  { name: \"Serverless Functions\", ring: 0, sector: 1 },\n  { name: \"Edge Computing\", ring: 1, sector: 1 },\n  { name: \"Platform Engineering\", ring: 1, sector: 1 },\n  { name: \"WebAssembly Runtimes\", ring: 2, sector: 1 },\n  { name: \"Confidential Computing\", ring: 2, sector: 1 },\n  { name: \"Quantum-Safe Networking\", ring: 3, sector: 1 },\n  { name: \"Zero Trust Access\", ring: 0, sector: 2 },\n  { name: \"AI Threat Detection\", ring: 0, sector: 2 },\n  { name: \"Passwordless Auth\", ring: 1, sector: 2 },\n  { name: \"Passkeys Everywhere\", ring: 1, sector: 2 },\n  { name: \"Post-Quantum Crypto\", ring: 2, sector: 2 },\n  { name: \"Homomorphic Encryption\", ring: 3, sector: 2 },\n  { name: \"Carbon-Aware IT\", ring: 0, sector: 3 },\n  { name: \"Green Data Centers\", ring: 1, sector: 3 },\n  { name: \"Grid-Scale Battery Storage\", ring: 1, sector: 3 },\n  { name: \"Circular Hardware\", ring: 2, sector: 3 },\n  { name: \"Direct Air Capture\", ring: 3, sector: 3 },\n  { name: \"Fusion Energy Pilots\", ring: 3, sector: 3 },\n];\n\n// --- Geometry (half-circle: rings sweep 180°→0°, sectors divide the arc) ----\n// The chart's domain is set 1:1 with CSS px (chartWidth === xDomainLen,\n// chartHeight === yDomainLen, zero ChartContainer margin) so the semicircle\n// is sized directly against the canvas instead of being squeezed by a\n// height-driven aspect ratio that leaves the sides empty.\nconst R_MAX = 640;\nconst RING_BOUNDS = [0, 160, 320, 480, 640];\nconst SECTOR_SPAN = 180 / SECTORS.length;\nconst SECTOR_LABEL_R = R_MAX + 40;\n// Outward reach of each item's label, per ring — smallest on the outer ring\n// so it doesn't crowd the sector header just beyond R_MAX.\nconst LABEL_OFFSET = [28, 48, 44, 24];\n// The outermost ring sits closest to the sector headers; nudging its items\n// off the sector's exact center angle keeps single-occupant outer cells from\n// landing directly under the header text (same ray, different radius only).\nconst OUTER_RING_NUDGE = 12;\nconst X_HALF = SECTOR_LABEL_R + 100;\nconst Y_TOP = R_MAX + 24;\nconst Y_BOTTOM = -60;\n\n// Group items by (ring, sector) cell, then fan multiple occupants across the\n// sector's angular span and stagger their radius within the ring band — the\n// \"smart label placement / angular jitter / radial offset\" the spec's Notes\n// call for. A lone occupant (n=1) still lands dead-center at the ring's\n// midpoint radius, matching the simple case exactly.\nconst CELLS = new Map();\nITEMS.forEach((item) => {\n  const key = `${item.ring}-${item.sector}`;\n  if (!CELLS.has(key)) CELLS.set(key, []);\n  CELLS.get(key).push(item);\n});\n\nconst POINTS = [];\nCELLS.forEach((cell, key) => {\n  const [ringIdx, sectorIdx] = key.split(\"-\").map(Number);\n  const sectorStart = 180 - sectorIdx * SECTOR_SPAN;\n  const ringInner = RING_BOUNDS[ringIdx];\n  const ringOuter = RING_BOUNDS[ringIdx + 1];\n  const baseRadius = (ringInner + ringOuter) / 2;\n  const radialJitterStep = (ringOuter - ringInner) * 0.32;\n  const isOuterRing = ringIdx === RINGS.length - 1;\n  // Rings alternate which portion of the sector's angular span they favor,\n  // so two crowded cells sharing a sector but sitting in adjacent rings\n  // don't land on near-identical angles.\n  const [pad, span] = ringIdx % 2 === 0 ? [0.2, 0.6] : [0.3, 0.4];\n  // Within a sector, sin(angle) rises toward the sector nearest the top (90°)\n  // and falls away from it; staggering the radius in that same direction\n  // (instead of a fixed sign) makes the angular and radial spread compound\n  // into more vertical separation rather than canceling out.\n  const sign = sectorIdx < SECTORS.length / 2 ? 1 : -1;\n  const n = cell.length;\n  cell.forEach((item, i) => {\n    const frac = n === 1 ? 0.5 : pad + span * (i / (n - 1));\n    const angle = sectorStart - SECTOR_SPAN * frac - (isOuterRing ? OUTER_RING_NUDGE : 0);\n    const radius = baseRadius + sign * (i - (n - 1) / 2) * radialJitterStep;\n    POINTS.push({ ...item, angle, radius, labelOffset: LABEL_OFFSET[ringIdx] });\n  });\n});\n\n// --- Radar diagram (custom marks composed on the MUI X cartesian scales) ----\nfunction RadarDiagram() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  const toPx = (radius, angleDeg) => {\n    const rad = (angleDeg * Math.PI) / 180;\n    return [xScale(radius * Math.cos(rad)), yScale(radius * Math.sin(rad))];\n  };\n  const arcPoints = (radius, steps = 48) =>\n    Array.from({ length: steps + 1 }, (_, i) => toPx(radius, 180 - (180 * i) / steps));\n  const toPath = (pts) => pts.map(([x, y], i) => `${i === 0 ? \"M\" : \"L\"}${x},${y}`).join(\" \");\n\n  const dividerAngles = Array.from({ length: SECTORS.length + 1 }, (_, i) => 180 - i * SECTOR_SPAN);\n\n  return (\n    <g>\n      {/* Subtle alternating fill per ring — separates the time horizons */}\n      {RINGS.map((ring, i) => {\n        const rInner = RING_BOUNDS[i];\n        const rOuter = RING_BOUNDS[i + 1];\n        const outerArc = arcPoints(rOuter);\n        const innerArc = rInner === 0 ? [toPx(0, 90)] : arcPoints(rInner).reverse();\n        const d = `${toPath(outerArc)} L${innerArc.map(([x, y]) => `${x},${y}`).join(\" L\")} Z`;\n        return <path key={ring.key} d={d} fill={t.ink} opacity={i % 2 === 0 ? 0.035 : 0.07} stroke=\"none\" />;\n      })}\n\n      {/* Ring boundary lines (outer ring gets a bolder frame) */}\n      {RING_BOUNDS.slice(1).map((r, i) => (\n        <path\n          key={`ring-line-${r}`}\n          d={toPath(arcPoints(r))}\n          fill=\"none\"\n          stroke={i === RING_BOUNDS.length - 2 ? t.inkSoft : t.grid}\n          strokeWidth={i === RING_BOUNDS.length - 2 ? 2.5 : 1.5}\n        />\n      ))}\n\n      {/* Baseline diameter */}\n      <path d={toPath([toPx(R_MAX, 180), toPx(R_MAX, 0)])} stroke={t.inkSoft} strokeWidth={2.5} />\n\n      {/* Sector divider spokes */}\n      {dividerAngles.map((angle) => (\n        <path key={`div-${angle}`} d={toPath([toPx(0, angle), toPx(R_MAX, angle)])} stroke={t.grid} strokeWidth={1.5} />\n      ))}\n\n      {/* Ring names integrated at each ring's own boundary along the top\n          spine (a sector divider, so it never competes with an item label) —\n          closer to the rings than a separate row below the baseline. */}\n      {RING_BOUNDS.slice(1).map((r, i) => {\n        const [x, y] = toPx(r, 90);\n        return (\n          <text key={`ring-label-${RINGS[i].key}`} x={x + 10} y={y - 6} fontSize={13} fontWeight={600} fill={t.inkSoft} textAnchor=\"start\" dominantBaseline=\"middle\">\n            {RINGS[i].label}\n          </text>\n        );\n      })}\n\n      {/* Sector headers along the outer edge */}\n      {SECTORS.map((sector, i) => {\n        const sectorStart = 180 - i * SECTOR_SPAN;\n        const mid = sectorStart - SECTOR_SPAN / 2;\n        const [x, y] = toPx(SECTOR_LABEL_R, mid);\n        const cos = Math.cos((mid * Math.PI) / 180);\n        const anchor = cos > 0.25 ? \"start\" : cos < -0.25 ? \"end\" : \"middle\";\n        return (\n          <text key={`sector-label-${sector.key}`} x={x} y={y} fontSize={18} fontWeight={700} fill={t.ink} textAnchor={anchor} dominantBaseline=\"middle\">\n            {sector.label}\n          </text>\n        );\n      })}\n\n      {/* Sector color legend, a single compact row below the baseline */}\n      {SECTORS.map((sector, i) => {\n        const slot = (2 * X_HALF) / SECTORS.length;\n        const cx = xScale(-X_HALF + slot * (i + 0.5));\n        const cy = yScale(-32);\n        return (\n          <g key={`legend-${sector.key}`}>\n            <circle cx={cx - 55} cy={cy} r={9} fill={t.palette[i]} stroke={t.pageBg} strokeWidth={2} />\n            <text x={cx - 40} y={cy} fontSize={14} fill={t.ink} textAnchor=\"start\" dominantBaseline=\"middle\">\n              {sector.label}\n            </text>\n          </g>\n        );\n      })}\n\n      {/* Items — marker + outward radial label, colored by sector */}\n      {POINTS.map((p) => {\n        const [mx, my] = toPx(p.radius, p.angle);\n        const [lx, ly] = toPx(p.radius + p.labelOffset, p.angle);\n        const cos = Math.cos((p.angle * Math.PI) / 180);\n        const anchor = cos > 0.2 ? \"start\" : cos < -0.2 ? \"end\" : \"middle\";\n        return (\n          <g key={p.name}>\n            <circle cx={mx} cy={my} r={9} fill={t.palette[p.sector]} stroke={t.pageBg} strokeWidth={2.2} />\n            <text x={lx} y={ly} fontSize={13} fill={t.ink} textAnchor={anchor} dominantBaseline=\"middle\">\n              {p.name}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const size = window.ANYPLOT_SIZE;\n  const titleHeight = 64;\n  // Domain bounds are set 1:1 with CSS px (see geometry comment above), so\n  // the ChartContainer's own size *is* the drawing area, filling the\n  // available width directly instead of being derived from it.\n  const chartWidth = 2 * X_HALF;\n  const chartHeight = Y_TOP - Y_BOTTOM;\n\n  return (\n    <Box sx={{ width: size.width, height: size.height, display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\" }}>\n      <Box sx={{ height: titleHeight, display: \"flex\", alignItems: \"center\" }}>\n        <Typography sx={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          radar-innovation-timeline · javascript · muix · anyplot.ai\n        </Typography>\n      </Box>\n      <ChartContainer\n        width={chartWidth}\n        height={chartHeight}\n        series={[]}\n        xAxis={[{ min: -X_HALF, max: X_HALF, scaleType: \"linear\", domainLimit: \"strict\" }]}\n        yAxis={[{ min: Y_BOTTOM, max: Y_TOP, scaleType: \"linear\", domainLimit: \"strict\" }]}\n        margin={{ top: 0, bottom: 0, left: 0, right: 0 }}\n        skipAnimation\n      >\n        <RadarDiagram />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}