{"spec_id":"network-directed","library":"muix","language":"javascript","code":"// anyplot.ai\n// network-directed: Directed Network Graph\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Module dependency graph (in-memory, deterministic) ---------------------\n// A layered \"build order\" layout: layer 0 (foundation) has no dependencies,\n// each later layer imports from earlier ones. x = layer index, y = position\n// within the layer (centered so layers with fewer modules stay balanced).\nconst LAYERS = [\n  { id: \"foundation\", label: \"Foundation\", color: t.palette[0] },\n  { id: \"core\", label: \"Core Services\", color: t.palette[1] },\n  { id: \"domain\", label: \"Domain Logic\", color: t.palette[2] },\n  { id: \"api\", label: \"API Layer\", color: t.palette[3] },\n  { id: \"ui\", label: \"UI & Apps\", color: t.palette[5] }, // skip palette[4] — reserved matte-red for the flagged cycle below\n];\n\nconst RAW_NODES = [\n  { id: \"utils\", label: \"utils\", layer: 0, group: \"foundation\" },\n  { id: \"config\", label: \"config\", layer: 0, group: \"foundation\" },\n  { id: \"logger\", label: \"logger\", layer: 1, group: \"core\" },\n  { id: \"auth\", label: \"auth\", layer: 1, group: \"core\" },\n  { id: \"cache\", label: \"cache\", layer: 1, group: \"core\" },\n  { id: \"orders\", label: \"orders\", layer: 2, group: \"domain\" },\n  { id: \"inventory\", label: \"inventory\", layer: 2, group: \"domain\" },\n  { id: \"billing\", label: \"billing\", layer: 2, group: \"domain\" },\n  { id: \"rest-api\", label: \"rest-api\", layer: 3, group: \"api\" },\n  { id: \"graphql-api\", label: \"graphql-api\", layer: 3, group: \"api\" },\n  { id: \"web-app\", label: \"web-app\", layer: 4, group: \"ui\" },\n  { id: \"mobile-app\", label: \"mobile-app\", layer: 4, group: \"ui\" },\n  { id: \"admin-panel\", label: \"admin-panel\", layer: 4, group: \"ui\" },\n];\n\n// Center each layer's nodes vertically around y = 0.\nconst layerCounts = new Map();\nRAW_NODES.forEach((n) => layerCounts.set(n.layer, (layerCounts.get(n.layer) ?? 0) + 1));\nconst layerSeen = new Map();\nconst NODES = RAW_NODES.map((n) => {\n  const seen = layerSeen.get(n.layer) ?? 0;\n  layerSeen.set(n.layer, seen + 1);\n  const count = layerCounts.get(n.layer);\n  return { ...n, x: n.layer, y: seen - (count - 1) / 2 };\n});\nconst NODE_BY_ID = new Map(NODES.map((n) => [n.id, n]));\n\n// Edges read \"source depends on / imports target\" — the arrow points from the\n// importer to the imported module (build order flows right to left). The\n// third tuple entry is the optional spec `weight` field (import call count),\n// which drives arrow/stroke thickness below.\nconst RAW_EDGES = [\n  [\"config\", \"utils\", 2],\n  [\"logger\", \"utils\", 3],\n  [\"auth\", \"utils\", 2],\n  [\"auth\", \"config\", 1],\n  [\"cache\", \"utils\", 2],\n  [\"orders\", \"auth\", 2],\n  [\"orders\", \"logger\", 3],\n  [\"orders\", \"cache\", 2],\n  [\"inventory\", \"cache\", 2],\n  [\"inventory\", \"logger\", 2],\n  [\"billing\", \"auth\", 2],\n  [\"billing\", \"orders\", 1],\n  [\"rest-api\", \"orders\", 3],\n  [\"rest-api\", \"inventory\", 2],\n  [\"rest-api\", \"billing\", 2],\n  [\"graphql-api\", \"orders\", 3],\n  [\"graphql-api\", \"billing\", 1],\n  [\"web-app\", \"rest-api\", 3],\n  [\"web-app\", \"graphql-api\", 2],\n  [\"mobile-app\", \"graphql-api\", 2],\n  [\"admin-panel\", \"rest-api\", 2],\n  [\"admin-panel\", \"inventory\", 1],\n  // \"config\" is Foundation yet reaches up into Core — combined with the\n  // existing auth -> config edge this closes a 2-cycle: a real circular\n  // dependency, flagged in the semantic error red instead of the group color.\n  [\"config\", \"auth\", 1],\n];\nconst EDGES = RAW_EDGES.map(([source, target, weight]) => ({\n  source,\n  target,\n  weight,\n  flagged: source === \"config\" && target === \"auth\",\n}));\nconst EDGE_KEYS = new Set(EDGES.map((e) => `${e.source}>${e.target}`));\nconst isBidirectional = (e) => EDGE_KEYS.has(`${e.target}>${e.source}`);\n\nconst NODE_RADIUS = 30;\nconst END_GAP = 6; // leaves room for the arrowhead before it touches the node\nconst FLAG_COLOR = \"#AE3030\"; // Imprint palette position 5 — semantic anchor for a real bug\n\nfunction edgePath(e, xScale, yScale) {\n  const s = NODE_BY_ID.get(e.source);\n  const d = NODE_BY_ID.get(e.target);\n  const x1 = xScale(s.x);\n  const y1 = yScale(s.y);\n  const x2 = xScale(d.x);\n  const y2 = yScale(d.y);\n\n  if (!isBidirectional(e)) {\n    const dx = x2 - x1;\n    const dy = y2 - y1;\n    const dist = Math.hypot(dx, dy) || 1;\n    const ex = x2 - (dx / dist) * (NODE_RADIUS + END_GAP);\n    const ey = y2 - (dy / dist) * (NODE_RADIUS + END_GAP);\n    return `M ${x1} ${y1} L ${ex} ${ey}`;\n  }\n\n  // Bow bidirectional pairs apart (see spec note on curved edges) so the two\n  // opposing arrows never sit exactly on top of one another. The normal must\n  // be computed from a direction-independent (canonical) chord — otherwise it\n  // flips sign along with the edge's own direction and the two bows cancel\n  // out instead of separating.\n  const mx = (x1 + x2) / 2;\n  const my = (y1 + y2) / 2;\n  const canonicalFrom = e.source < e.target ? s : d;\n  const canonicalTo = e.source < e.target ? d : s;\n  const cdx = xScale(canonicalTo.x) - xScale(canonicalFrom.x);\n  const cdy = yScale(canonicalTo.y) - yScale(canonicalFrom.y);\n  const cdist = Math.hypot(cdx, cdy) || 1;\n  const nx = -cdy / cdist;\n  const ny = cdx / cdist;\n  const sign = e.source < e.target ? 1 : -1;\n  const bow = Math.max(55, cdist * 0.4) * sign;\n  const cx = mx + nx * bow;\n  const cy = my + ny * bow;\n  // Tangent at the curve's end is exactly the control-point → end direction.\n  const tdx = x2 - cx;\n  const tdy = y2 - cy;\n  const tdist = Math.hypot(tdx, tdy) || 1;\n  const ex = x2 - (tdx / tdist) * (NODE_RADIUS + END_GAP);\n  const ey = y2 - (tdy / tdist) * (NODE_RADIUS + END_GAP);\n  return `M ${x1} ${y1} Q ${cx} ${cy} ${ex} ${ey}`;\n}\n\n// --- Edges layer (drawn first, so nodes sit visually on top) ----------------\nfunction GraphEdges() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  return (\n    <g>\n      <defs>\n        <marker\n          id=\"np-arrow\"\n          viewBox=\"0 0 12 10\"\n          refX=\"11\"\n          refY=\"5\"\n          markerWidth=\"13\"\n          markerHeight=\"11\"\n          markerUnits=\"userSpaceOnUse\"\n          orient=\"auto-start-reverse\"\n        >\n          <path d=\"M0,0 L12,5 L0,10 Z\" fill={t.inkSoft} />\n        </marker>\n        <marker\n          id=\"np-arrow-flag\"\n          viewBox=\"0 0 12 10\"\n          refX=\"11\"\n          refY=\"5\"\n          markerWidth=\"15\"\n          markerHeight=\"13\"\n          markerUnits=\"userSpaceOnUse\"\n          orient=\"auto-start-reverse\"\n        >\n          <path d=\"M0,0 L12,5 L0,10 Z\" fill={FLAG_COLOR} />\n        </marker>\n      </defs>\n      {EDGES.map((e, i) => (\n        <path\n          key={`${e.source}-${e.target}-${i}`}\n          d={edgePath(e, xScale, yScale)}\n          fill=\"none\"\n          stroke={e.flagged ? FLAG_COLOR : t.inkSoft}\n          strokeWidth={e.flagged ? 3.5 : 1.3 + e.weight * 0.5}\n          strokeOpacity={e.flagged ? 0.95 : 0.68}\n          markerEnd={e.flagged ? \"url(#np-arrow-flag)\" : \"url(#np-arrow)\"}\n        />\n      ))}\n    </g>\n  );\n}\n\n// --- Node markers — a custom ScatterPlot scatter slot so each circle gets a\n// page-background stroke ring for definition where edges cross underneath. --\nfunction NodeMarker({ series, xScale, yScale, color, colorGetter, markerSize }) {\n  return (\n    <g>\n      {series.data.map((d, i) => (\n        <circle\n          key={d.id ?? i}\n          cx={xScale(d.x)}\n          cy={yScale(d.y)}\n          r={markerSize}\n          fill={colorGetter ? colorGetter(i) : color}\n          stroke={t.pageBg}\n          strokeWidth={3}\n        />\n      ))}\n    </g>\n  );\n}\n\n// --- Node labels (drawn last, always legible over edges/markers) -----------\nfunction NodeLabels() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  return (\n    <g>\n      {NODES.map((n) => (\n        <text\n          key={n.id}\n          x={xScale(n.x)}\n          y={yScale(n.y) + NODE_RADIUS + 24}\n          textAnchor=\"middle\"\n          fontSize={15}\n          fontWeight={500}\n          fill={t.ink}\n          stroke={t.pageBg}\n          strokeWidth={4}\n          paintOrder=\"stroke\"\n        >\n          {n.label}\n        </text>\n      ))}\n    </g>\n  );\n}\n\n// --- Manual note explaining the flagged cycle, placed under the group legend\nfunction CircularDependencyNote() {\n  const drawingArea = useDrawingArea();\n  const x = drawingArea.left + drawingArea.width + 46;\n  const y = drawingArea.top + LAYERS.length * 30 + 58;\n  return (\n    <g>\n      <line x1={x} y1={y} x2={x + 32} y2={y} stroke={FLAG_COLOR} strokeWidth={3.5} markerEnd=\"url(#np-arrow-flag)\" />\n      <text x={x + 44} y={y + 5} fontSize={14} fill={t.ink}>\n        Circular dependency\n      </text>\n    </g>\n  );\n}\n\nconst TITLE = \"network-directed · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 70;\nconst MARGIN = { top: 30, right: 260, bottom: 30, left: 40 };\n\nconst series = LAYERS.map((g) => ({\n  type: \"scatter\",\n  id: g.id,\n  label: g.label,\n  color: g.color,\n  markerSize: NODE_RADIUS,\n  data: NODES.filter((n) => n.group === g.id).map((n) => ({ x: n.x, y: n.y, id: n.id })),\n}));\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <div\n      style={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: 22,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={window.ANYPLOT_SIZE.width}\n        height={window.ANYPLOT_SIZE.height - TITLE_HEIGHT}\n        margin={MARGIN}\n        skipAnimation\n        xAxis={[{ scaleType: \"linear\", min: -0.5, max: 4.5 }]}\n        yAxis={[{ scaleType: \"linear\", min: -1.5, max: 1.5 }]}\n        series={series}\n      >\n        <GraphEdges />\n        <ScatterPlot slots={{ scatter: NodeMarker }} />\n        <NodeLabels />\n        <ChartsLegend\n          position={{ vertical: \"top\", horizontal: \"right\" }}\n          direction=\"column\"\n          itemMarkWidth={18}\n          itemMarkHeight={18}\n          markGap={10}\n          itemGap={14}\n          padding={{ top: 10, right: 20 }}\n          labelStyle={{ fontSize: 15, fill: t.ink }}\n        />\n        <CircularDependencyNote />\n      </ChartContainer>\n    </div>\n  );\n}\n"}