{"spec_id":"heatmap-adjacency","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// heatmap-adjacency: Network Adjacency Matrix Heatmap\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG) ------------------------------------\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst groups = [\n  { name: \"Design\", size: 7 },\n  { name: \"Engineering\", size: 9 },\n  { name: \"Marketing\", size: 6 },\n];\nconst nodes = [];\ngroups.forEach((group, groupIndex) => {\n  for (let i = 0; i < group.size; i++) {\n    nodes.push({ label: `${group.name[0]}${i + 1}`, group: groupIndex });\n  }\n});\nconst nodeCount = nodes.length;\nconst labels = nodes.map((node) => node.label);\nconst groupBoundaries = groups.reduce((acc, group) => {\n  acc.push((acc.length ? acc[acc.length - 1] : 0) + group.size);\n  return acc;\n}, []);\ngroupBoundaries.pop(); // drop trailing boundary at the matrix edge\n\n// Symmetric weighted adjacency — collaboration strength between coworkers.\n// Same-group pairs connect more often and more strongly than cross-group pairs.\nconst adjacency = Array.from({ length: nodeCount }, () => new Array(nodeCount).fill(0));\nfor (let i = 0; i < nodeCount; i++) {\n  for (let j = i + 1; j < nodeCount; j++) {\n    const sameGroup = nodes[i].group === nodes[j].group;\n    const connectChance = sameGroup ? 0.85 : 0.22;\n    const weight = rand() < connectChance ? (sameGroup ? 0.5 : 0.1) + rand() * (sameGroup ? 0.5 : 0.25) : 0;\n    adjacency[i][j] = weight;\n    adjacency[j][i] = weight;\n  }\n}\nconst maxWeight = Math.max(...adjacency.flat());\n\nconst cells = [];\nfor (let row = 0; row < nodeCount; row++) {\n  for (let col = 0; col < nodeCount; col++) {\n    cells.push({ x: labels[col], y: labels[row], v: row === col ? 0 : adjacency[row][col] });\n  }\n}\n\n// --- Color mapping (Imprint imprint_seq — single-polarity weight) ----------\nfunction hexToRgb(hex) {\n  const clean = hex.replace(\"#\", \"\");\n  return [0, 2, 4].map((offset) => parseInt(clean.slice(offset, offset + 2), 16));\n}\nconst seqStart = hexToRgb(t.seq[0]);\nconst seqEnd = hexToRgb(t.seq[1]);\nfunction weightColor(value) {\n  if (value <= 0) return t.pageBg; // absent edge — distinct background, per spec\n  const ratio = value / maxWeight;\n  const [r, g, b] = seqStart.map((start, i) => Math.round(start + (seqEnd[i] - start) * ratio));\n  return `rgb(${r}, ${g}, ${b})`;\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Plugin: draw matrix cells as square fills (Chart.js core has no matrix\n// chart type; this draws directly on the canvas via a plugin, not a plugin\n// package) plus community-boundary dividers exposing block-diagonal structure.\nconst heatmapCells = {\n  id: \"heatmapCells\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const cellW = scales.x.width / nodeCount;\n    const cellH = scales.y.height / nodeCount;\n    const side = Math.min(cellW, cellH) - 2;\n\n    ctx.save();\n    cells.forEach((cell) => {\n      const cx = scales.x.getPixelForValue(cell.x);\n      const cy = scales.y.getPixelForValue(cell.y);\n      ctx.fillStyle = weightColor(cell.v);\n      ctx.fillRect(cx - side / 2, cy - side / 2, side, side);\n    });\n\n    const left = scales.x.getPixelForValue(labels[0]) - cellW / 2;\n    const right = scales.x.getPixelForValue(labels[nodeCount - 1]) + cellW / 2;\n    const top = scales.y.getPixelForValue(labels[0]) - cellH / 2;\n    const bottom = scales.y.getPixelForValue(labels[nodeCount - 1]) + cellH / 2;\n\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1.5;\n    ctx.strokeRect(left, top, right - left, bottom - top);\n\n    ctx.strokeStyle = t.ink;\n    ctx.globalAlpha = 0.35;\n    ctx.lineWidth = 2;\n    groupBoundaries.forEach((boundaryIndex) => {\n      const frac = boundaryIndex / nodeCount;\n      const bx = left + frac * (right - left);\n      const by = top + frac * (bottom - top);\n      ctx.beginPath();\n      ctx.moveTo(bx, top);\n      ctx.lineTo(bx, bottom);\n      ctx.stroke();\n      ctx.beginPath();\n      ctx.moveTo(left, by);\n      ctx.lineTo(right, by);\n      ctx.stroke();\n    });\n    ctx.restore();\n  },\n};\n\n// --- Plugin: colorbar legend for the weight scale ---------------------------\nconst colorbarPlugin = {\n  id: \"colorbarPlugin\",\n  afterDraw(chart) {\n    const { ctx, chartArea } = chart;\n    const barWidth = 26;\n    const barX = chartArea.right + 46;\n    const barTop = chartArea.top;\n    const barHeight = chartArea.height;\n\n    const gradient = ctx.createLinearGradient(0, barTop + barHeight, 0, barTop);\n    gradient.addColorStop(0, t.seq[0]);\n    gradient.addColorStop(1, t.seq[1]);\n\n    ctx.save();\n    ctx.fillStyle = gradient;\n    ctx.fillRect(barX, barTop, barWidth, barHeight);\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 1;\n    ctx.strokeRect(barX, barTop, barWidth, barHeight);\n\n    ctx.fillStyle = t.inkSoft;\n    ctx.font = \"13px sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"top\";\n    ctx.fillText(maxWeight.toFixed(2), barX + barWidth + 8, barTop - 2);\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(\"0.00\", barX + barWidth + 8, barTop + barHeight + 2);\n\n    ctx.save();\n    ctx.translate(barX + barWidth + 58, barTop + barHeight / 2);\n    ctx.rotate(-Math.PI / 2);\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.fillStyle = t.ink;\n    ctx.font = \"15px sans-serif\";\n    ctx.fillText(\"Connection weight\", 0, 0);\n    ctx.restore();\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        data: cells.map((cell) => ({ x: cell.x, y: cell.y })),\n        pointRadius: 0,\n        showLine: false,\n      },\n    ],\n  },\n  plugins: [heatmapCells, colorbarPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: {\n      padding: { right: 140, bottom: 6 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"heatmap-adjacency · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 4 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Groups: D = Design · E = Engineering · M = Marketing\",\n        color: t.inkSoft,\n        font: { size: 14 },\n        padding: { bottom: 16 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        type: \"category\",\n        labels,\n        position: \"top\",\n        offset: true,\n        grid: { display: false },\n        border: { display: false },\n        ticks: { color: t.inkSoft, font: { size: 11 }, autoSkip: false, maxRotation: 90, minRotation: 90 },\n        title: { display: true, text: \"Target node\", color: t.ink, font: { size: 14 } },\n      },\n      y: {\n        type: \"category\",\n        labels,\n        offset: true,\n        grid: { display: false },\n        border: { display: false },\n        ticks: { color: t.inkSoft, font: { size: 11 }, autoSkip: false },\n        title: { display: true, text: \"Source node\", color: t.ink, font: { size: 14 } },\n      },\n    },\n  },\n});\n"}