{"spec_id":"heatmap-adjacency","library":"muix","language":"javascript","code":"// anyplot.ai\n// heatmap-adjacency: Network Adjacency Matrix Heatmap\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// heatmap-adjacency: Network Adjacency Matrix Heatmap\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-05\n\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { ScatterChart } from \"@mui/x-charts/ScatterChart\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst tokens = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) — cross-team collaboration network ----\n// A fixed-seed LCG replaces the browser's non-reproducible Math.random().\nfunction lcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(1664525, s) + 1013904223) >>> 0;\n    return s / 4294967295;\n  };\n}\nconst random = lcg(42);\n\nconst teams = [\n  { name: \"Engineering\", members: [\"Ava\", \"Noah\", \"Mia\", \"Liam\", \"Zoe\"] },\n  { name: \"Design\", members: [\"Ivy\", \"Theo\", \"Nora\", \"Omar\", \"Luca\"] },\n  { name: \"Product\", members: [\"Maya\", \"Eli\", \"Ruby\", \"Finn\", \"Sara\"] },\n  { name: \"Marketing\", members: [\"Nina\", \"Cole\", \"Ana\", \"Drew\", \"Wes\"] },\n];\nconst names = teams.flatMap((team) => team.members);\nconst clusterSize = teams[0].members.length;\nconst nodeCount = names.length;\n\n// Edge weight = shared Slack threads/docs per month. Same-team pairs link\n// often and strongly; cross-team pairs link rarely and weakly — this is what\n// makes the community structure visible as darker diagonal blocks.\nconst weights = Array.from({ length: nodeCount }, () => new Array(nodeCount).fill(0));\nfor (let i = 0; i < nodeCount; i += 1) {\n  for (let j = i + 1; j < nodeCount; j += 1) {\n    const sameTeam = Math.floor(i / clusterSize) === Math.floor(j / clusterSize);\n    const linkRoll = random();\n    let weight = 0;\n    if (sameTeam && linkRoll < 0.85) {\n      weight = Math.round(35 + random() * 65);\n    } else if (!sameTeam && linkRoll < 0.22) {\n      weight = Math.round(5 + random() * 30);\n    }\n    weights[i][j] = weight;\n    weights[j][i] = weight;\n  }\n}\n\n// Full matrix — both triangles filled, since the underlying graph is\n// undirected (a diagonal stays 0: no self-collaboration edges).\nconst points = [];\nfor (let row = 0; row < nodeCount; row += 1) {\n  for (let col = 0; col < nodeCount; col += 1) {\n    points.push({ id: `${row}-${col}`, x: names[col], y: names[row], z: weights[row][col] });\n  }\n}\n\nconst edgeWeights = weights.flat().filter((w) => w > 0);\nconst minWeight = Math.min(...edgeWeights);\nconst maxWeight = Math.max(...edgeWeights);\n\n// Custom marker: filled square matrix cells instead of the default circles.\n// Absent edges (z === 0) render as the plain page background — visually\n// distinct from every real, colored edge — rather than the palest color step.\nfunction AdjacencyCell(props) {\n  const { series, xScale, yScale, colorGetter, color } = props;\n  const cellWidth = xScale.bandwidth();\n  const cellHeight = yScale.bandwidth();\n\n  return (\n    <g>\n      {series.data.map((point, i) => {\n        const x0 = xScale(point.x) ?? 0;\n        const y0 = yScale(point.y) ?? 0;\n        const fill = point.z > 0 ? (colorGetter ? colorGetter(i) : color) : tokens.pageBg;\n        const isTopLink = point.z === maxWeight;\n        return (\n          <rect\n            key={point.id}\n            x={x0}\n            y={y0}\n            width={cellWidth}\n            height={cellHeight}\n            fill={fill}\n            stroke={isTopLink ? tokens.ink : \"none\"}\n            strokeWidth={isTopLink ? 2 : 0}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// Ink-soft dividers at team boundaries (drawn at low opacity, distinct from\n// both the colored edges and the blank/absent-edge background) so the\n// block-diagonal cluster structure reads at a glance.\nfunction ClusterBoundaries() {\n  const xScale = useXScale(\"col\");\n  const yScale = useYScale(\"row\");\n  const drawingArea = useDrawingArea();\n  const marks = [];\n  for (let k = clusterSize; k < nodeCount; k += clusterSize) {\n    const bx = xScale(names[k]) ?? 0;\n    const by = yScale(names[k]) ?? 0;\n    marks.push(\n      <line\n        key={`v-${k}`}\n        x1={bx}\n        y1={drawingArea.top}\n        x2={bx}\n        y2={drawingArea.top + drawingArea.height}\n        stroke={tokens.inkSoft}\n        strokeOpacity={0.4}\n        strokeWidth={2}\n      />,\n      <line\n        key={`h-${k}`}\n        x1={drawingArea.left}\n        y1={by}\n        x2={drawingArea.left + drawingArea.width}\n        y2={by}\n        stroke={tokens.inkSoft}\n        strokeOpacity={0.4}\n        strokeWidth={2}\n      />,\n    );\n  }\n  return <g>{marks}</g>;\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const TITLE_HEIGHT = 76;\n  const MARGIN_TOP = 50;\n  const MARGIN_LEFT = 70;\n  const MARGIN_RIGHT = 130;\n  const MARGIN_BOTTOM = 40;\n\n  const LEGEND_EDGE_PADDING = 14;\n  const chartWidth = width - LEGEND_EDGE_PADDING;\n  const chartHeight = height - TITLE_HEIGHT;\n\n  return (\n    <Box sx={{ width, height, bgcolor: tokens.pageBg, display: \"flex\", flexDirection: \"column\" }}>\n      <Box sx={{ height: TITLE_HEIGHT, display: \"flex\", flexDirection: \"column\", justifyContent: \"center\", alignItems: \"center\" }}>\n        <Typography sx={{ color: tokens.ink, fontSize: 22, fontWeight: 500, lineHeight: 1.2, fontFamily: \"inherit\" }}>\n          heatmap-adjacency · javascript · muix · anyplot.ai\n        </Typography>\n        <Typography sx={{ color: tokens.inkSoft, fontSize: 13, lineHeight: 1.2, fontFamily: \"inherit\", pt: \"4px\" }}>\n          Monthly shared threads between teammates, grouped by team\n        </Typography>\n      </Box>\n      <Box sx={{ flex: 1, display: \"flex\", alignItems: \"flex-start\", justifyContent: \"flex-start\" }}>\n        <ScatterChart\n          width={chartWidth}\n          height={chartHeight}\n          skipAnimation\n          disableVoronoi\n          series={[\n            {\n              id: \"collaboration\",\n              type: \"scatter\",\n              data: points,\n              label: \"Collaboration weight\",\n              xAxisId: \"col\",\n              yAxisId: \"row\",\n              zAxisId: \"weight\",\n            },\n          ]}\n          xAxis={[\n            {\n              id: \"col\",\n              scaleType: \"band\",\n              data: names,\n              categoryGapRatio: 0.04,\n              tickLabelStyle: { fontSize: 13, fill: tokens.inkSoft },\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          yAxis={[\n            {\n              id: \"row\",\n              scaleType: \"band\",\n              data: names,\n              categoryGapRatio: 0.04,\n              tickLabelStyle: { fontSize: 13, fill: tokens.inkSoft },\n              disableTicks: true,\n              disableLine: true,\n            },\n          ]}\n          zAxis={[\n            {\n              id: \"weight\",\n              min: minWeight,\n              max: maxWeight,\n              colorMap: { type: \"continuous\", min: minWeight, max: maxWeight, color: [tokens.seq[0], tokens.seq[1]] },\n            },\n          ]}\n          topAxis=\"col\"\n          bottomAxis={null}\n          leftAxis=\"row\"\n          rightAxis={null}\n          margin={{ top: MARGIN_TOP, right: MARGIN_RIGHT, bottom: MARGIN_BOTTOM, left: MARGIN_LEFT }}\n          slots={{ scatter: AdjacencyCell }}\n          slotProps={{ legend: { hidden: true } }}\n        >\n          <ClusterBoundaries />\n          <ContinuousColorLegend\n            axisId=\"weight\"\n            axisDirection=\"z\"\n            position={{ horizontal: \"right\", vertical: \"middle\" }}\n            direction=\"column\"\n            length=\"55%\"\n            thickness={14}\n            minLabel={({ formattedValue }) => `${formattedValue} threads`}\n            maxLabel={({ formattedValue }) => `${formattedValue} threads`}\n            labelStyle={{ fontSize: 12, fill: tokens.inkSoft, fontFamily: \"inherit\" }}\n          />\n        </ScatterChart>\n      </Box>\n    </Box>\n  );\n}\n"}