{"spec_id":"network-hierarchical","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// network-hierarchical: Hierarchical Network Graph with Tree Layout\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: a small org chart, 4 management levels, asymmetric branching ---\nconst orgTree = {\n  label: \"CEO\",\n  children: [\n    {\n      label: \"VP Engineering\",\n      children: [\n        {\n          label: \"Eng Mgr A\",\n          children: [\n            { label: \"Sr Engineer\" },\n            { label: \"Engineer\" },\n            { label: \"QA Engineer\" },\n          ],\n        },\n        { label: \"Eng Mgr B\", children: [{ label: \"Engineer\" }] },\n        {\n          label: \"Eng Mgr C\",\n          children: [{ label: \"Sr Engineer\" }, { label: \"DevOps Eng\" }],\n        },\n      ],\n    },\n    {\n      label: \"VP Sales\",\n      children: [\n        {\n          label: \"Sales Mgr A\",\n          children: [{ label: \"Acct Exec\" }, { label: \"Sales Rep\" }],\n        },\n        { label: \"Sales Mgr B\", children: [{ label: \"Sales Rep\" }] },\n      ],\n    },\n    {\n      label: \"VP Product\",\n      children: [\n        {\n          label: \"PM Core\",\n          children: [{ label: \"Designer\" }, { label: \"Analyst\" }],\n        },\n        { label: \"PM Growth\", children: [{ label: \"Designer\" }] },\n      ],\n    },\n  ],\n};\n\nconst NUM_LEVELS = 4;\nconst CATEGORY_LABELS = [\"Executive\", \"VP\", \"Manager\", \"Individual Contributor\"];\nconst NODE_RADII = [24, 19, 15, 12];\n\n// Post-order layout: leaves get sequential x slots, parents center over children.\nlet leafIndex = 0;\nconst assignPositions = (node, level) => {\n  node.level = level;\n  if (!node.children || node.children.length === 0) {\n    node.x = leafIndex;\n    leafIndex += 1;\n  } else {\n    node.children.forEach((child) => assignPositions(child, level + 1));\n    node.x =\n      node.children.reduce((sum, child) => sum + child.x, 0) / node.children.length;\n  }\n  node.y = NUM_LEVELS - 1 - level;\n};\nassignPositions(orgTree, 0);\n\nconst nodesByLevel = [[], [], [], []];\nconst edges = [];\nconst collect = (node, parent) => {\n  nodesByLevel[node.level].push(node);\n  if (parent) edges.push([parent, node]);\n  (node.children || []).forEach((child) => collect(child, node));\n};\ncollect(orgTree, null);\n\nconst leafCount = leafIndex;\n\n// --- Helpers -----------------------------------------------------------------\nconst hexToRgba = (hex, alpha) => {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Datasets ------------------------------------------------------------\n// Edges first (drawn bottom), node levels after (drawn on top).\nconst edgeDatasets = edges.map(([parent, child]) => ({\n  type: \"line\",\n  data: [\n    { x: parent.x, y: parent.y },\n    { x: child.x, y: child.y },\n  ],\n  showLine: true,\n  fill: false,\n  borderColor: hexToRgba(t.inkSoft, 0.35),\n  borderWidth: 1.75,\n  pointRadius: 0,\n  pointHoverRadius: 0,\n  tension: 0,\n  isEdge: true,\n  order: 2,\n}));\n\nconst nodeDatasets = nodesByLevel.map((nodes, level) => ({\n  type: \"scatter\",\n  label: CATEGORY_LABELS[level],\n  data: nodes.map((n) => ({ x: n.x, y: n.y, label: n.label })),\n  backgroundColor: t.palette[level],\n  borderColor: t.pageBg,\n  borderWidth: 2.5,\n  pointStyle: \"circle\",\n  pointRadius: NODE_RADII[level],\n  pointHoverRadius: NODE_RADII[level] + 2,\n  isNodeSet: true,\n  order: 1,\n}));\n\n// --- Title (scale fontsize for the 67-char baseline) ----------------------\nconst TITLE = \"network-hierarchical · javascript · chartjs · anyplot.ai\";\nconst TITLE_SIZE =\n  TITLE.length > 67 ? Math.max(15, Math.round(22 * (67 / TITLE.length))) : 22;\n\n// --- Node label plugin (draws role titles below each marker) -------------\nconst nodeLabelPlugin = {\n  id: \"nodeLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx } = chart;\n    chart.data.datasets.forEach((dataset, i) => {\n      if (!dataset.isNodeSet) return;\n      const meta = chart.getDatasetMeta(i);\n      meta.data.forEach((point, idx) => {\n        ctx.save();\n        ctx.fillStyle = t.ink;\n        ctx.font = \"600 15px sans-serif\";\n        ctx.textAlign = \"center\";\n        ctx.textBaseline = \"top\";\n        ctx.fillText(dataset.data[idx].label, point.x, point.y + dataset.pointRadius + 6);\n        ctx.restore();\n      });\n    });\n  },\n};\n\n// --- Chart -----------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets: [...edgeDatasets, ...nodeDatasets] },\n  plugins: [nodeLabelPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 10, right: 30, bottom: 10, left: 30 } },\n    plugins: {\n      title: {\n        display: true,\n        text: TITLE,\n        color: t.ink,\n        font: { size: TITLE_SIZE, weight: \"600\" },\n        padding: { bottom: 24 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          filter: (item, data) => !data.datasets[item.datasetIndex].isEdge,\n        },\n      },\n    },\n    scales: {\n      x: { display: false, min: -1, max: leafCount },\n      y: { display: false, min: -0.6, max: NUM_LEVELS - 1 + 0.6 },\n    },\n  },\n});\n"}